线程安全是每一门多线程编程语言都要考虑的问题。
Java语言中解决线程安全可以用互斥同步的方法来实现。它通过规定的互斥量,信号量和临界区进行线程的同步操作。
在Java语言层面上有两种实现方法:synchronized与重入锁(ReentrantLcok)。
synchronized
synchronized是一种语法上的同步方法。他在字节码执行是通过添加monitorenter指令和monitorexit两个指令执行临界区代码的。当一个线程这两个指令中运行时,说明该线程已经获取到了锁,其他的竞争锁的线程都在阻塞中。
当执行到monitorexit指令时,说明临界区代码已经执行完毕。虚拟机会唤醒其他等待锁的线程,这个操作需要从操作系统的用户态切换到内核态,是一个耗时操作,因此一般的锁操作(重量级锁)比较费时。虚拟机会帮我们进行一些优化(下面会说),当然我们在编程时也要尽量避免使用。
重入锁(ReentrantLcok)
重入锁也是通过互斥同步实现线程安全问题。它与synchronized在使用上有几点不同。
1.synchronized是在语法层面上实现同步的,重入锁则是通过lock()和unlock()两个API方法配合异常处理流程完成同步的。
2.重入锁比起synchrnized有更加高级的功能:
1)等待可中断:遇到长时间不释放锁的线程是,等待的线程可以放弃等待做其他的事情。在遇到”赖皮“线程时能提高代码整体执行效率。
2)公平锁:获取锁的顺序由线程的等待时间决定,可以通过带布尔值的构造函数开启此功能。
3)绑定多个条件:synchronized只能通过wait和notify两个函数进行代码的条件操作。但是重入锁可以通过new Condition()增加条件以得到更复杂多样的条件操作。
锁优化
很早之前,线程同步都是通过锁操作互斥量实现的。在整个操作过程中对线程的操作(休眠,唤醒。。。)都需要进行用户态和内核态的相互切换,这是非常低效的。在JDK1.6时虚拟机增加了偏向锁和轻量级锁对这个情况进行了改善。
不仅如此,在虚拟机的解释编译时在字节码层面也对一些锁操作进行优化。
1.自旋锁
在锁的竞争时常发生的情形是一个锁正在被其他线程使用,于是新来的线程被阻塞等待。上面也说过,挂起线程的操作很费事,要是这个使用锁的线程马上就要释放锁了,新来的线程刚被阻塞,锁正好在这个时候释放了,这个新来的线程又立刻被唤醒,这种情况是最亏的,白白浪费了那么多时间。
于是自旋锁出现了,如果虚拟机通过监测计算这个锁被持有的时间不长或者马上要被释放了,那么新来的线程不会被挂起,而是进行一个空循环等待锁。
2.锁消除
通过逃逸分析技术,虚拟机可以判断一个变量的作用域。如果这个变量仅仅在这一个线程中存在使用,与其他线程相互独立,那么也就不存在线程同步的问题。这是虚拟机会消除这个锁。
3.锁粗化
有这么一段代码:
public void myMethod(StringBuffer sb) {
sb.append('1');
sb.append('1');
sb.append('1');
sb.append('1');
sb.append('1');
}
我们知道StringBuffer的append()方法已经是线程安全的了,在多次调用append()方法是会反复出现线程同步操作。锁粗化就是把多个相连的锁操作合并为一个。这段代码在经过优化后只有第一个append()方法会开启代码同步,然后在最后一个append()方法关闭代码同步;