synchronized / Lock+volatile

作者: 风信子丶 | 来源:发表于2016-12-31 23:36 被阅读336次

    最近在学习单例模式和Android消息传递方面的知识,都用到了synchronized同步关键字,于是整理下思路。

    由于同一进程内线程共享同一片内存单元,当多线程进行读写的时候就会存在冲突的问题。Java提供了synchronized和Lock来实现同步互斥访问,有效的避免了一个临界数据同时被多个线程同时访问可能出现的错误。

    synchronized

    synchronized是java中的一个关键字,是Java语言内置的特性。synchronized的使用主要有2种:同步方法和同步代码块。

    使用synchronized需要明确的几个问题:

    • 无论synchronized关键字加在方法上还是对象上,它取得的锁都是对象,而不是把一段代码或函数当作锁――而且同步方法很可能还会被其他线程的对象访问。
    • 每个对象只有一个锁(lock)与之相关联。
    • 实现同步是要很大的系统开销作为代价的,甚至可能造成死锁,所以尽量避免无谓的同步控制。

    synchronized方法

    synchronized 方法控制对类成员变量的访问:每个类实例对应一把锁,每个synchronized 方法都必须获得调用该方法的类实例的锁方能执行,否则所属线程阻塞,方法一旦执行,就独占该锁,直到从该方法返回时才将锁释放,此后被阻塞的线程方能获得该锁,重新进入可执行状态。这种机制确保了同一时刻对于每一个类实例,其所有声明为 synchronized 的成员函数中至多只有一个处于可执行状态(因为至多只有一个能够获得该类实例对应的锁),从而有效避免了类成员变量的访问冲突(只要所有可能访问类成员变量的方法均被声明为 synchronized)。synchronized修饰方法又分为修饰静态方法和修饰非静态方法。

    synchronized修饰非静态方法

    具体看下面例子:

    public class SynchronizedTest {
        public static void main(String[] args){
            Test t1 = new Test();
            t1.start();
            Test t2 = new Test();
            t2.start(); 
        }
    }
    
    class Test extends Thread{
        @Override
        public void run() { 
            writeSomething(); 
        } 
        public synchronized void writeSomething(){
            for (int i=0; i<10; i++){
                System.out.print(i+" ");
            }
        } 
        public void printSomething(){
            for (int i=0; i<10; i++){
                System.out.print(i+" ");
            } 
        }
    }
    

    输出结果如下:

    0 0 1 1 2 2 3 4 3 5 4 6 7 8 9 5
    6 7 8 9 
    //这里有一个换行
    

    Test类的writeSomething方法加了synchronized可是没有像预期那样输出俩行0-9,这是为什么呢?因为上例中synchronized用来修饰非静态方法,而非静态方法又是类对象所有,所以在不同对象的writeSomething()方法互不干扰。这对不对呢,我们在试一下就知道了。测试代码如下:

    public class SynchronizedTest {
        static Test test1 = new Test();
        public static void main(String[] args){
            new Thread(new Runnable() {
                @Override
                public void run() {
                    test1.writeSomething();
                }
            }).start();
            test1.writeSomething(); //这一句要放在new Thread后面,不然会把这一句执行完才执行new Thread
        }
    }
    
    class Test{ 
        public synchronized void writeSomething(){
            for (int i=0; i<10; i++){
                System.out.print(i+" ");
            } 
            System.out.println();
        }
    }
    

    new一个线程和主线程都执行test1的writeSomething方法,输出结果如下:

    0 1 2 3 4 5 6 7 8 9
    0 1 2 3 4 5 6 7 8 9 
    //这里有一个换行
    

    可以看到同一对象synchronized关键字起作用了,说明了synchronized修饰非静态方法,是作用在同一对象上的。下面还有个例子:

    public class SynchronizedTest {
        static Test test1 = new Test();
    
        public static void main(String[] args){
            new Thread(new Runnable() {
    
                @Override
                public void run() {
                    test1.writeSomething();
                }
            }).start();
    
            test1.printSomething(); //这一句要放在new Thread后面,不然会把这一句执行完才执行new Thread
        }
    }
    
    class Test{
        public synchronized void writeSomething(){
            for (int i=0; i<10; i++){
                System.out.print(i+" ");
            }
            System.out.println();
        } 
    
        public synchronized void printSomething(){
            for (int i=0; i<10; i++){
                System.out.print(i+" ");
            } 
        System.out.println(); 
        }
    }
    

    这个例子在上面的基础上多加了一个同步方法,猜想一下,正常情况下,不同线程执行不同的方法,应该是交叉执行打印的,先看下输出结果:

    0 1 2 3 4 5 6 7 8 9
     0 1 2 3 4 5 6 7 8 9
     //换行
    

    输出结果跟预期的不一样,输出2行0-9说明是按照先后的顺序执行的(不放心的话可以多执行几次)为什么会这样呢?前面提到每一个对象只有一锁与之对应,当执行test1.writeSomething()时相当于当前线程拿到了test1的锁,而其他线程只有等待它释放锁才能继续执行。而后面的test1.printSomething()方法继续执行正需要这个锁。所以它就必须等到前面test1.writeSomething()执行完,释放了锁以后拿到锁才能继续执行,所以就有了这样的输出结果。当一个线程访问object的一个synchronized同步方法时,其他线程对object中所有其它synchronized同步方法的的访问将被阻塞。

    synchronized修饰静态方法

    修饰静态方法没什么好解释的,因为静态方法不属于类对象,它是属于类的,所以如果用synchronized修饰静态方法,那么它在所有类对象中都是同步的。


    synchronized代码块

    我们在用synchronized关键字的时候,能缩小代码段的范围就尽量缩小,能在代码段上加同步就不要再整个方法上加同步。这叫减小锁的粒度,使代码更大程度的并发。因为当方法体过于庞大而需要同步的部分又很少,锁的时间就加长了,别的线程是不是要等很久。所以往往同步代码块比同步方法好用。synchronized代码块又分为这么几种:synchronized(this),synchronized(className.class)和synchronized(Object obj)。

    synchronized(this)

    synchronized(this)类似于前面的synchronized修饰非静态方法,锁都在当前对象,只限制当前对象对该代码块的同步。

    public synchronized void writeSomething(){
        //其他代码
        synchronized (this){
            for (int i=0; i<10; i++){
                System.out.print(i+" ");
            }
        System.out.println();
        }
        //其他代码
    }
    

    synchronized(className.class)

    synchronized(className.class)类似于前面的synchronized修饰静态方法,锁在类而不在类对象,只要是className类对象访问该代码块都被要求同步。

    class Test {
        public synchronized void writeSomething() {
            //其他代码
            synchronized (Test.class) {
                for (int i = 0; i < 10; i++) {
                    System.out.print(i + " ");
                }
                System.out.println();
            }
            //其他代码
        }
    }
    

    synchronized(Object obj)

    这时锁就是对象,谁拿到这个锁谁就可以运行它所控制的那段代码。当有一个明确的对象作为锁时,就可以这样写程序,但当没有明确的对象作为锁,只是想让一段代码同步时,可以创建一个特殊的instance变量(它得是一个对象)来充当锁:

    class Test {
        private static byte[] lock = new byte[0]; // 特殊的instance变量 public
    
        synchronized void writeSomething() {
            //其他代码 
            synchronized (lock) {
                for (int i = 0; i < 10; i++) {
                    System.out.print(i + " ");
                }
                System.out.println(); 
            }
            //其他代码
        }
    }
    

    (tips:用的比较多的就是零长度的byte数组对象,创建起来将比任何对象都经济。查看编译后的字节码:生成零长度的byte[]对象只需3条操作码,而Object lock = new Object()则需要7行操作码。)

    这里的锁的作用范围取决于lock的作用域,谁能拿到这个lock就能访问该代码块。比如将lock作为staic全局变量就是类所有,这时synchronized (lock)就相当于synchronized (className.class);相反,将lock作为局部变量(放在方法内)该synchronized 将失效,因为每个访问该方法的都能获得一个lock对象。

    Lock

    之前在面试中被问过相关问题,所有之后就花时间了解了下。Lock和synchronized 不同,synchronized 会自动释放锁,而Lock必须手动释放,如果没有释放就可能造成死锁。并且Lock的使用一般放在try{}catch块中,最后在finally中释放锁,保证抛出异常时锁会被释放。点开Lock的源码可以看到,Lock是一个接口

    public interface Lock {
        void lock();
        void lockInterruptibly() throws InterruptedException;
        boolean tryLock();
        boolean tryLock(long time, TimeUnit unit) throws InterruptedException;
        void unlock(); 
        Condition newCondition();
    }
    

    lock()、tryLock()、tryLock(long time, TimeUnit unit)和lockInterruptibly()是用来获取锁的。unLock()方法是用来释放锁的。newCondition()返回的是一个Condition对象,关于这个类我翻了翻api表示看不懂。

    lock()、tryLock()、tryLock(long time, TimeUnit unit)和lockInterruptibly()都是用来获取锁的,那他们有什么区别呢?
    lock()是使用的最多的,它就是用来获取锁,如果锁被其他线程拿到,它就等待。
    tryLock()是有返回值的,尝试获取锁,成功就返回true失败就返回false。所以说这个方法无论拿不拿得到锁都会立即返回而不会在那等待。

    tryLock(long time, TimeUnit unit)方法和tryLock()方法是类似的,只不过区别在于这个方法在拿不到锁时会等待一定的时间,在时间期限之内如果还拿不到锁,就返回false。如果一开始拿到锁或者在等待期间内拿到了锁,则返回true。

    lockInterruptibly()方法比较特殊,当通过这个方法去获取锁时,如果线程正在等待获取锁,则这个线程能够响应中断,即中断线程的等待状态。也就使说,当两个线程同时通过lock.lockInterruptibly()想获取某个锁时,假若此时线程A获取到了锁,而线程B只有在等待,那么对线程B调用threadB.interrupt()方法能够中断线程B的等待过程。


    volatile

    volatile的使用场景,通过关键字sychronize可以防止多个线程进入同一段代码,在某些特定场景中,volatile相当于一个轻量级的sychronize,因为不会引起线程的上下文切换,一旦一个共享变量(类的成员变量、类的静态成员变量)被volatile修饰之后,那么就具备了两层语义:

    • 保证了不同线程对这个变量进行操作时的可见性,即一个线程修改了某个变量的值,这新值对其他线程来说是立即可见的。volatile关键字会强制将修改的值立即写入主存,使线程的工作内存中缓存变量行无效。
    • 禁止进行指令重排序。

    在java虚拟机的内存模型中,有主内存和工作内存的概念,每个线程对应一个工作内存,并共享主内存的数据。

    • 对于普通变量:读操作会优先读取工作内存的数据,如果工作内存中不存在,则从主内存中拷贝一份数据到工作内存中;写操作只会修改工作内存的副本数据,这种情况下,其它线程就无法读取变量的最新值。
    • 对于volatile变量,读操作时JMM会把工作内存中对应的值设为无效,要求线程从主内存中读取数据;写操作时JMM会把工作内存中对应的数据刷新到主内存中,这种情况下,其它线程就可以读取变量的最新值。我第一次接触到volatile关键字是在双重锁的单例模式中
    public class Singleton {
    
        private volatile static Singleton sSingleton;  
    
        private Singleton (){}  
    
        public static Singleton getSingleton() {
            if (sSingleton == null) {
                synchronized (Singleton.class) {
                    if (sSingleton == null) {
                        sSingleton = new Singleton();
                    }
                }
             }  
             return sSingleton;
          }
    }
    

    当时想了蛮久为什么要判断2次不为空,所以印象蛮深刻的。这是因为如果没有volatile关键字,问题可能会出在singleton = new Singleton();这句,用伪代码表示

    inst = allocat();  // 分配内存 
    sSingleton = inst; // 赋值
    constructor(inst);;// 真正执行构造函数
    

    可能会由于虚拟机的优化等导致赋值操作先执行,而构造函数还没完成,导致其他线程访问得到singleton变量不为null,但初始化还未完成,导致程序崩溃。


    synchronized的不足

    如果一个代码块被synchronized修饰了,当一个线程获取了对应的锁,并执行该代码块时,其他线程便只能一直等待,等待获取锁的线程释放锁,而这里获取锁的线程释放锁只会有两种情况:

    1)获取锁的线程执行完了该代码块,然后线程释放对锁的占有;

    2)线程执行发生异常,此时JVM会让线程自动释放锁。

    那么如果这个获取锁的线程由于要等待IO或者其他原因(比如调用sleep方法)被阻塞了,但是又没有释放锁,其他线程便只能干巴巴地等待,试想一下,这多么影响程序执行效率。因此就需要有一种机制可以不让等待的线程一直无期限地等待下去(比如只等待一定的时间或者能够响应中断),通过Lock就可以办到。再举个例子:当有多个线程读写文件时,读操作和写操作会发生冲突现象,写操作和写操作会发生冲突现象,但是读操作和读操作不会发生冲突现象。但是采用synchronized关键字来实现同步的话,就会导致一个问题:如果多个线程都只是进行读操作,所以当一个线程在进行读操作时,其他线程只能等待无法进行读操作。因此就需要一种机制来使得多个线程都只是进行读操作时,线程之间不会发生冲突,通过Lock就可以办到。另外,通过Lock可以知道线程有没有成功获取到锁。这个是synchronized无法办到的。

    也就是说Lock提供了比synchronized更多的功能。但是要注意以下几点:

    1)Lock不是Java语言内置的,synchronized是Java语言的关键字,因此是内置特性。Lock是一个类,通过这个类可以实现同步访问;

    2)Lock和synchronized有一点非常大的不同,采用synchronized不需要用户去手动释放锁,当synchronized方法或者synchronized代码块执行完之后,系统会自动让线程释放对锁的占用;而Lock则必须要用户去手动释放锁,如果没有主动释放锁,就有可能导致出现死锁现象。


    Lock与synchronized的不同:

    • Lock支持在等待一定的时间或者能够响应中断。
    • Lock支持在多个线程都只是进行读操作时,线程之间不会发生冲突,通过Lock就可以办到。
    • 通过Lock可以知道线程有没有成功获取到锁。
    • Lock不是Java语言内置的。synchronized是Java语言的关键字,因此是内置特性。
    • Lock是一个类,通过这个类可以实现同步访问。
    • Lock必须要用户去手动释放锁,而synchronized方法或者synchronized代码块执行完之后,系统会自动让线程释放对锁的占用。

    参考链接:
    Java synchronized详解
    Java并发编程:Lock
    Synchronized/Lock/Volatile
    单例模式,你知道的和你所不一定知道的一切

    相关文章

      网友评论

        本文标题:synchronized / Lock+volatile

        本文链接:https://www.haomeiwen.com/subject/pgybvttx.html