Java中的锁
Lock接口
锁用来控制多个线程访问共享资源的方式。
synchronized是隐式地获取锁和释放锁,而Lock接口以及相关的实现类拥有了锁获取和锁释放的可操作性、可中断性、超时获取锁等synchronized所不具备的功能
Lock的使用方式
Lock lock = new ReentrantLock();
lock.lock();//获取锁
try{
}finally{
lock.unlock();//释放锁
}
Lock特性
- 非阻塞的获取锁,当前线程获取锁并且这一时刻其他线程没有获取到这个锁,则成功获取。
- 能被中断的获取锁,与synchronized不同,获取到的锁能够被中断,持有锁的线程能够响应中断,被中断时,中断异常会被抛出同时会释放锁
- 超时获取锁,指定截止时间前获取锁,如果这段时间内没有获取到,则返回。
队列同步器
AbstractQueuedSynchronizer 同步器 用来构建锁和其他同步组件的基础框架,有一个int成员变量来表示同步状态,内置一个 FIFO队列来完成现成的排队工作。
它自身没有实现任何接口,仅仅是定义了若干同步状态获取和释放的方法来供自定义同步组件使用。
其主要使用方法是通过继承他的抽象方法来管理同步状态。最常用的例如 getState()、setState(int newState)、compareAndSetState(int expect,int update)。
锁与同步器
锁时面向使用者的、它定义了使用者与锁交互的接口。隐藏了实现细节;
同步器面向锁的实现者,简化了锁的实现方式,屏蔽了同步状态管理,线程的排队、等待和唤醒等底层操作。
接口与实例
同步器使用的是模板设计模式,需要继承同步器并重写它的方法。然后才可以在同步组件中组合同步器得以实现 ,最好写为静态内部类
同步器提供的可重写方法
getState()
setState(int newState)
compareAndSetState(int expect,int update)使用CAS设置当前状态,保证状态设置的原子性。
提供的模板方法就不一一细数了,大致分为三类,
- 独占式获取和释放同步状态
- 共享式的获取和释放同步状态(同一时刻有多个线程获取锁)
- 查询同步队列中等待线程情况
独占锁示例
/**
* 独占锁
*
*/
package com.page123;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.AbstractQueuedSynchronizer;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
public class Mutex implements Lock{
private static class Sync extends AbstractQueuedSynchronizer{
protected boolean isHeldExclusively(){
return getState()==1;
}
//状态为0的获取锁
public boolean tryAcquire(int aquaires){
if(compareAndSetState(0, 1)){
setExclusiveOwnerThread(Thread.currentThread());
return true;
}
return false;
}
//释放独占锁 将状态设为0
protected boolean tryRelease(int releases){
if(getState()==0) throw new IllegalMonitorStateException();
setExclusiveOwnerThread(null);
setState(0);
return true;
}
//返回一个Condition 每一个condition都包含了一个condition队列
Condition newCondition(){
return new ConditionObject();
}
}
//将操作代理到 Sync上
private final Sync sync = new Sync();
@Override
public void lock() {
// TODO Auto-generated method stub
sync.acquire(1);
}
@Override
public void lockInterruptibly() throws InterruptedException {
// TODO Auto-generated method stub
sync.acquireInterruptibly(1);
}
@Override
public boolean tryLock() {
// TODO Auto-generated method stub
return sync.tryAcquire(1);
}
@Override
public boolean tryLock(long time, TimeUnit unit) throws InterruptedException {
// TODO Auto-generated method stub
return sync.tryAcquireNanos(1, unit.toNanos(time));
}
@Override
public void unlock() {
// TODO Auto-generated method stub
sync.release(1);
}
@Override
public Condition newCondition() {
// TODO Auto-generated method stub
return sync.newCondition();
}
}
队列同步器的底层实现
1.同步队列
依赖一个同步队列(FIFO双向队列)来完成同步队列的管理,当线程获取同步状态失败时,会将当前线程以及等待状态等信息构造成一个节点并将其加入同步队列,同时阻塞当前线程,当同步状态释放时,会把首节点中的线程唤醒,让它再次尝试获取同步状态,
加入队列的过程必须要保证线程安全因此同步器提供了一个基于CAS的设置尾节点的方法:compareAndSetTail(Node expect,Node update),它需要传递当前线程认定的尾节点和当前节点。
设置首节点,首节点是获取同步状态成功的节点,首节点的线程在释放同步状态时,将会唤醒后继节点,而后继节点将会在获取同步状态成功的时候讲自己设置为首节点,
首节点设置设置首节点是获取同步状态成功的线程完成,由于只有一个线程所以设置首节点不需要使用CAS
2. 独占式同步状态获取和释放
当节点获取同步状态时,同步器维护一个同步队列,获取状态失败的线程都会被加入到队列中并在队列中自旋,移出队列(或停止自旋)的条件是,前驱节点为头节点且成功获取了同步状态。使用tryRelease(int arg)方法释放同步状态,然后唤醒头节点的后续节点。
3.共享式同步状态获取和释放
同一时刻有多个线程获取到同步状态,比如对文件的读,同一时刻可以有多个线程读,但是此时写操作会被阻塞。写操作要求对资源独占式的访问,所以在写的时候其他的读写操作是不允许的。
释放的时候要确保线程安全释放,因为释放同步状态的操作会同时来自多个线程,一般通过循环和CAS来保证。
4.自定义同步组件
自定义一个时刻只能最多有两个线程访问的工具,超过两个线程的访问会被阻塞。
分析:
- 同一时刻有两个线程访问 显然是共享式访问 需要使用同步器的 acquireShared(int arg)方法等和Shared有关的方法 并且需要重写 tryAcquireShared(int arg) 和 tryReleaseShared(int arg)
- 定义资源数,同一时刻允许至多两个线程的同时访问,表明同步资源数为2。
public class TwinsLock implements Lock{
private final Sync sync = new Sync(2);
private static final class Sync extends AbstractQueuedSynchronizer{
Sync(int count){
if(count<0){
throw new IllegalArgumentException("count must large than zero");
}
setState(count);
}
public int tryAcquireShared(int reduceCount){
for(;;){
int current = getState();
int newCount = current - reduceCount;
if(newCount<0||compareAndSetState(current, newCount)){//<0资源被占有要加入到同步队列
return newCount;
}
}
}
public boolean tryReleaseShared(int returnCount){
for(;;){
int current = getState();
int newCount = current+returnCount;
if(compareAndSetState(current, newCount)){
return true;
}
}
}
}
public void lock(){
sync.tryAcquireShared(1);
}
public void unlock(){
sync.tryReleaseShared(1);
}
}
Test
package com.page134;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Lock;
import org.junit.Test;
public class TwinsLockTest {
@Test
public void test(){
final Lock lock = new TwinsLock();
class Worker extends Thread{
public void run(){
while(true){
lock.lock();
try{
TimeUnit.SECONDS.sleep(1);
System.out.println(Thread.currentThread().getName());
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}finally {
lock.unlock();
}
}
}
}
for(int i =0;i<10;i++){
Worker worker = new Worker();
worker.setName("thread"+i);
worker.setDaemon(true);
worker.start();
}
for(int i=0;i<10;i++){
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
System.out.println();
}
}
}
重入锁
ReentreantLock 可重进入锁,就是在调用lock方法时,已经获取锁的线程能够再次调用lock()获取锁而不被阻塞。
n次获取n次释放后才能真正释放锁
构造函数的一个boolean参数决定是否是公平锁
公平获取锁就是等待时间最长的线程优先获取锁 FIFO
非公平锁只要CAS设置同步状态成功,则表示线程获取了锁 而公平锁还要检查当前节点是否有前驱节点需要等前驱节点获取并释放锁之后才能获取锁。
公平锁保证了锁的获取按照FIFO原则,而代价是进行大量的线程切换,非公平锁虽然可能造成线程饥饿但极少线程切换 ,保证了更大的吞吐量。
读写锁
读写锁维护了一对锁
读写锁将读写状态切割为高16位表示读状态、 低16位 表示写状态
读写锁保证当写锁被获取到时,后续的读写操作都会被阻塞,写释放后所有操作继续执行。
一般读写锁的性能比排它锁好,因为大多数场景读是多于写的,读写锁能够提供更好的并发性和吞吐量
读写锁的接口: ReadWriteLock 其实现为 ReentrantReadWriteLock
写锁是一个支持重入的排他锁,当前线程如果获取写锁,就增加写状态,如果当前线程获取写锁的时候读锁已被获取,或者这个线程不是已经获取写锁的线程,则当前线程进入等待状态。
读锁是一个支持重入的共享锁,当前线程如果获取读锁,就增加读状态,如果当前线程获取读锁,写锁被其它线程获取,则进入等待状态
降级锁是指把持住写锁再获取到读锁,随后释放写锁的过程。
Condition接口
Condition接口提供类似Object的监视器方法 与Lock配合实现等待/通知模式
与对象监视器不同它支持多个等待队列,支持在等待队列中响应中断,支持等待到deadline。
Condition依赖Lock对象 Lock.newCondition 一般会把condition作为成员变量 await()后当前线程释放锁并等待,其它线程调用Condition.signal() 通知当前线程后才从await()返回
调用了await()的线程必然是获取了锁的线程,所以在添加到等待队列的时候不需要CAS操作。
一个对象拥有一个同步队列和多个等待队列
等待
能够调用await()及类似方法的线程是成功获取了锁的线程,也就是同步队列中的首节点,调用了该方法后,会将当前线程构造成节点并加入等待队列中(末尾),然后释放同步状态,唤醒同步队列的后续节点,然后当前节点进入等待状态。
通知
signal()方法会唤醒在等待队列中等待时间最长的节点(首节点),唤醒之前会将节点移到同步队列中