引言

 

  很久没有跟大家再聊聊并发了,今天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();
     }
     
}
View Code

  首先上面这一段代码在编译以后会变成下面这个样子,这点各位可以使用反编译工具去验证。

public class Static {

    private static String someField1;
    
    private static String someField2;
    
    static {
        someField1 = someMethod1();
        someField2 = someMethod2();
    }
    
}
View Code

  不过在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;
            }
        }
    }

}
View Code

  也就是说在实际执行一个类的静态初始化代码块时,虚拟机内部其实对其进行了同步,这就保证了无论多少个线程同时加载一个类,静态块中的代码执行且只执行一次。这点在单例模式当中得到了有效的应用,各位猿友有兴趣的可以去翻看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();
        }
    }
    
}
View Code

  这种方式一般要优于使用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;
    }
    
}
View Code

  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;
    }
    
}
View Code

  这种加锁方式比较挑战人的细心程度,万一哪个不小心把顺序搞错了,就可能造成死锁。因此如果你非要使用这种方式,请做好被你的上司行刑的准备。

  

详解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();
        }
    }

}
View Code

相关文章: