引言
很久没有跟大家再聊聊并发了,今天LZ闲来无事,跟大家再聊聊并发。由于时间过去的有点久,因此LZ就不按照常理出牌了,只是把自己的理解记录在此,如果各位猿友觉得有所收获,就点个推荐或者留言激励下LZ,如果觉得浪费了自己宝贵的时间,也可以发下牢骚。
好了,废话就不多说了,现在就开始咱们的并发之旅吧。
并发编程的简单分类
并发常见的编程场景,一句话概括就是,需要协调多个线程之间的协作,已保证程序按照自己原本的意愿执行。那么究竟应该如何协调多个线程?
这个问题比较宽泛,一般情况下,我们按照方式的纬度去简单区分,有以下两种方式:
1,第一种是利用JVM的内部机制。
2,第二种是利用JVM外部的机制,比如JDK或者一些类库。
第一种方式一般是通过synchronized关键字等方式去实现,第二种则一般是使用JDK当中的类去手动实现。两种方式十分相似,他们的区别有点类似于C/C++和Java的垃圾搜集方式的区别,C/C++手动释放内存的方式更加灵活和高效,而Java自动垃圾搜集的方式则更加安全和方便。
并发一直被认为是编程当中的高级特性,也是很多大公司在面试的时候都比较在意的部分,因此掌握好并发的简单技巧,还是能够让自己的技术沉淀有质的飞跃的。
详解JVM内部机制——同步篇
JVM有很多内部同步机制,这在有的时候是非常值得我们去使用和学习的,接下来咱们就一起看看,JVM到底提供了哪些内部的同步方式。
1,static的强制同步机制
static这个关键字相信大家都不陌生,不过它附带的同步机制估计是很多猿友都不知道的。例如下面这个简单的类。
public class Static {
private static String someField1 = someMethod1();
private static String someField2;
static {
someField2 = someMethod2();
}
}
首先上面这一段代码在编译以后会变成下面这个样子,这点各位可以使用反编译工具去验证。
public class Static {
private static String someField1;
private static String someField2;
static {
someField1 = someMethod1();
someField2 = someMethod2();
}
}
不过在JVM真正执行这段代码的时候,其实它又变成了下面这个样子。
public class Static {
private static String someField1;
private static String someField2;
private static volatile boolean isCinitMethodInvoked = false;
static {
synchronized (Static.class) {
if (!isCinitMethodInvoked) {
someField1 = someMethod1();
someField2 = someMethod2();
isCinitMethodInvoked = true;
}
}
}
}
也就是说在实际执行一个类的静态初始化代码块时,虚拟机内部其实对其进行了同步,这就保证了无论多少个线程同时加载一个类,静态块中的代码执行且只执行一次。这点在单例模式当中得到了有效的应用,各位猿友有兴趣的可以去翻看LZ之前的单例模式博文。
2,synchronized的同步机制
synchronized是JVM提供的同步机制,它可以修饰方法或者代码块。此外,在修饰代码块的时候,synchronized可以指定锁定的对象,比如常用的有this,类字面常量等。在使用synchronized的时候,通常情况下,我们会针对特定的属性进行锁定,有时也会专门建立一个加锁对象。
直接给方法加synchronized关键字,或者使用this,类字面常量作为锁的方式比较常用,也比较简单,这里就不再举例了。我们来看看对某一属性进行锁定的方式,如下。
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方法产生互斥。通常情况下,使用属性进行加锁,能够更加有效的提高并发度,从而在保证程序正确的前提下尽可能的提高性能。
再来看一段比较特殊的代码,如果猿友们经常看JDK源码或者一些优秀的开源框架源码的话,或许会见过这种方式。
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;
}
}
lock是一个专门用于监控的对象,它没有任何实际意义,只是为了与synchronized配合,完成对两个属性的统一锁定。当然,一般情况下,也可以使用this代替lock,这其实没有什么死的规定,完全可以按照实际情况而定。还有一种比较不推荐的方式,就是下面这种。
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;
}
}
这种加锁方式比较挑战人的细心程度,万一哪个不小心把顺序搞错了,就可能造成死锁。因此如果你非要使用这种方式,请做好被你的上司行刑的准备。
详解JVM外部机制——同步篇
与JVM内部的同步机制对应的,就是外部的同步机制,也可以叫做编程式的同步机制。接下来,咱们就看看一些常用的外部同步方法。
ReentrantLock(可重入的锁)
ReentrantLock是JDK并发包中locks当中的一个类,专门用于弥补synchronized关键字的一些不足。接下来咱们就看一下synchronized关键字都有哪些不足,接着咱们再尝试使用ReentrantLock去解决这些问题。
1)synchronized关键字同步的时候,等待的线程将无法控制,只能死等。
解决方式:ReentrantLock可以使用tryLock(timeout, unit)方法去控制等待获得锁的时间,也可以使用无参数的tryLock方法立即返回,这就避免了死锁出现的可能性。
2)synchronized关键字同步的时候,不保证公平性,因此会有线程插队的现象。
解决方式:ReentrantLock可以使用构造方法ReentrantLock(fair)来强制使用公平模式,这样就可以保证线程获得锁的顺序是按照等待的顺序进行的,而synchronized进行同步的时候,是默认非公平模式的,但JVM可以很好的保证线程不被饿死。
ReentrantLock有这样一些优点,当然也有不足的地方。最主要不足的一点,就是ReentrantLock需要开发人员手动释放锁,并且必须在finally块中释放。
下面给出两个简单的ReentrantLock例子,请各位猿友收看。
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(); } } }