一.简介

Java SDK并发包通过Lock和Condition两个接口来实现管程,其中Lock用于解决互斥问题,Condition 用于解决同步问题。

二.原理

2.1 管程

在 Java 的 1.5 版本中,synchronized 性能不如 SDK 里面的 Lock,但 1.6 版本之后,synchronized 做了很多优化,将性能追了上来,所以 1.6 之后的版本又有人推荐使用 synchronized 了。

但是synchronized 自动加锁和解锁,无法解决破坏无可抢占的方案,基于这种情况Lock 应运而生,有三种方案:

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

2.2 可见性

利用volatile相关的Happens-Before规则,JavaSDK里面的ReentrantLock,内部持有一个 volatile 的成员变量 state,获取锁的时候,会读写 state 的值;解锁的时候,也会读写 state 的值(简化后的代码如下面所示)。也就是说,在执行 value+=1 之前,程序先读写了一次 volatile 变量 state,在执行 value+=1 之后,又读写了一次 volatile 变量 state。

class SampleLock {
  volatile int state;
  // 加锁
  lock() {
    // 省略代码无数
    state = 1;
  }
  // 解锁
  unlock() {
    // 省略代码无数
    state = 0;
  }
}
  • 顺序性规则,对于线程T1,value+=1 Happens-Before释放锁的操作unlock();
  • volatile变量规则,由于state = 1会先读取state,所以线程T1的unlock()操作Happens-Before 线程 T2 的 lock() 操作;
  • 传递性规则,线程T1的value+=1 Happens-Before 线程 T2 的 lock() 操作。

2.3 可重入锁

可重入锁的意思,指的是线程可以重复获取同一把锁。

class X {
  private final Lock rtl =
  new ReentrantLock();
  int value;
  public int get() {
    // 获取锁
    rtl.lock();         
    try {
      return value;
    } finally {
      // 保证锁能释放
      rtl.unlock();
    }
  }
  public void addOne() {
    // 获取锁
    rtl.lock();  
    try {
      value = 1 + get(); 
    } finally {
      // 保证锁能释放
      rtl.unlock();
    }
  }
}

2.4 公平锁与非公平锁

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

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

公平锁,唤醒的策略就是谁等待的时间长,就唤醒谁,很公平;如果是非公平锁,则不提供这个公平保证,有可能等待时间短的线程反而先被唤醒。

2.5 用锁的最佳实践

并发大师 Doug Lea《Java 并发编程:设计原则与模式》

  • 永远只在更新对象的成员变量时加锁
  • 永远只在访问可变的成员变量时加锁
  • 永远不在调用其他对象的方法时加锁

2.6 示例

生成消费模型,同步器。

public class AQSLockTest{
    final  static Queue<String> queue = new LinkedBlockingQueue<>();
    final static Lock lock = new ReentrantLock();
    final static Condition full =  lock.newCondition();
    final static Condition empty =  lock.newCondition();
    final static int QUEUE_MAX_SIZE = 3;
    public static void add() throws InterruptedException {
        lock.lock();
        try {
            //队列满了
            while (queue.size() == QUEUE_MAX_SIZE){
                full.await();
            }
            System.out.println("prd:" + "hello");
            queue.add("test1-"+new Random().nextInt(100));
            empty.signalAll();
        }finally {
            lock.unlock();
        }
    }
    public static void  poll() throws InterruptedException {
        lock.lock();
        try {
            // 当队列queue中一个字符串都没有,就将剩下的消费线程丢进enpty对应的队列中
            while (queue.size() == 0) {
                empty.await();
            }
            // 消费队列queue中的字符串
            String poll = queue.poll();
            System.out.println("consumer:" + poll);
            // 消费成功,就唤醒full中所有的生产线程去生产字符串
            full.signalAll();
        }finally {
            lock.unlock();
        }
    }

    public static void main(String[] args) {
        // 生产者线程
        for (int i = 0; i < 5; i++) {
            new Thread(() -> {
                try {
                    add();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }).start();
        }
        // 消费者线程
        for (int i = 0; i < 5; i++) {
            new Thread(() -> {
                try {
                    poll();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }).start();
        }
    }
}

参考

《Java并发编程实战》

公众号

Java并发Lock和Condition_公平锁

微信公众号(bigdata_limeng)