【速看】一文清明讲明白线程同步关键字synchronized

在理解线程安全之前,我们先来看了解明确好以下几个概念:

基本概念

线程安全:指当多个线程并发访问某个java对象时,无论系统如何调度这些线程将如何交替操作,这个对象都能表现出一致的、正常的行为。

临界区资源: 一种可以被多个线程使用的公共资源或共享数据,但是每一次只能有一个线程使用它。一旦临界区资源被占用,想使用该资源的其他线程则必须等待。在并发的情况下,临界区资源是受保护的对象。

临界区代码段:每个线程中访问临界资源的那段代码,多个线程必须互斥地对临界区资源进行访问。线程在进入临界区代码段之前, 必须在进入区申请资源,申请成功之后执行临界区代码段,执行万传给你之后释放资源。

竟态条件:多个线程访问同一个资源,如果对资源的访问顺序敏感。如果多个线程在临界区内执行,但是由于代码的执行序列不同而导致结果无法预测,就称为发生了竞态条件。最常见的竞态条件是先检测后执行。 执行依赖于检测结果,而检测结果依赖于多个线程的执行时序,而多个线程的执行时序通常情况下是不固定不可判断的,从而导致执行结果出现各种问题。

synchronized 关键字介绍

synchronized 关键字是java的保留字,synchronized 关键字保障了原子性、可见性和有序性。

在java中,线程同步使用最多的方法是使用synchronized关键字,使用synchronized(synchObject)调用相当于获取synchObject的内置锁,对临界区代码段进行排他性保护。synchronized块是java提供的一种原子性内置锁,java中的每个对象都可以把它当做一个同步锁来使用,这些java内置的使用者看不到的锁被称为内部锁,也叫做监视器锁。

synchronized 关键字拥有重入锁(自己可以再次获取自己的内部锁)的功能,即在使用synchronized 时,当一个线程得到一个对象后,再次请求此对象时是可以再次得到该对象锁的。

synchronized 关键字包括monitor enter 和 monitor exit 两个JVM 指令,它能够保证在任何时候任何线程执行到monitor enter 成功之前都必须从主内存中获取数据,而不是从缓存中,在monitor exit运行成功之后,共享变量被更新后的值必须刷入主内存。

synchronized 的指令严格准守java happens-before规则,一个monitor exit 指令之前必定要有一个monitor enter。

synchronized同步方法

当使用synchronized 关键字修饰一个方法的时候,该方法被称为同步方法,synchronized关键字的位置处于同步方法的返回类型之前,对临界区代码段进行保护,例如:

   public class SynchronizedDemo {
    private  int accountBalance;
    /**
     * 同步方法
     */
    public  synchronized  void methodA(){
        accountBalance++;
    }
}

在方法声明中设置synchronized关键字,保证其方法的代码执行流程是排他性的。任何时间只允许一个线程进入同步方法,如果其他线程需要执行同一个方法,那么只能等待和排队。

synchronized 方法的同步实质上使用了this对象锁,使用synchronized(this)同步代码块将当前类的对象作为锁,代码如下:

   /**
     * 同步方法
     */
    public    void methodA(){
        //对方法内部全部代码进行保护
        synchronized(this){
            accountBalance++;
        }
    }


synchronized同步方法在字节码指令中的原理

在方法上使用synchronized关键字实现同步的原因是使用了flags标记ACC_SYNCHRONIZED,当调用方法时,调用指令将会检查方法的ACC_SYNCHRONIZED访问标志是否设置,如果设置了,执行先持有同步锁,然后再执行方法,最后在方法完成时释放锁,以下代码为例:

   public  synchronized     void methodA(){
        accountBalance++;
    }

编译完成生成.class文件后,在IDEA的terminal中使用javap命令进行查看, -v 表示输出附加信息,-c 表示对代码进行反汇编。如下:
javap 使用

