一、synchronized同步锁
synchronized的用法和实现原理
参考我的另外一篇文章:Java多线程:synchronized同步锁的使用和实现原理
synchronized的不足
synchronized在线程同步的使用方面,优点是使用简单,可以自动加锁和解锁,但是也存在一些不足:
synchronized是阻塞的,不支持非阻塞,中断和超时退出特性;
synchronized是互斥锁,不支持多个线程对资源的共享访问,如多个读线程进行并发读;
当多个方法共享多个monitor时,要注意使用synchronized加锁的顺序,否则容易产生死锁;
synchronized只支持基于monitor这个对象来进行线程之间条件化通信,即多个线程只能基于一个monitor的wait,notify,notifyAll来进行线程之间的通信,不够灵活,如读写线程之间无法区分;同时由于只有monitor对象的一个等待队列,故所有等待线程均处于该等待队列中,无法区分出是生产者线程还是消费者线程,所以当需要唤醒某个线程时,需要调用notifyAll来唤醒所有线程,否则如果使用notify则可能会出现假死,如本来想唤醒生产者线程,而notify了一个消费者线程,则此时系统就出现“假死”了;
synchronized基于操作系统的Metux Lock来实现,线程之间的切换需要进行上下文切换,成本较高,性能较低。
所以为了解决以上问题,在JDK1.5中提供了Lock和Condition接口来实现synchronized和监视器monitor的功能,核心实现为ReentrantLock。
二、ReentrantLock:可重入锁
设计目的
ReentrantLock也是一个可重入的互斥锁,跟synchronized提供一样的线程同步功能,但是比synchronized更加灵活,即优化了以上所说的synchronized的不足。
ReentrantLock是基于AQS来实现线程同步功能的:
AQS提供了线程等待队列的实现,ReentrantLock自定义线程同步状态state来实现互斥锁的功能即可,即state等于0,表示当前没有任何线程占用这个锁,state大于0,表示当前存在线程占用这个锁,且该占用线程每访问一个使用该锁同步的方法,则state递增1,实现可重入,这个实现逻辑是跟synchronized一样的;
AQS是通过使用自旋和UNSAFE提供的CAS硬件级别的原子操作来对线程等待队列进行增删节点来实现线程的切换的,整个过程为无锁操作,即不需要依赖于操作系统的Metux Lock来实现,故不需要进行线程上下文切换,提高了性能。
在使用方面,与synchronized的自动加锁解锁不同的是,ReentrantLock是需要在应用代码中显式进行加锁和解锁操作的,通常需要结合try-finally来实现,避免异常是无法解锁,如下:
实现
ReentrantLock基于AQS实现,故需要使用一个内部类来实现AQS接口,提供Syn同步锁的功能。在AQS实现类中,主要是需要实现tryAcquire方法定义是否可以成功获取锁,实现tryRelease方法定义释放锁。
对于获取锁tryAcquire,ReentrantLock提供了公平和非公平锁两个实现,默认为非公平锁,公平的含义是根据线程请求获取锁的先后顺序来获取锁,即利用了FIFO队列的特性;非公平的含义是每个请求获取锁的线程在需要锁时,先请求一下是否可以获取锁,如果无法获取,则再放入FIFO队列中。
1.请求获取锁tryAcquire
公平版本:
非公平版本:
2.释放锁tryRelease
在基类Sync中定义,Sync继承AbstractQueuedSynchronizer:递减state直到0
3.各个版本的lock加锁
1.阻塞加锁直到获取锁为止,与synchronized语义一样,不支持中断、超时:
2.阻塞可中断版本:
public void lockInterruptibly() throws InterruptedException {
sync.acquireInterruptibly(1);
}
3.非阻塞版本:非阻塞,非公平,即使使用的是公平锁,能获取锁则返回true,否则返回false。
public boolean tryLock() {
return sync.nonfairTryAcquire(1);
}
4.阻塞可超时版本:阻塞指定时间,若在该指定时间到达之后,还没获取锁,则返回false:
public boolean tryLock(long timeout, TimeUnit unit)
throws InterruptedException {
return sync.tryAcquireNanos(1, unit.toNanos(timeout));
}
三、基于Condition实现生产者消费者模型
Condition在Lock体系设计中,用于实现与synchronized中monitor对象所提供的wait,notify,notifyAll相同的语义,对应的方法分别为await,signal,signalAll。在此基础上进行的优化是:一个Lock可以对应多个Condition,每个Condition对应一个条件化线程等待队列,而在synchronized中只能使用monitor这一个Condition。
一个Lock支持多个Condition的好处是:可以将等待线程进行分类,即每个Condition对应一个条件化线程等待队列,而不是全部放在一个条件化线程等待队列,这样每个Condition在条件满足时,可以调用signal或者signalAll来通知该Condition对应的条件化线程等待队列的线程,而不是所有线程:
这样可以在一定程度上优化性能,特别是signalAll,只需通知对应的条件化线程等待队列即可,让这个线程子集去竞争Lock锁,其他Condition的条件化等待队列中的线程继续休眠;
其次对于signal的调用,可以“精确”通知到该Condition对应的条件化线程等待队列中的一个线程,从而避免了在synchronized中的可能出现“假死”的问题:如在生产者消费者模型中,当生产者往数据队列放入数据后,基于Condition的实现中,可以通知和唤醒消费者线程等待队列的一个线程去数据队列读取数据了,而在synchronized的实现中,由于没有对线程进行区分,故可能通知到线程等待队列的一个生产者线程,如果此时数据队列满了,则该生产者线程被唤醒后发现数据队列还是满了,则继续休眠,此后则没有生产者线程来通知消费者线程消费数据了,整个生产者消费者体系就“假死”了,即生产者无法填充数据,消费者不知道有数据可读继续休眠,所以在synchronized中通常需要使用notifyAll来避免这种情况发生,唤醒所有线程去竞争锁。
所以有了Condition之后,只需要调用condition的signal就看准确唤醒对应某个线程,如生产者线程或者消费者线程,而不需要调用signalAll方法,像synchronized一样调用monitor对象的notifyAll唤醒所有线程,从而提高了性能。
1.await等待的实现
synchronized的wait的语义为:线程占有锁,发现条件不满足,释放锁,线程阻塞休眠,进入条件化等待队列,等待其他线程notify唤醒。
Lock的await的调用也是跟synchronized的wait一样,首先对应的线程需要获取Lock锁进入同步代码,即如果方法没有先调用如lock.lock()获取锁的情况下,调用await了,则会抛IllegalMonitorStateException异常。
await对synchronized的wait的语义实现如下:将当前线程放入条件化等待队列,然后释放锁,在while循环内阻塞休眠,直到被放到AQS同步队列了,这时说明条件满足了,可以去竞争获取锁了,通过调用acquireQueued去竞争获取锁。如果获取锁成功了,则真正从await返回。
在应用代码中,await通常需要在while循环中检查条件是否满足,只有对应线程被唤醒,获取锁成功,然后再在while循环检查条件是否满足,如果满足,则继续执行,因为此时只有当前线程占有锁,不会出现并发修改导致条件不满足,如下为LinkedBlockingQueue的put的实现:
2.各个版本的await等待条件满足
1.可中断阻塞等待
public final void await() throws InterruptedException
2.可中断,可超时阻塞等待:分别为基于纳秒,指定日期,自定义时间单位的版本
public final long awaitNanos(long nanosTimeout)
throws InterruptedException
public final boolean awaitUntil(Date deadline) throws InterruptedException
public final boolean await(long time, TimeUnit unit) throws InterruptedException
3. signal通知的实现
signal主要是当前占有锁正在执行的线程,在条件满足时,通知和唤醒该Condition对应的条件化等待队列的一个线程,让该线程去竞争获取锁,然后继续执行。
Condition的signal实现主要是将Condition对应的条件化等待队列的头结点移到到AQS的同步队列中,具体为移动到同步队列的尾部,这样这个节点对应的线程就可以去竞争锁了。如下:
在使用方面,signal不需要在while循环中,因为调用signal的线程是当前占有锁,正在执行的线程。
四、为什么await,signal,signalAll需要在获取lock锁的前提下调用?
与monitor对象的wait,notify,notifyAll需要在synchronized同步的方法或者方法块内执行一样,Condition的await,signal,signalAll需要在获取lock锁的前提下调用,否则会抛IllegalMonitorStateException一次:因为Condition的条件化等待队列中的线程在唤醒时,是被移动到Lock的同步队列中,然后与其他同步队列中的线程一样竞争获取Lock锁,故需要在获取lock锁的前提下才能调用。