文章目录

  • 1. synchronized介绍
  • 2. synchronized底层原理
  • 3. synchronized锁的膨胀升级过程
  • 4. synchronized锁状态的记录位置
  • 5. synchronized的线程通信
  • 6. System.out.println与synchronized的坑!

1. synchronized介绍

synchronized内置锁是一种对象锁(锁的是对象而非引用),作用的粒度是对象,可以解决并发的原子性、一致性、有序性等问题,是可重入的。

1.1 加锁的方式有三种:

  • ①:加在静态方法上,锁的对象是Class对象,只有一个!
  • ②:加在普通方法上,锁的是当前调用者这个对象,有多个!
  • ③:加在方法内部的代码块上,锁的是括号里的对象!

1.2 synchronized的发展史

在jdk1.6之前,synchronized是一个单一的重量级锁,无论竞争是否激烈,都给当前对象加一个重量级锁,性能比较低。

后来,为了解决加锁性能问题,并发之父 Doug Lee用java语言写了 ReentrantLock,这个可重入锁,不仅可以加锁,而且性能比synchronized高很多。

再后来,oracle公司在jdk1.6版本之后,升级了synchronized。因为他发现没必要所有的线程一进来就加一个重量级锁,大部分加偏向锁和轻量级锁就可以满足需求,所以在以前的synchronized的基础上增加了由 无锁 => 偏向锁 => 轻量级锁 => 重量级锁的膨胀升级过程,性能方面和李二狗的ReentrantLock差不多。

1.3 synchronized存在的意义

  1. 首先要说一下多线程存在的意义:可以并发的执行代码。但这个是有前提的,这个前提是每个线程不会把cpu时间占满,要有空余时间执行其他线程。比如说一段获取磁盘文件的代码,在等待磁盘io的这段时间内,cpu是空闲的,如果是单线程,cpu就会闲着没事干,造成cpu资源浪费。此时可以考虑多线程,让cpu在等待时间中切换到别的线程做别的事,这就是多线程的意义:充分利用cpu!
  2. 但多线程也有其限制,比如多个线程对共享变量的争用或修改,会造成数据混乱,产生bug。为了保证数据的正确性,就诞生了同步机制,可以使用synchronized来保证业务的正确性,但同时牺牲了性能。
  3. sychronized包住的代码,相当于多个线程串行执行了,效率低,这也是同步锁机制的局限性。所以如果用sychronized,要尽量少的包住代码,只包有线程不安全的代码。

2. synchronized底层原理

synchronized是基于JVM内置锁实现,通过同步对象的Monitor(监视器锁)实现,监视器锁Monitor是通过JVM调用操作系统的互斥原语mutex来实现。互斥原语mutex规定:被阻塞的线程会被挂起、等待重新调度、唤醒,这个过程会导致 用户态 和 内核态 之间来回切换,对性能有较大影响。当然,JVM内置锁在jdk1.6之后版本做了重大的优化,主要有以下几种,通过这些优化手段,synchronized的并发性能已经基本与Lock持平。

  • 锁粗化(Lock Coarsening)
  • 锁消除(Lock Elimination)
  • 轻量级锁(Lightweight Locking)
  • 偏向锁(Biased Locking)
  • 自旋锁(Adaptive Spinning)

从上面synchronized 的加锁方式,我们可以知道:synchronized无论加在方法上,还是加在方法中的同步代码块上,都有一个对象作为锁的,如:synchronized(Object)。这个对象的作用是:jvm使用与这个对象所关联的Monitor监视器锁来控制代码同步! 接下来看一下什么是Monitor监视器锁

Monitor监视器锁

在Java的设计中 ,每一个Java对象自被new出来之后就带了一把看不见的锁,它叫做内部锁或者Monitor锁。也就是通常所说的Synchronized的对象锁。任何一个对象都有一个Monitor与之关联,当且一个Monitor被持有后,它将处于锁定状态。加锁过程如下图所示:

Synchronized在JVM里的实现都是基于进入(monitorenter)和退出(monitorexit)Monitor对象来实现方法同步和代码块同步。synchronized关键字被编译成字节码后会被翻译成monitorenter(进入)monitorexit(退出) 两条指令,分别在同步块逻辑代码的起始位置与结束位置。

