JMM定义了一套在多线程读写共享数据时(成员变量,数组)时,对数据的 可见性、原子性和有序性 的规则和保障。
1 java内存模型
1.1 原子性
Java对静态变量的自增或者自减(i++,i--)不是原子操作。
i++的字节码指令为:(i为静态变量,局部变量的话不一样)
getstatic i //获取静态变量i的值
iconst_1 //准备常量1
iadd //自增
putstatic i //将修改后的值存入静态变量i
i--的字节码指令为:
getstatic i //获取静态变量i的值
iconst_1 //准备常量1
isub //自减
putstatic i //将修改后的值存入静态变量i
java的内存模型如下,完成静态变量的自增或者自减,需要在主存和线程内存中进行数据交换。由于多个线程按照时间片轮流使用cpu,会导致切换的时候值为脏值。
1.2 可见性
public class Demo4_2 {
static boolean run = true;
public static void main(String[] args) throws InterruptedException {
new Thread(() -> {
while (run) {}
}, "t").start();
TimeUnit.SECONDS.sleep(1);
run = false;
}
}
休眠1秒后run置为false后并没有让线程停止,这是由于:
- 初始状态,t线程刚开始从主内存读取了run的值加载到工作内存。
- 因为t线程要频繁从主存中读取run的值,JIT编译器会将run的值缓存至自己工作内存中的高速缓存中,减少对主存中run的访问,提高效率。
- 1秒之后,main线程修改了run值,并同步至主存,而t是从自己的工作内存中的告诉缓存中读取run,结果永远是旧值。
- 解决方式:
- 可以通过添加volatile(保证可见性,不保证原子性,禁止指令重排)解决。
- 在while中加入System.out.println();也能保证结束,这是由于输出语句底层采用了synchronized关键字,保证了原子性与可见性,强制了t线程从主存中读取。
1.3 有序性
主要是因为存在 指令重排,同样可以通过volatile来禁止指令重排。
2 CAS与原子类
2.1 CAS
CAS即 Compare and Swap,体现乐观锁的思想。
获取共享变量时,为了保证变量的可见性,需要使用volatile修饰。结合CAS和volatile可以实现无锁并发,适用于竞争不激烈、多核CPU的场景下。
- 因为没有使用synchronized,所以线程不会陷入阻塞,这是效率提升的因素之一
- 但如果竞争激烈,重试必然频繁发生,反而效率会受影响
CAS底层依赖于一个Unsafe类来直接调用操作系统底层的CAS指令。
2.2 乐观锁与悲观锁
java中的乐观锁其实就是CAS,悲观锁就是synchronized。
- CAS集于乐观锁的思想:最乐观的估计,不怕别的线程来修改共享变量,就算改了也没有关系,我吃亏点在重试。
- synchronized是基于悲观锁的思想:最悲观的估计,得放着其他线程来修改共享变量,我上了锁你们都别想改,我改完了释放锁你们才能改。
2.3 原子操作类
juc(java.until.concurrent)中提供了原子操作类,可以提供线程安全的操作,例如:AtomicInteger、AtomicBoolean等,它们底层就是采用CAS技术+volatile来实现的。
但是CAS会存在ABA问题。
3 synchronized
java HotSpot虚拟机中,每个对下你个都有对象头(包括class指针和Mark Word)。Mark Word平时存储该对象的 哈希码、分代年龄,当加锁时,这些信息就根据情况被替换为 标记位、线程锁记录指针、重量级锁指针、线程ID等内容。
3.1 轻量级锁
多个线程交替执行,不存在竞争,那么可以使用轻量级锁进行优化。
3.2 锁膨胀
如果在尝试加轻量级锁的过程中,CAS操作无法成功,这时一种情况就是有其他线程为此对象加上了轻量级锁(有竞争),这时需要进行锁膨胀,将轻量级锁变为重量级锁。
3.3 重量锁
重量级锁竞争的时候,还可以使用自旋来进行优化,如果当前线程自旋成功(即这时候持锁线程已经退出了同步块,释放了锁),这时当前线程就可以避免阻塞。
在Java6之后自旋锁是自适应的,比如对象刚刚的一次自旋操作成功过,那么认为这次自旋成功的可能性会高,就多自旋几次;反之,就少自旋甚至不自旋,总之,比较智能。
- 自旋会占用CPU时间,单核CPU自旋就是浪费时间,多核CPU自旋才能发挥优势。
- 好比等红灯时汽车是不是熄火,不熄火相当于自旋(等待时间短了划算),熄火了相当于阻塞(等待时间长了划算)。
- Java7后不能控制是否开启自旋功能。
3.4 偏向锁
轻量级锁在没有竞争时,每次重入仍然需要执行CAS操作。Java6中引入了偏向锁来做进一步优化:只有第一次使用CAS将线程ID设置到对象的Mark Word头,之后发现这个线程ID是自己的就表示没有竞争,不用重新CAS。
- 撤销偏向需要将持锁线程升级为轻量级锁,这个过程中所有线程需要暂停(STW)
- 访问对象的hashCode也会撤销偏向锁
- 如果对象虽然被多个线程访问,但没有竞争,这时偏向了线程T1的对象仍有机会重新偏向T2,重偏向会重置对象的Thread ID
- 撤销偏向和重偏向都是批量进行的,以类为单位
- 如果撤销偏向达到某个阈值,整个类的所有对象都会变为不可偏向的
- 可以主动使用 -XX:-UseBiasedLocking 禁用偏向锁
3.5 其他优化
3.5.1 减少上锁时间
同步代码块中尽量短
3.5.2 减少锁的粒度
将一个锁拆分为多个锁提高并发度,如:
- ConcurrentHashMap
- LongAdder 分为base和cells两部分。没有并发争用的时候或者是cells数组正在初始化的时候,会使用CAS来累加值到base,有并发争用,会初始化cells数组,数组有多好个cell,就允许有多少线程并行修改,最后将数组中每个cell累加,在加上base就是最终的值
- LinkedBlockingQueue 入队和出队使用不同的锁,想对于LinkedBlockingArray只有一个锁效率更高
3.5.3 锁粗化
多次循环进入同步块不如同步块内多次循环
另外JVM可能会做如下优化,把多次append的加锁操作粗化为一次(因为都是对同一个对象加锁,没必要重入多次)
new StringBuffer().append("a").append("b").append("c");
3.5.4 锁消除
JVM会进行代码的逃逸分析,例如某个加锁对象是方法内的局部变量,不会被其他线程所访问到,这时候就会被即时编译器忽略掉所有的同步操作。
3.5.5 读写分离
CopyOnWriteArrayList
ConvOnWriteSet