0
点赞
收藏
分享

微信扫一扫

Synchronized 原理与锁升级

什么是线程安全

多线程访问了共享的数据,就会产生线程的安全

举例

多个窗口,同时卖一种票,如果不进行控制,可以会出现卖重复票的现象

代码实现

编写卖票线程业务,然后开启多线程同时执行,代码如下

/**
* @author BNTang
*/
public class TicketRunnableImpl implements Runnable {
/**
* 票
*/
private int ticket = 100;

/**
* 线程任务:卖票
*/
@Override
public void run() {
while (ticket > 0) {
// 为了提高线程安全问题出现的几率
// 让线程睡眠10毫秒,放弃cpu的执行权
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 卖票操作,ticket--
System.out.println(Thread.currentThread().getName() + "正在卖第" + ticket + "张票");
ticket--;
}
}

public static void main(String[] args) {
// 创建Runnable接口的实现类对象
TicketRunnableImpl r = new TicketRunnableImpl();

// 创建3个线程
Thread t0 = new Thread(r);
Thread t1 = new Thread(r);
Thread t2 = new Thread(r);

// 开启新的线程
t0.start();
t1.start();
t2.start();
}
}

最终观察结果发现出现了如上所说的问题,出现了重复的票,如下图

Synchronized 原理与锁升级_并发编程

同步代码块 synchronized 解决线程安全

synchronized 作用


  • 确保线程互斥的访问同步代码
  • 保证共享变量的修改能够及时可见
  • 有效解决重排序问题


synchronized 使用格式

synchronized(锁对象){
出现安全问题的代码(访问了共享数据的代码)
}

synchronized 注意事项


  • 锁对象可以是任意对象,new Person、new Student ...
  • 必须保证多个线程使用的是同一个锁对象
  • 锁对象的作用:把​​{}​​ 中代码锁住,只让一个线程进去执行


synchronized 示例

使用 synchronized 改造上方的卖票示例

Synchronized 原理与锁升级_线程安全_02

/**
* @author BNTang
*/
public class TicketRunnableImpl implements Runnable {
/**
* 票
*/
private int ticket = 100;

/**
* 锁对象
*/
private Object obj = new Object();

/**
* 线程任务: 卖票
*/
@Override
public void run() {
synchronized (obj) {
while (ticket > 0) {
// 为了提高线程安全问题出现的几率
// 让线程睡眠10毫秒,放弃cpu的执行权
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 卖票操作,ticket--
System.out.println(Thread.currentThread().getName() + "正在卖第" + ticket + "张票");
ticket--;
}
}
}

public static void main(String[] args) {
// 创建Runnable接口的实现类对象
TicketRunnableImpl r = new TicketRunnableImpl();

// 创建3个线程
Thread t0 = new Thread(r);
Thread t1 = new Thread(r);
Thread t2 = new Thread(r);

// 开启新的线程
t0.start();
t1.start();
t2.start();
}
}

总结


  • 同步中的线程,没有执行完毕,不会释放锁对象,同步外的线程没有锁对象进不去同步代码块当中
  • 当没有锁对象时,进入​​阻塞状态​​,一直等待
  • 出了同步后,会把锁对象归还
  • 同步保证了只能有一个线程在同步中执行共享数据
  • 保证了安全,但是程序频繁的判断锁,释放锁,程序的效率会降低


同步方法解决线程安全

同步方法使用格式

修饰符 synchronized 返回值类型 方法名(参数列表){
出现安全问题的代码(访问了共享数据的代码)
}

使用步骤


  • 创建一个方法,方法的修饰符后面添加上​​synchronized​
  • 把访问了共享数据的代码放入到方法中
  • 调用同步方法


同步方法示例

Synchronized 原理与锁升级_线程安全_03

