1.简介

1.1 作用

并发场景中,保证同一时刻只有一个线程对有并发隐患的代码进行操作

1.2 错误案例

需求:两个线程对 count 变量进行200000次循环增加,预期结果是400000次

public class SynchronizedDemo implements Runnable {
private static int count = 0;
static SynchronizedDemo synchronizedInstance = new SynchronizedDemo();
public static void main(String[] args) {
Thread t1 = new Thread(synchronizedInstance);
Thread t2 = new Thread(synchronizedInstance);
t1.start();
t2.start();
try {
t1.join();
t2.join();
System.out.println("count 最终的值为: " + count);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
@Override
public void run() {
synchronized (this) {
for (int i = 0; i < 200000; i++) {
count++;
}
}
}
}

结果 :显然不等于400000次所以出现了运算错误

原因:

 count++;

该语句包含三个操作:

  1. 线程t1、t2 从主内存中获取共享变量count的值,到自己的工作内存中
  2. 将自己的工作内存中的count值进行+1操作
  3. 将修改完的count变量的值存入到主内存中

注意:他们是将自己工作内存中的值进行改变刷回主内存,假设当前count的值为8,t1、t2将count的值复制到自己的工作内存中进行修改,如果此时t1将count变成9、t2此时也将count的值变成9,当t1、t2两个线程都将值刷回主内存的时候count值为9,并不是10,这个时候就会造成最后的结果和预期的不一致。

1.3 正确案例

  1. 代码块上加对象锁 this
@Override
public void run() {
synchronized (this) {
for (int i = 0; i < 200000; i++) {
count++;
}
}
}
  1. 在普通方法上加锁
@Override
public synchronized void run() {
for (int i = 0; i < 200000; i++) {
count++;
}
}
  1. 加.class锁
@Override
public void run() {
for (int i = 0; i < 200000; i++) {
synchronized (SynchronizedDemo.class) {
count++;
}
}
}

输出结果:

后文详细讲解四种加 synchronized 的方式


2.用法

2.1 对象锁

2.1.1 方法锁

修饰普通方法默认锁对象为this当前实例对象

public synchronized void method() ;在普通方法上面加synchronized

public class SynchronizedDemo3 implements Runnable {
static SynchronizedDemo3 synchronizedDemo3 = new SynchronizedDemo3();
public synchronized void method() {
System.out.println("线程名称" + Thread.currentThread().getName());
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("线程名称" + Thread.currentThread().getName() + "运行完成");
}
@Override
public void run() {
method();
}
public static void main(String[] args) {
Thread t1 = new Thread(synchronizedDemo3);
t1.setName("我是线程 t1");
Thread t2 = new Thread(synchronizedDemo3);
t2.setName("我是线程 t2");
t1.start();
t2.start();
try {
t1.join();
t2.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}

输出结果: 线程 t1 和线程 t2 执行过程是顺序执行的

2.1.2 同步代码块

  1. 代码示例:没有加锁而定义的两个线程执行的情况

输出结果:线程 t1 和线程 t2 交叉执行形成了乱序


  1. 代码示例:加Synchronized 锁而定义的两个线程执行的情况,锁对象的是this(当前对象)

输出结果:线程 t1 和线程 t2 执行过程是顺序执行的


  1. 代码示例:加Synchronized 锁而定义的两个线程执行的情况,锁对象的是自定义对象

输出结果:线程 t1 和线程 t2 执行形成了顺序,这种情况下和this没有什么区别,但是如果是多个同步代码块的话就需要进行自定义对象锁


  1. 代码示例:多个同步代码块使用自定义对象锁,(两个自定义对象锁对应两个同步代码块)

输出结果:输出顺序线程t1 和线程t2 代码进行了交叉执行,出现了乱序


  1. 代码示例:多个同步代码块使用自定义对象锁,(一个自定义对象锁对应两个同步代码块)

输出结果:线程 t1 和线程 t2 执行形成了顺序

2.2 类锁

特点:类锁只能在同一时间被一个对象拥有(无论有多少个实例想访问也是一个对象持有它)

2.2.1 synchronized修饰静态的方法

  1. 代码示例: synchronized 加在普通方法上面
public class SynchronizedDemo4 implements Runnable {
private static SynchronizedDemo4 synchronizedInstance1 = new SynchronizedDemo4();
private static SynchronizedDemo4 synchronizedInstance2 = new SynchronizedDemo4();
public synchronized void method() {
System.out.println("线程名称" + Thread.currentThread().getName());
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("线程名称" + Thread.currentThread().getName() + "运行完成");
}
@Override
public void run() {
method();
}
public static void main(String[] args) {
Thread t1 = new Thread(synchronizedInstance1);
t1.setName("我是线程 t1");
Thread t2 = new Thread(synchronizedInstance2);
t2.setName("我是线程 t2");
t1.start();
t2.start();
try {
t1.join();
t2.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}

输出结果:输出顺序线程t1 和线程t2 代码进行了交叉执行,出现了乱序

  1. 代码示例: synchronized 加在静态方法上面

public static synchronized void method();使用方式

public class SynchronizedDemo4 implements Runnable {
private static SynchronizedDemo4 synchronizedInstance1 = new SynchronizedDemo4();
private static SynchronizedDemo4 synchronizedInstance2 = new SynchronizedDemo4();
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() + "运行完成");
}
@Override
public void run() {
method();
}
public static void main(String[] args) {
Thread t1 = new Thread(synchronizedInstance1);
t1.setName("我是线程 t1");
Thread t2 = new Thread(synchronizedInstance2);
t2.setName("我是线程 t2");
t1.start();
t2.start();
try {
t1.join();
t2.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}

输出结果:线程 t1 和线程 t2 执行形成了顺序

2.2.2 指定锁对象为Class对象

  1. 代码示例:synchronized 加.class

synchronized (SynchronizedDemo5.class)

public class SynchronizedDemo5 implements Runnable {
private static SynchronizedDemo5 synchronizedInstance1 = new SynchronizedDemo5();
private static SynchronizedDemo5 synchronizedInstance2 = new SynchronizedDemo5();
void method() {
synchronized (SynchronizedDemo5.class) { //类锁只有一把
System.out.println("线程名称" + Thread.currentThread().getName());
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("线程名称" + Thread.currentThread().getName() + "运行完成");
}
}
@Override
public void run() {
method();
}
public static void main(String[] args) {
Thread t1 = new Thread(synchronizedInstance1);
t1.setName("我是线程 t1");
Thread t2 = new Thread(synchronizedInstance2);
t2.setName("我是线程 t2");
t1.start();
t2.start();
try {
t1.join();
t2.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}

输出结果: 线程 t1 和线程 t2 执行形成了顺序

3.性质

3.1 可重入性也叫递归锁

就是说你已经获取了一把锁,等想要再次请求的时候不需要释放这把锁和其他线程一起竞争该锁,可以直接使用该锁

好处:避免死锁

粒度:线程而非调用

3.2案例证明可重入性

  1. 证明同一个方法是可重入

代码实例:

package synchronizedPage;
public class SynchronizedDemo6 {
int count = 0;
public static void main(String[] args) {
SynchronizedDemo6 synchronizedDemo6 = new SynchronizedDemo6();
synchronizedDemo6.method();
}
private synchronized void method() {
System.out.println(count);
if (count == 0) {
count++;
method();
}
}
}

输出结果:

  1. 证明可重入不要求是同一个方法

代码实例:

package synchronizedPage;
public class SynchronizedDemo7 {
private synchronized void method1() {
System.out.println("method1");
method2();
}
private synchronized void method2() {
System.out.println("method2");
}
public static void main(String[] args) {
SynchronizedDemo7 synchronizedDemo7 = new SynchronizedDemo7();
synchronizedDemo7.method1();
}
}

输出结果:

  1. 证明可重入不要求是同一个类中的

代码实例:

package synchronizedPage;
public class SynchronizedDemo8 {
public synchronized void doSomething() {
System.out.println("我是父类方法");
}
}
class childrenClass extends SynchronizedDemo8{
public synchronized void doSomething() {
System.out.println("我是子类方法");
super.doSomething();
}
public static void main(String[] args) {
childrenClass childrenClass = new childrenClass();
childrenClass.doSomething();
}
}

输出结果:

3.3 不可中断

当A线程持有这把锁时,B线程如果也想要A线程持有的锁时只能等待,A永远不释放的话,那么B线程永远的等待下去。

4.底层原理实现

4.1 加锁和释放锁的原理

  • synchronized加在代码块上
public void test() {
synchronized(this){
count++;
}
}

利用 javap -verbose 类的名字查看编译后的文件

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 的所有权

monitorexit指令出现了两次,第1次为同步正常退出释放锁;第2次为发生异步退出释放锁

  • synchronized加在方法上(无论时普通方法还是静态方法)
public synchronized void test() {
count++;
}

利用 javap -verbose 类的名字查看编译后的文件

方法的同步并没有通过指令monitorentermonitorexit来完成,不过相对于普通方法,其常量池中多了ACC_SYNCHRONIZED标示符。JVM就是根据该标示符来实现方法的同步的:

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

5.Java虚拟机对synchronized的优化

从JDK6开始,就对synchronized的实现机制进行了较大调整,包括使用JDK5引进的CAS自旋之外,还增加了自适应的CAS自旋、锁消除、锁粗化、偏向锁、轻量级锁这些优化策略。所以synchronized关键字的优化使得性能极大提高,同时语义清晰、操作简单、无需手动关闭,所以推荐在允许的情况下尽量使用此关键字,同时在性能上此关键字还有优化的空间。

5.1 锁主要存在的四种状态

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

锁的膨胀过程

无锁状态 -> 偏向锁 -> 轻量级锁 -> 重量级锁

只能从低到高升级,不会出现锁的降级

5.2 自旋锁

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

使用场景: 自旋锁适用于锁保护的临界区很小的情况,临界区很小的话,锁占用的时间就很短。

缺点:虽然它可以避免线程切换带来的开销,但是它占用了CPU处理器的时间。如果持有锁的线程很快就释放了锁,那么自旋的效率就非常好,反之,自旋的线程就会白白消耗掉处理的资源,它不会做任何有意义的工作,所以增加了适应性自选锁

5.3 适应性自旋锁

所谓自适应就意味着自旋的次数不再是固定的,它是由前一次在同一个锁上的自旋时间及锁的拥有者的状态来决定。

线程如果自旋成功了,那么下次自旋的次数会更加多,因为上次成功了,那么此次自旋也很有可能会再次成功,那么它就会允许自旋等待持续的次数更多。反之,很少能够成功,那么以后自旋的次数会减少甚至省略掉自旋过程,以免浪费处理器资源。

5.4 锁消除

为了保证数据的完整性,在进行操作时需要对这部分操作进行同步控制,但是在有些情况下,JVM检测到不可能存在共享数据竞争,这是JVM会对这些同步锁进行锁消除。作为写程序的人应该会知道哪里存在数据竞争,不可能随便的加锁。

5.5 锁粗化

将多个连续的加锁、解锁操作连接在一起,扩展成一个范围更大的锁。虽然我们平时倡导把加锁的片段尽量小为了增加并发效率和性能。但是如果一系列的连续加锁解锁操作,可能会导致不必要的性能损耗,所以引入锁粗化。

5.6 偏向锁

在大多数情况下,锁不仅不存在多线程竞争,而且总是由同一线程多次获得,为了让线程获得锁的代价更低,引进了偏向锁。偏向锁是在单线程执行代码块时使用的机制,如果在多线程并发的环境下(即线程A尚未执行完同步代码块,线程B发起了申请锁的申请),则一定会转化为轻量级锁或者重量级锁。

引入偏向锁主要目的是:为了在没有多线程竞争的情况下尽量减少不必要的轻量级锁执行路径。因为轻量级锁的加锁解锁操作是需要依赖多次CAS原子指令的,而偏向锁只需要在置换ThreadID的时候依赖一次CAS原子指令。

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

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

偏向锁的获取和撤销流程:

5.7 轻量级锁

引入轻量级锁的主要目的是在没有多线程竞争的前提下,减少传统的重量级锁使用操作系统互斥量产生的性能消耗。当关闭偏向锁功能或者多个线程竞争偏向锁导致偏向锁升级为轻量级锁,则会尝试获取轻量级锁,其步骤如下:

  1. 在线程进入同步块时,如果同步对象锁状态为无锁状态(锁标志位为“01”状态,是否为偏向锁为“0”),虚拟机首先将在当前线程的栈帧中建立一个名为锁记录(Lock Record)的空间,用于存储锁对象目前的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. 如果替换失败,说明有其他线程尝试过获取该锁(此时锁已膨胀),那就要在释放锁的同时,唤醒被挂起的线程

问题:

  1. 为什么升级为轻量锁时要把对象头里的Mark Word复制到线程栈的锁记录中呢?

因为在申请对象锁时需要以该值作为CAS的比较条件,同时在升级到重量级锁时,能通过这个比较判定是否在持有锁的过程中此锁被其他线程申请过,如果被其他线程申请了,则在释放锁的时候要唤醒被挂起的线程。

  1. 为什么会尝试CAS不成功以及什么情况下会不成功?
  2. CAS本身是不带锁机制的,其是通过比较来操作得。假设如下场景:线程A和线程B都在对象头里的锁标识为无锁状态进入,那么如线程A先更新对象头为其锁记录指针成功之后,线程B再用CAS去更新,就会发现此时的对象头已经不是其操作前的对象了,所以CAS会失败。也就是说,只有两个线程并发申请锁的时候会发生CAS失败。
  3. 此时线程B进行CAS自旋,等待对象头的锁标识重新变回无锁状态或对象头内容等于对象,这也就意味着线程A执行结束,此时线程B的CAS操作终于成功了,于是线程B获得了锁以及执行同步代码的权限。如果线程A的执行时间较长,线程B经过若干次CAS时钟没有成功,则锁膨胀为重量级锁,即线程B被挂起阻塞、等待重新调度

5.8 重量级锁

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

6. 缺点

  1. 效率低

    • 锁的释放情况少
    • 试图获取锁时不能设定超时
    • 不能中断一个正在试图获得锁的线程
  2. 不够灵活
    • 加锁和释放锁的时候单一,每个锁仅有一个单一条件
  3. 不知道是否成功获取锁

Synchronized 精讲的更多相关文章

  1. Java岗 面试考点精讲(基础篇01期)

    即将到来金三银四人才招聘的高峰期,渴望跳槽的朋友肯定跟我一样四处找以往的面试题,但又感觉找的又不完整,在这里我将把我所见到的题目做一总结,并尽力将答案术语化.标准化.预祝大家面试顺利. 术语会让你的面 ...

  2. 深入Java核心 Java内存分配原理精讲

    深入Java核心 Java内存分配原理精讲 栈.堆.常量池虽同属Java内存分配时操作的区域,但其适用范围和功用却大不相同.本文将深入Java核心,详细讲解Java内存分配方面的知识. Java内存分 ...

  3. WKWebView API精讲(OC)

    WKWebView API精讲(OC) 前言 鉴于LL同志对笔者说:“能不能写个OC版本的WKWebView的使用教程?”,还积极打赏了30RMB,笔者又怎么好意思拒绝呢,于是才有了下文. 所有看到本 ...

  4. 《VC++ 6简明教程》即VC++ 6.0入门精讲 学习进度及笔记

    VC++6.0入门→精讲 2013.06.09,目前,每一章的“自测题”和“小结”三个板块还没有看(备注:第一章的“实验”已经看完). 2013.06.16 第三章的“实验”.“自测题”.“小结”和“ ...

  5. iOS开发——语法篇OC篇&高级语法精讲二

    Objective高级语法精讲二 Objective-C是基于C语言加入了面向对象特性和消息转发机制的动态语言,这意味着它不仅需要一个编译器,还需要Runtime系统来动态创建类和对象,进行消息发送和 ...

  6. iOS开发——语法篇OC篇&高级语法精讲

    高级语法精讲 一.NSSet.NSMutableSet集合的介绍 1)NSSet.NSMutableSet集合,元素是无序的,不能有重复的值. 2)用实例方法创建一个不可变集合对象 例如: //宏定义 ...

  7. iOS CAShapeLayer精讲

    前言 CAShapeLayer继承自CALayer,因此,可使用CALayer的所有属性.但是,CAShapeLayer需要和贝塞尔曲线配合使用才有意义. 关于UIBezierPath,请阅读文章:i ...

  8. 【C++自我精讲】基础系列二 const

    [C++自我精讲]基础系列二 const 0 前言 分三部分:const用法.const和#define比较.const作用. 1 const用法 const常量:const可以用来定义常量,不可改变 ...

  9. iOS-UI控件精讲之UIView

    道虽迩,不行不至:事虽小,不为不成. 相关阅读 1.iOS-UI控件精讲之UIView(本文) 2.iOS-UI控件精讲之UILabel ...待续 UIView是所有UI控件的基类,在布局的时候通常 ...

随机推荐

  1. 实验吧 Once more

    0x1函数解析 ereg(): *用指定的模式搜索一个字符串中指定的字符串,如果匹配成功返回true,否则,则返回false. 搜索字母的字符是大小写敏感的. * 此函数存在两个漏洞: ①%00截断及 ...

  2. 记阿里云 RDS MySQL 的一个大坑

    花了一个下午的时间,终于把一个阿里云 RDS MySQL 的一个大坑填上了,解决方法令人匪夷所思!绝对会让各位看官感到大吃一惊,阿里云 RDS MySQL 居然有这样 xx 的大坑! 问题 最近应业务 ...

  3. NET CORE通过NodeService调用js

    在 .NET Framework 时,我们可以通过V8.NET等组件来运行 JavaScript,不过目前我看了好几个开源组件包括V8.NET都还不支持 .NET Core ,我们如何在 .NET C ...

  4. CSP-S 2020 题解

    赛后我重拳出击,赛场上我却爆零.哎. 题解本人口胡.有错请各位大佬们指出. A. 儒略日 这题是大型模拟题. 介绍两种写法:一种代码量致死(赛 场 自 闭),一种是非常好写的. 写法 1 我在赛场的思 ...

  5. AWT01-体系概述

    1.概述 AWT(Abstract Window Toolkit),中文译为抽象窗口工具包,该包提供了一套与本地图形界面进行交互的接口,是Java提供的用来建立和设置Java的图形用户界面的基本工具. ...

  6. 学习笔记——JS语言精粹

    JS作用域是基于词法作用域的顶级对象. JS是一门弱类型语言,强类型能在编译时检测错误. JS是唯一一门所有浏览器都能识别的语言. 块注释对于被注释的代码是不安全的,例如/*  var rm=/a*/ ...

  7. css样式规则

    在css样式规则中: 1.选择器用于指定CSS样式作用的HTML对象,花括号内是对该对象设置的具体样式. 2.属性和属性值以"键值对"的形式出现. 3.属性是对指定的对象设置的样式 ...

  8. 我是如何用go-zero 实现一个中台系统的

    最近发现golang社区里出了一个新星的微服务框架,来自好未来,光看这个名字,就很有奔头,之前,也只是玩过go-micro,其实真正的还没有在项目中运用过,只是觉得 微服务,grpc 这些很高大尚,还 ...

  9. Python 中更优雅的日志记录方案

    在 Python 中,一般情况下我们可能直接用自带的 logging 模块来记录日志,包括我之前的时候也是一样.在使用时我们需要配置一些 Handler.Formatter 来进行一些处理,比如把日志 ...

  10. Core3.0发布到IIS的流程

    前言 参考链接 https://www.cnblogs.com/wutongjun/p/11981798.html 在IIS上部署 .Net Core 3.0 项目的主要流程有: 安装并启用IIS 安 ...