2.并发编程-同步锁

同步锁

原子性问题

在下面的案例中,演示了两个线程分别去去调用 demo.incr 方法来对 i 这个变量进行叠加,预期结果应该是20000,但是实际结果却是小于等于20000的值。

public class Demo {
int i = 0;
public void incr(){
 // 该操作是非原子的
	i++;
}
public static void main(String[] args) {
  Demo demo = new Demo();
  Thread[] threads=new Thread[2];
  for (int j = 0;j<2;j++) {
      threads[j]=new Thread(() -> { // 创建两个线程
          for (int k=0;k<10000;k++) { // 每个线程跑10000次
          	demo.incr();
          }
      });
      threads[j].start();
  }
  try {
      threads[0].join();
      threads[1].join();
  } catch (InterruptedException e) {
      e.printStackTrace();
  }
  System.out.println(demo.i);
}
}

问题的原因

上述代码的现状就是典型的线程安全问题中的原子性问题的体现,那么什么是原子性呢?

在上面这段代码中,i++是属于Java高级语言中的编程指令,而这些指令最终可能会有多条CPU指令来组成,而i++最终会生成3条指令, 通过javap -v xxx.class 查看字节码指令如下

public incr()V
L0
LINENUMBER 13 L0
ALOAD 0
DUP
GETFIELD com/gupaoedu/pb/Demo.i : I // 访问变量i
ICONST_1                            // 将整形常量1放入操作数栈
IADD                                // 把操作数栈中的常量1出栈并相加,将相加的结果放入操作数栈
PUTFIELD com/gupaoedu/pb/Demo.i : I // 访问类字段(类变量),复制给Demo.i这个变量	

这三个操作,如果要满足原子性,那么就要保证某个线程在执行这个指令时,不允许其他线程干扰。

问题的本质

一个CPU核心在同一时刻只能执行一个线程,如果线程数量远远大于CPU核心数,就会发生线程的切换,这个切换动作可以发生在任何一条CPU指令执行完之前

对于i++这三个CPU指令来说,如果线程A在执行指令1之后,做了线程切换,假设切换到B线程,B同样执行CPU指令,执行顺序如下图,就会导致结果测错误

在这里插入图片描述

这就是多线程环境下,存在的原子性问题,那么怎么解决呢?

大家认真观察上面这个图,表面上是多个线程对于同一个变量的操作,实际上是count++这行代码,它不是原子的。所以才导致在多线程环境下出现这样一个问题。

也就是说,我们只需要保证,count++这个指令在运行期间,在同一时刻只能由一个线程来访问,就可以解决问题。这就需要引出 同步锁Synchronized

Synchronized的基本应用

synchronized有三种方法来加锁,不同的修饰类型,代表锁的控制粒度

  1. 修饰实例方法,作用于当前示例加锁,进入同步代码前要获得当前实例的锁
  2. 静态方法,作用于当前类对象加锁,进入同步代码前要获取当前类对象的锁
  3. 修饰代码块,指定加锁对象,对给定对象加锁,进入同步代码块钱要获得给定对象的锁

锁的基本实现模型理解

synchronized到底帮我们做了什么,为什么能够解决原子性呢

在没有加锁之前,多个线程去调用incr()方法时,没有任何限制,都可以直接拿到这个i的值进行++操作,但是当加了synchronized锁之后,线程A和B就由并行变成了串行

在这里插入图片描述

MarkWord对象头

简单的理解,就是一个对象在JVM内存中的布局或存储的形式

在Hotspot虚拟机中,对象在内存中的存储布局,可以分为三个区域:对象头(Header)、实例数据(Instance Data)、对齐填充(Padding)

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-SonKTjmb-1624415295917)(.\对象布局.jpg)]

**mark-word:**对象标记占4个字节,用于存储一些列的标记位,比如:哈希值、轻量级锁的标记位、偏向锁的标记位、分代年龄等

**Class Pointer :**Class 对象的类型指针,jdk1.8默认开启指针压缩后为4字节,关闭指针压缩后长度为8字节。其指向的位置是对象对应的Class对象的内存地址

**对象实际数据:**包括对象的所有成员变量,大小由各个成员变量决定,比如:byte占1个字节8比 特位、int占4个字节32比特位。

**对齐:**最后这段空间补全并非必须,仅仅为了起到占位符的作用。由于HotSpot虚拟机的内存管理 系统要求对象起始地址必须是8字节的整数倍,所以对象头正好是8字节的倍数。因此当对象实例 数据部分没有对齐的话,就需要通过对齐填充来补全

通过ClassLayout打印对象头

