Java多线程(14)——Lock接口详解

1 基本概括

2 主要介绍

2.1 Lock接口的作用

2.1.1 简单介绍

        锁是用来控制多个线程访问共享资源的方式,一般来说,一个锁能够防止多个线程同时访问共享资源,解决数据的一致性问题。在Lock接口出现之前,Java程序是靠synchronized关键字实现锁功能的,而Java SE 5之后,并发包中新增了Lock接口(以及相关实现类)用来实现锁功能,它提供了与synchronized关键字类似的同步功能,只是在使用时需要显式地获取和释放锁。虽然它缺少了隐式获取释放锁的便捷性,但是却拥有了锁获取与释放的可操作性、可中断的获取锁以及超时获取锁等多种synchronized关键字所不具备的同步特性。

        使用synchronized关键字将会隐式地获取锁,当synchronized的方法或代码块执行完成再释放。这种方式简化了同步的管理,可是扩展性没有显示的锁获取和释放来的好。在比较复杂的同步控制中,可操作性不如显式锁来的灵活. 例如,针对一个场景,手把手进行锁获取和释放,先获得锁A,然后再获取锁B,当锁B获得后,释放锁A同时获取锁C,当锁C获得后,再释放B同时获取锁D,以此类推。这种场景下,synchronized关键字就不那么容易实现了,而使用Lock却容易许多。

2.1.2 实现思想

        需要实现锁的功能,两个必备元素:

        1)一个是表示(锁)状态的变量(我们假设0表示没有线程获取锁,1表示已有线程占有锁),该变量必须声明为voaltile类型;

        2)另一个是队列,队列中的节点表示因未能获取锁而阻塞的线程。

        为了解决多核处理器下多线程缓存不一致的问题,表示状态的变量必须声明为voaltile类型,并且对表示状态的变量和队列的某些操作要保证原子性和可见性。原子性和可见性的操作主要通过Atomic包中的方法实现。

        线程获取锁的大致过程(这里没有考虑可重入和获取锁过程被中断或超时的情况)

        1. 读取表示锁状态的变量

        2. 如果表示状态的变量的值为0,那么当前线程尝试将变量值设置为1(通过CAS操作完成),当多个线程同时将表示状态的变量值由0设置成1时,仅一个线程能成功,其它线程都会失败:

        若成功,表示获取了锁,

        1)如果该线程(或者说节点)已位于该队列中,则将其出列(并将下一个节点则变成了队列的头节点)

        2)如果该线程未入列,则不用对队列进行维护

然后当前线程从lock方法中返回,对共享资源进行访问。

        若失败,则当前线程将自身放入等待(锁的)队列中并阻塞自身,此时线程一直被阻塞在lock方法中,没有从该方法中返回(被唤醒后仍然在lock方法中,并从下一条语句继续执行,这里又会回到第1步重新开始)

        3. 如果表示状态的变量的值为1,那么将当前线程放入等待队列中,然后将自身阻塞(被唤醒后仍然在lock方法中,并从下一条语句继续执行,这里又会回到第1步重新开始)

线程释放锁的大致过程

        1. 释放锁的线程将状态变量的值从1设置为0,并唤醒等待(锁)队列中的队首节点,释放锁的线程从就从unlock方法中返回,继续执行线程后面的代码

        2. 被唤醒的线程(队列中的队首节点)和可能和未进入队列并且准备获取的线程竞争获取锁,重复获取锁的过程

        注意:可能有多个线程同时竞争去获取锁,但是一次只能有一个线程去释放锁,队列中的节点都需要它的前一个节点将其唤醒,例如有队列A<-B-C,即由A释放锁时唤醒B,B释放锁时唤醒C

2.1.3 Lock接口提供的synchronized关键字所不具备的主要特性

特性

描述

尝试非阻塞地获取锁

当前线程尝试获取锁,如果此时锁没被其他线程获取到,则当前线程获取锁成功

响应中断地获取锁

当获取到锁的线程被中断时,中断异常将抛出,同时释放锁

超时地获取锁

在给定的时间内获取锁,如果超出给定时间仍未获取到锁,方法仍要返回