/**
* @author BNTang
*/
public class TicketRunnableImpl implements Runnable {
/**
* 票
*/
private int ticket = 100;

/**
* 线程任务: 卖票
*/
@Override
public void run() {
ticketMethods();
}

/**
* 卖票
*/
public synchronized void ticketMethods() {
while (ticket > 0) {
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 卖票操作,ticket--
System.out.println(Thread.currentThread().getName() + "正在卖第" + ticket + "张票");
ticket--;
}
}

public static void main(String[] args) {
// 创建Runnable接口的实现类对象
TicketRunnableImpl r = new TicketRunnableImpl();

// 创建3个线程
Thread t0 = new Thread(r);
Thread t1 = new Thread(r);
Thread t2 = new Thread(r);

// 开启新的线程
t0.start();
t1.start();
t2.start();
}
}

锁对象是谁

锁对象为 ​​this​​​,校验锁对象就是 this 改造一下上方的同步方法,用 synchronized 加上锁对象的方式来校验锁对象就是 ​​this​

Synchronized 原理与锁升级_并发编程_04

/**
* @author BNTang
*/
public class TicketRunnableImpl implements Runnable {
/**
* 票
*/
private int ticket = 100;

/**
* 线程任务: 卖票
*/
@Override
public void run() {
ticketMethods();
}

/**
* 卖票
*/
public void ticketMethods() {
synchronized (this) {
while (ticket > 0) {
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 卖票操作,ticket--
System.out.println(Thread.currentThread().getName() + "正在卖第" + ticket + "张票");
ticket--;
}
}
}

public static void main(String[] args) {
// 创建Runnable接口的实现类对象
TicketRunnableImpl r = new TicketRunnableImpl();

// 创建3个线程
Thread t0 = new Thread(r);
Thread t1 = new Thread(r);
Thread t2 = new Thread(r);

// 开启新的线程
t0.start();
t1.start();
t2.start();
}
}

静态同步方法

Synchronized 原理与锁升级_线程安全_05

/**
* @author BNTang
*/
public class TicketRunnableImpl implements Runnable {
/**
* 票
*/
private static int ticket = 100;

/**
* 线程任务: 卖票
*/
@Override
public void run() {
ticketMethods();
}

/**
* 卖票
*/
public static synchronized void ticketMethods() {
while (ticket > 0) {
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 卖票操作,ticket--
System.out.println(Thread.currentThread().getName() + "正在卖第" + ticket + "张票");
ticket--;
}
}

public static void main(String[] args) {
// 创建Runnable接口的实现类对象
TicketRunnableImpl r = new TicketRunnableImpl();

// 创建3个线程
Thread t0 = new Thread(r);
Thread t1 = new Thread(r);
Thread t2 = new Thread(r);

// 开启新的线程
t0.start();
t1.start();
t2.start();
}
}

锁对象是谁

对于 static 方法,我们使用当前方法所在类的字节码对象 ​​(类名.class)​​ 来作为锁对象

Synchronized 原理与锁升级_ide_06

/**
* @author BNTang
*/
public class TicketRunnableImpl implements Runnable {
/**
* 票
*/
private static int ticket = 100;

/**
* 线程任务: 卖票
*/
@Override
public void run() {
ticketMethods();
}

/**
* 卖票
*/
public static void ticketMethods() {
synchronized (TicketRunnableImpl.class) {
while (ticket > 0) {
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 卖票操作,ticket--
System.out.println(Thread.currentThread().getName() + "正在卖第" + ticket + "张票");
ticket--;
}
}
}

public static void main(String[] args) {
// 创建Runnable接口的实现类对象
TicketRunnableImpl r = new TicketRunnableImpl();

// 创建3个线程
Thread t0 = new Thread(r);
Thread t1 = new Thread(r);
Thread t2 = new Thread(r);

// 开启新的线程
t0.start();
t1.start();
t2.start();
}
}

锁机制特性

互斥性


  • 在同一时间只允许一个线程持有某个对象锁,通过这种特性来实现多线程中的协调机制
  • 在同一时间只有一个线程对需同步的代码块 (复合操作) 进行访问
  • 互斥性我们也往往称为操作的​​原子性​


