synchronized这个关键字对应Java程序猿来说是非常的熟悉,只要遇到要解决线程安全问题的地方都会使用这个关键字。接下来一起来探讨一下synchronized到底时怎么实现线程同步,使用synchronized实现线程同步到底好不好。

在讨论synchronized之前,是否大家对cup怎么实现多线程是否有所了解?

上下文切换

即使是单核处理器也支持多线程执行代码,CPU通过给每个线程分配CPU时间片来实现这个机制。时间片是CPU分配给各个线程的时间,因为时间片非常短,所以CPU通过不停地切换线程执行,让我们感觉多个线程是同时执行的,时间片一般是几十毫秒(ms)。

CPU通过时间片分配算法来循环执行任务,当前任务执行一个时间片后会切换到下一个任务。但是,在切换前会保存上一个任务的状态,以便下次切换回这个任务时,可以再加载这个任务的状态。所以任务从保存到再加载的过程就是一次上下文切换。

这就像我们同时读两本书,当我们在读一本英文的技术书时,发现某个单词不认识,于是便打开中英文字典,但是在放下英文技术书之前,大脑必须先记住这本书读到了多少页的第多少行,等查完单词之后,能够继续读这本书。这样的切换是会影响读书效率的,同样上下文切换也会影响多线程的执行速度

上下文切换既然会影响多线程执行速度,那怎么优化上下文切换带来的弊端呢?

减少上下文切换的方法有无锁并发编程、CAS算法、使用最少线程和使用协程。

  • 无锁并发编程。多线程竞争锁时,会引起上下文切换,所以多线程处理数据时,可以用一些办法来避免使用锁,如将数据的ID按照Hash算法取模分段,不同的线程处理不同段的数据。
  • CAS算法。Java的Atomic包使用CAS算法来更新数据,而不需要加锁。
  • 使用最少线程。避免创建不需要的线程,比如任务很少,但是创建了很多线程来处理,这样会造成大量线程都处于等待状态。
  • 协程:在单线程里实现多任务的调度,并在单线程里维持多个任务间的切换。

synchronized原理

在Java SE 1.6之前synchronized都会使多线程进入到等待状态,因此带来了非常平凡的上下文切换,导致线程执行效率下降,而Java SE 1.6以后进行了大量的优化。

我们将从以下几个方面来了解synchronized:

  1. synchronized怎么获取锁?
  2. synchronized偏向锁、轻量级锁、重量级锁实现原理;

synchronized怎么获取锁?

JVM规范定义:JVM基于进入与退出Monitor对象与来实现方法同步和代码块同步:

  • 代码块同步:使用monitorenter和monitorexit指令实现。
  • 方法同步:使用另外一种方式,但是同样是使用这两个指令实现。只是具体表现形式有所不同
synchronized的实现有以下三种方式:
  • 普通同步方法,锁是当前实例对象。
  • 静态同步方法,锁时当前类的Class对象。
  • 同步代码块,锁是synchronized括号里配置的对象。
public class SynchronizedTest {
public synchronized void test1() {
} public void test2() {
synchronized (this) {
}
}
}

使用javap -verbose SynchronizedTest.class工具查看生成的class文件信息分析synchronized实现:

省略部分代码,如下所示:

{
public com.zero.test.SynchronizedTest();
..... public synchronized void test1();
descriptor: ()V
flags: ACC_PUBLIC, ACC_SYNCHRONIZED
Code:
stack=0, locals=1, args_size=1
0: return
LineNumberTable:
line 5: 0 public void test2();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=2, locals=3, args_size=1
0: aload_0
1: dup
2: astore_1
3: monitorenter
4: aload_1
5: monitorexit
6: goto 14
9: astore_2
10: aload_1
11: monitorexit
12: aload_2
13: athrow
14: return ......
}

从上面可以看出同步代码块使用monitorenter和monitorexit指令实现的,同步方法使用方法修饰符上的ACCSYNCHRONIZED实现。无论哪一种方式,其本质都是一个对象的监视器(monitor)进行获取。

监视器锁(Monitor)本质是依赖于底层的操作系统的Mutex Lock(互斥锁)来实现的。每个对象都对应于一个可称为" 互斥锁" 的标记,这个标记用来保证在任一时刻,只能有一个线程访问该对象。本文不对Monitor做更细致的讲解。

Java对象头

Java对象保存在内存中时,由以下三部分组成:

  1. 对象头
  2. 实例数据
  3. 对齐填充字节

java的对象头也由三部分组成:

  1. Mark Word
  2. 指向类的指针
  3. 数组长度(只有数组对象才有)

Mark Word记录了对象和锁有关的信息,当这个对象被synchronized关键字当成同步锁时,围绕这个锁的一系列操作都和Mark Word有关。

Mark Word在32位JVM中的长度是32bit,在64位JVM中长度是64bit。

