多线程(五)Lock 和 ReentrantLock

线程锁是用来实现同步机制的,前面讲到过使用synchronized关键字来实现同步。

使用这个关键字实现的同步块有一些缺点:

(1)锁只有一种类型

(2)线程得到锁或者阻塞

(3)不能实现很好的并发

为了解决如上的各种问题,后来又提出了一种更为复杂的锁 - 线程锁。

线程锁可以在几个方面进行提升:

(1)添加不同类型的锁,如读取锁和写入锁(主要实现类为ReentrantReadWriteLock类)

(2)对锁的阻塞没有限制,即可以在一个方法中上锁,在另外一个方法中解锁。

(3)如果线程得不到锁,比如锁由另外一个线程持有,就允许该线程后退或继续执行,或者做其他事情 - 使用类中提供的tryLock()方法

(4)允许线程尝试取锁,并可以在超过等待时间后放弃。

1、下面来认识一下一个简单的线程锁 - Lock锁。

public interface Lock {

/*    获取锁。如果锁不获取,在获得锁之前,该线程将一直处于休眠状态。 
    */
    void lock();
/*    如果当前线程未被中断,则获取锁。如果锁可用,则获取锁,并立即返回。
    如果锁不可用,则该线程将一直处于休眠状态,直到:
    (1) 锁由当前线程获得;或者 
    (2)其他某个线程中断当前线程。
    注意:如果当前线程,
    (1)在进入此方法时已经设置了该线程的中断状态;或者 
    (2)在获取锁时被中断,并且支持对锁获取的中断, 
    则将抛出 InterruptedException,并清除当前线程的已中断状态。    
     */
    void lockInterruptibly() throws InterruptedException;
/*    仅在调用时,锁为空闲状态才获取该锁。 
    如果锁可用,则获取锁,并立即返回值 true。
    如果锁不可用,则此方法将立即返回值 false。
    此方法的典型使用语句如下: 
      Lock lock = ...;
      if (lock.tryLock()) {
          try {
              // manipulate protected state
          } finally {
              lock.unlock();
          }
      } else {
          // perform alternative actions
      }
     此用法可确保如果获取了锁,则会释放锁,如果未获取锁,则不会试图将其释放。
     */
    boolean tryLock();
/*    如果锁在给定的等待时间内空闲,并且当前线程未被中断,则获取锁。 
    如果锁可用,则此方法将立即返回值 true。
    如果锁不可用,该线程将一直处于休眠状态:直到下面三种情况出现 
    (1)锁由当前线程获得;或者 
    (2)其他某个线程中断当前线程,并且支持对锁获取的中断;或者 
    (3)已超过指定的等待时间 
    如果获得了锁,则返回值 true。 
    注意:如果当前线程: 
    (1)在进入此方法时已经设置了该线程的中断状态;或者 
    (2)在获取锁时被中断,并且支持对锁获取的中断, 
    则将抛出 InterruptedException,并会清除当前线程的已中断状态。 
    如果超过了指定的等待时间,则将返回值 false。
    如果 time 小于等于 0,该方法将完全不等待。 
    time为等待时长,unit为time参数的时间单位
     */
    boolean tryLock(long time, TimeUnit unit) throws InterruptedException;

/*    释放锁。
     */
    void unlock();

/*    返回绑定到此 Lock 实例的新 Condition 实例。 
    在等待条件前,锁必须由当前线程保持。调用 Condition.await() 将在等待前以原子方式释放锁,
    并在等待返回前重新获取锁。 
     */
    Condition newCondition();
}

Lock 实现提供了比使用 synchronized 方法和语句可获得的更广泛的锁定操作。

锁是控制多个线程对共享资源进行访问的工具。通常,锁提供了对共享资源的独占访问。一次只能有一个线程获得锁,对共享资源的所有访问都需要首先获得锁。不过,某些锁可能允许对共享资源并发访问,如 ReadWriteLock 的读取锁。

synchronized 方法或语句的使用提供了对与每个对象相关的隐式监视器锁的访问,但却强制所有锁获取和释放均要出现在一个块结构中,Lock 接口的实现允许锁在不同的作用范围内获取和释放,并允许以任何顺序获取和释放多个锁。不使用块结构锁就失去了使用 synchronized 方法和语句时会出现的锁自动释放功能。而Lock需要自己调用unlock()手动释放锁,否则会产生死锁。锁定和取消锁定出现在不同作用范围中时,必须谨慎地确保保持锁定时所执行的所有代码用 try-finally 或 try-catch 加以保护,以确保在必要时释放锁。

