(二)线程安全

线程安全

当多个线程访问同一个类(对象或者静态成员)时,这个类表现出的行为和单个线程访问它时保持一致(在单线程环境执行正确),就称这个类是线程安全的。

无状态的对象一定是线程安全的。

线程安全的诱因

一是存在共享数据(也称临界资源)。

二是存在多条线程共同操作共享数据。

竞态条件

当某个计算的正确性取决于多个线程的交替执行时序时,就会发生竞态条件。

最常见的竞态条件为:先检查后执行。

例如,小明先检查磁盘上存不存在文件A,检查之后,其实这个检查结果已经失效了,假设A存在,这时有另一个程序在磁盘上删除A,破坏了小明之前的检查结果,现在小明想删除A,却发现报了一个错误,文件不存在。这就是静态条件导致的问题。

原子操作

要避免竞态条件问题,就必须在某个线程修改该变量时,通过某种方式防止其他线程使用这个变量,从而确保其他线程只能在修改操作完成之前或之后读取和修改状态,而不是在修改状态的过程中获取一个未知的状态。

假定有两个操作A和B,在A看来,当另一个线程执行B时,要么将B全部执行完,要么完全不执行B,那么A和B对彼此来说是原子的。

锁机制

synchronized块(同步代码块)

同步代码块包括两部分:一个作为锁的对象引用,一个作为由这个锁保护的代码块。

每个Java对象都可以用做一个实现同步的锁,这些锁被称为内置锁(Intrinsic Lock)或监视器锁(Monitor Lock)。

线程在进入同步代码块之前会自动获得锁,无论是通过正常的控制路径退出,还是通过从代码块中抛出异常退出,在退出同步代码块时自动释放锁。

同步代码块的锁是互斥锁,同一时刻只能一个线程持有同一把锁。

public void safe(){
    synchronized (this){
        //do something
    }
}

synchronized也可以修饰方法从而使整个方法变成同步方法,如果是实例方法默认的锁是当前对象,静态方法的默认锁则是Class对象。

注意:实例方法加synchronized修饰线程安全的前提是多个线程操作同一个实例。

如果一个类的多个实例方法都加了synchronized修饰,那么这些方法都是互斥的,同一时间只允许一个线程操作同一个实例的其中的一个synchronized方法。

public synchronized void safe(){
    //do something
}

另外,同步代码块的锁是可重入的,所以一个线程试图获取一个已经被它持有的锁时,也会成功。

具体实现:

为每个锁关联一个获取计数值和一个所有者线程。当计数值为0时,这个锁就被认为是没有被任何线程持有。当线程请求一个未被持有的锁时,JVM将记下锁的持有者,并且将获取计数值置为1。如果同一个线程再次获取这个锁,计数值将递增,而当线程退出同步代码块时,计数器会相应地递减。当计数值为0时,这个锁将被释放。

如果锁是不可重入的,那么递归调用同步方法将会死锁。

挖掘synchronized的原理

ObjectMonitor

每个对象的对象头都持有一个monitor对象的引用,monitor就可以认为是synchronized的实现,monitor在Hotspot虚拟机中实现为ObjectMonitor(称为管程)。

现在从字节码层面看看synchronized的真身 ,先来看以下代码

package com.example.demo.juc;

/**
 * <p>同步代码块测试</p>
 *
 * @author lch
 * @version 1.0
 * @date 2022/10/23 17:34
 */
public class SyncTest {
    private int count=0;

    public synchronized void safe(){
        count++;
    }
    public void safe1(){
        synchronized (this){
            count++;
        }
    }

    public static void main(String[] args) {
        
    }
}

 下面为上面的代码编译后的使用javap命令生成的字节码分析文件。

Classfile /E:/javacodetemplate/demo/src/test/java/com/example/demo/juc/SyncTest.class
  Last modified 2022-10-25; size 554 bytes
  MD5 checksum 7ed2d545194fc32ac5791ae88e8ddc8e
  Compiled from "SyncTest.java"
public class com.example.demo.juc.SyncTest
  minor version: 0
  major version: 52
  flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
   #1 = Methodref          #4.#21         // java/lang/Object."<init>":()V
   #2 = Fieldref           #3.#22         // com/example/demo/juc/SyncTest.count:I
   #3 = Class              #23            // com/example/demo/juc/SyncTest
   #4 = Class              #24            // java/lang/Object
   #5 = Utf8               count
   #6 = Utf8               I
   #7 = Utf8               <init>
   #8 = Utf8               ()V
   #9 = Utf8               Code
  #10 = Utf8               LineNumberTable
  #11 = Utf8               safe
  #12 = Utf8               safe1
  #13 = Utf8               StackMapTable
  #14 = Class              #23            // com/example/demo/juc/SyncTest
  #15 = Class              #24            // java/lang/Object
  #16 = Class              #25            // java/lang/Throwable
  #17 = Utf8               main
  #18 = Utf8               ([Ljava/lang/String;)V
  #19 = Utf8               SourceFile
  #20 = Utf8               SyncTest.java
  #21 = NameAndType        #7:#8          // "<init>":()V
  #22 = NameAndType        #5:#6          // count:I
  #23 = Utf8               com/example/demo/juc/SyncTest
  #24 = Utf8               java/lang/Object
  #25 = Utf8               java/lang/Throwable
{
  public com.example.demo.juc.SyncTest();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=2, locals=1, args_size=1
         0: aload_0
         1: invokespecial #1                  // Method java/lang/Object."<init>":()V
         4: aload_0
         5: iconst_0
         6: putfield      #2                  // Field count:I
         9: return
      LineNumberTable:
        line 10: 0
        line 11: 4

  public synchronized void safe();
    descriptor: ()V
    flags: ACC_PUBLIC, ACC_SYNCHRONIZED       // $$1
    Code:
      stack=3, locals=1, args_size=1
         0: aload_0
         1: dup
         2: getfield      #2                  // Field count:I
         5: iconst_1
         6: iadd
         7: putfield      #2                  // Field count:I
        10: return
      LineNumberTable:
        line 14: 0
        line 15: 10

  public void safe1();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=3, locals=3, args_size=1
         0: aload_0
         1: dup
         2: astore_1
         3: monitorenter                      // $$2
         4: aload_0
         5: dup
         6: getfield      #2                  // Field count:I
         9: iconst_1
        10: iadd
        11: putfield      #2                  // Field count:I
        14: aload_1
        15: monitorexit
        16: goto          24
        19: astore_2
        20: aload_1
        21: monitorexit                       //$$3
        22: aload_2
        23: athrow
        24: return
      Exception table:
         from    to  target type
             4    16    19   any
            19    22    19   any
      LineNumberTable:
        line 17: 0
        line 18: 4
        line 19: 14
        line 20: 24
      StackMapTable: number_of_entries = 2
        frame_type = 255 /* full_frame */
          offset_delta = 19
          locals = [ class com/example/demo/juc/SyncTest, class java/lang/Object ]
          stack = [ class java/lang/Throwable ]
        frame_type = 250 /* chop */
          offset_delta = 4

  public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=0, locals=1, args_size=1
         0: return
      LineNumberTable:
        line 24: 0
}
SourceFile: "SyncTest.java"

可以看到safe方法的flags标志有ACC_SYNCHRONIZED这个标志($$1处),这表示此方法是一个同步方法。当方法调用时,调用指令将会检查方法的ACC_SYNCHRONIZED访问标志是否被设置,如果设置了,执行线程将先持有monitor(管程),然后再执行方法,最后再方法完成(无论是正常完成还是非正常完成)时释放monitor。在方法执行期间,执行线程持有了monitor,其他任何线程都无法再获得同一个monitor。如果一个同步方法执行期间抛出了异常,并且在方法内部无法处理此异常,那这个同步方法所持有的monitor将在异常抛到同步方法之外时自动释放。

safe1方法的字节码指令中含有monitorentermonitorexit两个指令($$2处),这两个指令就是synchronized的真身。

monitorenter指令指向同步代码块的开始位置,monitorexit指令则指明同步代码块的结束位置,当执行monitorenter指令时,当前线程将试图获取对象锁所对应的monitor的持有权,当 对象锁的monitor的进入计数器为0,那线程可以成功取得monitor,并将计数器值设置为1,获得锁的持有权。如果当前线程已经拥有对象锁的monitor的持有权,那它可以重入这个 monitor,并且重入时计数器的值加1。假设monitor已被其他线程拥有,那当前线程将被阻塞,直到正在执行线程的monitorexit指令被执行,执行线程将释放monitor并设置计数器值为0 ,其他线程将有机会持有monitor。

