Chiriri's blog Chiriri's blog
首页
  • Java

    • JavaSE
    • JavaEE
    • 设计模式
  • Python

    • Python
    • Python模块
    • 机器学习
  • Golang

    • Golang
    • gRPC
  • 服务器

    • Linux
    • MySQL
    • NoSQL
    • Kubernetes
  • 项目

    • 传智健康
    • 畅购商城
  • Hadoop生态

    • Hadoop
    • Zookeeper
    • Hive
    • Flume
    • Kafka
    • Azkaban
    • Hbase
    • Scala
    • Spark
    • Flink
  • 大数据项目

    • 离线数仓
  • 青训营

    • 第四届青训营
  • HTML

    • HTML
    • JavaScript
  • Vue

    • Vue2
    • TypeScript
    • Vue3
    • Uni-APP
  • 数据结构与算法
  • C语言
  • 考研数据结构
  • 计算机组成原理
  • 计算机操作系统
  • Java基础

    • Java基础
    • Java集合
    • JUC
    • JVM
  • 框架

    • Spring
    • Dubbo
    • Spring Cloud
  • 数据库

    • MySQL
    • Redis
    • Elasticesearch
  • 消息队列

    • RabbitMQ
    • RocketMQ
  • 408

    • 计算机网络
    • 操作系统
    • 算法
  • 分类
  • 标签
  • 归档
  • 导航站
GitHub (opens new window)

Iekr

苦逼后端开发
首页
  • Java

    • JavaSE
    • JavaEE
    • 设计模式
  • Python

    • Python
    • Python模块
    • 机器学习
  • Golang

    • Golang
    • gRPC
  • 服务器

    • Linux
    • MySQL
    • NoSQL
    • Kubernetes
  • 项目

    • 传智健康
    • 畅购商城
  • Hadoop生态

    • Hadoop
    • Zookeeper
    • Hive
    • Flume
    • Kafka
    • Azkaban
    • Hbase
    • Scala
    • Spark
    • Flink
  • 大数据项目

    • 离线数仓
  • 青训营

    • 第四届青训营
  • HTML

    • HTML
    • JavaScript
  • Vue

    • Vue2
    • TypeScript
    • Vue3
    • Uni-APP
  • 数据结构与算法
  • C语言
  • 考研数据结构
  • 计算机组成原理
  • 计算机操作系统
  • Java基础

    • Java基础
    • Java集合
    • JUC
    • JVM
  • 框架

    • Spring
    • Dubbo
    • Spring Cloud
  • 数据库

    • MySQL
    • Redis
    • Elasticesearch
  • 消息队列

    • RabbitMQ
    • RocketMQ
  • 408

    • 计算机网络
    • 操作系统
    • 算法
  • 分类
  • 标签
  • 归档
  • 导航站
GitHub (opens new window)
  • JavaSE

  • JavaEE

  • Linux

  • MySQL

  • NoSQL

  • Python

  • Python模块

  • 机器学习

  • 设计模式

  • 传智健康

  • 畅购商城

  • 博客项目

  • JVM

  • JUC

    • 多线程
    • CompletableFuture
    • 锁
    • LockSupport与线程中断
    • Java内存模型之JMM
    • Volatile与Java内存模型
    • CAS
    • 原子操作类
    • ThreadLocal
    • Java对象内存布局和对象头
    • Synchronized与锁升级
      • 管程
        • 共享带来的问题
        • 临界区 Critical Section
        • 竞态条件 Race Condition
      • Synchronized
        • 方法上的 synchronized
        • 线程八锁
        • 变量的线程安全分析
        • 局部变量线程安全分析
        • 常见线程安全类
        • 线程安全类方法的组合
        • 不可变类线程安全性
        • 实例分析
      • Synchronized 锁优化的背景
      • Synchronized的性能变化
        • java5以前,只有Synchronized,这个是操作系统级别的重量级操作
        • 为什么每一个对象都可以成为一个锁????
        • Monitor(监视器锁)
        • java6开始,优化Synchronized
      • Synchronized锁种类及升级步骤
        • 字节码
        • 升级流程
        • 无锁
        • 偏锁
        • 主要作用
        • 偏向锁的持有
        • 偏向锁JVM命令
        • 演示
        • 偏向锁的撤销
        • 撤销 - 其它线程使用对象
        • 撤销 - 调用 wait/notify
        • 批量重偏向
        • 批量撤销
        • 总体流程图
        • 轻锁
        • 轻量级锁的获取
        • Code演示
        • 步骤流程图示
        • 自旋达到一定次数和程度
        • Java6之前
        • Java6之后
        • 轻量锁与偏向锁的区别和不同
        • 重锁
        • 自旋优化
        • 总结
        • JIT编译器对锁的优化
        • 锁消除
        • 锁粗化
    • AbstractQueuedSynchronizer之AQS
    • ReentrantLock、ReentrantReadWriteLock、StampedLock讲解
    • JUC总结
    • 并发集合
  • Golang

  • Kubernetes

  • 硅谷课堂

  • C

  • 源码

  • 神领物流

  • RocketMQ

  • 短链平台

  • 后端
  • JUC
Iekr
2023-12-04
目录

Synchronized与锁升级

# Synchronized 与锁升级

# 管程

Monitor (监视器),也就是我们平时所说的锁。Monitor 其实是一种同步机制,他的义务是保证(同一时间)只有一个线程可以访问被保护的数据和代码。

每个 Java 对象都可以关联一个 Monitor 对象,如果使用 synchronized 给对象上锁(重量级)之后,该对象头的 Mark Word 中就被设置指向 Monitor 对象的指针

Monitor 结构如下

image-20231226083221892

  • 刚开始 Monitor 中 Owner 为 null
  • 当 Thread-2 执行 synchronized (obj) 就会将 Monitor 的所有者 Owner 置为 Thread-2,Monitor 中只能有一个 Owner
  • 在 Thread-2 上锁的过程中,如果 Thread-3,Thread-4,Thread-5 也来执行 synchronized (obj),就会进入 EntryList BLOCKED
  • Thread-2 执行完同步代码块的内容,然后唤醒 EntryList 中等待的线程来竞争锁,竞争的时是非公平的
  • 图中 WaitSet 中的 Thread-0,Thread-1 是之前获得过锁,但条件不满足进入 WAITING 状态的线程,后面讲 wait-notify 时会分析

注意:

  • synchronized 必须是进入同一个对象的 monitor 才有上述的效果
  • 不加 synchronized 的对象不会关联监视器,不遵从以上规则

JVM 中同步是基于进入和退出监视器对象 (Monitor, 管程对象) 来实现的,每个对象实例都会有一个 Monitor 对象,

Object o = new Object();

new Thread(() -> {
    synchronized (o)
    {

    }
},"t1").start();
1
2
3
4
5
6
7
8

Monitor 对象会和 Java 对象一同创建并销毁,它底层是由 C++ 语言来实现的。

image-20231203190445193

# 共享带来的问题

两个线程对初始值为 0 的静态变量一个做自增,一个做自减,各做 5000 次,结果是 0 吗?