为了可以更加直观的看到对象的存储和实现,我们可以使用JOL查看对象的内存布局。

  • 添加依赖

    • <dependency>
          <groupId>org.openjdk.jol</groupId>
          <artifactId>jol-core</artifactId>
          <version>0.9</version>
      </dependency>
      
  • 编写测试代码

    • public class Demo {
          Object o=new Object();
          public static void main(String[] args) {
              Demo demo=new Demo(); //o这个对象,在内存中是如何存储和布局的。
              System.out.println(ClassLayout.parseInstance(demo).toPrintable());
          }
      }
      
  • 自行查看控制台输出

Synchronized锁的升级

jdk1.6对锁的实现引入了大量的优化,比如自旋锁、适应性自旋锁、锁消除、锁粗化、偏向锁、轻量级锁等技术来减少锁操作的开销

锁主要存在四种状态,依次是:无锁状态 -> 偏向锁状态 -> 轻量级锁状态 -> 重量级锁状态,他们会随着竞争的激烈逐步升级

这么设计的目的,其实是为了减少重量级锁带来的性能开销,尽可能的在无锁状态下解决线程并发问题,其中偏向锁和轻量级锁的底层实现是基于自旋锁,它相对于重量级锁来说,算是一种无锁的实现

在这里插入图片描述

  • 默认情况下偏向锁是开启状态,偏向锁的线程ID是0,偏向一个Anonymous BiasedLock
  • 如果有线程去抢占锁,那么这个时候线程会去抢占偏向锁,也就是把MarkWord的线程ID改为当前抢占锁的线程ID
  • 如果有线程竞争,这个时候会撤销偏向锁,升级到轻量级锁,线程会在自己的线程栈帧中创建一个LockRecord,用CAS操作把MarkWord设置为指向自己这个线程的LR的指针,设置成功表示抢占到锁
  • 如果竞争加剧,比如有线程超过10次自旋(-XX:PreBlockSpin参数配置),或者自旋锁线程数超过CPU核心数的一半,在JDK1.6之后,加入了自适应自旋Adapative Self Spinning. JVM会根据上次竞争 的情况来自动控制自旋的时间
  • 升级到重量级锁,向操作系统申请资源, Linux Mutex,然后线程被挂起进入到等待队列

偏向锁的获取及原理

默认情况下,偏向锁的开启是有延迟的,默认4秒,为什么这么设计呢

因为JVM虚拟机有一些自己默认启动的线程,这些线程里面有很多synchronized代码,这些synchronized在启动的时候就会触发竞争,如果使用偏向锁,就会造成偏向锁不断的进行锁的升级和撤销,效率较低

可以通过配置将延迟设置为0

-XX:BiasedLockingStartupDelay=0

再次运行下面的代码。

public class Demo01 {
    Object o=new Object();
    public static void main(String[] args) {
        //demo这个对象,在内存中是如何存储和布局的。
        Demo demo=new Demo();
        // 加锁之前
        System.out.println(ClassLayout.parseInstance(demo).toPrintable());
        synchronized (demo){
            // 加锁之后
            System.out.println(ClassLayout.parseInstance(demo).toPrintable());
        }
    }
}

可以得到如下的对象布局,可以看到对象头中的高位第一个字节的最后三位数为【101】,表示当前为偏向锁状态

这里的第一个对象和第二个对象的锁状态都是101,是因为偏向锁打开状态下,默认会有配置匿 名的对象获得偏向锁。

