多线程—Lock、Condition、ReentrantLock、ReentrantReadWriteLock

Lock接口

public interface Lock {
//下面4个方法都是获得锁
  void lock();    
  void lockInterruptibly() throws InterruptedException; // 如果当前线程未被中断,则获取锁,可以响应中断  
  boolean tryLock();    //如果获取到锁返回true,否则false
  boolean tryLock(long var1, TimeUnit var3) throws InterruptedException; //如果获取不到锁,就等待一段时间,超时返回false。
//解除锁,在finally里调用
  void unlock();
//返回Condition实例
  Condition newCondition();
}

对于Lock接口方法的实现,大多都是调用AQS的方法来实现。实现Lock接口的类含有继承了AQS一个内部类(例如ReentrantLock的Sync内部类),从而调用内部类的继承自AQS的方法或者重写的方法来实现Lock接口的方法。

Lock接口最后还有一个返回Condition实例的方法。Condition是和Lock配合使用的。

在一个AQS同步器中,可以定义多个Condition,每一个Condition是一条FIFO队列。只需要多次lock.newCondition(),每次都会返回一个新的ConditionObject对象。在ConditionObject中,通过一个条件队列来维护等待的线程,这个队列跟AQS的队列不是同一条队列,一个同步器中可以有多个条件队列。

Condition接口

public interface Condition {

    // 当前线程进入等待状态直到被通知(signal)或被中断
    void await() throws InterruptedException;
    // 不响应中断等待,直到被通知(signal)
    void awaitUninterruptibly();
    // 等待指定时长直到被通知或中断或超时。
    long awaitNanos(long nanosTimeout) throws InterruptedException;
    // 等待指定时长直到被通知或中断或超时。
    boolean await(long time, TimeUnit unit) throws InterruptedException;
    // 当前线程进入等待状态直到被通知、中断或者到某个时间。如果没有到指定事件就被通知,方法返回true,否则false。 
    boolean awaitUntil(Date deadline) throws InterruptedException;
    // 唤醒一个等待在Condition上的线程,该线程从等待方法返回前必须获得与Condition相关联的锁  
    void signal();
    // 唤醒所有等待在Condition上的线程,该线程从等待方法返回前必须获得与Condition相关联的锁  
    void signalAll();
}

ConditionObject

ConditionObject是AQS中的内部类,提供了条件锁的同步实现,实现了Condition接口,并且实现了其中的await(),signal(),signalALL()等方法。因为Condition的操作需要获取相关联的锁,所以作为同步器的内部类也较为合理。每个Condition对象都包含着一个FIFO等待队列,该队列是Condition对象实现等待/通知功能的关键。

当线程获取到锁之后,Condition对象调用await相关的方法:

public final void await() throws InterruptedException {
    if (Thread.interrupted())
        throw new InterruptedException();
    // 添加当前线程到条件队列
    Node node = addConditionWaiter();
    // 释放已经获取的锁资源,并返回释放前的同步状态
    int savedState = fullyRelease(node);
    int interruptMode = 0;
    // 如果当前节点不在同步队列中,线程进入阻塞状态,等待被唤醒
    while (!isOnSyncQueue(node)) {
        LockSupport.park(this);
        if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
            break;
    }
    if (acquireQueued(node, savedState) && interruptMode != THROW_IE)
        interruptMode = REINTERRUPT;
    if (node.nextWaiter != null) // clean up if cancelled
        unlinkCancelledWaiters();
    if (interruptMode != 0)
        reportInterruptAfterWait(interruptMode);
}

可以看到因为ConditionObject是AQS内部类,可以获取到外部类的数据,调用await将线程放进条件队列,然后同样调用LockSupport类的park方法进行阻塞。

Condition对象调用signal或者signalAll方法时:

// 将条件队列中第一个有效的元素移除并且添加到同步队列中
private void doSignal(Node first) {
    do {
        if ( (firstWaiter = first.nextWaiter) == null)
            lastWaiter = null;
        first.nextWaiter = null;
    // 将条件队列中等待最久的那个有效元素添加到同步队列中
    } while (!transferForSignal(first) &&
             (first = firstWaiter) != null);
}

// 将条件队列中的节点转换到同步队列中
final boolean transferForSignal(Node node) {
    // 如果节点的等待状态不能被修改,说明当前线程已经被取消等待
    if (!compareAndSetWaitStatus(node, Node.CONDITION, 0))
        return false;

    // 加入到同步队列中,并且尝试将前驱节点设置为可唤醒状态
    Node p = enq(node); 
    int ws = p.waitStatus;
    // 如果前驱节点不需要唤醒,或者设置状态为‘唤醒’失败,则唤醒线程时期重新争夺同步状态
    if (ws > 0 || !compareAndSetWaitStatus(p, ws, Node.SIGNAL))
        LockSupport.unpark(node.thread);
    return true;
}

singal方法将节点放回同步队列尾端,调用enq方法,该方法在AQS的时候分析过,该方法使用了死循环, 即以自旋方式将节点插入队列,如果失败则不停的尝试, 直到成功为止,运用到了乐观锁的原理,该方法也负责在队列为空时, 初始化队列。

为什么要有Condition呢?