编译器会确保无论方法通无论怎样结束,方法中调用过的每条monitorenter指令都有执行其对应monitorexit指令。编译器会自动产生一个异常处理器($$3处),这个异常处理器声明可处理所有的异常,保证在方法异常完成时monitorexit指令可以正确执行。

JVM对锁的优化

在JDK早期版本,synchronized属于重量级锁,因为monitor依赖于操作系统底层的互斥锁Mutex Lock,操作系统实现线程间的切换需要从用户态切换为内核态,耗费时间较长,所以synchronized早期效率低下。

JDK6之后引入了轻量级锁和偏向锁。

锁有四种状态:无锁、偏向锁、轻量级锁、重量级锁。锁可以从偏向锁升级到轻量级锁,再升级到重量级锁,但是不能降级。

偏向锁 

它是一种针对加锁操作的优化手段,一般来说在大多数情况下,锁不仅不存在多线程竞争,而且总是由同一线程多次获得,因此为了减少同一线程获取锁(会涉及到一些CAS操作,耗时)的代价而引入偏向锁。偏向锁的核心思想是,如果一个线程获得了锁,那么锁就进入偏向模式,当这个线程再次请求锁时,无需再做任何同步操作,直接获取,这样就省去了大量有关锁申请的操作,从而也就提供程序的性能。

对于没有锁竞争的场合,偏向锁有很好的优化效果。但是对于锁竞争比较激烈的场合,偏向锁就失效了,因为这样的场景极有可能每次申请锁的线程都是不相同的,因此这种场合下不应该使用偏向锁。偏向锁失败后,先升级为轻量级锁。

可以这样理解:要进入一个公司,首先要在大门申请临时卡,标识已经登记过一次,短时间内我再来,就不需要在申请临时卡了,直接进入。

轻量级锁

倘若偏向锁失败,会尝试使用一种称为轻量级锁的优化手段。轻量级锁能够提升程序性能的依据是“对绝大部分的锁,在整个同步周期内都不存在竞争”,注意这是经验数据。需要了解的是,轻量级锁所适应的场景是线程交替执行同步块的场合,如果存在同一时间访问同一锁的场合,就会导致轻量级锁膨胀为重量级锁。

例如:2个人在一张纸上画画,A画完一笔,B再画一笔,两个人交叉作画,不会冲突。

自旋锁 

轻量级锁失败后,虚拟机为了避免线程真实地在操作系统层面挂起,还会进行一项称为自旋锁的优化手段。这是基于在大多数情况下,线程持有锁的时间都不会太长,如果直接挂起操作系统层面的线程可能会得不偿失,毕竟操作系统实现线程之间的切换时需要从用户态转换到核心态,这个状态之间的转换需要相对比较长的时间,时间成本相对较高,因此自旋锁会假设在不久将来,当前的线程可以获得锁,因此虚拟机会让当前想要获取锁的线程做几个空循环(这也是称为自旋的原因),一般不会太久,可能是50个循环或100循环,在经过若干次循环后,如果得到锁,就顺利进入临界区。如果还不能获得锁,那就会将线程在操作系统层面挂起,这就是自旋锁的优化方式。如果还无法获得锁就只能升级为重量级锁了。

比如现在想上厕所,但是厕所有人,先在厕所外原地踏步,别闲着,等几分钟之后如果厕所里的人还没出来,就去睡觉(挂起),如果直接去睡觉,可能厕所里的人很快就出来了,这时候再去唤醒你,就浪费了大量的时间。

锁消除

消除锁是虚拟机另外一种锁的优化,JVM在JIT编译时(即时编译),通过对运行上下文的扫描,去除不可能存在共享资源竞争的锁,消除没有必要的锁,可以节省请求锁的时间。

常见的例子就算在方法中new了一个线程安全的对象来使用,这时JVM会优化掉线程安全的对象方法,也就是说就算在不需要同步的代码里加了锁,也能优化掉。

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值