可见性


  • 必须确保在锁被释放之前,对共享变量所做的修改
  • 对于随后获得该锁的另一个线程是可见的(即在获得锁时应获得最新共享变量的值)
  • 否则另一个线程可能是在本地缓存的某个副本上继续操作从而引起不一致


Synchronized 原理

monitor 对象


  • 在 Java 中,每个对象都会有一个​​monitor​​​ 对象,​​监视器​
  • 某一线程占有这个对象的时候,先看 monitor 的计数器是不是​​0​
  • 如果是​​0​​ 代表还没有线程占有,这个时候线程占有这个对象,并且对这个对象的 `monitor + 1
  • 如果不为​​0​​,表示这个线程已经被其他线程占有,这个线程处于等待状态
  • 当线程释放占有权的时候,`monitor - 1


Monitor 和线程的关系

Synchronized 原理与锁升级_并发编程_07

Entry Set 进入区


  • 表示线程通过 synchronized 要求获取对象的锁
  • 如果对象未被锁住,则迚入拥有者,否则则进入区等待
  • 一旦对象锁被其他线程释放,立即参与竞争


The Owner 拥有区


  • 表示某一线程成功竞争到对象锁


Wait Set 等待区


  • 表示线程通过对象的​​wait​​ 方法,释放对象的锁,并在等待区等待被唤醒


总结


  • 一个 Monitor 在某个时刻,只能被一个线程拥有,该线程就是​​“Active Thread”​
  • 而其它线程都是​​“Waiting Thread”​​​,分别在两个队列​​“ Entry Set”​​​ 和​​“Wait Set”​​ 里面等候


同步代码块反编译

/**
* @author BNTang
*/
public class SynchronizedDemo {

public void method() {
synchronized (this) {
System.out.println("Method 1 start");
}
}

public static void main(String[] args) {

}
}

反编译

Synchronized 原理与锁升级_线程安全_08

monitorenter

每个对象有一个监视器锁(monitor)当 monitor 被占用时就会处于锁定状态,线程执行 monitorenter 指令时尝试获取 monitor 的所有权


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


monitorexit


  • 执行 monitorexit 的线程必须是 objectref 所对应的 monitor 的所有者
  • 指令执行时,monitor 的进入数减​​1​​​,如果减​​1​​ 后进入数为 0,那线程退出 monitor,不再是这个 monitor 的所有者
  • 其他被这个 monitor 阻塞的线程可以尝试去获取这个 monitor 的所有权


为什么会有两个 monitorexit


  • 第一个 monitorexit 指令是同步代码块正常释放锁的一个标志
  • 如果同步代码块中出现​​Exception​​​ 或者​​Error​
  • 则会调用第二个​​monitorexit​​ 指令来保证释放锁


同步方法反编译

Synchronized 原理与锁升级_ide_09

/**
* @author BNTang
*/
public class SynchronizedDemo {

public synchronized void method() {
System.out.println("Method 1 start");
}

public static void main(String[] args) {

}
}


  • 同步方法并没有通过指令​​monitorenter​​​ 和​​monitorexit​​ 来完成(理论上其实也可以通过这两条指令来实现)
  • 不过相对于普通方法,其常量池中多了​​ACC_SYNCHRONIZED​​ 标示符
  • JVM 就是根据该标示符来实现方法的同步的:当方法调用时,调用指令将会检查方法的​​ACC_SYNCHRONIZED​​ 访问标志是否被设置
  • 如果设置了,执行线程将先获取 monitor,获取成功之后才能执行方法体,方法执行完后再释放 monitor
  • 在方法执行期间,其他任何线程都无法再获得同一个 monitor 对象
  • 其实本质上没有区别
  • 只是方法的同步是一种隐式的方式来实现,无需通过字节码来完成


Synchronized 底层优化

为什么说 Synchronized 性能低


  • Synchronized 是通过对象内部的一个叫做监视器锁(monitor)来实的
  • 但是监视器锁本质又是依赖于底层的操作系统的​​Mutex Lock​​ 来实现的
  • 而操作系统实现线程之间的切换这就需要从用户态转换到核心态,这个成本非常高
  • 状态之间的转换需要相对比较长的时间,这就是为什么 Synchronized 效率低的原因
  • 因此,这种依赖于操作系统​​Mutex Lock​​​ 所实现的锁我们称之为​​“重量级锁”​


