8.1 线程安全
“当多个线程访问一个对象时,如果不用考虑这些线程在运行时环境下的调度和交替执行,也不需要额外的同步,或者在调用方进行任何其他的协调操作,调用这个对象的行为都可以获得正确的结果,那这个对象就是线程安全的。”
这里作者把Java的线程安全分为五个等级:
1.不可变:
不可变的对象一定是线程安全的,前提是在构建过程未逃逸。final修饰的对象就是不可变对象。final修饰基本类型就不可变,修饰对象的话对象不可重新初始化,只能调用其中方法,个人理解是内存起始不可变。
2.绝对线程安全:
难。
但作者给的示例代码已经失效了,科技在进步。只会不停地跑下去,还是安全的。
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.println(vector.get(i));
}
});
removeThread.start();
printThread.start();
System.out.println(" current have " + Thread.activeCount() );
while(Thread.activeCount() > 20) System.out.println(" already have " + Thread.activeCount() );
}
3.相对线程安全:
通常意义的线程安全,不需要额外加入同步限制。Java的Vector,HashTable都是线程安全的。
4.线程兼容:
对象本身不是线程安全的,在加入同步手段后可以安全地使用。基本都是这样的。
5.线程对立:
无论加入什么措施都无法多线程并发。Thread的suspend()和resume()方法就是线程对立的。
8.2 线程安全的实现方法
互斥同步(Mutual Exclusion & Synchronization),阻塞式同步,是一种保证一个对象同一时间只在一个线程中被使用的方法。利用互斥(临界区、互斥量、信号量)来实现同步。synchronized在java中是一个重量级操作,在有必要的时候才使用。
除了synchronized,还有ReentrantLock可以实现同步。基于同步的基础上,重入锁加入三中功能:等待可中断,持有锁的线程长时间不释放锁,等待的线程可以放弃等待,去做其他事情;公平锁,多个线程同时等待一个锁时,按照申请锁的时间依次获得锁,synchronized不行;锁绑定多个条件,一个ReentrantLock可以绑定多个Condition对象,使用newCondition()就可以添加,不像synchronized每增加一个条件都要添加一个锁。
非阻塞同步与互斥同步不同,互斥同步在发生锁时,会立刻处于等待阶段,阻塞状态。非阻塞同步是一种乐观锁,先操作,再看该对象是否被多线程竞争,如果被竞争,则采取补救措施(重试),否则就继续。
无同步方案指一些天生线程安全的方法,不需要同步来保护。可重入代码(Reentrant Code),也叫纯代码(Pure Code),可以在任何地方被打断,且重入后不会出现错误。线程本地存储(Thread Local Storage),如果把有竞争的变量都统一放在一个线程中,则无需同步。
8.3 锁优化
8.3.1 自旋锁与自适应自旋
由于线程的总调度要交给内核线程,所以线程阻塞的时候的挂起和恢复操作都要转入内核态来完成,是非常效率低的。且大多数情况等待时间较短,没必要切换状态。为了减少这种状态切换,可以让线程执行一个忙自旋,让线程在等待时“原地踏步”,这个技术是自旋锁。自旋等待避免了状态切换,但会占CPU时间,在等待时间较短时,是非常划算的。因此自旋锁的自旋时间需要有限制,不能没完没了地原地踏步。
自适应自旋指自旋的次数根据上次在同一个锁上的自旋时间决定,如果上一次成功获得锁,且持有锁的线程正在运行,代表本次自旋是值得的,时间长一些也可以;如果某个锁自旋很少成功过,则本次可能不选择自旋。
8.3.2 锁消除
锁消除指自动消除一些“没必要”的锁。如果一个对象被同步,但检测到不存在对其数据的竞争,那本次的锁时无必要的,可以消除。如果一段代码的所有数据都不会逃逸,则他们是线程私有的,很安全,没必要上锁。然而代码不上锁不代表运行时无同步,有时Java的内在类会包装一些同步措施,在这个技术下可以被摘除。
8.3.3 锁粗化
通常来说,为了节省阻塞时间,同步范围越小越好。然而一个对象反反复复被同步,且某程序块都会被反复竞争,则可以将同步块的范围扩大。
8.3.4 轻量级锁
虚拟机的对象的布局中对象头(Object Head)分为两个部分,第一个部分用于存储对象自身的运行时数据,如HashCode,Generation GC Age等,官方称为“Mark Word”;另一部分用于存储指向方法区对象类型数据的指针。为了节省空间,Mark Word的空间不是固定的,是可伸缩可复用的。
简单来说,轻量级锁的原理就是在当前线程中建立一块Lock Record空间,存储同步对象的Mark Word,然后让该对象的Mark Word指向Lock Record中的指针,这样线程就有了该对象的锁。如果更新失败,虚拟机检查对象的Mark Word是否指向该线程的栈帧,如果有锁,则该线程拥有了该对象的锁,否则该对象在被其他线程锁定了。如果存在两条线程以上的线程竞争同一个锁,则轻量级锁要升级到重量锁,此时Mark Word中存储重量级锁(互斥量)的指针,等待锁的线程进入等待状态。
在解锁中,对象的Mark Word指向栈帧中Lock Record中的指针,将两指针调换,则解除锁定。为啥叫轻量级锁?因为很多时候虽然同步了,但不一定有竞争。但真的有了竞争,轻量级锁使用了CAS来加锁解锁,速度倒慢了。所以也不可能取代重量锁。
8.3.5 偏向锁
在无竞争的情况下,直接消除整个同步。偏向锁的意思是锁会偏向第一个获取锁的线程,在他的执行过程中,如果没有别的线程竞争此对象,则不同步。偏向锁提高了无竞争,且有同步的程序性能。
至此,本书结束。其实就编程而言,并无太多实用之处,更适合一些硬核的底层设计人员。但开卷有益,也许今日觉得无用的东西,在哪天突然见到了实用性,就会知道回头看看,也是不错的。