美文网首页
JUC(二)锁

JUC(二)锁

作者: NIIIICO | 来源:发表于2022-05-09 18:05 被阅读0次

一、引言

1、volatile能不能保证线程安全?

public class Test {
    public static int i = 1;

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

// 字节码
0 getstatic #2 <com/example/demo/Test.i : I>
3 iconst_1
4 iadd
5 putstatic #2 <com/example/demo/Test.i : I>

JUC(一)JMM内存模型可知,volatile可以保证可见性和有序性,那么使用volatile可以达到线程安全吗?上面代码是i++的字节码,假设有这样一种场景,由于volatile不能保证原子性,就会导致出现线程不安全:

序号 步骤 线程1工作内存 线程2工作内存 主内存 备注
1 线程1执行,执行getstatic #2;
线程2不执行
计算结果0
i = 1
计算结果0
i = 0
i = 1 -
2 线程1执行,执行iconst_1;
线程2不执行
计算结果0
i = 1
计算结果0
i = 0
i = 1 -
3 线程1执行,执行iadd;
线程2不执行
计算结果2
i = 1
计算结果0
i = 0
i = 1 iadd只进行计算,还未进行赋值
4 线程1不执行;
线程2执行,执行getstatic #2
计算结果2
i = 1
计算结果0
i = 1
i = 1 -
5 线程1不执行;
线程2执行,执行iconst_1
计算结果2
i = 1
计算结果0
i = 1
i = 1 -
6 线程1不执行;
线程2执行,执行iadd
计算结果2
i = 1
计算结果2
i = 1
i = 1 -
7 线程1不执行;
线程2执行,执行putstatic #2
计算结果2
i = 1(失效)
计算结果2
i = 2
i = 2 由于volatile的可见性,给i赋值后,
会同步其他线程i已经失效
8 线程1执行,重新读取i;
线程2不执行
计算结果2
i = 2
计算结果2
i = 2
i = 2 线程1重新从主内存中获取i的值
9 线程1执行,执行putstatic #2;
线程2不执行
计算结果2
i = 2
计算结果2
i = 2
i = 2 线程1将计算结果赋值给i,
导致执行了两次i++,结果还是2

2、竞态条件与临界区

  • 如上文的场景,当多个线程竞争同一资源时,如果对资源的访问顺序敏感,就称存在竞态条件。导致竞态条件发生的代码区称作临界区。
  • 在临界区中使用适当的同步就可以避免竞态条件;如:synchronized、Lock。

二、synchronized

1、非静态方法锁和静态方法锁的区别

public class Test {
    private int i = 1;
    private static int j = 1;

    // 方法锁
    public synchronized void add() {
        i++;
    }

    // 静态方法锁
    public synchronized static void addStatic() {
        j++;
    }
}

上述代码等价于

public class Test {
    private int i = 1;
    private static int j = 1;
    
    public void add() {
        synchronized (this) {
            i++;
        }
    }
    
    public static void addStatic() {
        synchronized (Test.class) {
            j++;
        }
    }
}

2、锁分类

JVM(三)堆与对象可知,对象头中的MarkWord会有位置标识锁状态:

markword
<1> 偏向锁
  • 当只有一个线程持有锁,并且该对象没有调用过系统的hashcode方法,则它可被设置成偏向锁
  • 对象被设置成偏向锁后,再调用系统的hashcode方法,则膨胀为轻量锁或重量锁
<2> 轻量锁
Lock Record 结构
  • 在线程的栈帧中有Lock Record存储锁对象的MarkWord
  • 竞争线程会通过自旋尝试获取锁
  • 自旋结束,膨胀为重量锁
<3> 重量锁
Monitor结构
  • 如果使用synchronized给对象加了重量级锁,该对象的MarkWord就会指向Monitor。
  • obj的MarkWord会存储到Monitor中
  • Owner指向当前持有锁的线程
  • 如果有新的线程想要获取锁,会进入EntryList列表进行等待
  • 调用锁对象wait方法的线程会进入WaitSet中,等待被notify唤醒,唤醒后进入EntryList
<4> 锁膨胀过程
锁膨胀过程

3、其他

<1> 为什么要有偏向锁、轻量锁?
  • 在多线程环境下,为了避免未抢到锁的线程空转,浪费资源。需要对这些线程进行阻塞,当占用锁的线程释放锁后,需要唤醒另一个竞争到锁的线程。
  • 在JVM中,这些操作都需要切换到内核态实现。
  • 重量锁涉及到用户态、内核态的切换,比较消耗资源。
  • 在单线程和只有两个线程竞争的情况,使用到偏向锁、轻量锁,来优化性能。
<2> 重入
  • 锁重入,就是支持正在持有锁的线程再次获取锁,不会出现自己锁死自己的情况。
  • 在竞争锁时,如果发现锁已经被持有了,并且持有人是自己,那么就做一个标记。
  • 轻量锁重入时会新增一个Lock Record,只是这个Lock Record不再存储锁的MarkWord。
  • 重量锁重入时,会将monitor的count+1。
  • 解锁时,重量锁会先将count-1;count == 0表示释放锁。
synchronized (this){
    synchronized (this){
        
    }
}
<3> 公平锁、非公平锁
  • 持有锁的线程释放锁后,EntryList中的线程开始竞争锁;
  • 多个线程按照申请锁的顺序去获得锁,就是公平锁;
  • 多个线程按照系统的调度获取锁,就是非公平锁。

相关文章

  • JUC源码循序渐进

    目录 必读篇 JUC源码分析—CAS和Unsafe JUC源码分析—AQS JUC锁篇 JUC源码分析-JUC锁(...

  • JUC(二)锁

    一、引言 1、volatile能不能保证线程安全? 由JUC(一)JMM内存模型[https://www.jian...

  • JUC源码分析-JUC锁(二):ReentrantReadWri

    1. 概述 在AQS一篇中我们对“独占锁”和“共享锁”做了简单说明。在J.U.C中,共享锁包括CountDownL...

  • JAVA基础—JUC包(java.util.concurrent

    1. JUC - CountDownLatch 倒计时锁 运行结果 2. JUC之Semaphore信号量 运行结...

  • 同步器AbstractQueuedSynchronizer浅析

    Java中的锁主要有:synchronized锁和JUC(java.util.concurrent)locks包中...

  • JUC

    1.说说JUC包你都知道多少? 2.说说AQS 3.JUC中的锁,ReentrantLock 4.说说Atomic...

  • synchronized和juc比较

    为什么有了synchronized之后还需要juc的锁呢? 1、synchronized是非公平锁,无法用作公平锁...

  • JUC中的锁(一)概述

    Java中的锁,可以分为Synchronized”同步锁”和”JUC包中的锁”。同步锁之前说过很多了,可以看之前的...

  • 实现分布式锁

    Java中的锁主要包括synchronized锁和JUC包中的锁,这些锁都是针对单个JVM实例上的锁,对于分布式环...

  • 2018-07-26 CountDownLatch

    【转】Java多线程系列--“JUC锁”09之 CountDownLatch原理和示例

网友评论

      本文标题:JUC(二)锁

      本文链接:https://www.haomeiwen.com/subject/akmkjrtx.html