2、Lock的实现类ReentrantLock(可重入锁,独占锁)

首先说一下ReentrantLock的内置类Sync,而Sync 是extends AbstractQueuedSynchronizer(可独占,可共享)

ReentrantLock是一个可重入的互斥锁,又被称为“独占锁”。也就是说ReentrantLock在同一个时间点只能被一个线程获取。

Java的synchronized块并不保证尝试进入它们的线程的顺序。因此,如果多个线程不断竞争访问相同的synchronized同步块,就存在一种风险,其中一个或多个线程永远也得不到访问权 —— 也就是说访问权总是分配给了其它线程。这种情况被称作线程饥饿。为了避免这种问题,锁需要实现公平性。

ReentrantLock分为“公平锁”和“非公平锁”。它们的区别体现在获取锁的机制上是否公平。ReentraantLock是通过一个FIFO的等待队列来管理获取该锁所有线程的。synchronized是一种非公平锁。

  • 在“公平锁”的机制下,线程依次排队获取锁;
  • “非公平锁”在锁是可获取状态时,不管自己是不是在队列的开头都会获取锁。

来看一下ReentrantLock互斥锁的构造函数:

public ReentrantLock() { // 默认非公平锁
    sync = new NonfairSync();
}
public ReentrantLock(boolean fair) { // 通过参数选择公开锁还是非公平锁
    sync = fair ? new FairSync() : new NonfairSync();
}

当我们在程序中通过如下方式获取到实例后,就可以调用lock()方法进行独占锁的获取了,如下:

private Lock lock = new ReentrantLock(); 

调用lock.lock()方法可以获取独占锁,源代码如下:

public void lock() {
    sync.lock();
}

1、获取公平的独占锁

其基本的方法调用流程如下所示,后面将会进行详细的讲解。

 

  static final class FairSync extends Sync {
   	
        final void lock() {     
            acquire(1); // 直接调用acquire()方法获取锁
        }
        // 试着去获取锁. 只有递归调用、或者没有等待者或者在等待队列的第一个时获取到该锁.
        protected final boolean tryAcquire(int acquires) {
            final Thread current = Thread.currentThread(); // 获取当前线程
            int c = getState();  // 获取独占锁的状态
            if (c == 0) {         // c=0 意味着锁没有被任何线程锁拥有
                // 若锁没有被任何线程锁拥有,则判断当前线程是不是CLH队列中的第一个线程线程,
                if (!hasQueuedPredecessors() && compareAndSetState(0, acquires)) {
                    setExclusiveOwnerThread(current);// 获取该锁,设置锁的状态,并切设置锁的拥有者为当前线程
                    return true;
                }
            } else if (current == getExclusiveOwnerThread()) {//独占锁的拥有者已经为当前线程
                int nextc = c + acquires;//acquires一般为1,所以每获取一次状态+1;
                if (nextc < 0)
                    throw new Error("Maximum lock count exceeded");
                setState(nextc);  // 更新锁的状态
                return true;
            }
            return false;
        }
    }

在静态的不变类中调用lock()方法,这个方法会调用AbstractQueueSynchronizer类中的acquire()方法,并传递参数1. 由于ReentrantLock是可重入锁,所以独占锁可以被单个线程多此获取,每获取1次就将锁的状态加1。 也就是说,初次获取锁时,通过acquire(1)将锁的状态值设为1;再次获取锁时,将锁的状态值设为2;依次类推...

 public final void acquire(int arg) {
        if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
            selfInterrupt();
}

 首先要看tryAcquire()方法调用返回的结果。如果锁不是被当前的线程占用,则返回false,否则直接就获取到这个锁了。

对比代码和方法调用流程图,下一部就是调用addWaiter()方法了,并传递参数Node.EXCLUSIVE,表示是独占锁类型。

     private Node addWaiter(Node mode) {
        // 新建一个Node节点,节点对应的线程是当前线程,当前线程的锁的模型是mode。
        Node node = new Node(Thread.currentThread(), mode);
        Node pred = tail;
        // 若队列不为空,则将当前线程添加到队列末尾
        if (pred != null) {
            node.prev = pred;
            if (compareAndSetTail(pred, node)) { // 以原子的方式进行添加
                pred.next = node;
                return node;
            }
        }
        enq(node);// 若队列为空,则调用enq()新建队列,然后再将当前线程添加到队列中
        return node;
    }

如上方法是将这个请求独占锁类型的线程添加到了队列的末尾。由于在添加的时候,要防止可能其他的线程对队列进行了修改,所以调用了compareAndSetTail()方法进行处理。

接下来调用acquireQueued()方法,并将代表当前线程的节点做为参数传递,返回是否被中断,源码如下:

     final boolean acquireQueued(final Node node, int arg) {
        boolean failed = true;
        try {
            // interrupted表示在队列的调度中,当前线程在休眠时,有没有被中断过
            boolean interrupted = false;
            for (;;) {
                // 获取上一个节点,node是当前线程对应的节点,这里就意味着获取上一个等待锁的线程
                final Node p = node.predecessor();
                if (p == head && tryAcquire(arg)) {// 使用p==head来保证锁的公平性。如果当前线程是因为“线程被中断”而唤醒,那么显然就不是公平了
                    setHead(node);
                    p.next = null;                 // help GC
                    failed = false;
                    return interrupted;            // 只有在这里才能跳出死循环
                }
                if (shouldParkAfterFailedAcquire(p, node) && parkAndCheckInterrupt())
                    interrupted = true;
            }
        } finally {
            if (failed)
                cancelAcquire(node);
        }
    }

注意,在for(;;)死循环中,唯一跳出去的途径就是满足p==head和tryAcquire()方法返回值为true。也就是说,当前线程会根据公平性原则进行阻塞等待直到获取锁为止。

(1)当p==head时,表示当前线程前面的线程已经得到执行,这样就保证了锁的公正性。即按照队列的先后顺序进行执行。

(2)tryAcquire()方法返回true时,表示当前线程成功获取了独占公平锁。可以进行返回了。但是返回的并不是true,而是interrupted变量的值。那么这又是怎么回事呢?

其实为了保证绝对的公平性,代码考虑到了中断这种特殊的情况。线程被阻塞后有在这样的代码中两种情况可以唤醒:

第1种情况:unpark()唤醒。“前继节点对应的线程”使用完锁之后,通过unpark()方式唤醒当前线程。
第2种情况:中断唤醒。其它线程通过interrupt()中断当前线程。

对于第2种情况,线程在阻塞状态被中断唤醒而获取到cpu执行权利。但是,如果该线程的前面还有其它等待锁的线程,根据公平性原则,该线程依然无法获取到锁。它会再次阻塞! 该线程再次阻塞,直到该线程被它的前面等待锁的线程锁唤醒;线程才会获取锁并执行。

所以如果以下代码如果执行的话,会让interrupted变量为true。

  if (shouldParkAfterFailedAcquire(p, node) && parkAndCheckInterrupt())
               interrupted = true;
     // 获取锁失败后判断当前线程是否应该阻塞
    private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
        int ws = pred.waitStatus;
        // 如果前继节点是SIGNAL状态,则意味这当前线程需要被unpark唤醒。此时,返回true
        if (ws == Node.SIGNAL)
            return true;
        if (ws > 0) {// 前继节点是取消状态
            do {
                node.prev = pred = pred.prev;//设置 当前节点的 前继节点为 原前继节点的前继节点
            } while (pred.waitStatus > 0);
            pred.next = node;
        } else {
            // 如果前继节点为0或者共享锁状态,则设置前继节点为SIGNAL状态
            compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
        }
        return false;
    }

也就是说:

(1)如果前继节点状态为SIGNAL(后继线程需要被唤醒),表明当前节点需要被unpark(唤醒),此时则返回true。则interrupted值为false,当前线程不会产生自中断。
(2)如果前继节点状态为CANCELLED(ws>0),说明前继节点已经被取消,则通过先前回溯找到一个有效(非CANCELLED状态)的节点,并返回false
(3)如果前继节点状态为非SIGNAL、非CANCELLED,那么就是需要被Condition唤醒或者是共享的线程,则设置前继的状态为SIGNAL,并返回false。

  // Convenience method to park and then check if interrupted
    private final boolean parkAndCheckInterrupt() {
        LockSupport.park(this);      // 通过LockSupport的park()阻塞当前线程
        return Thread.interrupted(); // 返回线程的中断状态,并清除中断标志
    }

如果acquire()方法的if条件判断全部为真,则会产生一个自中断,如下:

 // 当前线程产生一个自我中断
    private static void selfInterrupt() {
        Thread.currentThread().interrupt();
    }

2、获取非公平的独占锁

来看一下非公平独占锁调用方法的流程,如下图:


 

其实大部分的流程是一样的,只是在开始的时候实现的方法不一样,如下:

     // 非公平方式获取锁
    static final class NonfairSync extends Sync {
        final void lock() {
            if (compareAndSetState(0, 1))// 如果线程没有被持有,则以原子方式设置为1,表示持有
                setExclusiveOwnerThread(Thread.currentThread()); // 设置为当前线程持有
            else
                acquire(1);              // 锁被持有,调用acquire()方法获取
        }
        protected final boolean tryAcquire(int acquires) {
            return nonfairTryAcquire(acquires);
        }
    }

公平方式获取时是直接调用acquire()方法,而非公平锁获取首先会判断锁有没有被占用,如果没有则直接获取。接下来再调用acquire()方法,其实在调用acquire()方法时与获取公平锁的流程是一样的,这里就不再说了,看一下tryAcquire()方法中调用的nonfairTryAcquire()方法,如下:

         // 不公平方式来获取锁状态
        final boolean nonfairTryAcquire(int acquires) {
            final Thread current = Thread.currentThread();// 获取当前线程
            int c = getState();   // 获取锁的状态 
            if (c == 0) {         // c=0意味着锁没有被任何线程锁拥有
                // 若锁没有被任何线程锁拥有,则通过CAS函数设置锁的状态为acquires
                if (compareAndSetState(0, acquires)) {//与公平锁比较少了个判断是否有前面祖先,!hasQueuedPredecessors()
                    setExclusiveOwnerThread(current);//设置当前线程为锁的持有者
                    return true;
                }
            } else if (current == getExclusiveOwnerThread()) {
                // 如果锁的持有者已经是当前线程,则将更新锁的状态。
                int nextc = c + acquires;
                if (nextc < 0) // overflow
                    throw new Error("Maximum lock count exceeded");
                setState(nextc);
                return true;
            }
            return false;
        }

这里与获取公开锁也有区别。公正锁在锁没有被占有有时候,还会比较当前线程是不是在队列的头部,然后再决定,这里是直接获取到了这个独占锁,也体现出了锁的非公平获取。

 

3、锁的释放操作

释放操作的流程相对简单,首先调用unlock()方法,代码如下:

  public void unlock() {
        sync.release(1);
 }

由于锁是可重入的,所以对于同一个线程,每释放锁一次,锁的状态要减去1。

     // 试着释放当前线程持有的锁并唤醒后继节点
    public final boolean release(int arg) {
        if (tryRelease(arg)) {//试着释放当前线程持有的锁
            Node h = head;
            if (h != null && h.waitStatus != 0)
                unparkSuccessor(h);// 唤醒后继节点
            return true;
        }
        return false;
    }                

释放锁时,调用tryRelease()进行释放。

 protected final boolean tryRelease(int releases) {
            int c = getState() - releases; // c是本次释放锁之后的状态
            // 如果当前线程不是锁的持有者,则抛出异常
            if (Thread.currentThread() != getExclusiveOwnerThread())
                throw new IllegalMonitorStateException();
            boolean free = false;
            // 如果锁已经被当前线程彻底释放
            if (c == 0) {
                free = true;
                setExclusiveOwnerThread(null);//设置锁的持有者为null,即锁是可获取状态
            }
            setState(c);  // 设置当前线程的锁的状态。
            return free;
        }

更新当前线程对应的锁的状态。如果当前线程对锁已经彻底释放,也就是state为0,则设置锁的持有线程为null,设置当前线程的状态为空,然后唤醒后继线程。

     // 唤醒一个有效的后继节点
    private void unparkSuccessor(Node node) {
        int ws = node.waitStatus;  // 获取当前线程的状态
        if (ws < 0)                // 如果状态<0,以原子的方式设置状态=0 
            compareAndSetWaitStatus(node, ws, 0);
        // 获取当前节点的有效的后继节点,无效的话,则通过for循环进行获取。
        // 这里的有效,是指“后继节点对应的线程状态<=0
        Node s = node.next;
        if (s == null || s.waitStatus > 0) {                       // 为空或已经被取消
            s = null;
            for (Node t = tail; t != null && t != node; t = t.prev)// 队列从后到前查找waitStatus<=0的节点
                if (t.waitStatus <= 0)
                    s = t;
        }
        if (s != null)                                             // 唤醒后继节点s对应的线程
            LockSupport.unpark(s.thread);
    }

需要注意的是,在使用这个锁的时候,必须在finally块中释放锁。否则,如果在被保护的代码块中抛出异常,那么这个锁就永远也无法得到释放。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值