在synchronized中,我们可以调用object.await和object.notify,让线程等待和唤醒,但方法仅在synchronized中可以使用。通过Condition能够更加精细的控制多线程的休眠与唤醒,synchronized相比ReentrantLock、ReadWriteLock等实现了Lock接口和内含继承AQS类的子类的锁来说,synchronized显得笨重,不够灵活,适合小段代码使用。

condition.signal和object.notify区别

  • obj.notify();   随机唤醒一个处于等待状态的线程,可能有多个线程处于等待状态,继续执行wait后面的代码。与synchronized同步关键字配合
  • condition.signal(); 唤醒在同条件队列的线程,与Lock配合

使用要求:

  • signal()、await()、signalAll()方法使用之前必须要先进行lock()获取锁,类似使用Object的notify()、wait()、notifyAll()之前必须要对Object对象进行synchronized操作,且两类方法不能混合使用,否则都会抛IIlegalMonitorStateException。

Synchronized和Lock比较

  • Synchronized是关键字,内置语言实现,Lock是接口。
  • Synchronized在线程发生异常时会自动释放锁,因此不会发生异常死锁。Lock异常时不会自动释放锁,所以需要在finally中实现释放锁。
  • Lock是可以中断锁,Synchronized是非中断锁,必须等待线程执行完成释放锁。
  • Lock可以使用读锁提高多线程读效率。

关系图:

重入锁

可重入就是说某个线程已经获得某个锁,可以再次获取锁而不会出现死锁。

为什么需要再次获取锁呢?

一个线程在执行一个带锁的方法,该方法中又调用了另一个需要相同锁的方法,则该线程可以直接执行调用的方法,而无需重新获得锁,从而避免了死锁。

实现原理

通过为每个锁关联一个请求计数器和一个获得该锁的线程。该计数器不是AQS的state,当计数器为0时,认为锁是未被占用的。线程请求一个未被占用的锁时,JVM将记录该线程并将请求计数器设置为1,此时该线程就获得了锁,当该线程再次请求这个锁,计数器将递增,当线程退出同步方法或者同步代码块时,计数器将递减,当计数器为0时,线程就释放了该对象,其他线程才能获取该锁。

重入锁有:

  • synchronized
  • ReentrantLock
  • ReentrantReadWriteLock

两者区别

  • synchronized是独占锁,加锁和解锁的过程自动进行,易于操作,但不够灵活。ReentrantLock也是独占锁,加锁和解锁的过程需要手动进行,不易操作,但非常灵活。
  • synchronized可重入,因为加锁和解锁自动进行,不必担心最后是否释放锁;ReentrantLock也可重入,但加锁和解锁需要手动进行,且次数需一样,否则其他线程无法获得锁。
  • synchronized不可响应中断,一个线程获取不到锁就一直等着;ReentrantLock可以相应中断。

ReentrantLock

ReentrantLock的内部类Sync继承了AQS,分为公平锁FairSync和非公平锁NonfairSync。

Lock lock=new ReentrantLock();//默认非公平锁
Lock lock=new ReentrantLock(true);//公平锁
Lock lock=new ReentrantLock(false);//非公平锁
getHoldCount() 查询当前线程保持此锁的次数,也就是执行此线程执行lock方法的次数。

getQueueLength()返回正等待获取此锁的线程数,比如启动10个线程,1个线程获得锁,此时返回的是9。

getWaitQueueLength(Condition condition)返回在该条件队列的线程数。

hasWaiters(Condition condition) 查询该条件队列是否有等待线程。

hasQueuedThread(Thread thread) 查询指定的线程是否正在等待获取Lock锁。

hasQueuedThreads() 是否有线程等待此锁

isFair() 该锁是否公平锁

isHeldByCurrentThread() 当前线程是否保持锁锁定,线程的执行lock方法的前后分别是false和true

isLock() 此锁是否有任意线程占用

lockInterruptibly() 如果当前线程未被中断,获取锁

tryLock() 线程尝试获取锁,如果获取成功,返回 true,否则返回 false

tryLock(long timeout,TimeUnit unit) 线程如果在指定等待时间内获得了锁,就返回true,否则返回 false

ReentrantReadWriteLock

Java并发包中ReadWriteLock是一个接口,主要有两个方法,如下:

public interface ReadWriteLock {
    //返回读锁
    Lock readLock();
    
    //返回写锁
    Lock writeLock();
}

ReetrantReadWriteLock实现了ReadWriteLock接口并添加了可重入的特性。

有两个锁,一个是读操作相关的锁,称为共享锁;一个是写相关的锁,称为排他锁。

读写锁的机制:

  • "读-读" 不互斥
  • "读-写" 互斥
  • "写-写" 互斥

线程进入读锁的前提条件:

  • 没有其他线程的写锁或者有写请求,但调用线程和持有锁的线程是同一个。

线程进入写锁的前提条件:

  • 没有其他线程的读锁
  • 没有其他线程的写锁

3个特性:

  • 公平选择性:支持非公平(默认)和公平的锁获取方式,吞吐量还是非公平优于公平。
  • 重进入:读锁和写锁都支持线程重进入。
  • 锁降级:遵循获取写锁、获取读锁再释放写锁的次序,写锁能够降级成为读锁。不支持锁升级。

 

 

 

 

 

 

 

 

 

 

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章