zoukankan      html  css  js  c++  java
  • Java的volatile和Synchronized关键字

    volatile基本介绍:

      volatile可以看成是synchronized的一种轻量级的实现,但volatile并不能完全代替synchronized,volatile有synchronized可见性的特性,但没有synchronized原子性的特性。可见性即用volatile关键字修饰的成员变量表明该变量不存在工作线程的副本,线程每次直接都从主内存中读取,每次读取的都是最新的值,这也就保证了变量对其他线程的可见性。另外,使用volatile还能确保变量不能被重排序,保证了有序性

      volatile比synchronized编程更容易且开销更小,但具有一点的使用局限性,使用要相当小心,不能当锁使用。volatile不会像synchronized一样阻塞程序如果是读操作远多于写操作的情况可以建议使用volatile,它会有更好的性能。

    Java volatile关键字:

      volatile解决的问题是多个线程的内存可见性问题,在并发环境下,每个线程都会有自己的工作空间,每个线程只能访问各自的工作空间,而一些共享变量会被加载到每个线程的工作空间中,所以这里面就有一个问题,内存中的数据什么时候被加载到线程的工作缓存中,而线程工作空间中的内容什么时候会回写到内存中去。这两个步骤处理不当就会造成内存可加性问题,也就是数据的不一致,比如某个共享变量被线程A修改了,但是没有回写到内存中去,而线程B在加载了内存中的数据之后读取到的共享变量是脏数据,正确的做法应该是线程A的修改应该对线程B是可见的,更为通用一些,就是在并发环境下共享变量对多个线程是一致的。

      对于内存可见性的一点补充是,之所以会造成多个线程看到的共享变量的值不一样,是因为线程在占用CPU时间的时候,cpu为了提高处理速度不会直接和内存交互,而是会先将内存中的共享内容读取到内部缓存中(L1,L2),然后cpu在处理的过程中就只会和内部缓存交互,在多核心的机器中这样的处理方式就会造成内存可见性问题。

      volatile可以解决并发环境下的内存可见性问题,只需要在共享变量前面加上volatile关键字就可以解决,但是需要说明的是,volatile仅仅是解决内存可见性问题,对于像i++这样的问题还是需要使用其他的方式来保证线程安全。使用volatile解决内存可见性问题的原理是,如果对被volatile修饰的共享变量执行写操作的话,JVM就会向cpu发送一条Lock前缀的指令,cpu将会这个变量所在的缓存行(缓存中可以分配的最小缓存单位)写回到内存中去。但是在多处理器的情况下,将某个cpu上的缓存行写回到系统内存之后,其他cpu上该变量的缓存还是旧的,这样再进行后面的操作的时候就会出现问题,所以为了使得所有线程看到的内容都是一致的,就需要实现缓存一致性协议,cpu将会通过监控总线上传递过来的数据来判断自己的缓存是否过期,如果过期,就需要使得缓存失效,如果cpu再来访问该缓存的时候,就会发现缓存失效了,这时候就会重新从内存加载缓存。

    总结一下,volatile的实现原则有两条:

      1、JVM的Lock前缀的指令将使得cpu缓存写回到系统内存中去
      2、为了保证缓存一致性原则,在多cpu的情景下,一个cpu的缓存回写内存会导致其他的cpu上的缓存都失效,再次访问会重新从系统内存加载新的缓存内容。

    volatile使用场景:

      如果正确使用volatile的话,必须依赖下以下种条件:

        1、对变量的写操作不依赖当前变量的值;

        2、该变量没有包含在其他变量的不变式中。

        第1个条件就说明了volatile不是原子性的操作,不能使用n++类似的计数器,它不是线程安全的。

      1、状态的改变

        有些场景肯定会有状态的改变,完成一个主线程的停止等。首先我们开启了一个无限循环的主线程,判断变量isStop变量是否为true,如果true的话就退出程序,否则就一直循环,所以这个isStop的值是别的线程改变的。

     1 public class VolatileDemo {
     2     //使用volatile修饰变量
     3     private static volatile boolean isStop = false;
     4     public static void stopIt(){
     5         isStop = true;
     6     }
     7 
     8     public static void main(String[] args) {
     9         new Thread1().start();
    10         while (!isStop){
    11         }
    12         System.out.println("stop!!!");
    13     }
    14     static class Thread1 extends Thread{
    15         @Override
    16         public void run() {
    17             try {
    18                 Thread.sleep(3000);
    19             } catch (InterruptedException e) {
    20                 e.printStackTrace();
    21             }
    22             stopIt();
    23         }
    24     }
    25 }

      上面这段程序如果不加volatile的话会一直卡在循环,此时的线程拿到的值永远为false,加了volatile3秒后就输出stop,所以这段程序很好的解释了可见性的特点。

      2、读多写少的情况

         假设这样一种场景,有N个线程在读取变量的值,只有一个线程写变量的值,这时候就能保证读线程的可见性,又能保证写线程的线程安全问题。

         像n++不是原子类的操作,其实可以通过synchronized对写方法锁住,再用volatile修饰变量,这样就保证了读线程对变量的可见性,又保证了变量的原子性

     1 public class SynchronizedDemo {
     2     //使用volatile修饰变量
     3     private static volatile int n = 0;
     4     public static synchronized void add(){
     5         n++;
     6     }
     7 
     8     public static void main(String[] args) {
     9         new Thread1().start();
    10         while (n<100){
    11 
    12         }
    13         System.out.println("stop");
    14     }
    15     static class Thread1 extends Thread{
    16         @Override
    17         public void run() {
    18             try {
    19                 Thread.sleep(3000);
    20             } catch (InterruptedException e) {
    21                 e.printStackTrace();
    22             }
    23             for (int i = 0; i < 200; i++) {
    24                 add();
    25             }
    26         }
    27     }
    28 }

      如果n不加volatile,程序将一直循环,不能输出stop,也就是此时的线程拿到的值永远为0。

      当然不加volatile,对获取n的方法进行synchronized修饰也是能及时获取最新值的,但是性能会远低于volatile。

    Synchronized关键字:

      synchronized 除了保障原子性外,其实也保障了可见性。因为 synchronized 无论是同步的方法还是同步的代码块,都会先把主内存的数据拷贝到工作内存中,同步代码块结束,会把工作内存中的数据更新到主内存中,这样主内存中的数据一定是最新的。更重要的是禁用了乱序重组以及保证了值对存储器的写入,这样就可以保证可见性。

    Synchronized关键字的缺点:

      1.synchronized关键字同步的时候,等待的线程将无法控制,只能死等

      2.synchronized关键字同步的时候,不保证公平性,因此会有线程插队的现象。

    Synchronized同步方法:

    synchronized T methodName(){
    //do smoething
    }

      同步方法锁定的是当前对象。当多线程通过同一个对象引用多次调用当前同步方法时,需同步执行。也就是说当一个线程访问同步方法时,其他线程访问这个方法将会被阻塞(等待锁)。

    Synchronized同步代码块:

      用关键字 synchronized 声明方法在某些情况下是有弊端的,比如 A 线程调用同步方法执行一个较长时间的任务,那么 B 线程必须等待比较长的时间。这种情况下可以尝试使用 synchronized 同步代码块来解决问题。

      同步代码块的同步粒度更加细致,是商业开发中推荐的编程方式。可以定位到具体的同步位置,而不是简单的将方法整体实现同步逻辑。在效率上,相对更高。

      把需要同步的代码块包起来,注意不要把耗时的操作放在同步代码块中。比如打印输出、IO 操作等等。

    Synchronized锁定的对象:

      1.锁定临界对象

    public class SynchronizedDemo<T> {
        Object object = new Object();
    
        T methodName(){
            synchronized(object){
            //do something
            }
        }
    }

      同步代码块在执行时,是锁定 object对象。当多个线程调用同一个方法时,锁定对象不变的情况下,需同步执行。

      synchronized(非this对象 object),这个对象如果是实例变量的话,指的是对象的引用,只要对象的引用不变,即使改变了对象的属性,运行结果依然是同步的。也就是锁的是堆内存中的对象,并不是引用。

      1.当多个线程同时执行 synchronized(object){} 同步代码块时呈同步效果

      2.当其他线程执行 object 对象中的 synchronized 同步方法时呈同步效果

      3.当其他线程执行 object 对象方法中的 synchronized(this) 代码块时也呈同步效果

      4.在定义同步代码块时,不要使用常量对象作为锁目标对象。比如字符串常量、整形等。

      2.锁定当前对象:

    T methodName(){
        synchronized(this){
        //do something
        }
    }

      当锁定对象为this 时,相当于同步方法。

      3.锁定Class对象(同步静态方法)

    /**
     * 同步方法 - static
     * 静态同步方法,锁的是当前类型的类对象。在本代码中就是Test_02.class
     */
    package concurrent.t01;
    import java.util.concurrent.TimeUnit;
    
    public class Test_02 {
        private static int staticCount = 0;
    
        public static synchronized void testSync4(){
            System.out.println(Thread.currentThread().getName() 
                    + " staticCount = " + staticCount++);
            try {
                TimeUnit.SECONDS.sleep(3);
            } catch (InterruptedException e) {
                // TODO Auto-generated catch block
                e.printStackTrace();
            }
        }
    
        public static void testSync5(){
            synchronized(Test_02.class){
                System.out.println(Thread.currentThread().getName() 
                        + " staticCount = " + staticCount++);
            }
        }
    }

      synchronized 还可以应用在静态方法上,如果这么写,则代表的是对当前 .java 文件对应的 Class 类加锁。静态同步方法和非静态同步方法持有的是不同的锁,前者是类锁,后者是对象锁。

      3.Synchronized锁重入:

    public class Test_06 {
    
        synchronized void m1(){ // 锁this
            System.out.println("m1 start");
            try {
                TimeUnit.SECONDS.sleep(2);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            m2();//调用加锁方法
            System.out.println("m1 end");
        }
        synchronized void m2(){ // 锁this
            System.out.println("m2 start");
            try {
                TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("m2 end");
        }
    
        public static void main(String[] args) {
            new Test_06().m1();
        }
    }

      关键字 synchronized 拥有锁重入的功能。所谓锁重入的意思就是:当一个线程得到一个对象锁后,再次请求此对象锁时时可以再次得到该对象的锁的。 锁重入的实现是通过同一个线程,多次调用同步代码,锁定同一个锁对象,可重入。

      这种锁重入的机制,也支持在父子类继承的环境中。 子类同步方法覆盖父类同步方法。可以指定调用父类的同步方法。

    Synchronized多方法调用原子性问题:

    public class Test_05 {
        private double d = 0.0;
        public synchronized void m1(double d){
            try {
                // 相当于复杂的业务逻辑代码。
                TimeUnit.SECONDS.sleep(2);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            this.d = d;
        }
    
        public double m2(){
            return this.d;
        }
    
        public static void main(String[] args) {
            final Test_05 t = new Test_05();
    
            new Thread(new Runnable() {
                @Override
                public void run() {
                    t.m1(100);
                }
            }).start();
            System.out.println(t.m2());
            try {
                TimeUnit.SECONDS.sleep(3);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(t.m2());
        }
    }

      打印结果为:

        0.0

        100.0

      由以上打印结果可知:

        同步方法只能保证当前方法的原子性,不能保证多个业务方法之间的互相访问的原子性。

        注意:在商业开发中,多方法要求结果访问原子操作,需要多个方法都加锁,且锁定统一个资源。

        一般来说,商业项目中,不考虑业务逻辑上的脏读问题。在数据库上要考虑脏读。

    Synchronized锁的底层实现?

      Java 虚拟机中的同步(Synchronization)是基于进入和退出管程(Monitor)对象实现。同步方法并不是由 monitor enter 和 monitor exit 指令来实现同步的,而是由方法调用指令读取运行时常量池中方法的 ACC_SYNCHRONIZED 标志来隐式实现的。

      对象内存简图:

      

       对象头:存储对象的 hashCode、锁信息或分代年龄或 GC 标志,类型指针指向对象的类元数据,JVM 通过这个指针确定该对象是哪个类的实例等信息。

      实例变量:存放类的属性数据信息,包括父类的属性信息

      填充数据:由于虚拟机要求对象起始地址必须是8字节的整数倍。填充数据不是必须存在的,仅仅是为了字节对齐

      当在对象上加锁时,数据是记录在对象头中。当执行 synchronized 同步方法或同步代码块时,会在对象头中记录锁标记,锁标记指向的是 monitor 对象(也称为管程或监视器锁)的起始地址。每个对象都存在着一个 monitor 与之关联,对象与其 monitor 之间的关系有存在多种实现方式,如 monitor 可以与对象一起创建销毁或当线程试图获取对象锁时自动生成,但当一个 monitor 被某个线程持有后,它便处于锁定状态。在Java虚拟机(HotSpot)中,monitor 是由 ObjectMonitor 实现的。

      ObjectMonitor 中有两个队列,_WaitSet 和 _EntryList,以及 _Owner 标记。其中 _WaitSet 是用于管理等待队列(wait)线程的,_EntryList 是用于管理锁池阻塞线程的,_Owner 标记用于记录当前执行线程。线程状态图如下:

      

       当多线程并发访问同一个同步代码时,首先会进入 _EntryList,当线程获取锁标记后,monitor 中的 _Owner 记录此线程,并在 monitor 中的计数器执行递增计算(+1),代表锁定,其他线程在 _EntryList 中继续阻塞。若执行线程调用 wait 方法,则monitor中的计数器执行赋值为0计算,并将 _Owner 标记赋值为 null,代表放弃锁,执行线程进如 _WaitSet 中阻塞。若执行线程调用 notify/notifyAll 方法,_WaitSet 中的线程被唤醒,进入 _EntryList 中阻塞,等待获取锁标记。若执行线程的同步代码执行结束,同样会释放锁标记,monitor中的 _Owner 标记赋值为 null,且计数器赋值为0计算。

     锁的种类:

      Java 中锁的种类大致分为:

        1)偏向锁

        2)自旋锁

        3)轻量级锁

        4)重量级锁

      锁的使用方式:先提供偏向锁,如果不满足的时候,升级为轻量级锁,再不满足,升级为重量级锁。自旋锁是一个过渡的锁状态,不是一种实际的锁类型。

      注意:锁只能升级,不能降级。

      同步方法同步代码块中解释的就是重量级锁

      偏向锁:

        是一种编译解释锁。如果代码中不可能出现多线程并发争抢同一个锁的时候,JVM 编译代码,解释执行的时候,会自动的放弃同步信息。消除 synchronized 的同步代码结果。使用锁标记的形式记录锁状态。在 Monitor 中有变量ACC_SYNCHRONIZED。当变量值使用的时候,代表偏向锁锁定。可以避免锁的争抢和锁池状态的维护。提高效率。

      轻量级锁:

        过渡锁。当偏向锁不满足,也就是有多线程并发访问锁定同一个对象的时候,先提升为轻量级锁。也是使用标记ACC_SYNCHRONIZED 标记记录的。ACC_UNSYNCHRONIZED 标记记录未获取到锁信息的线程。就是只有两个线程争抢锁标记的时候,优先使用轻量级锁。

      自旋锁:

        是一个过渡锁,是偏向锁和轻量级锁的过渡。当获取锁的过程中,未获取到。为了提高效率,JVM 自动执行若干次空循环再次申请锁,而不是进入阻塞状态的情况。称为自旋锁。自旋锁提高效率就是避免线程状态的变更。

    总结:

    1.   每个对象都有一个锁。用来在多线程访问的时候实现同步。
    2.   synchronized 取得的锁都是对象锁,而不是把一段代码或方法(函数)当作锁,哪个线程先执行带 synchronized 关键字的方法,哪个线程就持有该方法所属对象的锁,其他线程都只能呈等待状态。
    3.   从执行效率的角度考虑,有时候我们未必要把整个方法都加上synchronized,而是可以采取 synchronized 块的方式,对会引起线程安全问题的那一部分代码进行 synchronized 就可以了。
    4.   两个 synchronized 块之间具有互斥性
    5.   synchronized 块获得的是一个对象锁,换句话说,synchronized 块锁定的是整个对象。
    6.   Java 还支持对”任意对象”作为对象监视器来实现同步的功能。这个”任意对象”大多数是实例变量及方法的参数,使用格式为 synchronized(非this对象)
    7.   同步方法只影响锁定同一个锁对象的同步方法。不影响其他线程调用非同步方法,或调用其他锁资源的同步方法。
  • 相关阅读:
    Framework 4.0 新关键字dynamic 之我见(二)
    随便歇歇
    最近的一些总结
    一周最新示例代码回顾 (7/16 7/22)
    一周最新示例代码回顾 (5/28–6/3)
    一周最新示例代码回顾 (5/14–5/20)
    一周最新示例代码回顾 (6/25 7/1)
    微软一站式示例代码浏览器本周更新发布
    一周最新示例代码回顾 (6/11 6/17)
    示例代码浏览器5.4功能更新
  • 原文地址:https://www.cnblogs.com/wk-missQ1/p/12889974.html
Copyright © 2011-2022 走看看