2.1.4 公平锁和非公平锁

        锁可以分为公平锁和不公平锁,重入锁和非重入锁(关于重入锁的介绍会在ReentrantLock源代码分析中介绍),以上过程实际上是非公平锁的获取和释放过程。

        公平锁严格按照先来后到的顺去获取锁,而非公平锁允许插队获取锁。

        公平锁获取锁的过程上有些不同,在使用公平锁时,某线程想要获取锁,不仅需要判断当前表示状态的变量的值是否为0,还要判断队列里是否还有其他线程,若队列中还有线程则说明当前线程需要排队,进行入列操作,并将自身阻塞;若队列为空,才能尝试去获取锁。而对于非公平锁,当表示状态的变量的值是为0,就可以尝试获取锁,不必理会队列是否为空,这样就实现了插队获取锁的特点。通常来说非公平锁的吞吐率比公平锁要高,我们一般常用非公平锁。

        这里需要解释一点,什么情况下才会出现,表示锁的状态的变量的值是为0而且队列中仍有其它线程等待获取锁的情况。

        假设有三个线程A、B、C。A线程为正在运行的线程并持有锁,队列中有一个C线程,位于队首。现在A线程要释放锁,具体执行的过程操作可分为两步:

        1. 将表示锁状态的变量值由1变为0,

        2. C线程被唤醒,这里要明确两点:

        (1)C线程被唤醒并不代表C线程开始执行,C线程此时是处于就绪状态,要等待操作系统的调度

        (2)C线程目前还并未出列,C线程要进入运行状态,并且通过竞争获取到锁以后才会出列。

        如果C线程此时还没有进入运行态,同时未在队列中的B线程进行获取锁的操作,B就会发现虽然当前没有线程持有锁,但是队列不为空(C线程仍然位于队列中),要满足先来后到的特点(B在C之后执行获取锁的操作),B线程就不能去尝试获取锁,而是进行入列操作。

2.2 Lock接口的方法

2.2.1 lock() 方法

        lock() 是最基础的获取锁的方法。在线程获取锁时如果锁已被其他线程获取,则进行等待,是最初级的获取锁的方法。

                对于 Lock 接口而言,获取锁和释放锁都是显式的,不像 synchronized 那样是隐式的,所以 Lock 不会像 synchronized 一样在异常时自动释放锁(synchronized 即使不写对应的代码也可以释放),lock的加锁和释放锁都必须以代码的形式写出来,所以使用 lock() 时必须由我们自己主动去释放锁,因此最佳实践是执行 lock() 后,首先在 try{} 中操作同步资源,如果有必要就用 catch{} 块捕获异常,然后在 finally{} 中释放锁,以保证发生异常时锁一定被释放,示例代码如下所示。

Lock lock = ...;
lock.lock();
try{
    //获取到了被本锁保护的资源,处理任务
    //捕获异常
}finally{
    lock.unlock();   //释放锁
}

        在这段代码中我们创建了一个 Lock,并且用 Lock 方法加锁,然后立刻在 try 代码块中进行相关业务逻辑的处理,如果有需要还可以进行 catch 来捕获异常,但是最重要的是 finally,大家一定不要忘记在 finally 中添加 unlock() 方法,以便保障锁的绝对释放。

        如果我们不遵守在 finally 里释放锁的规范,就会让 Lock 变得非常危险,因为你不知道未来什么时候由于异常的发生,导致跳过了 unlock() 语句,使得这个锁永远不能被释放了,其他线程也无法再获得这个锁,这就是 Lock 相比于 synchronized 的一个劣势,使用 synchronized 时不需要担心这个问题。

        与此同时,lock() 方法不能被中断,这会带来很大的隐患:一旦陷入死锁,lock() 就会陷入永久等待,所以一般我们用 tryLock() 等其他更高级的方法来代替 lock(),下面我们就看一看 tryLock() 方法。

2.2.2 tryLock()

        tryLock() 用来尝试获取锁,如果当前锁没有被其他线程占用,则获取成功,返回 true,否则返回 false,代表获取锁失败。相比于 lock(),这样的方法显然功能更强大,我们可以根据是否能获取到锁来决定后续程序的行为。

                因为该方法会立即返回,即便在拿不到锁时也不会一直等待,所以通常情况下,我们用 if 语句判断 tryLock() 的返回结果,根据是否获取到锁来执行不同的业务逻辑,典型使用方法如下。

Lock lock = ...;
if(lock.tryLock()) {
     try{
         //处理任务
     }finally{
         lock.unlock();   //释放锁
     } 
}else {
    //如果不能获取锁,则做其他事情
}

        我们创建 lock() 方法之后使用 tryLock() 方法并用 if 语句判断它的结果,如果 if 语句返回 true,就使用 try finally 完成相关业务逻辑的处理,如果 if 语句返回 false 就会进入 else 语句,代表它暂时不能获取到锁,可以先去做一些其他事情,比如等待几秒钟后重试,或者跳过这个任务,有了这个强大的 tryLock() 方法我们便可以解决死锁问题,代码如下所示。

    public void tryLock(Lock lock1, Lock lock2) throws InterruptedException {
        while (true) {
            if (lock1.tryLock()) {
                try {
                    if (lock2.tryLock()) {
                        try {
                            System.out.println("获取到了两把锁,完成业务逻辑");
                            return;
                        } finally {
                            lock2.unlock();
                        }
                    }
                } finally {
                    lock1.unlock();
                }
            } else {
                Thread.sleep(new Random().nextInt(1000));
            }
        }
    }

        如果代码中我们不用 tryLock() 方法,那么便可能会产生死锁,比如有两个线程同时调用这个方法,传入的 lock1 和 lock2 恰好是相反的,那么如果第一个线程获取了 lock1 的同时,第二个线程获取了 lock2,它们接下来便会尝试获取对方持有的那把锁,但是又获取不到,于是便会陷入死锁,但是有了 tryLock() 方法之后,我们便可以避免死锁的发生,首先会检测 lock1 是否能获取到,如果能获取到再尝试获取 lock2,但如果 lock1 获取不到也没有关系,我们会在下面进行随机时间的等待,这个等待的目标是争取让其他的线程在这段时间完成它的任务,以便释放其他线程所持有的锁,以便后续供我们使用,同理如果获取到了 lock1 但没有获取到 lock2,那么也会释放掉 lock1,随即进行随机的等待,只有当它同时获取到 lock1 和 lock2 的时候,才会进入到里面执行业务逻辑,比如在这里我们会打印出“获取到了两把锁,完成业务逻辑”,然后方法便会返回。

2.2.3 tryLock(long time, TimeUnit unit)

        tryLock() 的重载方法是 tryLock(long time, TimeUnit unit),这个方法和 tryLock() 很类似,区别在于 tryLock(long time, TimeUnit unit) 方法会有一个超时时间,在拿不到锁时会等待一定的时间,如果在时间期限结束后,还获取不到锁,就会返回 false;如果一开始就获取锁或者等待期间内获取到锁,则返回 true。

        这个方法解决了 lock() 方法容易发生死锁的问题,使用 tryLock(long time, TimeUnit unit) 时,在等待了一段指定的超时时间后,线程会主动放弃这把锁的获取,避免永久等待;在等待的期间,也可以随时中断线程,这就避免了死锁的发生。

2.2.4 lockInterruptibly()

        这个方法的作用就是去获取锁,如果这个锁当前是可以获得的,那么这个方法会立刻返回,但是如果这个锁当前是不能获得的(被其他线程持有),那么当前线程便会开始等待,除非它等到了这把锁或者是在等待的过程中被中断了,否则这个线程便会一直在这里执行这行代码。一句话总结就是,除非当前线程在获取锁期间被中断,否则便会一直尝试获取直到获取到为止。

        顾名思义,lockInterruptibly() 是可以响应中断的。相比于不能响应中断的 synchronized 锁,lockInterruptibly() 可以让程序更灵活,可以在获取锁的同时,保持对中断的响应。我们可以把这个方法理解为超时时间是无穷长的 tryLock(long time, TimeUnit unit),因为 tryLock(long time, TimeUnit unit) 和 lockInterruptibly() 都能响应中断,只不过 lockInterruptibly() 永远不会超时。

        这个方法本身是会抛出 InterruptedException 的,所以使用的时候,如果不在方法签名声明抛出该异常,那么就要写两个 try 块,如下所示:

public void lockInterruptibly() {
    try {
        lock.lockInterruptibly();
        try {
            System.out.println("操作资源");
        } finally {
            lock.unlock();
        }
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
}

2.2.5 unlock()

                是用于解锁的,unlck()方法比较简单,对于 ReentrantLock 而言,执行 unlock() 的时候,内部会把锁的“被持有计数器”减 1,直到减到 0 就代表当前这把锁已经完全释放了,如果减 1 后计数器不为 0,说明这把锁之前被“重入”了,那么锁并没有真正释放,仅仅是减少了持有的次数。

2.3 Lock的接口实现类

2.3.1 ReentrantLock类(重入锁/递归锁) - Lock接口实现类

public class ReentrantLock extends Object implements Lock, Serializable

1 主要方法

构造方法:
ReentrantLock() //创建一个 ReentrantLock 的实例
ReentrantLock(boolean fair) //创建一个具有给定公平策略的 ReentrantLock 
普通方法:
int getHoldCount() //查询当前线程保持此锁的次数
protected Thread getOwner() //返回目前拥有此锁的线程,如果此锁不被任何线程拥有,则返回 null 
protected Collection<Thread> getQueuedThreads() //返回一个collection,它包含可能正等待获取此锁的线程 
int getQueueLength() //返回正等待获取此锁的线程估计数 
protected Collection<Thread> getWaitingThreads(Condition condition) //返回一个 collection,它包含可能正在等待与此锁相关给定条件的那些线程 
int getWaitQueueLength(Condition condition) //返回等待与此锁相关的给定条件的线程估计数
boolean hasQueuedThread(Thread thread) //查询给定线程是否正在等待获取此锁
boolean hasQueuedThreads() //查询是否有些线程正在等待获取此锁
boolean hasWaiters(Condition condition) //查询是否有些线程正在等待与此锁有关的给定条件
boolean isFair() //如果此锁的公平设置为 true,则返回true 
boolean isHeldByCurrentThread() //查询当前线程是否保持此锁
boolean isLocked() //查询此锁是否由任意线程保持
void lock() //获取锁
void lockInterruptibly() //如果当前线程未被中断,则获取锁。
Condition newCondition() //返回用来与此 Lock 实例一起使用的 Condition 实例 
boolean tryLock() //仅在调用时锁未被另一个线程保持的情况下,才获取该锁
boolean tryLock(long timeout, TimeUnit unit) //如果锁在给定等待时间内没有被另一个线程保持,且当前线程未被中断,则获取该锁 
void unlock() //试图释放此锁

2 注意事项

        Lock接口的实现类,与synchronized一样具有互斥锁功能。 注意:

        1)使用Lock,需要显式地获取锁和释放锁;

        2)为了避免拿到锁的线程在运行期间出现异常,导致程序终止没有释放锁!应用try-finally来保证无论是否出现异常,最终必须释放锁;

        3)ReentrantLock为重入锁,避免递归,如果必须递归,必须正确控制退出条件。 (此锁支持同一线程的2147483647个递归锁的最大值。试图在Error超过这个限制的结果将从锁定的方法。)

2.3.2 ReentrantReadWriteLock类(读写锁) - ReadWriteLock接口实现类

1 主要方法

   /** 读锁 */
    private final ReentrantReadWriteLock.ReadLock readerLock;
    /** 写锁 */
    private final ReentrantReadWriteLock.WriteLock writerLock;
    final Sync sync;
    /** 使用默认(非公平)的排序属性创建一个新的 ReentrantReadWriteLock */
    public ReentrantReadWriteLock() {
        this(false);
    }
    /** 使用给定的公平策略创建一个新的 ReentrantReadWriteLock */
    public ReentrantReadWriteLock(boolean fair) {
        sync = fair ? new FairSync() : new NonfairSync();
        readerLock = new ReadLock(this);
        writerLock = new WriteLock(this);
    }
    /** 返回用于写入操作的锁 */
    public ReentrantReadWriteLock.WriteLock writeLock() { return writerLock; }
    /** 返回用于读取操作的锁 */
    public ReentrantReadWriteLock.ReadLock  readLock()  { return readerLock; }
    abstract static class Sync extends AbstractQueuedSynchronizer {}
    static final class NonfairSync extends Sync {}
    static final class FairSync extends Sync {}
    public static class ReadLock implements Lock, java.io.Serializable {}
    public static class WriteLock implements Lock, java.io.Serializable {}

2 注意事项

        一种支持一写多读的同步锁,读写分离,可分别分配读锁、写锁;

        支持多次分配读锁,使多个读操作可以并发执行(写锁同步,读锁异步)。

        互斥规则:

        ① 写-写:互斥,阻塞;

        ② 读-写:互斥,读阻塞写、写阻塞读;

        ③ 读-读:不互斥、不阻塞;

        在读操作远远高于写操作的环境下,可在保证线程安全的情况下,提高运行效率。

3 简单用例

3.1 如何使用ReentrantLock


public class LockTest1 {
    private Lock lock = new ReentrantLock();

    public static void main(String[] args) {
        final LockTest2 test = new LockTest2();

        new Thread(){
            public void run(){
                test.insert(Thread.currentThread());
            }
        }.start();

        new Thread(){
            public void run(){
                test.insert(Thread.currentThread());
            }
        }.start();
    }

    public void insert(Thread thread){
        lock.lock();
        try {
            System.out.println(thread.getName() + "得到了锁");
            for (int i = 0; i < 5; i++) {
                System.out.println("ThreadName=" + Thread.currentThread().getName() + (" " + (i + 1)));
            }
        } catch (Exception e) {
        } finally {
            System.out.println(thread.getName() + "释放了锁");
            lock.unlock();
        }
    }
}

