线程安全


线程安全的定义

当多个线程同时访问一个对象时,如果不用考虑这些线程在运行时环境下的调度和交替执行,也不需要进行额外的同步,或者在调用方进行任何其他的协调操作,调用这个对象的行为都可以获得正确的结果,那么就称这个对象是线程安全的。

final修饰的变量只要成功的创建出来,那么外部的可见状态永远不会变。“不可变”带来的安全性是最直接、最纯粹的。
Java中,对于基本数据类型,只要在定义的时候用final进行修饰就可以保证它是不可变的。对于对象数据,就需要对象自身保证其行为不会对他的状态产生影响。如String类的对象实例,它是一个典型的不可变对象,用户调用它的substring(),replace()和concat()这些方法都不会影响其原来的值,只会返回一个新构造的字符串对象。

线程安全的实现方法

互斥同步

如Synchronized是典型的互斥同步,同步是指在多个线程并发访问共享数据时,保证共享数据在同一个时刻只被一条线程使用。
互斥是实现同步的手段,临界区、互斥量、信号量都是常见的互斥实现方式。
互斥是因,同步是果;互斥是方法,同步是目的。
Synchronized关键字在经过javac编译后,会在同步代码块前后分别形成一个 monitorenter 和 monitorexit 这两个字节码指令。这两个字节码都需要一个reference 类型的参数来指明要锁定和解除的对象。如果明确指定了对象参数,那就以这个对象的引用作为reference;如果没有明确指定,就根据Synchronized修饰的实例方法或类,来决定是什么来持有这把锁。

重入锁 (ReentrantLock)是Lock接口最常见的一种,与Synchronized一样是可重入的。

可重入指一条线程能够反复进入被它自己持有锁的同步代码块的特性,即锁关联计数器,如果持有锁的线程再次获得它,计数器加1,每次释放锁计数器减1,当计数器归零,才能真正的释放锁。

ReentrantLock与Synchronized的区别

ReentrantLock相比较Synchronized而言增加了一些高级功能,主要为:等待可中断、可实现公平锁、可以绑定多个条件。

  • 等待可中断 :当持有锁的线程尝试不释放锁的时候,正在等待的线程可以去处理其他事情
  • 公平锁 :指等待锁的多个线程必须要按照申请锁的顺序依次来获取锁。非公平锁就不能保证这一点,非公平锁在释放锁时,等待的线程会进行重新的锁资源竞争。Synchronized就是非公平的,ReentrantLock默认也是非公平的,但是可以通过布尔值构造函数要求使用公平锁,不过这样会造成它的性能降低,吞吐量下降。
  • 绑定多个条件 :指一个ReentrantLock可以同时绑定多个condition对象。在Synchronized中,锁对象的wait要跟notify、或者notifyAll配合使用才行。
  • Synchronized是在语法层面的同步,足够清晰简单
  • Lock要确保在finally中释放锁,否则可能永远不会释放这把锁
  • java虚拟机更容易对Synchronized进行优化,因为java虚拟机可以在线程和对象的元数据中记录Synchronized中的锁信息,而使用Lock的话,java虚拟机很难得知具体是哪些对象是由特定线程持有的。

非阻塞同步

常见的比如CAS compare and swap
CAS(V,A,B)
V:变量的内存地址 或者要进行替换的值
A:旧的预期值
B: 准备设置的新值
当 V 等于A时,才会将用B替换V,不管是否进行了替换,都会返回V的旧值。这一系列操作都是原子性的操作,执行期间不会被线程中断。

无同步方案

volatile:修饰的变量表示这个变量是可变的,当一个线程修改了被volatile修饰的变量后,会将修改后的数据强制刷到内存,并将其他引用该变量的线程置为无效,使它们从内存中获取最新的值。
ThreadLocal :每一个thread都可以拥有一个ThreadLocalMap对象,这个对象存储了一组以ThreadLocal.threadLocalHashCode为键,以本地线程变量为值的K-V键值对,ThreadLocal对象就是当前线程的ThreadLocalMap的访问入口,每一个ThreadLocal对象都包含了一个独一无二的ThreadLocalHashCode值,这个值可以在线程K-V中找到对应的本地线程变量。