static int counter = 0;
public static void main(String[] args) throws InterruptedException {
    Thread t1 = new Thread(() -> {
        for (int i = 0; i < 5000; i++) {
            counter++;
        }
    }, "t1");
    Thread t2 = new Thread(() -> {
        for (int i = 0; i < 5000; i++) {
            counter--;
        }
    }, "t2");
    t1.start();
    t2.start();
    t1.join();
    t2.join();
    log.debug("{}",counter);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

以上的结果可能是正数、负数、零。为什么呢?因为 Java 中对静态变量的自增,自减并不是原子操作,要彻底理解,必须从字节码来进行分析

例如对于 i++ 而言(i 为静态变量),实际会产生如下的 JVM 字节码指令:

getstatic i // 获取静态变量i的值
iconst_1 // 准备常量1
iadd // 自增
putstatic i // 将修改后的值存入静态变量i
1
2
3
4

而对应 i-- 也是类似:

getstatic i // 获取静态变量i的值
iconst_1 // 准备常量1
isub // 自减
putstatic i // 将修改后的值存入静态变量i
1
2
3
4

而 Java 的内存模型如下,完成静态变量的自增,自减需要在主存和工作内存中进行数据交换:

image-20231226072422088

如果是单线程以上 8 行代码是顺序执行(不会交错)没有问题:

image-20231226072448964

但多线程下这 8 行代码可能交错运行会出现两种情况

出现负数的情况:

image-20231226072518497

出现正数的情况:

image-20231226072552083

# 临界区 Critical Section

  • 一个程序运行多个线程本身是没有问题的

  • 问题出在多个线程访问共享资源

    • 多个线程读共享资源其实也没有问题

    • 在多个线程对共享资源读写操作时发生指令交错,就会出现问题

  • 一段代码块内如果存在对共享资源的多线程读写操作,称这段代码块为临界区

static int counter = 0;
static void increment()
    // 临界区
{
    counter++;
}
static void decrement()
    // 临界区
{
    counter--;
}
1
2
3
4
5
6
7
8
9
10
11

# 竞态条件 Race Condition

多个线程在临界区内执行,由于代码的执行序列不同而导致结果无法预测,称之为发生了竞态条件

# Synchronized

为了避免临界区的竞态条件发生,有多种手段可以达到目的。

  • 阻塞式的解决方案:synchronized,Lock
  • 非阻塞式的解决方案:原子变量

我们使用阻塞式的解决方案:synchronized,来解决上述问题,即俗称的【对象锁】,它采用互斥的方式让同一 时刻至多只有一个线程能持有【对象锁】,其它线程再想获取这个【对象锁】时就会阻塞住。这样就能保证拥有锁 的线程可以安全的执行临界区内的代码,不用担心线程上下文切换

虽然 java 中互斥和同步都可以采用 synchronized 关键字来完成,但它们还是有区别的:

  • 互斥是保证临界区的竞态条件发生,同一时刻只能有一个线程执行临界区代码
  • 同步是由于线程执行的先后、顺序不同、需要一个线程等待其它线程运行到某个点

语法

synchronized(对象) // 线程1, 线程2(blocked)
{
    临界区
}
1
2
3
4

解决上述两个线程 5000 做自增和自减案例

static int counter = 0;
static final Object room = new Object();
public static void main(String[] args) throws InterruptedException {
    Thread t1 = new Thread(() -> {
        for (int i = 0; i < 5000; i++) {
            synchronized (room) {
                counter++;
            }
        }
    }, "t1");
    Thread t2 = new Thread(() -> {
        for (int i = 0; i < 5000; i++) {
            synchronized (room) {
                counter--;
            }
        }
    }, "t2");
    t1.start();
    t2.start();
    t1.join();
    t2.join();
    log.debug("{}",counter);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

image-20231226072832108

你可以做这样的类比:

  • synchronized(对象) 中的对象,可以想象为一个房间(room),有唯一入口(门)房间只能一次进入一人进行计算,线程 t1,t2 想象成两个人
  • 当线程 t1 执行到 synchronized(room) 时就好比 t1 进入了这个房间,并锁住了门拿走了钥匙,在门内执行 count++ 代码
  • 这时候如果 t2 也运行到了 synchronized(room) 时,它发现门被锁住了,只能在门外等待,发生了上下文切换,阻塞住了
  • 这中间即使 t1 的 cpu 时间片不幸用完,被踢出了门外(不要错误理解为锁住了对象就能一直执行下去哦),这时门还是锁住的,t1 仍拿着钥匙,t2 线程还在阻塞状态进不来,只有下次轮到 t1 自己再次获得时间片时才能开门进入
  • 当 t1 执行完 synchronized{} 块内的代码,这时候才会从 obj 房间出来并解开门上的锁,唤醒 t2 线程把钥匙给他。t2 线程这时才可以进入 obj 房间,锁住了门拿上钥匙,执行它的 count-- 代码

流程图

image-20231226073024697

synchronized 实际是用对象锁保证了临界区内代码的原子性,临界区内的代码对外是不可分割的,不会被线程切 换所打断。

我们可以进一步改进,将 Room 封装成一个类,把需要保护的共享变量放入一个类

class Room {
    int value = 0;
    public void increment() {
        synchronized (this) {
            value++;
        }
    }
    public void decrement() {
        synchronized (this) {
            value--;
        }
    }
    public int get() {
        synchronized (this) {
            return value;
        }
    }
}

@Slf4j
public class Test1 {

    public static void main(String[] args) throws InterruptedException {
        Room room = new Room();
        Thread t1 = new Thread(() -> {
            for (int j = 0; j < 5000; j++) {
                room.increment();
            }
        }, "t1");
        Thread t2 = new Thread(() -> {
            for (int j = 0; j < 5000; j++) {
                room.decrement();
            }
        }, "t2");
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        log.debug("count: {}" , room.get());
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41

# 方法上的 synchronized

对象锁,即锁的对象是本身 this

同步方法:就是把 synchronized 关键字加到方法上

class Test{
    public synchronized void test() {

    }
}
// 等价于
class Test{
    public void test() {
        synchronized(this) {

        }
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13

类锁,即锁的对象是类

同步静态方法:就是把 synchronized 关键字加到静态方法上

class Test{
    public synchronized static void test() {
    }
}
// 等价于
class Test{
    public static void test() {
        synchronized(Test.class) {

        }
    }
}
1
2
3
4
5
6
7
8
9
10
11
12

不加 synchronzied 的方法就好比不遵守规则的人,不去老实排队(好比翻窗户进去的)

# 线程八锁

考察 synchronized 锁住的是哪个对象

情况 1:12 或 21

锁住的为同一对象,2 个线程都有可能执行

@Slf4j(topic = "c.Number")
class Number{
    public synchronized void a() {
        log.debug("1");
    }
    public synchronized void b() {
        log.debug("2");
    }
}
public static void main(String[] args) {
    Number n1 = new Number();
    new Thread(()->{ n1.a(); }).start();
    new Thread(()->{ n1.b(); }).start();
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14

情况 2:1s 后 12,或 2 1s 后 1

锁住的为同一对象,2 个线程都有可能执行

@Slf4j(topic = "c.Number")
class Number{
    public synchronized void a() {
        sleep(1);
        log.debug("1");
    }
    public synchronized void b() {
        log.debug("2");
    }
}
public static void main(String[] args) {
    Number n1 = new Number();
    new Thread(()->{ n1.a(); }).start();
    new Thread(()->{ n1.b(); }).start();
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

情况 3:3 1s 12 或 23 1s 1 或 32 1s 1 或 12 1s 3 ...

锁住的为同一对象,3 个线程都有可能执行。根据排列组合,一共有 6 种可能。

@Slf4j(topic = "c.Number")
class Number{
    public synchronized void a() {
        sleep(1);
        log.debug("1");
    }
    public synchronized void b() {
        log.debug("2");
    }
    public void c() {
        log.debug("3");
    }
}
public static void main(String[] args) {
    Number n1 = new Number();
    new Thread(()->{ n1.a(); }).start();
    new Thread(()->{ n1.b(); }).start();
    new Thread(()->{ n1.c(); }).start();
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

情况 4:2 1s 后 1

锁住的不为同一对象,不存在锁竞争,由于第一个线程休眠了一秒,所以第二个线程先打印结果。

@Slf4j(topic = "c.Number")
class Number{
    public synchronized void a() {
        sleep(1);
        log.debug("1");
    }
    public synchronized void b() {
        log.debug("2");
    }
}
public static void main(String[] args) {
    Number n1 = new Number();
    Number n2 = new Number();
    new Thread(()->{ n1.a(); }).start();
    new Thread(()->{ n2.b(); }).start();
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

情况 5:2 1s 后 1

锁住的不为同一对象,不存在锁竞争,第二个线程先执行,第一个锁的是类,第二个是对象。

@Slf4j(topic = "c.Number")
class Number{
    public static synchronized void a() {
        sleep(1);
        log.debug("1");
    }
    public synchronized void b() {
        log.debug("2");
    }
}
public static void main(String[] args) {
    Number n1 = new Number();
    new Thread(()->{ n1.a(); }).start();
    new Thread(()->{ n1.b(); }).start();
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

情况 6:1s 后 12, 或 2 1s 后 1

锁住的为同一类,2 个线程都有可能执行

@Slf4j(topic = "c.Number")
class Number{
    public static synchronized void a() {
        sleep(1);
        log.debug("1");
    }
    public static synchronized void b() {
        log.debug("2");
    }
}
public static void main(String[] args) {
    Number n1 = new Number();
    new Thread(()->{ n1.a(); }).start();
    new Thread(()->{ n1.b(); }).start();
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

情况 7:2 1s 后 1

锁住的一个是对象,一个是类,不存在锁竞争。第二个线程无休眠,先执行。

@Slf4j(topic = "c.Number")
class Number{
    public static synchronized void a() {
        sleep(1);
        log.debug("1");
    }
    public synchronized void b() {
        log.debug("2");
    }
}
public static void main(String[] args) {
    Number n1 = new Number();
    Number n2 = new Number();
    new Thread(()->{ n1.a(); }).start();
    new Thread(()->{ n2.b(); }).start();
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

情况 8:1s 后 12, 或 2 1s 后 1

锁住的为同一类,2 个线程都有可能执行

@Slf4j(topic = "c.Number")
class Number{
    public static synchronized void a() {
        sleep(1);
        log.debug("1");
    }
    public static synchronized void b() {
        log.debug("2");
    }
}
public static void main(String[] args) {
    Number n1 = new Number();
    Number n2 = new Number();
    new Thread(()->{ n1.a(); }).start();
    new Thread(()->{ n2.b(); }).start();
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

# 变量的线程安全分析

成员变量和静态变量是否线程安全?

  • 如果它们没有共享,则线程安全
  • 如果它们被共享了,根据它们的状态是否能够改变,又分两种情况
    • 如果只有读操作,则线程安全
    • 如果有读写操作,则这段代码是临界区,需要考虑线程安全

局部变量是否线程安全?

  • 局部变量是线程安全的
  • 但局部变量引用的对象则未必
    • 如果该对象没有逃离方法的作用访问,它是线程安全的
    • 如果该对象逃离方法的作用范围,需要考虑线程安全

# 局部变量线程安全分析

public static void test1() {
    int i = 10;
    i++;
}
1
2
3
4

每个线程调用 test1 () 方法时局部变量 i,会在每个线程的栈帧内存中被创建多份,因此不存在共享

public static void test1();
 descriptor: ()V 
 flags: ACC_PUBLIC, ACC_STATIC
 Code:
 stack=1, locals=1, args_size=0
 0: bipush 10
 2: istore_0
 3: iinc 0, 1
 6: return
 LineNumberTable:
 line 10: 0
 line 11: 3
 line 12: 6
 LocalVariableTable:
 Start Length Slot Name Signature
 3        4     0   i      I
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

image-20231226074425749

局部变量的引用稍有不同

先看一个成员变量的例子

class Test {
  static final int THREAD_NUMBER = 2;
  static final int LOOP_NUMBER = 200;
  public static void main(String[] args) {
    ThreadUnsafe test = new ThreadUnsafe();
    for (int i = 0; i < THREAD_NUMBER; i++) {
      new Thread(() -> {
        test.method1(LOOP_NUMBER);
      }, "Thread" + i).start();
    }
  }

}

class ThreadUnsafe {
  ArrayList<String> list = new ArrayList<>();
  public void method1(int loopNumber) {
    for (int i = 0; i < loopNumber; i++) {
      // { 临界区, 会产生竞态条件
      method2();
      method3();
      // } 临界区
    }
  }
  private void method2() {
    list.add("1");
  }
  private void method3() {
    list.remove(0);
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31

其中一种情况是,如果线程 2 还未 add,线程 1 remove 就会报错:

Exception in thread "Thread1" java.lang.IndexOutOfBoundsException: Index: 0, Size: 0 
 at java.util.ArrayList.rangeCheck(ArrayList.java:657) 
 at java.util.ArrayList.remove(ArrayList.java:496) 
 at cn.itcast.n6.ThreadUnsafe.method3(TestThreadSafe.java:35) 
 at cn.itcast.n6.ThreadUnsafe.method1(TestThreadSafe.java:26) 
 at cn.itcast.n6.TestThreadSafe.lambda$main$0(TestThreadSafe.java:14) 
 at java.lang.Thread.run(Thread.java:748) 
1
2
3
4
5
6
7

分析:

  • 无论哪个线程中的 method2 引用的都是同一个对象中的 list 成员变量
  • method3 与 method2 分析相同

image-20231226081557936

将 list 修改为局部变量

class ThreadSafe {
    public final void method1(int loopNumber) {
        ArrayList<String> list = new ArrayList<>();
        for (int i = 0; i < loopNumber; i++) {
            method2(list);
            method3(list);
        }
    }
    private void method2(ArrayList<String> list) {
        list.add("1");
    }
    private void method3(ArrayList<String> list) {
        list.remove(0);
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

那么就不会有上述问题了

分析:

  • list 是局部变量,每个线程调用时会创建其不同实例,没有共享
  • 而 method2 的参数是从 method1 中传递过来的,与 method1 中引用同一个对象
  • method3 的参数分析与 method2 相同

image-20231226081636958

方法访问修饰符带来的思考,如果把 method2 和 method3 的方法修改为 public 会不会代理线程安全问题?

  • 情况 1:有其它线程调用 method2 和 method3
  • 情况 2:在 情况 1 的基础上,为 ThreadSafe 类添加子类,子类覆盖 method2 或 method3 方法,
class ThreadSafe {
    public final void method1(int loopNumber) {
        ArrayList<String> list = new ArrayList<>();
        for (int i = 0; i < loopNumber; i++) {
            method2(list);
            method3(list);
        }
    }
    private void method2(ArrayList<String> list) {
        list.add("1");
    }
    private void method3(ArrayList<String> list) {
        list.remove(0);
    }
}
class ThreadSafeSubClass extends ThreadSafe{
    @Override
    public void method3(ArrayList<String> list) {
        new Thread(() -> {
            list.remove(0);
        }).start();
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

从这个例子可以看出 private 或 final 提供【安全】的意义所在,请体会开闭原则中的【闭】

# 常见线程安全类

  • String
  • Integer
  • StringBuffer
  • Random
  • Vector
  • Hashtable
  • java.util.concurrent 包下的类

这里说它们是线程安全的是指,多个线程调用它们同一个实例的某个方法时,是线程安全的。也可以理解为

Hashtable table = new Hashtable();
new Thread(()->{
    table.put("key", "value1");
}).start();
new Thread(()->{
    table.put("key", "value2");
}).start();
1
2
3
4
5
6
7
  • 它们的每个方法是原子的
  • 但注意它们多个方法的组合不是原子的,见后面分析
# 线程安全类方法的组合

分析下面代码是否线程安全?

Hashtable table = new Hashtable();
// 线程1,线程2
if( table.get("key") == null) {
	table.put("key", value);
}
1
2
3
4
5

image-20231226081908793

# 不可变类线程安全性

String、Integer 等都是不可变类,因为其内部的状态不可以改变,因此它们的方法都是线程安全的 有同学或许有疑问,String 有 replace,substring 等方法【可以】改变值啊,那么这些方法又是如何保证线程安 全的呢?

public class Immutable{
    private int value = 0;
    public Immutable(int value){
        this.value = value;
    }
    public int getValue(){
        return this.value;
    }
}
1
2
3
4
5
6
7
8
9

如果想增加一个增加的方法呢?

public class Immutable{
    private int value = 0;
    public Immutable(int value){
        this.value = value;
    }
    public int getValue(){
        return this.value;
    }

    public Immutable add(int v){
        return new Immutable(this.value + v);
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13

# 实例分析

例 1:

public class MyServlet extends HttpServlet {
    // 是否安全?  不安全
    Map<String,Object> map = new HashMap<>();
    // 是否安全?  安全
    String S1 = "...";
    // 是否安全?  安全
    final String S2 = "...";
    // 是否安全?  不安全
    Date D1 = new Date();
    // 是否安全?  不安全
    final Date D2 = new Date();

    public void doGet(HttpServletRequest request, HttpServletResponse response) {
        // 使用上述变量
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

例 2:

public class MyServlet extends HttpServlet {
    // 是否安全?  不安全
    private UserService userService = new UserServiceImpl();

    public void doGet(HttpServletRequest request, HttpServletResponse response) {
        userService.update(...);
    }
}

public class UserServiceImpl implements UserService {
    // 记录调用次数
    private int count = 0;

    public void update() {
        // ...
        count++;
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

例 3:

@Aspect
@Component
public class MyAspect {
    // 是否安全? 不安全
    private long start = 0L;

    @Before("execution(* *(..))")
    public void before() {
        start = System.nanoTime();
    }

    @After("execution(* *(..))")
    public void after() {
        long end = System.nanoTime();
        System.out.println("cost time:" + (end-start));
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

例 4:

public class MyServlet extends HttpServlet {
    // 是否安全  安全
    private UserService userService = new UserServiceImpl();

    public void doGet(HttpServletRequest request, HttpServletResponse response) {
        userService.update(...);
    }
}

public class UserServiceImpl implements UserService {
    // 是否安全 安全
    private UserDao userDao = new UserDaoImpl();

    public void update() {
        userDao.update();
    }
}

public class UserDaoImpl implements UserDao {
    public void update() {
        String sql = "update user set password = ? where username = ?";
        // 是否安全 安全
        try (Connection conn = DriverManager.getConnection("","","")){
            // ...
        } catch (Exception e) {
            // ...
        }
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29

例 5:

public class MyServlet extends HttpServlet {
    // 是否安全
    private UserService userService = new UserServiceImpl();

    public void doGet(HttpServletRequest request, HttpServletResponse response) {
        userService.update(...);
    }
}

public class UserServiceImpl implements UserService {
    // 是否安全
    private UserDao userDao = new UserDaoImpl();

    public void update() {
        userDao.update();
    }
}

public class UserDaoImpl implements UserDao {
    // 是否安全 
    private Connection conn = null;
    public void update() throws SQLException {
        String sql = "update user set password = ? where username = ?";
        conn = DriverManager.getConnection("","","");
        // ...
        conn.close();
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28

例 6:

public class MyServlet extends HttpServlet {
    // 是否安全
    private UserService userService = new UserServiceImpl();

    public void doGet(HttpServletRequest request, HttpServletResponse response) {
        userService.update(...);
    }
}

public class UserServiceImpl implements UserService {
    public void update() {
        UserDao userDao = new UserDaoImpl();
        userDao.update();
    }
}

public class UserDaoImpl implements UserDao {
    // 是否安全
    private Connection = null;
    public void update() throws SQLException {
        String sql = "update user set password = ? where username = ?";
        conn = DriverManager.getConnection("","","");
        // ...
        conn.close();
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26

例 7:

public abstract class Test {

    public void bar() {
        // 是否安全
        SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
        foo(sdf);
    }

    public abstract foo(SimpleDateFormat sdf);


    public static void main(String[] args) {
        new Test().bar();
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

其中 foo 的行为是不确定的,可能导致不安全的发生,被称之为外星方法

public void foo(SimpleDateFormat sdf) {
    String dateStr = "1999-10-11 00:00:00";
    for (int i = 0; i < 20; i++) {
        new Thread(() -> {
            try {
                sdf.parse(dateStr);
            } catch (ParseException e) {
                e.printStackTrace();
            }
        }).start();
    }
}
1
2
3
4
5
6
7
8
9
10
11
12

请比较 JDK 中 String 类的实现

例 8:

private static Integer i = 0;
public static void main(String[] args) throws InterruptedException {
    List<Thread> list = new ArrayList<>();
    for (int j = 0; j < 2; j++) {
        Thread thread = new Thread(() -> {
            for (int k = 0; k < 5000; k++) {
                synchronized (i) {
                    i++;
                }
            }
        }, "" + j);
        list.add(thread);
    }
    list.stream().forEach(t -> t.start());
    list.stream().forEach(t -> {
        try {
            t.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    });
    log.debug("{}", i);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

# Synchronized 锁优化的背景

用锁能够实现数据的安全性,但是会带来性能下降。无锁能够基于线程并行提升程序性能,但是会带来安全性下降。

image-20231204195902438

synchronized 锁:由对象头中的 Mark Word 根据锁标志位的不同而被复用及锁升级策略

# Synchronized 的性能变化

# java5 以前,只有 Synchronized,这个是操作系统级别的重量级操作

java5 以前,只有 Synchronized,这个是操作系统级别的重量级操作。

重量级锁,假如锁的竞争比较激烈的话,性能下降,Java5 之前,用户态和内核态之间的切换

image-20231204195941219

java 的线程是映射到操作系统原生线程之上的,如果要阻塞或唤醒一个线程就需要操作系统介入,需要在户态与核心态之间切换,这种切换会消耗大量的系统资源,因为用户态与内核态都有各自专用的内存空间,专用的寄存器等,用户态切换至内核态需要传递给许多变量、参数给内核,内核也需要保护好用户态在切换时的一些寄存器值、变量等,以便内核态调用结束后切换回用户态继续工作。

在 Java 早期版本中,synchronized 属于重量级锁,效率低下,因为监视器锁(monitor)是依赖于底层的操作系统的 Mutex Lock 来实现的,挂起线程和恢复线程都需要转入内核态去完成,阻塞或唤醒一个 Java 线程需要操作系统切换 CPU 状态来完成,这种状态切换需要耗费处理器时间,如果同步代码块中内容过于简单,这种切换的时间可能比用户代码执行的时间还长”,时间成本相对较高,这也是为什么早期的 synchronized 效率低的原因

Java 6 之后,为了减少获得锁和释放锁所带来的性能消耗,引入了轻量级锁和偏向锁

# 为什么每一个对象都可以成为一个锁????

markOop.hpp

image-20231204200027559

Monitor 可以理解为一种同步工具,也可理解为一种同步机制,常常被描述为一个 Java 对象。Java 对象是天生的 Monitor,每一个 Java 对象都有成为 Monitor 的潜质,因为在 Java 的设计中 ,每一个 Java 对象自打娘胎里出来就带了一把看不见的锁,它叫做内部锁或者 Monitor 锁。

image-20231204200055370

Monitor 的本质是依赖于底层操作系统的 Mutex Lock 实现,操作系统实现线程之间的切换需要从用户态到内核态的转换,成本非常高。

# Monitor (监视器锁)

Mutex Lock Monitor 是在 jvm 底层实现的,底层代码是 c++。本质是依赖于底层操作系统的 Mutex Lock 实现,操作系统实现线程之间的切换需要从用户态到内核态的转换,状态转换需要耗费很多的处理器时间成本非常高。所以 synchronized 是 Java 语言中的一个重量级操作。

Monitor 与 java 对象以及线程是如何关联 ?

  1. 如果一个 java 对象被某个线程锁住,则该 java 对象的 Mark Word 字段中 LockWord 指向 monitor 的起始地址
  2. Monitor 的 Owner 字段会存放拥有相关联对象锁的线程 id

Mutex Lock 的切换需要从用户态转换到核心态中,因此状态转换需要耗费很多的处理器时间。

# java6 开始,优化 Synchronized

Java 6 之后,为了减少获得锁和释放锁所带来的性能消耗,引入了轻量级锁和偏向锁

需要有个逐步升级的过程,别一开始就捅到重量级锁

# Synchronized 锁种类及升级步骤

多线程访问情况,3 种

  • 只有一个线程来访问,有且唯一 Only One
  • 有 2 个线程 A、B 来交替访问
  • 竞争激烈,多个线程来访问

# 字节码

static final Object lock = new Object();
static int counter = 0;
public static void main(String[] args) {
    synchronized (lock) {
        counter++;
    }
}
1
2
3
4
5
6
7

对应的字节码为

public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
              flags: ACC_PUBLIC, ACC_STATIC
              Code:
              stack=2, locals=3, args_size=1
              0: getstatic #2 // <- lock引用 (synchronized开始)
              3: dup
              4: astore_1 // lock引用 -> slot 1
              5: monitorenter // 将 lock对象 MarkWord 置为 Monitor 指针
              6: getstatic #3 // <- i
              9: iconst_1 // 准备常数 1
              10: iadd // +1
              11: putstatic #3 // -> i
              14: aload_1 // <- lock引用
              15: monitorexit // 将 lock对象 MarkWord 重置, 唤醒 EntryList
              16: goto 24
              19: astore_2 // e -> slot 2 
              20: aload_1 // <- lock引用
              21: monitorexit // 将 lock对象 MarkWord 重置, 唤醒 EntryList
              22: aload_2 // <- slot 2 (e)
              23: athrow // throw e
              24: return
              Exception table:
              from to target type
              6    16  19    any
              19   22  19    any
              LineNumberTable:
              line 8: 0
              line 9: 6
              line 10: 14
              line 11: 24
              LocalVariableTable:
              Start Length Slot Name Signature
              0     25     0    args [Ljava/lang/String;
              StackMapTable: number_of_entries = 2
              frame_type = 255 /* full_frame */
              offset_delta = 19
              locals = [ class "[Ljava/lang/String;", class java/lang/Object ]
              stack = [ class java/lang/Throwable ]
              frame_type = 250 /* chop */
              offset_delta = 4
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41

注意

方法级别的 synchronized 不会在字节码指令中有所体现

# 升级流程

synchronized 用的锁是存在 Java 对象头里的 Mark Word 中,锁升级功能主要依赖 MarkWord 中锁标志位和释放偏向锁标志位。

image-20231204200305990

# 无锁

 
package com.atguigu.juc.senior.inner.object;

import org.openjdk.jol.info.ClassLayout;

/**
 * @auther zzyy
 * @create 2020-06-13 11:24
 */
public class MyObject
{
    public static void main(String[] args)
    {
        Object o = new Object();

        System.out.println("10进制hash码:"+o.hashCode());
        System.out.println("16进制hash码:"+Integer.toHexString(o.hashCode()));
        System.out.println("2进制hash码:"+Integer.toBinaryString(o.hashCode()));

        System.out.println( ClassLayout.parseInstance(o).toPrintable());
    }
}

 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24

image-20231204200326087

image-20231204200338475

# 偏锁

轻量级锁在没有竞争时(就自己这个线程),每次重入仍然需要执行 CAS 操作。

Java 6 中引入了偏向锁来做进一步优化:只有第一次使用 CAS 将线程 ID 设置到对象的 Mark Word 头,之后发现 这个线程 ID 是自己的就表示没有竞争,不用重新 CAS。以后只要不发生竞争,这个对象就归该线程所有

# 主要作用

  • 当一段同步代码一直被同一个线程多次访问,由于只有一个线程那么该线程在后续访问时便会自动获得锁
  • 同一个老顾客来访,直接老规矩行方便
  • 看看多线程卖票,同一个线程获得体会一下

Hotspot 的作者经过研究发现,大多数情况下:

多线程的情况下,锁不仅不存在多线程竞争,还存在锁由同一线程多次获得的情况,偏向锁就是在这种情况下出现的,它的出现是为了解决只有在一个线程执行同步时提高性能。

image-20231204200649126

可以通过 CAS 方式修改 markword 中的线程 ID

# 偏向锁的持有

在实际应用运行过程中发现,“锁总是同一个线程持有,很少发生竞争”,也就是说锁总是被第一个占用他的线程拥有,这个线程就是锁的偏向线程。

那么只需要在锁第一次被拥有的时候,记录下偏向线程 ID。这样偏向线程就一直持有着锁 (后续这个线程进入和退出这段加了同步锁的代码块时,不需要再次加锁和释放锁。而是直接比较对象头里面是否存储了指向当前线程的偏向锁)。

如果相等表示偏向锁是偏向于当前线程的,就不需要再尝试获得锁了,直到竞争发生才释放锁。以后每次同步,检查锁的偏向线程 ID 与当前线程 ID 是否一致,如果一致直接进入同步。无需每次加锁解锁都去 CAS 更新对象头。如果自始至终使用锁的线程只有一个,很明显偏向锁几乎没有额外开销,性能极高。

假如不一致意味着发生了竞争,锁已经不是总是偏向于同一个线程了,这时候可能需要升级变为轻量级锁,才能保证线程间公平竞争锁。偏向锁只有遇到其他线程尝试竞争偏向锁时,持有偏向锁的线程才会释放锁,线程是不会主动释放偏向锁的。

技术实现:

一个 synchronized 方法被一个线程抢到了锁时,那这个方法所在的对象就会在其所在的 Mark Word 中将偏向锁修改状态位,同时还 会有占用前 54 位来存储线程指针作为标识。若该线程再次访问同一个 synchronized 方法时,该线程只需去对象头的 Mark Word 中去判断一下是否有偏向锁指向本身的 ID,无需再进入 Monitor 去竞争对象了。

image-20231204200649126

偏向锁的操作不用直接捅到操作系统,不涉及用户到内核转换,不必要直接升级为最高级,我们以一个 account 对象的 “对象头” 为例

image-20231204200846019

假如有一个线程执行到 synchronized 代码块的时候,JVM 使用 CAS 操作把线程指针 ID 记录到 Mark Word 当中,并修改标偏向标示,标示当前线程就获得该锁。锁对象变成偏向锁(通过 CAS 修改对象头里的锁标志位),字面意思是 “偏向于第一个获得它的线程” 的锁。执行完同步代码块后,线程并不会主动释放偏向锁。

image-20231204200854440

这时线程获得了锁,可以执行同步代码块。当该线程第二次到达同步代码块时会判断此时持有锁的线程是否还是自己(持有锁的线程 ID 也在对象头里),JVM 通过 account 对象的 Mark Word 判断:当前线程 ID 还在,说明还持有着这个对象的锁,就可以继续进入临界区工作。由于之前没有释放锁,这里也就不需要重新加锁。 如果自始至终使用锁的线程只有一个,很明显偏向锁几乎没有额外开销,性能极高。

结论:JVM 不用和操作系统协商设置 Mutex (争取内核),它只需要记录下线程 ID 就标示自己获得了当前锁,不用操作系统接入。 上述就是偏向锁:在没有其他线程竞争的时候,一直偏向偏心当前线程,当前线程可以一直执行。

# 偏向锁 JVM 命令

  • java -XX:+PrintFlagsInitial |grep BiasedLock*

image-20231204200937417

实际上偏向锁在 JDK1.6 之后是默认开启的,但是启动时间有延迟,所以需要添加参数 -XX:BiasedLockingStartupDelay=0 ,让其在程序启动时立刻启动。

  • 开启偏向锁: -XX:+UseBiasedLocking -XX:BiasedLockingStartupDelay=0

  • 关闭偏向锁:关闭之后程序默认会直接进入 ------------------------------------------>>>>>>>> 轻量级锁状态。 -XX:-UseBiasedLocking

# 演示

image-20231204201105964

一切默认,如果开启了偏向锁(默认开启),那么对象创建后,markword 值为 0x05 即最后 3 位为 101,这时它的 thread、epoch、age 都为 0

package com.atguigu.juc.senior.inner.object;

import org.openjdk.jol.info.ClassLayout;

/**
 * @auther zzyy
 * @create 2020-06-13 11:24
 */
public class MyObject
{
    public static void main(String[] args)
    {
        Object o = new Object();

        new Thread(() -> {
            synchronized (o){
                System.out.println(ClassLayout.parseInstance(o).toPrintable());
            }
        },"t1").start();
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

演示无效果

image-20231204201139915

因为参数系统默认开启

image-20231204201156243

  • -XX:+UseBiasedLocking :开启偏向锁 (默认)
  • -XX:-UseBiasedLocking :关闭偏向锁
  • -XX:BiasedLockingStartupDelay=0 :关闭延迟 (演示偏向锁时需要开启)

参数说明:

偏向锁在 JDK1.6 以上默认开启,开启后程序启动几秒后才会被激活,可以使用 JVM 参数来关闭延迟 -XX:BiasedLockingStartupDelay=0

如果确定锁通常处于竞争状态则可通过 JVM 参数 -XX:-UseBiasedLocking 关闭偏向锁,那么默认会进入轻量级锁.

关闭延时参数,启用该功能 -XX:BiasedLockingStartupDelay=0

image-20231204201255221

code 演示

static final Object obj = new Object();
public static void m1() {
    synchronized( obj ) {
        // 同步块 A
        m2();
    }
}
public static void m2() {
    synchronized( obj ) {
        // 同步块 B
        m3();
    }
}
public static void m3() {
    synchronized( obj ) {
        // 同步块 C
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
graph LR
subgraph 偏向锁
t5("m1内调用synchronized(obj)")
t6("m2内调用synchronized(obj)")
t7("m2内调用synchronized(obj)")
t8(对象)
t5 -.用ThreadID替换MarkWord.-> t8
t6 -.检查ThreadID是否是自己.-> t8
t7 -.检查ThreadID是否是自己.-> t8
end
subgraph 轻量级锁
t1("m1内调用synchronized(obj)")
t2("m2内调用synchronized(obj)")
t3("m2内调用synchronized(obj)")
t1 -.生成锁记录.-> t1
t2 -.生成锁记录.-> t2
t3 -.生成锁记录.-> t3
t4(对象)
t1 -.用锁记录替换markword.-> t4
t2 -.用锁记录替换markword.-> t4
t3 -.用锁记录替换markword.-> t4
end

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

# 偏向锁的撤销

当有另外线程逐步来竞争锁的时候,就不能再使用偏向锁了,要升级为轻量级锁,竞争线程尝试 CAS 更新对象头失败,会等待到全局安全点(此时不会执行任何代码)撤销偏向锁。

偏向锁的撤销,偏向锁使用一种等到竞争出现才释放锁的机制,只有当其他线程竞争锁时,持有偏向锁的原来线程才会被撤销。

撤销需要等待全局安全点 (该时间点上没有字节码正在执行),同时检查持有偏向锁的线程是否还在执行:

  1. 第一个线程正在执行 synchronized 方法 (处于同步块),它还没有执行完,其它线程来抢夺,该偏向锁会被取消掉并出现锁升级。 此时轻量级锁由原持有偏向锁的线程持有,继续执行其同步代码,而正在竞争的线程会进入自旋等待获得该轻量级锁。
  2. 第一个线程执行完成 synchronized 方法 (退出同步块),则将对象头设置成无锁状态并撤销偏向锁,重新偏向 。

image-20231204201430669

# 撤销 - 其它线程使用对象

当有其它线程使用偏向锁对象时,会将偏向锁升级为轻量级锁

private static void test2() throws InterruptedException {
    Dog d = new Dog();
    Thread t1 = new Thread(() -> {
        synchronized (d) {
            log.debug(ClassLayout.parseInstance(d).toPrintableSimple(true));
        }
        synchronized (TestBiased.class) {
            TestBiased.class.notify();
        }
        // 如果不用 wait/notify 使用 join 必须打开下面的注释
        // 因为:t1 线程不能结束,否则底层线程可能被 jvm 重用作为 t2 线程,底层线程 id 是一样的
        /*try {
 System.in.read();
 } catch (IOException e) {
 e.printStackTrace();
 }*/
    }, "t1");
    t1.start();
    Thread t2 = new Thread(() -> {
        synchronized (TestBiased.class) {
            try {
                TestBiased.class.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        log.debug(ClassLayout.parseInstance(d).toPrintableSimple(true));
        synchronized (d) {
            log.debug(ClassLayout.parseInstance(d).toPrintableSimple(true));
        }
        log.debug(ClassLayout.parseInstance(d).toPrintableSimple(true));
    }, "t2");
    t2.start();
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
# 撤销 - 调用 wait/notify
public static void main(String[] args) throws InterruptedException {
    Dog d = new Dog();
    Thread t1 = new Thread(() -> {
        log.debug(ClassLayout.parseInstance(d).toPrintableSimple(true));
        synchronized (d) {
            log.debug(ClassLayout.parseInstance(d).toPrintableSimple(true));
            try {
                d.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            log.debug(ClassLayout.parseInstance(d).toPrintableSimple(true));
        }
    }, "t1");
    t1.start();
    new Thread(() -> {
        try {
            Thread.sleep(6000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        synchronized (d) {
            log.debug("notify");
            d.notify();
        }
    }, "t2").start();
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
# 批量重偏向

如果对象虽然被多个线程访问,但没有竞争,这时偏向了线程 T1 的对象仍有机会重新偏向 T2,重偏向会重置对象 的 Thread ID

当撤销偏向锁阈值超过 20 次后,jvm 会这样觉得,我是不是偏向错了呢,于是会在给这些对象加锁时重新偏向至 加锁线程

private static void test3() throws InterruptedException {
    Vector<Dog> list = new Vector<>();
    Thread t1 = new Thread(() -> {
        for (int i = 0; i < 30; i++) {
            Dog d = new Dog();
            list.add(d);
            synchronized (d) {
                log.debug(i + "\t" + ClassLayout.parseInstance(d).toPrintableSimple(true));
            }
        }
        synchronized (list) {
            list.notify();
        } 
    }, "t1");
    t1.start();

    Thread t2 = new Thread(() -> {
        synchronized (list) {
            try {
                list.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        log.debug("===============> ");
        for (int i = 0; i < 30; i++) {
            Dog d = list.get(i);
            log.debug(i + "\t" + ClassLayout.parseInstance(d).toPrintableSimple(true));
            synchronized (d) {
                log.debug(i + "\t" + ClassLayout.parseInstance(d).toPrintableSimple(true));
            }
            log.debug(i + "\t" + ClassLayout.parseInstance(d).toPrintableSimple(true));
        }
    }, "t2");
    t2.start();
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
# 批量撤销

当撤销偏向锁阈值超过 40 次后,jvm 会这样觉得,自己确实偏向错了,根本就不该偏向。于是整个类的所有对象 都会变为不可偏向的,新建的对象也是不可偏向的

static Thread t1,t2,t3;
private static void test4() throws InterruptedException {
    Vector<Dog> list = new Vector<>();
    int loopNumber = 39;
    t1 = new Thread(() -> {
        for (int i = 0; i < loopNumber; i++) {
            Dog d = new Dog();
            list.add(d);
            synchronized (d) {
                log.debug(i + "\t" + ClassLayout.parseInstance(d).toPrintableSimple(true));
            }
        }
        LockSupport.unpark(t2);
    }, "t1");
    t1.start();
    t2 = new Thread(() -> {
        LockSupport.park();
        log.debug("===============> ");
        for (int i = 0; i < loopNumber; i++) {
            Dog d = list.get(i);
            log.debug(i + "\t" + ClassLayout.parseInstance(d).toPrintableSimple(true));
            synchronized (d) {
                log.debug(i + "\t" + ClassLayout.parseInstance(d).toPrintableSimple(true));
            }
            log.debug(i + "\t" + ClassLayout.parseInstance(d).toPrintableSimple(true));
        }
        LockSupport.unpark(t3);
    }, "t2");
    t2.start();
    t3 = new Thread(() -> {
        LockSupport.park();
        log.debug("===============> ");
        for (int i = 0; i < loopNumber; i++) {
            Dog d = list.get(i);
            log.debug(i + "\t" + ClassLayout.parseInstance(d).toPrintableSimple(true));
            synchronized (d) {
                log.debug(i + "\t" + ClassLayout.parseInstance(d).toPrintableSimple(true));
            }
            log.debug(i + "\t" + ClassLayout.parseInstance(d).toPrintableSimple(true));
        }
    }, "t3");
    t3.start();
    t3.join();
    log.debug(ClassLayout.parseInstance(new Dog()).toPrintableSimple(true));
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45

参考资料

https://github.com/farmerjohngit/myblog/issues/12

https://www.cnblogs.com/LemonFive/p/11246086.html

https://www.cnblogs.com/LemonFive/p/11248248.html

[偏向锁论文](Eliminating Synchronization-Related Atomic Operations with Biased Locking and Bulk Rebiasing (oracle.com) (opens new window))

# 总体流程图

image-20231204201557487

# 轻锁

轻量级锁的使用场景:如果一个对象虽然有多线程要加锁,但加锁的时间是错开的(也就是没有竞争),那么可以使用轻量级锁来优化。

轻量级锁对使用者是透明的,即语法仍然是 synchronized

主要作用是有线程来参与锁的竞争,但是获取锁的冲突时间极短,本质就是自旋锁。

image-20231204200305990

# 轻量级锁的获取

轻量级锁是为了在线程近乎交替执行同步块时提高性能。

主要目的: 在没有多线程竞争的前提下,通过 CAS 减少重量级锁使用操作系统互斥量产生的性能消耗,说白了先自旋再阻塞。

升级时机: 当关闭偏向锁功能或多线程竞争偏向锁会导致偏向锁升级为轻量级锁

假如线程 A 已经拿到锁,这时线程 B 又来抢该对象的锁,由于该对象的锁已经被线程 A 拿到,当前该锁已是偏向锁了。 而线程 B 在争抢时发现对象头 Mark Word 中的线程 ID 不是线程 B 自己的线程 ID (而是线程 A),那线程 B 就会进行 CAS 操作希望能获得锁。

此时线程 B 操作中有两种情况:

如果锁获取成功,直接替换 Mark Word 中的线程 ID 为 B 自己的 ID (A → B),重新偏向于其他线程 (即将偏向锁交给其他线程,相当于当前线程 "被" 释放了锁),该锁会保持偏向锁状态,A 线程 Over,B 线程上位;

image-20231204202902693

如果锁获取失败,则偏向锁升级为轻量级锁,此时轻量级锁由原持有偏向锁的线程持有,继续执行其同步代码,而正在竞争的线程 B 会进入自旋等待获得该轻量级锁。

image-20231204202911048

# Code 演示

image-20231204203101519

如果关闭偏向锁,就可以直接进入轻量级锁, -XX:-UseBiasedLocking

假设有两个方法同步块,利用同一个对象加锁

static final Object obj = new Object();
public static void method1() {
    synchronized( obj ) {
        // 同步块 A
        method2();
    }
}
public static void method2() {
    synchronized( obj ) {
        // 同步块 B
    }
}
1
2
3
4
5
6
7
8
9
10
11
12

创建锁记录(Lock Record)对象,每个线程都的栈帧都会包含一个锁记录的结构,内部可以存储锁定对象的 Mark Word

image-20231226084330551

让锁记录中 Object reference 指向锁对象,并尝试用 cas 替换 Object 的 Mark Word,将 Mark Word 的值存入锁记录

image-20231226084356162

如果 cas 替换成功,对象头中存储了 锁记录地址和状态 00 ,表示由该线程给对象加锁,这时图示如下

image-20231226084416427

如果 cas 失败,有两种情况:

  • 如果是其它线程已经持有了该 Object 的轻量级锁,这时表明有竞争,进入锁膨胀过程
  • 如果是自己执行了 synchronized 锁重入,那么再添加一条 Lock Record 作为重入的计数

image-20231226084513368

当退出 synchronized 代码块(解锁时)如果有取值为 null 的锁记录,表示有重入,这时重置锁记录,表示重入计数减一

image-20231226084533363

当退出 synchronized 代码块(解锁时)锁记录的值不为 null,这时使用 cas 将 Mark Word 的值恢复给对象头

  • 成功,则解锁成功
  • 失败,说明轻量级锁进行了锁膨胀或已经升级为重量级锁,进入重量级锁解锁流程

# 步骤流程图示

image-20231204203201420

# 自旋达到一定次数和程度

# Java6 之前

默认启用,默认情况下自旋的次数是 10 次 -XX:PreBlockSpin=10 来修改

或者自旋线程数超过 cpu 核数一半

# Java6 之后

自适应,自适应意味着自旋的次数不是固定不变的

而是根据:

  • 同一个锁上一次自旋的时间。
  • 拥有锁线程的状态来决定。

# 轻量锁与偏向锁的区别和不同

  • 争夺轻量级锁失败时,自旋尝试抢占锁
  • 轻量级锁每次退出同步块都需要释放锁,而偏向锁是在竞争发生时才释放锁

# 重锁

如果在尝试加轻量级锁的过程中,CAS 操作无法成功,这时一种情况就是有其它线程为此对象加上了轻量级锁(有竞争),这时需要进行锁膨胀,将轻量级锁变为重量级锁。

有大量的线程参与锁的竞争,冲突性很高

image-20231204203408884

image-20231204203415879

static Object obj = new Object();
public static void method1() {
    synchronized( obj ) {
        // 同步块
    }
}
1
2
3
4
5
6

当 Thread-1 进行轻量级加锁时,Thread-0 已经对该对象加了轻量级锁

image-20231226084736139

这时 Thread-1 加轻量级锁失败,进入锁膨胀流程

  • 即为 Object 对象申请 Monitor 锁,让 Object 指向重量级锁地址
  • 然后自己进入 Monitor 的 EntryList BLOCKED

image-20231226084810219

当 Thread-0 退出同步块解锁时,使用 cas 将 Mark Word 的值恢复给对象头,失败。这时会进入重量级解锁流程,即按照 Monitor 地址找到 Monitor 对象,设置 Owner 为 null,唤醒 EntryList 中 BLOCKED 线程

# 自旋优化

重量级锁竞争的时候,还可以使用自旋 (循环尝试获取重量级锁) 来进行优化,如果当前线程自旋成功(即这时候持锁线程已经退出了同步块,释放了锁),这时当前线程就可以避免阻塞。 (进入阻塞再恢复,会发生上下文切换,比较耗费性能)

自旋重试成功的情况

线程 1 (core 1 上) 对象 Mark 线程 2 (core 2 上)
- 10(重量锁) -
访问同步块,获取 monitor 10(重量锁)重量锁指针 -
成功(加锁) 10(重量锁)重量锁指针 -
执行同步块 10(重量锁)重量锁指针 -
执行同步块 10 (重量锁)重量锁指针 访问同步块,获取 monitor
执行同步块 10(重量锁)重量锁指针 自旋重试
执行完毕 10(重量锁)重量锁指针 自旋重试
成功(解锁) 01(无锁) 自旋重试
- 10(重量锁)重量锁指针 成功(加锁)
- 10(重量锁)重量锁指针 执行同步块
- ... ...

自旋重试失败的情况

线程 1 (core 1 上) 对象 Mark 线程 2 (core 2 上)
- 10(重量锁) -
访问同步块,获取 monitor 10(重量锁)重量锁指针 -
成功(加锁) 10(重量锁)重量锁指针 -
执行同步块 10(重量锁)重量锁指针 -
执行同步块 10(重量锁)重量锁指针 访问同步块,获取 monitor
执行同步块 10(重量锁)重量锁指针 自旋重试
执行同步块 10(重量锁)重量锁指针 自旋重试
执行同步块 10(重量锁)重量锁指针 自旋重试
执行同步块 10(重量锁)重量锁指针 阻塞
- ... ...
  • 自旋会占用 CPU 时间,单核 CPU 自旋就是浪费,多核 CPU 自旋才能发挥优势。
  • 在 Java 6 之后自旋锁是自适应的,比如对象刚刚的一次自旋操作成功过,那么认为这次自旋成功的可能性会 高,就多自旋几次;反之,就少自旋甚至不自旋,总之,比较智能。
  • Java 7 之后不能控制是否开启自旋功能

# 总结

各种锁优缺点、synchronized 锁升级和实现原理

image-20231204203434379

synchronized 锁升级过程总结:一句话,就是先自旋,不行再阻塞。

实际上是把之前的悲观锁 (重量级锁) 变成在一定条件下使用偏向锁以及使用轻量级 (自旋锁 CAS) 的形式

synchronized 在修饰方法和代码块在字节码上实现方式有很大差异,但是内部实现还是基于对象头的 MarkWord 来实现的。

JDK1.6 之前 synchronized 使用的是重量级锁,JDK1.6 之后进行了优化,拥有了无锁 -> 偏向锁 -> 轻量级锁 -> 重量级锁的升级过程,而不是无论什么情况都使用重量级锁。

  • 偏向锁:适用于单线程适用的情况,在不存在锁竞争的时候进入同步方法 / 代码块则使用偏向锁。
  • 轻量级锁:适用于竞争较不激烈的情况 (这和乐观锁的使用范围类似), 存在竞争时升级为轻量级锁,轻量级锁采用的是自旋锁,如果同步方法 / 代码块执行时间很短的话,采用轻量级锁虽然会占用 cpu 资源但是相对比使用重量级锁还是更高效。
  • 重量级锁:适用于竞争激烈的情况,如果同步方法 / 代码块执行时间很长,那么使用轻量级锁自旋带来的性能消耗就比使用重量级锁更严重,这时候就需要升级为重量级锁。

# JIT 编译器对锁的优化

JIT:Just In Time Compiler,一般翻译为即时编译器

# 锁消除

package com.atguigu.itdachang;

/**
 * 锁消除
 * 从JIT角度看相当于无视它,synchronized (o)不存在了,这个锁对象并没有被共用扩散到其它线程使用,
 * 极端的说就是根本没有加这个锁对象的底层机器码,消除了锁的使用
 */
public class LockClearUPDemo
{
    static Object objectLock = new Object();//正常的

    public void m1()
    {
        //锁消除,JIT会无视它,synchronized(对象锁)不存在了。不正常的
        Object o = new Object();

        synchronized (o)
        {
            System.out.println("-----hello LockClearUPDemo"+"\t"+o.hashCode()+"\t"+objectLock.hashCode());
        }
    }

    public static void main(String[] args)
    {
        LockClearUPDemo demo = new LockClearUPDemo();

        for (int i = 1; i <=10; i++) {
            new Thread(() -> {
                demo.m1();
            },String.valueOf(i)).start();
        }
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33

# 锁粗化

锁粗化:有些情况下我们反而希望把很多次锁的请求合并成一个请求,以降低短时间内大量锁请求、同步、释放带来的性能损耗。

对相同对象多次加锁,导致线程发生多次重入,可以使用锁粗化方式来优化。

package com.atguigu.itdachang;

/**
 * 锁粗化
 * 假如方法中首尾相接,前后相邻的都是同一个锁对象,那JIT编译器就会把这几个synchronized块合并成一个大块,
 * 加粗加大范围,一次申请锁使用即可,避免次次的申请和释放锁,提升了性能
 */
public class LockBigDemo
{
    static Object objectLock = new Object();


    public static void main(String[] args)
    {
        new Thread(() -> {
            synchronized (objectLock) {
                System.out.println("11111");
            }
            synchronized (objectLock) {
                System.out.println("22222");
            }
            synchronized (objectLock) {
                System.out.println("33333");
            }
        },"a").start();

        new Thread(() -> {
            synchronized (objectLock) {
                System.out.println("44444");
            }
            synchronized (objectLock) {
                System.out.println("55555");
            }
            synchronized (objectLock) {
                System.out.println("66666");
            }
        },"b").start();

    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
编辑 (opens new window)
上次更新: 2025/01/01, 10:09:39
Java对象内存布局和对象头
AbstractQueuedSynchronizer之AQS

← Java对象内存布局和对象头 AbstractQueuedSynchronizer之AQS→

最近更新
01
k8s
06-06
02
进程与线程
03-04
03
计算机操作系统概述
02-26
更多文章>
Theme by Vdoing | Copyright © 2022-2025 Iekr | Blog
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式