• synchronized详解


    关于synchronized,本文从使用方法,底层原理和锁的升级优化这几个方面来介绍。

    1.synchronized的使用:

    synchronized可以保证在同一时刻,只有一个线程可以操作共享变量,并且该共享变量的变化对其他线程可见。它的使用方法有三种:

    1.1 作用于实例方法

    当synchronized作用于实例方法时,它的锁是当前的实例对象。通过以下demo来看下它的用法:

    public class SynchronizedDemo implements Runnable{
    
        static int i = 0;
        
    // 作用于实例方法
    public synchronized void increase(){ i++; } @Override public void run() { for (int i = 0; i < 10000; i++) { increase(); } } public static void main(String[] args) throws InterruptedException { SynchronizedDemo synchronizedDemo = new SynchronizedDemo(); Thread t1 = new Thread(synchronizedDemo); Thread t2 = new Thread(synchronizedDemo); t1.start(); t2.start(); t1.join(); t2.join(); System.out.println(i); } }

    以上demo的输出结果是:20000;在java中,任意一个对象都可以作为锁,注意在这里线程t1和t2共用了一把锁,都是synchronizedDemo这个对象。再看下,如下demo:

    public class SynchronizedDemo implements Runnable{
    
        static int i = 0;
        
        public synchronized void increase(){
            i++;
        }
        
        @Override
        public void run() {
            for (int i = 0; i < 10000; i++) {
                increase();
            }
        }
        
        public static void main(String[] args) throws InterruptedException {
            SynchronizedDemo synchronizedDemo = new SynchronizedDemo();
            SynchronizedDemo synchronizedDemo2 = new SynchronizedDemo();
            Thread t1 = new Thread(synchronizedDemo);
            Thread t2 = new Thread(synchronizedDemo2);
            t1.start();
            t2.start();
            t1.join();
            t2.join();
            System.out.println(i);
        }
    
    }

    这个demo会出现什么结果呢?通过多次运行我们发现,输出的值可能会小于20000;原因就是线程t1和t2使用了各自的锁,那么synchronized的存在就毫无意义了,无法保证线程安全。那么针对这种有多个对象(锁)的情况,如何解决呢?将synchronized作用于静态方法就行了。

    1.2 作用于静态方法

    当作用于静态方法时,锁是当前类的class对象。看如下demo:

    public class SynchronizedDemo implements Runnable{
    
        static int i = 0;
        
        public static synchronized void increase(){
            i++;
        }
        
        @Override
        public void run() {
            for (int i = 0; i < 10000; i++) {
                increase();
            }
        }
        
        public static void main(String[] args) throws InterruptedException {
            SynchronizedDemo synchronizedDemo = new SynchronizedDemo();
            SynchronizedDemo synchronizedDemo2 = new SynchronizedDemo();
            Thread t1 = new Thread(synchronizedDemo);
            Thread t2 = new Thread(synchronizedDemo2);
            t1.start();
            t2.start();
            t1.join();
            t2.join();
            System.out.println(i);
        }
    
    }

    运行结果:20000;此时是线程安全的,因为t1和t2共用了同一个锁,该锁就是SynchronizedDemo的class对象。

    1.3 作用于代码块

    当作用于代码块时,锁是synchronized括号里配置的对象。对于作用于代码块的使用场景是这样的:如果一个方法体很大,里面有一些耗时操作,但是我们需要同步的仅仅是一部分代码,如果对整个方法进行同步,显然是不合理的,所以可以针对代码块做同步。

     * @date 2018年9月25日
     */
    
    public class SynchronizedDemo implements Runnable{
    
        static int x = 0;
        
        public void run() {
            
            //其他耗时操作。。。
            
            synchronized (SynchronizedDemo.class) {
                for (int i = 0; i < 10000; i++) {
                    x++;
                }
            }
            
            
            //其他耗时操作。。。
        }
        
        public static void main(String[] args) throws InterruptedException {
            SynchronizedDemo synchronizedDemo = new SynchronizedDemo();
            Thread t1 = new Thread(synchronizedDemo);
            Thread t2 = new Thread(synchronizedDemo);
            t1.start();
            t2.start();
            t1.join();
            t2.join();
            System.out.println(x);
        }
    
    }

    在这个demo中,锁对象是SynchronizedDemo.class,当然可以是任意的java对象。

    2.synchronized底层原理

    synchronized在JVM中的实现原理,是基于进入和退出Monitor对象来实现方法同步和代码块同步,但是两者的实现细节不同;代码块同步是基于monitorenter和monitorexit指令来实现的,而方法同步是使用另外一种方式。在详解介绍之前,先了解下java对象头。

    2.1 java对象头

    在JVM中,对象在内存中的布局分为三块区域:对象头,实例变量和填充数据。而synchronized使用的锁对象就是存放在java对象头中的,对于java对象头由Mark Word和Class MetaData Address组成,如果当前对象是数据,则还有Array Length。如下图:

    长度 内容 说明
    32/64bit Mark Word 存储对象的hashCode,锁信息,分代年龄或者GC标志信息
    32/64bit Class MetaData Address 存储到对象类型数据的指针,JVM通过这个指针能确定该对象是哪个类的实例
    32/64bit Array Length 如果当前对象是数组,则表示数组的长度

    对于32位的JVM,Mark Word默认的存储结构如下:

    锁状态 25bit 4bit 1bit是否是偏向锁 2bit锁标志位
    无锁状态 对象的hashCode 对象分代年龄 0 01

    在32位JVM下,除了上面的Mark Word默认的存储结构外,还有如下可变的的结构:

    锁状态 25bit 4bit 1bit 2bit
    23bit 2bit 是否是偏向锁 锁标志位
    轻量级锁 指向栈中锁记录的指针 00
    重量级锁 指向重量级锁的指针 10
    GC标记 11
    偏向锁 线程ID Epoch 对象粉黛年龄 1 01

    在这里,synchronized的对象锁,锁标志位10,指针指向的是monitor对象的起始地址,每一个对象都有一个monitor对象与之关联。当一个monitor对象被一个线程持有后,它就处于锁定状态。

    2.2 同步代码块底层原理

    对于1.3中的demo,经过javap反编译后得到如下结果:

    public class SynchronizedDemo implements java.lang.Runnable {
      static int x;
    
      public SynchronizedDemo();
        Code:
           0: aload_0
           1: invokespecial #1                  // Method java/lang/Object."<init>":
    ()V
           4: return
    
      public void run();
        Code:
           0: ldc_w         #2                  // class SynchronizedDemo
           3: dup
           4: astore_1
           5: monitorenter                      // 进入同步方法
           6: iconst_0
           7: istore_2
           8: iload_2
           9: sipush        10000
          12: if_icmpge     29
          15: getstatic     #3                  // Field x:I
          18: iconst_1
          19: iadd
          20: putstatic     #3                  // Field x:I
          23: iinc          2, 1
          26: goto          8
          29: aload_1
          30: monitorexit                       // 退出同步方法
          31: goto          39
          34: astore_3
          35: aload_1
          36: monitorexit                      // 退出同步方法 
          37: aload_3
          38: athrow
          39: return
        Exception table:
           from    to  target type
               6    31    34   any
              34    37    34   any
    
      public static void main(java.lang.String[]) throws java.lang.InterruptedExcept
    ion;
        Code:
           0: new           #2                  // class SynchronizedDemo
           3: dup
           4: invokespecial #4                  // Method "<init>":()V
           7: astore_1
           8: new           #5                  // class java/lang/Thread
          11: dup
          12: aload_1
          13: invokespecial #6                  // Method java/lang/Thread."<init>":
    (Ljava/lang/Runnable;)V
          16: astore_2
          17: new           #5                  // class java/lang/Thread
          20: dup
          21: aload_1
          22: invokespecial #6                  // Method java/lang/Thread."<init>":
    (Ljava/lang/Runnable;)V
          25: astore_3
          26: aload_2
          27: invokevirtual #7                  // Method java/lang/Thread.start:()V
    
          30: aload_3
          31: invokevirtual #7                  // Method java/lang/Thread.start:()V
    
          34: aload_2
          35: invokevirtual #8                  // Method java/lang/Thread.join:()V
          38: aload_3
          39: invokevirtual #8                  // Method java/lang/Thread.join:()V
          42: getstatic     #9                  // Field java/lang/System.out:Ljava/
    io/PrintStream;
          45: getstatic     #3                  // Field x:I
          48: invokevirtual #10                 // Method java/io/PrintStream.printl
    n:(I)V
          51: return
    
      static {};
        Code:
           0: iconst_0
           1: putstatic     #3                  // Field x:I
           4: return
    }

    monitorenter指令是在编译后插入到同步代码块的开始位置,monitorexit指令插入到同步代码块的结束位置和异常处,当线程执行到monitorenter指令时,将会尝试获取对象所对应的monitor的所有权,即尝试获取对象的锁。当monitorexit指令被执行时,执行线程会释放monitor锁。在上面的代码中可以看到,还有一个monitorexit指令,是在异常结束时执行的指令以释放monitor锁。对于同步方法的底层原理,细节实现上和这不同,这里暂时不做叙述。

    3 锁的优化

    在java SE1.6中,引入了偏向锁和轻量级锁,锁一共有四种状态,从低到高是:无锁状态,偏向锁状态,轻量级锁状态,重量级锁状态。这几种状态会随着竞争的提高,锁不断升级,但是不能降级。

    3.1 偏向锁

     经研究发现,大多数情况下,锁不仅不存在竞争,而且总是由同一个线程多次获得。为了让线程获得锁的代价更低,所以就引入了偏向锁。当一个线程访问代码块并获取锁时,会在对象头和栈帧的锁记录里存储锁偏向的线程ID,以后线程当再次进入同步代码块时,不需要再加锁和解锁,只需要测试下该对象头中是否存储着指向该线程的偏向锁即可。注意,当没有锁竞争时,偏向锁有很好的优化效果,但是一旦锁竞争激烈,偏向锁就会失效,升级为轻量级锁。

    3.1.1 偏向锁的撤销

    偏向锁使用了一种竞争出现才释放锁的机制,所以当其他线程竞争偏向锁时,持有该偏向锁的线程才会释放锁,我们称之为偏向锁的撤销。流程如下:如果A线程正在持有一个偏向锁,当B线程竞争该偏向锁时,会暂停A线程,然后检查A线程是否还活着,如果A线程不处于活动状态,则将对象头设置为无锁状态;如果A线程还处于活动状态,则将对象头的锁偏向于B线程或者恢复到无锁,最后,唤醒A线程。

    3.2 轻量级锁

    3.2.1 轻量级锁加锁

    线程尝试使用CAS将对象头中Mark Word替换为指向锁记录中的指针,如果成功,则获取锁成功。如果失败,则继续通过自旋CAS来获取锁。

    3.2.2 轻量级锁解锁

    轻量级锁升级到重量级锁,是在轻量级锁解锁的过程中发生的。线程在获取锁的时候拷贝了对象头中的Mark Word;在它释放锁的时候发现在它持有锁期间有其它线程尝试获取锁,并且该线程对Mark Word做了修改,两者发现不一致,则切换到重量级锁。

    参考资料:《java并发编程的艺术》

  • 相关阅读:
    Oracle EXP
    Using Spring in Web and WinForms
    System.ComponentModel(未完...)
    工作必须得到强势方的支持!
    book.Save()还是bookManager.Save(book)?
    C#中常用的Attribute搜集(刚开始...)
    领域模型是否能够屏蔽数据库?
    合格代码的最基本的标准
    关于配置系统的设计
    对象分类
  • 原文地址:https://www.cnblogs.com/51life/p/9687024.html
Copyright © 2020-2023  润新知