详细synchronized关键字分析

并发问题

在这里插入图片描述
如上图所示,当多个线程同时访问某一个变量时,线程首先会把共享的变量读取到线程私有的工作内存中,以便提高线程对变量的访问速度。当变量值发生变化时先修改工作内存中的值,然后再回刷回主内存中,这样当多线程同时访问某一个变量时候,自己私用的变量值很有可能不相等。如上图中,当Thread1和Thread2同时回写主内存时,就会存在值的覆盖问题,最终造成数据的不准确。
下面我们用代码创建两个线程模拟多线程访问同一个变量的问题。

public class ConcurrencyThread {
    private static int a = 0;
    private static void autoAdd(){
        a++;
    }

    static class AddThread extends Thread{
        ConcurrencyThread concurrencyThread = new ConcurrencyThread();
        @Override
        public void run() {
            for (int i = 0; i < 1000; i++) {
                concurrencyThread.autoAdd();
            }
        }
    }
    public static void main(String[] args) throws InterruptedException {
        Thread[] threads = new Thread[2];
        for (int i = 0; i < 2; i++) {
            threads[i] = new Thread(new AddThread());
            threads[i].start();
        }
        threads[0].join();
        threads[1].join();
        System.out.println("a == "+a);
    }
}

上面代码中,我们使用了两个线程,分别对a变量循环自增1000次,等确保两个线程run()方法的逻辑都执行完成之后输出a。我们想要的预期结果应该是2000,实际执行完之后可能不是2000。比如我找了一次运行之后不是2000的结果:
在这里插入图片描述

使用方法

  1. 同步方法
    synchronized可以用来修饰整个方法,以保证多个线程访问该方法时的同步和互斥。
    例如:
public synchronized void method() {
    // ...
}
  1. 同步代码块
    synchronized还可以用来修饰代码块,以保证多个线程访问该代码块时的同步和互斥。代码块需要指定锁对象,锁对象可以是任意对象,但是在多线程共享对象的情况下,应该使用共享对象作为锁对象。
    例如:
public void method() {
    Object lock = new Object();
    synchronized(lock) {
        // ...
    }
}

需要注意的是,在synchronized中提供了两种锁:类锁、对象锁。
类锁是全局的,即使多个线程调用不同的对象实例时也会产生互斥。类锁锁的是Class文件,Class文件在JVM启动过程中每个.class文件会在元空间中加载产生一个Class对象,Class对象在JVM进程中是全局唯一的。
当synchronized作用在方法时使用static修饰时候锁的作用范围是类锁;当在代码块synchronized的括号里是一个Class对象时候锁的作用范围同样是类锁。
对象锁,锁的范围是一个对象实例。
类锁和对象锁有点类似表锁和行锁,锁的粒度不同。

对象头

synchronized实现线程之间的同步,是通过修改对象头中的参数来控制的,所以我们先来看下对象头。
java对象头有三部分组成:

  1. Class Metadata Address(类元数据指针),这是一个指向对象类型元数据的指针。
  2. Array Length(数组长度),用于记录数组的长度,此字段仅限制于数组对象,如果不是数组对象,此字段不存在。
  3. Mark Word,这个字段是Java对象头中最重要的部分,包括了对象的hash码、GC分代年龄和锁相关的标志位。

mark word

32位操作系统

锁状态25bit4bit1bit2bit
23bit1bit是否偏向锁锁标记位
无锁对象的hashCode分代年龄001
偏向锁线程IDEpoch分代年龄101
轻量级锁指向栈中锁记录的指针00
重量级锁指向重量级锁的指针10
GC标记11

64位操作系统

锁状态56bit1bit4bit1bit2bit
25bit31bit是否偏向锁锁标记位
无锁unused对象的hashCodecmc_free分代年龄001
偏向锁线程ID(54bit)Epoch(2bit)分代年龄分代年龄101
轻量级锁指向栈中锁记录的指针00
重量级锁指向重量级锁的指针10
GC标记11

锁升级

在synchronized中引入了偏向锁、轻量级锁、重量级锁,通过修改对象头的锁标记为来记录锁的类型,当前线程具体会用到哪种类型锁,要根据当前的并发激烈程度。随着并发程度的提升,锁会沿着无锁 -> 偏向锁 -> 轻量级锁 -> 重量级锁的方向升级,并且锁一旦发生升级便不可逆向。

