Icharle | Don't forget your first thoughts - Java https://icharle.com/category/Java zh-CN Sat, 27 Jul 2019 03:03:00 +0800 Sat, 27 Jul 2019 03:03:00 +0800 Mybatis分页插件PageHelper踩坑过程 https://icharle.com/mybatispagehelper.html https://icharle.com/mybatispagehelper.html Sat, 27 Jul 2019 03:03:00 +0800 Icharle 前言

最近在整一个基于Dubbo RPC调用统一用户模块,想法是落地实践学生团队。学校学生用户这么多涉及到查询的自然需要涉及到分页查询。之前在猫眼实习时候,有看过内部使用的美团开源的美团点评集团统一使用的MySQL数据库访问层的中间件,里面还集成有数据库读写分离、分库分表、监控等等,小团队目前还没必要用到这些功能,一切从简,先把整个骨架子搭起来,因此选择PageHelper这个插件。

Question && Solve

  • 问题一

     Failed to parse config resource: class path resource [config/mybatis/mybatis-configuration.xml]; 
     nested exception is org.apache.ibatis.builder.BuilderException: Error parsing SQL Mapper Configuration. 
     Cause: java.lang.ClassCastException: com.github.pagehelper.PageHelper cannot be cast to org.apache.ibatis.plugin.Interceptor
    

    原因是:5.x版本后拦截器不在是PageHelper,已经更改为PageInterceptor

    <plugin interceptor="com.github.pagehelper.PageInterceptor"></plugin>
    
  • 问题二

    nested exception is org.apache.ibatis.builder.BuilderException: Error parsing SQL Mapper Configuration. 
    Cause: com.github.pagehelper.PageException:java.lang.ClassNotFoundException: mysql
    

    原因是:配置分页插件数据库不在是dialect,而是helperDialect,并且支持自动检测当前的数据库链接,因此不用配置也是ok的啦。

]]>
1 https://icharle.com/mybatispagehelper.html#comments https://icharle.com/feed/mybatispagehelper.html
Java并发编程之J.U.C中Atomic原子包总结 https://icharle.com/javajucatomic.html https://icharle.com/javajucatomic.html Sun, 21 Jul 2019 08:54:00 +0800 Icharle 类型

基本类型

  • AtomicInteger:整型原子类
  • AtomicLong:长整型原子类
  • AtomicBoolean:布尔型原子类
# 示例
import java.util.concurrent.atomic.AtomicInteger;

class AtomicTest {

    public static void main(String[] args) {
        int temp;
        AtomicInteger atomicInteger = new AtomicInteger(0);
        temp = atomicInteger.getAndIncrement(); // 相当于i++
        System.out.println("Old Value:" + temp + " New Value:" + atomicInteger); // Old Value:0 New Value:1
        temp = atomicInteger.incrementAndGet(); // 相当于++i
        System.out.println("Old Value:" + temp + " New Value:" + atomicInteger); // Old Value:2 New Value:2
    }
}

数组类型

  • AtomicIntegerArray:整型数组原子类
  • AtomicLongArray:长整型数组原子类
  • AtomicBoolean:布尔型原子类
# 示例
import java.util.concurrent.atomic.AtomicIntegerArray;

class AtomicTest {

    public static void main(String[] args) {
        int[] num = {1, 2, 3, 4, 5};
        AtomicIntegerArray array = new AtomicIntegerArray(num);
        array.getAndSet(1,5);
        System.out.println(array); // [1, 5, 3, 4, 5]
        array.compareAndSet(1,5,7);
        System.out.println(array); // [1, 7, 3, 4, 5]
    }
}

引用类型

  • AtomicReference:引用类型原子类
  • AtomicStampedRerence:原子更新带有版本号的引用类型(可用于解决CAS中ABA问题)
  • AtomicMarkableReference:原子更新带有标记位的引用类型
# 示例
import lombok.AllArgsConstructor;
import lombok.Data;

import java.util.concurrent.atomic.AtomicReference;

class AtomicTest {

    public static void main(String[] args) {
        AtomicReference<User> atomicReference = new AtomicReference<>();
        User user = new User("icharle", 18);
        atomicReference.set(user);
        System.out.println("name:" + atomicReference.get().getName() + " age:" + atomicReference.get().getAge()); // name:icharle age:18
        User newUser = new User("mlui", 19);
        atomicReference.compareAndSet(user, newUser);
        System.out.println("name:" + atomicReference.get().getName() + " age:" + atomicReference.get().getAge()); // name:mlui age:19
    }
}

对象属性修改类型

  • AtomicIntegerFieldUpdater:原子更新整型字段的更新
  • AtomicLongFieldUpdater:原子更新长整型字段的更新
# 示例
import lombok.AllArgsConstructor;
import lombok.Data;

import java.util.concurrent.atomic.AtomicIntegerFieldUpdater;

class AtomicTest {

    public static void main(String[] args) {
        AtomicIntegerFieldUpdater fieldUpdater = AtomicIntegerFieldUpdater.newUpdater(User.class, "age");

        User user = new User("icharle", 18);
        System.out.println(fieldUpdater.incrementAndGet(user));  // 19
    }
}

@AllArgsConstructor
@Data
class User {
    private String name;
    public volatile int age;
}

AtomicXXX

这里就用AtomicInteger做例子,当然还有原子类均java.util.concurrent.atomic包下,这里用几个经典的例子分析说明。
201930

AtomicInteger count = new AtomicInteger(0);
count.getAndIncrement();

# 上面一段相当于count++
# 但是在多线程并发情况下count++是不安全的,因为++操作并非是原子操作。

AtomicInteger原理分析

# AtomicInteger类
/**
 * Atomically increments by one the current value.
 *
 * @return the previous value
 */
 public final int getAndIncrement() {
    return unsafe.getAndAddInt(this, valueOffset, 1);
 }
 
 # unsafe类
 public final int getAndAddInt(Object var1, long var2, int var4) {
        int var5;
        do {
            var5 = this.getIntVolatile(var1, var2);
        } while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));

        return var5;
    }

AtomicXXX在多线程并发情况下,是通过CAS方式来保证。其中unsafe类就是CAS的核心。

  • getAndAddInt(Object var1, long var2, int var4),var1为当前对象、var2为当前对象内存值、var4为需要更新的值。
  • var5 = this.getIntVolatile(var1, var2);,取到当前对象的内存值。
  • while循环直到var2 = var5 时候才更新值。

