《深入理解Java虚拟机》第十三章
当多个线程访问一个对象时,如果不用考虑这些线程在运行时环境下的调度和交替执行,也不需要进行额外的同步,或者在调用方进行任何其他的协调操作,调用这个对象的行为都可以获得正确的结果,那这个对象是线程安全的。
线程安全强度排序:不可变,绝对线程安全,相对线程安全,线程兼容,线程对立。
- 不可变:不可变的对象一定是线程安全的,无论是对象的方法实现还是方法的调用者,都不需要采取任何保障线程安全的措施,String和八大基础类型的包装类(没有变量的类,有变量必须final修饰的基础类型或者final修饰的不可变类)
- 绝对线程安全:不管运行时环境如何,调用者都不需要任何额外的同步措施。
- 相对线程安全:对象线程安全,但调用时需要额外的同步措施,这个类型就是常说的线程安全,Java中大部分的线程安全类都属于这种类型,Vector,HashTable,Collections,synchronizeCollection()方法包装的集合。
- 线程兼容:对象线程不安全,调用时采取正确的同步手段,使其线程安全
- 线程对立:不管采取怎样的同步措施,都不能保证线程安全。
线程安全的实现方法
- 互斥同步
- 非阻塞同步
- 无同步
互斥同步:多个线程访问共享数据时,同一时刻共享数据仅被一个线程使用,其他线程阻塞。
重量级锁synchronize,经过编译后代码前后monitorenter,monitorexit两个字节码指令(计数器),两个字节码指令都需要一个refrence类型的参数指明要锁定和解锁的对象。
synchronize:有明确指定对象参数,就是指定的参数对应的对象,
没有明确指定参数(即用在方法上)时,实例方法锁定实例对象,类方法锁定类对象。
synchronize:锁定对象对同一线程是可以重入的,不会出现自己把自己锁死的问题,只会阻塞其他线程。
synchronize导致的问题:如果阻塞或唤醒一个线程,都需要操作系统来帮忙完成就会在用户态和核心态频繁切换,效率低,优化:通知操作系统阻塞时自旋等待,避免进入核心态。
除synchronize外,重入锁ReentrantLock也可以实现同步。利用Lock类实现的。
ReentrantLock增加了一些高级功能:
- 等待可中断 :等待时间过长,可选择放弃,改处理其他事情。
- 可实现公平锁:多个线程在等待同一个锁时,必须按照申请锁的时间顺序来依次获得锁。默认是非公平的。
- 锁可以绑定多个条件
未来的jdk改进会偏向于原生的synchronize
非阻塞同步:基于冲突检测乐观并发策略,先进行操作,如果没有其他线程争用共享数据,操作成功。有争取,产生了冲突,采取补偿措施(最常见的补偿措施:不断重试,知道成功为止)。
乐观并发策略需要“硬件指令集的发展”才能进行,操作+冲突检测的原子性
一条处理器指令就能完成操作
测试并设置(Test-and-Set)
获取并增加(Fetch-and-Increment)
交换(Swap)
比较并交换(Compare-and-Swap,CAS)
加载链接、条件存储(Load-Linked/Store-Conditional,LL、SC)
Jdk1.5之后,java程序中才可以使用CAS操作,sun.misc.Unsafe类中compareAndSwapInt和compareAndSwapLong等方法包装提供。
Unsafe类仅启动类加载器加载的类才能执行,用户程序不能直接调用,可用反射,不然只能通过java.util.concurrent来间接调用。
while(true){ int current = get(); int next = current + 1; if(compareAndSet(current,next)){ return next } }
CAS操作的ABA问题:A-->B-->A,认定变量没有被线程改变过
无同步方案
可重入代码:纯代码,每次相同输入,返回结果相同。
线程本地存储 : 共享数据的代码保证在同一线程中执行,可把共享数据可见范围限制在同一个线程内,ThreadLocal的ThreadLocalMap
锁优化
- 自旋锁自适应自旋:互斥同步时,等待线程继续消耗处理器执行时间,防止进入核心态。但等待时间长,会浪费处理器性能。
- 锁消除:虚拟机即时编译器在运行时,对代码上同步,但检测到不可能存在共享数据竞争的锁进行消除。
- 锁粗化:虚拟机检测,同步块锁定同一对象,多且零碎。优化到整个操作序列外面。
- 轻量级锁:没有多线程竞争的前提下,减少传统的重量级锁所使用操作系统互斥量产生的性能消耗。两条线程争用锁,无用
- 偏向锁:消除数据在无竞争情况下的同步原语,进一步提高程序的运行性能。单线程每次获取资源需要同步锁,偏向锁就是第一次获取偏向锁,后续如果没有其他线程同步锁,这个线程后续请求资源不需要重新获取锁