线程安全与优化
本文为《深入理解Java虚拟机_JVM高级特性与最佳实践·周志明》学习笔记
文章目录
线程安全
概念:当多个线程同时访问一个对象时,设计者和调用者不考虑这些线程在运行环境下的调度和交替执行,也不需要进行额外的同步,或者在调用方进行任何其他的协调操作,调用这个对象的行为都可以获得正确的结果,那么就称这个对象是线程安全的;
共享数据分类
根据线程安全程度分类:不可变,绝对线程安全,相对线程安全,线程兼容,线程对立;
1. 不可变
不可变对象:无需任何线程安全保障措施,其状态永远不会改变,不会在多线程中处于状态不一致的情况;一定是线程安全的;
基本数据类型可以使用final进行修饰,其余类型暂不支持;
举例:String,枚举,以及Number的部分子类;
2. 绝对线程安全
概念就是线程安全的概念;当多个线程同时访问一个对象时,设计者和调用者不考虑这些线程在运行环境下的调度和交替执行,也不需要进行额外的同步,或者在调用方进行任何其他的协调操作,调用这个对象的行为都可以获得正确的结果,那么就称这个对象是线程安全的;
举例:木有,JavaApi标注的都不是绝对安全的;
3. 相对线程安全
对这个单次的操作是线程安全的,我们在调用的时候不需要进行额外的保障措施,特别是对于一些特定顺序的连续调用,就可能需要调用者自行使用同步手段保证调用的正确性;
举例:Vector,HashTable,Collections的synchronizedCollection()方法包装的集合等;
4. 线程兼容
本身不是线程安全的,但可以通过在调用端正确地使用同步手段来保证对象在并发环境中可以安全地使用。
线程对立
5. 线程对立
不管是否采用了同步措施,都无法在多线程中并发使用代码。
举例:废弃的中断线程,恢复线程方法
Thread::suspend(),Thread::resume()
线程安全的实现方法
1. 互斥同步
同步是指多线程并发访问共享数据时,保证共享数据在同一时刻只被一条线程使用。互斥是实现同步的一种手段;
策略:悲观锁(无论是否出现竞争都会加锁,但JVM会优化掉一部分);
面临主要问题:进行线程阻塞和唤醒所带来的性能开销,这种同步也叫阻塞同步;
互斥的方式:临界区,互斥量,信号量等;
synchronized
:java中最基本的互斥同步手段。javac
编译后,会在同步块前后形成monitorenter
和monitorexit
这两个字节码指令;如果synchronized
指明了对象参数,那就锁定这个对象;如果未指定对象参数,则根据其修饰的方法类型(实例方法,或类方法)来决定是取代码所在的对象实例还是取类型对应的Class对象来作为线程要持有的锁。
synchronized
修饰的特点:
- 对同一条线程来说是可重入的,同一线程反复进入同步块也不会出现自己把自己锁死的情况。
- 在持有锁的线程执行完毕并释放锁之前,会无条件地阻塞后面其他线程的进入;
- 重量级操作;
- 非公平锁;
Lock
:java中最基本的互斥同步手段。用户能够以非块结构实现互斥同步。但需要手动释放锁。
重入锁(ReentrantLock
):是Lock接口的实现,可实现可等待中断,公平锁,绑定多个条件;
- 等待可中断:当持锁线程长期不释放锁时,正在等待的线程可以选择放弃等待,改为处理其他事情;
- 公平锁:多个线程在等待同一锁时,必须按照申请锁的时间顺序来依次获得锁;
- 锁绑定多个条件:可以同时绑定多个Condition对象,多次调用newCondition()方法即可;
2. 非阻塞同步
策略:乐观锁(不管是否有风险,先进行操作,无冲突则成功,有冲突则补偿,通常是不断重试);
优势:不再需要把线程阻塞挂起,可叫非阻塞同步;
背景:依赖硬件指令集的发展;因为我们必须要求操作和冲突检测这两个步骤具备原子性。常见的多次操作行为只通过一条的指令有:
- 测试并设置(Test-and-Set)
- 获取并增加(Fetch-and-Increment)
- 交换(Swap)
- 比较并交换(Compare-and-Swap,CAS)
- 加载链接/条件存储(Load-Linked/Store-Conditional, LL/SC)
CAS
指令:需要3个操作数,分别是内存位置V,旧的预期值A,准备设置的新值B。CAS执行时,当地址V对应的旧值是A时,处理器才会将V对应的值更新为B,否则就不执行更新。该操作为原子操作,不会被其他线程中断;
CAS
问题:存在ABA问题,如果一个变量V初次读取的时候是A值,并且在准备赋值的时候检查到它仍然为A值,不能说明它的值没有被其他线程改变过。因为可能他在这段期间被改为了B后来又改回A。如果遇到此问题,使用互斥同步来处理;
3. 无同步方案
同步和线程安全并非是必须的,也有一些代码本身就是线程安全的;
可重入代码:
又称为纯代码,可以在代码执行的任何时刻中断它,转而执行其他代码,而在控制权返回后,原来的程序不会出现任何错误,其结果也不会产生影响。意味着可重入代码是线程安全的。
特征:不依赖全局变量、存储在对上的数据和公用的系统资源。用到的状态量都是由参数中传入,不调用非可重入的方法等。
线程本地存储:
如果一段代码中所需要的数据必须与其他代码共享,那就看看这些共享数据的代码是否能保证在同一个线程中执行,如果能保证,我们就可以把共享数据的可见范围限制在同一线程中,避免竞争。
ThreadLocal
:每一个线程的Thread对象中都有一个ThreadLocalMap
对象,这个对象存储了一组以ThreadLocal.threadLocalHashCode
为键,以本地线程变量为值的K-V对,ThreadLocal
对象就是当前线程的ThreadLocalMap
的访问入口,可以通过这个值获取本地线程变量。