CAS缺点: 存在一个do···while语句,如果一直更新不成功,则会出现自旋操作。再则容易出现ABA问题。

AtomicStampReference解决CAS中ABA问题(原子引用、版本号机制)

class VolatileDemo {

    public static void main(String[] args) {
        AtomicStampedReference<Integer> reference = new AtomicStampedReference<>(10, 1);

        new Thread(() -> {
            int stamp = reference.getStamp();
            System.out.println("T1拿到的第一次的版本号:" + stamp);
            // 先暂停1秒,等T2线程拿到相同的初始版本号
            try {
                TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            reference.compareAndSet(10, 101, reference.getStamp(), reference.getStamp() + 1);
            System.out.println("T1线程第一次操作后的版本号为:"  + reference.getStamp());
            reference.compareAndSet(101, 10, reference.getStamp(), reference.getStamp() + 1);
            System.out.println("T1线程第二次操作后的版本号为:"  + reference.getStamp());
        }, "T1").start();

        new Thread(() -> {
            int stamp = reference.getStamp();
            System.out.println("T2拿到的第一次的版本号:" + stamp);
            // 先暂停3秒,等T1线程有充分的时候做一次ABA操作
            try {
                TimeUnit.SECONDS.sleep(3);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            boolean b = reference.compareAndSet(10, 2019, stamp, stamp + 1);
            System.out.println("当前内存中的最新值为:" + reference.getReference());
            System.out.println("T2线程在T1线程执行完ABA问题后在执行的结果为:" + b);
        }, "T2").start();
    }
}

参考文章死磕Java——CAS

AtomicLong与LongAdder区别

在AtomicLong中add()方法是通过CAS不断自旋方式更新值,当在多线程同时竞争激烈,更新值得过程不断自旋尝试CAS会造成CPU很大开销。而LongAdder则以“空间换时间”的思想,LongAdder存在一个Cell数组,因此它会针对Cell数组中的值进行CAS操作。(有点类似于jdk1.7中ConcurrentHashMap分段锁原理)

通过源码分析

先从add()方法入手,add方法中存在一个Cell数组,是Striped64的一个内部类,官方的解释为AtomicLong的填充变体仅支持原始访问和CAS

public void add(long x) {
    Cell[] as; long b, v; int m; Cell a;
    // ①判断cells是否被还没初始化 ②尝试对值直接进行cas操作
    if ((as = cells) != null || !casBase(b = base, b + x)) {
        boolean uncontended = true;
        //①cell[]数组是否初始化
        //②cell[]数组虽然已经初始化但是数组长度是否为0
        //③该线程对应的cell是否为null
        //④尝试对该线程对应的cell单元进行cas更新失败
        if (as == null || (m = as.length - 1) < 0 ||
            (a = as[getProbe() & m]) == null ||
            !(uncontended = a.cas(v = a.value, v + x)))
            longAccumulate(x, null, uncontended);
    }
}

if条件中有一个条件符合则进入到longAccumulate方法进行更新值。

final void longAccumulate(long x, LongBinaryOperator fn,
                              boolean wasUncontended) {
        int h;
        // getProbe()作用是根据当前线程hash出一个int值
        // 如果getProbe()为0表示还未初始化,则进行强制初始化
        if ((h = getProbe()) == 0) {
            ThreadLocalRandom.current(); // force initialization
            h = getProbe();
            wasUncontended = true;
        }
        boolean collide = false;                // True if last slot nonempty
        for (; ; ) {
            Cell[] as;
            Cell a;
            int n;
            long v;
            // cell[]数组已初始化但是数组并且长度是否为大于0
            if ((as = cells) != null && (n = as.length) > 0) {
                // 该线程对应的cell是否为null
                if ((a = as[(n - 1) & h]) == null) {
                    // 如果busy锁没有被占用,则进行新建一个cell
                    if (cellsBusy == 0) {       // Try to attach new Cell
                        Cell r = new Cell(x);   // Optimistically create
                        // 检测busy是否为0,并且尝试锁busy
                        if (cellsBusy == 0 && casCellsBusy()) {
                            boolean created = false;
                            try {               // Recheck under lock
                                Cell[] rs;
                                int m, j;
                                // 再次确认当前线程hash对应的cell是否为null,并且将新建cell赋值
                                if ((rs = cells) != null &&
                                        (m = rs.length) > 0 &&
                                        rs[j = (m - 1) & h] == null) {
                                    rs[j] = r;
                                    created = true;
                                }
                            } finally {
                                // 释放锁
                                cellsBusy = 0;
                            }
                            if (created)
                                break;
                                
                            continue;           // Slot is now non-empty
                        }
                    }
                    collide = false;
                }
                //  针对“尝试对该线程对应的cell单元进行cas更新失败”置为true后交给循环重试
                else if (!wasUncontended)       // CAS already known to fail
                    wasUncontended = true;      // Continue after rehash
                else if (a.cas(v = a.value, ((fn == null) ? v + x :
                        fn.applyAsLong(v, x))))
                    break;
                else if (n >= NCPU || cells != as)
                    collide = false;            // At max size or stale
                else if (!collide)
                    collide = true;
                else if (cellsBusy == 0 && casCellsBusy()) {
                    try {
                        if (cells == as) {      // Expand table unless stale
                        // 尝试扩大cell 并将前n个copy进新cell数组
                            Cell[] rs = new Cell[n << 1];
                            for (int i = 0; i < n; ++i)
                                rs[i] = as[i];
                            cells = rs;
                        }
                    } finally {
                        cellsBusy = 0;
                    }
                    collide = false;
                    continue;                   // Retry with expanded table
                }
                // 线程竞争过于激烈 重新hash当前线程中HashCode值分配槽  
                h = advanceProbe(h);
            }
            // cells还未初始化情况并且能够获得锁情况 
            else if (cellsBusy == 0 && cells == as && casCellsBusy()) {
                boolean init = false;
                try {                           // Initialize table
                    if (cells == as) {
                        // 初始化Cell为2的数组
                        Cell[] rs = new Cell[2];
                        // 计算hashcode值
                        rs[h & 1] = new Cell(x);
                        cells = rs;
                        init = true;
                    }
                } finally {
                    // 释放锁
                    cellsBusy = 0;
                }
                if (init)
                    break;
            }
            // 重试一次casBase对值直接累加
            else if (casBase(v = base, ((fn == null) ? v + x :
                    fn.applyAsLong(v, x))))
                break;                          // Fall back on using base
        }
    }

sum方法中,将base值以及遍历Cell数组累加和。

public long sum() {
        Cell[] as = cells; Cell a;
        long sum = base;
        if (as != null) {
            for (int i = 0; i < as.length; ++i) {
                if ((a = as[i]) != null)
                    sum += a.value;
            }
        }
        return sum;
    }

LongAdder对比AtomicLong,使用Cell数组去承接并发cas以提升性能,但LongAdder在统计的时候如果有并发更新,可能会导致统计的数据有误差。

参考文章从 LongAdder 中窥见并发组件的设计思路

]]>
0 https://icharle.com/javajucatomic.html#comments https://icharle.com/feed/javajucatomic.html
Guava类库中Lists.transform踩坑 https://icharle.com/guavaliststransform.html https://icharle.com/guavaliststransform.html Wed, 03 Jul 2019 10:58:00 +0800 Icharle 前言

最近在Java踩坑的道路上越踩越多坑,随手写篇笔记做个记录。

问题分析

Guava类库很强大,但也有一点坑,最近在做项目的正好遇到。使用Lists.transform转换数据,遇到坑①修改源对象数据会直接影响转换后数据 ②直接修改转换后数据修改无效

代码例子:

import com.google.common.base.Function;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Lists;

import java.util.List;

//源对象
class People {

    private String name;

    private int age;

    private String desc;

    public People() {
    }

    public People(String name, int age, String desc) {
        this.name = name;
        this.age = age;
        this.desc = desc;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }

    public String getDesc() {
        return desc;
    }

    public void setDesc(String desc) {
        this.desc = desc;
    }

    @Override
    public String toString() {
        return "People{" +
                "name='" + name + '\'' +
                ", age=" + age +
                ", desc='" + desc + '\'' +
                '}';
    }
}

// 转换目标对象
class PeopleVo {
    private String name;

    private int age;

    private String desc;

    public PeopleVo() {
    }

    public PeopleVo(String name, int age, String desc) {
        this.name = name;
        this.age = age;
        this.desc = desc;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }

    public String getDesc() {
        return desc;
    }

    public void setDesc(String desc) {
        this.desc = desc;
    }

    @Override
    public String toString() {
        return "People{" +
                "name='" + name + '\'' +
                ", age=" + age +
                ", desc='" + desc + '\'' +
                '}';
    }
}

public class GuavaExample {

    public static PeopleVo peopleToVo(People people) {
        PeopleVo peopleVo = new PeopleVo();
        peopleVo.setName(people.getName());
        peopleVo.setAge(people.getAge());
        peopleVo.setDesc(people.getDesc() + " To Vo");
        return peopleVo;
    }

    public static void main(String[] args) {
        List<People> peopleList = ImmutableList.of(
                new People("icharle", 18, "good"),
                new People("pad", 19, "better"),
                new People("soarteam", 20, "best")
        );

        List<PeopleVo> peopleVos = Lists.transform(peopleList, new Function<People, PeopleVo>() {
            @Override
            public PeopleVo apply(People people) {
                return peopleToVo(people);
            }
        });

        //1.对源对象数据进行修改会影响到转换对象的数据
        System.out.println("-----------------源对象数据-----------------");
        System.out.println(peopleVos.toString());

        System.out.println("----------------对源对象修改-----------------");
        for(People people : peopleList){
            people.setAge(people.getAge()+1);
        }
        System.out.println(peopleVos.toString());


        //2.对转换后的对象进行修改数据,修改无效
        System.out.println("-----------------修改转换对象数据-----------------");
        for(PeopleVo peopleVo : peopleVos){
            peopleVo.setAge(peopleVo.getAge()+1);
        }
        System.out.println(peopleVos.toString());
    }

}

输出数据:

# 输出片段一
-----------------源对象数据-----------------
[People{name='icharle', age=18, desc='good To Vo'}, People{name='pad', age=19, desc='better To Vo'}, People{name='soarteam', age=20, desc='best To Vo'}]
# 输出片段二
----------------对源对象修改-----------------
[People{name='icharle', age=19, desc='good To Vo'}, People{name='pad', age=20, desc='better To Vo'}, People{name='soarteam', age=21, desc='best To Vo'}]
# 输出片段三
-----------------修改转换对象数据-----------------
[People{name='icharle', age=19, desc='good To Vo'}, People{name='pad', age=20, desc='better To Vo'}, People{name='soarteam', age=21, desc='best To Vo'}]

情况说明:

  • 输出判断一跟片段二对比,当对源数据peopleList的age加一操作情况下,输出转换对象后的数据是更变的。
  • 输出片段二跟片段三对比,当对转换后数据peopleVos的age加一的操作情况下,输出转换对象的数据是不改变。

查阅谷歌相关文章,发现转换后的数据是一个视图模型,因此再修改原对象情况下,必然会影响视图内容;而直接修改视图,对它的任何更改都是无效的。

参考文章:关于Guava类库中Lists.transform的问题解析

]]>
0 https://icharle.com/guavaliststransform.html#comments https://icharle.com/feed/guavaliststransform.html
HashMap用可变对象作为key踩坑 https://icharle.com/hashmapkebianobj.html https://icharle.com/hashmapkebianobj.html Mon, 01 Jul 2019 02:31:00 +0800 Icharle 前言

在Java道路上越踩越多坑,最近被问到一个知识点,当对象作为HashMap一个key时,再未重写equalshashcode方法时候,get方法返回的值为null

分析

在下面一段代码中,未重写equals以及hashCode方法情况下,输出结果为null

import java.util.HashMap;
import java.util.Map;

class People {
    private String name;

    public People() {
    }

    public People(String name) {
        this.name = name;
    }

    public static void main(String[] args) {
        Map<People, Integer> map = new HashMap<People, Integer>();
        map.put(new People("icharle"), 18);
        
        System.out.println(map.get(new People("icharle")));
    }
}

# 输出结果为null

改造后代码:

import java.util.HashMap;
import java.util.Map;

class People {
    private String name;

    public People() {
    }