org.example.thread.Demo object internals:
OFFSET  SIZE   TYPE DESCRIPTION                               VALUE
   0     4        (object header)                           05 00 00 00 (00000101 00000000 00000000 00000000) (5)
   4     4        (object header)                           00 00 00 00 (00000000 00000000 00000000 00000000) (0)
   8     4        (object header)                           44 c1 00 f8 (01000100 11000001 00000000 11111000) (-134168252)
  12     4        (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

org.example.thread.Demo object internals:
OFFSET  SIZE   TYPE DESCRIPTION                               VALUE
   0     4        (object header)                           05 38 f7 02 (00000101 00111000 11110111 00000010) (49756165)
   4     4        (object header)                           00 00 00 00 (00000000 00000000 00000000 00000000) (0)
   8     4        (object header)                           44 c1 00 f8 (01000100 11000001 00000000 11111000) (-134168252)
  12     4        (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total


Process finished with exit code 0

此时你可能发现,为什么在没有加锁的时候,就已经是【101】

是因为JVM启动以后,就已经有很多的匿名线程,有引用这个对象

轻量级锁的获取以及原理

接下来,我们通过下面的例子来演示一下,通过加锁之后继续打印对象布局信息,来关注对象头里面的 变化。

public class Demo01 {
 Object o=new Object();
 public static void main(String[] args) {
     //demo这个对象,在内存中是如何存储和布局的。
     Demo demo=new Demo();
     System.out.println(ClassLayout.parseInstance(demo).toPrintable());
     synchronized (demo){
         System.out.println(ClassLayout.parseInstance(demo).toPrintable());
     }
     new Thread(()->{
         synchronized (demo){
             System.out.println(ClassLayout.parseInstance(demo).toPrintable());
         }
     }).start();
 }

}

得到的对象布局信息如下

org.example.thread.Demo object internals:
OFFSET  SIZE   TYPE DESCRIPTION                               VALUE
   0     4        (object header)                           05 00 00 00 (00000101 00000000 00000000 00000000) (5)
   4     4        (object header)                           00 00 00 00 (00000000 00000000 00000000 00000000) (0)
   8     4        (object header)                           44 c1 00 f8 (01000100 11000001 00000000 11111000) (-134168252)
  12     4        (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

org.example.thread.Demo object internals:
OFFSET  SIZE   TYPE DESCRIPTION                               VALUE
   0     4        (object header)                           05 38 10 03 (00000101 00111000 00010000 00000011) (51394565)
   4     4        (object header)                           00 00 00 00 (00000000 00000000 00000000 00000000) (0)
   8     4        (object header)                           44 c1 00 f8 (01000100 11000001 00000000 11111000) (-134168252)
  12     4        (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

org.example.thread.Demo object internals:
OFFSET  SIZE   TYPE DESCRIPTION                               VALUE
   0     4        (object header)                           10 f3 82 1f (00010000 11110011 10000010 00011111) (528675600)
   4     4        (object header)                           00 00 00 00 (00000000 00000000 00000000 00000000) (0)
   8     4        (object header)                           44 c1 00 f8 (01000100 11000001 00000000 11111000) (-134168252)
  12     4        (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total


Process finished with exit code 0

从这里可以看到,从【101】升级到了【000】

重量级锁的获取以及原理

在竞争比较激烈的情况下,线程一直无法获得锁的时候,就会升级到重量级锁

public class Demo01 {

 Object o=new Object();
 public static void main(String[] args) {
     //demo这个对象,在内存中是如何存储和布局的。
     Demo demo=new Demo();
     System.out.println(ClassLayout.parseInstance(demo).toPrintable());
     synchronized (demo){
         System.out.println(ClassLayout.parseInstance(demo).toPrintable());
     }
     new Thread(()->{
         synchronized (demo){
             System.out.println(ClassLayout.parseInstance(demo).toPrintable());
         }
     }).start();
     new Thread(()->{
         synchronized (demo){
             System.out.println(ClassLayout.parseInstance(demo).toPrintable());
         }
     }).start();
 }

}

从结果可以看出,在竞争的情况下最终锁标记为【010】,其中锁标记【10】表示重量级锁

org.example.thread.Demo object internals:
OFFSET  SIZE   TYPE DESCRIPTION                               VALUE
   0     4        (object header)                           05 00 00 00 (00000101 00000000 00000000 00000000) (5)
   4     4        (object header)                           00 00 00 00 (00000000 00000000 00000000 00000000) (0)
   8     4        (object header)                           44 c1 00 f8 (01000100 11000001 00000000 11111000) (-134168252)
  12     4        (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

org.example.thread.Demo object internals:
OFFSET  SIZE   TYPE DESCRIPTION                               VALUE
   0     4        (object header)                           05 38 8e 03 (00000101 00111000 10001110 00000011) (59652101)
   4     4        (object header)                           00 00 00 00 (00000000 00000000 00000000 00000000) (0)
   8     4        (object header)                           44 c1 00 f8 (01000100 11000001 00000000 11111000) (-134168252)
  12     4        (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

org.example.thread.Demo object internals:
OFFSET  SIZE   TYPE DESCRIPTION                               VALUE
   0     4        (object header)                           00 f0 1d 20 (00000000 11110000 00011101 00100000) (538832896)
   4     4        (object header)                           00 00 00 00 (00000000 00000000 00000000 00000000) (0)
   8     4        (object header)                           44 c1 00 f8 (01000100 11000001 00000000 11111000) (-134168252)
  12     4        (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

org.example.thread.Demo object internals:
OFFSET  SIZE   TYPE DESCRIPTION                               VALUE
   0     4        (object header)                           da 5b d4 1c (11011010 01011011 11010100 00011100) (483679194)
   4     4        (object header)                           00 00 00 00 (00000000 00000000 00000000 00000000) (0)
   8     4        (object header)                           44 c1 00 f8 (01000100 11000001 00000000 11111000) (-134168252)
  12     4        (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total


Process finished with exit code 0

锁标记位对照表

在这里插入图片描述

CAS机制

CAS这个在Synchronized底层用得非常多,它的全称有两种

  • Compare and swap
  • Compare and exchange

就是比较并交换的意思。它可以保证在多线程环境下对一个变量修改的原子性。

CAS的原理很简单,包含三个值,当前内存值(V),预期原来的值(E)以及期待更新的值(N)

在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值