生成.class 文件对应的字节码指令核心代码如下:


  public synchronized void methodA();
    descriptor: ()V
    flags: ACC_PUBLIC, ACC_SYNCHRONIZED
    Code:
      stack=3, locals=3, args_size=1
         0: aload_0
         1: getfield      #3                  // Field accountBalance:Ljava/lang/Integer;
         4: astore_1
         5: aload_0
         6: aload_0
         7: getfield      #3                  // Field accountBalance:Ljava/lang/Integer;
        10: invokevirtual #8                  // Method java/lang/Integer.intValue:()I
        13: iconst_1
        14: iadd
        15: invokestatic  #2                  // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
        18: dup_x1
        19: putfield      #3                  // Field accountBalance:Ljava/lang/Integer;
        22: astore_2
        23: aload_1
        24: pop
        25: return
      LineNumberTable:
        line 20: 0
        line 21: 25
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0      26     0  this   Lcom/th/thread/SynchronizedDemo;


在反编译的字节码指令中对public synchronized void methodA()方法使用了flag 标记ACC_SYNCHRONIZED,说明此方法是同步的。

synchronized 同步块

将synchronized加在方法上,如果其保护的临界区代码段包含的临界区资源多于一个,就会造成临界区资源的闲置等待,为了提升执行效率和吞吐量,可以将synchronized关键字放在函数体内,同步一个代码块。synchronized 同步块的写法如下:

 //同步块
synchronized (accountBalance){
    //临界区代码段的代码块
}

synchronized后面括号中是一个syncObject对象,意思是进入临界区代码段需要获取syncObject对象的监视锁,换句话说就是将syncObject 对象监视锁作为临界区代码段的同步锁。由于每一个Java对象都有一把监视锁,因此任何java对象都能作为synchronized的同步锁。当一个线程获得syncObject对象的监视锁后,其他线程就只能等待。

synchronized 方法和synchronized同步块的联系:
(1)synchronized方法是一种粗粒度并发控制,某一个时刻只能有一个线程执行该synchronized方法。
(2)synchronized代码块是一种细粒度的并发控制,处于synchronized块之外的其他代码是可以被多个现成并发访问的。
(3)synchronized代码块比synchronized方法更加细粒度地控制了多个线程的同步访问。
(4)在java的内部实现上, synchronized方法实际上等同于用一个synchronized代码块,这个代码块包含同步方法中的所有与,然后在synchronized代码块的括号中传入this关键字,使用this对象锁作为进入临界区的同步锁。

synchronized同步块在字节码指令中的原理

如果使用synchronized同步块,则使用monitorenter 和monitorexit 治理你进行同步处理,测试代码如下:

  public  int availableBalance;
     public  Integer availableLock = new Integer(1);

    /**
     * 同步方法
     */
    public    void methodB(){
       synchronized (this.availableLock){
           this.accountBalance++;
       }
       
    }


使用javap -c -v xx.class 命令生产.class文件对应的字节码指令核心代码如下:

    public void methodB();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=3, locals=6, args_size=1
         0: aload_0
         1: getfield      #6                  // Field availableLock:Ljava/lang/Integer;
         4: dup
         5: astore_1
         6: monitorenter
         7: aload_0
         8: astore_2
         9: aload_2
        10: getfield      #3                  // Field accountBalance:Ljava/lang/Integer;
        13: astore_3
        14: aload_2
        15: aload_2
        16: getfield      #3                  // Field accountBalance:Ljava/lang/Integer;
        19: invokevirtual #10                 // Method java/lang/Integer.intValue:()I
        22: iconst_1
        23: iadd
        24: invokestatic  #2                  // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
        27: dup_x1
        28: putfield      #3                  // Field accountBalance:Ljava/lang/Integer;
        31: astore        4
        33: aload_3
        34: pop
        35: aload_1
        36: monitorexit
        37: goto          47
        40: astore        5
        42: aload_1
        43: monitorexit
        44: aload         5
        46: athrow
        47: return
      Exception table:
         from    to  target type
             7    37    40   any
            40    44    40   any
      LineNumberTable:
        line 34: 0
        line 35: 7
        line 36: 35
        line 41: 47


