1 前言
在Java多线程中独占锁的实现可以使用synchronized关键字来实现线程之间的同步互斥,但在JDK1.5中新增加ReentranLock(可重入锁)类也可以达到同样的效果,并且在扩展功能上更加强大,比如具有嗅探锁定、多路分支通知等,而且在使用上也比synchronized更加灵活,也更适合复杂的并发场景。
2 正文
ReentrantLock是唯一实现了Lock接口的类,并且ReentrantLock提供了更多的方法。
ReentranLock的基本语法如下:
private Lock lock = new ReentrantLock(); lock.lock();//用来获取锁。 lock.unlock();//用来释放锁 复制代码
比如:
public class Demo12 { public static void main(String[] args) { Lock lock = new ReentrantLock(); Thread t1 = new Demo01Thread(lock); Thread t2 = new Demo01Thread(lock); Thread t3 = new Demo01Thread(lock); t1.start(); t2.start(); t3.start(); } } class Demo01Thread extends Thread{ private Lock lock; public Demo01Thread(Lock lock){ this.lock = lock; } @Override public void run() { // 加上同步锁 lock.lock(); for (int i=0; i< 5; i++){ System.out.println(Thread.currentThread().getName() + ", " + (i + 1)); } // 解开同步锁 lock.unlock(); } } 复制代码
结果如下:
从上面的运行结果来看,当前线程打印完毕之后将锁进行释放,其它线程才可以继续 打印。线程打印的数组是分级打印,因为当前线程已经持有锁,但线程之间打印的顺序是随机的。
调用lock方法的线程就会持有对象锁,其它的线程只能等待锁被释放(调用unlock方法)才可以再次争抢锁。效果和使用synchronized关键字一样,线程之间还是按照顺序执行。
Lock分为公平锁与非公平锁两种。公平锁表示线程获取锁的顺序是按照加锁的顺序来分配的,也就是先来先得。而非公平锁就是一种获取锁的抢占机制 ,是随机获得锁,与公平锁不一样的是先来的不一定先得到锁,这种方式可能会造成某些线程一直都拿不到锁,结果也就是不公平的。
进入ReentranLock的源码:
通过ReentranLock的源码可以发现无参的ReentrantLock是非公平锁,而通过boolean参数可以控制锁的类型,如果是true会使用公平锁,否则就是非公平锁。
package com.jiangxia.chap3; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; /** * 公平锁和非公平锁 */ public class Demo13 { public static void main(String[] args) { //公平锁 Demo13Service demo13Service = new Demo13Service(true); //非公平锁 //Demo13Service demo13Service = new Demo13Service(false); Thread[] threads = new Thread[10]; for (int i = 0; i < threads.length; i++) { threads[i] = new Demo13Thread(demo13Service); } for (int i = 0; i <threads.length ; i++) { threads[i].start(); } } } class Demo13Service{ private Lock lock; /** * 通过isFair参数控制锁的类型,true就是公平锁,false为非公平锁 * @param isFair */ public Demo13Service(boolean isFair) { this.lock = new ReentrantLock(isFair); } public void hello(){ lock.lock(); System.out.println(Thread.currentThread().getName()+"获得锁。。。"); lock.unlock(); } } class Demo13Thread extends Thread{ private Demo13Service demo13Service; public Demo13Thread(Demo13Service demo13Service) { this.demo13Service = demo13Service; } @Override public void run() { demo13Service.hello(); } } 复制代码
公平锁输出结果如下:
非公平锁输出结果如下:
在java 1.5中才出现的Condition,它可以用来替代传统的Object的wait()、notify()实现线程间的协作,相比使用Object的wait()、notify(),使用Condition的await()、signal()这种方式实现线程间协作更加安全和高效。
Condition接口也提供了类似Object的监视器方法,与Lock配合可以实现等待/通知模式,也就是在一个Lock对象里面可以创建多个Condition实例,线程对象可以注册在指定的Condition中,从而可以有选择性地进行线程通知,在调试线程上更加灵活。
在使用notify/notifyAll方法进行通知时,被通知线程是由JVM随机选择的,但使用ReentrantLock结合Condition类可以实现选择性通知,这个功能是非常重要的,而且在Condition在中是默认提供的。
而synchronized就相当于整个Lock对象中只有一个单一Condition对象,所有的线程都是注册在它一个对象的身上。线程开始notifyAll时需要通知所有正在等待的线程,没有选择权,会出现相当大效率问题。
condition可以通俗的理解为条件队列。当一个线程在调用了await方法以后,直到线程等待的某个条件为真的时候才会被唤醒。这种方式为线程提供了更加简单的等待/通知模式。Condition必须要配合锁一起使用,因为对共享状态变量的访问发生在多线程环境下。一个Condition的实例必须与一个Lock绑定,因此Condition一般都是作为Lock的内部实现。
Condition接口常用方法
1、await() :造成当前线程在接到信号或被中断之前一直处于等待状态。
2、await(long time, TimeUnit unit) :造成当前线程在接到信号、被中断或到达指定等待时间之前一直处于等待状态。
3、awaitNanos(long nanosTimeout) :造成当前线程在接到信号、被中断或到达指定等待时间之前一直处于等待状态。返回值表示剩余时间,如果在nanosTimesout之前唤醒,那么返回值 = nanosTimeout - 消耗时间,如果返回值 <= 0 ,则可以认定它已经超时了。
4、awaitUninterruptibly() :造成当前线程在接到信号之前一直处于等待状态。【注意:该方法对中断不敏感】。
5、awaitUntil(Date deadline) :造成当前线程在接到信号、被中断或到达指定最后期限之前一直处于等待状态。如果没有到指定时间就被通知,则返回true,否则表示到了指定时间,返回返回false。
6、signal() :唤醒一个等待线程。该线程从等待方法返回前必须获得与Condition相关的锁。
7、signal()All :唤醒所有等待线程。能够从等待方法返回的线程必须获得与Condition相关的锁。
ReentrantLock使用Condition:
import java.util.concurrent.locks.Condition; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; /** * condition的使用 */ public class Demo14 { private Lock lock = new ReentrantLock(); private Condition condition = lock.newCondition(); public static void main(String[] args) throws InterruptedException { Mehtod14 mehtod14 = new Mehtod14(); Thread t = new Demo14_Thread(mehtod14); t.start(); Thread.sleep(2000); mehtod14.conditionsignal(); } } class Mehtod14{ private Lock lock = new ReentrantLock(); //condition对象是依赖于lock对象的,condition对象需要通过lock对象进行创建出来(调用Lock对象的newCondition()方法) private Condition condition = lock.newCondition(); public void conditionaWait(){ //加锁 lock.lock(); System.out.println("conditionaWait方法开始:"+System.currentTimeMillis()); System.out.println(Thread.currentThread().getName() + "拿到锁了"); System.out.println(Thread.currentThread().getName() + "等待信号"); try { //这里需要抛出异常 condition.await(); System.out.println("conditionaWait方法结束:"+System.currentTimeMillis()); System.out.println(Thread.currentThread().getName() + "拿到信号"); //解锁 lock.unlock(); } catch (InterruptedException e) { e.printStackTrace(); } } public void conditionsignal(){ try { lock.lock(); System.out.println("conditionsignal方法开始:" + System.currentTimeMillis()); Thread.sleep(5000); System.out.println(Thread.currentThread().getName() + "拿到锁了"); condition.signal(); System.out.println(Thread.currentThread().getName() + "发出信号"); lock.unlock(); } catch (InterruptedException e) { e.printStackTrace(); } } } class Demo14_Thread extends Thread{ private Mehtod14 mehtod14; public Demo14_Thread(Mehtod14 mehtod14){ this.mehtod14 = mehtod14; } @Override public void run() { mehtod14.conditionaWait(); } } 复制代码
运行结果如下:
一般都会将Condition对象作为成员变量。当调用await()方法后,当前线程会释放锁并在此等待,而其他线程调用Condition对象的signal()方法,通知当前线程后,当前线程才从await()方法返回,并且在返回前已经获取了锁。
使用Condition唤醒不同的线程:
import java.util.concurrent.locks.Condition; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; //condition唤醒不同线程 public class Demo15 { public static void main(String[] args) throws InterruptedException { Method15 method15 = new Method15(); Thread t1 = new Demo15Thread_A(method15); t1.setName("A"); t1.start(); Thread t2 = new Demo15Thread_B(method15); t2.setName("B"); t2.start(); Thread.sleep(2000); method15.conditionSignalAll_A(); } } class Method15{ private Lock lock = new ReentrantLock(); private Condition conditionA = lock.newCondition(); private Condition conditionB = lock.newCondition(); public void conditionAwiat_A(){ try { lock.lock(); System.out.println(Thread.currentThread().getName()+"开始执行conditionAwiat_A"); conditionA.await(); System.out.println(Thread.currentThread().getName()+"结束执行conditionAwiat_A"); } catch (InterruptedException e) { e.printStackTrace(); }finally { lock.unlock(); } } public void conditionAwiat_B(){ try { lock.lock(); System.out.println(Thread.currentThread().getName()+"开始执行conditionAwiat_B"); conditionA.await(); System.out.println(Thread.currentThread().getName()+"结束执行conditionAwiat_B"); } catch (InterruptedException e) { e.printStackTrace(); }finally { lock.unlock(); } } public void conditionSignalAll_A(){ try{ lock.lock(); System.out.println(Thread.currentThread().getName() + "唤醒所有的线程在"); //唤醒所有的线程 conditionA.signalAll(); }finally { lock.unlock(); } } public void conditionSignalAll_B(){ try{ lock.lock(); System.out.println(Thread.currentThread().getName() + "唤醒所有的线程在"); conditionB.signalAll(); }finally { lock.unlock(); } } } class Demo15Thread_A extends Thread{ private Method15 method15; public Demo15Thread_A(Method15 method15){ this.method15 = method15; } @Override public void run() { method15.conditionAwiat_A(); } } class Demo15Thread_B extends Thread{ private Method15 method15; public Demo15Thread_B(Method15 method15){ this.method15 = method15; } @Override public void run() { method15.conditionAwiat_B(); } } 复制代码
结果如下:
分别唤醒不同的线程,就需要使用多个Condition对象,也就是Condition对象可以唤醒部分指定的线程,有助于提升程序的运行效率。
通过上面源码分析可以看出Condition是AQS的内部类。每个Condition对象都包含一个等待队列,在队列中的每个节点都包含了一个线程引用,该线程就是在Condition对象上等待的线程,如果一个线程调用了Condition.await()方法,那么该线程将会释放锁、构造成节点加入等待队列并进入等待状态。
等待分为首节点和尾节点。当一个线程调用Condition.await()方法,将会以当前线程构造节点,并将节点从尾部加入等待队列。新增节点就是将尾部节点指向新增的节点。节点引用更新本来就是在获取锁以后的操作,所以不需要CAS保证。同时也是线程安全的操作。
当线程调用了await方法以后。线程就作为队列中的一个节点被加入到等待队列中去了。同时会释放锁的拥有。当从await方法返回的时候。一定会获取condition相关联的锁。当等待队列中的节点被唤醒的时候,则唤醒节点的线程开始尝试获取同步状态。如果不是通过 其他线程调用Condition.signal()方法唤醒,而是对等待线程进行中断,则会抛出InterruptedException异常信息。
而调用Condition的signal()方法,将会唤醒在等待队列中等待最长时间的节点(条件队列里的首节点),在唤醒节点前,会将节点移到同步队列中。在调用signal()方法之前必须先判断是否获取到了锁。接着获取等待队列的首节点,将其移动到同步队列并且利用LockSupport唤醒节点中的线程。被唤醒的线程将从await方法中的while循环中退出。随后加入到同步状态的竞争当中去。成功获取到竞争的线程则会返回到await方法之前的状态。
使用Lock的Condition实现生产者与消费者模型:
import java.util.concurrent.locks.Condition; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; public class Demo16 { public static void main(String[] args) { Demo16Service service = new Demo16Service(); // 一生产一消费 // Thread producer = new Demo05ProducerThread(service); // producer.start(); // // Thread consumer = new Demo05ConsumerThread(service); // consumer.start(); // 多生产多消费 int size = 2; Thread[] producers = new Thread[size]; Thread[] consumers = new Thread[size]; for (int i = 0; i < size; i++) { char c = (char)('A' + i); producers[i] = new Demo16ProducerThread(service); producers[i].setName("生产者" + c); producers[i].start(); consumers[i] = new Demo16ConsumerThread(service); consumers[i].setName("消费者" + c); consumers[i].start(); } } } class Demo16Service{ private Lock lock = new ReentrantLock(); private Condition condition = lock.newCondition(); private String val = ""; public void set(){ try{ lock.lock(); while(!"".equals(val)){ System.out.println(Thread.currentThread().getName() + "开始等待"); condition.await(); } val = System.currentTimeMillis() + "-" + System.nanoTime(); System.out.println(Thread.currentThread().getName() + "生产值:" + val); //condition.signal(); condition.signalAll(); }catch (InterruptedException e){ e.printStackTrace(); } finally { lock.unlock(); } } public void get(){ try{ lock.lock(); while("".equals(val)){ System.out.println(Thread.currentThread().getName() + "开始等待"); condition.await(); } System.out.println(Thread.currentThread().getName() + "消费值:" + val); val = ""; // condition.signal(); condition.signalAll(); }catch (InterruptedException e){ e.printStackTrace(); } finally { lock.unlock(); } } } class Demo16ProducerThread extends Thread{ private Demo16Service service; public Demo16ProducerThread(Demo16Service service){ this.service = service; } @Override public void run() { while(true){ service.set(); } } } class Demo16ConsumerThread extends Thread{ private Demo16Service service; public Demo16ConsumerThread(Demo16Service service){ this.service = service; } @Override public void run() { while(true){ service.get(); } } } 复制代码
部分结果如下:
类ReentrantLock具有完全互斥排他锁,也就是同一时间内只有一个线程可以在执行Reentrant.lock方法后面的任务。这样虽然可以保证实例变量的线程安全性,但是效率却是非常的低下。所以在JDK中提供了一种读写锁ReentrantReadWriteLock类,使用它可以加快运行的效率,在某些不需要操作实例变量的方法中,完全可以使用读写锁来提升方法的代码运行效率。
读写锁表示有两个锁,一个是读操作也称作共享锁,另一个是写操作也叫排他锁。也就是多个读锁之间不互斥,读锁与写锁是互斥,写锁与写锁之间也是互斥的。在没有线程进入定入操作时,进行读取操作的多个线程都可以获取 读取锁,而进行写入操作时只有在获取写锁后才能进行写入操作。即可以有多个线程同时进行读取操作,但同一时内只能有一个线程进行写操作。
ReentrantReadWriteLock源码:
package com.jiangxia.chap3; import java.util.concurrent.locks.ReentrantReadWriteLock; /** * ReentrantReadWriteLock */ public class Demo17 { public static void main(String[] args) { Demo17Service service = new Demo17Service(); Thread t1 = new Demo17ThreadA(service); t1.setName("读线程A"); t1.start(); Thread t2 = new Demo17ThreadA(service); t2.setName("读线程B"); t2.start(); Thread t3 = new Demo17ThreadB(service); t3.setName("写线程A"); t3.start(); Thread t4 = new Demo17ThreadB(service); t4.setName("写线程B"); t4.start(); } } class Demo17Service{ private ReentrantReadWriteLock lock = new ReentrantReadWriteLock(); public void read(){ try{ lock.readLock().lock(); System.out.println(Thread.currentThread().getName() + "获得读锁于" + System.currentTimeMillis()); Thread.sleep(1000); System.out.println(Thread.currentThread().getName() + "解除读锁于" + System.currentTimeMillis()); }catch (InterruptedException e){ e.printStackTrace(); }finally { lock.readLock().unlock(); } } public void write(){ try{ lock.writeLock().lock(); System.out.println(Thread.currentThread().getName() + "获得写锁于" + System.currentTimeMillis()); Thread.sleep(1000); System.out.println(Thread.currentThread().getName() + "解除写锁于" + System.currentTimeMillis()); }catch (InterruptedException e){ e.printStackTrace(); }finally { lock.writeLock().unlock(); } } } class Demo17ThreadA extends Thread{ private Demo17Service service; public Demo17ThreadA(Demo17Service service){ this.service = service; } @Override public void run() { service.read(); } } class Demo17ThreadB extends Thread{ private Demo17Service service; public Demo17ThreadB(Demo17Service service){ this.service = service; } @Override public void run() { service.write(); } } 复制代码
结果如下:
通过上述的结果可以看出读写或写读操作都是互斥的,只要出现 写操作的过程,就是互斥的。读读是共享的。
3 锁的相关概念介绍
1、可重入锁
如果锁具备可重入性,则称作为可重入锁 。像 synchronized和ReentrantLock都是可重入锁,可重入性在我看来实际上表明了锁的分配机制:基于线程的分配,而不是基于方法调用的分配。比如下面的例子,当一个线程执行到某个synchronized方法时,比如说method1,而在method1中会调用另外一个synchronized方法method2,此时线程不必重新去申请锁,而是可以直接执行方法method2。
class MyClass { public synchronized void method1() { method2(); } public synchronized void method2() { } } 复制代码
上述代码中的两个方法method1和method2都用synchronized修饰了。假如某一时刻,线程A执行到了method1,此时线程A获取了这个对象的锁,而由于method2也是synchronized方法,假如synchronized不具备可重入性,此时线程A需要重新申请锁。但是,这就会造成死锁,因为线程A已经持有了该对象的锁,而又在申请获取该对象的锁,这样就会线程A一直等待永远不会获取到的锁。而由于synchronized和Lock都具备可重入性,所以不会发生上述现象。
2、可中断锁
可中断锁就是可以响应中断的锁。在Java中,synchronized就不是可中断锁,而Lock是可中断锁。
如果某一线程A正在执行锁中的代码,另一线程B正在等待获取该锁,可能由于等待时间过长,线程B不想等待了,想先处理其他事情,可以让它中断自己或者在别的线程中中断它,这种就是可中断锁。
3、公平锁/非公平锁
公平锁即尽量以请求锁的顺序来获取锁。比如,同是有多个线程在等待一个锁,当这个锁被释放时,等待时间最久的线程(最先请求的线程)会获得该所,这种就是公平锁。而非公平锁是指多个线程获取锁的顺序并不是按照申请锁的顺序,有可能后申请的线程比先申请的线程优先获取锁。有可能,会造成优先级反转或者饥饿现象。
在Java中,synchronized就是非公平锁,它无法保证等待的线程获取锁的顺序。而对于ReentrantLock 和 ReentrantReadWriteLock,它默认情况下是非公平锁,但是可以设置为公平锁。
4、独享锁/共享锁
独享锁是指该锁一次只能被一个线程所持有。共享锁是指该锁可被多个线程所持有。
Java中的ReentrantLock是独享锁。但是对于Lock的另一个实现类ReadWriteLock,其读锁是共享锁,其写锁是独享锁。读锁的共享锁可保证并发读是非常高效的,读写,写读 ,写写的过程是互斥的。独享锁与共享锁也是通过AQS来实现的,通过实现不同的方法,来实现独享或者共享。而Synchronized是独享锁。
5、互斥锁/读写锁
前面的独享锁/共享锁就是一种广义的说法,而互斥锁/读写锁就是具体的实现。
互斥锁在Java中的具体实现就是ReentrantLock。
读写锁在Java中的具体实现就是ReadWriteLock。
6、乐观锁/悲观锁
乐观锁与悲观锁不是指具体的什么类型的锁,而是指看待并发同步的角度。
悲观锁会认为对于同一个数据的并发操作,一定是会发生修改的,哪怕没有修改,也会认为修改。悲观的认为,不加锁的并发操作一定会出问题。因此对于同一个数据的并发操作,悲观锁采取加锁的形式。
而乐观锁则认为对于同一个数据的并发操作,是不会发生修改的。在更新数据的时候,会采用尝试更新,不断重新的方式更新数据。乐观的认为,不加锁的并发操作是没有事情的。
悲观锁适合写操作非常多的场景,乐观锁适合读操作非常多的场景,不加锁会带来大量的性能提升。
悲观锁在Java中的使用,就是利用各种锁。
乐观锁在Java中的使用,是无锁编程,常常采用的是CAS算法,典型的例子就是原子类,通过CAS自旋实现原子操作的更新。
7、偏向锁/轻量级锁/重量级锁
这三种锁是指锁的状态,并且是针对Synchronized。在Jdk1.5通过引入锁升级的机制来实现高效Synchronized。这三种锁的状态是通过对象监视器在对象头中的字段来表明的。
偏向锁是指一段同步代码一直被一个线程所访问,那么该线程会自动获取锁。降低获取锁的代价。
轻量级锁是指当锁是偏向锁的时候,被另一个线程所访问,偏向锁就会升级为轻量级锁,其他线程会通过自旋的形式尝试获取锁,不会阻塞,提高性能。
重量级锁是指当锁为轻量级锁的时候,另一个线程虽然是自旋,但自旋不会一直持续下去,当自旋一定次数的时候,还没有获取到锁,就会进入阻塞,该锁膨胀为重量级锁。重量级锁会让其他申请的线程进入阻塞,性能降低。
8、分段锁
分段锁是一种锁的设计,并不是具体的一种锁,最常见的ConcurrentHashMap的并发的实现就是通过分段锁的形式来实现高效的并发操作。
ConcurrentHashMap中的分段锁称为Segment,它即类似于HashMap(JDK1.7与JDK1.8中HashMap的实现)的结构,即内部拥有一个Entry数组,数组中的每个元素又是一个链表;同时又是一个ReentrantLock(Segment继承了ReentrantLock)。当需要put元素的时候,并不是对整个hashmap进行加锁,而是先通过hashcode来知道他要放在那一个分段中,然后对这个分段进行加锁,所以当多线程put的时候,只要不是放在一个分段中,就实现了真正的并行的插入。但是,在统计size的时候,可就是获取hashmap全局信息的时候,就需要获取所有的分段锁才能统计。
分段锁的设计目的是细化锁的粒度,当操作不需要更新整个数组的时候,就仅仅针对数组中的一项进行加锁操作。
9、自旋锁
自旋锁是指当一个线程在获取锁的时候,如果锁已经被其它线程获取,那么该线程将循环等待,然后不断的判断锁是否能够被成功获取,直到获取到锁才会退出循环。
获取锁的线程一直处于活跃状态,但是并没有执行任何有效的任务,使用这种锁会造成busy-waiting。
所以这样的好处是减少线程上下文切换的消耗,缺点是循环会消耗CPU。
4 总结
本篇文章主要介绍了java多线程编程中lock的相关概念以及其实现类ReentrantLock的使用,以及如何使用condition进行线程之间的通信还有读写锁ReentrantReadWriteLock类的简单使用。在多线程同步中可以使用Lock和synchronized,但是Lock和synchronized有一点非常大的不同,就是采用synchronized不需要用户去手动释放锁,当synchronized方法或者synchronized代码块执行完之后,系统会自动让线程释放对锁的占用;而Lock则必须要用户去手动释放锁,如果没有主动释放锁,就有可能导致出现死锁现象。