目录
1.什么是JMM
2.JMM怎样保障数据的可见性、有序性、原子性
2.1保证原子性
2.2.保证可见性
2.3保证有序性
3.CAS
3.1乐观锁和悲观锁
3.2 CAS介绍
4.重量级锁的自旋优化
1.什么是JMM
JMM即Java内存模型 ,定义了一套在多线程读写共享数据(如数组、成员变量等)时,对数据的可见性、有序性、原子性的规则和保障。JMM和Java内存结构关系不大,内存结构就是JVM的内存组成、垃圾回收以及字节码和类加载的一些技术。
2.JMM怎样保障数据的可见性、有序性、原子性
2.1保证原子性
对于多个线程共享的数据而言,如果不对数据进行加锁,那么多个线程在调用同一资源时很可能出现异常操作。
比如线程1和线程2分别对一个初始值为0的变量 i 执行 i++ 和 i-- 操作,理论上执行的结果是0,但实际情况却不一定。由于操作系统在处理线程时采用的是时间片轮转的方式,很有可能出现线程1还未执行完 i++ 时间片就耗尽了,接下来就轮到线程2执行 i-- 操作。线程2在时间片内完成了对 i 的修改,此时 i 的值为-1。然后再次轮到线程1,由于线程1拿到的 i 的初始值是0,所以 i++ 是在 i=0 的基础上执行的,执行完成后 i 的值为1,覆盖掉了线程2的执行结果-1,所以相当于线程2对于 i 的修改是无效的。
这就需要进行加锁,当一个线程对一个共享资源未使用完毕前其他线程不能使用这一资源,也就是保障了对共享资源的操作在执行完之前不会有其他线程访问这一资源。
加锁操作:
synchronized(要加锁的对象){需要保障原子性的代码
}
锁粗化:在加锁时要尽量扩大加锁的范围,比如对变量 i 执行一千次加1操作,如果仅对加一的操作进行加锁,那么就会执行一千次加锁和解锁的操作;但如果是对整个循环进行加锁,不仅可以保证原子性,而且只用执行一次加锁和解锁的操作,能够缩短运行时间。
2.2.保证可见性
共享资源存储在主内存中,当一个线程频繁读取某一资源时JIT(即时编译器)会进行优化,将该资源的值存入到该线程工作内存中的高速缓存中,后面再读取这个资源的值时会从这个高速缓存中读取,虽然提高了效率,但当对这一资源的值进行修改后,还是会从高速缓存中读取之前的旧值,导致线程对这个资源的修改不可见。
有两种方式保证可见性:
- 使用synchronized加锁
在加锁时会清空工作内存中对所有共享资源的缓存,强制要求到主内存中重新读取共享变量的值,保证了拿到的共享变量的值是最新的。并且在解锁时,无论是否对共享变量的值进行了修改,都会将共享变量的值刷新回主内存,以确保主内存中的共享变量的值是最新的。也可以直接调用System.out.println(),因为执行输出操作时会调用到synchronized。
优点是既能确保原子性又能确保可见性,缺点就是synchronized属于重量级的操作,性能较低。
- 使用volatile关键字
对共享变量加上volatile关键字能够强制要求线程在读取共享变量的值时总是从主内存中读取,保证了可见性。该方式性能较高,但仅适用于一个写线程其他都是读线程的情况,因为如果有多个写线程由于不能保证原子性就无法确定拿到的值是否是正确的。
2.3保证有序性
JVM在执行赋值操作时会根据指令是否耗时而进行指令重排,在不影响结果的条件下调整指令的执行顺序,但在并发执行时更改指令的执行顺序就可能会出现错误,此时可以通过加上volatile关键字禁用指令重排来保证有序性。
3.CAS
3.1乐观锁和悲观锁
在介绍CAS之前先来了解一下乐观锁和悲观锁:
3.2 CAS介绍
CAS即Compare And Swap,是一种乐观锁的思想,不使用synchronized对共享变量加锁,通过volatile关键字的配合实现了无锁并发。原理是通过一个while循环不断尝试修改共享变量的值,在循环时暂时存储该共享变量的原值和本线程修改后的值,并通过compareAndSwap方法来判断该线程修改共享变量结束后存储的原值是否和当前共享变量的值相同。如果不同说明在该线程修改共享变量期间其他线程对共享变量的值进行了修改,那么本次修改就是无效的,需要再次进入循环重新获取共享变量的值并尝试修改;如果相同说明修改期间其他线程并没有对共享变量进行修改,那么就会将修改的结果更新到主内存中,并返回true,可以通过if语句实现跳出循环。
至于要volatile关键字的配合是因为要保证可见性,在每次循环时都能获取到共享变量最新的值。
由于synchronized加锁会使线程进入阻塞,并进行上下文切换,需要保存线程阻塞前的状态并在被唤醒时恢复,这一过程是非常耗时的,所以CAS无锁并发能够提升效率;但CAS适用于线程竞争不激烈并且是多核CPU的情况下,因为当竞争比较激烈时肯定会进行频繁的循环,此时就需要花费大量的时间来不断尝试修改共享变量的值;而如果是单核CPU,那么当时间片耗尽时只能等待下次拿到时间片时才能继续尝试,但多核CPU就能实现在其他线程运行的同时不断尝试,这才能体现CAS的优势。