Synchronized 原理与锁升级_同步方法_10

锁的状态总共有四种

​JDK1.6 之后​​ 默认开启如下锁


  • 无锁状态
  • 偏向锁
  • 轻量级锁
  • 重量级锁


Synchronized 原理与锁升级_同步方法_11

随着锁的竞争,锁可以从 ​​偏向锁​​​ 升级到 ​​轻量级锁​​​,再升级到 ​​重量级锁​

但是锁的升级是单向的,也就是说只能从低到高升级,不会出现锁的降级

Java 对象结构

对象头

Mark Word


  • Mark Word(标记字)主要用来表示对象的线程锁状态,另外还可以用来配合 GC、存放该对象的 hashCode
  • Mark Word 在 32位 JVM 中的长度是​​32bit​​​,在 64 位 JVM 中长度是​​64bit​


Klass Word


  • Klass Word 是一个指向方法区中 Class 信息的指针,意味着该对象可随时知道自己是哪个 Class 的实例
  • 该指针在 32 位 JVM 中的长度是​​32bit​​​,在 64 位 JVM 中长度是​​64bit​


数组长度


  • 是可选的,只有当本对象是一个数组对象时才会有这个部分
  • 该数据在 32 位和 64 位 JVM 中长度都是​​32bit​


对象体


  • 对象体是用于保存对象属性和值的主体部分,占用内存空间取决于对象的属性数量和类型


对齐字节


  • 因为 JVM 要求 Java 对象占的内存大小应该是​​8bit​​ 的倍数
  • 所以后面有几个字节用于把对象的大小补齐至 8bit 的倍数,没有特别的功能


对象头

对象头概述

对象头被分为两个部分,第一个部分是 Mark Word,代表标记信息,例如 hash code,锁的标志位,GC 分代年龄等

第二个部分是 KClass Word,代表类型信息,该部分是一个指针,指向方法区 (元数据空间) 中的实际类型

对象头状态

普通状态

普通状态,由 25 位哈希码,4 位 GC 分代年龄,1 位偏向锁标识,2 位锁标志 (01) 组成

偏向状态

由 23 位线程标识,2 位时间戳,4 位 GC 分代年龄,1 位偏向锁标识,2 位锁标识 (01) 组成

轻量级锁状态

由 30 位指针 (指向锁记录) 2 位锁标识 (00) 组成

重量级锁状态

由 30 位指针指向重量级锁的 monitor 2 位锁标识 (10) 组成

Mared for GC

待 GC 回收状态,只有最后 2 位锁标识 (11) 有效

Synchronized 原理与锁升级_并发编程_12

偏向锁


  • 偏向锁是为了在无多线程竞争的情况下尽量减少不必要的轻量级锁执行路径
  • 轻量级锁的获取及释放依赖多次​​CAS​​ 原子指令,而偏向锁只需要在置换 ThreadID 的时候依赖一次 CAS 原子指令
  • 轻量级锁是为了在线程交替执行同步块时提高性能,而偏向锁则是在只有一个线程执行同步块时进一步提高性能


偏向锁获取过程

(1)访问 Mark Word 中偏向锁的标识是否设置成 1,锁标志位是否为 01 ——确认为可偏向状态

(2)如果为可偏向状态,则测试线程ID是否指向当前线程,如果是,进入步骤(5)否则进入步骤(3)

(3)如果线程ID并未指向当前线程,则通过 CAS 操作竞争锁

如果竞争成功,则将 Mark Word 中线程ID设置为当前线程ID,然后执行(5)如果竞争失败,执行(4)

(4)如果 CAS 获取偏向锁失败,则表示有竞争

当到达全局安全点(safepoint)时获得偏向锁的线程被挂起,​​偏向锁​​​ 升级为 ​​轻量级锁​​,然后被阻塞在安全点的线程继续往下执行同步代码

