Java多线程之Lock详解
Lock的工作原理
获得锁
- 从线程中读取表示锁状态的变量
- 如果状态为0,就改为1,如果有多个线程,只会有一个成功
- 如果修改成功就获得了锁,进入维护队列
- 如果失败,则进入等待队列并阻塞自身,此时线程一直被阻塞在lock方法中,没有从该方法中返回
- 如果表示状态的变量的值为1,那么将当前线程放入等待队列中,然后将自身阻塞(被唤醒后仍然在lock方法中,并从下一条语句继续执行,这里又会回到第1步重新开始)
注意: 唤醒并不表示线程能立刻运行,而是表示线程处于就绪状态,仅仅是可以运行而已
释放锁
- 释放锁的线程将状态变量的值从1设置为0,并唤醒等待(锁)队列中的队首节点,释放锁的线程从就从unlock方法中返回,继续执行线程后面的代码
- 被唤醒的线程(队列中的队首节点)和可能和未进入队列并且准备获取的线程竞争获取锁,重复获取锁的过程
注意:可能有多个线程同时竞争去获取锁,但是一次只能有一个线程去释放锁,队列中的节点都需要它的前一个节点将其唤醒,例如有队列A<-B-<C ,即由A释放锁时唤醒B,B释放锁时唤醒C
主要方法和使用
实际上Lock是一个接口,常用的方法有:
//尝试获取锁,获取成功则返回,否则阻塞当前线程 void lock(); //尝试获取锁,线程在成功获取锁之前被中断,则放弃获取锁,抛出异常 void lockInterruptibly() throws InterruptedException; //尝试获取锁,获取锁成功则返回true,否则返回false boolean tryLock(); //尝试获取锁,若在规定时间内获取到锁,则返回true,否则返回false,未获取锁之前被中断,则抛出异常 boolean tryLock(long time, TimeUnit unit) throws InterruptedException; //释放锁 void unlock(); //返回当前锁的条件变量,通过条件变量可以实现类似notify和wait的功能,一个锁可以有多个条件变量 Condition newCondition();
使用方法
多线程下访问(互斥)共享资源时, 访问前加锁,访问结束以后解锁,解锁的操作推荐放入finally块中。
Lock l = ...; //根据不同的实现Lock接口类的构造函数得到一个锁对象 l.lock(); //获取锁位于try块的外面 try { // access the resource protected by this lock } finally { l.unlock(); }
注意,加锁位于对资源访问的try块的外部,特别是使用lockInterruptibly方法加锁时就必须要这样做,这为了防止线程在获取锁时被中断,这时就不必(也不能)释放锁。
try { l.lockInterruptibly();//获取锁失败时不会执行finally块中的unlock语句 try{ // access the resource protected by this lock }finally{ l.unlock(); } } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); }
Lock的实现
Lock有三个实现类,一个是ReentrantLock,另两个是ReentrantReadWriteLock类中的两个静态内部类ReadLock和WriteLock。
- ReentrantLock(可重入锁):互斥锁,次只能一个线程拥有互斥锁,其他线程只有等待
- ReentrantReadWriteLock(读写锁):将读和写分离开两个锁,读锁不互斥,写锁互斥
- 自旋锁:一次只能有一个进程进入临界区,读写锁是自旋锁的一个特例。
ReentrantLock特性
- 轮询锁:在每次请求资源的时候,就用tryLock(),如果资源被占用,就返回,而不是一直阻塞。
- 定时锁:在进行轮询的时候,还可以设置时间,超时后就返回,而不是一直阻塞。
- 公平性:所谓公平锁,线程将按照他们发出请求的顺序来获取锁,不允许插队;但在非公平锁上,则允许插队。
- 可中断锁:lockInterruptibly方法能够在获取锁的同时保持对中断的响应,因此无需创建其它类型的不可中断阻塞操作。
ReentrantLock最大的作用
- 避免死锁
- 使用灵活,自己加锁和解锁
读写锁特性
- 拥有可重入锁特性的情况下,还有下面特性
- 多个读者可以同时进行读
- 写者必须互斥(只允许一个写者写,也不能读者写者同时进行)
- 写者优先于读者(一旦有写者,则后续读者必须等待,唤醒时优先考虑写者)
读写锁最大的作用
- 提高读写速度,当很多读线程,很少写线程时,可以选择使用
应用场景
1. 以队列操作为例:
线程A对队列负责将数据写入队列。须采取“互斥锁”或“读写锁的写锁”
线程B队列负责从队列读出数据。须采取“互斥锁”或“读写锁的写锁”,读队列操作,不可采取“读写锁的读锁”,因为从队列读出数据时,需要更改队列本身的下标索引,如果多个线程同时操作该队列的话,就会导致队列下标索引混乱。
但是对队列的查询操作则最好采取“读写锁的读锁”来提高效率。
Condition接口
一个Condition实例的内部实际上维护了一个队列,队列中的节点表示由于(某些条件不满足而)线程自身调用await方法阻塞的线程。Condition接口中有两个重要的方法,即 await方法和 signal方法。
线程调用这个方法之前该线程必须已经获取了Condition实例所依附的锁。
这样的原因有两个
- 对于await方法,它内部会执行释放锁的操作,所以使用前必须获取锁。
- 对于signal方法,是为了避免多个线程同时调用同一个Condition实例的singal方法时引起的(队列)出列竞争。
Condition方法的执行流程
await方法:
1. 入列到条件队列(注意这里不是等待锁的队列)
2. 释放锁
3. 阻塞自身线程
------------被唤醒后执行-------------
4. 尝试去获取锁(执行到这里时线程已不在条件队列中,而是位于等待(锁的)队列中,参见signal方法)
4.1 成功,从await方法中返回,执行线程后面的代码
4.2 失败,阻塞自己(等待前一个节点释放锁时将它唤醒)
注意:await方法时自身线程调用的,线程在await方法中阻塞,并没有从await方法中返回,当唤醒后继续执行await方法中后面的代码(也就是获取锁的代码)。可以看出await方法释放了锁,又尝试获得锁。当获取锁不成功的时候当前线程仍然会阻塞到await方法中,等待前一个节点释放锁后再将其唤醒。
signal方法:
1. 将条件队列的队首节点取出,放入等待锁队列的队尾
2. 唤醒该节点对应的线程
注意:signal是由其它线程调用
Condition主要方法
// 让线程进入等通知待状态 void await() throws InterruptedException; void awaitUninterruptibly(); //让线程进入等待通知状态,超时结束等待状态,并抛出异常 long awaitNanos(long nanosTimeout) throws InterruptedException; boolean await(long time, TimeUnit unit) throws InterruptedException; boolean awaitUntil(Date deadline) throws InterruptedException; //将条件队列中的一个线程,从等待通知状态转换为等待锁状态 void signal(); //将条件队列中的所有线程,从等待通知阻塞状态转换为等待锁阻塞状态 void signalAll();
Lock和Condition一起使用
下面这个例子,就是利用lock和condition实现B线程先打印一句信息后,然后A线程打印两句信息(不能中断),交替十次后结束
public class ConditionDemo { volatile int key = 0; Lock l = new ReentrantLock(); Condition c = l.newCondition(); public static void main(String[] args){ ConditionDemo demo = new ConditionDemo(); new Thread(demo.new A()).start(); new Thread(demo.new B()).start(); } class A implements Runnable{ @Override public void run() { int i = 10; while(i > 0){ l.lock(); try{ if(key == 1){ System.out.println("A is Running"); System.out.println("A is Running"); i--; key = 0; c.signal(); }else{ c.awaitUninterruptibly(); } } finally{ l.unlock(); } } } } class B implements Runnable{ @Override public void run() { int i = 10; while(i > 0){ l.lock(); try{ if(key == 0){ System.out.println("B is Running"); i--; key = 1; c.signal(); }else{ c.awaitUninterruptibly(); } } finally{ l.unlock(); } } } } }