Synchronized浅析

本文详细解析了Java中的synchronized关键字如何确保线程安全,重点介绍了原子性、可见性和有序性的概念,以及在实例方法、静态方法和同步代码块上的应用。深入剖析了Synchronized底层实现,包括对象头、monitor和锁升级原理。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

是什么?

在并发编程中线程安全问题是值得我们注意的,而造成线程安全问题主要是由于多线程操作共享数据导致的。其中一种解决方式就是加锁,当一个线程正在操作共享数据时,其他线程只能进入等待的状态,直到该线程处理完数据并且释放锁。在Java中,关键词Synchronized可以保证在同一时刻,只有一个线程可以进入方法或者代码块,对数据进行操作。并且Synchronized可以保证线程对于共享数据的操作,也就是共享数据的变化可以被其他的线程所感知,因此保证了可见性。

作用

保证了下面三个性质:

原子性

一个操作或者多个操作,要么全部执行并且执行的过程不会被任何因素打断,要么就都不执行

可见性

多个线程访问一个资源时,该资源的状态、值信息等对于其他线程都是可见的

有序性

程序执行的顺序按照代码先后执行

三大应用方式

synchronized关键字不能继承

作用在实例方法上

修饰实例方法,相当于给当前对象加锁,进入同步方法需要获取当前实例的锁