偏向锁

偏向锁其实可以理解为在没有竞争的情况下访问synchronized修饰的代码块的枷锁场景,目的是减少多线程竞争下的锁消耗,提高程序运行效率。在无竞争的情况下,偏向锁可以降低锁的重量级,使得锁的性能与非锁状态相当。
在没有线程竞争的情况下访问synchronized代码块时,会先基于CAS把对象头中的线程ID修改为当前线程id,如果修改成功,则说明偏向锁抢占成功。此时对象头中的信息如上面的表格中偏向锁的一行。当抢占成功的线程再次进入同样的synchronized代码块时代码块时,只需要比较线程id与对象头中的线程ID是否相等即可判断是否可以执行代码块中的逻辑。
偏向锁的优点在于可以减少多线程竞争下的锁消耗,避免不必要的锁竞争,提高程序的执行效率。但是偏向锁也存在一些缺点,比如当多个线程竞争同一把锁时,偏向锁会变得无效,此时需要升级为轻量级锁或重量级锁;偏向锁会增加对象头的额外存储,增加了内存开销。因此,在实际应用中,偏向锁的使用需要根据具体的场景进行评估。

CAS(乐观锁)

CompareAndSwap即CAS,比较并替换,所以它是一种乐观锁的思想。CAS方法中会传入三个参数,第一个参数V表示要更新的变量,第二个参数E表示期望值,第三个参数参数U表示更新后的值。如果V==E,期望值和实际值相等,则将V修改成U。

Created with Raphaël 2.3.0 CAS(V,E,U) V==E? 更新变量V并设值为U 返回true 不更新变量V 返回false yes no

乐观锁也需要依赖系统层面的锁

如果多个线程调用CAS,并且同时去执行期望值与实际值的判断,那么也应该存在原子性问题才对。
为解决这个问题JVM在底层的CAS实现上增加了一个Lock指令,来保证它的原子性。

轻量级锁

如果偏向锁存在竞争,那么当前线程就会触发锁膨胀,采用轻量级锁来抢占锁资源。
轻量级锁的实现原理是,当一个线程获得锁时,虚拟机会将对象头Mark Word中的标志位设置为“轻量级锁”,并将线程ID记录在对象头中。此时,虚拟机会将对象头中的指针指向线程栈中的锁记录(Lock Record)。如果其他线程想要获取该锁,虚拟机会检查对象头中的线程ID是否与当前线程ID相同,如果相同,则表示当前线程已经获取了该锁,可以直接执行同步块中的代码;否则,虚拟机会尝试使用自旋锁(自旋+CAS)的方式,等待锁的释放。如果自旋等待的时间超过一定的限制,虚拟机会将锁升级为重量级锁,此时需要使用操作系统的互斥量来保证同步。

轻量级锁的优点在于可以减少线程的上下文切换和线程阻塞,提高程序的执行效率。但是轻量级锁也存在一些缺点,比如在竞争激烈的情况下,自旋等待会消耗大量的CPU时间,影响程序的性能。因此,在实际应用中,轻量级锁的使用需要根据具体的场景进行评估。

重量级锁

当多个线程竞争同一个锁时,如果轻量级锁不能成功获取锁,那么虚拟机会将锁膨胀为重量级锁。重量级锁是一种阻塞锁,其实现方式是使用操作系统的互斥量来保证同步,线程在获取锁失败后,会进入阻塞状态,直到获得锁之后才会继续执行。
在获取重量级锁之前,会先实现锁的膨胀,在膨胀方法中首先创建一个ObjectMonitor对象。具体来说,每个Java对象在虚拟机内部都对应着一个监视器(monitor)对象,用于实现同步。当一个线程需要获得某个对象的锁时,它会尝试获取该对象的监视器,如果获取失败,则进入阻塞状态,等待其他线程释放该对象的锁。
在Java虚拟机中,重量级锁的实现涉及到以下几个步骤:

当一个线程需要获取某个对象的锁时,它会尝试获取该对象的监视器锁(monitor lock),如果该锁没有被其他线程占用,则该线程可以获得锁。

如果该锁已经被其他线程占用,则当前线程会进入阻塞状态,等待锁的释放。

当一个线程获得了锁之后,它可以执行一些操作,然后释放锁。

当一个线程释放锁之后,它会唤醒等待该锁的其他线程,让它们继续竞争锁。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值