线程安全&Java内存模型
Java内存模型
Java内存模型(JMM)主要目标是定义多线程的情况下线程访问变量的规则。
JMM规定线程之间的共享变量存储在主内存中,每个线程都有一个本地内存(工作内存),本地内存存储了共享变量的副本。
关于线程安全
-
什么是线程安全问题?
当多个线程同时共享同一个全局变量做写的操作时候,可能会受到其他线程的干扰,导致数据脏读。(数据一致性问题)
-
如何解决线程安全问题?
核心思想:在同一时刻,只能有一个线程执行。
通过加锁使线程更加安全,也使程序的执行效率更低。
-
衡量线程安全的3个要素:
- 原子性:一个操作或者多个操作要么全部执行,要么都不执行
- 可见性:多个线程访问同一变量,一个线程修改了变量的值,其他线程可以立即看到修改的值
- 有序性:程序按照代码的顺序先后执行(与指令重排有关)
Volatile关键字
volatile是一种轻量级的同步机制,可以保证可见性【及时将修改的变量刷新到主内存中】,但不能保证原子性,并且禁止重排序。
volatile在多线程下的适用场景:一写多读
volatile如何保证内存可见性?
当一个线程对volatile修饰的变量进行写操作时,该线程中的本地内存的变量会被立刻刷新到主内存中。
当一个线程对volatile修饰的变量进行读操作时,该线程直接读取主内存的变量。
volatile能否保证线程安全?
不能,保证线程安全需要同时具备原子性,可见性和有序性。而volatile只能保证可见性和有序性,无法保证原子性。
Synchronized关键字
核心思想:在多线程执行同一个方法时,只有获取到锁,才能进入方法里面执行
使用方式:
- 修饰一个类:其作用的范围是synchronized后面括号括起来的部分,作用的对象是这个类的所有对象;
- 修饰一个方法:被修饰的方法称为同步方法,其作用的范围是整个方法,作用的对象是调用这个方法的对象;
- 修饰一个静态的方法:其作用的范围是整个方法,作用的对象是这个类的所有对象;
- 修饰一个代码块:被修饰的代码块称为同步语句块,其作用范围是大括号{}括起来的代码块,作用的对象是调用这个代码块的对象;
锁的分类
轻量级锁&重量级锁
轻量级锁:手动上锁解锁,扩展性强。代表:Lock
重量级锁:自动上锁解锁,封装程度高。代表:Synchronized
可重入锁&不可重入锁
可重入锁(递归锁):当一个线程已经获取到锁后,再次请求该锁,就可直接获取。(锁的传递,锁的嵌套)代表:Synchronized,Lock
锁的可重入性避免了大部分死锁情况的产生
不可重入锁:不具备传递性
读写锁
ReentrantReadWriteLock
相对Synchronized效率更高,但在多线程情况下,只支持读读共存,不支持读写,写写。
乐观锁与悲观锁
乐观锁(适合多读场景)
-
思想:认为不会发生线程冲突(本质上是没有锁的)
-
执行流程,先读取数据,然后在更新前检查在读取至更新这段时间数据是否被修改
- 未修改:直接更新数据
- 已修改:重新读取,再次提交更新(或者放弃操作)
为什么乐观锁适合多读场景?
乐观锁是一种更新前的检查机制,相对于悲观锁来说在多读场景下可以减少锁的性能开销,对于多写场景,乐观锁会一直进入已修改,重新读取,再次提交的循环,反而带来更多的资源消耗。
悲观锁(适合多写场景)
-
思想:认为一定会发生线程冲突
-
执行流程:读取数据的时候上锁(其他用户无法读取),直到本次数据更新完成才会释放锁。在多写场景下,能保证较高的数据一致性。
【总的来说,乐观锁回滚重试,悲观锁阻塞事务】
CAS无锁机制
原子类:AtomicBoolean,AtomicInteger,AtomicLong,AtomicReference可保证线程安全,底层使用CAS无锁机制
CAS:Compare and Swap,比较再交换,属于乐观锁的一种
-
CAS原理
CAS包含3个参数,CAS(V,E,N),V:主内存的变量值,E:本地内存修改前的值,N:本地内存修改后的值
比较主内存的值和本地内存修改前的值是否一致,若一致,将修改后的值刷新到主内存,若不一致,当前线程放弃更新,将主内存数据刷新到本地内存,再次重试。
-
优点:非阻塞,不会发生死锁情况,效率更高
-
缺点:ABA问题(可以通过加入版本号来区分变量是否被修改)