public class Test {
    public synchronized void func() {
        for (int i = 0; i < 10; i++) {
            System.out.print(i);
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
    
    public static void main(String[] args) throws InterruptedException {
        Test test = new Test();
        Thread t1 = new Thread() {
            @Override
            public void run() {
                test.func();
            }
        };
        Thread t2 = new Thread() {
            @Override
            public void run() {
                test.func();
            }
        };
        t1.start();
        t2.start();
        
    }
}

输出结果:01234567890123456789
注意:当一个线程正在访问一个同步的实例方法时,其他线程不能访问对象的其他同步方法 ,因为一个对象只有一把锁。但是其他线程可以访问该实例对象的其他非同步方法。
如果是一个线程 A 需要访问实例对象obj1的同方法 f1(当前对象锁是obj1),另一个线程 B 需要访问实例对象obj2 的同步方法 f2(当前对象锁是obj2),这样是允许的,因为两个实例对象锁并不同相同

        Test test1 = new Test();
        Test test2 = new Test();
        Thread t1 = new Thread() {
            @Override
            public void run() {
                test1.func();
            }
        };
        Thread t2 = new Thread() {
            @Override
            public void run() {
                test2.func();
            }
        };

输出结果:00112233445566778899

作用在静态方法上

修饰静态方法,相当于给当前类对象加锁,进入同步方法需要获取当前类对象的锁
给func函数加上static关键字

        Thread t1 = new Thread() {
            @Override
            public void run() {
                Test.func();
            }
        };
        Thread t2 = new Thread() {
            @Override
            public void run() {
                Test.func();
            }
        };

输出结果:01234567890123456789

作用在同步代码块上

修饰同步代码块,相当于给给定对象加锁,进入同步代码块需要获得给定对象的锁

public void func() {
        synchronized (this) {
            for (int i = 0; i < 10; i++) {
                System.out.print(i);
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }

输出结果:01234567890123456789

底层实现

前置知识

由于Synchronized锁与对象息息相关,所以必须要了解一下对象的内部结构
在JVM中,对象在内存中主要分为三块区域:对象头、实例数据、填充字节。

  • 实例数据:存放类的属性数据信息,包括父类的属性信息
  • 填充字节:由于虚拟机要求对象起始地址必须是8字节的整数倍。填充数据不是必须存在的,仅仅是为了字节对齐
  • 对象头:
    • Mark Word:存储自身的运行时数据,例如 HashCode、GC 年龄、锁相关信息等内容
    • Klass Pointer:类型指针指向它的类元数据的指针。

考虑到JVM的空间效率,Mark Word 被设计成为一个非固定的数据结构,以便存储更多有效的数据,它会根据对象本身的状态复用自己的存储空间

在这里插入图片描述

对象头与Monitor

虽然 Java6之后对锁进行了锁升级的设计,但是我们暂时先放一边,先分析一下Synchronized锁在重量级锁状态,也就是Java6之前Synchronized的实现。
当处于重量级锁状态的时候,Mark Word中前62位存放的是指向互斥量(重量级锁)的指针,而指向重量级锁的指针就是所谓的同步监视器monitor。每一个对象都与一个monitor关联,而在Java虚拟机(HotSpot)中,monitor是由ObjectMonitor实现的,其主要数据结构如下:

ObjectMonitor() {
     _count        = 0; //记录个数
    _owner        = NULL;//持有锁的线程
    _WaitSet      = NULL; //处于wait状态的线程,会被加入到_WaitSet
    _EntryList    = NULL ; //处于等待锁block状态的线程,会被加入到该列表
    //....  
}

在这里插入图片描述

ObjectMonitor包含两个重要的队列EntrySet和WaitSet,当多线程访问同步代码时,会进入EntrySet。当某个线程获取到锁,相当于获取到monitor时,则会进入到Owner(蓝色区域),Owner变量设置为当前线程同时计数器count+1。若线程调用了wait方法则会释放monitor,owner变为null,count-1,同时进入waitset队列等待被唤醒。如果当前线程执行完毕会释放锁并将count复位,以便其他线程可以获取到锁。

同步代码块

         3: monitorenter//进入同步方法
         4: iconst_0
         5: istore_2
         6: iload_2
         7: bipush        10
         9: if_icmpge     39
        12: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;
        15: iload_2
        16: invokevirtual #3                  // Method java/io/PrintStream.print:(I)V
        19: ldc2_w        #4                  // long 100l
        22: invokestatic  #6                  // Method java/lang/Thread.sleep:(J)V
        25: goto          33
        28: astore_3
        29: aload_3
        30: invokevirtual #8                  // Method java/lang/InterruptedException.printStackTrace:()V
        33: iinc          2, 1
        36: goto          6
        39: aload_1
        40: monitorexit//退出同步方法

通过编译javac和反编译javap,我们可以看出同步代码块的实现其实是依靠monitorenter和monitorexit两条指令。当执行monitorenter时,线程会尝试获取对象的monitor,并且计数器++,如果锁被其他线程所持有,那么线程将进入堵塞状态。当其他线程执行完毕,即monitorexit指令被执行,其他线程会释放锁并将计数器复位,其他线程才有机会去持有monitor。

同步方法

JVM通过方法常量池 ACC_SYNCHRONIZED 访问标志区分一个方法是否同步方法。当方法调用时,调用指令将会 检查方法的 ACC_SYNCHRONIZED 访问标志是否被设置,如果设置了,执行线程将先持有monitor, 然后再执行方法,最后再方法完成时释放monitor。在方法执行期间,执行线程持有了monitor,其他任何线程都无法再获得同一个monitor。

 public synchronized void func();
    descriptor: ()V
    //方法标识ACC_PUBLIC代表public修饰,ACC_SYNCHRONIZED指明该方法为同步方法
    flags: ACC_PUBLIC, ACC_SYNCHRONIZED
    Code:
      stack=2, locals=3, args_size=1
         0: iconst_0
         1: istore_1
         2: iload_1

补充

因为监视器锁(monitor)是依赖于底层的操作系统的Mutex Lock来实现的,而操作系统实现线程之间的切换时需要从用户态转换到核心态,这个状态之间的转换需要相对比较长的时间,时间成本相对较高。而在Java6,对Synchronized引入了锁升级的概念,对锁机制进行优化。

锁升级

锁的四种状态:无锁,偏向锁,轻量级锁,重量级锁(依靠monitor实现)

无锁

资源不会被竞争,也就不需要加锁

偏向锁

在多线程的情况下,当一个线程获取到锁时,锁就进入偏向锁的状态,Mark Word结构也相应编程偏向锁的结构,保存着这个线程的信息。当下一次该线程再次请求锁的时候,需要比较当前线程的threadID和Mark Word中的threadID是否一致,如果一致,则无需使用CAS来加锁、解锁。如果不一致,对象会发现不止一个线程而是有多个线程正在竞争锁,那么就会升级为轻量级锁。

轻量级锁

Lock Record=Displaced Mark Word+Owner
线程在执行同步代码块之前,每个的栈桢都新建一个锁记录的结构,提前将对象的Mark Word复制到锁记录中,官方称为Displaced Mark Word。
线程尝试通过CAS 将锁对象的 Mark Word 更新为指向Lock Record的指针,如果更新成功,该线程获取到轻量级锁,并且需要把对象头的Mark Word的低两位改成10。
线程获取轻量级锁失败,锁膨胀为重量级锁,对象头的Mark Word改为指向重量级锁monitor的指针。获取失败的线程不会立即阻塞,先适应性自旋,尝试获取锁。到达临界值后,阻塞该线程,直到被唤醒。

重量级锁

升级为重量级锁时,锁标志的状态值变为“10”,此时Mark Word中存储的是指向重量级锁的指针

补充

  • 锁只能升级,不能降级
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

程序员小赵OvO

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值