多线程编程核心技术(十一)Lock
两大核心问题:一个是互斥,即同一时刻只允许一个线程访问共享资源;另一个是同步,即线程之间如何通信、协作。这两大问题,管程都是能够解决的。Java SDK 并发包通过 Lock 和 Condition 两个接口来实现管程,其中 Lock 用于解决互斥问题,Condition 用于解决同步问题。
Java 语言本身提供的 synchronized 也是管程的一种实现,既然 Java 从语言层面已经实现了管程了,那为什么还要在 SDK 里提供另外一种实现呢?
在 Java 的 1.5 版本中,synchronized 性能不如 SDK 里面的 Lock,但 1.6 版本之后,synchronized 做了很多优化,将性能追了上来,所以 1.6 之后的版本又有人推荐使用 synchronized 了。
问题就是synchronized没有办法解决“破坏不可抢占条件方案”。 原因是synchronized 申请资源的时候,如果申请不到,线程直接进入阻塞状态了,而线程进入阻塞状态,啥都干不了,也释放不了线程已经占有的资源。synchronized的加锁释放操作由JVM为我们进行。
如果我们重新设计一把互斥锁去解决这个问题,应该具有以下的能力
1.能够响应interrupt:如果一个线程进入了死锁的阶段是需要进行中断进行锁的释放的。
2.支持超时。如果线程一段时间无法获得到锁,返回一个错误,也释放曾持有的锁,这样也能破坏不可抢占条件
3.非阻塞地获取锁。如果尝试获取锁失败,并不进入阻塞状态,而是直接返回,那这个线程也有机会释放曾经持有的锁。这样也能破坏不可抢占条件。
在Lock接口中就实现了这些
boolean tryLock(long time, TimeUnit unit) throws InterruptedException; void lockInterruptibly() throws InterruptedException; boolean tryLock();
Java Sdk里面的锁Lock保证可见性:利用了volatile相关的Happens-Before规则。获取锁的时候,会读写 state 的值;解锁的时候,也会读写 state 的值(简化后的代码如下面所示)。
class SampleLock { volatile int state; // 加锁 lock() { // 省略代码无数 state = 1; } // 解锁 unlock() { // 省略代码无数 state = 0; } }
也就是说,在执行 value+=1 之前,程序先读写了一次 volatile 变量 state,在执行 value+=1 之后,又读写了一次 volatile 变量 state。根据相关的 Happens-Before 规则:
顺序性规则:对于线程 T1,value+=1 发生在 释放锁的操作 unlock() 之前;
volatile 变量规则:由于 state = 1 会先读取 state,所以线程 T1 的 unlock() 操作 发生在 线程 T2 的 lock() 操作 之前;
传递性规则:线程 T1 的 value+=1 发生在 线程 T2 的 lock() 操作 之前。
所以说,后续线程 T2 能够看到 value 的正确结果。
什么是可重入锁
同一个线程可以重复获取同一把锁,并且是安全的。
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(); } } }
例如下面代码中,当线程 T1 执行到 ① 处时,已经获取到了锁 rtl ,当在 ① 处调用 get() 方法时,会在 ② 再次对锁 rtl 执行加锁操作。此时,如果锁 rtl 是可重入的,那么线程 T1 可以再次加锁成功;如果锁 rtl 是不可重入的,那么线程 T1 此时会被阻塞。
推荐的三个用锁的最佳实践,它们分别是:
- 永远只在更新对象的成员变量时加锁
- 永远只在访问可变的成员变量时加锁
- 永远不在调用其他对象的方法时加锁
活锁问题
class Account { private int balance; private final Lock lock = new ReentrantLock(); // 转账 void transfer(Account tar, int amt){ while (true) { if(this.lock.tryLock()) { try { if (tar.lock.tryLock()) { try { this.balance -= amt; tar.balance += amt; } finally { tar.lock.unlock(); } }//if } finally { this.lock.unlock(); } }//if }//while }//transfer }
1.线程A给B转账,B给A转账,两个都有自己的锁,没有对象的锁
2.一段时间后释放锁,重复1步骤
Lock接口实现了互斥,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(); } } }
线程之间的通信不管是syn还是lock都是对对象进行的
线程等待和通知需要调用 await()、signal()、signalAll(),它们的语义和 wait()、notify()、notifyAll() 是相同的。但是不一样的是,Lock&Condition 实现的管程里只能使用前面的 await()、signal()、signalAll(),而后面的 wait()、notify()、notifyAll() 只有在 synchronized 实现的管程里才能使用。
public class demo13 { private final Object lock = new Object(); public void add() throws InterruptedException { synchronized (lock){ System.out.println(Thread.currentThread().getName()+" is in"); System.out.println(Thread.currentThread().getName()+" is wait"); lock.wait(2000); lock.notifyAll(); System.out.println(Thread.currentThread().getName()+" is notify"); } } public static void main(String[] args) throws InterruptedException { demo13 demo13 = new demo13(); for (int i = 0; i < 20; i++) { Thread thread = new Thread(() -> { try { demo13.add(); } catch (InterruptedException e) { e.printStackTrace(); } }); thread.start(); thread.join(); } } }
Java 程序支持异步的两种实现方式: 1、调用方创建一个子线程,在子线程中执行方法调用,这种调用我们称为异步调用。 2、方法实现的时候,创建一个新的线程执行主要逻辑,主线程直接return,这种方法我们一般称为异步方法。