对于Synchronized和Volatile的深入理解

目录

1、概述

2、synchronized

2.1、组成

2.2、原理  

2.2.1.对象头

2.2.2.Monitor 机制

2.3、实现

2.3.1.同步代码块

2.3.2.同步方法

2.3.3.区别

2.4、特点

2.5、使用方式

2.6、优缺点

3、volatile

3.1、介绍

3.2、原理

3.3、特点

3.4、适合范围

3.5、i++问题

3.6、优缺点

4. 锁升级

4.1 偏向锁

4.2 轻量级锁

4.3 重量级锁

5、联系


前言

先介绍一下线程的内存可见和执行顺序。

内存可见:就是线程执行结果在内存中对其它线程的可见性(volatile,final,synchronized)。

执行顺序:控制代码的执行顺序及是否可以并发执行。

   在java的并发编程中,关于线程安全问题层出不穷,以下介绍两种常见的关键字synchronized 和 volatile ,它们用于实现线程安全和可见性的机制。


1、概述

以下就是本章节要讲解的内容:

    synchronized 是 Java 的一种锁机制,可以保证同一时间只有一个线程可以执行某个方法或代码块。当一个线程获取到某个对象的锁时,其他线程必须等待直到该锁被释放。

    volatile 是一个轻量级的同步机制,用于保证变量的可见性。它告诉 JVM 不要对该变量进行缓存,每次都从主内存读取,而不是从线程的工作内存(缓存)中读取。


2、synchronized

2.1、组成

  synchronized 是一种机制,用于确保同一时刻只有一个线程能够访被 synchronized 修饰的代码,是一种内置的关键字,用于实现线程间的互斥访问。

        它主要通过对象头中的 Mark Word 来记录锁的状态,并利用 JVM 提供的 Monitor(监视器)来管理线程之间的竞争

即:mark word+monitor组成实现;

内存模型:关于synchronized的monitor的介绍

2.2、原理  

原理:对象头 +monitor-enter和monitor-exit来完成加锁和释放锁的操作。

2.2.1.对象头

由图可知,java的object对象在内存里面由:对象头+实例数据+对齐填充组成。

而关于对象头markwork由64bit组成。具体可参考以下图:

锁标志位可分为:

  • 00:轻量级锁

  • 01:无锁/偏向锁

  • 10:重量级锁

  • 11:GC标记

2.2.2.Monitor 机制

   Monitor 是 Java 中实现同步的基础机制,每个 Java 对象都可以关联一个 Monitor 对象。

关于monitor的结构如下:

如上图可知:monitor主要由:entrylist、owner、waitset、Recursions(重入次数)组成;

整体的执行流程如下图:

1. monitorenter 执行流程

  1. 检查对象的 Mark Word 中的锁标志位

  2. 如果是无锁状态(01):

    • 通过 CAS 操作尝试获取锁

    • 成功则将 Mark Word 中的锁标志位改为 00(轻量级锁)

    • 并将 Mark Word 内容替换为指向当前线程栈中锁记录的指针

  3. 如果是轻量级锁(00):

    • 检查是否是当前线程持有锁(锁重入)

    • 如果是,则增加重入计数

    • 如果不是,则锁升级为重量级锁(10)

  4. 如果是重量级锁(10):

    • 检查 Owner 是否是当前线程

    • 如果是,则增加重入计数

    • 如果不是,则线程进入 EntryList 阻塞等待

2. monitorexit 执行流程

  1. 检查当前线程是否是锁的持有者

  2. 如果是轻量级锁:

    • 减少重入计数

    • 如果计数为0,则释放锁,恢复 Mark Word

  3. 如果是重量级锁:

    • 减少重入计数

    • 如果计数为0:

      • 设置 Owner 为 null

      • 唤醒 EntryList 中的等待线程

2.3、实现

2.3.1.同步代码块

通过 monitorenter 和 monitorexit 指令实现。

代码示例同步代码块字节码:

public void syncBlock();
  Code:
     0: aload_0
     1: dup
     2: astore_1
     3: monitorenter          // 显式加锁
     4: getstatic     #2      // Field java/lang/System.out:Ljava/io/PrintStream;
     7: ldc           #3      // String synchronized block
     9: invokevirtual #4      // Method java/io/PrintStream.println:(Ljava/lang/String;)V
    12: aload_1
    13: monitorexit           // 显式释放锁(正常路径)
    14: goto          22
    17: astore_2
    18: aload_1
    19: monitorexit           // 显式释放锁(异常路径)
    20: aload_2
    21: athrow
    22: return
    Exception table:
       from    to  target type
           4    14    17   any
          17    20    17   any