3.2 ReentrantReadWriteLock的简单使用

public class TestReadWriteLock {
      public static void main(String[] args) {
            Student s = new Student();
            ExecutorService es = Executors.newFixedThreadPool(20);
            WriteTask write = new WriteTask(s);
            ReadTask read = new ReadTask(s);
            
            long start = System.currentTimeMillis();
            es.submit(write);
            es.submit(write);
            
            for (int i = 1; i <= 18; i++) {
                  es.submit(read);
            }
            
            es.shutdown();
            while(true) {
                  System.out.println("结束了吗?");
                  if (es.isTerminated() == true) {
                        break;
                  }
            }
            
            long end = System.currentTimeMillis();
            System.out.println("运行时间:" + (end-start));
      }
}
class WriteTask implements Callable<Object> {
      Student stu;
      public WriteTask(Student stu) {
            this.stu = stu;
      }
      @Override
      public Object call() throws Exception {
            this.stu.setAge(20);
            return null;
      }
}
class ReadTask implements Callable<Object> {
      Student stu;
      public ReadTask(Student stu) {
            this.stu = stu;
      }
      @Override
      public Object call() throws Exception {
            this.stu.getAge();
            return null;
      }
}
class Student {
      private int age;
      //Lock lock = new ReentrantLock(); // 读写情况下都加锁,性能过低!
      ReentrantReadWriteLock rrwl = new ReentrantReadWriteLock(); // 有两把锁
      ReadLock read = rrwl.readLock(); // 读锁 - 内部类
      WriteLock write = rrwl.writeLock(); // 写锁 - 内部类
      // 赋值:写操作
      public void setAge(int age) throws InterruptedException {
            //lock.lock();
            write.lock();
            try {
                  Thread.sleep(1000);
                  this.age = age;
            } finally {
                  //lock.unlock();
                  write.unlock();
            }
      }
      // 取值:读操作
      public int getAge() throws InterruptedException {
            //lock.lock();
            read.lock();
            try {
                  Thread.sleep(1000);
                  return this.age;
            } finally {
                  //lock.unlock();
                  read.lock();
            }
      }
}
// Lock互斥锁运行时间20034毫秒,ReadWriteLock读写锁运行时间3004毫秒

4 常见问题详解

4.1 ReentrantReadWriteLock类和ReentrantLock类的区别

当有很多线程都从某个数据结构中读取数据而很少有线程对其进行修改时,后者就很有用了。在这种情况下,允许读取器线程共享访问是合适的。当然,写入器线程依然必须是互斥访问的

下面是使用读/写锁的必要步骤:

        (1) 创建一个ReentrantReadWriteLock对象

 private ReentrantReadWriteLock rwl = new ReentrantReadWriteLock();

        (2)抽取读锁和写锁:

private Lock readLock = rwl.readLock();//得到一个可被多个读操作共用的读锁,但它会排斥所有写操作
private Lock writeLock = rwl.writeLock();//得到一个写锁,它会排斥所有其他的读操作和写操作

        (3) 对所有访问者加读锁

public double getTotalBalance(){
    readLock.lock();
    try{...};
    finally{readLock.unlock();}
}

        对所有修改者加写锁

public void transfer(){
    writeLock.lock();
    try{...};
    finally{writeLock.unlock();}
}

4.2 Lock与synchronized的区别

        1. Lock的加锁和解锁都是由java代码配合native方法(调用操作系统的相关方法)实现的,而synchronize的加锁和解锁的过程是由JVM管理的

        2. 当一个线程使用synchronize获取锁时,若锁被其他线程占用着,那么当前只能被阻塞,直到成功获取锁。而Lock则提供超时锁和可中断等更加灵活的方式,在未能获取锁的 条件下提供一种退出的机制。

        3. 一个锁内部可以有多个Condition实例,即有多路条件队列,而synchronize只有一路条件队列;同样Condition也提供灵活的阻塞方式,在未获得通知之前可以通过中断线程以 及设置等待时限等方式退出条件队列。

        4. synchronize对线程的同步仅提供独占模式,而Lock即可以提供独占模式,也可以提供共享模式

4.3 Reentrantlock类详解

见下篇

4.4 ReentrantReadWriteLock类详解

见下下篇

4.5 Reentrantlock和synchronized区别?

见下下下篇。

4.6 AQS详解

见下下下下篇

常见出现的问题会在后面的文章讨论,一起学习的朋友可以点点关注,会持续更新,文章有帮助的话可以长按点赞有惊喜!!! 收藏转发,有什么补充可以在下面评论,谢谢大家

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

有鹿如溪

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值