美文网首页程序员
多线程——Synchronized原理实现

多线程——Synchronized原理实现

作者: 洞见星河 | 来源:发表于2020-06-13 21:40 被阅读0次

    前言:为什么使用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的一种锁优化机制,通过扩大锁的范围,避免反复的加锁和释放锁。

    相关文章

      网友评论

        本文标题:多线程——Synchronized原理实现

        本文链接:https://www.haomeiwen.com/subject/uuoztktx.html