Lock和Condition隐藏在并发包里的管程

Java SDK并发包通过Lock和Condition两个接口来实现管程(管程——并发编程的万能钥匙),其中Lock用于解决互斥问题,Condition用于解决同步问题。

再造管程的理由

既然Java里已经存在管程的实现synchronized并且做了许多优化,为什么还需要在并发包里开发Lock和Condition。原因是synchronized申请资源的时候,如果申请不到,线程直接进入阻塞状态,而线程进入阻塞状态,啥也干不了,也释放不了线程已经占有的资源。

我们希望的是,对于“不可抢占”这个条件,占用部分资源的线程进一步申请其他资源时,如果申请不到,可以主动释放它已经占有的资源,这样不可抢占这个条件就被破坏掉了。

设计一把互斥锁去解决这个问题:

  1. 能够响应中断,synchronized的问题是,持有锁A后,如果尝试获取锁B失败,那么线程就进入阻塞状态,一旦发生死锁就没有任何机会来唤醒阻塞的线程,但是如果阻塞状态的线程能够响应中断信号,也就是说我们给阻塞的线程发送中断信号的时候,能够唤醒它,那它就有机会释放曾经持有的锁A,这样就破坏了不可抢占条件了。
  2. 支持超时,如果线程在一段时间内没有获取到锁,不是进入阻塞状态,而是返回一个错误,那这个线程也有机会释放曾经持有的锁,这样也能破坏不可抢占条件。
  3. 非阻塞地获取锁,如果尝试获取锁失败,并不进入阻塞状态,而是直接返回,那这个线程也有机会释放曾经持有的锁,这样也能破坏不可抢占条件。
// 支持中断的API
void lockInterruptibly() throws InterruptedException;

// 支持超时的API
boolean tryLock(long time,TimeUnit unit) throws InterruptedException;

// 支持非阻塞获取锁的API
boolean tryLock();

使用synchronized发生死锁

private static Object lockA = new Object();
    private static Object lockB = new Object();
    
    private static void task3(){
        synchronized (lockA){
            System.out.println(Thread.currentThread().getName() + "正在执行任务3A...");
            synchronized (lockB){
                System.out.println(Thread.currentThread().getName() + "正在执行任务3B...");
            }
        }
    }
    private static void task4(){
        synchronized (lockB){
            System.out.println(Thread.currentThread().getName() + "正在执行任务4B...");
            synchronized (lockA){
                System.out.println(Thread.currentThread().getName() + "正在执行任务4A...");
            }
        }
    }

public static void main(String[] args) {
        Thread t1 = new Thread(DealLock::task3,"Thread1");
        Thread t2 = new Thread(DealLock::task4,"Thread2");
        t1.start();
        t2.start();
    }

在这里插入图片描述

使用Lock完成相同任务

    private static ReentrantLock lockC  = new ReentrantLock();
    private static ReentrantLock lockD  = new ReentrantLock();
	private static void task1() {
        lockC.tryLock();
        try {
            System.out.println(Thread.currentThread().getName() + "正在执行任务1A...");
            lockD.tryLock();
            System.out.println(Thread.currentThread().getName() + "正在执行任务1B...");
        }finally {
            lockD.unlock();
            lockC.unlock();
        }
    }
    private static void task2() {
        lockD.tryLock();
        try {
            System.out.println(Thread.currentThread().getName() + "正在执行任务2A...");
            lockC.tryLock();
            System.out.println(Thread.currentThread().getName() + "正在执行任务2B...");
        }finally {
            lockC.unlock();
            lockD.unlock();
        }
    }
        Thread t3 = new Thread(DealLock::task1,"Thread1");
        Thread t4 = new Thread(DealLock::task2,"Thread2");
        t3.start();
        t4.start();

当获取不到锁的时候抛出错误,避免死锁发生。
在这里插入图片描述

公平锁与非公平锁

在使用ReentrantLock的时候会发现这个类有两个构造函数,一个是无参构造函数,一个是传入fair参数的构造函数。fair参数代表的是锁的公平策略,如果传入true就表示需要构造一个公平锁,反之则表示要构造一个非公平锁。


//无参构造函数:默认非公平锁
public ReentrantLock() {
    sync = new NonfairSync();
}
//根据公平策略参数创建锁
public ReentrantLock(boolean fair){
    sync = fair ? new FairSync() 
                : new NonfairSync();
}

锁都对应一个管程,管程中介绍过入口等待队列,如果一个线程没有获得锁,就会进入等待队列,当有线程释放锁的时候,就需要从等待队列中唤醒一个等待的线程。如果是公平锁,唤醒的策略就是谁等待的时间长,就唤醒谁,很公平;如果是非公平锁,则不提供这个公平保证,有可能等待时间段的线程反而先被唤醒。

非公平锁演示

 	private static ReentrantLock notFairLock = new ReentrantLock();
    private static void task(){
        notFairLock.lock();
        try {
            System.out.println(Thread.currentThread().getName()+"线程正在执行任务");
            System.out.println("当前等待线程个数:" + notFairLock.getQueueLength());
            Thread.sleep(10);
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            notFairLock.unlock();
        }
    }

    public static void main(String[] args) throws InterruptedException {
    	// 启动50个线程排队去访问共享资源
        for (int i = 0; i < 50; i++) {
            new Thread(FairLockDemo::task,String.valueOf(i)).start();
        }
    }

结果分析:可以看到线程3抢到了线程2前面运行,由此可知在线程1刚好释放了锁时,被线程3抢占,入口等待队列中的线程2没有抢占到。

第一次运行结果

0线程正在执行任务
当前等待线程个数:0
1线程正在执行任务
当前等待线程个数:48
3线程正在执行任务
当前等待线程个数:47
2线程正在执行任务
当前等待线程个数:46
...
48线程正在执行任务
当前等待线程个数:1
49线程正在执行任务
当前等待线程个数:0

Process finished with exit code 0

第二次运行结果

19线程正在执行任务
当前等待线程个数:30
20线程正在执行任务
当前等待线程个数:29
21线程正在执行任务
当前等待线程个数:28
13线程正在执行任务
当前等待线程个数:27
23线程正在执行任务
当前等待线程个数:26

用锁的最佳实践

  1. 永远只在更新对象的成员变量时加锁;
  2. 永远只在访问可变的成员变量时加锁;
  3. 永远不再调用其他对象的方法时加锁;

Condition条件变量

如何利用两个条件变量快速实现阻塞队列


public class BlockedQueue<T>{
  final Lock lock =
    new ReentrantLock();
  // 条件变量:队列不满  
  final Condition notFull =
    lock.newCondition();
  // 条件变量:队列不空  
  final Condition notEmpty =
    lock.newCondition();

  // 入队
  void enq(T x) {
    lock.lock();
    try {
      while (队列已满){
        // 等待队列不满
        notFull.await();
      }  
      // 省略入队操作...
      //入队后,通知可出队
      notEmpty.signal();
    }finally {
      lock.unlock();
    }
  }
  // 出队
  void deq(){
    lock.lock();
    try {
      while (队列已空){
        // 等待队列不空
        notEmpty.await();
      }  
      // 省略出队操作...
      //出队后,通知可入队
      notFull.signal();
    }finally {
      lock.unlock();
    }  
  }
}

注意:在Lock&Condition实现的管程里只能使用await、signal()、signalAll,而wait、notify、botifyAll只有在synchronized实现的管程里才能使用。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值