1、线程安全
不可变:不可变对象一定是线程安全的(即并发环境下永远不影响自身值)
绝对安全:"不管运行时环境如何,调用者都不需要任何额外的同步措施";Java API标注线程安全的类大多数不是绝对安全
相对安全:保证对这个对象单独的操作是线程安全的,调用时不需要额外保障,但对于特定顺序的连续调用,可能需要在调用段使用额外的同步手段来保证调用的正确性;如Vector、Hashtable
线程兼容:对象本身不是线程安全的,但是可以通过在调用段使用同步手段来保证对象在并发环境中可以安全地使用;如ArrayList、HashMap
线程对立:无论调用段是否采取同步措施,都无法在多线程环境中并发使用的代码;如suspend和resume、System.setIn/setOut和System.runFinalizersOnExit等
2、线程安全的实现
1.互斥同步
也叫阻塞同步,是一种悲观并发策略,主要问题是线程阻塞和唤醒所带来的性能问题;互斥是手段,同步是目的;临界区、互斥量、信号量是主要的互斥实现方式;
Java中最基本的互斥同步手段是synchronized(原生语法层面的互斥锁),会在同步块前后形成monitorenter和monitorexit字节码指令,这两个字节码指令都需要一个reference类型的参数来指明要锁定和解锁的对象;如果synchronized有对象参数,那就是这个reference,否则就是synchronized修饰的方法对象实例或Class对象;monitorenter将锁计数器加1,monitorexit将锁计数器减一;monitorenter可重入,不会出现自己锁死自己,同步块在已进入的线程执行完之前,会阻塞后面线程的进入;阻塞或唤醒一个线程都需要切换到内核态,由操作系统来完成,因此synchronized是一个重量级操作,目前也进行了一些优化,如通知操作系统阻塞线程之前加入一段自旋等待过程,防止频繁切入内核态
ReentrantLock是API层面的互斥锁(通过lock、unlock配合try/finally完成);高级特性:等待可中断、公平锁、锁可以绑定多个条件;等待可中断是指当持有锁的线程长期不释放时,等待线程可以选择放弃等待,该特性对处理执行时间非常长的同步块很有帮助;公平锁是多个线程等待同一个锁时,必须按照申请锁的时间顺序依次获得锁,synchronized和ReentranLock默认都是非公平锁;锁绑定多个条件是指一个ReentrantLock对象可以同时绑定多个Condition对象,synchronized中锁对象要和多个条件关联时需要额外添加锁,ReentrantLock只需要多次调用newCondition即可
2.非阻塞同步
一种乐观并发策略,不需要挂起线程;先操作,如果没冲突就成功,有冲突则采取补偿措施(如不断重试直到成功为止);需要硬件指令集来保证操作和冲突检测两个步骤的原子性,这些指令常用有:测试并设置、获取并增加、交换、比较并交换(CAS)、加载链接/条件存储(LL/SC);JDK1.5开始使用CAS操作,由sun.misc.Unsafe类的compareAndSwapInt/compareAndSwapLong等方法包装提供;Unsafe只允许BootStrap ClassLoader加载的类访问,只能通过反射或其他Java API(如AtomicXXX类)间接使用;CAS存在ABA问题,目前concurrent包提供了原子引用类"AtomicStampedReference"来检测变量值版本,目前比较“鸡肋”,大部分ABA不影响并发正确性,如果需要解决不如改用传统的互斥同步
3、自旋锁和自适应自旋
为了让线程等待,让线程执行一个忙循环(自旋),这项技术就是自旋锁。JDK1.6默认开始自旋锁(-XX:+UseSpinning),自旋等待不能代替阻塞,自旋避免了线程切换,但是要占用处理器时间的,因此如果锁被占用时间很短,自旋等待效果会非常好,如果锁被占用时间很长,自旋等待只会白白浪费处理器资源,因此自旋等待时间必须有一定的限度,自旋次数默认10次(-XX:PreBlockSpin)。JDK1.6引入了自适应的自旋锁,自适应因为自旋的时间不再固定,而是由前一次在同一个锁上的自旋时间及锁拥有者的状态来决定。如果在同一锁对象上,自旋等待刚刚成功获得锁并且持有锁的线程正在运行,那么JVM认为这次自旋也很有可能再次成功,将允许自旋等待持续相对更长的时间,比如100个循环。如果对于某个锁,自旋很少成功,那么以后获取这个锁时将可能省略掉自旋过程
4、锁消除
锁消除是指JVM即时编译器运行时,对一些代码上要求同步,但是被检测到不可能存在共享数据竞争的锁进行消除。锁消除的主要判定依据来源于逃逸分析的数据支持,如果判定堆上所有数据不会逃逸出去被其他线程访问到,就可以把它当做栈上数据对待,认为是线程私有从而无须同步加锁。如return str1+str2+str3在JDK1.5之前会编译成StringBuffer.append(str1).append(str2).append(str3),append内部有同步块,JVM在检测到StringBuffer对象不会发生逃逸,这段代码就会直接执行而忽略所有同步
5、锁粗化
如果JVM检测到有一串零碎的操作对同一个对象加锁,将会把锁同步的范围扩展到这个操作序列外部。如一系列连续操作对同一个对象反复加锁和解锁,甚至加锁操作出现在循环体中,那么即使没有线程竞争,频繁地进行互斥同步也会造成不必要的性能损耗。如锁消除示例,JVM会把锁同步范围扩展到第一个append之前和最后一个append之后,只需加锁一次
6、轻量级锁
目的是在没有多线程竞争的前提下,减少传统重量级锁使用操作系统互斥量产生的性能消耗。MarkWord是实现轻量级锁和偏向锁的关键。轻量级锁提升性能的依据是“对于绝大部分锁,在整个同步周期内都是不存在竞争的”,这是经验数据,如果没有竞争,轻量级锁使用CAS避免了使用互斥量的开销,如果存在竞争,除了互斥量的开销还额外发生了CAS,此时轻量级锁比重量级锁更慢;加锁,代码进入同步快时对象没有被锁定(锁标志位01),JVM在栈帧中建立一个名为锁记录的空间(存储锁对象目前的MarkWord拷贝,Displaced Mark Word),然后JVM使用CAS尝试将对象的MarkWord更新为执行锁记录的指针,成功则线程拥有了锁(锁标志位00),失败则JVM首先检查对象的MarkWord是否指向当前线程的锁,是则说明当前线程拥有该对象锁,进入代码块继续执行,否则说明该对象锁已被其他线程抢占。如果有两条以上的线程争用同一个锁,轻量级锁膨胀为重量级锁(锁标志位10),MarkWord中存储的就是重量级锁(互斥量)的指针,后续等待锁线程进入阻塞状态;解锁,也是通过CAS操作进行,如果对象MarkWord仍然指向线程锁记录,那就用CAS操作将对象当前的MarkWord和线程中复制的Displaced Mark Word替换回来,成功则同步过程结束,失败说明有其他线程尝试过获取该锁,那就要在释放的同时唤醒被挂起的线程
7、偏向锁
目的是在无竞争情况下消除数据同步原语,进一步提高程序的运行性能。如果说轻量级锁是在无竞争下使用CAS操作消除同步使用的互斥量,那么偏向锁就是在无竞争情况下把整个同步都消除掉,连CAS都不做。偏向锁就是偏向于第一个获取它的线程,如果在接下来的执行过程中锁没有被其他线程获取,则持有偏向锁的线程永远不需要同步。JDK1.6默认开启偏向锁(-XX:+UseBiasedLocking),当锁对象第一次被线程获取时,JVM会把对象头中的锁标志位设为"01"(偏向模式),同时使用CAS操作把获取到这个锁的线程ID记录在对象的MarkWord中,如果CAS成功则持有偏向锁的线程以后每次进入这个锁相关的同步块时JVM都可以不再进行任何同步(如Locking、Unlocking以及对MarkWord的Update等)。当有其他线程尝试获取锁时,偏向模式结束,根据锁对象目前是否处于被锁定状态,撤销偏向后恢复到未锁定(锁标志位01)或轻量级锁(锁标志位00)。偏向锁可以提高带有同步但无竞争的程序性能,但是如果程序中大多数锁总是被多个线程访问,那偏向模式就是多余的,有时候禁止偏向锁反而更能提高性能