JAVA多线程之synchronized锁

文章目录

  • 1. 临界区
  • 2. synchronized使用
    • 2.1 不加锁实现
    • 2.2 synchronized加锁
    • 2.3 面向对象的改进
    • 2.4 方法上加synchronized
    • 2.5 线程安全
  • 3. Monitor
    • 3.1 Java对象头
    • 3.2 Monitor工作流程
    • 3.3 字节码角度
  • 4. synchronized原理
    • 4.1 轻量级锁
    • 4.2 锁膨胀
    • 4.3 偏向锁
      • 4.3.1 偏向锁过程
      • 4.3.2 偏向状态
      • 4.3.3 撤销偏向
      • 4.3.4 批量重偏向
      • 4.3.5 批量撤销
  • 5. synchronized优化
    • 5.1 自旋锁
    • 5.2 锁消除
    • 5.3 锁粗化
    • 5.4 多把锁

1. 临界区

在介绍synchronized锁之前,我们先来了解一下什么是临界区以及临界资源。

临界资源:一次仅允许一个进程使用的资源成为临界资源

临界区:访问临界资源的代码块

竞态条件:多个线程在临界区内执行,由于代码的执行序列不同而导致结果无法预测,称之为发生了竞态条件

一个程序运行多个线程是没有问题,多个线程读共享资源也没有问题,在多个线程对共享资源读写操作时发生指令交错,就会出现问题,而为了让多线程模式下能够有序的使用临界资源,就引入了锁的概念。

为了避免临界区的竞态条件发生(解决线程安全问题):

  • 阻塞式的解决方案:synchronized,lock
  • 非阻塞式的解决方案:原子变量

管程(Monitor):由局部于自己的若干公共变量和所有访问这些公共变量的过程所组成的软件模块,保证同一时刻只有一个进程在管程内活动,即管程内定义的操作在同一时刻只被一个进程调用(由编译器实现)

synchronized:对象锁,保证了临界区内代码的原子性,采用互斥的方式让同一时刻至多只有一个线程能持有对象锁,其它线程获取这个对象锁时会阻塞,保证拥有锁的线程可以安全的执行临界区内的代码,不用担心线程上下文切换,其实现依赖于Monitor

互斥和同步都可以采用 synchronized 关键字来完成,区别:

  • 互斥是保证临界区的竞态条件发生,同一时刻只能有一个线程执行临界区代码
  • 同步是由于线程执行的先后、顺序不同、需要一个线程等待其它线程运行到某个点

性能:

  • 线程安全,性能差
  • 线程不安全性能好,假如开发中不会存在多线程安全问题,建议使用线程不安全的设计类

2. synchronized使用

synchronized锁使用的语法如下:

synchronized(锁对象){// 访问共享资源的核心代码
}

锁对象:理论上可以是任意的唯一对象

synchronized 是可重入、不公平的重量级锁

原则上:

  • 锁对象建议使用共享资源
  • 在实例方法中使用 this 作为锁对象,锁住的 this 正好是共享资源
  • 在静态方法中使用类名 .class 字节码作为锁对象,因为静态成员属于类,被所有实例对象共享,所以需要锁住类

示例:

如果我们开两个线程对一个变量分别进行加5000次和减5000次,那么结果会是什么呢?

2.1 不加锁实现

不加锁实现的即开一个线程加5000次,开另一个线程减5000次,然后等待线程执行完,查看最后的结果:

@Slf4j(topic = "c.TwoPhaseTermination")
public class Test1 {static int counter = 0;public static void main(String[] args) throws InterruptedException {Thread t1 = new Thread(()->{for(int i = 0;i < 5000;i++){counter++;}}, "t1");Thread t2 = new Thread(()->{for(int i = 0;i < 5000;i++){counter--;}}, "t1");t1.start();t2.start();t1.join();t2.join();log.debug("counter: {}", counter);}
}

这样执行后发现,执行结果并不总是0,这是由于没有同步所造成的。

2.2 synchronized加锁

我们可以使用synchronized关键字对对象进行加锁,达到同步的目的,代码如下:

@Slf4j(topic = "c.TwoPhaseTermination")
public class Test1 {static int counter = 0;static final Object room = new Object();public static void main(String[] args) throws InterruptedException {Thread t1 = new Thread(()->{for(int i = 0;i < 5000;i++){synchronized (room){counter++;}}}, "t1");Thread t2 = new Thread(()->{for(int i = 0;i < 5000;i++){synchronized (room){counter--;}}}, "t1");t1.start();t2.start();t1.join();t2.join();log.debug("counter: {}", counter);}
}

2.3 面向对象的改进

可以将上面的锁写为一个对象,然后调用对象中的方法来进行线程同步的加减法,修改如下:

@Slf4j(topic = "c.TwoPhaseTermination")
public class Test1 {public static void main(String[] args) throws InterruptedException {Room room = new Room();Thread t1 = new Thread(()->{for(int i = 0;i < 5000;i++){room.increment();}}, "t1");Thread t2 = new Thread(()->{for(int i = 0;i < 5000;i++){room.decrement();}}, "t1");t1.start();t2.start();t1.join();t2.join();log.debug("counter: {}", room.getCounter());}
}class Room{private int counter = 0;public void increment(){synchronized (this){counter++;}}public void decrement(){synchronized (this){counter--;}}public int getCounter(){return counter;}
}

这样将需要加的锁都加到了另一个对象room中,我们只需要调用room的加减方法即可。

2.4 方法上加synchronized

synchronized的另一种用法是加在方法上,其使用的形式如下:

/** 非静态方法:普通方法锁住的是对象的实例 **/
class Test{public synchronized void test(){}
}// 上面的写法等价于class Test{public void test(){synchronized(this){}}
}/** 静态方法:静态方法的synchronized锁住的是类的Class对象 **/
class Test{public synchronized static void test(){}
}// 上面的写法等价于class Test{public static void test(){synchronized(Test.class){}}
}

2.5 线程安全

线程安全的是指,多个线程调用它们同一个实例的某个方法时,能够达到预期的效果。

成员变量和静态变量

  • 如果它们没有共享,则线程安全
  • 如果它们被共享了,根据它们的状态是否能够改变,分两种情况:
    • 如果只有读操作,则线程安全
    • 如果有读写操作,则这段代码是临界区,需要考虑线程安全问题

局部变量

  • 局部变量是线程安全的
  • 局部变量引用的对象不一定线程安全(逃逸分析):
    • 如果该对象没有逃离方法的作用访问,它是线程安全的(每一个方法有一个栈帧)
    • 如果该对象逃离方法的作用范围,需要考虑线程安全问题(暴露引用)

常见线程安全类:String、Integer、StringBuffer、Random、Vector、Hashtable、java.util.concurrent 包,他们的每个方法都是原子的,但是,他们方法的组合并不是原子的

不可变类线程安全:String、Integer 等都是不可变类,内部的状态不可以改变,所以方法是线程安全。

像String的replace,substring方法也没有改变内部的状态,这些方法在底层都是新建一个对象,将值复制过去的。

抽象方法如果有参数,被重写后行为不确定可能造成线程不安全,被称之为外星方法public abstract foo(Student s);

Map<String,Object> map = new HashMap<>();	// 线程不安全
String S1 = "...";							// 线程安全
final String S2 = "...";					// 线程安全
Date D1 = new Date();						// 线程不安全
final Date D2 = new Date();					// 线程不安全,final让D2引用的对象不能变,但对象的内容可以变

3. Monitor

Monitor 被翻译为监视器或管程。每个 Java 对象都可以关联一个 Monitor 对象,Monitor 也是 class,其实例存储在堆中,Monitor这个锁是由操作系统提供的,也被称为重量级锁。synchronized 的重量级锁就是Monitor实现的。

3.1 Java对象头

以32位虚拟机为例,java的对象由两部分组成,第一部分是对象头,第二部分是一些成员变量,普通对象的对象头如下:

在这里插入图片描述

其中 Klass Word 指向了对象所属的Class,标明对象是哪个类型,比如Integer,String,Student等等,可以找到类对象。

其中Mark Word 的结构如下:

在这里插入图片描述

我们先来看Normal模式的Mark Word,前面25位是一个hashcode,之后4位是表示生代的年龄位数,biased_lock表示是否是偏向锁,最后的01表示其加锁状态。

对于数组对象的话,其除了Mark Word和Klass Word 之外,还有一个数组长度,数组对象结构示意如下:

在这里插入图片描述

3.2 Monitor工作流程

Monitor的流程示意图如下:

在这里插入图片描述

  1. 开始时 Monitor 中 Owner 为 null
  2. 当 Thread-2 执行 synchronized(obj) 就会将 Monitor 的所有者 Owner 置为 Thread-2,Monitor 中只能有一个 Owner,obj 对象的 Mark Word 指向 Monitor,把对象原有的 MarkWord 存入线程栈中的锁记录中(轻量级锁部分详解)
  3. 在 Thread-2 上锁的过程,Thread-3、Thread-4、Thread-5 也执行 synchronized(obj),就会进入 EntryList BLOCKED(双向链表)
  4. Thread-2 执行完同步代码块的内容,根据 obj 对象头中 Monitor 地址寻找,设置 Owner 为空,把线程栈的锁记录中的对象头的值设置回 MarkWord
  5. 唤醒 EntryList 中等待的线程来竞争锁,竞争是非公平的,如果这时有新的线程想要获取锁,可能直接就抢占到了,阻塞队列的线程就会继续阻塞
  6. WaitSet 中的 Thread-0,是以前获得过锁,但条件不满足进入 WAITING 状态的线程(wait-notify 机制),如下图

在这里插入图片描述

注意:

  • synchronized 必须是进入同一个对象的 Monitor 才有上述的效果
  • 不加 synchronized 的对象不会关联监视器,不遵从以上规则

3.3 字节码角度

比如我们有如下的代码:

public static void main(String[] args) {Object lock = new Object();synchronized (lock) {System.out.println("ok");}
}

编译为字节码,字节码如下:

0: 	new				#2		// new Object
3: 	dup
4: 	invokespecial 	#1 		// invokespecial <init>:()V,非虚方法
7: 	astore_1 				// lock引用 -> lock
8: 	aload_1					// lock (synchronized开始)
9: 	dup						// 一份用来初始化,一份用来引用
10: astore_2 				// lock引用 -> slot 2
11: monitorenter 			// 【将 lock对象 MarkWord 置为 Monitor 指针】
12: getstatic 		#3		// System.out
15: ldc 			#4		// "ok"
17: invokevirtual 	#5 		// invokevirtual println:(Ljava/lang/String;)V
20: aload_2 				// slot 2(lock引用)
21: monitorexit 			// 【将 lock对象 MarkWord 重置, 唤醒 EntryList】
22: goto 30
25: astore_3 				// any -> slot 3
26: aload_2 				// slot 2(lock引用)
27: monitorexit 			// 【将 lock对象 MarkWord 重置, 唤醒 EntryList】
28: aload_3
29: athrow
30: return
Exception table:from to target type12 22 25 		any25 28 25 		any
LineNumberTable: ...
LocalVariableTable:Start Length Slot Name Signature0 	31 		0 args [Ljava/lang/String;8 	23 		1 lock Ljava/lang/Object;

从字节码中我们可以看出,当使用synchronized加锁的时候,会将对象复制一份用来进行锁的引用,然后就是进行第11到21步的Monitor过程,之后应该就完了,那为什么还会有第22到29步呢?我们可以看到上面有一个异常捕获表,如下:

Exception table:from to target type12 22 25 		any25 28 25 		any

表示的是第12到22步中如果有错误就跳转到第25到28步。

4. synchronized原理

synchronized 是可重入、不公平的重量级锁,所以可以对其进行优化。

  • synchronized 的可重入表现为同一个线程在外层方法获取锁的时候,再进入该线程的内层方法会自动获取锁,不会因为之前已经获取过还没释放而阻塞,示例如下:

    public class ReEntryLockDemo{public static void main(String[] args){final Object objectLockA = new Object();new Thread(() -> {synchronized (objectLockA){System.out.println("-----外层调用");synchronized (objectLockA){System.out.println("-----中层调用");synchronized (objectLockA){System.out.println("-----内层调用");}}}},"t1").start();}
    }
    

    上面代码的输出为如下:

    -----外层调用
    -----中层调用
    -----内层调用

    这就是可重入的体现,如果是不可重入锁,那么只会输出 -----外层调用

  • synchronized 的不公平表现为synchronized里面的对象会先尝试直接进行上锁,如果上锁失败,才会加入等待队列里面。

  • synchronized 锁的级别如下

    无锁 -> 偏向锁 -> 轻量级锁 -> 重量级锁	// 随着竞争的增加,只能锁升级,不能降级
    

    锁的级别图示如下,我们可以先看各个级别的锁的原理再来看这幅图。

在这里插入图片描述

4.1 轻量级锁

轻量级锁的使用场景:如果一个对象虽然有多线程要加锁,但加锁的时间是错开的(也就是没有竞争),那么可以使用轻量级锁来优化。

轻量级锁对使用者是透明的,即语法仍然是 synchronized,不用关心其具体实现。

可重入锁:线程可以进入任何一个它已经拥有的锁所同步着的代码块,可重入锁最大的作用是避免死锁

轻量级锁的加锁过程如下:

假设加锁的代码如下:

static final Object obj = new Object();
public static void method1() {synchronized( obj ) {// 同步块 Amethod2();}
}
public static void method2() {synchronized( obj ) {// 同步块 B}
}
  1. 创建锁记录(Lock Record)对象,每个线程的栈帧都会包含一个锁记录的结构,存储锁定对象的 Mark Word,因为锁住之后对象的Mark Word会指向锁的地址

    在这里插入图片描述

  2. 让锁记录中 Object reference 指向锁住的对象,并尝试用 CAS 替换 Object 的 Mark Word,将 Mark Word 的值存入锁记录,这里的CAS是比较并交换的意思,这是一个原子操作,可以将其理解为不会被其他线程打断出错的一个比较交换操作,详细原理我们会在后面讲解。

    在这里插入图片描述

  3. 如果 CAS 替换成功,对象头中存储了锁记录地址和状态 00(轻量级锁) ,表示由该线程给对象加锁
    在这里插入图片描述

  4. 如果 CAS 失败,有两种情况:

    • 如果是其它线程已经持有了该 Object 的轻量级锁,这时表明有竞争,进入锁膨胀过程
    • 如果是线程自己执行了 synchronized 锁重入,就添加一条 Lock Record 作为重入的计数

    在这里插入图片描述

  5. 当退出 synchronized 代码块(解锁时)

    • 如果有取值为 null 的锁记录,表示有重入,这时重置锁记录,表示重入计数减 1
    • 如果锁记录的值不为 null,这时使用 CAS 将 Mark Word 的值恢复给对象头
      • 成功,则解锁成功
      • 失败,说明轻量级锁进行了锁膨胀或已经升级为重量级锁,进入重量级锁解锁流程

    在这里插入图片描述

4.2 锁膨胀

在尝试加轻量级锁的过程中,CAS 操作无法成功,可能是其它线程为此对象加上了轻量级锁(有竞争),这时需要进行锁膨胀,将轻量级锁变为重量级锁

  1. 当 Thread-1 进行轻量级加锁时,Thread-0 已经对该对象加了轻量级锁
    在这里插入图片描述

  2. Thread-1 加轻量级锁失败,进入锁膨胀流程:

    • 为 Object 对象申请 Monitor 锁,通过 Object 对象头获取到持锁线程,将 Monitor 的 Owner 置为 Thread-0,将 Object 的对象头指向重量级锁地址
    • 然后自己进入 Monitor 的 EntryList BLOCKED

    在这里插入图片描述

  3. 当 Thread-0 退出同步块解锁时,使用 CAS 将 Mark Word 的值恢复给对象头失败,这时进入重量级解锁流程,即按照 Monitor 地址找到 Monitor 对象,设置 Owner 为 null,唤醒 EntryList 中 BLOCKED 线程

4.3 偏向锁

轻量级锁在没有竞争时(就自己这个线程),每次重入仍然需要执行 CAS 操作,如果重入的次数多了,对效率的影响也会加大,于是产生了偏向锁。

4.3.1 偏向锁过程

偏向锁的思想是偏向于让第一个获取锁对象的线程,这个线程之后重新获取该锁不再需要进行CAS操作。

只有第一次使用 CAS 将线程 ID 设置到对象的 Mark Word 头,之后发现这个线程 ID 是自己的就表示没有竞争,不用重新 CAS。以后只要不发生竞争,这个对象就归该线程所有。

例如有下面的一个类中的代码:

static final Object obj = new Object();
public static void m1() {synchronized( obj ) {// 同步块 Am2();}
}
public static void m2() {synchronized( obj ) {// 同步块 Bm3();}
}
public static void m3() {synchronized( obj ) {// 同步块 C}
}

上述代码在轻量级锁中的状态如下:

在这里插入图片描述

引入偏向锁后,示意图如下:

在这里插入图片描述

4.3.2 偏向状态

我们首先来看64位Java虚拟机中的对象头的MarkWord,其结构如下:

在这里插入图片描述

当一个对象创建的时候:

  • 如果开启了偏向锁(默认开启),那么对象创建后,MarkWord 值为 0x05 即最后 3 位为 101,thread、epoch、age 都为 0

  • 偏向锁是默认是延迟的,不会在程序启动时立即生效,如果想避免延迟,可以加 VM 参数 -XX:BiasedLockingStartupDelay=0 来禁用延迟。JDK 8 延迟 4s 开启偏向锁原因:在刚开始执行代码时,会有好多线程来抢锁,如果开偏向锁效率反而降低

  • 当一个对象已经计算过 hashCode,就再也无法进入偏向状态了,正常状态对象一开始是没有 hashCode 的,第一次调用才生成

  • 添加 VM 参数 -XX:-UseBiasedLocking 禁用偏向锁

4.3.3 撤销偏向

偏向锁的撤销有如下三种情况:

  1. 当一个对象已经计算过 hashCode,就再也无法进入偏向状态了,正常状态对象一开始是没有 hashCode 的,第一次调用才生成,比如一个对象 dog ,其原本是加的偏向锁,调用 dog.hashCode() 后,其会失去偏向锁
  2. 当有其它线程使用偏向锁对象时,会将偏向锁升级为轻量级锁
  3. 调用 wait/notify,需要申请 Monitor,进入 WaitSet

4.3.4 批量重偏向

如果对象虽然被多个线程访问,但没有竞争,这时偏向了线程 T1 的对象仍有机会重新偏向 T2,重偏向会重置对象的 Thread ID。当撤销偏向锁阈值超过 20 次后,jvm 会这样觉得,我是不是偏向错了呢,于是会在给这些对象加锁时重新偏向至加锁线程。

测试代码如下:

@Slf4j(topic = "c.TestBiased")
public class Test1 {public static void main(String[] args) {Vector<Dog> list = new Vector<>();Thread t1 = new Thread(() -> {for (int i = 0; i < 30; i++) {Dog d = new Dog();list.add(d);synchronized (d) {log.debug(i + "\t" + ClassLayout.parseInstance(d).toPrintable());}}synchronized (list) {list.notify();}}, "t1");t1.start();Thread t2 = new Thread(() -> {synchronized (list) {try {list.wait();} catch (InterruptedException e) {e.printStackTrace();}}log.debug("===============> ");for (int i = 0; i < 30; i++) {Dog d = list.get(i);log.debug(i + "\t" + ClassLayout.parseInstance(d).toPrintable());synchronized (d) {log.debug(i + "\t" + ClassLayout.parseInstance(d).toPrintable());}log.debug(i + "\t" + ClassLayout.parseInstance(d).toPrintable());}}, "t2");t2.start();}}class Dog {}

在上面的代码中,我们首先在t1中将30个对象创建并且加入到了list中,然后t2等待list将对象全部加入,此时的30个对象是带有偏向锁的,之后t2调用这30个对象,调用前20个对象的时候,偏向锁转为轻量级锁,之后由于批量重偏向的作用,会将第20个对象及以后的对象都改为偏向锁,所以,t2线程打印出的对象头中,前20个对象的末尾都是001,后10个对象的末尾是101。

4.3.5 批量撤销

如果对象被多个线程访问,但没有竞争,这时偏向了线程 T1 的对象仍有机会重新偏向 T2,重偏向会重置对象的 Thread ID。当撤销偏向锁阈值超过 40 次后,JVM 会觉得自己确实偏向错了,根本就不该偏向,于是整个类的所有对象都会变为不可偏向的,新建的对象也是不可偏向的

测试如下:

@Slf4j(topic = "c.TestBiased")
public class Test1 {static Thread t1,t2,t3;public static void main(String[] args) throws InterruptedException {test4();}private static void test4() throws InterruptedException {Vector<Dog> list = new Vector<>();int loopNumber = 39;t1 = new Thread(() -> {for (int i = 0; i < loopNumber; i++) {Dog d = new Dog();list.add(d);synchronized (d) {log.debug(i + "\t" + ClassLayout.parseInstance(d).toPrintable());}}LockSupport.unpark(t2);}, "t1");t1.start();t2 = new Thread(() -> {LockSupport.park();log.debug("===============> ");for (int i = 0; i < loopNumber; i++) {Dog d = list.get(i);log.debug(i + "\t" + ClassLayout.parseInstance(d).toPrintable());synchronized (d) {log.debug(i + "\t" + ClassLayout.parseInstance(d).toPrintable());}log.debug(i + "\t" + ClassLayout.parseInstance(d).toPrintable());}LockSupport.unpark(t3);}, "t2");t2.start();t3 = new Thread(() -> {LockSupport.park();log.debug("===============> ");for (int i = 0; i < loopNumber; i++) {Dog d = list.get(i);log.debug(i + "\t" + ClassLayout.parseInstance(d).toPrintable());synchronized (d) {log.debug(i + "\t" + ClassLayout.parseInstance(d).toPrintable());}log.debug(i + "\t" + ClassLayout.parseInstance(d).toPrintable());}}, "t3");t3.start();t3.join();log.debug(ClassLayout.parseInstance(new Dog()).toPrintable());}
}class Dog {}

上面的示例中,创建对象的个数达到了39个,这里使用了unpack和pack来进行线程的等待,在t2中使用 LockSupport.park() 等待t1的39个对象创建完成,之后t2调用其中的对象,然后前19个对象进行偏向锁转为轻量级锁,偏向锁撤销了19次,之后的19个对象由于批量重偏向的作用,重新加上偏向锁。然后t3调用list中的对象时由于,前19个对象已经是轻量级锁了,无需操作,后面20个对象由于批量重偏向膨胀成轻量级锁,一共撤销了39次偏向锁,最后重新new一个对象的时候,是第40次,此时触发批量撤销,因此,最后new出来的对象是轻量级锁。

5. synchronized优化

5.1 自旋锁

重量级锁竞争时,尝试获取锁的线程不会立即阻塞,可以使用自旋(默认 10 次)来进行优化,采用循环的方式去尝试获取锁

注意:

  • 自旋占用 CPU 时间,单核 CPU 自旋就是浪费时间,因为同一时刻只能运行一个线程,多核 CPU 自旋才能发挥优势
  • 自旋失败的线程会进入阻塞状态

优点:不会进入阻塞状态,减少线程上下文切换的消耗

缺点:当自旋的线程越来越多时,会不断的消耗 CPU 资源

自旋锁情况:

  • 自旋成功的情况:
    在这里插入图片描述

  • 自旋失败的情况:
    在这里插入图片描述

自旋锁说明:

  • 在 Java 6 之后自旋锁是自适应的,比如对象刚刚的一次自旋操作成功过,那么认为这次自旋成功的可能性会高,就多自旋几次;反之,就少自旋甚至不自旋,比较智能
  • Java 7 之后不能控制是否开启自旋功能,由 JVM 控制

自旋锁实现如下:

//手写自旋锁
public class SpinLock {// 泛型装的是Thread,原子引用线程AtomicReference<Thread> atomicReference = new AtomicReference<>();public void lock() {Thread thread = Thread.currentThread();System.out.println(thread.getName() + " come in");//开始自旋,期望值为null,更新值是当前线程while (!atomicReference.compareAndSet(null, thread)) {Thread.sleep(1000);System.out.println(thread.getName() + " 正在自旋");}System.out.println(thread.getName() + " 自旋成功");}public void unlock() {Thread thread = Thread.currentThread();//线程使用完锁把引用变为nullatomicReference.compareAndSet(thread, null);System.out.println(thread.getName() + " invoke unlock");}public static void main(String[] args) throws InterruptedException {SpinLock lock = new SpinLock();new Thread(() -> {//占有锁lock.lock();Thread.sleep(10000); //释放锁lock.unlock();},"t1").start();// 让main线程暂停1秒,使得t1线程,先执行Thread.sleep(1000);new Thread(() -> {lock.lock();lock.unlock();},"t2").start();}
}

5.2 锁消除

锁消除是指对于被检测出不可能存在竞争的共享数据的锁进行消除,这是 JVM 即时编译器的优化

锁消除主要是通过逃逸分析来支持,如果堆上的共享数据不可能逃逸出去被其它线程访问到,那么就可以把它们当成私有数据对待,也就可以将它们的锁进行消除(同步消除:JVM 逃逸分析)

比如下面的代码:

@Fork(1)
@BenchmarkMode(Mode.AverageTime)
@Warmup(iterations=3)
@Measurement(iterations=5)
@OutputTimeUnit(TimeUnit.NANOSECONDS)
public class MyBenchmark {static int x = 0;@Benchmarkpublic void a() throws Exception {x++;}@Benchmark// JIT  即时编译器public void b() throws Exception {Object o = new Object();synchronized (o) {x++;}}
}

上面的代码其实需不需要锁都行,JIT就会对上面的代码编译进行优化,将锁给去掉,提高程序的执行效率。

5.3 锁粗化

对相同对象多次加锁,导致线程发生多次重入,频繁的加锁操作就会导致性能损耗,可以使用锁粗化方式优化

如果虚拟机探测到一串的操作都对同一个对象加锁,将会把加锁的范围扩展(粗化)到整个操作序列的外部

  • 一些看起来没有加锁的代码,其实隐式的加了很多锁:

    public static String concatString(String s1, String s2, String s3) {return s1 + s2 + s3;
    }
    
  • String 是一个不可变的类,编译器会对 String 的拼接自动优化。在 JDK 1.5 之前,转化为 StringBuffer 对象的连续 append() 操作,每个 append() 方法中都有一个同步块

    public static String concatString(String s1, String s2, String s3) {StringBuffer sb = new StringBuffer();sb.append(s1);sb.append(s2);sb.append(s3);return sb.toString();
    }
    

扩展到第一个 append() 操作之前直至最后一个 append() 操作之后,只需要加锁一次就可以

5.4 多把锁

比如一间大屋子有两个功能睡觉、学习,互不相干。现在一个人要学习,一个人要睡觉,如果只用一间屋子,那么一个人学习的时候另一个人就不能睡觉,一个人睡觉的时候另一个人就不能学习,并发度是很低的。

解决问题的方法就是划分多个房间,比如将一间屋子划分为书房和卧室,睡觉的人分去卧室,学习的人去书房就行,这就是将锁的粒度细分。

将锁的粒度细分:

  • 好处,是可以增强并发度
  • 坏处,如果一个线程需要同时获得多把锁,就容易发生死锁

代码示例如下,即像下面的代码一样分为两个对象分别上锁:

public static void main(String[] args) {BigRoom bigRoom = new BigRoom();new Thread(() -> { bigRoom.study(); }).start();new Thread(() -> { bigRoom.sleep(); }).start();
}
class BigRoom {private final Object studyRoom = new Object();private final Object sleepRoom = new Object();public void sleep() throws InterruptedException {synchronized (sleepRoom) {System.out.println("sleeping 2 小时");Thread.sleep(2000);}}public void study() throws InterruptedException {synchronized (studyRoom) {System.out.println("study 1 小时");Thread.sleep(1000);}}
}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/757702.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

【链表】Leetcode 2. 两数相加【中等】

两数相加 给你两个 非空 的链表&#xff0c;表示两个非负的整数。它们每位数字都是按照 逆序 的方式存储的&#xff0c; 并且每个节点只能存储 一位 数字。请你将两个数相加&#xff0c;并以相同形式返回一个表示和的链表。你可以假设除了数字 0 之外&#xff0c;这两个数都不…

Redis数据结构对象中的对象共享、对象的空转时长

对象共享 概述 除了用于实现引用计数内存回收机制之外&#xff0c;对象的引用计数属性还带有对象共享的作用。 在Redis中&#xff0c;让多个键共享同一个值对象需要执行以下两个步骤: 1.将数据库键的值指针指向一个现有的值对象2.将被共享的值对象的引用计数增一 目前来说…

pytorch 实现线性回归(Pytorch 03)

一 从零实现线性回归 1.1 生成训练数据 原始 计算公式&#xff0c; 我们先使用该公式生成一批数据&#xff0c;然后使用 结果数据去计算 计算 w1, w2 和 b。 %matplotlib inline import random import torch from d2l import torch as d2ldef synthetic_data(w, b, num_ex…

敏捷开发最佳实践:质量维度实践案例之接口级自动化测试

本次分享我们将继续给大家带来全新的质量维度实践案例&#xff1a;接口级自动化测试。 本实践节选自《2022中国企业敏捷实践白皮书》&#xff0c;分享者为查俊&#xff0c;是来自腾讯的高级研发项目经理。 问题&#xff1a; 版本持续迭代&#xff0c;关键路径上的场景持续增…

C#面:简述 var 和 dynamic

var 关键字&#xff1a; var 关键字是在编译时进行类型推断的。也就是说&#xff0c;编译器会根据变量的初始化表达式来确定变量的类型&#xff0c;并在编译时将其替换为实际的类型。var 关键字只能用于局部变量&#xff0c;不能用于字段、方法参数或返回类型。var 关键字声明…

基于springboot+vue的餐饮管理系统

博主主页&#xff1a;猫头鹰源码 博主简介&#xff1a;Java领域优质创作者、CSDN博客专家、阿里云专家博主、公司架构师、全网粉丝5万、专注Java技术领域和毕业设计项目实战&#xff0c;欢迎高校老师\讲师\同行交流合作 ​主要内容&#xff1a;毕业设计(Javaweb项目|小程序|Pyt…

抖音商城小店电话采集使用教程

下面是一个简单的Python代码示例&#xff0c;用于抓取抖音商城小店的电话号码&#xff1a; import requests import jsondef get_phone_numbers(url):headers {User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/58.0.3…

Java学习笔记21——使用JDBC访问MySQL数据库

JDBC&#xff08;Java Database Connectivity&#xff0c;Java数据库连接&#xff09;是应用程序编程借口&#xff08;API&#xff09;&#xff0c;描述了一套访问关系数据库的标准Java类库。可以在程序中使用这些API&#xff0c;连接到关系数据库&#xff0c;执行SQL语句&…

IDEA Git恢复DropCommit删除的提交

刚刚Dorp commit了&#xff0c;本地代码也被删除了&#xff0c;如何恢复呢&#xff0c; 从项目中登录git&#xff0c;找到刚刚的commit代码&#xff0c;如下所示&#xff1a;输入命令git reflog 复制代码&#xff0c;到idea中&#xff0c;打开GIt&#xff0c;找到RESET HEAD, …

rust学习笔记(8-12)

8 集合 Rust 标准库中包含一系列被称为 集合&#xff08;collections&#xff09;的非常有用的数据结构。大部分其他数 据类型都代表一个特定的值&#xff0c;不过集合可以包含多个值。不同于内建的数组和元组类型&#xff0c;这些 集合指向的数据是储存在堆上的&#xff0c;这…

Redis:什么是redis?①

一、思想 Redis是一个开源的高性能基于内存key-value数据库&#xff0c;常用作数据库、缓存或消息代理 二、数据类型 String List

初始 Navicat BI 工具

早前&#xff0c;海外 LearnBI online 博主 Adam Finer 对 Navicat Charts Creator 这款 BI&#xff08;商业智能&#xff09;工具进行了真实的测评。今天&#xff0c;我们来看下他对 Navicat BI 工具的初始之感&#xff0c;希望这能给用户一些启发与建议。LearnBI online 作为…

《计算机考研精炼1000题》为你考研之路保驾护航

创作背景 在这个充满挑战与竞争的时代&#xff0c;每一位考生在备战研究生考试的过程中&#xff0c;都希望通过更多符合考纲要求的练习题来提高自己的知识和技能。为了满足这一需求&#xff0c;我们精心策划和编辑了这本《计算机考研精炼1000题》。在考研政治和考研数学领域&a…

springboot283图书商城管理系统

图书商城管理系统 摘 要 现代经济快节奏发展以及不断完善升级的信息化技术&#xff0c;让传统数据信息的管理升级为软件存储&#xff0c;归纳&#xff0c;集中处理数据信息的管理方式。本图书商城管理系统就是在这样的大环境下诞生&#xff0c;其可以帮助管理者在短时间内处理…

耳机壳UV树脂制作私模定制耳塞需要什么样的设备和技术?

制作私模定制耳塞需要使用到一些特定的设备和技术&#xff0c;包括但不限于以下内容&#xff1a; 耳模制作工具&#xff1a;用于获取用户耳型的耳模制作工具&#xff0c;如硅胶、橡皮泥等。需要使用熟练的手法和技术&#xff0c;确保耳模的准确性和稳定性。UV树脂&#xff1a;…

社交变革:探索Facebook的魔力

社交媒体平台的崛起已经改变了我们与世界的交互方式&#xff0c;而Facebook作为其中的巨头&#xff0c;其影响力和魔力更是不可忽视。本文将深入探讨Facebook如何引领社交变革&#xff0c;并探索其背后的魔力所在。 连接世界的纽带 Facebook的独特之处在于它作为一个社交平台&…

CRON 定时任务

检测是否安装了 cron systemctl status crond 如果没有安装使用 sudo yum install cronie 编辑 crontab -e * * * * * php /path/your.php Esc键 然后输入 :q 退出 :wq 保存并退出 第一个 * 表示分钟&#xff0c;表示每分钟执行一次。第二个 * 表示小时&#xff0c;表示每…

PI+重复控制在逆变器控制中的应用

当前现状与困境 众说周知&#xff0c;电网是以工频周期跳变的交流电&#xff0c;具有天然的周期特性&#xff0c;当电网接入RLC不同负载时&#xff0c;电网的电压谐波也会呈现周期性的波动。此时对于光伏逆变器来说&#xff0c;工作环境是不太友好的&#xff0c;所以怎么去抑制…

「滚雪球学Java」:安全(章节汇总)

&#x1f3c6;本文收录于「滚雪球学Java」专栏&#xff0c;专业攻坚指数级提升&#xff0c;助你一臂之力&#xff0c;带你早日登顶&#x1f680;&#xff0c;欢迎大家关注&&收藏&#xff01;持续更新中&#xff0c;up&#xff01;up&#xff01;up&#xff01;&#xf…

【视频异常检测】Diversity-Measurable Anomaly Detection 论文阅读

Diversity-Measurable Anomaly Detection 论文阅读 Abstract1. Introduction2. Related Work3. Diversity-Measurable Anomaly Detection3.1. The framework3.2. Information compression module3.3. Pyramid deformation module3.4. Foreground-background selection3.5. Trai…