线程安全的定义
来自《Java高并发实战》“当多个线程访问一个对象的时候,如果不用考虑这些线程在运行时环境下的调度和交替执行,也不需要进行额外的同步,或者在调用方法的时候进行任何的协调工作,调用的对象的行为都能获得正确的结果,那这个对象就是线程安全的。”
这句话要求线程安全的代码都必须具备一个特征:代码本身封装了所有的正确的手段(同步或者互斥等),令调用者无需再做任何措施来保证线程的安全。
Java中的线程安全的理解
首先线程安全就限定于多个线程访问共享资源的情况,这是前提。并且线程安全不是一个非真既假的判断题,我们可以按照共享数据的类型划分“安全程度”,划分成5类:不可变,绝对线程安全,相对线程安全,线程兼容和线程对立。
不可变
不可变的对象一定是线程安全的,无论是对象方法的实现还是方法的调用者,都不需要再擦去任何线程安全保障措施。在Java语言中,如果共享数据是一个基本数据类型那直接使用final关键字修饰就是不可变的。如果共享数据是一个对象的话,那就需要保证对象的行为不会对对象的状态产生影响才醒。例如String就是一个不可变的类,调用他的replace(),subString()方法等都会生成一个新的字符串对象,不会改变旧的值。在Java中不可变的类型有多种,枚举也是不可变的以及Number的子类,BigDecimal,BigInteger,Integer,但是AtomicInteger和AtomicLong并非不可变。
绝对线程安全
在Java API中标注的线程安全的类严格来讲,都不是绝对线程安全的。例如Vector中每个方法都用Synchronized修饰可以说调用的时候线程安全啦,但是执行以下代码就会报数组越界异常。
import java.util.Vector;
public class VectorTest {
private static Vector<Integer> vector = new Vector<>();
public static void main(String[] args) {
while (true) {
for (int i = 0; i < 10; i++) {
vector.add(i);
}
Thread removeThread = new Thread(new Runnable() {
@Override
public void run() {
for (int i = 0; i < vector.size(); i++) {
vector.remove(i);
}
}
});
Thread printThread = new Thread(new Runnable() {
@Override
public void run() {
for (int i = 0; i < vector.size(); i++) {
System.out.print(vector.get(i));
}
}
});
removeThread.start();
printThread.start();
while (Thread.activeCount() > 20) {
// return;
}
}
}
}
要是保证上面的代码安全执行需要将removeThread和printThread方法用synchronized修饰。
所以使用线程安全的集合在调用的时候并不意味着不在进行同步了。
相对线程安全
相对线程安全就是平时通常意义上讲的线程安全,我们在调用的过程中不需要做额外的保障措施,但是对于一些特定顺序的连续调用,就可能需要在调用的时候才去额外的同步来保证结果的正确性。
线程兼容
线程兼容是指:对象本身不是线程安全的,但是可以通过调用时正确的同步来保证线程安全。Java的大多数API都是线程兼容的。
线程对立
线程对立:无论调用端采不采取同步都无法保证线程安全。例如Thread的suspend方法和resume方法,如果有两个线程同时持有一个线程对象,一个尝试中断线程,一个恢复线程,如果并发进行的话,无论调用是否进行了同步都会产生死锁。
实现线程安全的方式
1、互斥同步
同步是指在多个线程并发访问共享资源时,保证共享数据在同一时刻只能被一个线程使用,而互斥是实现同步的一种方式。互斥可以设置临界区,互斥量和信号量,这些都是实现互斥的一种方式,因此,在4个字里面,互斥是因,同步是果。互斥是方法,同步是目的。
在Java中最基本的互斥同步手段就是使用synchronized关键字,synchronized关键字经过编译会在同步代码块前后形成monitorenter和monitorexit指令,这两个指令都需要一个引用类型的参数作为锁定和解锁的对象,如果程序中明确指定了这个对象,那参数就是这个对象;如果没有明确指定,那就根据synchronized修饰的方法是实例方法是类方法,去取对应的对象实例或Class对象来作为锁对象。
在执行monitorenter指令的时候需要去获取对象的锁,如果已获取就把锁的计数器加一,相应的在执行monitorexit指令时会将锁技术器-1,当计数器为0,所就被释放。如果获取锁失败,那该线程就一直阻塞,直到另一个线程释放锁。
需要注意的是:synchronized是可重入锁,他不会把自己锁死。意思是一个线程可以多次获取同一把锁,又称递归锁,如果之前已经获取到锁,那就可以直接使用获取到的锁进入。用来防止一个线程多次获取锁把自己锁死。
锁优化
自旋锁与自适应自旋
所谓的自旋简单来说就是一个while死循环,从JDK1.6开始就默认开启了自旋,但是让线程自旋等待,在等待时间内什么事都不干一直循环判断也是资源上的浪费,因此设置了自旋次数,默认次数是10没如果自旋尝过了限定的次数仍然没有成功获取锁就直接把这个线程挂起了不再进行自旋。
在JDK1.6又引入了自适应自旋,这个就比较智能了,自旋时间不再固定,由前一次在同一个锁上的自旋时间以及锁的拥有者的状态来决定。如果虚拟机认为这次自旋也很有可能再次成功那就会次序较多的时间,如果自旋很少成功,那以后可能就直接省略掉自旋过程,避免浪费处理器资源。
锁消除
锁消除是指:虚拟机编译器在运行时,检测到了共享数据没有竞争的锁,将这些锁进行消除。
public String concatString(String s1,String s2,String s3){
StringBuffer sb = new StringBuffer();
sb.append(s1);
sb.append(s2);
sb.append(s3);
return sb.toString();
}
上面的代码中StringBuffer是线程安全的,append方法有个同步块,锁是sb对象。虚拟机观察sb,发现他的作用域限制在这个方法内部,所以sb的引用不会被用在方法外,其他线程无法访问到他,因此这里面append方法虽然有锁但是就被虚拟机消除了。所以直接忽略所有同步就执行了。
锁粗化
这个怎么理解呢?其实就是推荐将同步块的作用范围写的尽可能小,这样是为了是的需要的同步操作数量尽可能少,如果存在锁竞争,那也可以尽快的拿到锁,锁粗化就是将多次上锁解锁的请求合并为一次同步请求。
例如
for(int i=0;i<10;i++){
synchronized(lock){
// do something
}
}
经过锁粗化就变成了下面的代码
synchronized(lock){
for(int i=0;i<10;i++){
// do something
}
}
偏向锁
偏向锁也是JDk1.6中引入的一项锁优化,他的目的是消除数据在无竞争情况下的同步原语,以此来提高性能。偏向锁的作用在于可以在无竞争锁的情况下将整个同步都消除掉,CAS也消除掉。
偏向锁的偏其实就是偏向 第一个获取他的线程,如果在接下来的执行过程中没有其他线程获取锁,那么持有偏向锁的线程
将永远不需要在进行同步。
轻量级锁
在轻量级锁状态下继续锁竞争,没有抢到锁的线程将自旋,即不停地循环判断锁是否能够被成功获取。获取锁的操作,其实就是通过CAS修改对象头里的锁标志位。先比较当前锁标志位是否为“释放”,如果是则将其设置为“锁定”,比较并设置是原子性发生的。这就算抢到锁了,然后线程将当前锁的持有者信息修改为自己。
锁升级的过程
无锁->偏向锁->轻量级锁->重量级锁
无锁是对资源没有锁定,所有线程都可以访问并修改同一个资源,但是在同一时刻只能有一个线程能修改成功。其他线程会不断重试直到修改成功。
偏向锁的升级
如果当前虚拟机启用了偏向锁(-XX:+UseBiasedLocking
),那么当一个锁被线程获取的时候,虚拟机会把这个锁(其实也就是一个对象)中的标志位设置为 01 ,代表偏向模式。同时使用CAS操作把获取到这个锁的线程的ThreadID记录在锁对象的Mark Word之中,如果CAS操作成功,持有偏向锁的线程以后每次进行这个锁相关的同步代码块时,虚拟机都可以不在进行任何同步操作。
当另一个线程尝试获取这个锁的时候,偏向模式宣告结束。根据锁对象目前是否处于被锁定的状态来撤销偏向锁恢复为无锁状态,或者是升级到轻量级锁状态。
偏向锁的取消
-XX:-UseBiasedLocking=false
轻量级锁起作用的依据就是:“对于绝对大部分的锁,在整个同步周期内都是不存在竞争性的”这是经验所得出的结论。
当两条以上的线程争夺使用同一把锁时,轻量级锁不在有效,膨胀为重量级锁。Mark Word中存储的就是指向重量级的指针,后面没有获得锁的线程进入阻塞状态。
Mark Word:32比特空间,25位是HashCode,4比特是GC分代年龄,2比特是用来存储锁标志,1比特固定为0
存储内容 | 标志位 | 状态 |
---|---|---|
对象哈希码、对象分代年龄 | 01 | 未锁定状态 |
偏向线程ID,偏向时间戳,对象分代年龄 | 01 | 偏向锁状态 |
指向锁记录的指针 | 00 | 轻量级锁定 |
指向重量级锁的指针 | 10 | 重量级锁定 |
空,不需要记录信息 | 11 | GC标记 |
以上有理解不到位的地方欢迎指出!
参考:https://zhuanlan.zhihu.com/p/71156910
深入理解Java虚拟机 第二版
https://juejin.im/post/5ca766dcf265da30d02fb35c