持续创作,加速成长!这是我参与「掘金日新计划 · 10 月更文挑战」的第19天,点击查看活动详情
前言
在谈线程安全之前我们需要了解CPU和主存之间的关系.
而高速缓存在解决CPU的内存速度不一致问题的同时,也引起了新的问题,那就是共享数据的不一致性.
一、java线程安全
当多个线程访问某个类时,不管运行时环境采用何种调度方式,或者这些进程如何交替执行,并且在主调代码中不需要任何额外的同步或协同.
这个类都能表现出正确的行为,那么就称这个类是线程安全的.
1.1 原子性
提供了互斥访问,同一时刻只能有一个线程对它进行操作
1.2 可见性
一个线程对主内存的修改可以及时地被其他线程观察到.
1.3 有序性
防止指令重排
二、Synchronized
Synchronized关键字能同时解决程序执行的“原子性”,“可见性”,和“有序性”,当我们获得锁的时候,执行同步代码,线程会被强制从主内存中读取数据,先把主内存的数据复制到本地内存,然后在本地内存进行修改,在释放锁的时候,会把数据写会主内存。
但是这样做的效率无疑很低。
三、volatile
volatile保证了可见性和有序性
3.1 可见性
volatile是一个类型修饰符,它是被设计用来修饰被不同线程访问和修改的变量,可以被异步的线程所修改。
volatile修饰的变量是放到共享内存中的,可以让所有的线程获取和修改。
3.2 有序性
Java内存模型会将volatile修饰的字段,将在写操作后插入一个写屏障 指令,在读操作前插入一个读屏障指令。
如果你对一个volatile字段进行写操作时
- 1、一旦你完成写入,任何访问这个字段的线程将 会得到最新的值。
- 2、在你写入前,会保证所有之前发生的事已经发生,并且任何更新过的数据值也是可见的,因为内存屏障会把之前的写入值都刷新到缓存。
3.3 不保证原子性
例如你让一个volatile的integer自增(i++),其实要分成4步:
- 读取volatile变量值到local;
- 增加变量的值;
- 把local的值写回
- 通过内存屏障让其它的线程可见。
这4步的jvm指令为:
mov
0xc(%r10),%r8d
; Load
inc
%r8d ; Increment
mov
%r8d,0xc(%r10)
; Store
lock
addl $0x0,(%rsp)
; StoreLoad Barrier
从Load到store到内存屏障,一共4步。
其中最后一步jvm让这个最新的变量的值在所有线程可见,也就是最后一步让所有的CPU内核都获得了最新的值,但中间的几步(从Load到Store)是不安全的,中间如果其他的CPU修改了值将会丢失。
3.4 总结
所以volatile不能保证i++操作的原子性
四、CAS
CAS英文解释是比较和交换,是cpu底层的源语,是解决共享变量实现方案,它定义了三个变量,内存地址值对应V,期待值E和要修改的值U
4.1 ABA问题
CAS操作只有内存值与预期值相等才会更新内存中的值,所有CAS操作可能会出现这种现象:
原来内存值为A,线程1和线程2都对i值操作,i初始值为1,线程1使用CAS将i值修改为5,然后又使用CAS将内存值修改回1;
这时线程2使用CAS对i值进行修改时发现内存值仍然是1,然后线程2修改成功。
这种现象是“ABA问题”。
五、Atomic包
Java在Atomic提供了若干原子类,通过CAS操作提供了互斥访问,同一时刻只能有一个线程来对它进行操作.
六、单例
6.1 饿汉式单例
public class Singleton {
//在类加载的时候就完成了实例化
private final static Singleton INSTANCE = new Singleton();
private Singleton(){}
public static Singleton getInstance(){
return INSTANCE;
}
}
因为在类加载时就完成了实例化,从而避免了多线程同步问题.
但是缺点也是很明显
缺点
类加载时就实例化了,没有达到Lazy Loading (懒加载) 的效果,如果该实例没被使用,内存就浪费了
6.2 普通的懒汉式
public class Singleton {
private static Singleton instance = null;
private Singleton() {
}
public static Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}
缺点
这个单例看似没什么问题,但是如果当存在并发时,A线程和B线程会同时实例化多个单例,实际上是线程不安全的.
6.3 同步方法的懒汉式
public class Singleton {
private static Singleton instance = null;
private Singleton() {
}
public static synchronized Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}
通过对方法加上synchronized锁,实现线程安全
缺点
但是我们也在上面提到了synchronized的缺点,让线程同步等待的时间过长,有点消耗时间和性能
6.4 双重检查懒汉式
public class Singleton {
private static volatile Singleton instance;
private Singleton() {}
public static Singleton getInstance() {
if (instance == null) {
synchronized (Singleton.class) {
if (instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
}
可以看到,我们对获取单例的方法并没有加锁,那么是如何保证线程安全的呢?
我们在判断单例对象为空后,加了synchronized锁,但是由于对变量赋值也会涉及到指令重排.总共有三步
- 1、分配对象的内存地址
- 2、初始化对象
- 3、将instance指向初始化对象
如果我们没有使用volatile关键字修饰单例时,可能存在线程A拿到的是线程B已经还没有初始化控件,但已指向该对象的instance对象.
那么为了保证指令的可见性和有序性,我们使用volatile关键字去修饰变量.
七、 ReentrantLock
ReentrantLock是一个可重入的互斥锁,又被称为"独占锁"。
ReentrantLock类实现了lock,它拥有与synchronized相同的并发性和内存语义,但是添加了类似锁投票、定时锁等候和可中断锁等候的一些特性。此外,它还提供了在激烈争用情况下更佳的性能(换句话说,当许多线程都想访问共享资源时,JVM可以花更少的时间来调度线程,把更多时间用在执行线程上)
顾名思义,ReentrantLock锁在同一个时间点只能被一个线程锁持有;而可重入的意思是,ReentrantLock锁,可以被单个线程多次获取.
ReentrantLock分为公平锁和非公平锁。他们的区别提现在获取锁的机制上是否公平。
锁是为了保护竞争资源,防止多个线程同时操作数据而出错。
ReentrantLock在同一个时间点只能被一个线程获取(当某线程获取到锁时,其他线程就必须等待)ReentrantLock是通过一个FIFO的等待队列来管理获取该锁所有线程的,在公平锁的机制下,线程依次排队获取锁,而非公平锁在锁是可获取状态时,不管自己是不是在队列的开头都会获取锁
而关于ReentrantLock的源码分析我们将在下节详细阅读~