线程安全实现方式

互斥同步

同步是指在多个线程并发访问共享数据时,保证共享数据在同一个时刻只被一个线程使用(或者是一些,使用信号量的时候),互斥是实行同步的一种手段,临界区、互斥量和信号量都是主要的互斥实现方式。互斥是因,同步是果,互斥是方法,同步是目的。

Java中最基本的互斥同步手段是使用synchronized关键字,synchronized关键字编译后,会在同步代码块前后分别形成monitorentermonitorexit字节码指令,这两个字节码都需要一个reference类型的参数来指明要锁定和解锁的对象。若synchronized明确指定了对象参数,那就是这个对象的reference,若没有明确指定,就根据synchronized修饰的是实例方法还是类方法,则取对应的对象实例Class对象来作为锁对象。

虚拟机规范要求,执行monitorenter指令时,首先尝试获取对象的锁,若对象没有被锁定或当前线程已经拥有这个对象的锁,将锁的计数器加一,执行monitorexit指令时将锁计数器减一,当计数器为零时锁被释放。若获取对象锁失败,当前线程阻塞等待,直到对象锁被另一个线程释放。

synchronized同步块对同一条线程是可重入的,不会将自己死锁。同步块在已进入的线程执行完成之前,会阻塞后面其他线程的进入。Java线程是映射到操作系统原生线程上的,阻塞或唤醒线程都需要操作系统帮忙,需要从用户状态转换到核心态中,因此转态转换需要耗费很多处理器时间

虚拟机自身进行了一些优化,在通知操作系统阻塞线程之前加入一段自旋等待过程,避免频繁地切入到核心态。

除了synchronized关键字外还可以使用java.lang.concurrent包中的重入锁ReentrantLock来实现同步,基本用法上ReentrantLocksynchronized类似都具备一样的线程重入特性,只是写法上有些许区别,ReentrantLock表现为API层面的互斥锁lock()unlock()方法配合try/finally语句来完成),而synchronized表现为原生语法层面的互斥锁。相比synchronizedReentrantLock增加了等待可中断公平锁锁绑定多个条件

  • 等待可中断:指当持有锁的线程长期不释放时,正在等待的线程可选择放弃等待,改为处理其他事情;
  • 公平锁:指多个线程在等待同一个锁时,必须按照申请锁的时间顺序来依次获得锁,反之则为非公平锁;synchronized是非公平锁,ReentrantLock默认也是非公平锁,但可通过带布尔值构造函数要求使用公平锁。
  • 锁绑定多个条件:指ReentrantLock对象能同时绑定多个Condition对象,在synchronized中所对象的wait()notify()notifyAll()方法能实现一个隐含条件,若与多于一个关联条件需要额外添加锁,而ReentrantLock只需要多次调用newCondition()方法。

JDK1.5下synchronized吞吐量下降非常严重,而ReentrantLock能基本保持在用一个比较稳定的水平。JDK1.6及之后synchronizedReentrantLock性能基本上完全持平,提倡优先考虑使用synchronized进行同步。

互斥同步最主要的问题是进行线程阻塞和唤醒所带来的性能问题,因此也称为阻塞同步互斥同步属于一种悲观并发策略

非阻塞同步

非阻塞同步是一种基于冲突检测的乐观并发策略,先进行操作,若没有其他线程争用共享数据操作成功,若共享数据有争用产生了冲突,再采取其他补偿措施,最常见的补偿措施就是不断重试直到成功。但乐观并发策略需要硬件指令集的发展才能进行,因为操作冲突检测两个步骤具备原子性需要硬件来保证。常用硬件指令有:

  • 测试并设置(Test-and-Set)
  • 获取并增加(Fetch-and-Increment)
  • 交换(Swap)
  • 比较并交换(Compare-and-Swap,CAS
  • 加载链接/条件存储(Load-Linked/Store-Conditional,LL/SC)

CAS指令需要三个操作,分别是内存位置旧预期值新值CAS指令在执行时,当且仅当内存位置符合旧预期值时,处理器用新值更新内存位置的值,否则不执行更新,但无论是否更新内存位置,都返回内存位置的旧值,该处理过程是一个原子操作。

JDK1.5后才能使用CAS操作,该操作由sun.misc.Unsafe类中的compareAndSwapInt()compareAndSwapLong()等几个方法包装提供,虚拟机内部对这些方法做了特殊处理,即时编译出来的结果是一条平台相关的处理器CAS指令,没有方法调用过程。

Unsafe类不提供给用户程序调用,若不采用反射只能通过其他Java API间接使用,如J.U.C包中的整数原子类,其中的compareAndSet()compareAndIncrement()等方法都使用了Unsafe类的CAS操作。

CAS并不完美,存在一个ABA问题的逻辑漏洞J.U.C包为了解决该问题,提供了一个带标记的原子引用类AtomicStampedReference通过控制变量值的版本来保证CAS正确性。但比较鸡肋,ABA问题大部分情况不会影响正确性,若要解决ABA问题用互斥同步可能更高效。

无同步方案

要保证线程安全,并不是一定要进行同步,两者没有因果关系。

可重入代码也加纯代码,能在代码执行的任何时刻中断,转而执行另一段代码,包括递归调用其本身,在控制权返回后原来的程序不会出现任何错误。所有的可重入代码都是线程安全的,相对线程安全来说,可重入性是更根本的特性,但并非所有线程安全的代码都是可重入的

可重入代码共性:不依赖存储在堆上的数据和公用的系统资源、用到的状态量都是由参数传入、不调用非可重入方法等。

可重入性简单判定原则:若一个方法返回结果可预测,输入相同的数据就能返回相同的结果。

线程本地存储,一段代码所需要的数据必须与其他代码共享,且共享数据的代码能保证在同一个线程中执行,能把共享数据的可见范围限制在同一个线程内。

常见的符合线程本地存储的条件的有:大部分使用消费队列的架构模式、web交互模型中的一个请求对应一个服务器线程。

若变量要被多个线程访问,可使用volatile关键字声明为易变的,若只被某个线程独享,可使用java.lang.ThreadLocal类来实现线程本地存储功能,每个线程的Thread对象中都有一个ThreadLocalMap对象。