    public People(String name) {
        this.name = name;
    }

    @Override
    public boolean equals(Object obj) {
        if (this == obj) {
            return true;
        }
        if (obj == null || getClass() != obj.getClass()) {
            return false;
        }

        People people = (People) obj;
        if (name != null ? !name.equals(people.name) : people.name != null) {
            return false;
        }
        return true;
    }

    @Override
    public int hashCode() {
        return name != null ? name.hashCode() : 0;
    }

    public static void main(String[] args) {
        Map<People, Integer> map = new HashMap<People, Integer>();
        map.put(new People("icharle"), 18);

        System.out.println(map.get(new People("icharle")));
    }
}
# 输出结果为18

原因分析

HashMap在查找某一个key时,先是用hashCode函数根据该key的地址计算,再用equals函数根据对象的地址进行比较。

  • 在代码片段一中,map.put(new People("icharle"), 18);以及System.out.println(map.get(new People("icharle")));中默认是两个对象(也就是说两个对象的地址不一样,自然hashcode函数得到的值是不一样(因为两个的对象的地址不同,自然在equals时候更不可能相等,最终得到的结果为null
  • 在代码片段二中,重载hashCode()函数的作用是:对于同一个key,得到相同的hash值,重载equals()函数的作用是:向HashMap表明当前对象和key上所保存的对象是相等的。因此该情况下最终结果为18
]]>
0 https://icharle.com/hashmapkebianobj.html#comments https://icharle.com/feed/hashmapkebianobj.html
Java线程调度 https://icharle.com/javathread.html https://icharle.com/javathread.html Thu, 20 Jun 2019 03:31:00 +0800 Icharle 线程调度是指系统为线程分配处理器使用权的过程,方式主要有:协同式线程调度抢占式线程调度

协同式线程调度

线程的执行时间由线程本身来控制,线程把自己的工作执行完成之后,要主动通知系统切换到另外一个线程上。

  • 好处:实现简单。且由于线程要把自己的事情干完后才会进行线程切换,切换操作对线程自己是可知。不存在线程同步问题。
  • 缺点:线程执行时间不可控制,甚至如果一个线程编写有问题,一直不告知系统进行线程切换,则程序就会一直阻塞在那里。

抢占式线程调度

每个线程将由系统来分配执行时间,线程的切换不由线程本身来决定(Java中,Thread.yield()可以让出执行时间,但是要获取执行时间的话,线程本身是没有办法的)。

  • 好处:线程的执行时间是系统可控的,也不会有一个线程导致整个进程阻塞的问题,Java使用的线程调度方式就是抢占式调度。

线程状态转换

201927

  • 新建(New):创建后尚未启动的线程处于这种状态
  • 运行(Runable):Runable包括了操作系统线程状态中的Running和Ready,也就是处于此状态的线程可能正在执行,也可能正在等待CPU为它分配执行时间。
  • 无限期等待(Waiting):处于这中状态的线程不会被分配CPU执行时间,它们要等待被其他线程显示地唤醒。
    • 没有设置Timeout参数的Object.wait()方法。
    • 没有设置Timeout参数的Thread.join()方法。
    • LockSupport.park()方法。
  • 限期等待(Timed Waiting):处于这种状态的线程也不会被分配CPU执行时间,不过无须等待被其他线程显示地唤醒,在一定时间之后由系统自动唤醒。
    • Thread.sleep()方法
    • 设置Timeout参数的Object.wait()方法
    • 设置了Timeout参数的Thread.join()方法
    • LockSupport.parkNanos()方法
    • LockSupport.parkUntil()方法
  • 阻塞(Blocked):线程被阻塞了。在程序等待进入同步区域的时候,线程将进入这种状态。
    • “阻塞状态”在等待着获取到一个排他锁,这个时间将在另外一个线程放弃这个锁的时候发生;
    • “等待状态”则是在等待一段时间,或者唤醒动作的发生。
  • 结束(Terminated):已终止线程的线程状态,线程已经结束执行。

Java中的线程安全

  • 不可变
    • 不可变(Immutable)的对象一定是线程安全的,无论是对象的方法实现还是方法的调用者,都不需要再采取任何的线程安全保障措施。
    • 只要一个不可变的对象被正确地构建出来(没有发生this引用逃逸的情况),则外部的可见状态永远也不会改变,永远也不会看到他在多个线程之中处于不一致的状态。
    • 不可变的安全性是最简单、最纯粹的
  • 绝对线程安全
  • 相对线程安全
  • 线程兼容
  • 线程独立

线程安全的实现方法

  • 不可变
  • 互斥同步
    • synchronized实现
    • 重入锁(ReentrantLock)实现,相比synchronized,其增加了一些高级功能如等待可中断、可实现公平锁、锁可以绑定多个条件
  • 非阻塞同步
    • CAS
    • AtomicInteger
  • 无同步方案
    • 可重入代码(Reentrant Code)
    • 线程本地存储(Thread Local Storage)

锁优化

  • 自旋锁以及自适应自旋

    自旋锁的思想是让一个线程在请求一个共享数据的锁时执行忙循环一段时间,如果在这段时间内能获得锁,就可以避免阻塞状态。
    自旋虽然避免了线程切换的开销,但它是要占用处理器时间,因此如果锁被占用的时间很短,自旋等待的效果就会非常好反之如果所被占用的时间很长,那自旋的线程只会白白消耗处理器资源而不会做任何有用的工作,反而带来性能上的浪费
    自适应意味着自旋的时间不在固定,而是由前一次在同一个锁上的自旋时间及锁的拥有者的状态来决定。

  • 锁消除

    指虚拟机即时编译器在运行时,对一些代码上要求同步,但是被检测不可能存在共享数据竞争的锁进行消除。
    主要的判断依据来源于逃逸分析的数据支持,如果判断在一段代码中,堆上的所有数据都不会逃逸出去从而被其他线程访问到,那就可以把它们当做栈上数据对待,认为它们是线程私有,同步加锁自然就无须进行。

  • 锁粗化

    大部分情况下,总是推荐将同步块的作用范围限制得尽量小(只在共享数据的实际作用域中才进行同步,这样是为了使得需要同步的操作数量尽可能变小,存在锁竞争,则让等待锁的线程能尽快拿到锁),但是如果一系列的连续操作都对同一个对象反复加锁和解锁,甚至加锁操作是出现在循环体中,那即使没有线程竞争,频繁地进行互斥同步操作也会导致不必要的性能损耗。

  • 轻量级锁

    在没有多线程竞争的前提下,减少传统的重量级锁使用操作系统互斥量产生的性能消耗。

  • 偏向锁

    目的是消除数据在无竞争情况下的同步原语,进一步提高程序的运行性能。偏向锁就是在无竞争的情况下把整个同步都消除掉,连CAS操作都不做。

]]>
0 https://icharle.com/javathread.html#comments https://icharle.com/feed/javathread.html
Java线程池 https://icharle.com/javaexecutor.html https://icharle.com/javaexecutor.html Sun, 09 Jun 2019 02:41:00 +0800 Icharle Java里面线程池的顶级接口是Executor,但是严格意义上讲Executor并不是一个线程池,而只是一个执行线程的工具。真正的线程池接口是ExecutorService。
201926

