后端精进笔记07:线程中的lock锁

423 阅读5分钟

Lock接口是另外一种更加优秀的并发锁设计,调用其lock、unlock方法就可以实现加锁、解锁,需要注意的是,lock、unlock需要成对出现(lock2次,则也需要unlock2次,如果只unlock一次,后续的其他线程也无法再次获取锁),即加锁次数与解锁次数相同,才算是完全释放锁。

Lock接口有如下2个常用到实现类:ReentrantLock、ReadWriteLock中的readerLock、writerLock

一、 ReentrantLock

ReentrantLock是独享锁,支持公平锁、非公平锁,也是可重入锁。

1.1 重入锁示例

/*
执行结果:
外部方法开始执行
内部方法开始执行
内部方法结束执行
外部方法结束执行
*/
public class Demo312 {
    private static ReentrantLock lock = new ReentrantLock();
    public static void main(String[] args) {
        lock.lock();
        try {
            System.err.println("外部方法开始执行");
            Thread.sleep(100);
            demoMethod();
            System.err.println("外部方法结束执行");
        } catch (InterruptedException e) {
            e.printStackTrace();
        }finally {
            lock.unlock();
        }
    }

    private static void demoMethod(){
        lock.lock();
        try {
            System.err.println("内部方法开始执行");
            Thread.sleep(100);
            System.err.println("内部方法结束执行");
        } catch (InterruptedException e) {
            e.printStackTrace();
        }finally {
            lock.unlock();
        }
    }
}

1.2 lock2次,只unlock1次的示例:

/*
执行结果(出现死锁,程序未能正常结束):
子线程执行2次加锁
主线程尝试获取锁……
子线程执行1次解锁
*/
public class Demo313 {
    private static ReentrantLock lock = new ReentrantLock();
    public static void main(String[] args) {
        try {
            Thread.sleep(100);
            new Thread(new Runnable() {
                @Override
                public void run() {
                    try {
                        System.err.println("子线程执行2次加锁");
                        lock.lock();
                        lock.lock();
                    } finally {
                        System.err.println("子线程执行1次解锁");
                        lock.unlock();
                      	//lock.unlock();
                    }
                }
            }).start();
            System.err.println("主线程尝试获取锁……");
            lock.lock();
            System.err.println("主线程获取到锁");
        } catch (InterruptedException e) {
            e.printStackTrace();
        }finally {
            System.err.println("主线程释放锁");
            lock.unlock();
        }
    }
}

二、读写锁 ReadWriteLock

ReadWriteLock其实是维护了一对组合锁,改进了独占锁,适合读操作多于写操作的场景。

  • readerLock:读锁,只用于读取操作,可多个线程同时持有(我读的时候,你可以读不可以写);
  • writerLock:写锁,只用于写入操作,只能单个线程持有(我写的时候,你不可读不可写);

下面分别使用独占锁、读写锁进行读写操作,可以明显看到,读写锁的性能更高。

2.1 使用常规独占锁进行读写操作

public class Demo314 {

    public static void main(String[] args) {
        new Thread(Demo314::read).start();
        new Thread(Demo314::read).start();
        new Thread(Demo314::write).start();
    }

    private synchronized static void read(){
        long l = System.currentTimeMillis();
        System.err.println(Thread.currentThread().getName()+"正在执行读操作……");
        while(System.currentTimeMillis()-l<1000L){

        }
        System.err.println(Thread.currentThread().getName()+"读操作执行完毕");
    }

    private synchronized static void write(){
        long l = System.currentTimeMillis();
        System.err.println(Thread.currentThread().getName()+"正在执行写操作……");
        while(System.currentTimeMillis()-l<1000L){

        }
        System.err.println(Thread.currentThread().getName()+"写操作执行完毕");
    }
}

可以看到,读操作是不能并行执行的,执行结果:

Thread-0正在执行读操作……
Thread-0读操作执行完毕
Thread-1正在执行读操作……
Thread-1读操作执行完毕
Thread-2正在执行写操作……
Thread-2写操作执行完毕

2.2 使用读写锁进行读写操作

public class Demo315 {
    private final static ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
    private final static Lock readLock = lock.readLock();
    private final static Lock writeLock = lock.writeLock();

    public static void main(String[] args) {
        new Thread(Demo315::read).start();
        new Thread(Demo315::read).start();
        new Thread(Demo315::write).start();
    }

    private static void read(){
        try {
            readLock.lock();
            long l = System.currentTimeMillis();
            System.err.println(Thread.currentThread().getName()+"正在执行读操作……");
            while(System.currentTimeMillis()-l<1000L){

            }
            System.err.println(Thread.currentThread().getName()+"读操作执行完毕");
        } finally {
            readLock.unlock();
        }

    }