2.3.2.同步方法

ACC_SYNCHRONIZED 

1.当锁住同步方法:通过方法表中的 ACC_SYNCHRONIZED 标志实现。

public synchronized void syncMethod() {
    // 代码
}

2.主要原因是:

  • JVM 可以统一处理同步方法的进入和退出。

  • 减少字节码大小(不需要为方法内的每个退出点生成 monitorexit)。

3.处理流程

  1. 方法调用时

    • 当调用带有 ACC_SYNCHRONIZED 的方法时。

    • JVM 会隐式获取锁(对于实例方法是 this,静态方法是 Class 对象)。

    • 相当于在方法开始处自动插入 monitorenter。

  2. 方法执行期间

    • 当前线程持有锁。

    • 其他线程无法获取同一个锁。

  3. 方法返回时

    • 无论是正常返回还是异常退出。

    • JVM 都会自动释放锁。

    • 相当于在方法所有退出路径插入 monitorexit。

2.3.3.区别

与显式 monitorenter/monitorexit 不同:

  • 不需要为每个可能的异常路径生成 monitorexit。

  • JVM 保证在任何退出情况下都会释放锁。

  • 不会出现因异常导致锁无法释放的情况。

代码示例:

public synchronized void test() {
    System.out.println("synchronized method");
}

对应的字节码(使用 javap -v 查看):

public synchronized void test();
  descriptor: ()V
  flags: (0x0021) ACC_PUBLIC, ACC_SYNCHRONIZED
  Code:
    stack=2, locals=1, args_size=1
      0: getstatic     #2  // Field java/lang/System.out:Ljava/io/PrintStream;
      3: ldc           #3  // String synchronized method
      5: invokevirtual #4  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
      8: return

注意 flags 中的 ACC_SYNCHRONIZED 标志。

当一个线程持有该对象的锁时,其他线程必须等待锁释放后才能进入 synchronized 的代码块。

  • 加锁机制
    当一个线程进入 synchronized 方法或代码块时,JVM 首先会尝试获取该对象上的锁。如果成功,则继续执行;否则,当前线程会被阻塞并加入等待队列。

  • 解锁机制
    一旦持有锁的线程完成方法调用或者退出代码块范围,就会释放锁资源,允许其他处于等待状态下的线程重新争夺此锁。

示例:

public class Counter {
    private int count;

    public synchronized void increment() { // 对象级别锁定
        count++;
    }

    public static void main(String[] args) throws InterruptedException {
        Counter counter = new Counter();
        
        Thread t1 = new Thread(() -> {
            for (int i = 0; i < 1000; ++i) {
                counter.increment();
            }
        });

        Thread t2 = new Thread(() -> {
            for (int i = 0; i < 1000; ++i) {
                counter.increment();
            }
        });
        
        t1.start();
        t2.start();

        t1.join();
        t2.join();

        System.out.println(counter.getCount());
    }

    public int getCount(){
        return this.count;
    }
}

输出:
count数字=2000

2.4、特点

        1.保证互斥性

        通过加锁,确保同一时间只有一个线程可以执行被 synchronized 修饰的代码,避免数据不一致。

        2.保证内存可见性

        当一个线程修改被 synchronized 修饰的变量后,其他线程在获得该锁时,会看到最新的变量值。

2.5、使用方式

1.实例方法

这里的 synchronized 修饰的是实例方法,相当于对当前实例对象加锁。

使用原理参照2.3.2章节的acc_synchronized来进行实现。

示例:

public synchronized void instanceMethod() {
    // 线程安全的代码
}

2.静态方法

这里的synchronized修饰的是静态方法,相当于对这个类的类对象(Class 对象)加锁。

public static synchronized void staticMethod() {
    // 线程安全的代码
}

3.同步代码块

参考2.3.1章节的monitor_enter和monitor_exit来完成加锁和释放锁。

public void method() {
    synchronized (this) { // 也可以用其他对象
        // 线程安全的代码
    }
}

允许更精细化的控制,只对特定代码块加锁,相比于修饰方法,它的性能开销更小。

2.6、优缺点

  • 优点
    • 提供强一致性,确保线程安全。
    • 适合复杂的同步需求。
  • 缺点
    • 性能开销较大,可能导致线程阻塞和上下文切换。
    • 易导致死锁,需小心使用。

3、volatile

3.1、介绍

          一般情况下线程在执行时,Java中为了加快程序的运行效率,会先把主存数据拷贝到线程本地(寄存器或是CPU缓存)操作完成后再把结果从线程本地缓存刷新到主存中,这样就会导致修改后放入变量结果同步到主存中需要一个过程,而此时另外的线程看到的还是修改之前的变量值,这样就会导致不一致。

      如下图所示:

在这种情况volatile关键字就可以完美解决可见性的问题.

        主要用于保证变量的可见性。当一个线程对 volatile 变量的写入,其他线程会立即看到这个更新。JVM 确保对 volatile 变量的操作不会被重排序,并且会从主内存中强制读取这些变量,而不是从线程的本地内存。

通过在变量声明时加上 volatile 关键字:

private volatile int count;

3.2、原理

        volatile是变量修饰符,其修饰的变量具有内存可见性,基于CPU内存屏障指令(强制 CPU 按照特定的顺序执行加载和存储操作,防止因优化导致的指令重排)实现的。

3.3、特点

        1.可见性

        volatile 它会使得所有对 volatile 变量的读写都会直接读写主存,而不是先读写线程本地缓存,这样就保证了变量的内存可见性。

示例:

public class VolatileExample {
    private volatile boolean flag = false; // 使用 volatile 声明

    public void writer() {
        System.out.println("Writer thread is running...");
        // 模拟一些操作
        try {
            Thread.sleep(1000); // 睡眠一秒,模拟工作
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        flag = true; // 修改 flag 的值
        System.out.println("Writer thread has set flag to true.");
    }

    public void reader() {
        System.out.println("Reader thread is running...");

        // 当 flag 为 false 时,保持循环
        while (!flag) {
            // 空循环,等待 flag 的值变为 true
        }

        // 当 flag 为 true 时,打印信息
        System.out.println("Reader thread sees flag is true. Exiting loop.");
    }

    public static void main(String[] args) {
        VolatileExample example = new VolatileExample();

        Thread writerThread = new Thread(example::writer);
        Thread readerThread = new Thread(example::reader);

        readerThread.start(); // 启动读线程
        writerThread.start(); // 启动写线程
    }
}

输出:

Reader thread is running...
Writer thread is running...
Writer thread has set flag to true.
Reader thread sees flag is true. Exiting loop.

        2.防止指令重排

          volatile可以禁止进行指令重排

         处理器为了提高程序运行效率,可能会对输入代码进行优化,它不保证各个语句的执行顺序同代码中的顺序一致,但是它会保证程序最终执行结果和代码顺序执行的结果是一致的。

        指令重排序不会影响单个线程的执行,但是会影响到线程并发执行时的正确性。

        线程执行到volatile修饰变量的读写操作时,其他线程对这个变量的操作肯定已经完成了,且结果已经同步到了主存中,即对其他的线程可见,本线程再对该变量操作完全没有问题的。

3.4、适合范围

    volatile 适用于简单的状态标志(如 boolean 变量,如boolen、 short 、int 、long等)或单个变量的更新,而不适合复杂的数据结构或方法,因为它无法提供互斥访问。

3.5、i++问题

i++ 这类操作实际上是三个步骤的组合:

  1. 读取变量 i 的当前值。
  2. 将这个值加 1。
  3. 将新值写回变量 i

        假如多个线程同时执行i++,volatile只能保证他们操作的i是同一块内存,但不能保证i结果的正确性。

示例:

public class Counter {
    
    private volatile int count = 0; // 使用 volatile 声明

    public void increment() {
        for (int i = 0; i < 1000; i++) {
            count++; // 这里是 i++
        }
    }

    public static void main(String[] args) throws InterruptedException {
        Counter counter = new Counter();

        Thread t1 = new Thread(counter::increment);
        Thread t2 = new Thread(counter::increment);

        t1.start();
        t2.start();

        t1.join();
        t2.join();

        System.out.println("Final count: " + counter.count);
    }
}

多次测试发现

        由于两个线程都在并发执行 count++ 操作,在执行过程中,它们可能会同时读取到相同的 count 值,导致更新冲突。

解决方案:

1.synchronized

示例:

public class SynchronizedExample {
    private int count = 0;

    public synchronized void increment() {
        for (int i = 0; i < 1000; i++) {
            count++; // 这里的 i++ 操作被 synchronized 修饰,确保它是原子的
        }
    }

    public static void main(String[] args) throws InterruptedException {
        SynchronizedExample example = new SynchronizedExample();

        Thread t1 = new Thread(example::increment);
        Thread t2 = new Thread(example::increment);
        
        t1.start();
        t2.start();
        
        t1.join();
        t2.join();

        System.out.println("Final count: " + example.count); // 现在这里应该是 2000
    }
}

2.AtomicInteger

示例:

import java.util.concurrent.atomic.AtomicInteger;

public class AtomicExample {
    private AtomicInteger count = new AtomicInteger(0);