monitorenter的执行逻辑

  1. 线程执行monitorenter指令时会尝试获取monitor的所有权
  2. 如果monitor的进入数为0,则该线程进入monitor,然后将进入数设置为1,该线程即为monitor的所有者
  3. 如果线程已经占有该monitor,只是重新进入,则进入monitor的进入数加1,这是synchronized 可重入的体现
  4. 如果其他线程已经占用了monitor,则该线程进入阻塞状态,直到monitor的进入数为0,再重新尝试获取monitor的所有权

monitorexit的执行逻辑

  1. 线程执行monitorexit时,monitor的进入数减1
  2. 如果减1后进入数不为0,说明该线程还有其他重入逻辑,等待重入逻辑执行完毕再次减1,直到减为0,其他被这个monitor阻塞的线程才可以尝试去获取这个 monitor 的所有权。
  3. 如果减1后进入数为0,那线程退出monitor,不再是这个monitor的所有者,其他线程尝试获取这个 monitor 的所有权。

通过上面的描述,我们应该能很清楚的看出Synchronized的实现原理,其实wait/notify等方法也依赖于monitor对象,这就是为什么只有在同步块或者方法中才能调用wait/notify等方法,否则会抛出java.lang.IllegalMonitorStateException的异常的原因。

通过一个同步方法实践上述结论:

public class SynchronizedMethod {// 这是一个同步方法public synchronized void method() {System.out.println("Hello World!");}
}

反编译结果:

        从反编译的结果来看,方法的同步并没有通过指令 monitorentermonitorexit 来完成(理论上其实也可以通过这两条指令来实现,如果是同步块synchronized(Object)反编译时可以看到这两个指令),不过相对于普通方法,其常量池中多了 ACC_SYNCHRONIZED 标示符。

JVM就是根据该标示符来实现方法的同步的:当方法调用时,调用指令将会检查方法的 ACC_SYNCHRONIZED 访问标志是否被设置,如果设置了,执行线程将先获取monitor,获取成功之后才能执行方法体,方法执行完后再释放monitor。在方法执行期间,其他任何线程都无法再获得同一个monitor对象。

3. synchronized锁的膨胀升级过程

在jdk1.6之后synchronized不再只有单一的重量级锁,随着锁的竞争,锁会经历 无锁 => 偏向锁 => 轻量级锁 => 重量级锁的膨胀升级过程。这四种锁状态只能从低到高升级,不会出现锁的降级

  • ①:无锁状态:即还未对线程加锁的状态。

  • ②:偏向锁:经过研究发现,在大多数情况下,锁不仅不存在多线程竞争,而且总是由同一线程多次获得,因此为了减少同一线程反复获取锁(会涉及到一些CAS操作,耗时)的代价而引入偏向锁。偏向锁要求自始至终,使用锁的线程都只有一个。这个线程获得了锁,那么锁就会进入偏向模式(偏向当前线程),此时对象头中的Mark Word的结构也会变为偏向锁的锁标志位。当这个线程再次请求锁时,无需任何操作,即可获取到锁,这样就省去了大量有关锁申请的操作,提高了程序性能。
            所以,对于没有锁竞争的场合,偏向锁有很好的优化效果,毕竟只有一个线程使用CAS申请一次锁。但是对于存在锁竞争的场合,偏向锁就失效了,偏向锁会先升级为轻量级锁,再升级为重量级锁。轻量级锁每次申请、释放锁都至少需要一次CAS,但偏向锁只有初始化时需要一次CAS

  • ③:轻量级锁:轻量级锁是相对于重量级锁而言的。使用轻量级锁时,不需要申请互斥量mutex,仅仅将Mark Word中的部分字节CAS更新指向线程栈中的Lock Record,如果更新成功,则轻量级锁获取成功,记录锁状态为轻量级锁;否则,说明已经有线程获得了轻量级锁,发生了锁竞争,则启动自旋锁优化,如果自旋一定次数后还是没获取到锁,说明竞争激烈,接下来膨胀为重量级锁。

  • ④:重量级锁:重量级锁是依赖对象内部的monitor监视器锁来实现的,而监视器锁monitor直接对应底层操作系统中的互斥量(mutex)。这种同步方式的成本非常高,包括系统调用引起的内核态与用户态切换、线程阻塞造成的线程切换等。因此,后来称这种锁为“重量级锁”。

偏向锁、轻量级锁、重量级锁适用于不同的并发场景:

  • 偏向锁:无实际竞争,且将来只有第一个申请锁的线程会使用锁。
  • 轻量级锁:无实际竞争,多个线程交替使用锁;允许短时间的锁竞争。
  • 重量级锁:有实际竞争,且锁竞争时间长。

问题一:为什么说重量级锁开销大呢?

当系统检查到锁是重量级锁之后,会把等待想要获得锁的线程进行阻塞,被阻塞的线程不会消耗cup。但是阻塞或者唤醒一个线程时,都需要操作系统来帮忙,这就需要从用户态转换到内核态,而转换状态是需要消耗很多时间的,有可能比用户执行代码的时间还要长。 这就是说为什么重量级线程开销很大的。

问题二:什么是锁撤销?

在锁的膨胀升级后,比如由偏向锁升级为轻量级锁,由于偏向锁已失效,接下来就要把已失效的锁撤掉,被称为锁撤销。

锁撤销的开销花费还是挺大的,其大概的过程如下:
        ①:在安全点停止要进行锁撤销的线程
        ②:遍历线程栈,如果存在锁记录的话,需要修复锁记录和Markword,使其变成无锁状态。
        ③:唤醒当前线程,将当前锁升级成轻量级锁。

所以,如果某些同步代码块大多数情况下都是有两个及以上的线程竞争的话,那么偏向锁就会是一种累赘,对于这种情况,我们可以一开始就把偏向锁这个默认功能给关闭

问题三:什么是自旋锁优化?

自旋锁的目标是降低线程切换的成本。发生在轻量级锁竞争失败的情况下,当竞争轻量级锁失败后,虚拟机为了避免线程真实地在操作系统层面直接挂起,会启用自旋锁。

由于轻量级锁天然瞄准不存在锁竞争的场景,如果存在锁竞争但不激烈,仍然可以用自旋锁优化,自旋失败后再膨胀为重量级锁。避免线程直接阻塞挂起。因为挂起需要唤醒,需要用户态和内核态的相互转换,开销挺大的。

因此虚拟机会让当前想要获取锁的线程做几个空循环(这也是称为自旋的原因),一般不会太久,经过若干次循环,如果得到锁就执行,万事大吉。如果还不能获得锁,再将线程在操作系统层面挂起,这就是自旋锁的优化方式。

这种方式确实也是可以提升效率的。最后没办法也就只能升级为重量级锁了。

问题四:什么是锁消除、锁的粗化?

锁消除和锁粗化都是一种锁的优化。

锁消除: JVM在编译期间会对代码进行扫描,去除不可能存在竞争的锁,通过这种方式,节省不必要的请求锁的时间。比如某个方法返回值为void,经过逃逸分析发现其内部的对象存在栈帧中,那么这个对象就不会被其他线程使用,不可能存在共享资源竞争的情景,如果代码在方法内部对这个对象加锁,将毫无意义,JVM会自动将其锁消除。锁消除的依据是逃逸分析的 数据支持。

锁粗化: 方法中如果有多个synchronized修饰的代码块,而且锁的对象都一样,jvm在扫描代码时,会使用锁的粗化对代码进行优化,把多个synchronized修饰的代码块的内容,转移到一个synchronized修饰的代码中,了避免重复加锁释放锁的耗时!

1.6版本后synchronized经过这么多优化,性能已经和ReetrantLock很接近了!

4. synchronized锁状态的记录位置

我们知道synchronized是把锁加在对象上,在锁状态的膨胀升级过程中,这个对象是如何记录锁的状态呢?、

在jvm的堆中,对象的存储布局可以分为三个部分:对象头、实例数据、和对齐填充。对象大小为8字节的整数位,不足的部分回使用对其填充补齐!

对象头又包含两部分:①Mark Word、②Klass Pointer
synchronized锁状态是被记录在每个对象的对象头(Mark Word)中

Mark Word详情图:


不同的锁状态有不同的锁标志位

  • 无锁:001
  • 偏向锁:101
  • 轻量级锁:00
  • 重量级锁:10

通过jol-core依赖包,可以打印synchronized的锁标志位。会发现一个原本无锁的对象,在加了synchronized后,标志位会从 001 ==> 00,也就是从无锁编程轻量级锁! 跳过了偏向锁。

     public static void main(String[] args) {//加锁对象T t = new T();//未出现任何获取锁的时候System.out.println(ClassLayout.parseInstance(t).toPrintable());synchronized (t){// 获取一次锁之后System.out.println(ClassLayout.parseInstance(t).toPrintable());}}

运行结果:

为什么对象加锁后,会直接由无锁跳过偏向锁,直接变为轻量级锁呢?

答:因为jvm在启动时,会延时启动偏向锁,所以对象会先被加上轻量级锁。

为什么偏向锁会延时启动呢?因为jvm启动时,其内部也有很多同步块,也会存在竞争,大部分竞争都需要轻量级锁以上的锁状态!为了避免在启动时频繁的由偏向锁==>轻量级锁==>重量级锁的膨胀升级带来的开销,所以推迟启动了偏向锁,大概延迟4秒左右。如果让线程睡5秒钟,即可打印出偏向锁!

5. synchronized的线程通信

等待方遵循如下原则

  • 1)获取obj对象的锁
  • 2)如果条件不满足,那么调用对象的wait()方法,
  • 3)被通知后仍要检查条件是否满足,如果满足才跳出循环

通知方遵循如下原则。

  • 1)获得obj对象的锁。
  • 2)改变条件。
  • 3)唤醒等待线程。

以一个面试题来举例:有一个容器ArrayList,写两个线程,线程1添加10个元素到容器中, 线程2实现监控元素的个数,当个数到5个时,线程2给出提示并结束

解法一:使用synchronized的wait()、notify()、notifyAll()来实现

public class NotifyWaitV2 {private static volatile List<String> list = new ArrayList<>();private static final Object lock = new Object();public void add() {list.add("freedom");}public int size() {return list.size();}// 开启两个线程public static void main(String[] args) {final NotifyWaitV2 v1 = new NotifyWaitV2();//线程一:不断增加元素,添加到第五个时 执行notifyAll唤醒其他线程Thread t1 = new Thread(new Runnable() {@Overridepublic void run() {try {synchronized (lock) {for (int i = 0; i < 10; i++) {v1.add();System.out.println("当前线程:"+ Thread.currentThread().getName()+ "添加了一个元素!");//                            Thread.sleep(100);if (v1.size() == 5) {System.out.println("当前线程:"+ Thread.currentThread().getName()+ "  发出了notify通知!");lock.notifyAll();}}}} catch (Exception e) {// TODO Auto-generated catch blocke.printStackTrace();}}}, "t1");//线程二:获取锁后,如果元素个数不等于5就阻塞Thread t2 = new Thread(new Runnable() {@Overridepublic void run() {synchronized (lock) {if (v1.size() != 5) {try {lock.wait();} catch (InterruptedException e) {// TODO Auto-generated catch blocke.printStackTrace();}}System.out.println("当前线程:"+ Thread.currentThread().getName()+ " list.size==5退出循环,任务执行完成!");throw new RuntimeException();}}}, "t2");t2.start();try {Thread.sleep(500);} catch (InterruptedException e) {// TODO Auto-generated catch blocke.printStackTrace();}t1.start();}}

运行结果:

当然这个面试题还可以有其他的解法

  • 使用volatile关键字保证可见性:

思路:在容器数组ArrayList上添加volatile关键字保证其可见性,

  • 第一个线程正常for循环添加元素
  • 第二个线程使用While(true)判断集合元素是否等于5
  • 由于使用volatile关键字保证了可见性,所以可不用加锁!
  • 使用CountDownLatch

思路:

  • 由于synchronizednotify在唤醒时需要释放锁,并不解决线程间的实时通信问题
  • 初始化new CountDownLatch(5) ,线程一每添加一个执行一次`countDownLatch.countDown();
  • 线程二使用countDownLatch.await()等待

wait()、notify()、notifyAll() 的使用逻辑

逻辑图如下所示:

6. System.out.println与synchronized的坑!

做一个简单的测试

public class TestOut {private static long timeOut = System.currentTimeMillis() + 1000L;// 不使用 System.out.printlnprivate static void outQuick() {long i = 1L;while (timeOut >= System.currentTimeMillis()) {i++;}System.out.println(i);}// 打印 数字 时间为1秒钟private static void outOnlyNumber() {long i = 1L;while (timeOut >= System.currentTimeMillis()) {System.out.println(i++);}}// 打印 字符串 时间为1秒钟private static void outOnlyString() {long i = 1L;while (timeOut >= System.currentTimeMillis()) {System.out.println(""+i++);}}public static void main(String[] args) {outQuick();//outOnlyNumber();//outOnlyString();}
}

首先测试的没有使用标准输出方法的运算,得到的结果稳定在2亿4千万次到2亿七千万次左右,下面给出几次结果

256908210
241685465
271216750
259256219

第二次测试的打印数字,执行结果大概在17万到25万之间,其中大多数为17万左右

170775
160830
175684
251080

第三次测试打印把数字转成字符然后再输出,执行结果稳定在20万以上

276857
252494
230742
291023

得到以下几条信息

  1. System.out.println()标准输出方法对性能的影响导致执行效率下降了1500倍左右
  2. System.out.println()标准输出方法使用字符串输出时执行效率只下降1000倍左右

为什么会这样呢?

看看源码,发现System.out.println()标准输出方法的执行过程是这样的

/**  * 参数不同会调用不同的构造方法*/public void println(String x) {synchronized (this) {print(x);newLine();}}/**  * 如果是一个对象,则会多一句代码String s = String.valueOf(x);*/public void println(Object x) {String s = String.valueOf(x);// 该方法是一个synchronized的方法,首先打印字符,然后换一行。synchronized (this) {print(s);// newLine()也是一个synchronized的方法newLine();}}/**  * 多出的这一句代码实质上是调用了对象的toString()方法并做了空判断*/public static String valueOf(Object obj) {return (obj == null) ? "null" : obj.toString();}/**  * 也许你以为newLine()方法只是打印一个\n,但是实际上,他却是这样的* 其中textOut.flushBuffer();也是一个synchronized方法* 如果在这个过程中发生了IO中断异常,newLine()方法会中断掉当前线程*/
private void newLine() {try {synchronized (this) {ensureOpen();textOut.newLine();textOut.flushBuffer();charOut.flushBuffer();if (autoFlush)out.flush();}}catch (InterruptedIOException x) {Thread.currentThread().interrupt();}catch (IOException x) {trouble = true;}}/**  * 将输出缓冲区刷新到基础字符流*/void flushBuffer() throws IOException {synchronized (lock) {ensureOpen();if (nextChar == 0)return;out.write(cb, 0, nextChar);nextChar = 0;}}/**  * 打印字符,如果字符不为空的话*/public void print(String s) {if (s == null) {s = "null";}write(s);}/**  * 输出字符的write方法和newLine动作其实是一致的* System.out.println()标准输出方法其实相当于print()方法调用两次*/
private void write(String s) {try {synchronized (this) {ensureOpen();// textOut是BufferedWriter的一个对象,该类继承至Object以及Writer类// 该类是一个抽象类,作用是向字符流中执行写入。textOut.write(s);textOut.flushBuffer();charOut.flushBuffer();if (autoFlush && (s.indexOf('\n') >= 0))out.flush();}}catch (InterruptedIOException x) {Thread.currentThread().interrupt();}catch (IOException x) {trouble = true;}}/**  * textOut.write(s);调用的是该方法* 但是该方法只是把字符拆分一些信息来传递参数*/public void write(String str, int off, int len) throws IOException {synchronized (lock) {char cbuf[];if (len <= WRITE_BUFFER_SIZE) {if (writeBuffer == null) {writeBuffer = new char[WRITE_BUFFER_SIZE];}cbuf = writeBuffer;} else {    // Don't permanently allocate very large buffers.cbuf = new char[len];}str.getChars(off, (off + len), cbuf, 0);write(cbuf, 0, len);}}

可以看到println底层使用的是synchronized关键字来控制并发,而system.out是一个单例,所以多线程中使用System.out.println()打印时,会导致在执行打印方法时,只有一个线程可以执行,其他线程被挂起,多线程没有得到很好的利用,影响整体系统性能。

所以在多线程中尽量避免使用System.out.println()打印。可以使用log.info输出日志信息!

synchronized的使用和底层原理、锁状态的膨胀升级过程相关推荐

  1. 【java】 从hotspot底层对象结构理解锁膨胀升级过程

    文章目录 1.概述 2. 案例 2.1 对象分布 2.2 偏向锁 2.3 轻量级锁 2.4 重量级锁 M.扩展 1.概述 本文章是视频: 从hotspot底层对象结构理解锁膨胀升级过程 的笔记. 此文 ...

  2. 从hotspot底层对象结构理解锁膨胀升级过程||深入jdk源码理解longadder的分段cas优化机制——分段CAS优化

    深入jdk源码理解longadder的分段cas优化机制 longadder

  3. java架构升级_java架构之路(多线程)synchronized详解以及锁的膨胀升级过程

    上几次博客,我们把volatile基本都说完了,剩下的还有我们的synchronized,还有我们的AQS,这次博客我来说一下synchronized的使用和原理. synchronized是jvm内 ...

  4. synchronized锁升级_synchronized详解以及锁的膨胀升级过程

    点击上方"码之初"关注,···选择"设为星标" 与精品技术文章不期而遇 来源:www.cnblogs.com/cxiaocai/p/12189848.html ...

  5. java架构之路(多线程)synchronized详解以及锁的膨胀升级过程

    4: astore_1 5: monitorenter 6: getstatic #3 // Field java/lang/System.out:Ljava/io/PrintStream; 9: l ...

  6. synchronized原理_面试必备—Synchronized 关键字使用、底层原理

    在并发编程中存在线程安全问题,主要原因有: 1.存在共享数据 2.多线程共同操作共享数据 关键字synchronized可以保证在同一时刻,只有一个线程可以执行某个方法或某个代码块,同时synchro ...

  7. iOS进阶之底层原理-锁、synchronized

    锁主要分为两种,自旋锁和互斥锁. 自旋锁 线程反复检查锁变量是否可用,处于忙等状态.一旦获取了自旋锁,线程会一直保持该锁,直至释放,会阻塞线程,但是避免了线程上下文的调度开销,适合短时间的场合. 互斥 ...

  8. python字典内存分析_(一)Python入门-3序列:18字典-核心底层原理-内存分析-查找值对象过程...

    一:根据键查找"键值对"的底层过程 明白一个键值对是如何存储到数组中的,根据键对象取到值对象,理解起来就 简单了. >>> a.get("name&qu ...

  9. Synchronized锁升级底层原理

    思考问题 首先请您思考下面的问题: Synchronized锁同步机制性能不好嘛? 一个对象天生对应一个monitor锁吗? 为什么说synchronized是非公平锁? synchronized字节 ...

最新文章

  1. 今天,苹果遭遇大宕机
  2. 《Effective C#》某些地方实在是对不起Effective这个词(I)
  3. 英文金曲大赛_JAVA
  4. DayDayUp:发明专利授予条件、撰写发明专利的注意事项以及申辩模板
  5. 以太坊智能合约简介(Solidity)
  6. NGUI 减少Draw Call
  7. 短语密码_使用密码短语以提高安全性
  8. Hibernate 缓存的使用
  9. [AHOI 2009]chess 中国象棋
  10. linux提示有新邮件,/var/spool/mail/root 中有新邮件 解决方法
  11. oracle歸檔日誌,oracle歸檔日誌清理 | 學步園
  12. php php拼接字符串函数_PHP_PHP开发中常用的字符串操作函数,1,拼接字符串 拼接字符串是最 - phpStudy...
  13. oracle12c用plsql连不上,plsql developer 连不上11G64位ORACLE
  14. java 自然对数的底数_Java求自然对数底e的值
  15. Python学习笔记--day10函数入门
  16. 大数据技术生态体系组件概述
  17. 一个程序员单枪匹马,靠一个网站一年赚1个亿
  18. ppt图片设计素材下载网站搭建模板
  19. 将镜像刻录到U盘的方法
  20. 2012中国情爱报告

热门文章

  1. 一些java,spring boot图解
  2. 【spring学习】03
  3. matlab中结构体的定义,matlab中怎么定义结构体啊 !!!
  4. 关于document.cookie的使用
  5. Win8 Metro(C#)数字图像处理--2.50图像运动模糊
  6. 使用git将code同时提交github,gitee,coding
  7. SpringMVC 拦截器实现
  8. python版trace命令显示归属地
  9. json介绍及简单示例
  10. 26.如何实现关机时清空页面文件: