并发常见的编程场景,一句话概括就是,需要协调多个线程之间的协作,已保证程序按照自己原本的意愿执行。那么究竟应该如何协调多个线程?
这个问题比较宽泛,一般情况下,我们按照方式的纬度去简单区分,有以下两种方式:
1,第一种是利用JVM的内部机制。
2,第二种是利用JVM外部的机制,比如JDK或者一些类库。
JVM有很多内部同步机制,这在有的时候是非常值得我们去使用和学习的,接下来咱们就一起看看,JVM到底提供了哪些内部的同步方式。
内部的同步方式 1: static的强制同步机制
static这个关键字相信大家都不陌生,不过它附带的同步机制估计是很多猿友都不知道的。例如下面这个简单的类。
public class Static { private static String someField1 = someMethod1(); private static String someField2; static { someField2 = someMethod2(); } }
也就是说在实际执行一个类的静态初始化代码块时,虚拟机内部其实对其进行了同步,这就保证了无论多少个线程同时加载一个类,静态块中的代码执行且只执行一次。这点在单例模式当中得到了有效的应用,各位猿友有兴趣的可以去翻看LZ之前的单例模式博文。
内部的同步方式 2: synchronized的同步机制
synchronized是JVM提供的同步机制,它可以修饰方法或者代码块。此外,在修饰代码块的时候,synchronized可以指定锁定的对象,比如常用的有this,类字面常量等。在使用synchronized的时候,通常情况下,我们会针对特定的属性进行锁定,有时也会专门建立一个加锁对象。
public class Synchronized { private List<String> someFields; public void add(String someText) { //some code synchronized (someFields) { someFields.add(someText); } //some code } public Object[] getSomeFields() { //some code synchronized (someFields) { return someFields.toArray(); } } }
这种方式一般要优于使用this或者类字面常量进行锁定的方式,因为synchronized修饰的非静态成员方法默认是使用的this进行锁定,而synchronized修饰的静态成员方法默认是使用的类字面常量进行的锁定,因此如果直接在synchronized代码块中使用this或者类字面常量,可能会不经意的与synchronized方法产生互斥。通常情况下,使用属性进行加锁,能够更加有效的提高并发度,从而在保证程序正确的前提下尽可能的提高性能。
创建一个lock对象,是一个专门用于监控的对象:
它没有任何实际意义,只是为了与synchronized配合,完成对两个属性的统一锁定。当然,一般情况下,也可以使用this代替lock,这其实没有什么死的规定,完全可以按照实际情况而定。就是下面这种。
public class Synchronized { private Object lock = new Object(); private List<String> someFields1; private List<String> someFields2; public void add(String someText) { //some code synchronized (lock) { someFields1.add(someText); someFields2.add(someText); } //some code } public Object[] getSomeFields() { //some code Object[] objects1 = null; Object[] objects2 = null; synchronized (lock) { objects1 = someFields1.toArray(); objects2 = someFields2.toArray(); } Object[] objects = new Object[someFields1.size() + someFields2.size()]; System.arraycopy(objects1, 0, objects, 0, objects1.length); System.arraycopy(objects2, 0, objects, objects1.length, objects2.length); return objects; } }
不推荐以下这样,锁里面套锁。不小心出错,就容易发生死锁。
public class Synchronized { private List<String> someFields1; private List<String> someFields2; public void add(String someText) { //some code synchronized (someFields1) { synchronized (someFields2) { someFields1.add(someText); someFields2.add(someText); } } //some code } public Object[] getSomeFields() { //some code Object[] objects1 = null; Object[] objects2 = null; synchronized (someFields1) { synchronized (someFields2) { objects1 = someFields1.toArray(); objects2 = someFields2.toArray(); } } Object[] objects = new Object[someFields1.size() + someFields2.size()]; System.arraycopy(objects1, 0, objects, 0, objects1.length); System.arraycopy(objects2, 0, objects, objects1.length, objects2.length); return objects; } }
内部的同步方式 3:——条件等待:wait(),notify(),notifyAll()
刚才已经讨论过JVM内部同步的机制,接下来咱们一起看一下JVM内部的条件等待机制。Java当中的类有一个共同的父类Object,而在Object中,有一个wait的本地方法,这是一个神奇的方法。
它可以用来协调线程之间的协作,使用方式也比较简单,看一下下面这个例子,你就基本入门了哦。
public class ObjectWait { private volatile static boolean lock; public static void main(String[] args) throws InterruptedException { final Object object = new Object(); Thread thread1 = new Thread(new Runnable() { @Override public void run() { System.out.println("等待被通知!"); try { synchronized (object) { while (!lock) { object.wait(); } } } catch (InterruptedException e) { throw new RuntimeException(e); } System.out.println("已被通知"); } }); Thread thread2 = new Thread(new Runnable() { @Override public void run() { System.out.println("马上开始通知!"); synchronized (object) { object.notify(); lock = true; } System.out.println("已通知"); } }); thread1.start(); thread2.start(); Thread.sleep(100000); } }
这是一个最基本的例子,我们使用一个线程在object对象上等待另外一个线程的通知,当另外一个线程通知了以后,等待的线程将会继续运行。其实初次接触这个东西,是不是感觉很有意思呢。
wait一般情况下最常用的场景是构造一个花销非常大的对象的时候,比如JDK动态代理在生成代理类的时候就使用了这种方式。JDK6在生成一个代理类之前,会先检测一个是否正在生成中的标识,如果正在生成的话,JDK6就会在对象上等待,直到正在生成的代理类生成完毕,然后直接从缓存中获取。
这里需要提醒大家的一点是,wait,notify和notifyAll方法在使用前,必须获取到当前对象的锁,否则会告诉你非法的监控状态异常。还有一点,则是如果有多个线程在wait等待,那么调用notify会随机通知其中一个线程,而不会按照顺序通知。换句话说,notify的通知机制是非公平的,notify并不保证先调用wait方法的线程优先被唤醒。notifyAll方法则不存在这个问题,它将通知所有处于wait等待的线程。
2. JVM外部机制——同步篇
与JVM内部的同步机制对应的,就是外部的同步机制,也可以叫做编程式的同步机制。接下来,咱们就看看一些常用的外部同步方法。
外部机制1: ReentrantLock(可重入的锁)
ReentrantLock是JDK并发包中locks当中的一个类,专门用于弥补synchronized关键字的一些不足。接下来咱们就看一下synchronized关键字都有哪些不足,接着咱们再尝试使用ReentrantLock去解决这些问题。
优点:
1)synchronized关键字同步的时候,等待的线程将无法控制,只能死等。
解决方式:ReentrantLock可以使用tryLock(timeout, unit)方法去控制等待获得锁的时间,也可以使用无参数的tryLock方法立即返回,这就避免了死锁出现的可能性。
2)synchronized关键字同步的时候,不保证公平性,因此会有线程插队的现象。
解决方式:ReentrantLock可以使用构造方法ReentrantLock(fair)来强制使用公平模式,这样就可以保证线程获得锁的顺序是按照等待的顺序进行的,而synchronized进行同步的时候,是默认非公平模式的,但JVM可以很好的保证线程不被饿死。
缺点:
最主要不足的一点,就是ReentrantLock需要开发人员手动释放锁,并且必须在finally块中释放。
- ReentrantReadWriteLock():创建默认非公平的锁。
- ReentrantReadWriteLock(boolean fair):创建指定公平策略的锁。
public class Lock { private ReentrantLock nonfairLock = new ReentrantLock(); private ReentrantLock fairLock = new ReentrantLock(true); private List<String> someFields; public void add(String someText) { // 等待获得锁,与synchronized类似 nonfairLock.lock(); try { someFields.add(someText); } finally { // finally中释放锁是无论如何都不能忘的 nonfairLock.unlock(); } } public void addTimeout(String someText) { // 尝试获取,如果10秒没有获取到则立即返回 try { if (!fairLock.tryLock(10, TimeUnit.SECONDS)) { return; } } catch (InterruptedException e) { return; } try { someFields.add(someText); } finally { // finally中释放锁是无论如何都不能忘的 fairLock.unlock(); } } }
以上主要展示了ReentrantLock的基本用法和限时的等待,接下来咱们来看看当需要锁定多个对象的时候,ReentrantLock是如何使用的。从以下代码可以看出,用法与上面的synchronized中的方式非常相似。
import java.util.List; import java.util.concurrent.locks.ReentrantLock; public class ReentrantLockTest { private ReentrantLock lock = new ReentrantLock(); private List<String> someFields1; private List<String> someFields2; public void add(String someText) { //some code lock.lock(); try { someFields1.add(someText); someFields2.add(someText); } finally { lock.unlock(); } //some code } public Object[] getSomeFields() { //some code Object[] objects1 = null; Object[] objects2 = null; lock.lock(); try { objects1 = someFields1.toArray(); objects2 = someFields2.toArray(); } finally { lock.unlock(); } Object[] objects = new Object[someFields1.size() + someFields2.size()]; System.arraycopy(objects1, 0, objects, 0, objects1.length); System.arraycopy(objects2, 0, objects, objects1.length, objects2.length); return objects; } }
外部机制2:——条件等待: Condition
Condition可以使用返回值标识是否达到了超时时间。
上面咱们已经看过JVM自带的条件控制机制,是使用的本地方法wait实现的。那么在JDK的类库中,也有这样的一个类Condition,来弥补wait方法本身的不足。与之前一样,说到这里,咱们就来谈谈wait到底有哪些不足。
1)wait方法当使用带参数的方法wait(timeout)或者wait(timeout,nanos)时,无法反馈究竟是被唤醒还是到达了等待时间,大部分时候,我们会使用循环(就像上面的例子一样)来检测是否达到了条件。
2)由于wait,notify,notifyAll方法都需要获得当前对象的锁,因此当出现多个条件等待时,则需要依次获得多个对象的锁,这是非常恶心麻烦且繁琐的事情。
解决方式:Condition只需要获得Lock的锁即可,一个Lock可以拥有多个条件。
第一个问题比较好理解,只是Condition的await方法多了一个返回参数boolean去标识究竟是被唤醒还是超时。但是第二个问题比较繁琐一些,因此这里给出一个简单的示例,如下。
public class ObjectWait { public static void main(String[] args) throws InterruptedException { final Object object1 = new Object(); final Object object2 = new Object(); Thread thread1 = new Thread(new Runnable() { public void run() { try { System.out.println("等待object1被通知!"); synchronized (object1) { //获取对象锁 object1.wait(); } System.out.println("object1已被通知,马上开始通知object2!"); synchronized (object2) { object2.notify(); } System.out.println("通知object2完毕!"); } catch (InterruptedException e) { throw new RuntimeException(e); } } }); Thread thread2 = new Thread(new Runnable() { public void run() { try { System.out.println("马上开始通知object1!"); synchronized (object1) { object1.notify(); } System.out.println("通知object1完毕,等待object2被通知!"); synchronized (object2) { object2.wait(); } System.out.println("object2已被通知!"); } catch (InterruptedException e) { throw new RuntimeException(e); } } }); thread1.start(); Thread.sleep(1000); thread2.start(); } }
这是一个多条件的示例。基本逻辑是,线程1先等待线程2通知,然后线程2再等待线程1通知。请记住,这是两个不同的条件。可以看到,如果使用wait的话,必须两次获得两个锁,一不小心可能还会出现死锁。接下来,咱们看看Condition实现一样的功能是怎么实现的public class ConditionTest {
private static ReentrantLock lock = new ReentrantLock(); public static void main(String[] args) throws InterruptedException {
final Condition condition1 = lock.newCondition(); final Condition condition2 = lock.newCondition();
Thread thread1 = new Thread(new Runnable() { public void run() { lock.lock(); try { System.out.println("等待condition1被通知!"); condition1.await(); System.out.println("condition1已被通知,马上开始通知condition2!"); condition2.signal(); System.out.println("通知condition2完毕!"); } catch (InterruptedException e) { throw new RuntimeException(e); } finally { lock.unlock(); } } }); Thread thread2 = new Thread(new Runnable() { public void run() { lock.lock(); try { System.out.println("马上开始通知condition1!"); condition1.signal(); System.out.println("通知condition1完毕,等待condition2被通知!"); condition2.await(); System.out.println("condition2已被通知!"); } catch (InterruptedException e) { throw new RuntimeException(e); } finally { lock.unlock(); } } }); thread1.start(); Thread.sleep(1000); thread2.start(); } }
可以看到,我们只需要获取lock一次就可以了,在内部咱们可以使用两个或多个条件而不再需要多次获得锁。这种方式会更加直观,大大增加程序的可读性。
JDK当中除了以上的ReentrantLock和Condition之外,还有很多帮助猿友们协调线程的工具类。接下来咱们就一一混个脸熟。
1,CountDownLatch
这个类是为了帮助猿友们方便的实现一个这样的场景,就是某一个线程需要等待其它若干个线程完成某件事以后才能继续进行。比如下面的这个程序。
public class CountDownLatchTest { public static void main(String[] args) throws InterruptedException { final CountDownLatch countDownLatch = new CountDownLatch(10); for (int i = 0; i < 10; i++) { final int number = i + 1; Runnable runnable = new Runnable() { public void run() { try { Thread.sleep(1000); } catch (InterruptedException e) {} System.out.println("执行任务[" + number + "]"); this.countDown2(); System.out.println("完成任务[" + number + "]"); } private void countDown2() { System.out.println("==开始先执行coundown方法"); } }; Thread thread = new Thread(runnable); thread.start(); } System.out.println("主线程开始等待..."); countDownLatch.await(); System.out.println("主线程执行完毕..."); } }
这个程序的主线程会等待CountDownLatch进行10次countDown方法的调用才会继续执行。我们可以从打印的结果上看出来,尽管有的时候完成任务的打印会出现在主线程执行完毕之后,但这只是因为countDown已经执行完毕,主线程的打印语句先一步执行而已。
2,CyclicBarrier
这个类是为了帮助猿友们方便的实现多个线程一起启动的场景,就像赛跑一样,只要大家都准备好了,那就开始一起冲。比如下面这个程序,所有的线程都准备好了,才会一起开始执行。
import java.util.concurrent.BrokenBarrierException; import java.util.concurrent.CyclicBarrier; public class CyclicBarrierTest { public static void main(String[] args) { final CyclicBarrier cyclicBarrier = new CyclicBarrier(10); for (int i = 0; i < 10; i++) { final int number = i + 1; Runnable runnable = new Runnable() { public void run() { try { Thread.sleep(1000); } catch (InterruptedException e) {} System.out.println("等待执行任务[" + number + "]"); try { cyclicBarrier.await(); } catch (InterruptedException e) { } catch (BrokenBarrierException e) { } System.out.println("开始执行任务[" + number + "]"); } }; Thread thread = new Thread(runnable); thread.start(); } } }