以下blog内容来自《深入理解Java虚拟机_JVM高级特性与最佳实践》感谢作者。
线程安全的概念在书中作者讨论了很多,但都是比较抽象的定义,我所理解的线程安全(主要是对共享数据的操作,保证代码操作的正确性,就是无论在单线程还是多线程操作下,代码得到的结果都是正确的)。
一个不可变的对象(类似final)一定是线程安全的。
如何理解呢?
java中如果共享数据是基本类型,定义是利用final修饰就可保证不变性,
如果共享数据是对象,就要类似String类,它是一个不可变的对象,所有的操作都是重新生成一个对象而不是在原有对象上操作。
java API中的不可变类型:String ,Enum,Long,Double,BigInteger,BigDecimal。
这是一个很难达到的条件,就是一个对象不管在任何条件下不需要任何的同步条件都是线程安全的,即时类似Vector这种所有的方法都添加了synchronized关键字的对象也不能保证(需要在方法外添加额外的同步)。
相对线程安全首先要保证对象单独的操作是线程安全的,一些特定顺序的连续调用时需要添加额外的同步代码保证线程安全,例如类中操作是线程安全的,但是连续的特定顺序调用需要添加额外同步代码保证线程安全(Vector,HashTable,Collections的synchronizedCollection()方法)。
线程兼容指对象本身并不是线程安全的,但是可以通过调用端正确的使用同步手段来保证线程安全,编程过程中遇到的绝大多数情况。
多线程环境下无法使用的代码,相当少见,类似开启一个线程就会关闭另外一个线程。
利用互斥保证同步(线程安全),同步指多线程并发访问共享数据时,保证共享数据在同一时刻只能被一个线程使用,临界区、互斥量、信号量都是主要的实现方式。互斥同步是阻塞同步的方案,因为一个线程工作会导致另外一个线程阻塞。
java中主要的互斥同步主要利用synchronized关键字和ReentrantLock锁实现。
synchronized利用添加monitorenter和monitorexit字节码指令实现(原理会在以后文章分析),
ReentrantLock则是语法层面的实现方式,利用lock和unlock实现同步。
互斥同步是阻塞同步,线程需要不停地阻塞唤醒,有时会带来性能问题,使用中悲观的并发策略,总认为会发生竞争,无论数据是否发生竞争都会加锁。
非阻塞同步是一种基于冲突检测的乐观并发策略,通常的方案就是先进行操作,如果共享数据没有其他线程争抢,操作成功,否则就一直等待重试直到成功,而不是阻塞线程(CAS)。(这里也有效率问题,如果多个线程重试,一直没有成功也很浪费资源)。
如果一些代码不涉及共享数据,也就不需要同步方案就能保证同步。
例子
可重入代码:可以在代码执行的任何时刻打断,然后回来执行不会出错。
线程本地存储,ThreadLocal
下面简单介绍锁优化,后续看完书之后会再进行总结。
互斥同步方案由于线程状态切换导致性能问题,非阻塞方案大量线程一直等待也会造成资源消耗,需要对各种锁进行优化,提高并发效率。
所谓自旋就是执行一个空的循环操作,如果自旋占用时间很短就成功执行效果很好,否则很浪费资源。
可以设置自旋的最大次数或者线程能够自己根据状态来控制自旋的次数。
编译期编译过程中分析到不存在共享数据竞争,可以把锁消除掉。
写代码过程中总是要求我们尽量缩小锁的范围这样可以提高效率,但有时如果再循环体中加锁,或者循环对同一个对象进行加锁反而会造成不必要的性能损耗,此时扩大锁的范围可以减轻这种消耗。
偏向锁基于“锁不仅不存在多线程竞争,而且总是由统一线程多次获得”,而线程的阻塞和唤醒需要CPU从用户态转为核心态,频繁的阻塞和唤醒对CPU来说是一件负担很重的工作,为了让线程获得锁的代驾更低而引入了偏向锁。
偏向锁获得锁的过程分为以下几步:
1)初始时对象的Mark Word位为1,表示对象处于可偏向的状态,并且ThreadId为0,这是该对象是biasable&unbiased状态,可以加上偏向锁进入2)。如果一个线程试图锁住biasable&biased并且ThreadID不等于自己ID的时候,由于锁竞争应该直接进入4)撤销偏向锁。
2)线程尝试用CAS将自己的ThreadID放置到Mark Word中相应的位置,如果CAS操作成功进入到3),否则进入4)
3)进入到这一步代表当前没有锁竞争,Object继续保持biasable状态,但此时ThreadID已经不为0了,对象处于biasable&biased状态
4)当线程执行CAS失败,表示另一个线程当前正在竞争该对象上的锁。当到达全局安全点时(cpu没有正在执行的字节)获得偏向锁的线程将被挂起,撤销偏向(偏向位置0),如果这个线程已经死了,则把对象恢复到未锁定状态(标志位改为01),如果线程还活着,则把偏向锁置0,变成轻量级锁(标志位改为00),释放被阻塞的线程,进入到轻量级锁的执行路径中,同时被撤销偏向锁的线程继续往下执行。
5)运行同步代码块
此处用到对象头的知识查看对象头
如果说偏向锁是只允许一个线程获得锁,那么轻量级锁就是允许多个线程获得锁,但是只允许他们顺序拿锁,不允许出现竞争,也就是拿锁失败的情况,轻量级锁的步骤如下:
1)线程1在执行同步代码块之前,JVM会先在当前线程的栈帧中创建一个空间用来存储锁记录,然后再把对象头中的Mark Word复制到该锁记录中,官方称之为Displaced Mark Word。然后线程尝试使用CAS将对象头中的Mark Word 替换为指向锁记录的指针。如果成功,则获得锁,进入步骤3)。如果失败执行步骤2)
2)线程自旋,自旋成功则获得锁,进入步骤3)。自旋失败,则膨胀成为重量级锁,并把锁标志位变为10,线程阻塞进入步骤3)
3)锁的持有线程执行同步代码,执行完CAS替换Mark Word成功释放锁,如果CAS成功则流程结束,CAS失败执行步骤4)
4)CAS执行失败说明期间有线程尝试获得锁并自旋失败,轻量级锁升级为了重量级锁,此时释放锁之后,还要唤醒等待的线程