zl程序教程

您现在的位置是:首页 >  后端

当前栏目

【Java】synchronized及其实现原理详解编程语言

JAVA原理编程语言 实现 详解 及其 synchronized
2023-06-13 09:11:51 时间

synchronized简介

并行程序开发的一大关注重点就是线程安全,而synchronized是实现线程安全最简单的一种方法。

关键字synchronized的作用是实现线程间的同步。他的工作是对同步的代码加锁,使得每一次只能有一个线程进入同步块,从而保证线程间的安全性。

除了用于线程同步、确保线程安全外,synchronized还可以保证线程间的可见性和有序性。从可见性的角度上讲,synchronized完全可以替代volatile的功能,只是使用上没有那么方便。就有序性而言,由于synchronized限制每次只有一个线程可以访问同步块,所以被synchronized限制的多个线程是串行执行的。

synchronized的使用

关键字synchronized有多种用法,这里做一个简单的整理:

指定加锁对象:对给定对象加锁,进入同步代码前要获得给定对象的锁; 直接作用于实例方法:相当于对当前实例加锁,进入同步代码前要获得当前实例的锁; 直接作用于静态方法:相当于对当前类加锁,进入同步代码前要获得当前类的锁。

这里假设每一个线程对一个变量i自加10000000次,最终要保证每一个线程都加了这么多次。可以想见,如果没有同步机制,最终i肯定不是我们想要的那个数值。于是我们可以使用synchronized来进行同步,这里synchronized的加锁方法有很多,这就是上述的synchronized的几种用法。

1、作用于对象

/** 

 * Created by makersy on 2019 

public class SynchronizedTest implements Runnable{


这里synchronized作用的是instance这个对象,因此,每次当线程进入synchronized包裹的代码段,就都会要求请求instance实例的锁。如果当前有其他线程正持有这把锁,那么新到的线程就要继续等待。这样就保证了每次只能有一个线程执行i++操作。

2、作用于实例方法

 

 * Created by makersy on 2019 

public class SynchronizedTest implements Runnable{


用一个increase方法实现i++,synchronized直接作用于这个实例方法。也就是说在进入increase方法前,线程必须获得当前对象实例的锁。在此例中就是instance对象。要注意的是,两个线程传入的Runnable对象必须是同一个,否则两个线程获取的就不是同一把锁,因为这个锁是作用于实例的。

这里假如传入两个不同的 SynchronizedTest 对象,那么结果就会出现错误。仅仅修改main方法中2个Thread的传入参数,如下:

Thread t1 = new Thread(new SynchronizedTest()); 

Thread t2 = new Thread(new SynchronizedTest()); 

结果就不对了:12491868。

如果要求传入实例不同,但是获得锁一样,那么就可以使用第三种方法。

3、作用于静态方法

/** 

 * Created by makersy on 2019 

public class SynchronizedTest implements Runnable{


Thread t1 = new Thread(new SynchronizedTest()); Thread t2 = new Thread(new SynchronizedTest()); t1.start(); t2.start(); t1.join(); t2.join(); System.out.println(i);

相比错误的实现,只是把increase方法改为了静态方法,这是利用了静态方法的特点,无论实例有多少个,静态方法只有一个,线程请求的还是当前类的锁,而不是当前实例的锁,因此,线程间还是可以正确同步。

synchronized底层实现原理

这里首先要了解在JVM堆内存中对象的布局是怎么样的,如下图:
在这里插入图片描述
实例变量:存放类的属性数据信息,包括父类的属性信息,如果是数组的实例部分还包括数组的长度,这部分内存按4字节对齐。

填充数据:由于虚拟机要求对象起始地址必须是8字节的整数倍。填充数据不是必须存在的,仅仅是为了字节对齐,这点了解即可。

对象头

重点要了解的是对象头,对象头的结构有两种情况:

数组对象,虚拟机使用3个字宽存储对象头。 非数组对象,则使用2个字宽来存储对象头。32位虚拟机中,1个字宽等于4字节,即32位。

具体结构是:


Class Metadata Address 类型指针,指向对象的类元数据,JVM通过这个指针确定该对象是那个类的数据

其中,Mark Word用于存储对象自身的运行时数据,它是实现轻量级锁和偏向锁的关键。
Mark Word内部结构如下所示:
在这里插入图片描述

Monitor

属于一种同步机制,通常被视为一个对象,所有Java对象都可以成为monitor,每一个Java对象一旦生成就会自带一把锁,这把锁被称为Monitor锁或内部锁。

任何一个对象都有一个monitor与之关联,当一个monitor被某个线程持有之后,该对象将处于锁定状态。同时monitor中有一个Owner字段存放拥有该锁的线程的唯一标识,表示该锁被这个线程占用。

代码块同步是使用monitorenter和monitorexit指令实现。monitorenter和monitorexit指令是在编译后插入到同步代码块开始和结束的的位置。

monitorenter :每个对象有一个监视器锁(monitor)。当monitor被占用时就会处于锁定状态,线程执行monitorenter指令时尝试获取monitor的所有权,过程如下:
1、如果monitor的进入数为0,则该线程进入monitor,然后将进入数设置为1,该线程即为monitor的所有者。
2、如果线程已经占有该monitor,只是重新进入,则进入monitor的进入数加1.
3、如果其他线程已经占用了monitor,则该线程进入阻塞状态,直到monitor的进入数为0,再重新尝试获取monitor的所有权。

monitorexit:执行monitorexit的线程必须是对象所对应的monitor的所有者。
指令执行时,monitor的进入数减1,如果减1后进入数为0,那线程退出monitor,不再是这个monitor的所有者。其他被这个monitor阻塞的线程可以尝试去获取这个 monitor 的所有权。
其实wait/notify等方法也依赖于monitor对象,这就是为什么只有在同步的块或者方法中才能调用wait/notify等方法,否则会抛出java.lang.IllegalMonitorStateException的异常的原因。

在HotSpotJVM实现中,锁有个专门的名字:对象监视器。

synchronized锁优化

在JDK 1.6之前,synchronized被认为是重量级的,由于它的易用性导致了synchronized的滥用,这也是它常被诟病的原因。
从jdk1.6之后对synchronized进行了很多优化,如自旋锁、适应性自旋锁、锁消除、锁粗化、偏向锁、轻量级锁等技术来减少锁操作的开销。

自旋锁:通过让线程执行几次忙循环等待锁的释放,不让出CPU。

自适应自旋锁:自选次数不固定,由前一次在同一个锁上的自旋时间及锁的拥有者的状态来决定

锁消除:更彻底的锁优化,通过逃逸分析,若变量不会逃出某个作用域,就将该作用域中的无用锁去除

锁粗化:遇到连续的对同一个锁加锁解锁,扩大加锁范围,避免反复加锁解锁

偏向锁:减少同一线程获取锁的代价。若一个线程获得锁,那么锁进入偏向模式,此时Mark Word结构也变为偏向锁结构,下次获取锁只需要检查Mark Word的锁标记位为偏向锁以及当前线程id为Mark Word的Thread ID即可。不适合锁竞争比较激烈的多线程场合。

轻量级锁:适合线程交替执行同步块。若有同一时间访问同一锁的情况,且自旋了一定次数还没获得锁,就会导致轻量级锁膨胀为重量级锁。

synchronized四种状态

锁膨胀方向:无锁- 偏向锁- 轻量级锁- 重量级锁

原创文章,作者:ItWorker,如若转载,请注明出处:https://blog.ytso.com/19356.html

cjavaxml