(5)执行同步代码

偏向锁的释放


  • 偏向锁只有遇到其他线程尝试竞争偏向锁时,持有偏向锁的线程才会释放锁,线程不会主动去释放偏向锁
  • 偏向锁的撤销,需要等待全局安全点(在这个时间点上没有字节码正在执行)
  • 它会首先暂停拥有偏向锁的线程,判断锁对象是否处于被锁定状态
  • 撤销偏向锁后恢复到未锁定(标志位为​​“01”​​​)或轻量级锁(标志位为​​“00”​​)的状态


轻量级锁

轻量级锁并不是用来代替重量级锁的,它的本意是在没有多线程竞争的前提下,减少传统的重量级锁使用产生的性能消耗

轻量级锁所适应的场景是线程交替执行同步块的情况,如果存在同一时间访问同一锁的情况,就会导致轻量级锁膨胀为重量级锁

轻量级锁的加锁过程

(1)在代码进入同步块的时候,如果同步对象锁状态为无锁状态(锁标志位为 ​​“01”​​​ 状态,是否为偏向锁为 ​​“0”​​)

虚拟机首先将在当前线程的栈帧中建立一个名为锁记录(Lock Record)的空间

用于存储锁对象目前的 Mark Word 的拷贝,线程堆栈与对象头的状态

Synchronized 原理与锁升级_同步方法_13

(2)拷贝对象头中的 Mark Word 复制到锁记录中

(3)拷贝成功后,虚拟机将使用 CAS 操作尝试将对象的 Mark Word 更新为指向 Lock Record 的指针

并将 Lock record 里的 owner 指针指向 object mark word。如果更新成功,则执行步骤(3)否则执行步骤(4)

(4)如果这个更新动作成功了,那么这个线程就拥有了该对象的锁,并且对象 Mark Word 的锁标志位设置为 ​​“00”​

即表示此对象处于轻量级锁定状态,这时候线程堆栈与对象头的状态如下图中所示

Synchronized 原理与锁升级_ide_14

轻量级锁的解锁过程

(1)通过 CAS 操作尝试把线程中复制的 Displaced Mark Word 对象替换当前的 Mark Word

(2)如果替换成功,整个同步过程就完成了

(3)如果替换失败,说明有其他线程尝试过获取该锁(此时锁已膨胀)那就要在释放锁的同时,唤醒被挂起的线程

适应性自旋自旋锁


  • 从轻量级锁获取的流程中我们知道,当线程在获取轻量级锁的过程中执行 CAS 操作失败时,是要通过自旋来获取重量级锁的
  • 问题在于,自旋是需要消耗 CPU 的,如果一直获取不到锁的话,那该线程就一直处在自旋状态,白白浪费 CPU 资源
  • 解决这个问题最简单的办法就是指定自旋的次数,例如让其循环 10 次,如果还没获取到锁就进入阻塞状态
  • 适应性自旋,简单来说就是线程如果自旋成功了,则下次自旋的次数会更多,如果自旋失败了,则自旋的次数就会减少


锁粗化

将多次连接在一起的加锁、解锁操作合并为一次,将多个连续的锁扩展成一个范围更大的锁

锁消除

锁消除即删除不必要的加锁操作。根据 ​​代码逃逸​​ 技术,如果判断到一段代码中,堆上的数据不会逃逸出当前线程

那么可以认为这段代码是线程安全的,不必要加锁,JIT 在编译的时候把不必要的锁去掉

总结

JDK 中采用轻量级锁和偏向锁等对 Synchronized 的优化

但是这两种锁也不是完全没缺点的,比如竞争比较激烈的时候,不但无法提升效率,反而会降低效率,因为多了一个锁升级的过程

这个时候就需要通过 ​​-XX:-UseBiasedLocking​​ 来禁用偏向锁

几种锁的对比

Synchronized 原理与锁升级_同步方法_15

锁升级代码演示

创建一个 Maven 工程,然后引入依赖依赖内容如下,该依赖就是可以打印一些计算机底层所对应的内容可以显示出来

Synchronized 原理与锁升级_并发编程_16

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

创建 Test.java 内容如下

Synchronized 原理与锁升级_同步方法_17

/**
* @author BNTang
**/
public class Test {
public static void main(String[] args) throws InterruptedException {
Thread.sleep(4100);
Object obj = new Object();
System.out.println(ClassLayout.parseInstance(obj).toPrintable());
}
}

直接运行之后观察控制台输出结果

Synchronized 原理与锁升级_ide_18

采用操作系统的小端模型

00000000 00000000 00000000 00000101

匿名偏向,预先准备好,可以做偏向,默认是开启了偏向锁的,可以取消偏向锁,取消方式如下

-XX:-UseBiasedLocking

Synchronized 原理与锁升级_线程安全_19

Synchronized 原理与锁升级_同步方法_20

打开偏向锁,使用 synchroinzed 锁定 obj 对象

/**
* @author BNTang
**/
public class Test {
public static void main(String[] args) throws InterruptedException {
Thread.sleep(4100);
Object obj = new Object();
System.out.println(ClassLayout.parseInstance(obj).toPrintable());

synchronized (obj) {
System.out.println(ClassLayout.parseInstance(obj).toPrintable());
}
}
}

Synchronized 原理与锁升级_线程安全_21

升级轻量极锁

Synchronized 原理与锁升级_并发编程_22

/**
* @author BNTang
**/
public class Test {
public static void main(String[] args) throws InterruptedException {
Thread.sleep(4100);
Object obj = new Object();
System.out.println(ClassLayout.parseInstance(obj).toPrintable());

new Thread(() -> {
synchronized (obj) {
System.out.println(ClassLayout.parseInstance(obj).toPrintable());
}
}).start();

// 偏向锁
Thread.sleep(500);

new Thread(() -> {
// 轻量级锁
synchronized (obj) {
System.out.println(ClassLayout.parseInstance(obj).toPrintable());
}
}).start();
}
}

Synchronized 原理与锁升级_ide_23

升级重量级锁

/**
* @author BNTang
**/
public class Test {
public static void main(String[] args) throws InterruptedException {
Thread.sleep(4100);
Object obj = new Object();
System.out.println(ClassLayout.parseInstance(obj).toPrintable());

new Thread(() -> {
synchronized (obj) {
System.out.println(ClassLayout.parseInstance(obj).toPrintable());
}
}).start();

new Thread(() -> {
// 重量级锁
synchronized (obj) {
System.out.println(ClassLayout.parseInstance(obj).toPrintable());
}
}).start();
}
}

Synchronized 原理与锁升级_ide_24

竞争激烈

/**
* @author BNTang
**/
public class Test {
public static void main(String[] args) throws InterruptedException {
Thread.sleep(4100);
Object obj = new Object();
System.out.println(ClassLayout.parseInstance(obj).toPrintable());

new Thread(() -> {
synchronized (obj) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(ClassLayout.parseInstance(obj).toPrintable());
}
}).start();

new Thread(() -> {
synchronized (obj) {
// 重量级锁
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(ClassLayout.parseInstance(obj).toPrintable());
}
}).start();
}
}

Synchronized 原理与锁升级_线程安全_25

Synchronized 原理与锁升级_ide_26

相关面试题

说说 synchronized 关键字的底层原理是什么


  • 在 Java 中,每个对象都会有一个 monitor 对象,监视器
  • 某一线程占有这个对象的时候,先看 monitor 的计数器是不是 0
  • 如果是 0 代表还没有线程占有,这个时候线程占有这个对象,并且对这个对象的 monitor + 1
  • 如果不为 0,表示这个线程已经被其他线程占有,这个线程等待。当线程释放占有权的时候,monitor - 1


反编译

monitorenter

每个对象有一个监视器锁(monitor)当 monitor 被占用时就会处于锁定状态,线程执行 monitorenter 指令时尝试获取 monitor 的所有权


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


monitorexit


  • 执行 monitorexit 的线程必须是 objectref 所对应的 monitor 的所有者
  • 指令执行时,monitor 的进入数减 1,如果减 1 后进入数为 0,那线程退出 monitor,不再是这个 monitor 的所有者
  • 其他被这个 monitor 阻塞的线程可以尝试去获取这个 monitor 的所有权


同步和非同步方法是否可以同时调用

是可以的

/**
* @author BNTang
*/
public class SynchronizedTest01 {
public synchronized void method1() {
System.out.println(Thread.currentThread().getName() + " method1 start...");
SleepTools.sleepSecond(5);
System.out.println(Thread.currentThread().getName() + " method1 end");
}

public void method2() {
SleepTools.sleepSecond(2);
System.out.println(Thread.currentThread().getName() + " method2 ");
}

public static void main(String[] args) {
// 同步和非同步方法是否可以同时调用? 是可以同时调用的
SynchronizedTest01 t = new SynchronizedTest01();
new Thread(t::method1, "t1").start();
new Thread(t::method2, "t2").start();
}
}

可能会产生脏读的情况

/**
* @author BNTang
*/
public class SynchronizedTest02 {
static class Account {
/**
* 名字
*/
String name;

/**
* 钱
*/
double money = 10;

public synchronized void set(String name, double balance) {
this.name = name;
SleepTools.sleepSecond(2);
this.money = balance;
}

public double getBalance(String name) {
return this.money;
}
}

public static void main(String[] args) {
Account account = new Account();

new Thread(() -> account.set("zs", 100.0)).start();

SleepTools.sleepSecond(1);

// 第1次读的内容为10 主线程读到了另一个还没有操作完成的数据
// 解决办法 在读线程的时候也加锁
System.out.println(account.getBalance("zs"));

// 第2次读的内容为100
System.out.println(account.getBalance("zs"));
}
}

Synchronized 原理与锁升级_并发编程_27

一个同步方法能不能调用另外一个同步的方法


  • 一个同步方法可以调用另外一个同步方法
  • 一个线程已经拥有某个对象的锁,再次申请的时候仍然会得到该对象的锁
  • 也就是说 synchronized 获得的锁是可重入的


/**
* @author BNTang
*/
public class SynchronizedTest03 {
synchronized void method1() {
System.out.println("method1 start");
SleepTools.sleepSecond(1);
method2();
System.out.println("method1 end");
}

synchronized void method2() {
SleepTools.sleepSecond(2);
System.out.println("method2");
}

public static void main(String[] args) {
new SynchronizedTest03().method1();
}
}

Synchronized 原理与锁升级_ide_28

如果程序出现异常锁会不会被释放

/**
* @author BNTang
*/
public class SynchronizedTest04 {
/**
* 数
*/
int count = 0;

synchronized void method() {
System.out.println(Thread.currentThread().getName() + " start");
while (true) {
count++;
System.out.println(Thread.currentThread().getName() + " count = " + count);
SleepTools.sleepSecond(1);
if (count == 5) {
// 此处抛出异常,锁将被释放
// 如果不想释放锁, 对异常进行 catch 处理, 让程序继续执行
int i = 1 / 0;
System.out.println(i);
}
}
}

public static void main(String[] args) {
SynchronizedTest04 t = new SynchronizedTest04();

Runnable r = t::method;

new Thread(r, "thread1").start();

SleepTools.sleepSecond(3);
new Thread(r, "thread2").start();
}
}

Synchronized 原理与锁升级_并发编程_29

volatile 与 synchronized 有什么区别


  • volatile 只能修饰变量,synchronized 只能修饰方法和语句块
  • synchronized 可以保证原子性,volatile 不能保证​​原子性​
  • 都可以保证可见性,但实现原理不同,volatile 对变量加了​​lock​​​,synchronized 使用​​monitorEnter​​​ 和​​monitorexit​​​、​​monitor​
  • volatile 能保证有序,synchronized 可以保证有序性,但是代价(重量级)并发退化到串行
  • synchronized 引起阻塞
  • volatile 不会引起阻塞


参考资料



举报

相关推荐

0 条评论