Mark Word在不同的锁状态下存储的内容不同,在32位JVM中是这么存的:

其中无锁和偏向锁的锁标志位都是01,只是在前面的1bit区分了这是无锁状态还是偏向锁状态。

JDK1.6以后的版本在处理同步锁时存在锁升级的概念,JVM对于同步锁的处理是从偏向锁开始的,随着竞争越来越激烈,处理方式从偏向锁升级到轻量级锁,最终升级到重量级锁。

偏向锁

HotSpot的作者经过研究发现,大多数情况下,锁不仅不存在多线程竞争,而且总是由同一线程多次获得,为了让线程获得锁的代价更低而引入了偏向锁。当一个线程访问同步块并获取锁时,会在对象头和栈帧中的锁记录里存储锁偏向的线程ID,以后该线程在进入和退出同步块时不需要进行CAS操作来加锁和解锁,只需简单地测试一下对象头的Mark Word里是否存储着指向当前线程的偏向锁。如果测试成功,表示线程已经获得了锁。如果测试失败,则需要再测试一下Mark Word中偏向锁的标识是否设置成1(表示当前是偏向锁):如果没有设置,则使用CAS竞争锁;如果设置了,则尝试使用CAS将对象头的偏向锁指向当前线程。

(1)偏向锁的撤销

偏向锁使用了一种等到竞争出现才释放锁的机制,所以当其他线程尝试竞争偏向锁时,持有偏向锁的线程才会释放锁。偏向锁的撤销,需要等待全局安全点(在这个时间点上没有正在执行的字节码)。它会首先暂停拥有偏向锁的线程,然后检查持有偏向锁的线程是否活着,如果线程不处于活动状态,则将对象头设置成无锁状态;如果线程仍然活着,拥有偏向锁的栈会被执行,遍历偏向对象的锁记录,栈中的锁记录和对象头的Mark Word要么重新偏向于其他线程,要么恢复到无锁或者标记对象不适合作为偏向锁,最后唤醒暂停的线程。下图中的线程1演示了偏向锁初始化的流程,线程2演示了偏向锁撤销的流程。

(2)关闭偏向锁

偏向锁在Java 6和Java 7里是默认启用的,但是它在应用程序启动几秒钟之后才激活,如有必要可以使用JVM参数来关闭延迟:-XX:BiasedLockingStartupDelay=0。如果你确定应用程序里所有的锁通常情况下处于竞争状态,可以通过JVM参数关闭偏向锁:-XX:-UseBiasedLocking=false,那么程序默认会进入轻量级锁状态。

轻量级锁

(1)轻量级锁加锁

线程在执行同步块之前,JVM会先在当前线程的栈桢中创建用于存储锁记录的空间,并将对象头中的Mark Word复制到锁记录中,官方称为Displaced Mark Word。然后线程尝试使用CAS将对象头中的Mark Word替换为指向锁记录的指针。如果成功,当前线程获得锁,如果失败,表示其他线程竞争锁,当前线程便尝试使用自旋来获取锁。
(2)轻量级锁解锁

轻量级解锁时,会使用原子的CAS操作将Displaced Mark Word替换回到对象头,如果成功,则表示没有竞争发生。如果失败,表示当前锁存在竞争,锁就会膨胀成重量级锁。下图是两个线程同时争夺锁,导致锁膨胀的流程图。

因为自旋会消耗CPU,为了避免无用的自旋(比如获得锁的线程被阻塞住了),一旦锁升级成重量级锁,就不会再恢复到轻量级锁状态。当锁处于这个状态下,其他线程试图获取锁时,都会被阻塞住,当持有锁的线程释放锁之后会唤醒这些线程,被唤醒的线程就会进行新一轮的夺锁之争。

锁的对比

下面对偏向锁、轻量级锁和重量级锁进行比较:

笔者的微信公众号,每天一篇好文章:

Java并发编程-深入探讨synchronized实现原理的更多相关文章

  1. 干货:Java并发编程系列之synchronized(一)

    1. 使用方法 synchronized 是 java 中最常用的保证线程安全的方式,synchronized 的作用主要有三方面: 确保线程互斥的访问代码块,同一时刻只有一个方法可以进入到临界区 保 ...

  2. 【Java并发编程实战】-----synchronized

    在我们的实际应用当中可能经常会遇到这样一个场景:多个线程读或者.写相同的数据,访问相同的文件等等.对于这种情况如果我们不加以控制,是非常容易导致错误的.在java中,为了解决这个问题,引入临界区概念. ...

  3. 【Java并发编程实战】—–synchronized

    在我们的实际应用其中可能常常会遇到这样一个场景:多个线程读或者.写相同的数据,訪问相同的文件等等.对于这样的情况假设我们不加以控制,是非常easy导致错误的. 在java中,为了解决问题,引入临界区概 ...

  4. Java 并发编程——volatile与synchronized

    一.Java并发基础 多线程的优点 资源利用率更好 程序设计在某些情况下更简单 程序响应更快 这一点可能对于做客户端开发的更加清楚,一般的UI操作都需要开启一个子线程去完成某个任务,否者会容易导致客户 ...

  5. 【Java并发编程】1、ConcurrentHashMap原理分析

    集合是编程中最常用的数据结构.而谈到并发,几乎总是离不开集合这类高级数据结构的支持.比如两个线程需要同时访问一个中间临界区(Queue),比如常会用缓存作为外部文件的副本(HashMap).这篇文章主 ...

  6. java并发编程:深入了解synchronized

    简介 synchronized是Java语言的关键字,可用来给对象和方法或者代码块加锁,当它锁定一个方法或者一个代码块的时候,同一时刻最多只有一个线程执行这段代码.同时它还保证了共享变量的内存可见性. ...

  7. java并发编程学习: ThreadLocal使用及原理

    多线程应用中,如果希望一个变量隔离在某个线程内,即:该变量只能由某个线程本身可见,其它线程无法访问,那么ThreadLocal可以很方便的帮你做到这一点. 先来看一下示例: package yjmyz ...

  8. 【Java并发编程】23、ConcurrentHashMap原理分析(1.7和1.8版本对比)

    jdk 1.8版本 ConcurrentHashMap在1.8中的实现,相比于1.7的版本基本上全部都变掉了.首先,取消了Segment分段锁的数据结构,取而代之的是数组+链表(红黑树)的结构.而对于 ...

  9. 干货:Java并发编程系列之volatile(二)

    接上一篇<Java并发编程系列之synchronized(一)>,这是第二篇,说的是关于并发编程的volatile元素. Java语言规范第三版中对volatile的定义如下:Java编程 ...

随机推荐

  1. Set接口中的HashSet,LinkedHashSet,TreeSet

    TestSet package com.aff.coll; import java.util.Comparator; import java.util.HashSet; import java.uti ...

  2. 是时候扔掉cmder, 换上Windows Terminal

    作为一个Windows的长期用户,一直没有给款好用的终端,知道遇到了 cmder,它拯救一个习惯用Windows敲shell命令的人. 不用跟我安利macOS真香!公司上班一直用macOS,一方面确实 ...

  3. kali下安装beef并联合Metasploit

    安装beef 在kali中安装beef比较容易,一条命令就可以安装了,打开终端,输入apt-get install beef-xss ,安装前可以先更新一下软件apt-get update 修改bee ...

  4. 使用PRTG和panabit结合定位网络阻塞的来源

    一.背景   在网络管理工作中,有时会出现网络阻塞,需要定位阻塞来源以采取措施解决问题.二.以一个网络阻塞案例说明定位方法   案例:某企业日常使用多条网络线路,某一段时间发现某条线路传输速率下降,对 ...

  5. Chisel3 - 运算符和位宽推断

    https://mp.weixin.qq.com/s/rI-CJM6GyI6EUHPZ3uYiFg   如同Verilog中的众多运算符,Chisel也针对自身的数据类型,提供了很多运算符.   Ch ...

  6. Java实现 LeetCode 609 在系统中查找重复文件(阅读理解+暴力大法)

    609. 在系统中查找重复文件 给定一个目录信息列表,包括目录路径,以及该目录中的所有包含内容的文件,您需要找到文件系统中的所有重复文件组的路径.一组重复的文件至少包括二个具有完全相同内容的文件. 输 ...

  7. Java实现 LeetCode 538 把二叉搜索树转换为累加树(遍历树)

    538. 把二叉搜索树转换为累加树 给定一个二叉搜索树(Binary Search Tree),把它转换成为累加树(Greater Tree),使得每个节点的值是原来的节点值加上所有大于它的节点值之和 ...

  8. Java实现 蓝桥杯VIP 算法提高 数的划分

    算法提高 数的划分 时间限制:1.0s 内存限制:256.0MB 问题描述 一个正整数可以划分为多个正整数的和,比如n=3时: 3:1+2:1+1+1: 共有三种划分方法. 给出一个正整数,问有多少种 ...

  9. Java实现 蓝桥杯VIP 算法提高 色盲的民主

    算法提高 色盲的民主 时间限制:1.0s 内存限制:256.0MB  色盲的民主 问题描述 n个色盲聚在一起,讨论一块布的颜色.尽管都是色盲,却盲得各不相同.每个人都有自己的主张,争论不休.最终,他 ...

  10. 【CSS】滚动条样式

    /*定义滚动条宽高及背景,宽高分别对应横竖滚动条的尺寸*/ .scrollbar::-webkit-scrollbar{ width: 16px; height: 16px; background-c ...