本片文章介绍线程安全所涉及的概念和分类,同步实现的方式及虚拟机的底层运作原理,以及虚拟机为了实现高效并发锁采取的一系列优化措施.

1.1 线程安全

当多个进程访问一个对象时,如果不用考虑这些线程在运行时环境下的调度和交替执行,也不需要进行额外的同步,调用这个对象的行为都可以获得正确的结果,那这个对象是线程安全的.

1.1.1 Java 语言中的线程安全

  1. 不可变

在 Java 语言中,不可变的对象一定是线程安全的.如果共享的是一个基本数据类型,只需要在定义时使用 final 关键字修饰.如果共享的是一个对象,就需要保证对象的行为不会对其状态产生影响,最简单的方法是将对象中带有状态的变量都声明为 final.

  1. 绝对线程安全

不管运行时环境如何,调用者都不需要任何额外的同步措施.但是这样需要付出很大的代价.在 Java API 中标注自己是线程安全的类,大多数都不是绝对的线程安全.

  1. 相对线程安全

相对线程安全就是我们通常意义上的线程安全,它需要保证对这个对象单独的操作是线程安全的,我们在调用的时候不需要做额外的保障措施,但是对于一些特定顺序的连续调用,就需要使用额外的同步手段保证正确性.

  1. 线程兼容

线程兼容是指对象本身不是线程安全的,但是可以通过使用同步手段来保证对象在并发环境可以安全使用.

  1. 线程对立

线程对立是指无论调用段是否采取了同步措施,都无法在多线程中并发使用的代码.

1.1.2 线程安全的实现方法

  1. 互斥同步

同步是指多个线程并发访问共享数据时,保证共享数据只在同一时刻只被一个线程使用.互斥是实现同步的一种手段.
在 Java 中实现互斥的手段就是 synchronized 关键字.同步快在已进入的线程执行完之前,会阻塞后面其他线程的进入,而阻塞或者唤醒一个线程,需要操作系统来帮忙完成,就需要从用户态转换到核心态中,状态转换消耗的时间有可能比用户执行代码的时间要长,所以 synchronized 是 java 语言中一个重量级操作.

  1. 非阻塞同步

先进行操作,如果没有其他线程争用共享数据,就操作成功.如果共享数据有争用,产生了冲突,就采取其他的补偿措施(不断重试,直到成功).这种策略不需要把线程挂起,因此这种同步操作成为非阻塞同步.

  1. 无同步方案

如果一个方法本来就不涉及共享数据,那它自然就无需任何同步措施去保证正确性,因此有一些代码天生就是线程安全的.

  • 可重入代码

在代码执行的任何时刻中断它,转而去执行另外的一段代码,而在控制权返回后,原来的程序不会出现任何错误.
如果一个方法,它的返回结果是可预测的,只要输入了相同的数据,就能返回相同的结果,那它就满足可重入性的要求,也就是线程安全的.

  • 线程本地存储

通过ThreadLocal 类来实现线程本地存储的功能,每一个线程中都有一个 ThreadLocalMap 对象,这个对象只能在当前线程访问.

1.2 锁优化

在 JDK1.6版本, HotSpot 虚拟机开发团队花费了大量的经理去实现各种锁优化,如自适应自旋,锁消除,锁粗化,轻量级锁和偏向锁等.

1.2.1 自旋锁与自适应自旋

在互斥同步中,对性能影响最大的是阻塞的实现,挂起和恢复线程都需要转入内核中完成,这些操作都给系统带来压力.
当产生多个线程同时执行时,就可以让后面请求锁的线程”稍微等一下”,但不放弃处理器的执行时间,为了让线程等待,只需要让线程执行一个忙循环,这项技术就是自旋锁.如果锁被占用的时间段,自旋等待效果会很好,反之自旋的线程只会白白消耗处理器资源.
JDK1.6 中引入了自适应的自旋锁.意味着自旋的时间不再固定了,而是由前一次在同一个锁的自旋时间及锁的拥有者的状态决定.

1.2.2 锁消除

锁消除是指虚拟机在及时编译器运行时,对一些代码上要求同步,但是被检测到不可能存在共享数据竞争的锁进行消除.
锁消除的主要判断依据来源于逃逸分析的数据支持,如果判断在一段代码中,堆上的所有数据都不会被逃逸出去而被其他线程访问到,那就可以把他们当做栈上数据对待,认为他们是私有的,同步加锁就无须进行.

1.2.3 锁粗化

当多个彼此靠近的同步块可以合并到一起,形成一个大的同步快时,就会进行锁粗化.

1.2.3 轻量级锁

在没有竞争的情况下,轻量级锁使用 CAS 操作避免了使用互斥量的开销.

1.2.4 偏向锁

偏向锁的目的是消除数据在无竞争情况下的同步原语,进一步提高程序性能.