synchronized 关键字详解

synchronized关键字详解


一、synchronized简介(是什么)

synchronized 是Java语言的关键字,可用来给对象、方法、代码块加锁,当它锁定一个方法或者一个代码块的时候,同一时刻最多只有一个线程执行这段代码

在JDK1.5之前synchronized是一个重量级锁,因为加锁解锁涉及到内核态与用户态的转换,有时转化耗时比代码体执行时间还长,导致性能不好,不过毕竟是JDK自带的关键字,所以在JDK1.6后对它进行优化,引入了偏向锁,轻量级锁,自旋锁等概念

用户态和内核态:

现在主流java虚拟机的线程是基于操作系统的内核线程实现的,synchronized加锁和解锁时涉及到线程的阻塞和唤醒,所以需要进行用户态和内核态的切换。

用户态和内核态是操作系统的两种运行状态,操作系统主要是为了对访问能力进行限制,用户态的权限较低,而内核态的权限较高

  • 用户态:用户态运行的程序只能受限的访问内存,只能直接读取用户程序的数据,并且不允许访问外围设备,用户态下的 CPU 不允许独占,也就是说 CPU 能够被其他程序获取。
  • 内核态:内核态运行的程序可以访问计算机的任何数据和资源,不受限制,包括外围设备,比如网卡、硬盘等。处于内核态的 CPU 可以从一个程序切换到另外一个程序,并且占用 CPU 不会发生抢占情况。

二、synchronized应用(怎么用)

synchronized的3种使用方式:

修饰实例方法:作用于当前实例加锁

修饰实例方法,作用于当前实例加锁,进入同步代码前要获得当前实例的锁

/**
 * 方法添加
 */
public  synchronized void methodSync(){
    //同步代码
}

修饰静态方法:作用于当前类对象加锁

修饰静态方法,作用于当前类对象加锁,进入同步代码前要获得当前类对象的锁

/**
 * 静态方法添加
 */
public static synchronized void staticSync(){
    //同步代码
}

修饰代码块:可以指定加锁对象

修饰代码块,指定加锁对象,对给定对象加锁,进入同步代码块前要获得给定对象的锁

/**
 * 代码块添加
 */
public void codeSync(){

    //用对象做锁
    synchronized (this){
        //同步代码
    }

    //用类做锁
    synchronized (Demo.class){
        //同步代码
    }

}

应用例子分析

举个例子 两个线程对count 变量进行叠加,预期结果count=2000000

public class MethodDemo {

    public static volatile int count = 0;

    public static void main(String[] args) throws InterruptedException {
        // 并发运行
        Thread t1 = new Thread(() -> add(1000000));
        Thread t2 = new Thread(() -> add(1000000));

        // 加锁运行
//        MethodDemo methodDemo = new MethodDemo();
//        Thread t1 = new Thread(() -> methodDemo.add2(1000000));
//        Thread t2 = new Thread(() -> methodDemo.add2(1000000));

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

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

        System.out.println(i);
    }

    public static void add(int n) {
        for (int m = 0; m < n; m++) {
            count++;
        }
    }


    // 修饰incr
//    public synchronized void add2(int n) {
//        for (int m = 0; m < n; m++) {
//            count++;
//        }
//    }
}

结果:
使用并发运行add方法最后输出结果小于等于2000000
使用synchronized修饰add方法运行最后输出结果刚好2000000

原因:
多个线程对同一变量进行操作,并发问题
Java 内存模型规定了所有的变量都存储在主内存中,每条线程有自己的工作内存。
线程的工作内存中保存了该线程中用到的变量的主内存副本拷贝,线程对变量的所有操作都必须在工作内存中进行,而不能直接读写主内存。

线程访问一个变量,首先将变量从主内存拷贝到工作内存,对变量的写操作,不会马上同步到主内存。
不同的线程之间也无法直接访问对方工作内存中的变量,线程间变量的传递均需要自己的工作内存和主存之间进行数据同步。
在这里插入图片描述

在没有加锁之前,多个线程去调用add()方法时,没有任何限制,都是可以同时拿到这个count的值进行 ++操 作,但是当加了Synchronized锁之后,线程A和B就由并行执行变成了串行执行

在这里插入图片描述

三、synchronized原理 (怎么实现)

谈synchronized的底层实现,就不得不谈数据在JVM内存的存储:Monitor对象监视器、以及Java对象头

监视器(Monitor)