从指令核心代码中可以看到使用了monitorenter 和monitorexit指令进行了处理。

每个对象都与一个monitor相关联,一个monitor 的lock的锁只能被一个线程在同一时间获得,在一个线程尝试获得与对象关联monitor的所有权会发生如下几件事:
(1) 如果monitor的计算为0,则意味着该monitor的lock 还没有被获得,某个线程获得之后将立即对该计算器加1,从此该线程就是这个monitor的所有者。
(2)如果一个已经拥有该monitor所有权的线程重入,则会导致monitor计算器再次累加。
(3)如果monitor已经被其他线程所有者,则其他线程尝试获取该monitor的所有权时,会被陷入阻塞状态直到monitor计数器变为0,才能再次尝试获取对monitor的所有权。

释放对monitor的所有权,想要释放对某个对象关联的monitor的所有权的前提是已获得所有权,释放monitor所有权的过程就是将monitor 的计数器减1,如果计数器的结果为0,那就意味着该线程不再拥有对该monitor的所有权,即解锁,于此同时被该monitor block的线程将再次尝试获得对该monitor的所有权。

synchronized修饰的静态同步方法

static 修饰的静态方法属于Class实例而不是当个Object实例,在静态方法内部是不可能访问Object实例的this引用的,所以修饰static 方法的synchronized关键字就没有办法获得Object实例的this对象的监视锁。

使用synchronized关键字修饰static方法时 ,synchronized的同步锁并不是普通Object 对象的监视锁,而是类所对应的Class对象的监视锁。

当synchronized关键字修饰static方法时,同步锁为Class对象的监视锁;当synchronized关键字修饰普通的成员方法(非静态方法)时,同步锁为Class对象的监视锁。由于类的对象实例可以有很多,但是每个类只有一个Class实例,因此使用类锁作为synchronized的同步锁时会造成同一个JVM内的所有线程只能互斥地进入临界区段。

一种场景是synchronized块(代码块或者方法)正确执行完毕,监视锁自动释放;另一种场景是程序出现异常,非正常退出synchronized块,监视锁也会自动释放。所以,使用synchronized块时不必担心监视锁的释放问题。

synchronized 关键字堆栈分析

在运行以上测试代码的时候使用JConsole(%java_home%bin)工具进行监控,JConsole.exe 文件路径如下图:
在这里插入图片描述

连接之前,需要先运行程序,连接时需要保证程序正在运行状态,连接界面如下:
在这里插入图片描述

会提示不安全连接,点击“不安全连接就可以了,进入JConsole控制台,将tab切换至【线程】,如下图:
在这里插入图片描述

随便选中一个线程,会发现只有一个线程状态是RUNNABLE,其他线程都进入了BLOCKE状态,运行状态:
在这里插入图片描述

阻塞状态:
在这里插入图片描述

也可以使用jstack pid 的方式进行查看,查看的结果只有一个线程状态是RUNNABLE,其他线程都进入了BLOCKE状态,如下图:
在这里插入图片描述

从结果可看出,Thread-6 持有 <0x0000000703206b80> 的锁并且处于RUNNABLE状态,其他的线程无法进入MethodA方法, 处于BLOCKED状态,并且等待获取 <0x0000000703206b80> 锁。

synchronized 的内存语义

进入synchronized块的内存语义是把在synchronized块内使用到的变量从线程的工作内存中清除,这样在synchronized块内使用到该变量时就不会从线程的工作内存中获取,而是直接从主内存中获取。退出synchronized块的内存语义是把在synchronized块内对共享变量的修改刷新到主内存。

其实这也是加锁和释放锁的语义,当获取锁后会清空锁块内本地内存中将会被用到的共享变量,在使用这些共享变量时从主内存进行加载,在释放锁时将本地内存中修改的共享变量刷新到主内存。

synchronized除可以解决共享变量内存可见性问题外,synchronized经常被用来实现原子性操作。但是synchronized关键字会引起线程上下文切换并带来线程调度开销。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

弯_弯

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值