Synchronized使用及核心原理

Synchronized使用及核心原理Synchronized是Java中解决并发问题的一种最常用的方法,也是最简单的一种方法。本文将从其使用场景及实现原理上做详细的分析。

大家好,欢迎来到IT知识分享网。

Synchronized是Java中解决并发问题的一种最常用的方法,也是最简单的一种方法。本文将从其使用场景及实现原理上做详细的分析。

Synchronized 的使用

Synchronized的作用主要有三个:

  • 原子性:确保线程互斥的访问同步代码;
  • 可见性:保证共享变量的修改能够及时可见,其实是通过Java内存模型中的 “对一个变量unlock操作之前,必须要同步到主内存中;如果对一个变量进行lock操作,则将会清空工作内存中此变量的值,在执行引擎使用此变量前,需要重新从主内存中load操作或assign操作初始化变量值” 来保证的;
  • 有序性:有效解决重排序问题,即 “一个unlock操作先行发生(happen-before)于后面对同一个锁的lock操作”;

在Java世界里,一切皆为对象。Java有两种对象:Object实例对象和Class对象。Synchronized可以把任何一个非null对象作为”锁”,为了以示区分,将Object对象的监视锁称之对象锁,将Class对象的监视锁称之类锁。

【对象锁】:无论synchronized关键字加在方法上还是对象上,如果它作用的对象是非静态的,则它取得的锁是对象;

【类锁】:如果synchronized作用的对象是一个静态方法或一个类,则它取得的锁是对类,该类所有的对象同一把锁。

1)每个对象只有一个锁(lock)与之相关联,谁拿到这个锁谁就可以运行它所控制的那段代码。

2)一把锁只能同时被一个线程获取,没有获得锁的线程只能等待;
3)每个实例都对应有自己的一把锁(this),不同实例之间互不影响;例外:锁对象是*.class以及 synchronized修饰的是 static方法的时候,所有对象共用同一把锁;
4)synchronized修饰的方法,无论方法正常执行完毕还是抛出异常,都会释放锁;

(1)对象锁

包括方法锁(默认锁对象为this,当前实例对象)和同步代码块锁(手动指定锁定对象,也可以是this,也可以是自定义的锁)。

方法锁形式: synchronized修饰普通方法,锁对象默认为this

public class SynchronizedObjectLock implements Runnable { static SynchronizedObjectLock instence = new SynchronizedObjectLock(); @Override public void run() { method(); } public synchronized void method() { System.out.println("我是线程" + Thread.currentThread().getName()); try { Thread.sleep(5000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName() + "结束"); } public static void main(String[] args) { Thread t1 = new Thread(instence); Thread t2 = new Thread(instence); t1.start(); t2.start(); } }

输出结果:

我是线程Thread-0 Thread-0结束 我是线程Thread-1 Thread-1结束

线程t1和线程t2同时抢占实例对象instence,同一时刻只能有一个线程获取锁,因此,两个线程顺序执行。

同步代码块形式-两个线程使用同一把锁:

public class SynchronizedObjectLock implements Runnable { static SynchronizedObjectLock instence = new SynchronizedObjectLock(); @Override public void run() { // 同步代码块形式——锁为this,两个线程使用的锁是一样的,线程1必须要等到线程0释放了该锁后,才能执行 synchronized (this) { System.out.println("我是线程" + Thread.currentThread().getName()); try { Thread.sleep(3000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName() + "结束"); } } public static void main(String[] args) { Thread t1 = new Thread(instence); Thread t2 = new Thread(instence); t1.start(); t2.start(); } }

输出结果:

我是线程Thread-0 Thread-0结束 我是线程Thread-1 Thread-1结束 

与方法锁一致,线程t1和t2同时抢占实例对象instence,顺序执行。

同步代码块形式-两个线程使用不同的锁:

public class SynchronizedObjectLock implements Runnable { static SynchronizedObjectLock instence = new SynchronizedObjectLock(); // 创建2把锁 Object block1 = new Object(); Object block2 = new Object(); @Override public void run() { // 这个代码块使用的是第一把锁,当他释放后,后面的代码块由于使用的是第二把锁,因此可以马上执行 synchronized (block1) { System.out.println("block1锁,我是线程" + Thread.currentThread().getName()); try { Thread.sleep(3000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("block1锁,"+Thread.currentThread().getName() + "结束"); } synchronized (block2) { System.out.println("block2锁,我是线程" + Thread.currentThread().getName()); try { Thread.sleep(3000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("block2锁,"+Thread.currentThread().getName() + "结束"); } } public static void main(String[] args) { Thread t1 = new Thread(instence); Thread t2 = new Thread(instence); t1.start(); t2.start(); } }

输出结果:

block1锁,我是线程Thread-0 block1锁,Thread-0结束 block2锁,我是线程Thread-0  // 可以看到当第一个线程在执行完第一段同步代码块之后,第二个同步代码块可以马上得到执行,因为他们使用的锁不是同一把 block1锁,我是线程Thread-1 block2锁,Thread-0结束 block1锁,Thread-1结束 block2锁,我是线程Thread-1 block2锁,Thread-1结束

虽然是同步代码块,但两个线程分别使用block1和block2两把锁,因此,线程间互不影响。

(2)类锁

指 synchronize修饰静态的方法或指定锁对象为Class对象,synchronize修饰静态方法。

synchronize修饰静态方法

public class SynchronizedObjectLock implements Runnable { static SynchronizedObjectLock instence1 = new SynchronizedObjectLock(); static SynchronizedObjectLock instence2 = new SynchronizedObjectLock(); @Override public void run() { method(); } // synchronized用在静态方法上,默认的锁就是当前所在的Class类,所以无论是哪个线程访问它,需要的锁都只有一把 public static synchronized void method() { System.out.println("我是线程" + Thread.currentThread().getName()); try { Thread.sleep(3000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName() + "结束"); } public static void main(String[] args) { Thread t1 = new Thread(instence1); Thread t2 = new Thread(instence2); t1.start(); t2.start(); } }

输出结果:

我是线程Thread-0 Thread-0结束 我是线程Thread-1 Thread-1结束

synchronized 指定锁对象为 Class对象

public class SynchronizedObjectLock implements Runnable { static SynchronizedObjectLock instence1 = new SynchronizedObjectLock(); static SynchronizedObjectLock instence2 = new SynchronizedObjectLock(); @Override public void run() { // 所有线程需要的锁都是同一把 synchronized(SynchronizedObjectLock.class){ System.out.println("我是线程" + Thread.currentThread().getName()); try { Thread.sleep(3000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName() + "结束"); } } public static void main(String[] args) { Thread t1 = new Thread(instence1); Thread t2 = new Thread(instence2); t1.start(); t2.start(); } }

输出结果:

我是线程Thread-0 Thread-0结束 我是线程Thread-1 Thread-1结束

上述两种情况,虽然都是创建两个实例instence1和instence2,但锁是当前所在的Class类,因为Class数据存在于永久代,因此静态方法锁相当于该类的一个全局锁,两个线程顺序执行。

加锁和释放锁的原理

synchronized的底层实现是完全依赖JVM虚拟机的,所以谈synchronized的底层实现,就不得不谈数据在JVM内存的存储:Java对象头,以及Monitor对象监视器

(1)Java对象头

在JVM虚拟机中,对象在内存中的存储布局,可以分为三个区域:对象头、实例数据、对齐填充。

1)实例数据:存放类的属性数据信息,包括父类的属性信息;

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

3)对象头:Java对象头一般占有2个机器码(在32位虚拟机中,1个机器码等于4字节,也就是32bit,在64位虚拟机中,1个机器码是8个字节,也就是64bit),但是 如果对象是数组类型,则需要3个机器码,因为JVM虚拟机可以通过Java对象的元数据信息确定Java对象的大小,但是无法从数组的元数据来确认数组的大小,所以用一块来记录数组长度。

Synchronized使用及核心原理

对象在内存中的布局

当线程访问同步块时首先需要获得锁并把相关信息存储在对象头中。所以 wait、notify、notifyAll 这些方法为什么被设计在 Object 中或许你已经找到答案了。

Hotspot 有两种对象头:

  • 数组类型,使用 arrayOopDesc 来描述对象头
  • 其它,使用 instanceOopDesc 来描述对象头

对象头由两部分组成

  • Mark Word:存储自身的运行时数据,例如 HashCode、GC 年龄、锁状态标志、线程持有的锁、偏向线程 ID、偏向时间戳等,是实现轻量级锁和偏向锁的关键。
  • Class Pointer:类型指针指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例。

(2)Monitor

任何一个对象都有一个Monitor与之关联,当且一个Monitor被持有后,它将处于锁定状态。Synchronized在JVM里的实现都是 基于进入和退出Monitor对象来实现方法同步和代码块同步,虽然具体实现细节不一样,但是都可以通过成对的MonitorEnter和MonitorExit指令来实现。

Monitor:可以把它理解为一个同步工具,也可以描述为一种同步机制,通常被描述为一个对象。

与一切皆对象一样,所有的Java对象是天生的Monitor,每一个Java对象都有成为Monitor的潜质,因为在Java的设计中 ,每一个Java对象天生就带了一把看不见的锁,它叫做内部锁或者Monitor锁。也就是通常说Synchronized的对象锁,MarkWord锁标识位为10,其中指针指向的是Monitor对象的起始地址。

我们通过一段简单的代码分析monitorenter 与 monitorexit:

public class SynchronizedDemo2 { Object object = new Object(); public void method1() { synchronized (object) { } } }

使用 javac命令进行编译生成 .class文件,然后使用 javap命令反编译查看.class文件的信息:

Synchronized使用及核心原理

反编译信息

monitorenter 和 monitorexit指令,会让对象在执行,使其锁计数器加1或者减1。每一个对象在同一时间只与一个 monitor(锁)相关联,而一个 monitor在同一时间只能被一个线程获得,一个对象在尝试获得与这个对象相关联的 Monitor锁的所有权的时候,monitorenter指令会发生如下三种情况之一:

【1】monitor计数器为0,意味着目前还没有被获得,那这个线程就会立刻获得然后把锁计数器+1,一旦+1,别的线程再想获取,就需要等待;

【2】如果这个 monitor已经拿到了这个锁的所有权,又重入了这把锁,那锁计数器就会累加,变成2,并且随着重入的次数,会一直累加;

【3】这把锁已经被别的线程获取了,等待锁释放;

monitorexit 指令:释放对于 monitor的所有权,释放过程很简单,就是將 monitor的计数器减1,如果减完以后,计数器不是0,则代表刚才是重入进来的,当前线程还继续持有这把锁的所有权,如果计数器变成0,则代表当前线程不再拥有该 monitor的所有权,即释放锁。下图表达了对象,对象监视器,同步队列以及执行线程状态之间的关系:

Synchronized使用及核心原理

该图可以看出,任意线程对 Object的访问,首先要获得 Object的监视器,如果获取失败,该线程就进入同步状态,线程状态变为BLOCKED,当 Object的监视器占有者释放后,在同步队列中得线程就会有机会重新获取该监视器。

Synchronized先天具有重入性。每个对象拥有一个计数器,当线程获取该对象锁后,计数器就会加一,释放锁后就会将计数器减一。

锁的状态

在 JDK1.6 版本之前,所有的 Java 内置锁都是重量级锁。重量级锁会造成 CPU 在用户态与核心态之间频繁切换,所以代价高、效率低。JDK1.6 版本为了减少获得锁和释放锁所带来的性能消耗,引入了“偏向锁”和“轻量级锁”实现。所以,在 JDK1.6 版本里内置锁一共有四种状态:无锁状态、偏向锁状态、轻量级锁状态、重量级锁状态,这些状态随着竞争情况逐渐升级。内置锁可以升级但不能降级,意味着偏向锁升级成轻量级锁后不能降级成偏向锁。这种能升级却不能降级的策略,其目的是为了提高获得锁和释放锁的效率。

Synchronized使用及核心原理

32位的Mark Word的默认结构

(1)无锁状态

Java 对象刚创建时,还没有任何线程来竞争,说明该对象处于无锁状态(无线程竞争它),这时偏向锁标识位是 0、锁状态 01。

Synchronized使用及核心原理

无锁状态Mark Word《Java高并发核心编程》

(2)偏向锁状态

偏向锁是指一段同步代码一直被一同个线程所访问,那么该线程会自动获取锁,降低获取锁的代价。如果内置锁处于偏向状态,当有一个线程来竞争锁时,先用偏向锁,表示内置锁偏爱这个线程,这个线程要执行该锁关联的同步代码时,不需要再做任何检查和切换。偏向锁在竞争不激烈的情况下,效率非常高。

偏向锁状态的 Mark Word 会记录内置锁自己偏爱的线程 ID,内置锁会将该线程当做自己的熟人。

Synchronized使用及核心原理

偏向锁Mark Word《Java高并发核心编程》

偏向锁是在单线程执行代码块时使用的机制,如果在多线程并发的环境下(即线程A尚未执行完同步代码块,线程B发起了申请锁的申请),则一定会转化为轻量级锁或者重量级锁。

偏向锁的获取和释放过程

Synchronized使用及核心原理

偏向锁 – aspirant博客

当一个线程访问同步块并获取锁时,会在对象头和栈帧中的锁记录里存储锁偏向的线程ID,以后该线程进入和退出同步块时不需要花费CAS操作来争夺锁资源,只需要检查是否为偏向锁、锁标识为以及ThreadID即可,处理流程如下:

  • (1)检测Mark Word是否为可偏向状态,即是否为偏向锁1,锁标识位为01;
  • (2)若为可偏向状态,则测试线程ID是否为当前线程ID,如果是,则执行步骤(5),否则执行步骤(3);
  • (3)如果测试线程ID不为当前线程ID,则通过CAS操作竞争锁,竞争成功,则将Mark Word的线程ID替换为当前线程ID,否则执行线程(4);
  • (4)通过CAS竞争锁失败,证明当前存在多线程竞争情况,当到达全局安全点,获得偏向锁的线程被挂起,偏向锁升级为轻量级锁,然后被阻塞在安全点的线程继续往下执行同步代码块;
  • (5)执行同步代码块;

偏向锁的释放采用了 一种只有竞争才会释放锁的机制,线程是不会主动去释放偏向锁,需要等待其他线程来竞争。偏向锁的撤销需要 等待全局安全点(这个时间点是上没有正在执行的代码)。其步骤如下:

  • (1)暂停拥有偏向锁的线程;
  • (2)判断锁对象是否还处于被锁定状态,否,则恢复到无锁状态(01),以允许其余线程竞争。是,则挂起持有锁的当前线程,并将指向当前线程的锁记录地址的指针放入对象头Mark Word,升级为轻量级锁状态(00),然后恢复持有锁的当前线程,进入轻量级锁的竞争模式;

注意:此处将当前线程挂起再恢复的过程中并没有发生锁的转移,仍然在当前线程手中,只是穿插了个 “将对象头中的线程ID变更为指向锁记录地址的指针” 这么个事。

偏向锁如何来减少不必要的CAS操作?

现在几乎所有的锁都是可重入的,即已经获得锁的线程可以多次锁住/解锁监视对象,按照之前的HotSpot设计,每次加锁/解锁都会涉及到一些CAS操作(比如对等待队列的CAS操作),CAS操作会延迟本地调用,因此偏向锁的想法是 一旦线程第一次获得了监视对象,之后让监视对象“偏向”这个线程,之后的多次调用则可以避免CAS操作,说白了就是置个变量,如果发现为true则无需再走各种加锁/解锁流程。

轻量级锁是为了在线程交替执行同步块时提高性能,而偏向锁则是在只有一个线程执行同步块时进一步提高性能。

(3)轻量级锁状态

当有两个线程开始竞争这个锁对象,情况发生变化了,不再是偏向(独占)锁了,锁会升级为轻量级锁,两个线程公平竞争,哪个线程先占有锁对象,锁对象的 Mark Word 就指向哪个线程的栈帧中的锁记录。轻量级锁状态下对象的 Mark Word 具体如下图所示。

Synchronized使用及核心原理

轻量级锁Mark Work《Java高并发核心编程》

当关闭偏向锁功能或者多个线程竞争偏向锁导致偏向锁升级为轻量级锁,则会尝试获取轻量级锁,其步骤如下:

  • (1)在线程进入同步块时,如果同步对象锁状态为无锁状态(锁标志位为“01”状态,是否为偏向锁为“0”),虚拟机首先将在当前线程的栈帧中建立一个名为锁记录(Lock Record)的空间,用于存储锁对象目前的Mark Word的拷贝,官方称之为 Displaced Mark Word。
  • (2)拷贝对象头中的Mark Word复制到锁记录(Lock Record)中;
  • (3)拷贝成功后,虚拟机将使用CAS操作尝试将对象Mark Word中的Lock Word更新为指向当前线程Lock Record的指针,并将Lock record里的owner指针指向object mark word。如果更新成功,则执行步骤(4),否则执行步骤(5);
  • (4)如果这个更新动作成功了,那么当前线程就拥有了该对象的锁,并且对象Mark Word的锁标志位设置为“00”,即表示此对象处于轻量级锁定状态
  • (5)如果这个更新操作失败了,虚拟机首先会检查对象Mark Word中的Lock Word是否指向当前线程的栈帧,如果是,就说明当前线程已经拥有了这个对象的锁,那就可以直接进入同步块继续执行。否则说明多个线程竞争锁,进入自旋执行(3),若自旋结束时仍未获得锁,轻量级锁就要膨胀为重量级锁,锁标志的状态值变为“10”,Mark Word中存储的就是指向重量级锁(互斥量)的指针,当前线程以及后面等待锁的线程也要进入阻塞状态。

轻量级锁的释放也是通过CAS操作来进行的,主要步骤如下:

  • (1)通过CAS操作尝试把线程中复制的Displaced Mark Word对象替换当前的Mark Word;
  • (2)如果替换成功,整个同步过程就完成了,恢复到无锁状态(01);
  • (3)如果替换失败,说明有其他线程尝试过获取该锁(此时锁已膨胀),那就要在释放锁的同时,唤醒被挂起的线程;
Synchronized使用及核心原理

轻量级锁 – aspirant博客

对于轻量级锁,其性能提升的依据是 “对于绝大部分的锁,在整个生命周期内都是不会存在竞争的”,如果打破这个依据则除了互斥的开销外,还有额外的CAS操作,因此在有多线程竞争的情况下,轻量级锁比重量级锁更慢。

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

(4)重量级锁状态

重量级锁会让其他申请的线程之间进入阻塞,性能降低。重量级锁也就叫做同步锁,这个锁 对象 Mark Word 再次发生变化,会指向一个监视器(Monitor)对象,该监视器对象用集合的形 式,来登记和管理排队的线程。重量级锁状态下对象的 Mark Word 具体如下图所示。

Synchronized使用及核心原理

重量级锁Mark Word《Java高并发核心编程》

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

(5)重量级锁、轻量级锁和偏向锁之间转换

Synchronized使用及核心原理

各种状态锁的优缺点对比

优点

缺点

使用场景

偏向锁

加锁和解锁不需要CAS操作,没有额外的性能消耗,和执行非同步方法相比仅存在纳秒级的差距

如果线程间存在锁竞争,会带来额外的锁撤销的消耗

适用于只有一个线程访问同步快的场景

轻量级锁

竞争的线程不会阻塞,提高了响应速度

如线程始终得不到所竞争的锁,使用自旋会消耗CPU性能

追求响应时间,同步快执行速度非常快

重量级锁

线程竞争不适用自旋,不会消耗CPU

线程阻塞,响应时间缓慢,在多线程下,频繁的获取释放锁,会带来巨大的性能消耗

追求吞吐量,同步快执行速度较长

锁优化

由于JVM的Synchronized重量级锁设计操作系统内核态下的互斥锁的使用,因此,其线程阻塞和唤醒都涉及进程在用户态到内核态的频繁切换,导致重量级锁开销大、性能低。而JVM的Synchronized轻量级锁使用CAS(Compare And Swap)进行自旋抢锁,CAS是CPU指令级的原子操作,并处于用户态下,所以JVM轻量级锁的开销较小。

《Java》高并发核心编程

(1)自旋锁

所谓自旋,就是指当一个线程尝试获取某个锁时,如果该锁已被其他线程占用,就一直循环检测锁是否被释放,而不是进入线程挂起或睡眠状态。

自旋锁解决那些问题?

线程的阻塞和唤醒需要CPU从用户态转为内核态,频繁的阻塞和唤醒对CPU来说是一件负担很重的工作,势必会给系统的并发性能带来很大的压力。同时我们发现在许多应用上面,对象锁的锁状态只会持续很短一段时间,为了这一段很短的时间频繁地阻塞和唤醒线程是非常不值得的。

自旋锁早在JDK1.4 中就引入了,只是当时默认时关闭的。在JDK 1.6后默认为开启状态。自旋锁本质上与阻塞并不相同,先不考虑其对多处理器的要求,如果锁占用的时间非常的短,那么自旋锁的性能会非常的好,相反,其会带来更多的性能开销(因为在线程自旋时,始终会占用 CPU的时间片,如果锁占用的时间太长,那么自旋的线程会白白消耗掉CPU资源)。因此自旋等待的时间必须要有一定的限度,如果自选超过了限定的次数仍然没有成功获取到锁,就应该使用传统的方式去挂起线程了,在 JDK定义中,自旋锁默认的自旋次数为10次,用户可以使用参数 -XX:PreBlockSpin来更改。

自旋锁存在的一个问题:如果线程锁在线程自旋刚结束就释放掉了锁,那么是不是有点得不偿失。所以这时候我们需要更加聪明的锁来实现更加灵活的自旋,来提高并发的性能。

如何实现更加灵活的自旋锁?

JDK 1.6引入了更加聪明的自旋锁,即自适应自旋锁。所谓自适应就意味着自旋的次数不再是固定的,它是由前一次在同一个锁上的自旋时间及锁的拥有者的状态来决定。那它如何进行适应性自旋呢?

线程如果自旋成功了,那么下次自旋的次数会更加多,因为虚拟机认为既然上次成功了,那么此次自旋也很有可能会再次成功,那么它就会允许自旋等待持续的次数更多。反之,如果对于某个锁,很少有自旋能够成功,那么在以后要或者这个锁的时候自旋的次数会减少甚至省略掉自旋过程,以免浪费处理器资源。

有了自适应自旋锁,随着程序运行和性能监控信息的不断完善,虚拟机对程序锁的状况预测会越来越准确,虚拟机会变得越来越聪明。

(2)锁消除

为了保证数据的完整性,在进行操作时需要对这部分操作进行同步控制,但是在有些情况下,JVM检测到不可能存在共享数据竞争,这是JVM会对这些同步锁进行锁消除。

锁消除的依据是逃逸分析的数据支持,更多可参考:Java 内存模型详解

如果不存在竞争,为什么还需要加锁呢?所以锁消除可以节省毫无意义的请求锁的时间。变量是否逃逸,对于虚拟机来说需要使用数据流分析来确定,但是对于程序员来说这还不清楚么?在明明知道不存在数据竞争的代码块前加上同步吗?但是有时候程序并不是我们所想的那样?虽然没有显示使用锁,但是在使用一些JDK的内置API时,如StringBuffer、Vector、HashTable等,这个时候会存在隐形的加锁操作。比如StringBuffer的append()方法,Vector的add()方法:

public void vectorTest(){ Vector<String> vector = new Vector<String>(); for(int i = 0 ; i < 10 ; i++){ vector.add(i + ""); } System.out.println(vector); }

在运行这段代码时,JVM可以明显检测到变量vector没有逃逸出方法vectorTest()之外,所以JVM可以大胆地将vector内部的加锁操作消除。

(3)锁粗化

原则上,我们都知道在加同步锁时,尽可能的将同步块的作用范围限制到尽量小的范围(只在共享数据的实际作用域中才进行同步,这样是为了使得需要同步的操作数量尽可能变小。在存在锁同步竞争中,也可以使得等待锁的线程尽早的拿到锁)。

大部分上述情况是完美正确的,但是如果存在连串的一系列操作都对同一个对象反复加锁和解锁,甚至加锁操作时出现在循环体中的,那即使没有线程竞争,频繁地进行互斥同步操作也会导致不必要地性能操作。

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

在上述地连续 append() 操作中就属于这类情况。JVM会检测到这样一连串地操作都是对同一个对象加锁,那么JVM会将加锁同步地范围扩展(粗化)到整个一系列操作的外部,使整个一连串地 append()操作只需要加锁一次就可以了。

免责声明:本站所有文章内容,图片,视频等均是来源于用户投稿和互联网及文摘转载整编而成,不代表本站观点,不承担相关法律责任。其著作权各归其原作者或其出版社所有。如发现本站有涉嫌抄袭侵权/违法违规的内容,侵犯到您的权益,请在线联系站长,一经查实,本站将立刻删除。 本文来自网络,若有侵权,请联系删除,如若转载,请注明出处:https://yundeesoft.com/60850.html

(0)

相关推荐

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注

关注微信