一、线程安全
线程安全是指:当多个线程访问一个对象时,如果不用考虑这些线程在运行时环境下的调度和交替执行,也 不需要额外的同步,或者在调用方进行任何其他的协调操作,调用这个对象的行为都可以获得正确的结果,则这个对象是线程安全的。
Java语言中各个操作共享的数据按线程安全等级分为以下5类:
1.1、不可变:final关键字修饰的变量
1.2、绝对线程安全:绝对线程安全需要满足“线程安全”的定义。在JavaAPI中标注自己是线程安全的类,大多数都不是绝对线程安全的。
1.3、相对线程安全:是指我们通常意义上讲的线程安全。我们在调用的时候不需要做额外的保障措施,但是对于一些特定顺序的连续调用,需要添加额外的同步手段来保障调用的正确性。比如Java中的HashTable等
1.4、线程兼容:对象本身并不是线程安全的,但是可以通过在调用端的正确使用同步手段来保障对象在并发环境下可以安全地使用。
1.5、线程对立:无论调用端是否采用了同步操作,都无法在多线程环境中并发使用的代码。比如Thread的suspend和resume方法。
二、线程安全的实现
2.1、同步互斥
在多个线程并发访问共享数据时,保障共享数据在同一个时刻只被一个线程使用。常见的同步互斥有:信号量、synchronized、互斥量。从处理方式来说,同步互斥是一种“悲观锁”。
2.2、非阻塞同步
随着硬件指令集的发展,可以使用基于冲突检测的乐观并发策略。即对共享数据进行操作,如果没有其他线程竞争共享数据,那操作成功;如果有冲突,就采取补偿措施(最常见的就是不断地重试)。
这种乐观锁是基于操作和冲突检测的原子性,所以是需要靠底层的硬件来完成这件事的。常见的非阻塞同步有CAS算法。
2.3、无同步方案
如果一个方法本身就不涉及共享数据,那它天然就无需任何同步措施去保证线程安全。比如以下两种情况:
2.3.1、可重入代码
在代码执行的任何时刻中断它,转而去执行另外一段代码,而在控制权返回后,原来的程序不会出现任何错误。
可重入代码有一些共同的特征,例如不依赖堆上面的数据和公共的系统资源、用到的状态量都是由参数中传入、不调用非可重用的方法等
2.3.2、线程本地存储
如果一段代码中所需要的数据必须与其他代码共享,那只要保证这些共享数据在一个线程中执行,就无需保证同步也不出现数据竞争的问题。比如消费队列的架构模式都会将消费过程尽量保证在一个线程中消费完、还有web交互模型中的“一个请求对应一个服务器线程”。我们还可以使用ThreadLocalMap对象,来实现线程本地存储的功能。
三、锁优化
HotSpot虚拟机为了提高并发效率,采用了多种锁优化技术
3.1、自旋锁
在互斥同步中,对性能影响最大的其实在于需要阻塞和唤醒其他线程,这个过程需要从用户态切换到内核态。其实在大部分场景里,一个线程只会“短暂”的占用共享数据,可以让后面请求的那个线程在不放弃处理器的情况下,“稍等一下”,看看锁是否很快会释放。自旋锁技术是指,让线程执行一个忙循环。
自适应的自旋锁会随着程序的运行和性能监控信息的完善,对程序锁的状况越来越准确,自适应为更多次数的循环或者直接放弃自旋。
3.2、锁消除
在编译运行时,对一些代码上要求同步,但是被检测到不可能存在共享数据竞争的锁进行消除。
3.3、锁粗化
如果一系列操作都对同个对象进行反复的加锁和解锁,甚至加锁操作是出现在循环体中的,频繁互斥也会导致不必要的性能消耗。当虚拟机检测到存在这样的情况时,将会加锁同步的范围扩展到整个个操作序列的外部,这样只需要加锁一次。
3.4、轻量级锁
轻量级锁是指当共享对象的Mark Word中的锁标志位显示未必锁定时,在当前线程栈帧中建立一个锁记录,并通过CAS将共享对象的Mark Word的更新为指向该锁记录。
如果发生冲突,轻量级锁就会膨胀为重量级锁,后续的线程都会进行堵塞。
轻量级锁是基于“对于大部分的锁,整个同步周期内都是不存在竞争的”的经验数据。
3.5、偏向锁
偏向锁相对于轻量级锁,是直接去掉了同步的过程,连CAS都不做了,会偏向于第一个获得它的线程。
如果发生冲突,偏向模式就宣告结束,后续的同步操作就如轻量级锁那样执行。