    private static void write(){
        try {
            writeLock.lock();
            long l = System.currentTimeMillis();
            System.err.println(Thread.currentThread().getName()+"正在执行写操作……");
            while(System.currentTimeMillis()-l<1000L){

            }
            System.err.println(Thread.currentThread().getName()+"写操作执行完毕");
        } finally {
            writeLock.unlock();
        }
    }
}

可以看到,读操作是在并行(交替)执行的,执行结果:

Thread-0正在执行读操作……
Thread-1正在执行读操作……
Thread-0读操作执行完毕
Thread-1读操作执行完毕
Thread-2正在执行写操作……
Thread-2写操作执行完毕

2.3 锁降级

锁降级指的是线程在写操作执行完毕之后,再获取到读锁,将持有的写锁释放的过程。只有锁降级,不存在锁升级。官方给出如下锁降级示例:

class CachedData {
   Object data;
   volatile boolean cacheValid;
   final ReentrantReadWriteLock rwl = new ReentrantReadWriteLock();

   void processCachedData() {
     rwl.readLock().lock();
     if (!cacheValid) {
        // Must release read lock before acquiring write lock
        rwl.readLock().unlock();
        rwl.writeLock().lock();
        try {
          // Recheck state because another thread might have
          // acquired write lock and changed state before we did.
          if (!cacheValid) {
            data = ...
            cacheValid = true;
          }
          // Downgrade by acquiring read lock before releasing write lock
          rwl.readLock().lock();
        } finally {
          rwl.writeLock().unlock(); // Unlock write, still hold read
        }
     }

     try {
       use(data);
     } finally {
       rwl.readLock().unlock();
     }
   }
 }

三、Condition中的await、signal与signalAll

synchronized锁对应有waitnotifynotifyAll方法,用于休眠单个、唤醒单个或全部线程,而Condition则是配合Lock使用的,但是一个Lock对象可以对应有多个Condition,这就提供了更多的集合(用于存放不同类型的等待线程),这就拥有了更细粒度的、更加精确的线程控制(notify是随机唤醒,而使用condition就可以有选择地唤醒某一类线程)。

一个典型的场景是阻塞队列:阻塞队列与普通队列的区别在于,当队列是空的时,从队列中获取元素的操作将会被阻塞,或者当队列是满时,往队列里添加元素的操作会被阻塞。试图从空的阻塞队列中获取元素的线程将会被阻塞,直到其他的线程往空的队列插入新的元素。同样,试图往已满的阻塞队列中添加新元素的线程同样也会被阻塞,直到其他的线程使队列重新变得空闲起来,如从队列中移除一个或者多个元素,或者完全清空队列,下图展示了如何通过阻塞队列是如何工作的:

01

手动模拟一个简单的队列

public class Demo316 {

    private static final ReentrantLock lock = new ReentrantLock();
    // 读condition
    private static final Condition readCondition = lock.newCondition();
    // 写condition
    private static final Condition writeCondition = lock.newCondition();

    private static int count = 2;
    private static List<Integer> dataArray = new ArrayList<>();


    public static void main(String[] args) throws InterruptedException {
        new Thread(Demo316::read).start();
        new Thread(Demo316::read).start();
        new Thread(Demo316::read).start();
        Thread.sleep(100L);
        new Thread(Demo316::write).start();
        new Thread(Demo316::write).start();
        new Thread(Demo316::write).start();
    }

    private static void read(){
        try {
            lock.lock();
            while(dataArray.size() == 0){
                readCondition.await();
            }
            Integer num = dataArray.remove(0);
            System.err.println(" -> ️"+Thread.currentThread().getName()+"执行读操作:"+num);
            writeCondition.signalAll();
        } catch (Exception e) {
            e.printStackTrace();
        }finally {
            lock.unlock();
        }
    }

    private static void write(){
        try {
            lock.lock();
            while(count <= dataArray.size()){
                writeCondition.await();
            }
            int num = (int) (Math.random() * 20);
            System.err.println(" <- "+Thread.currentThread().getName()+"️执行写操作:"+num);
            dataArray.add(num);
            readCondition.signalAll();
        } catch (Exception e) {
            e.printStackTrace();
        }finally {
            lock.unlock();
        }
    }
}

执行结果:

 <- Thread-3️执行写操作:4
 <- Thread-5️执行写操作:17
 -> ️Thread-0执行读操作:4
 -> ️Thread-1执行读操作:17
 <- Thread-4️执行写操作:17
 -> ️Thread-2执行读操作:17