    public void increment() {
        for (int i = 0; i < 1000; i++) {
            count.incrementAndGet(); // 使用原子操作
        }
    }

    public static void main(String[] args) throws InterruptedException {
        AtomicExample example = new AtomicExample();

        Thread t1 = new Thread(example::increment);
        Thread t2 = new Thread(example::increment);
        
        t1.start();
        t2.start();
        
        t1.join();
        t2.join();

        System.out.println("Final count: " + example.count.get()); // 这里应该是 2000
    }
}

    3.6、优缺点

    • 优点
      • 性能开销小,相比 synchronized 来说。
      • 简单易用,适合用于简单的可见性场景。
    • 缺点
      • 只能保证单个变量的可见性,无法保证原子性。
      • 不适用于需要复杂同步场景的数据操作。

    4. 锁升级

    想了解更多java的锁介绍,可参考本人文章:java常用的锁-CSDN博客

    由上面monitor-enter的执行流程可知,锁会进行升级,以下重点讲述下升级过程。

    1. 检查对象的 Mark Word 中的锁标志位,如下所示:

    根据上图可知:无锁和偏向锁区别是偏向锁的的锁标记为1。

    偏向锁----》轻量级锁-----》重量级锁。

    同时,若偏向锁的线程未活动,锁撤销,则回滚未无锁状态。

    4.1 偏向锁

    1. 初始时对象处于可偏向状态(匿名偏向)

    2. 第一个线程访问同步块:

      • 通过 CAS 将线程ID写入 Mark Word

      • 之后该线程进入同步块不需要同步操作

    3. 出现竞争时:

      • 检查偏向的线程是否存活

      • 如果不存活,则撤销偏向锁

      • 如果存活,则升级为轻量级锁

    4.2 轻量级锁

    1. 线程在栈帧中创建锁记录(Lock Record)

    2. 将对象头的 Mark Word 复制到锁记录中(Displaced Mark Word)

    3. 通过 CAS 将对象头的 Mark Word 替换为指向锁记录的指针

    4. 如果成功,则获取锁

    5. 如果失败,则自旋尝试

    6. 自旋超过阈值后升级为重量级锁

    4.3 重量级锁

    1. 向操作系统申请互斥量(mutex)

    2. 线程挂起进入阻塞状态

    3. 被放入 Monitor 的 EntryList 中

    4. 锁释放时唤醒 EntryList 中的线程

    锁升级代码示例:

    public class LockUpgradeDemo {
        private static final Object lock = new Object();
        private static int counter = 0;
        
        public static void main(String[] args) {
            // 第一阶段:偏向锁
            synchronized (lock) {
                counter++;
            }
            
            // 第二阶段:轻量级锁(两个线程交替执行)
            new Thread(() -> {
                for (int i = 0; i < 100; i++) {
                    synchronized (lock) {
                        counter++;
                    }
                }
            }).start();
            
            for (int i = 0; i < 100; i++) {
                synchronized (lock) {
                    counter++;
                }
            }
            
            // 第三阶段:重量级锁(多线程竞争)
            for (int i = 0; i < 10; i++) {
                new Thread(() -> {
                    for (int j = 0; j < 1000; j++) {
                        synchronized (lock) {
                            counter++;
                        }
                    }
                }).start();
            }
        }
    }

    在实际过程中,减少锁的竞争,

    比如在读写分离场景:可使用ReentrantReadWriteLock。

    总结

    • 无竞争时:使用最轻量的偏向锁

    • 轻度竞争:使用自旋的轻量级锁

    • 高竞争时:使用阻塞的重量级锁(考虑使用 java.util.concurrent 中的并发工具)


    5、联系

    1、volatile仅能使用在变量级别,synchronized则可以使用在变量、方法、和类级别的。

    2、volatile仅能实现变量的修改可见性,不能保证原子性,而synchronized则可以保证变量的修改可见性和原子性。

    3、volatile不会造成线程的阻塞,而synchronized可能会造成线程的阻塞。

    4、volatile标记的变量不会被编译器优化,而synchronized标记的变量可以被编译器优化。

    5、由于 4 中的区别,在某些情况下 volatile 的性能优于 synchronized。


    参考文章:

    1、volatile 和 synchronized 详解

    2、Volatile与synchronized详解

    评论
    添加红包

    请填写红包祝福语或标题

    红包个数最小为10个

    红包金额最低5元

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

    抵扣说明:

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

    余额充值