synchronized基础原理

Java内置锁synchronized


前言

在多线程中,有可能多个线程同时访问同一个共享资源、可变资源(可能是对象、变量、文件等)这个资源称之为临界资源。每个线程都有自己的工作内存(抽象概念-JMM模型),运行时访问主内存中的临界资源,操作后将操作结果写会主内存。

共享:资源可由多个线程同时访问。
可变:资源可以在其生命周期内被修改。

存在的问题:线程通过读取(read)、载入(load)原子操作将主内存中的变量(临界资源,变量更好理解)拷贝到工作内存的副本,每个线程只能看到自己工作内存中的变量更改。线程执行过程是不可控,可能存在多个线程同时操作同一个变量,将结果写回主内存时可能会覆盖其他线程修改的值。 如:两个线程同时对变量(x=1;)进行自增,两个线程将变量值同时拷贝到工作内存副本,A线程读到x=1,B线程也读到x=1,A线程执行自增x=1+1,B线程执行自增x=1+1,A线程、B线程执行完后同时将结果x=2写回主内存,主内存中结果为x=2,预期结果应该是x+1+1=3。

解决:采用序列化访问临界资源,同一时刻只能有一个线程访问临界资源,也称为同步互斥;同步器的本质是加锁,目的是序列化访问临界资源,Java中提供了两种方式。

  • synchronized
  • Lock

注意: 多线程中执行一个方法时,该方法内部的局部变量并不是临界资源,因为局部变量是每个线程栈私有的,不会导致线程安全问题。


一、synchronized修饰使用

synchronized内置锁:是一种对象锁(锁的是对象而非引用)、隐式锁,作用力度是对象,可重入。

1、同步实例方法:锁的是当前实例对象this(即谁new对象就是谁)。
2、同步类方法:锁的是当前类对象,锁的对象是xxx.class,粒度大。
雷区 :同一个类如果有多个静态方法加了synchronized会出现严重的性能问题(灾难性性能问题),因为多个synchronized静态方法的锁是同一个xxx.class;
3、 同步代码块:锁的是括号里的对象,粒度小。

二、synchronized原理

1.synchronized底层原理

synchronized是基于JVM内置锁,通过内部对象Monitor(监视器锁)实现。监视器锁的实现依赖底层操作系统的Mutex lock(互斥锁)实现,是重量级锁,性能低。JDK1.6之后对synchronized进行优化升级,加入偏向锁、轻量级锁、锁的粗化、锁的消除来减少开销,JDK1.6之后内置锁性能基本与Lock持平。锁的升级过程如下:
在这里插入图片描述
注意: synchronized锁的升级过程不可逆

2.Monitor监视器锁

任何一个对象都有一个Monitor与之关联,当一个Monitor被持有后,它处于锁定状态,Synchronized在JVM里的实现都是基于进入(MonitorEnter)和退出(MonitorExit)Monitor对象来实现方法同步和代码块同步。

  • MonitorEnter:每个对象都是一个监视器锁(monitor),当monitor被占用时就会处于锁定状态。 线程执行MonitorEnter指令时尝试获取monitor的所有权,过程如下:
    a、如果对象的monitor的进入数为0,则当前线程进入monitor,进入数设置为1,当前线程即为monitor的所有者;
    b、如果当前线程本身已经占有该monitor,只是重新进入,则monitor进入数+1;
    c、如果已有其他线程占用monitor,则当前线程进入阻塞状态,直到monitor的进入数为0,再重新尝试获取monitor所有权;
  • MonitorExit:指令出现了两次,第一次为同步正常退出释放锁;第二次为发生异常异步退出释放锁;

3.对象内存布局

HotSpot虚拟机中,对象在内存中的存储布局分为三个区域:对象头(Header)、实例数据(Instance Data)、对齐填充(Padding);

  • 对象头:比如 hash码,对象所属的年代,对象锁,锁状态标志,偏向锁(线程)ID,偏向时间,数组长度(数组对象)等。Java对象头一般占有2个机器码(在32位虚拟机中,1个机器码等于4字节,也就是32bit,在64位虚拟机中,1个机器码是8个字节,也就是64bit),但是 如果对象是数组类型,则需要3个机器码,因为JVM虚拟机可以通过Java对象的元数据信息确定Java对象的大小,但是无法从数组的元数据来确认数组的大小,所以用一块来记录数组长度。
  • 实例数据:存放类的属性数据信息,包括父类的属性信息;
  • 对齐填充:由于虚拟机要求 对象起始地址必须是8字节的整数倍。填充数据不是必须存在的,仅仅是为了字节对齐;

HotSpot虚拟机的对象头包括两部分信息,第一部分是“Mark Word”(高低位互换),用于存储对象自身的运行时数据, 如哈希码(HashCode)、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等等,它是实现轻量级锁和偏向锁的关键。Mark Word会随者程序运行而变化,例如32位变化如下:
32位Mark Word
64位对象头有点浪费空间,因此JVM会默认开启指针压缩。所以基本上也是按32位的形式记录对象头。

4.锁的膨胀升级

JVM默认开启偏向锁
开启偏向锁:-XX:+UseBiasedLocking -XX:BiasedLockingStartupDelay=0
关闭偏向锁:-XX:-UseBiasedLocking

JVM会默认延迟启动偏向锁(大概4S),原因:JVM内部线程有一定的竞争,为了避免大量升级,所以延迟启动偏向锁;存在多个线程进入同一个对象,对象锁回由偏向锁升级为轻量级锁(非同一时间,即竞争不激烈);存在多个线程激烈竞争的情况,对象锁会升级为重量级锁;
当偏向锁未启动时,对象一开始为无锁状态,加同步块后升级为轻量级锁(001->000)
当JVM开启偏向锁时,对象一开始会升级进入匿名偏向锁(可偏向状态),即没有指向任何线程,加同步块后指向线程(101->101且有线程指针记录)

  • 锁的粗化:同一方法内对同一对象进行多次加锁,JVM会对其进行优化(通过逃逸分析)。
// :-XX:+DoEscapeAnalysis 开启逃逸分析
public void method1(){
	synchronized (objectA){
		System.out.println(......);
	}
	synchronized (objectA){
		System.out.println(......);
	}
	synchronized (objectA){
		System.out.println(......);
	}
}
// JVM优化后
public void method1(){
	synchronized (objectA){
		System.out.println(......);
		System.out.println(......);
		System.out.println(......);
	}
}
  • 锁的消除:synchronized对方法内的局部变量进行加锁,JVM优化后会消除锁;因为方法中的局部变量对应的对象,属于当前线程栈帧,其他线程无法访问,因此不存在进程(通过逃逸分析)。
// :-XX:+DoEscapeAnalysis 开启逃逸分析
public void method1(){
	Object objectA = new Object();//引用存堆中,无法被其他线程访问到
	synchronized (objectA){
		System.out.println(......);
	}
}

在这里插入图片描述


总结

synchronized的优化有锁的膨胀升级(不可逆)、锁的粗化、锁的消除、自旋锁;JDK1.6之后性能与dog lea写的Lock基本上相差无几。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

qq_41056877

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

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

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

打赏作者

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

抵扣说明:

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

余额充值