ThreadPoolExecutor

public ThreadPoolExecutor(int corePoolSize,
                              int maximumPoolSize,
                              long keepAliveTime,
                              TimeUnit unit,
                              BlockingQueue<Runnable> workQueue) {
        this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue,
             Executors.defaultThreadFactory(), defaultHandler);
    }

相关参数

  • corePoolSize:线程池中线程的数量
  • maximumPoolSize:线程池中最大的线程数量
  • keepAliveTime:当线程池数量超过corePoolSize值时,多余的空闲线程等待新任务的最长时间
  • unit:keepAliveTime单位
  • workQueue:执行前用于保持任务的队列
  • threadFactory:线程工厂,用于创建线程
  • handler:拒绝策略

handler(拒绝策略)分类

作用:当线程池中线程已经耗尽且等待队列也已经排满,则对于新任务采取合理的拒绝处理。

  • AbortPolicy:该策略直接抛出异常(默认拒绝策略)
  • CallerRunsPolicy:该策略线程直接调用运行该任务的execute本身。
  • DiscardOldestPolicy:该策略直接丢弃最老的一个请求任务。
  • DiscardPolicy:该策略直接丢弃无法处理的任务,不做任何处理(与AbortPolicy类似,但不抛出异常)

线程池类型

  • newCachedThreadPool:创建一个可缓存的线程池。线程池大小依赖于JVM能够创建的最大线程大小。调用 execute 将重用以前构造的线程(如果线程可用)。如果现有线程没有可用的,则创建一个新线程并添加到池中。终止并从缓存中移除那些已有60秒钟未被使用的线程。
  • newSingleThreadExecutor:创建一个单线程的线程池,当该唯一线程因为异常退出会重新创建一个新线程。能够按照任务提交的顺序有序执行。
  • newFixedThreadPool:创建一个固定大小的线程池,并以无界队列方式来运行这些线程。
  • newScheduledThreadPool:创建一个大小无限,它可安排在给定延迟后运行命令或者定期地执行

可能在日常开发中我们习惯性的创建一个线程池方法:

ExecutorService pool = Executors.newCachedThreadPool();

但是在阿里巴巴开发规范守约中提示:
线程池不允许使用Executors去创建,而是通过ThreadPoolExecutor的方式, 这样的处理方式让写的同学更加明确线程池的运行规则,规避资源耗尽的风险。
说明: Executors各个方法的弊端:

  • newFixedThreadPooL和newSingleThreadExecutor:主要问题是堆积的请求处理队列可能会耗费非常大的内存,甚至OOM。
  • newCachedThreadPooL和newScheduledThreadPool:主要问题是线程数最大数是Integer.MAX_VALUE, 可能会创建数量非常多的线程,甚至OOM。

任务队列

  • SynchronousQueue:不存储数据、可用于传递数据。每一个put操作必须等待一个take操作,否则不能继续添加元素。
  • LinkedBlockingQueue:基于链表的阻塞队列。能够高效的处理并发数据,对于生产者和消费者端分别采用独立的锁来控制数据同步,以此提高整个队列的并发性能。
  • ArrayBlockingQueue:用数组实现的有界阻塞队列。默认情况下不保证访问者公平的访问队列。
]]>
0 https://icharle.com/javaexecutor.html#comments https://icharle.com/feed/javaexecutor.html
Java内存模型 https://icharle.com/javammmodel.html https://icharle.com/javammmodel.html Thu, 25 Apr 2019 10:06:00 +0800 Icharle

Java虚拟机规范中试图定义一种Java内存模型(JMM)来屏蔽掉各种硬件和操作系统的内存访问差异,以实现让Java程序在各种平台下都能达到一致的内存访问效果。

主内存与工作内存

  • Java内存模型规定了所有的变量都存储在主内存(Main Memory)中。
  • 每条线程还有自己的工作内存(Working Memory),线程的工作内存中保存了被该线程使用到的变量的主内存副本拷贝,线程都变量的所有操作(读取、赋值等)都必须在工作内存中进行,而不能直接读写主内存中变量。
  • 不同的线程之间也无法直接访问对方工作内存中的变量,线程间变量值得传递均需要通过主内存来完成,线程、主内存、工作内存三者的交互关系如下。

201929

内存间交互操作

201928

  • read(读取):作用于主内存的变量,它把一个变量的值从驻村传输到线程的工作内存中,以便随后的load动作使用。
  • load(载入):作用于工作内存的变量,它把read操作从主内存中得到的变量值放入工作内存的变量副本中。
  • use(使用):作用于工作内存的变量,它把工作内存中一个变量的值传递给执行引擎,每当虚拟机遇到一个需要使用到变量的值得字节码指令时将会执行这个操作。
  • assign(赋值):作用于工作内存的变量,它把一个从执行引擎接收到的值赋给工作内存的变量,每当虚拟机遇到一个给变量赋值的字节码指令时执行这个操作。
  • store(存储):作用于工作内存的变量,它把工作内存中一个变量的值传送到主内存中,以便随后的write操作使用。
  • write(写入):作用于主内存的变量,它把store操作从工作内存中得到的变量的值放入主内存的变量中。
  • lock(锁定):作用于主内存的变量,它把一个变量表识为一条线程独占的状态。
  • unlock(解锁):作用于主内存的变量,它把一个处于锁定状态的变量释放出来,释放后的变量才可以被其他线程锁定。

