在java多线程编程中,很多时候我们会使用synchronized关键字来实现线程之间的同步执行,并通过wait/notify机制实现线程之间的通信。JDK1.5中增加了ReentrantLock类也可以实现synchronized关键字相同的效果,并且使用Condition类可以实现类似于wait/notify一样的线程之间的通信。
一、使用ReentrantLock实现同步效果
在使用ReentrantLock的时候,要先创建一个ReentrantLock的实例化对象lock,用来起到对象监视器的作用,类似synchronized,我们需要在需要同步执行的代码之前,调用lock.lock()方法获取锁。在执行完同步代码之后执行lock.unlock()释放锁。所有线程使用同一个lock对象,就可以实现线程之间的同步执行。
代码实现
//线程类,包含ReentrantLock锁 public class MyThread extends Thread{ private Lock lock = new ReentrantLock(); public MyThread(Lock lock) { super(); this.lock = lock; } @Override public void run() { try { lock.lock(); System.out.println("线程"+Thread.currentThread().getName()+"执行开始,时间" +System.currentTimeMillis()); Thread.sleep(2000); System.out.println("线程"+Thread.currentThread().getName()+"执行结束,时间" +System.currentTimeMillis()); }catch(InterruptedException e) { e.printStackTrace(); }finally { lock.unlock(); } } } //创建三个线程依次执行 public class Run { public static void main(String args[]) { Lock lock = new ReentrantLock(); MyThread[] mt = new MyThread[3]; for(int i=0;i<3;i++) { mt[i] = new MyThread(lock); mt[i].setName(""+(i+1)); mt[i].start(); } } }
/*
线程1执行开始,时间1526890879772
线程1执行结束,时间1526890881778
线程2执行开始,时间1526890881778
线程2执行结束,时间1526890883783
线程3执行开始,时间1526890883784
线程3执行结束,时间1526890885789
*/
可以看到三个线程同步运行
二、使用Condition实现等待/通知
关键字synchronized与wait和notify相结合可以实现等待/通知机制,用以实现线程之间的通信。同样的,类ReentrantLock也可以通过等待/通知机制实现线程之间的通信,通过Condition类以及await()和signalAll()方法可以轻松实现。Condition类是JDK5中出现的新技术,使用它可以实现多路通知功能,也就是说在一个Lock中可以创建多个Condition对象,然后将不同类的线程分别注册在相应的Condition上,这样我们就可以在程序中选择性的通知需要被唤醒的线程,而不需要每次都使用notifyAll()将所有线程一并唤醒了,这样也会使线程调度更加灵活。
在使用notifyAll/notify方法来实现等待通知时,JVM会随机选择被通知的线程,但是使用ReentrantLock结合Condition实现的等待/通知机制可以做到有选择性的通知线程,这在多线程编程中是非常重要的。使用synchronized时,我们相当于将所有的线程都注册在一个Condition上,一旦需要唤醒,要么随机唤醒一个线程来获取lock,要么全部唤醒,由所有的线程来竞争lock。这就导致,可能竞争到对象锁的线程还并不满足继续执行的条件,它会再一次notifyAll,并且释放对象锁,这就造成了很大的资源浪费。这样一来,“选择性通知”就显得十分重要了。
下面这个例子演示选择性通知
public class MyService { private Lock lock = new ReentrantLock(); private Condition conA = lock.newCondition(); private Condition conB = lock.newCondition(); //使所有的注册在conA上的线程wait public void awaitA() { try { lock.lock(); System.out.println("线程"+Thread.currentThread().getName()+"await开始时间"+System.currentTimeMillis()); conA.await(); System.out.println("线程"+Thread.currentThread().getName()+"await结束时间"+System.currentTimeMillis()); }catch(InterruptedException e) { e.printStackTrace(); }finally { lock.unlock(); } } public void awaitB() { try { lock.lock(); System.out.println("线程"+Thread.currentThread().getName()+"await开始时间"+System.currentTimeMillis()); conB.await(); System.out.println("线程"+Thread.currentThread().getName()+"await结束时间"+System.currentTimeMillis()); }catch(InterruptedException e) { e.printStackTrace(); }finally { lock.unlock(); } } //使所有注册在conB上的线程signal public void signalAll_A() { try { lock.lock(); conA.signalAll(); System.out.println("signal的时间"+System.currentTimeMillis()); }finally { lock.unlock(); } } public void signalAll_B() { try { lock.lock(); conB.signalAll(); System.out.println("signal的时间"+System.currentTimeMillis()); }finally { lock.unlock(); } } } public class ConThreadA extends Thread{ MyService ms = new MyService(); public ConThreadA(MyService ms) { super(); this.ms = ms; } @Override public void run() { ms.awaitA(); } } public class ConThreadB extends Thread{ MyService ms = new MyService(); public ConThreadB(MyService ms) { super(); this.ms = ms; } @Override public void run() { ms.awaitB(); } } public class RunCon { public static void main(String args[]) { try { MyService ms = new MyService(); ConThreadA[] ctA = new ConThreadA[3]; ConThreadB[] ctB = new ConThreadB[3]; for(int i=0;i<3;i++) { ctA[i] = new ConThreadA(ms); ctA[i].setName("线程A"+(i+1)); ctA[i].start(); ctB[i] = new ConThreadB(ms); ctB[i].setName("线程B"+(i+1)); ctB[i].start(); } Thread.sleep(2000); //唤醒注册在conA上的所有线程 ms.signalAll_A(); }catch(InterruptedException e) { e.printStackTrace(); } } }
运行结果如下:
/*
线程线程A1await开始时间1526970622920
线程线程A3await开始时间1526970622920
线程线程B3await开始时间1526970622920
线程线程B1await开始时间1526970622920
线程线程A2await开始时间1526970622921
线程线程B2await开始时间1526970622921
signal的时间1526970624921
线程线程A1await结束时间1526970624921
线程线程A3await结束时间1526970624921
线程线程A2await结束时间1526970624921
*/
可以看到,我们将所有绑定在conA上的线程唤醒了,它们依次执行完毕,而其他的绑定在conB上的线程依旧处于Waiting状态。
三、实现生产者/消费者模式
既然说ReentrantLock结合Condition可以代替synchronized,那么它一定也能实现生产者/消费者模式,接下来我们就试着实现一个多生产者——多消费者的例子。
同样是一个生产枪支的情景,三个兵工厂依次生产AK-47步枪,每次生产一支并放入仓库,仓库总容量为5;三个士兵依次将仓库的步枪送上前线,每次取走一支。代码如下:
//仓库大门,包含仓库的lock以及两种不同的Condition public class Gate { private ReentrantLock lock = new ReentrantLock(); //用key表示Condition对象 private Condition fkey = lock.newCondition(); private Condition skey = lock.newCondition(); public void product() { try { lock.lock(); if(Storehouse.sh.size()==5) { f_await(); } Storehouse.sh.add("AK-47"); System.out.println(Thread.currentThread().getName()+"生产了AK-47..."); System.out.println("仓库里现在有"+Storehouse.sh.size()+"把AK-47自动步枪"); Thread.sleep(1000); //生产完枪唤醒soldier线程来取枪 s_signalAll(); }catch(InterruptedException e) { e.printStackTrace(); }finally{ lock.unlock(); } } public void consume() { try { lock.lock(); if(Storehouse.sh.size()==0) { s_await(); } Storehouse.sh.remove(0); System.out.println(Thread.currentThread().getName()+"将AK-47送上前线..."); System.out.println("仓库里现在有"+Storehouse.sh.size()+"把AK-47自动步枪"); Thread.sleep(1000); //取走枪唤醒factory线程继续生产 f_signalAll(); }catch(InterruptedException e) { e.printStackTrace(); }finally { lock.unlock(); } } public void f_await() { try { lock.lock(); fkey.await(); }catch(InterruptedException e) { e.printStackTrace(); }finally { lock.unlock(); } } public void s_await() { try { lock.lock(); skey.await(); }catch(InterruptedException e) { e.printStackTrace(); }finally { lock.unlock(); } } public void f_signalAll() { try { lock.lock(); fkey.signalAll(); }finally { lock.unlock(); } } public void s_signalAll() { try { lock.lock(); skey.signalAll(); }finally { lock.unlock(); } } } //兵工厂 public class Factory extends Thread{ private Gate gate; public Factory(Gate gate) { super(); this.gate = gate; } @Override public void run() { while(true) gate.product(); } } //士兵 public class Soldier extends Thread{ private Gate gate; public Soldier(Gate gate) { super(); this.gate = gate; } @Override public void run() { while(true) gate.consume(); } } //仓库 public class Storehouse { public static List sh= new ArrayList(); } public class Run { public static void main(String args[]) { Gate gate = new Gate(); Factory[] f = new Factory[3]; Soldier[] s = new Soldier[3]; for(int i=0;i<3;i++) { f[i] = new Factory(gate); s[i] = new Soldier(gate); f[i].setName("兵工厂"+(i+1)); s[i].setName("士兵"+(i+1)); f[i].start(); s[i].start(); } } }
部分运行结果如下:
/*
士兵2将AK-47送上前线...
仓库里现在有2把AK-47自动步枪
士兵2将AK-47送上前线...
仓库里现在有1把AK-47自动步枪
士兵2将AK-47送上前线...
仓库里现在有0把AK-47自动步枪
兵工厂3生产了AK-47...
仓库里现在有1把AK-47自动步枪
兵工厂3生产了AK-47...
仓库里现在有2把AK-47自动步枪
兵工厂3生产了AK-47...
仓库里现在有3把AK-47自动步枪
*/
可以看到使用ReentrantLock结合Condition也可以实现多生产者/多消费者模式。并且我们可以将生产者和消费者注册在不同的Condition对象上,在程序中进行相应的唤醒,提高效率。
四、Lock知识补充
1、公平锁与非公平锁
Lock分为公平锁与非公平锁。公平锁顾名思义就是线程竞争锁的方式是公平的,即讲究“先来后到”,也就是FIFO先进先出,锁的分配是按照线程的先后顺序来确定的。而非公平锁提供的是一种可抢占模式的锁分配方法,先来的线程可能会被后来的线程先抢到锁从而继续等待锁的分配,这样就有可能造成有些线程很久都获取不到锁,所以是非公平的。
创建公平锁的方式是ReentrantLock lock_fair = new ReentrantLock(true);
相应的创建非公平锁ReentrantLock lock_unfair = new ReentrantLock(false);
2、ReentrantLock内的其他方法
int getHoldCount():查询当前线程保持此锁定的个数,也就是调用lock()方法的次数。例如两个方法都需要调用lock(),而方法1内部又调用了方法2,那么当方法2执行完lock()后,再执行getHoldCount()会返回2。
int getQueueLength():返回正在等待此lock的线程的估计数。
int getWaitQueueLength(Condition condition):返回等待与此锁定相关的给定条件Condition的线程估计数。比如有五个线程每个线程都执行了一个condition的await方法,则调用getWaitQueueLength(Condition condition)返回5。
boolean hasQueuedThread(Thread thread):查询指定的线程是否正在等待获取此锁定。注意,这里指的是已经处于Runnable状态的线程,如果是await的线程则会返回false。
boolean hasQueuedThreads():查询是否有线程正在等待锁。
boolean hasWaiters(Condition condition):查询是否有正在等待此Condition有关条件的线程。
boolean isFair():判断是否是公平锁。
boolean isHeldByCurrentThread():查询当前线程是否保持此锁定。
boolean isLocked():查询此锁定是否由任意线程保持。
void lockInterruptibly():如果当前线程未被中断则获取锁定,否则抛出异常。
boolean tryLock():调用时,如果锁未被另一个线程所获得,则获得此锁。
awaitUninterruptibly():使线程进入await状态,并且在await状态下如果执行interrupt()也不会抛出异常。
awaitUntil(Date date):使得线程在指定时间到达之前处于等待状态,并且在这期间可以被其他线程唤醒。
五、ReentrantReadWriteLock类
前面介绍了ReentrantLock的各种用法,我们知道在执行了lock()后,线程接下来的操作是可以做到完全互斥排他的,这在保证了线程安全的情况下却显得不是那么的高效。试想如果大量的线程对于一个变量只进行读操作而并不修改变量的值,完全互斥的操作就显得有些多余,但是仍会有一些线程要对变量进行修改,所以变量仍然需要加锁,该怎样才能提高效率呢?
ReentrantReadWriteLock类就提供了一种解决办法,读写锁表示有两个锁,即读锁和写锁。读锁是共享锁,写锁是排他锁。这也就意味着,读与读之间是共享操作可以异步进行,而一旦有写操作加入就变成了互斥操作,必须同步进行。当有多个线程要进行读操作时,每个线程都可以获得读锁;当有线程要进行写操作时必须等当前所有线程都释放锁它才能获得写锁,而必须等到此线程写操作完毕释放写锁时,其他线程才能开始竞争锁。
用法如下:
创建读写锁
ReentrantReadWriteLock lock = new ReentrantReadWriteLock;
在读任务中,获得/释放锁的方式
lock.readlock.lock(); lock.readlock.unlock();
在写任务中,获得/释放锁的方式
lock.writelock.lock(); lock.writelock.unlock();