多线程编程学习四(Lock 的使用)
一、前言
本文要介绍使用Java5中 Lock 对象,同样也能实现同步的效果,而且在使用上更加方便、灵活,主要包括 ReentrantLock 类的使用和ReentrantReadWriteLock 类的使用。
lock 与 synchronized 关键字的区别?
-
- synchronized 是java内置关键字;Lock是个java类。
- synchronized 会自动释放锁;Lock需在finally中手工释放锁(unlock()方法释放锁)
- synchronized 的锁可重入、不可中断等待、非公平;而Lock锁可重入、可中断等待(利用超时机制)、可公平。
- synchronized 的 wait() 和 notify() 或 notifyAll() 方法可以实现一个隐含的条件。而 Lock锁可以同时绑定多个 Condition 对象。
等待可中断是指当持有锁的线程长期不释放锁的时候,正在等待的线程可以选择放弃等待,改为处理其他事情,可中断特性对处理执行时间非常长的同步块很有帮助。
在Java 5之前,当一个线程获取不到锁而被阻塞在 synchronized 之外时,对该线程进行中断操作,此时该线程的中断标志位会被修改,但线程依旧会阻塞在synchronized上,等待着获取锁;在Java 5中,AQS 同步器提供了 acquireInterruptibly(int arg) 方法,这个方法在等待获取同步状态时,如果当前线程被中断,会立刻返回,并抛出 InterruptedException。
二、使用ReentrantLock 类
1、在java多线程中,可以使用 synchronized 关键字来实现线程之间同步互斥,但在JDK1.5中新增加的 ReentrantLock(重入锁) 也能达到同样的效果,并且在扩展功能上也更加强大,比如具有嗅探锁定、多路分支通知等功能,而且在使用上也比 synchronized 更加的灵活。
2、调用lock.lock()代码的线程就持有了“对象监视器”,即lock 持有的是对象锁,依赖于该类的实例存在。
public class MyService { private Lock lock=new ReentrantLock(); public void testMethod(){ lock.lock(); for(int i=0;i<5;i++){ System.out.println(Thread.currentThread().getName()+(i+1)); } lock.unlock(); } }
3、关键字synchronized 与wait() 和 notify()/notifyAll() 方法相结合可以实现等待/通知模式,类ReentrantLock 也可以实现同样的功能,但需要借助于Condition对象。
public class Myservice { private Lock lock=new ReentrantLock(); private Condition condition=lock.newCondition(); //等待 public void waitMethod(){ try { lock.lock(); System.out.println("A"); condition.await();//调用的Condition的await等待方法也需要在同步方法中,否则会报错 System.out.println("B"); } catch (InterruptedException e) { e.printStackTrace(); }finally { lock.unlock(); } } //唤醒 public void signal(){ try { lock.lock(); System.out.println("现在开始唤醒..."); condition.signal(); }finally { lock.unlock(); } } }
4、使用多个 Condition 对象 实现线程之间的选择性通知。
public class MyService { private Lock lock=new ReentrantLock(); //通过定义多个Condition实现选择性通知,可以唤醒指定种类的线程,这是 //控制部分线程行为的方便形式 private Condition conditionA=lock.newCondition(); private Condition conditionB=lock.newCondition(); public void awaitA(){ try { lock.lock(); System.out.println("awaitA begin"); conditionA.await(); System.out.println("awaitA end"); } catch (InterruptedException e) { e.printStackTrace(); }finally { lock.unlock(); } } public void awaitB(){ try { lock.lock(); System.out.println("awaitB begin"); conditionB.await(); System.out.println("awaitB end"); } catch (InterruptedException e) { e.printStackTrace(); }finally { lock.unlock(); } } public void signalA(){ try { lock.lock(); System.out.println("现在开始唤醒awaitA"); conditionA.signalAll(); }finally { lock.unlock(); } } public void signalB(){ try { lock.lock(); System.out.println("现在开始唤醒awaitB"); conditionB.signalAll(); }finally { lock.unlock(); } } }
public class Run { public static void main(String[] args) throws InterruptedException { MyService myService=new MyService(); Thread threadA=new Thread(){ @Override public void run() { super.run(); myService.awaitA(); } }; Thread threadB=new Thread(){ @Override public void run() { super.run(); myService.awaitB(); } }; threadA.start(); threadB.start(); Thread.sleep(1000); myService.signalA(); Thread.sleep(1000); myService.signalB(); } }
5、公平锁和非公平锁
public class Service { private Lock lock; public Service(boolean isFair) { //通过这种方式创建公平锁(true)和非公平锁(false) lock=new ReentrantLock(isFair); } public void methodA(){ try { lock.lock(); System.out.println(Thread.currentThread().getName()+"正在运行"); }finally { lock.unlock(); } } }
public class Run { public static void main(String[] args) { final Service service=new Service(true); Runnable runnable=new Runnable() { @Override public void run() { service.methodA(); } }; Thread[] threads=new Thread[10]; for (int i=0;i<10;i++){ threads[i]=new Thread(runnable); threads[i].setName("线程"+(i+1)); threads[i].start(); } } }
公平锁保证了锁的获取按照FIFO原则,而代价是进行大量的线程切换。非公平性锁虽然可能造成线程“饥饿”,但极少的线程切换,保证了其更大的吞吐量。
6、ReentrantLock 常用方法介绍
(1) int getHoldCount() 查询当前线程保持此锁定的个数,也就是线程中调用lock方法的次数。
(2) int getQueueLength() 返回正等待此锁定的线程估计数,比如有5个线程,1个线程正占用了这个Lock锁在执行,则调用此方法返回的就是4。该值仅是估计的数字,因为在此方法遍历内部数据结构的同时,线程的数目可能动态地变化。此方法用于监视系统状态,不用于同步控制。
(3) int getWaitQueueLength(Condition condition) 返回等待与此锁定相关的给定条件Condition的线程估计数,比如有五个线程,每个线程都执行了同一个condition对象的await()方法,则调用此方法返回的值就是5。
public class Service { private ReentrantLock lock=new ReentrantLock(); private Condition condition=lock.newCondition(); public void methodA(){ try { lock.lock(); System.out.println("A getHoldCount 调用lock的次数=>"+lock.getHoldCount()); Thread.sleep(2000); System.out.println("A getQueueLength 正在等待的线程数=>"+lock.getQueueLength()); condition.await(); } catch (InterruptedException e) { e.printStackTrace(); } finally { lock.unlock(); } } //测试getWaitQueueLength方法 public Integer methodC(){ try { lock.lock(); return lock.getWaitQueueLength(condition); }finally { lock.unlock(); } } }
public class Run{ public static void main(String[] args) throws InterruptedException { Service service=new Service(); Runnable runnable=new Runnable() { @Override public void run() { service.methodA(); } }; Thread[] threads=new Thread[5]; for (int i=0;i<5;i++){ threads[i]=new Thread(runnable); threads[i].start(); } Thread.sleep(1000); System.out.println("执行了同一个Condition对象的的await()的线程有:"+service.methodC()); } }
(4) boolean hasQueuedThread(Thread thread) 查询指定的线程是否正在等待获取此锁定。
(5) boolean hasQueuedThreads() 查询是否有线程正在等待获取此锁定。
(6) boolean hasWaiters(Condition condition) 查询是否有线程正在等待与此锁定有关的condition条件
(7) boolean isFair() 判断是不是公平锁。
(8) boolean isHeldByCurrentThread() 查询当前线程是否保持此锁定。
(9) boolean isLocked() 查询此锁定是否由任意线程保持。
(10) void lockInterruptibly() 如果当前线程未被中断,则获取锁定,如果已经被中断,则出现异常。和 lock() 方法的区别在于该方法会响应中断,即在锁的获取中可以中断当前线程。
(11) boolean tryLock() 仅在调用时锁定未被另一个线程锁定的情况下,才获得此锁定。
(12) boolean tryLock(long timeout,TimeUnit unit) 如果锁定在给定等待时间内没有被另一个线程保持,且当前线程未被中断,则获取该锁定。
public class Service { private ReentrantLock lock=new ReentrantLock(); private Condition condition=lock.newCondition(); //测试lockInterruptibly public void methodA(){ try { lock.lockInterruptibly(); System.out.println("methodA=》"+Thread.currentThread().getName()); } catch (InterruptedException e) { e.printStackTrace(); }finally { if (lock.isHeldByCurrentThread()){//如果当前线程依旧保持对此锁的锁定,则释放 lock.unlock(); } } } //测试tryLock public void methodB(){ if (lock.tryLock()){ System.out.println(Thread.currentThread().getName()+"获得锁"); }else{ System.out.println(Thread.currentThread().getName()+"未获得锁"); } } }
public class Run { public static void main(String[] args) throws InterruptedException { Service service=new Service(); Runnable runnable=new Runnable() { @Override public void run() { service.methodA(); service.methodB(); } }; Thread threadA=new Thread(runnable); threadA.setName("A"); threadA.start(); Thread.sleep(1000); Thread threadB=new Thread(runnable); threadB.setName("B"); threadB.start(); threadB.interrupt(); } }
(13) lock.awaitUninterruptibly():这个线程将不会被中断,一直睡眠直到其他线程调用signal()或signalAll()方法。
(14) lock.awaitUntil(Date date):这个线程将会一直睡眠直到:
-
- 它被中断
- 其他线程在这个condition上调用singal()或signalAll()方法
- 指定的日期已经到了
在获取同步状态时,同步器维护一个同步队列,获取状态失败的线程都会被加入到队列中并在队列中进行自旋;移出队列(或停止自旋)的条件是前驱节点为头节点且成功获取了同步状态。在释放同步状态时,同步器调用 tryRelease(int arg) 方法释放同步状态,然后唤醒头节点的后继节点。
三、使用ReentrantReadWriteLock 类
public class Read { private ReentrantReadWriteLock lock=new ReentrantReadWriteLock(); public void read(){ try { lock.readLock().lock(); System.out.println(Thread.currentThread().getName()+"正在读"+System.currentTimeMillis()); Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } finally { lock.readLock().unlock(); } } }
public class Run { public static void main(String[] args) { Read read=new Read(); Runnable runnable=new Runnable() { @Override public void run() { read.read(); } }; Thread[] threads=new Thread[10]; for (int i=0;i<10;i++){ threads[i]=new Thread(runnable); threads[i].start(); //通过结果可以看到所有线程几乎同时进入lock()方法 //后面的代码,读读不互斥,可以提高程序运行效率,允许 //多个线程同时执行lock()方法后面的代码 } } }
public class Write { private ReentrantReadWriteLock lock=new ReentrantReadWriteLock(); public void write(){ try { lock.writeLock().lock(); System.out.println(Thread.currentThread().getName()+"正在写"+System.currentTimeMillis()); Thread.sleep(2000); } catch (InterruptedException e) { e.printStackTrace(); } finally { lock.writeLock().unlock(); } } }
public class Run { public static void main(String[] args) { Write write=new Write(); Runnable runnable=new Runnable() { @Override public void run() { write.write(); } }; Thread[] threads=new Thread[10]; for (int i=0;i<10;i++){ threads[i]=new Thread(runnable); threads[i].start(); //通过结果可以看到所有线程每隔两秒运行一次,写写互斥,线程之间是同步运行的 } } }
另外,写读、读写都是互斥的,就不举例了。总之,只要出现"写"操作,就是互斥的!原因在于:读写锁要确保写锁的操作对读锁可见,如果允许读锁在已被获取的情况下对写锁的获取,那么正在运行的其他读线程就无法感知到当前写线程的操作。
锁降级:锁降级是指把持住(当前拥有的)写锁,再获取到读锁,随后释放(先前拥有的)写锁的过程。简单来说就是同一个线程中,写锁处理数据的同时,对读锁获取(防止其他写线程争抢到锁,从而改变数据)
public class LockDowngrade { private ReentrantReadWriteLock lock = new ReentrantReadWriteLock(); private ReentrantReadWriteLock.ReadLock readLock = lock.readLock(); private ReentrantReadWriteLock.WriteLock writeLock = lock.writeLock(); private volatile Boolean update = false; public void processData() { readLock.lock(); if (!update) { // 必须先释放读锁 readLock.unlock(); // 锁降级从写锁获取到开始 writeLock.lock(); try { if (!update) { // 准备数据的流程(略) update = true; } readLock.lock(); } finally { writeLock.unlock(); } //锁降级完成,写锁降级为读锁 } try { // 使用数据的流程(略) } finally { readLock.unlock(); } } }
四、Lock 锁的实现原理
一些同步组件(ReentrantLock、ReentrantReadWriteLock 和 CountDownLatch 等)的实现基本都是通过聚合了一个同步器(AbstractQueuedSynchronizer)的子类来完成线程访问控制的。可以这样理解二者之间的关系:锁或其它同步组件是面向使用者的,它定义了使用者与锁交互的接口,隐藏了实现细节;同步器面向的是锁的实现者,它简化了锁的实现方式,屏蔽了同步状态管理、线程的排队、等待与唤醒等底层操作。锁和同步器很好地隔离了使用者和实现者所需关注的领域。
队列同步器 AbstractQueuedSynchronizer(基于模板方式模式),是用来构建锁或者其他同步组件的基础框架,它使用了一个 int成员变量(state)表示同步状态,通过内置的 FIFO 队列来完成资源获取线程的排队工作。
同步器的主要使用方式是继承,子类通过继承同步器并实现它的抽象方法来管理同步状态,在抽象方法的实现过程中免不了要对同步状态进行更改,这时就需要使用同步器提供的 3 个方法(getState()、setState(int newState)和compareAndSetState(int expect,int update))来进行操作,因为它们能够保证状态的改变是安全的。子类推荐被定义为自定义同步组件的静态内部类,同步器自身没有实现任何同步接口。它仅仅是定义了若干同步状态获取和释放的方法来供自定义同步组件使用,同步器既可以支持独占式地获取同步状态,也可以支持共享式地获取同步状态。
addWaiter() 的作用是把当前线程加入到AQS内部同步队列的尾部;
acquireQueued() 的作用是判断自己是不是同步队列中的第一个排队的节点,则尝试进行加锁,如果成功,则把自己变成head node,如果失败就阻塞当前线程。
独占锁的获取机制:
在 Object 的监视器模型上,一个对象拥有一个同步队列和等待队列,而并发包中的 Lock(更确切地说是同步器)拥有一个同步队列和多个等待队列。
当调用,ConditionObject.await() 时,相当于同步队列的首节点(获取了锁的节点)移动到Condition的等待队列(看起来是移动,实际上是构建了一个新的 Node)中,然后释放同步状态,唤醒同步队列中的后继节点,然后当前线程会进入等待状态 — LockSupport.park()。如果等待期间被中断,会抛出 InterruptedException。
当调用,ConditionObject.signal() 时,将会唤醒在等待队列中等待时间最长的节点(首节点),在唤醒节点之前,会将节点移到同步队列中(获取锁的过程),并使用 LockSupport.unpark 唤醒节点中的线程。
当调用,ConditionObject.signalAll() 时,相当于对等待队列中的每个节点均执行一次 signal()方法,效果就是将等待队列中所有节点全部移动到同步队列中,并唤醒每个节点的线程。