内存模型的特征

  • 原子性(Atomicity):由Java内存模型来直接保证的原子性变量操作包括read、load、assign、use、store、write。对于64位的数据类型(long和double),在模型中特别定义了一条相对宽松规定:允许虚拟机将没有被volatile修饰的64位数据的读写操作划分为两次32位的操作来进行,即允许虚拟机实现选择可以不保证64位数据类型的load、store、read、write这4个操作的原子性,这点就是所谓的long和doble的非原子性协定。
  • 可见性(Visibility):可见性是指当一个线程修改了共享变量的值,其他线程能够立即得知这个修改。Java内存模型是通过在变量修改后将新值同步回主内存,在变量读取全从主内存刷新变量值这种依赖主内存作为传递媒介的方式来实现可见性。普通变量与volatile变量的区别是,volatile的特殊规则保证了新值能立即同步到主内存以及每次使用前立即从主内存刷新。volatile保证了多线程操作是变量的可见性,而普通变量则不能保证这一点。
  • 有序性(Ordering):Java程序中天然的有序性总结为:如果在本地线程内观察,所有的操作都是有序的;如果在一个线程中观察另一个线程,所有的操作都是无序的。前半句是指“线程内表现为串行的语义”,后半句是指“指令重排序”现象和“工作内存与主内存同步延迟”现象。采用volatilesynchronized两个关键字来保证线程之间操作的有序性。volatile关键字:本身就包含了禁止指令重排序的语义,synchronize关键字:则是由“一个变量在同一个时刻只允许一条线程对其进行lock操作”,该条规则决定了持有同一个锁的两个同步块只能串行地进入。

volatile变量的理解

由于volatile变量只能保证可见性不保证原子性,在不符合以下两条规则的运算场景中,仍然需要通过加锁(使用synchronized或者J.U.C包中的原子类)来保证原子性

  • 运算结果并不依赖变量的当前值,或者能够确保只有单一的线程修改变量的值。
  • 变量不需要与其他的状态变量共同参与不变约束。

内存屏障

为了保证内存可见性,Java编译器在生成指令序列的适当位置会插入内存屏障指令来禁止特定类型的处理器重排序。

类型共分为以下四种:

  • LoadLoad屏障:适用于(Load1、LoadLoad、Load2)指令;在Load2及后续读取操作要读取的数据被访问前,保证Load1需要读取的数据都读取完毕。
  • StoreStore屏障:适用于(Store1、StoreStore、Store2)指令;在Store2及所有后续存储指令写入前,保证Store1数据对其他处理器可见。
  • LoadStore屏障:适用于(Load1、LoadStore、Store2)指令;在Store2及后续写入操作被刷出前,保证Load1要读取的数据被读取完。
  • StoreLoad屏障:适用于(Store1、StoreLoad、Load2)指令;在Load2及后续所有读取操作执行前,保证Store1的写入对所有处理器可见。

happens-before规则(先行发生原则)

先行发生是Java内存模型中定义的两项操作之间的偏序关系,如果说操作A先行发生于操作B,其实就是说在发生操作B之前,操作A产生的影响能被操作B观察到,“影响”包括修改了内存中共享变量的值、发送了消息、调用了方法等。

