Java中的线程安全性
一、原子性——atomic
- 定义:提供互斥访问,同一时刻只能有一个线程对数据进行操作(atomic,synchronized);
- atomic类:比如AtomicInteger,AtomicLong,AtomicBoolean等等。通过CAS实现原子性!
- CAS:compare and swap
JDK5以前之前Java语言是靠synchronized关键字保证同步的,这会导致有锁。
有锁的劣势:
synchronized悲观锁让没得到锁的进程进入阻塞态,争夺到资源的进入就绪态,产生等待延时,性能差。
注意此时volatile只能保证可见性,无法保证原子性!
①JDK5增加了并发包java.util.concurrent.*,其下面的类使用CAS算法实现了乐观锁,比如java.util.concurrent.atomic中的AtomicInteger。
concurrent包内源码:
首先,声明共享变量为volatile;
然后,使用CAS的原子条件更新来实现线程之间的同步;
同时,配合以volatile的读/写和CAS所具有的volatile读和写的内存语义来实现线程之间的通信。
②原理:
CAS机制中使用了3个基本操作数:内存地址V,旧的预期值A,要修改的新值B。
更新一个变量的时候,只有当变量的预期值A和内存地址V当中的实际值相同时,才会将内存地址V对应的值修改为B。
③举例:
1. 在内存地址V当中,存储着值为10的变量。
2. 此时线程1想把变量的值增加1.对线程1来说,旧的预期值A=10,要修改的新值B=11.
3. 在线程1要提交更新之前,另一个线程2抢先一步,把内存地址V中的变量值率先更新成了11。
4. 线程1开始提交更新,首先进行A和地址V的实际值比较,发现A不等于V的实际值,提交失败。
5. 线程1 重新获取内存地址V的当前值,并重新计算想要修改的值。此时对线程1来说,A=11,B=12。这个重新尝试的过程被称为自旋。
6. 这一次比较幸运,没有其他线程改变地址V的值。线程1进行比较,发现A和地址V的实际值是相等的。
7. 线程1进行交换,把地址V的值替换为B,也就是12。
- CAS的缺点:
1) CPU开销过大:在并发量比较高的情况下,如果许多线程反复尝试更新某一个变量,却又一直更新不成功,循环往复,会给CPU带来很到的压力。
2) 不能保证代码块的原子性:CAS机制所保证的只是一个变量的原子性操作,而不能保证整个代码块的原子性。比如需要保证3个变量共同进行原子性的更新,就不得不使用synchronized了。
3)ABA问题:当一个值从A变成B,又更新回A,普通CAS机制会误判通过检测。
我们现在来说什么是ABA问题。假设内存中有一个值为A的变量,存储在地址V中。 - 此时有三个线程想使用CAS的方式更新这个变量的值,每个线程的执行时间有略微偏差。线程1和线程2已经获取当前值,线程3还未获取当前值。
- 接下来,线程1先一步执行成功,把当前值成功从A更新为B;同时线程2因为某种原因被阻塞住,没有做更新操作;线程3在线程1更新之后,获取了当前值B。
- 在之后,线程2仍然处于阻塞状态,线程3继续执行,成功把当前值从B更新成了A。
- 最后,线程2终于恢复了运行状态,由于阻塞之前已经获得了“当前值A”,并且经过compare检测,内存地址V中的实际值也是A,所以成功把变量值A更新成了B。
- ABA问题解决办法:版本号!
我们在compare阶段不仅要比较期望值A和地址V中的实际值,还要比较变量的版本号是否一致。
Java中如何实现?
在JDK5以后有AtomicStampedReference类,其中的compareAndSet()方法: - 相比于atomicInteger类中的compareAndSet():
- 可以看到AtomicStampedReference里的compareAndSet()中多了 一个stamp比较(也就是版本),这个值是由每次更新时来维护的。
- CAS底层实现:
Unsafe类,底层不对外开放,java.utils.concurrent类提供了封装。 - 举例:
public class TestDemo {
static AtomicInteger a = new AtomicInteger(0);
public static void main(String args[]) {
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
try {
int count = 0;
while (count < 100) {
int value = a.addAndGet(1);
System.out.println("in thread1 a = " + value);
count++;
}
} catch (Exception e) {
}
}
});
t1.start();
Thread t2 = new Thread(new Runnable() {
@Override
public void run() {
try {
int count = 0;
while (count < 100) {
int value = a.addAndGet(1);
System.out.println("in thread2 a = " + value);
count++;
}
} catch (Exception e) {
}
}
});
t2.start();
}
}
运行如上代码,可以发现保证了线程安全,其中最重要的就是AtomicInteger类中的addAndGet()方法,看一下源码:
#AtomicInteger.java
public final int addAndGet(int delta) {
return U.getAndAddInt(this, VALUE, delta) + delta;
}
#Unsafe.java
public final int getAndAddInt(Object o, long offset, int delta) {
int v;
do {
//获取共享变量的值,通过偏移量获取
//getIntVolatile获取的变量是volatile修饰的,因此每次都能够拿到最新值
v = getIntVolatile(o, offset);
} while (!compareAndSwapInt(o, offset, v, v + delta));//不成功,则再次尝试
return v;
}
再看源码,发现compareAndSwapInt属于底层cpp语言,无法查看。
当调用Unsafe.java方法:compareAndSwapInt(xx),其底层是上了锁保证了原子性,只是这个锁是由CPU实现的(硬件层面)!!!!这里就是完成了CAS原理!!!!
所以addAndGet()方法实现了功能是:在底层用循环一直尝试修改变量的值,不成功就一直去尝试,成功则退出循环。
二、原子性——synchronized
- synchronized通过锁来实现原子操作
- 可作用于:代码块、方法、静态方法(锁定调用它们的对象所属的类,同一个时间只有一个对象在执行。)、类(锁定调用它们的对象所属的类,同一个时间只有一个对象在执行。)
- 原理:
synchronized底层是一个monitor对象,其存在两个字节码指令:
monitorenter:进入对象则加1
monitorexit:离开对象就减1
- 流程:
- 如果monitor的进入数为0,则该线程进入monitor,然后将进入数设置为1,该线程即为monitor的所有者。
- 如果线程已经占有该monitor,只是重新进入,则进入monitor的进入数加1.
- 如果其他线程已经占用了monitor,则该线程进入阻塞状态,直到monitor的进入数为0,再重新尝试获取monitor的所有权。
- synchronized致命缺点:
底层需要互斥锁(mutex lock):实现线程切换就是需要操作系统在用户态和核心态进行转化,耗时长!属于重量级锁! - JDK6对synchronized进行了优化:
锁的状态总共有四种:无锁状态、偏向锁、轻量级锁和重量级锁,这几个状态会随着锁竞争的情况逐步升级。为了提高获得锁和释放锁的效率,锁可以升级但是不能降级。
- 偏向锁: 偏向锁无法使用自旋锁优化,因为一旦有其他线程申请锁,就破坏了偏向锁的假定(偏向锁假设仅有一个线程操作)
- 轻量级锁:允许自旋(循环),自旋到一定程度就会转化为重量级锁
三、可见性——volatile
注意synchronized和volatile都能实现可见性!volatile 本质是告诉jvm当前变量在寄存器中的值是不安全的需要从内存中读取;sychronized 则是锁定当前变量,只有当前线程可以访问到,该变量其他线程被阻塞。
volatile会在写操作后加一条store屏障指令,将本地内存中的共享变量值刷新到主内存;
volatile在进行读操作时,会在读操作前加一条load指令,从内存中读取共享变量;
四、有序性
在JVM中为了性能考虑,允许编译器和处理器对指令进行重排序,但是重排序过程不会影响到单线程程序的执行,却会影响到多线程并发执行的正确性。
这里的指令重排序主要是JVM中的JIT编译器中的C2编译器完成的。JIT编译器分三类(C1,C2,分层编译。JDK8后默认是分层)。
可以通过volatile、synchronized、lock保证有序!
volatile本身禁止指令重排,syn和lock都是在同一时刻仅允许一条线程对其操作。
happenbefore原则:天然的关系
- 程序次序规则:在一个单独的线程中,按照程序代码书写的顺序执行
- 锁定规则:一个unlock操作happen—before后面对同一个锁的lock操作。
- volatile变量规则:对一个volatile变量的写操作happen—before后面对该变量的读操作。
- 线程启动规则:Thread对象的start()方法happen—before此线程的每一个动作。
- 线程终止规则:线程的所有操作都happen—before对此线程的终止检测,可以通过Thread.join()方法结束、Thread.isAlive()的返回值等手段检测到线程已经终止执行。
- 线程中断规则:对线程interrupt()方法的调用happen—before发生于被中断线程的代码检测到中断时事件的发生。
- 对象终结规则:一个对象的初始化完成(构造函数执行结束)happen—before它的finalize()方法的开始。
- 传递性:如果操作A happen—before操作B,操作B happen—before操作C,那么可以得出A happen—before操作C。