1. 互斥同步
互斥同步(MutualExclusion&Synchronization)是一种最常见也是最主要的并发正确性保障手段。同步是指在多个线程并发访问共享数据时,保证共享数据在同一个时刻只被一条(或者是一些,当使用信号量的时候)线程使用。而互斥是实现同步的一种手段,临界区(Critical Section)、互斥量(Mutex)和信号量( Semaphore)都是常见的互斥实现方式。因此在“互斥同步”这四个字里面,互斥是因,同步是果;互斥是方法,同步是目的。
其他名称:阻塞同步、悲观锁。面临的问题主要是进行线程阻塞和唤醒所带来的性能开销。
在Java里面,最基本的互斥同步手段就是synchronized关键字,这是一种块结构(Block Structured)的同步语法。
Synchronized内部结构
synchronized关键字经过Javac编译之后,会在同步块的前后分别形成monitorenter和monitorexit这两个字节码指令。这两个字节码指令都需要一个reference类型的参数来指明要锁定和解锁的对象。如果Java源码中的synchronized明确指定了对象参数,那就以这个对象的引用作为reference;如果没有明确指定,那将根据synchronized修饰的方法类型(如实例方法或类方法),来决定是取代码所在的对象实例还是取类型对应的Class对象来作为线程要持有的锁。
Synchronized原理和实现过程
根据《Java虚拟机规范》的要求,在执行monitorenter指 令时,首先要去尝试获取对象的锁。如果这个对象没被锁定,或者当前线程已经持有了那个对象的锁,就把锁的计数器的值增加一,而在执行monitorexit指令时会将锁计数器的值减一。一旦计数器的值为零,锁随即就被释放了。如果获取对象锁失败,那当前线程就应当被阻塞等待,直到请求锁定的对象被持有它的线程释放为止。
Synchronized注意点
-
被synchronized修饰的同步块对同一条线程来说是可以重入的。这意味着同一线程反复进入同步块也不会出现自己把自己锁死的情况。
-
被synchronized修饰的同步块在持有锁的线程执行完毕并释放锁之前,会无条件地阻塞后面其他线程的进入。这意味着无法像处理某些数据库中的锁那样,强制已获取锁的线程释放锁;也无法强制正在等待锁的线程中断等待或超时退出。
Synchronized执行成本
Synchronized是一个重量级的操作,有经验的程序员都只会在确实有必要的情况下才使用这种操作。虚拟机本身也会进行一些优化,比如在通知系统之前加入一段自选等待过程。
JDK5以后加入了LOCK,重入锁(ReentrantLock)是Lock接口一种常见的实现方式。
ReentrantLock相比Synchronized增加了一些高级功能,主要有以下三项:等待可中断、可实现公平锁以及所可以绑定多个条件。
等待可中断:是指当持有锁的线程长期不释放锁的时候,正在等待的线程可以选择放弃等待,改为处理其他事情。可中断特性对处理执行时间非常长的同步块很有帮助。
公平锁:是指多个线程在等待同一个锁时,必须按照申请锁的时间顺序来依次获得锁;而非公平锁则不保证这一点,在锁被释放时,任何一个等待锁的线程都有机会获得锁。synchronized中的锁是非公平的,ReentrantLock在默认情况下也是非公平的,但可以通过带布尔值的构造函数要求使用公平锁。不过一旦使用了公平锁,将会导致ReentrantLock的性能急剧下降,会明显影响吞吐量。
锁绑定多个条件:是指一个ReentrantLock对象可以同时绑定多个Condition对象。在synchronized中,锁对象的wait()跟它的notify()或者notifyAll()方法配合可以实现一个隐含的条件,如果要和多于一个的条件关联的时候,就不得不额外添加一个锁;而ReentrantLock则无须这样做,多次调用newCondition()方法即可。
Synchronized和ReentrantLock的对比
如果Synchronized和ReentrantLock都满足的情况下,优先推荐使用Synchronized
-
synchronized是在Java语法层面的同步,足够清晰,也足够简单。每个Java程序员都熟悉synchronized,但J.U.C中 的Lock接口则并非如此。因此在只需要基础的同步功能时,更推荐synchronized。
-
Lock应该确保在finally块中释放锁,否则一旦受同步保护的代码块中抛出异常,则有可能永远不会释放持有的锁。这一-点必须由程序员自己来保证,而使用synchronized的话则可以由Java虛拟机来确保即使出现异常,锁也能被自动释放。
-
尽管在JDK 5时代ReentrantLock曾经在性能上领先过synchronized,但这已经是十多年之前的胜利了。从长远来看,Java虛拟机更容易针对sy nchronized来进行优化,因为Java虚拟机可以在线程和对象的元数据中记录synchronized中锁的相关信息,而使用J.U.C中的Lock的话,Java虛 拟机是很难得知具体哪些锁对象是由特定线程锁持有的。
2. 非阻塞同步
非同步阻塞:就是不管风险,先进行操作,如果没有其他线程争用共享数据,那操作就直接成功了;如果共享的数据的确被争用,产生了冲突,那再进行其他的补偿措施,最常用的补偿措施是不断地重试,直到出现没有竞争的共享数据为止。
其他名词:乐观锁、无锁编程。
常见的乐观锁实现比如CAS(Java最终使用的也是这个),它的底层基于比较并交换(Compare-and-swap)指令。
CAS实现原理
CAS指令需要有三个操作数,分别是内存位置(在Java中可以简单地理解为变量的内存地址,用V表示)、旧的预期值(用A表示)和准备设置的新值(用B表示)。CAS指令执行时,当且仅当V符合A时,处理器才会用B更新V的值,否则它就不执行更新。但是,不管是否更新了V的值,都会返回V的旧值,上述的处理过程是一个原子操作,执行期间不会被其他线程中断。
CAS存在的问题
CAS从语义上来说并不是真正完美的,它存在一个逻辑漏洞:如果一个变量V初次读取的时候是A值,并且在准备赋值的时候检查到它仍然为A值,那就能说明它的值没有被其他线程改变过了吗?这是不能的,因为如果在这段期间它的值曾经被改成B,后来又被改回为A,那CAS操作就会误认为它从来没有被改变过。这个漏洞称为CAS操作的“ABA问题”。
解决方案:JUC包提供了一个带有标记的原子引用类AtomicStampedReference,它可以通过控制变量版本来保证CAS的正确性,但是性能方面还不如互斥同步。
3. 无同步方案
无同步:如果能让一个方法本来就不涉及共享数据,那他自然就不需要任何同步措施去保证其正确性。因此有一些代码天生就是线程安全的。
-
可重入代码(纯引用):可以在代码执行的任何时刻中断它,转而去执行另一段代码,而在控制权返回后,原来的程序不会出现错误,也不会对结果有所影响。
判断代码是否具备可重入性:如果一个方法的返回结果是可以预测的,只要输入了相同的数据,就都能返回相同的结果,那它就满足可重入性的要求,当然也就是线程安全的。
-
线程本地存储:如果一段代码中所需要的数据必须与其他代码共享,那就看看这些共享数据的代码是否能保证在同一个线程中执行。
常见的:消费队列、服务端请求