多线程——Synchronized原理实现

前言:为什么使用synchronized?

在多线程编程中,一个资源会被多个线程共享,为了避免因为资源抢占导致数据错乱,所以要对线程进行同步。因此,引入synchronized关键字。

以下,来探究下synchronized的使用和底层原理。

一、synchronized的作用

1.1原子性

原子性:指一个操作或多个操作,要么全部执行,要么全部不执行。

java中,赋值和读取值的操作都是原子的。

但是i++,i+=1等操作不是原子的,因为这些操作在底层是不是一个操作,而是被分成了读取,计算,赋值几步操作,所以保证这几步的原子性,才能保证i++的原子性。

注意:synchronized 可以保证原子性,但是volatile不能保证原子性。

1.2 可见性

可见性:指多线程访问同一资源时,该资源对所有线程都是可见的。

synchronized对一个资源加锁之后,在释放锁之前会将变量的修改刷新到主内存中,保证资源的可见的。

1.3 有序性

有序性:指程序中的代码会按照一定的顺序执行。

在java中,有编译器重排,指令级重排以及系统重排,这些指令重排会使指令实际执行的顺序与实际可见的顺序不同。

synchronized保证了指令不会被重排,按照显示的顺序执行。

:synchronized是可重入的。即一个线程已经获取该资源的synchronized锁时,还可以再次获得该资源的锁。

二、synchronized的作用范围

synchronized关键字可以修饰静态方法,实例函数,代码块。

public class SyncDemo {
    // 修饰静态方法,对类加锁
    public static synchronized void test1(){
    }

    // 修饰实例方法,对实例对象加锁
    public synchronized void test2(){
        //修饰代码块,对类加锁
        synchronized (SyncDemo.class){
        }
    }
    public void test3(){
        // 修饰代码块,对当前对象,即实例方法加锁
        synchronized (this){
        }
    }

    public static void main(String[] args) {

    }
}

2.1 修饰静态方法

由类加载机制可以知道,静态方法是和类同时加载的,归属于类。所以当synchronized修饰静态方法时,是对类加锁。

2.2 修饰实例方法

synchronized修饰实例方法 ,即对当前实例方法加锁。

2.3 修饰代码块

如上代码:修饰的第一个代码块是对实例方法加锁,修饰的第二个代码块是对类加锁。所以修饰代码块时,是可以选择加锁对象的。

三、synchronized底层原理

我们将上述代码反编译成字节码,看看底层实现原理。

从class字节码文件可以看出,一个通过方法flags标志,一个是通过monitorenter和monitorexit指令

3.1 修饰实例方法

在这里插入图片描述
可以看出,synchronized修饰实例方法时,是在方法的flags里面加了一个ACC_SYNCHRONIZED标志。

此标志表示JVM这是一个同步方法,该线程进入该方法时,需要先获取对应的锁,且锁计数器加1,释放时减1.

3.2 修饰代码块

从反编译的字节码可以看出,同步代码块是由monitorexit指令进入,然后monitorexit释放锁。

但是截图中可以看出,有两个monitorexit,这里为什么有两个monitorexit?

第二个monitorexit是来处理异常的。正常情况下,第一个monitorexit之后会执行goto指令,而该指令的返回是后面的return。正常情况下只会执行第一个monitorexit释放锁,然后返回。

而如果执行中发生了异常,第二个monitorexit起作用了,它由编译器自动生成,发生异常时处理异常,然后释放锁的。

四、synchronized锁的底层原理实现

上面我们了解了JVM中如何实现synchronized锁的,但是JVM中如何对对象加锁的呢?

JVM中,对象由三部分构成:对象头,实例数据,对齐填充。

先简单介绍下实例数据和对齐填充:
实例数据:存放类的属性数据信息,包括父类的属性信息。如果是数组实例,还包括数组的长度。
对齐填充:不是必需部分,是虚拟机要求对象地址是8字节的整数倍,所以仅仅是用来字节对齐。
对象头:包括两个部分,Mark Word和Class Metadata Address。Mark Word存储对象的hashCode,锁信息,和分代年龄等信息;而后者记录指针指向对象的类元信息,即确定对象是哪个类的实例。

锁的状态
额外说明下锁的状态,在JDK1.6之前,锁只有无锁和重量级锁两个状态。在JDK1.6之后,对synchronized锁进行了优化,锁状态有四个:无锁状态,偏向锁,轻量级锁,重量级锁。

五、synchronized锁的优化

因为JDK1.6之前,只有重量级锁,所以使用synchronized会造成很大的消耗,所以之后,对synchronized锁进行了优化。

5.1 偏向锁

针对使用了synchronized锁,但是实际操作时,不存在锁竞争时,会加上偏向锁。头对象会标记偏向锁的状态位,见上图。

这样就减少了同一线程获取锁的代价。

5.2 轻量级锁

由偏向锁升级而来,当存在第二个锁申请同一个锁对象时,偏向锁就会升级为轻量级锁。

5.3 重量级锁

由轻量级锁升级而来,当同一时间有多个线程竞争锁时,锁会倍升级为重量级锁。

5.4 锁升级

锁的状态会一步步升级,无锁——>偏向锁——> 轻量级锁——> 重量级锁。而且锁的升级是不可逆的。即升级到轻量级锁,重量级锁,是无法自动恢复到无锁,偏向锁的状态的。

5.5 锁消除

锁消除是JVM另一种锁优化机制,指编译时,对上下文的分析,去除不可能存在竞争的锁。

5.6 锁粗化

锁粗化也是JVM的一种锁优化机制,通过扩大锁的范围,避免反复的加锁和释放锁。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值