jvm中每个对象都会有一个监视器Monitor,Monitor直译为“监视器”,而操作系统领域一般翻译为“管程”,管程是指管理共享变量以及对共享变量操作的过程,让它们支持并发。在Java 1.5之前,Java语言提供的唯一并发语言就是管程,Java 1.5之后提供的SDK并发包也是以管程为基础的。
除了Java之外,C/C++、C#等高级语言也都是支持管程的。synchronized关键字和wait()、notify()、notifyAll()这三个方法是Java中实现管程技术的组成部分。

ObjectMonitor() {
    _header = NULL; //对象头 markOop
    _count = 0; //记录owner线程获取锁的次数
    _waiters = 0,
    _recursions = 0; // 锁的重入次数
    _object = NULL; //存储锁对象
    _owner = NULL; // 标识拥有该monitor的线程(当前获取锁的线程)
    _WaitSet = NULL; // 存放等待线程(调用wait)组成的双向循环链表,_WaitSet是第一个节点
    _WaitSetLock = 0;
    _Responsible = NULL ;
    _succ = NULL ;
    _cxq = NULL ; //多线程竞争锁会先存到这个单向链表中 (FILO栈结构)
    FreeNext = NULL ;
    _EntryList = NULL ; //存放在进入或重新进入时被阻塞(blocked)的线程 (也是存竞争锁失败的线程)
    _SpinFreq = 0;
    _SpinClock = 0;
    OwnerIsThread = 0;
    _previous_owner_tid = 0;
}

当使用snychronized修饰同步块代码,我们对代码进行反编译为字节码文件时会发现涉及到两个指令:
1、monitorenter
当monitor被占用时就会处于锁定状态,线程执行monitorenter指令时尝试获取monitor的所有权,过程如下:

  1. 如果monitor的进入数为0,则该线程进入monitor,然后将进入数设置为1,该线程即为monitor的所有者
  2. 如果线程已经占有该monitor,只是重新进入,则进入monitor的进入数加1.
  3. 如果其他线程已经占用了monitor,则该线程进入阻塞状态,直到monitor的进入数为0,再重新尝试获取monitor的所有权。

2、monitorexit
执行monitorexit的线程必须是持有obj锁对象的线程,指令执行时,monitor的进入数减1,如果减1后进入数为0,那线程释放monitor,不再是这个monitor的所有者。
其他被这个monitor阻塞的线程可以尝试去获取这个 monitor 的所有权。

Synchronized的语义底层是通过一个monitor的对象来完成,其实wait/notify等方法也依赖于monitor对象,这就是为什么只有在同步的块或者方法中才能调用wait/notify等方法,否则会抛出IllegalMonitorStateException的异常的原因。

对象头(重点:mark word、类型指针、对象数组)

mark word:主要存储对象的hashcode或锁信息等,是实现轻量级锁和偏向锁的关键

Mark Word用于存储对象自身的运行时数据,如:哈希码(HashCode)、GC分代年龄、锁状态标志、线程持有的锁、偏向线程 ID、偏向时间戳等
在这里插入图片描述

age*:4位的Java对象年龄。在GC中,如果对象在Survivor区复制一次,年龄增加1。当对象达到设定的阈值时,将会晋升到老年代。默认情况下,并行GC的年龄阈值为15,并发GC的年龄阈值为6。由于age只有4位,所以最大值为15,这就是-XX:MaxTenuringThreshold选项最大值为15的原因。
Survivor区:
Survivor的存在意义,就是减少被送到老年代的对象,减少Full GC的发生

identity_hashcode:31位的对象标识hashCode,采用延迟加载技术。调用方法System.identityHashCode()计算,并会将结果写到该对象头中。当对象加锁后(偏向、轻量级、重量级),MarkWord的字节没有足够的空间保存hashCode,因此该值会移动到管程Monitor中。

在这里插入图片描述

对象与Monitor(监视器)、Mark word(标记字段) 如何关联的

在这里插入图片描述

四、锁升级过程

synchronized 在JDK1.6 之后做了一些优化,为了减少获得锁和释放锁来的性能开销,引入了偏向锁、轻量级锁、自旋锁、重量级锁,锁的状态根据竞争激烈的程度从低到高不断升级。

锁主要存在四种状态,依次是:无锁状态、偏向锁状态、轻量级锁状态、重量级锁状态锁的升级是单向的,也就是说只能从低到高升级,不会出现锁的降级。而且这个过程就是开销逐渐加大的过程。

这么设计的目的,其实是为了减少重量级锁带来的性能开销,尽可能的在无锁状态下解决线程并发问 题,其中偏向锁和轻量级锁的底层实现是基于自旋锁,它相对于重量级锁来说,算是一种无锁的实现