“天然”先行发生原则:

  • 程序次序规则:在一个线程内,按照程序代码顺序,书写在前面的操作先行发生于书写在后面的操作。(准确地说,应该是控制流顺序而不是程序代码顺序,因为要考虑分支、循环等结构)
  • 管程锁定规则:一个unlock操作先行发生于后面对同一个锁的lock操作。
  • volatile变量规则:对一个volatile变量的写操作先行发生于后面对这个变量的都操作。
  • 线程启动规则:Thread对象的start()方法先行发生于此线程的每一个动作。
  • 线程终止规则:线程中的所有操作都先行发生与对此线程的终止检测。可以通过Thread.join()方法结束、Thread.isAlive()的返回值等手段检测到线程已经终止执行。
  • 线程中断规则:对线程interrupt()方法的调用先行发生于被中断线程的代码检测到中断事件的发生,可以通过Thread.interrupted()方法检测到是否有中断发生。
  • 对象终结规则:一个对象的初始化完成(构造函数执行结束)先行发生于它的finalize()方法的开始。
  • 传递性:如果操作A先行发生于操作B,操作B先行发生于操作C,那就可以得到操作A先行发生于操作C的结论。
]]>
0 https://icharle.com/javammmodel.html#comments https://icharle.com/feed/javammmodel.html
JVM内存分配与回收策略 https://icharle.com/jvmncuncelei.html https://icharle.com/jvmncuncelei.html Fri, 19 Apr 2019 03:50:00 +0800 Icharle
  • 新生代GC(Minor GC):指发生在新生代的垃圾收集动作,因为Java对象大多都具备朝生夕灭的特性,所以Minor GC非常频繁,一般回收速度也比较快。
  • 老年代(Major GC/Full GC):指发生在老年代的GC,出现了Major GC,经常会伴随至少一次的Minor GC(但非绝对的,在Parallel Scavenge收集器的收集策略里就有直接进行Major GC的策略选择过程)。Major GC的速度一般会比Minior GC慢10倍以上。
  • 内存分配策略

    JVM虚拟机提供了-XX:+PrintGCDetails这个收集器日志参数,告诉虚拟机在发生垃圾收集行为时打印内存回收日志,并且在进程退出的时候输出当前的内存各区域分配情况。

    • 对象优先在Eden分配

      • 在大多数情况,对象在新生代Eden区中分配,当Eden区分配没有足够空间进行分配时,虚拟机将发起一次Minor GC
    • 大对象直接进入老年代

      • 大对象是指,需要大量连续内存空间的Java对象,典型的大对象就是那种很长很长的字符串以及数组
      • 经常出现大对象容易导致内存还有不少空间就提前触发垃圾收集以获取足够的连续空间来存放大对象。
      • JVM虚拟机提供了一个-XX:PretenureSizeThreshold参数,令大于这个设置值的对象直接在老年代分配。注意:PretenureSizeThreshold参数只对Serial和ParNew两个收集器有效。
    • 长期存活的对象将进入老年代

      • JVM虚拟机采用分代收集的思想来管理内存,则内存回收也需要识别何为新生代,何为老年代,因此JVM定义了一对象年龄(Age)计数器。对象在Survior区中每“熬过”一次Minor GC,年龄就增加1岁,默认为15岁即可晋升老年代。
      • 对象晋升老年代阈值,可以通过-XX:MaxTenuringThreshold设置。
    • 动态对象年龄判定

      • JVM虚拟机并不是永远地要求对象的年龄必须达到MaxTenuringThreshold才能晋升老年代,如果在Survivor空间中相同年龄所有对象大小的总和大于Survivor空间的一半,年龄大于或等于该年龄的对象就可以直接进入老年代,无须等到MaxTenuringthreshold中要求的年龄。
    • 空间分配担保

      • 在发生Minor GC之前,虚拟机会先检查老年代最大可用的连续空间是否大于新生代所有对象总空间
      • 如果这个条件成立,那么Minor GC可以确保是安全的。如果不成立,则虚拟机会查看HandlePromotionFailure设置值是否允许担保失败
      • 如果允许,则会继续检查老年代最大可用的连续空间是否大于历次晋升到老年代对象的平均大小,如果大于,将尝试着进行一次Minor GC,尽管这次Minor GC是有风险的;如果小于,或者HandlePromotionFailure设置不允许冒险,则改为进行一次Full GC
    ]]>
    0 https://icharle.com/jvmncuncelei.html#comments https://icharle.com/feed/jvmncuncelei.html
    JVM垃圾收集器 https://icharle.com/jvmlajishoujiqi.html https://icharle.com/jvmlajishoujiqi.html Thu, 18 Apr 2019 21:51:00 +0800 Icharle 201919
    7种作用于不同分代的收集器,如果两个收集器之间存在连线,就说明可以搭配使用。

    Serial收集器

    201920

    • JVM虚拟机运行在Client模式下的默认新生代收集器。
    • 是一个单线程收集器,单线程即说明它只会使用一个CPU或者一条收集线程去完成垃圾收集工作,更重要的是在它进行垃圾收集时,必须暂停其他所有的工作线程,直到它收集结束。
    • 简单而高效(与其他收集器的单线程比),对于限定单个CPU的环境来说,Serial收集器由于没有线程交互的开销,因此获得最高的单线程收集效率
    • 在Client场景下,分配给虚拟机管理的内存一般来说不会很大,收集几十兆甚至一两百兆的新生代停顿时间完全可以控制在几十毫秒最多一百多毫秒以内。

    ParNew收集器

    201921

    • 是Serial收集器的多线程版本
    • 运行在Server模式下的虚拟机首选的新生代收集器
    • 除Serial收集器外,目前只有它能与CMS收集器配合工作
    • 由于存在线程交互的开销,ParNew收集器在单CPU的环境中绝对不会有比Serial收集器更好的效果。随着CPU数量增加,默认开启的收集器线程数与CPU的数量相同,可以使用-XX:ParallelGCThreads参数来限制垃圾收集的线程数。

    Parallel Scavenge收集器

    201922

    • 多线程新生代收集器
    • 基于复制算法实现
    • 其它收集器的关注点是尽可能地缩短垃圾收集时用户线程的停顿时间,而Parallel Scavenge收集器的目标则是达到一个可控制的吞吐量。吞吐量是指:CPU用于运行代码的时间与CPU总消耗时间的比值,即吞吐量=运行用户代码时间/(运行用户代码时间+垃圾收集时间)

    Serial Old收集器

    201920

    • 是Serial收集器的老年代版本,是一个单线程收集器
    • 采用“标记-整理”算法
    • Client模式下的虚拟机使用
    • 在Server模式下,则它主要还有两大用途:一种用途是在JDK1.5以及之前版本中与Parallel Scavenge收集器搭配使用;另外一种用途是作为CMS收集器的后备预案,在并发收集发生Concurrent Mode Failure时使用。

    Parallel Old收集器

    201922

    • 是Parallel Scavenge收集器的老年代版本,是一个多线程收集器
    • 采用“标记-整理”算法
    • 新生代Parallel Scavenge收集器由于无法与CMS收集器配合工作,只能与Serial Old收集器配合工作,但该收集器在服务端性能差,使用了Parallel Scavenge收集器也未必能在整体应用上获得吞吐量最大化的效果,由于单线程的老年代收集无法充分利用服务器多CPU的处理能力,在老年代很大而且硬件比较高级的环境还不一定有ParNew+CMS的组合
    • Parallel Old收集器的出现,在注重吞吐量以及CPU资源敏感的场合,优先考虑Parallel Scacenge + Parallel Old收集器。

    CMS收集器

    201923

    • 一种以获取最短回收停顿时间为目标的收集器
    • 采用“标记-清除”算法
    • 收集过程分为4步:
      • 初始标记(CMS initial mark):仅仅只是标记一下GC Roots能直接关联到的对象,速度很快,需要停顿
      • 并发标记(CMS concurrent mark):进行RootsTracing过程,不需要停顿,耗时长
      • 重新标记(CMS remark):为了修正并发标记期间因用户重新继续运作而导致标记产生的变动的哪一部分对象的标记记录,需要停顿
      • 并发清除(CMS concurrent sweep):不需要停顿

      由于整个过程中耗时最长的并发标记和并发清除过程收集器线程可以与用户线程一起工作,不需要停顿。

    • 存在的缺点:
      • CMS收集器对CPU资源非常敏感,它虽然不会导致用户线程停顿,但会因为占用一部分线程(CPU资源)而导致应用程序变慢,总吞吐量降低
      • 无法处理浮动垃圾,可能出现Concurrent Mode Failure失败而导致另一次Full GC的产生。浮动垃圾是指由于CMS并发清理阶段用户线程还在运行着,伴随着程序运行自然会有新的垃圾不断产生,这一部分垃圾出现在标记过程之后,CMS无法在打次收集中处理掉他们,只好留待下一次GC时再清理掉。CMS运行期间预留的内存无法满足程序需要,就会出现一次Concurrent Mode Failure失败,则将启动后备预案:临时请用Serial Old收集器重新进行老年代的垃圾收集,造成停顿时间加长,性能下降。
      • 基于“标记-清除”算法,会有大量的空间碎片产生,往往会出现老年代还有很大空间剩余,但无法找到足够大的连续空间来分配当前对象,不得不提前触发一次Full GC。

    G1收集器

    201924

    • 面向服务端应用的垃圾收集器
    • 具备特点:
      • 并行与并发:G1能充分利用多CPU、多核环境下的硬件优势,使用多个CPU(CPU或者CPU核心)来缩短停顿时间,部分收集器原需要停顿执行GC动作,G1收集器可以通过并发的方式让Java程序继续执行。
      • 分代收集:与其它收集器一样,分代概念在G1中依然得到保留。可以不需要其它收集器配合就能独立管理整个GC堆。
      • 空间整合:从整体来看是基于”标记-整理“算法实现的收集器,从局部(两个Region之间)上来看是基于”复制“算法实现,但无论如何这两种算法都意味着G1运作期间不会产生内存空间碎片。
      • 可预测的停顿:降低停顿时间是G1和CMS共同的关注点,但G1除了追求低停顿外,还建立可预测的停顿时间模型,能让使用者明确指定在一个长度为M毫秒的时间判断内,消耗在垃圾收集上的时间不得超过N毫秒。
    • G1收集器,它将整个Java堆划分为多个大小相等的独立区域(Region),虽然还保留有新生代和老年代的概念,但新生代和老年代不在物理隔离,它们都是一部分Region的结合。
    • G1收集器之所以能建立可预测的停顿时间模型,是因为它可以有计划地避免在整个Java堆中进行全区域的垃圾收集。G1跟踪各个Region里面的垃圾堆积的价值大小(回收所获得的空间大小以及回收所需要的经验值),在后台维护一个优先列表,每次根据允许的收集时间,优先回收价值最大的Region。
    • 每个Region都有一个与之对应的Remembered Set,用来检查Reference引用的对象是否处于不同的Region之中,从而保证不对全堆扫描也不会有遗漏。
    • 不维护Remembered Set,将分为下面4个步骤
      • 初始标记(Initial Marking):仅仅只是标记一下GC Roots能直接关联到的对象,并修改TEMS(Next Top at Mark Start)的值,让下一阶段用户程序并发允许时,能够正确可用Region中创建新对象,该阶段需要停顿,但耗时很短
      • 并发标记(Concurrent Marking):从GC Root开始对堆中对象进行可达性分析,找出存活的对象,该阶段比较耗时,但可与用户程序并发执行
      • 最终标记(Final Marking):为了修正在并发标记期间因用户程序继续运作而导致标记产生变动的那一部分标记记录,虚拟机将这端时间对象变化记录在线程Reembered Set Logs里面,并把Remembered Set Logs的数据合并到Remembered Set中,该阶段需要停顿线程,但是可并行执行
      • 筛选回收(Live Data Counting Evacuation):首先对各个Region的回收价值和成本进行排序,根据用户所期望的GC停顿时间来制定回收计划。此阶段其实也可以做到与用户程序一起并发执行,但是因为只回收一部分 Region,时间是用户可控制的,而且停顿用户线程将大幅度提高收集效率。
    ]]>
    0 https://icharle.com/jvmlajishoujiqi.html#comments https://icharle.com/feed/jvmlajishoujiqi.html
    Java容器总结笔记 https://icharle.com/javarongqi.html https://icharle.com/javarongqi.html Wed, 17 Apr 2019 08:12:00 +0800 Icharle Collection

    201917

    Set

    • TreeSet:
      • 底层基于红黑树实现
      • 支持有序性操作,查找,删除,添加等操作都是基于红黑树
      • 不允许插入为空
      • 线程不安全
      • 查找时间复杂度为O(logN)
    • HashSet:
      • 底层基于哈希表实现
      • 不支持有序性操作,并且失去元素插入顺序信息
      • 允许插入为空,但是最多一个
      • 线程不安全
      • 查找时间复杂度:O(1)
    • LinkedHashSet:
      • 继承自HashSet,同时使用双向链表维护元素的插入顺序

    List

    • ArrayList

      • 基于动态数组实现
      • 适合于顺序添加、随机访问(RandomAccess 接口标识该类支持快速随机访问)的场景
      • 允许插入为空,允许插入重复数据
      • 线程不安全

      数组的默认大小为10;添加元素时候,如果容量不足,需要使用 grow() 方法进行扩容,新容量是旧容量的1.5倍,扩容操作还需要将旧数组中的元素copy到新数组中,该过程比较耗费性能。
      添加一个元素,同样需要copy一次,因此该过程是比较耗费性能的。
      删除中间某个元素时候,后面元素需要整体向前移动,因此该过程也是耗费性能,而对于删除最后一个元素,则可以直接设为null,让gc垃圾回收机制去回收。

    • LinkedList

      • 基于双向链表实现
      • 允许插入为空,允许插入重复数据
      • 线程不安全
    • Vector

      • 原理与ArrayList相同
      • 允许插入为空,允许插入重复数据
      • 线程安全(采用synchronized同步实现)

      Vector与ArrayList类似,扩容是原大小的2倍,采用synchronized同步实现,因此开销就比ArrayList大。
      可以使用Collections.synchronizedList();得到一个线程安全的ArrayList。也可以使用concurrent并发包下的CopyOnWriteArrayList类。

    Queue

    • LinkedList
      • 基于双向链表,可以用于实现双向队列
    • PriorityQueue
      • 基于堆结构实现,可以用它来实现优先队列。

    Map

    201918

    • Hashtable

      • 基于哈希表实现
      • key、value均不能为null,且key不能重复,value允许重复
      • 失去元素插入顺序信息
      • 线程安全(采用synchronized同步实现)
    • HashMap

      • 基于哈希表实现
      • key、value允许为null,重复性:key重复会覆盖,value允许重复
      • 失去元素插入顺序信息
      • 线程不安全

      capacity:默认table容量为16,扩容时候保证为2的n次方
      loadFactor:装载因子,默认是0.75
      Java8之前:桶存储的是用链表。而Java8之后:链表长度大于8。时会将链表转换为红黑树。

    • LinkedHashMap

      • 基于双向链表实现
      • key、value都允许为空,重复性:key重复会覆盖 value允许重复
      • 线程不安全
    • TreeMap

      • 基于红黑树实现
      • key不能为null,value允许为空,重复性:key重复会覆盖,value允许重复
      • 根据key值进行排序
      • 线程不安全
    ]]>
    0 https://icharle.com/javarongqi.html#comments https://icharle.com/feed/javarongqi.html