偏向锁的获得和撤销

  1. 首先获取锁 对象的Markword,判断是否处于可偏向状态。(biased_lock=1、且ThreadId为空)

  2. 如果是可偏向状态,则通过CAS操作,把当前线程的ID写入到MarkWord

    a) 如果cas成功,那么markword就会变成这样。表示已经获得了锁对象的偏向锁,接着执行同步代码块

    b) 如果cas失败,说明有其他线程已经获得了偏向锁,这种情况说明当前锁存在竞争,需要撤销已获得偏向锁的线程,并且把它持有的锁升级为轻量级锁(这个操作需要等到全局安全点,也就是没有线程在执行字节码)才能执行

  3. 如果是已偏向状态,需要检查markword中存储的ThreadID是否等于当前线程的ThreadID

    a) 如果相等,不需要再次获得锁,可直接执行同步代码块

    b) 如果不相等,说明当前锁偏向于其他线程,需要撤销偏向锁并升级到轻量级锁
    在这里插入图片描述

轻量锁及膨胀流程

两个线程同时访问同步代码块,都将mark word复制到栈修改对mark word进行修改 获取锁,假如线程1修改mark word成功获取到锁,就会将mark word替换成轻量锁,然后就是执行同步体。
线程2获取锁失败就会自旋不断的获取锁,自旋到达一定阈值的时候就会将锁膨胀将mark word里面的锁状态修改为重量锁,自己本身也会进入阻塞状态。
线程1执行完要将这个mark word修改的时候就会失败,因为线程2已经将锁膨胀修改了mark word。
此时线程1就需要去释放锁并唤醒等待线程。
在这里插入图片描述

开始- -> 偏向锁:默认情况下是偏向锁是开启状态,偏向的线程ID是0,如果有线程去抢占锁,那么这个时候线程会先去抢占偏向锁,也就是把偏向锁的线程ID改为当前抢占锁的线程ID。
偏向锁 --> 轻量锁 : 如果有线程竞争,这个时候会撤销偏向锁,升级到轻量级锁,线程在自己的线程栈帧中会创建一个 LockRecord,用CAS操作把markword设置为指向自己这个线程的LR的指针,设置成功后表示抢占到锁。
轻量锁 --> 重量锁: 如果竞争加剧,比如有线程超过10次自旋(-XX:PreBlockSpin参数配置),或者自旋线程数超过 CPU核心数的一般,在1.6之后,加入了自适应自旋Adapative Self Spinning. JVM会根据上次竞争 的情况来自动控制自旋的时间。
重量锁:升级到重量级锁,向操作系统申请资源, Linux Mutex,然后线程被挂起进入到等待队列。

五、扩展

1. 注意事项

1.无论synchronized关键字加在方法上还是对象上,如果它作用的对象是非静态的,则它取得的锁是对象;
2.如果synchronized作用的对象是一个静态方法或一个类,则它取得的锁是对类,该类所有的对象同一把锁。
3.每个对象只有一个锁(lock)与之相关联,谁拿到这个锁谁就可以运行它所控制的那段代码。
4.实现同步是要很大的系统开销作为代价的,甚至可能造成死锁,所以尽量避免无谓的同步控制。

##2 锁的优缺点对比
在这里插入图片描述

3. 查看对象头信息方法

借助JOL工具可以看到对象头在加锁状态下的信息,首先需要使用依赖:

<!--查看对象头工具-->  
<dependency>  
    <groupId>org.openjdk.jol</groupId>  
    <artifactId>jol-core</artifactId>  
    <version>0.9</version> 
</dependency>

<dependency>  
    <groupId>org.openjdk.jol</groupId>  
    <artifactId>jol-core</artifactId>
  <version>0.16</version> 
</dependency>

创建类

public class Juc_book_fang_04_JolTest_02 {

    public static void main(String[] args) {
        Object dog = new Object();
        System.out.println("初始信息:"+ClassLayout.parseInstance(dog).toPrintable());
        System.out.println(dog.hashCode());
        System.out.println("hashcode信息:"+ClassLayout.parseInstance(dog).toPrintable());
        synchronized (dog) {
            System.out.println("加锁后信息:"+ClassLayout.parseInstance(dog).toPrintable());
        }
        System.out.println("释放锁后信息:"+ClassLayout.parseInstance(dog).toPrintable());
    }
}

控制台效果
在这里插入图片描述OFFSET:偏移地址,单位字节;
SIZE:占用的内存大小,单位为字节;
TYPE DESCRIPTION:类型描述,其中object header为对象头;
VALUE:对应内存中当前存储的值;

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 4
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值