synchronized关键字以及锁的原理学习笔记:

学习b站周扬老师视频:https://www.bilibili.com/video/BV1ar4y1x727

讲得真的很不错!

乐观锁和悲观锁介绍

  1. 悲观锁:

    认为自己在使用数据的时候一定有别的线程来修改数据,因此在获取数据的时候会先加锁,确保数据不会被别的线程修改。

    synchronized关键字和Lock的实现类都是悲观锁

    显式的锁定之后再操作同步资源

  2. 乐观锁:

    认为自己在使用数据时不会有别的线程修改数据或资源,所以不会添加锁,在Java中是通过使用无锁编程来实现,只是在更新数据的时候去判断,之前有没有别的线程更新了这个数据。如果这个数据没有被更新,当前线程将自己修改的数据成功写入。如果这个数据已经被其它线程更新,则根据不同的实现方式执行不同的操作,比如放弃修改、重试抢锁等等,如原子操作类那些底层是CAS算法,也就是乐观锁。

判断规则

1)版本号机制Version

2)最常采用的是CAS算法,Java原子类中的递增操作就通过CAS自旋实现的。

使用场景:

适合读操作多的场景,不加锁的特点能够使其读操作的性能大幅提升。

乐观锁则直接去操作同步资源,是种无锁算法,得之我幸不得我命,再努力就是

synchronized用法介绍

阿里巴巴Java规范手册上说明:

高并发时,同步调用应该去考置锁的性能损耗。能用无锁数据结构,就不要用锁;能锁区块,就不要锁整个方法体;能用对象锁,就不要用类锁。

说明︰尽可能使加锁的代码块工作量尽可能的小,避免在锁代码块中调用RPC方法。

synchronized的三种应用方式

  1. 作用于实例方法,当前实例加锁,进入同步代码前要获得当前实例的锁;
    public synchronized void sayHello(){
System.out.println("作用于实例方法,当前实例加锁,进入同步代码前要获得当前实例的锁;");
}
  1. 作用于代码块,对括号里配置的对象加锁。
		synchronized (this){
System.out.println("作用于代码块,对括号里配置的对象加锁");
}
  1. 作用于静态方法,当前类加锁,进去同步代码前要获得当前类对象的锁;
    public static synchronized void sayHello(){
System.out.println("作用于静态方法,当前类加锁,进去同步代码前要获得当前类对象的锁;");
}

synchronized和ReentrantLock的区别

  1. Sychronized是一个关键字,ReentrantLock是一个类
  2. Sychronized会自动的加锁和释放锁,ReentrantLock需要程序员手动的加锁和释放锁
  3. Sychronized底层是JVM层面的锁,ReentrantLock是API层面的锁
  4. Sychronized是非公平锁,ReentrantLock可以选择公平锁或非公平
  5. Sychronized锁的是对象,锁信息保存在对象头中,ReentrantLock通过代码中的int类型的state标识来标识锁的状态

经典8锁问题案例

①. 标准访问有ab两个线程,请问先打印邮件还是短信

②. sendEmail方法暂停3秒钟,请问先打印邮件还是短信

③. 新增一个普通的hello方法,请问先打印邮件还是hello

④. 有两部手机,请问先打印邮件还是短信

⑤. 两个静态同步方法,同1部手机,请问先打印邮件还是短信

⑥. 两个静态同步方法, 2部手机,请问先打印邮件还是短信

⑦. 1个静态同步方法,1个普通同步方法,同1部手机,请问先打印邮件还是短信

⑧. 1个静态同步方法,1个普通同步方法,2部手机,请问先打印邮件还是短信

class Phone{ //资源类
public static synchronized void sendEmail() {
//暂停几秒钟线程
try { TimeUnit.SECONDS.sleep(3); } catch (InterruptedException e) { e.printStackTrace(); }
System.out.println("-------sendEmail");
} public synchronized void sendSMS()
{
System.out.println("-------sendSMS");
} public void hello()
{
System.out.println("-------hello");
}
}
public class Lock8Demo{
public static void main(String[] args){//一切程序的入口,主线程
Phone phone = new Phone();//资源类1
Phone phone2 = new Phone();//资源类2 new Thread(() -> {
phone.sendEmail();
},"a").start(); //暂停毫秒
try { TimeUnit.MILLISECONDS.sleep(300); } catch (InterruptedException e) { e.printStackTrace(); } new Thread(() -> {
//phone.sendSMS();
//phone.hello();
phone2.sendSMS();
},"b").start(); }
}
/**
*
* ============================================
* 1-2
* * 一个对象里面如果有多个synchronized方法,某一个时刻内,只要一个线程去调用其中的一个synchronized方法了,
* * 其它的线程都只能等待,换句话说,某一个时刻内,只能有唯一的一个线程去访问这些synchronized方法
* * 锁的是当前对象this,被锁定后,其它的线程都不能进入到当前对象的其它的synchronized方法
*
* 3-4
* * 加个普通方法后发现和同步锁无关
* * 换成两个对象后,不是同一把锁了,情况立刻变化。
*
* 5-6 都换成静态同步方法后,情况又变化
* 三种 synchronized 锁的内容有一些差别:
* 对于普通同步方法,锁的是当前实例对象,通常指this,具体的一部部手机,所有的普通同步方法用的都是同一把锁——实例对象本身,
* 对于静态同步方法,锁的是当前类的Class对象,如Phone.class唯一的一个模板
* 对于同步方法块,锁的是 synchronized 括号内的对象
*
* 7-8
* 当一个线程试图访问同步代码时它首先必须得到锁,退出或抛出异常时必须释放锁。
* *
* * 所有的普通同步方法用的都是同一把锁——实例对象本身,就是new出来的具体实例对象本身,本类this
* * 也就是说如果一个实例对象的普通同步方法获取锁后,该实例对象的其他普通同步方法必须等待获取锁的方法释放锁后才能获取锁。
* *
* * 所有的静态同步方法用的也是同一把锁——类对象本身,就是我们说过的唯一模板Class
* * 具体实例对象this和唯一模板Class,这两把锁是两个不同的对象,所以静态同步方法与普通同步方法之间是不会有竞态条件的
* * 但是一旦一个静态同步方法获取锁后,其他的静态同步方法都必须等待该方法释放锁后才能获取锁。
**/

从字节码角度分析synchronized实现

反编译:javap -v -p *.class > 类.txt 将进行输出到txt中

-c:对代码进行反汇编

-v -verbose 输出附加信息(包括行号、本地变量表,反汇编等详细信息)

synchronized同步代码块

实现使用的是monitorenter和monitorexit指令,他俩总是搭配使用,一个monitorenter对应两个monitorexit,monitorenter表示获得锁,monitorexit表示释放锁。但是经过反编译发现,里面多了monitorexit,是为了发生异常时也能正常释放锁。正常情况下走前面的monitorexit,异常情况走后面的monitorexit。极端情况下,也会出现一对一的情况,在退出同步代码前抛出异常,此时就是一对一的情况,因为就没有正常情况了,无论那种情况都会抛出异常。

synchronized普通同步方法

javap -v .class文件反编译

调用指令将会检查方法的ACC_SYNCHRONIZED访问标志是否被设置。如果设置了,执行线程会将先持有monitor锁,然后再执行方法,

最后在方法完成(无论是正常完成还是非正常完成)时释放monitor

synchronized静态同步方法

javap -v .class文件反编译

ACC_STATIC,ACC_SYNCHRONIZED。相比于普通同步方法,静态同步方法多了一个ACC_STATIC访问标志,使用它来区分该方法是否静态同步方法

面试题:为什么任何一个对象都可以成为一个锁

答:

管程(Monitors):可以看做一个软件模块,它是将共享的变量和对于这些共享变量的操作封装起来,形成一个具有一定接口的功能模块,进程可以调用管程来实现进程级别的并发控制。

Java虚拟机可以支持方法级的同步和方法内部一段指令序列的同步,这两种同步结构都是使用管程(Monitor,更常见的是直接将它称为“锁”)来实现的。

方法级的同步是隐式的,无须通过字节码指令来控制,它实现在方法调用和返回操作之中。虚拟机可以从方法常量池中的方法表结构中的ACC_SYNCHRONIZED访问标志得知一个方法是否被声明为同步方法。当方法调用时,调用指令将会检查方法的ACC_SYNCHRONIZED访问标志是否被设置,如果设置了,执行线程就要求先成功持有管程,然后才能执行方法,最后当方法完成(无论是正常完成还是非正常完成)时释放管程。在方法执行期间,执行线程持有了管程,其他任何线程都无法再获取到同一个管程。如果一个同步方法执行期间抛出了异常,并且在方法内部无法处理此异常,那这个同步方法所持有的管程将在异常抛到同步方法边界之外时自动释放。

通过C底层原语了解,在HotSpot虚拟机中,monitor采用objectMonitor实现

ObjectMonitor.java→ObjectMonitor.cpp→objectMonitor.hpp

每个对象天生都带着一个对象监视器

每一个被锁住的对象都会和Monitor关联起来

objectMonitor.hpp的关键属性

公平锁与非公平锁

  1. 公平锁:

    指多个线程按照中请求锁的顺序来获取锁,这里类似排队买票,先来的人先买后来的人在队尾排着,这是公平的

    Lock lock = new ReentrantLock(true);/l/true表示公平锁,先来先得

  2. 非公平锁:

    是指多个线程获取锁的顺序并不是按照申请锁的顺序,有可能后申请的线程比先中请的线程优先获取锁,在高并发环境下,有可能造成优先级翻转或者饥饿的状态(某个线程一直得不到锁)

    Lock lock = new ReentrantLock(false); false表示非公平锁,后来的也可能先获得锁。空参默认非公平锁

为什么会有公平锁/非公平锁的设计?为什么默认非公平?

恢复挂起的线程到真正锁的获取还是有时间差的,从开发人员来看这个时间微乎其微,但是从CPU的角度来看,这个时间差存在的还是很明显的。所以非公平锁能更充分的利用CPU的时间片,尽量减少CPU空闲状态时间。

使用多线程很重要的考量点是线程切换的开销,当采用非公平锁时,当1个线程请求锁获取同步状态,然后释放同步状态,所以刚释放锁的线程在此刻再次获取同步状态的概率就变得非常大,所以就减少了线程的开销。

什么时候用公平?什么时候用非公平?

如果为了更高的吞吐量,很显然非公平锁是比较合适的,因为节省很多线程切换时间,吞吐量自然就上去了;

否则那就用公平锁,大家公平使用。

可重入锁(递归锁)

可重入锁又名递归锁,是指在同一个线程在外层方法获取锁的时候,再进入该线程的内层方法会自动获取锁(前提,锁对象得是同一个对象),不会因为之前已经获取过还没释放而阻塞。

如果是1个有 synchronized修饰的递归调用方法,程序第2次进入被自己阻塞了岂不是天大的笑话,出现了作茧自缚。所以Java中ReentrantLock和synchronized都是可重入锁,可重入锁的一个优点是可一定程度避免死锁。

一个线程中的多个流程可以获取同一把锁,持有这把同步锁可以再次进入。自己可以获取自己的内部锁

可重入锁种类

隐式锁(即synchronized关键字使用的锁)默认是可重入锁

指的是可重复可递归调用的锁,在外层使用锁之后,在内层仍然可以使用,并且不发生死锁,这样的锁就叫做可重入锁。

简单的来说就是:在一个synchronized修饰的方法或代码块的内部调用本类的其他synchronized修饰的方法或代码块时,是永远可以得到锁

显式锁(即Lock)也有ReentrantLock这样的可重入锁。

Lock.unLock();//正常情况,加锁几次就要解锁几次

由于加锁次数和释放次数不一样,第二个线程始终无法获取到锁,导致一直在等待

Synchronized的重入的实现机理

每个锁对象拥有一个锁计数器和一个指向持有该锁的线程的指针。

当执行monitorenter时,如果目标锁对象的计数器为零,那么说明它没有被其他线程所持有,Java虚拟机会将该锁对象的持有线程设置为当前线程,并且将其计数器加1。在目标锁对象的计数器不为零的情况下,如果锁对象的持有线程是当前线程,那么Java虚拟机可以将其计数器加1,否则需要等待,直至持有线程释放该锁。当执行monitorexit时,Java虚拟机则需将锁对象的计数器减1。计数器为零代表锁已被释放。

死锁

死锁是指两个或两个以上的线程在执行过程中,因争夺资源而造成的一种互相等待的现象,若无外力干涉那它们都将无法推进下去,如果系统资源充足,进程的资源请求都能够得到满足,死锁出现的可能性就很低,否则就会因争夺有限的资源而陷入死锁。

产生死锁主要原因

  • 系统资源不足
  • 资源分配不当
  • 进程运行推进的顺序不合适

死锁代码案例

public class DeadLockTest {

    public static void main(String[] args) {
Object objectA = new Object();
Object objectB = new Object();
new Thread(()->{
synchronized (objectA){
System.out.println("获得objectA锁,尝试获得objectB锁");
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (objectB){
System.out.println("获得objectA锁,成功获得objectB锁");
}
}
},"A").start(); new Thread(()->{
synchronized (objectB){
System.out.println("获得objectB锁,尝试获得objectA锁");
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (objectA){
System.out.println("获得objectB锁,成功获得objectA锁");
}
}
},"B").start();
}
}

tips

如何排查死锁?

纯命令:jps -l

jstack 进程编号

图形化:jconsole



总结

指针指向monitor对象(也称为管程或监视器锁)的起始地址。每个对象都存在着一个monitor与之关联。当一个montor被某个线程持有后,它便处于锁定状态。在Java虚拟机(HotSpot)中,monitor是由ObjectMonitor实现的,其主要数据结构如下(位于HotSpot虚拟机源码ObjectMonitor.hpp文件,C++实现的)

synchronized锁原理以及执行过程如下图所示:



JUC并发编程(2)—synchronized锁原理的更多相关文章

  1. 深入理解并发编程之----synchronized实现原理

    版权声明:本文为博主原创文章,请尊重原创,未经博主允许禁止转载,保留追究权 https://blog.csdn.net/javazejian/article/details/72828483 [版权申 ...

  2. 并发编程:synchronized 锁升级过程的验证

        关于synchronized关键字以及偏向锁.轻量级锁.重量级锁的介绍广大网友已经给出了太多文章和例子,这里就不再重复了,也可点击链接来回顾一下.在这里来实战操作一把,验证JVM是怎么一步一步 ...

  3. Java并发编程:Synchronized及其实现原理

    Java并发编程系列: Java 并发编程:核心理论 Java并发编程:Synchronized及其实现原理 Java并发编程:Synchronized底层优化(轻量级锁.偏向锁) Java 并发编程 ...

  4. Java并发编程:Synchronized底层优化(偏向锁、轻量级锁)

    Java并发编程系列: Java 并发编程:核心理论 Java并发编程:Synchronized及其实现原理 Java并发编程:Synchronized底层优化(轻量级锁.偏向锁) Java 并发编程 ...

  5. 【并发编程】synchronized的使用场景和原理简介

    1. synchronized使用 1.1 synchronized介绍 在多线程并发编程中synchronized一直是元老级角色,很多人都会称呼它为重量级锁.但是,随着Java SE 1.6对sy ...

  6. JUC并发编程学习笔记

    JUC并发编程学习笔记 狂神JUC并发编程 总的来说还可以,学到一些新知识,但很多是学过的了,深入的部分不多. 线程与进程 进程:一个程序,程序的集合,比如一个音乐播发器,QQ程序等.一个进程往往包含 ...

  7. 并发编程--CAS自旋锁

    在前两篇博客中我们介绍了并发编程--volatile应用与原理和并发编程--synchronized的实现原理(二),接下来我们介绍一下CAS自旋锁相关的知识. 一.自旋锁提出的背景 由于在多处理器系 ...

  8. 并发编程-CPU执行volatile原理探讨-可见性与原子性的深入理解

    volatile的定义 Java语言规范第3版中对volatile的定义如下:Java编程语言允许线程访问共享变量,为了确保共享变量能被准确和一致地更新,线程应该确保通过排他锁单独获得这个变量.Jav ...

  9. 并发编程之synchronized锁(一)

    一.设计同步器的意义 多线程编程中,有可能会出现多个线程同时访问同一个共享.可变资源的情况,这个资源我们称之其为临界资源:这种资源可能是:对象.变量.文件等. 共享:资源可以由多个线程同时访问 可变: ...

  10. Java并发编程:synchronized

    Java并发编程:synchronized 虽然多线程编程极大地提高了效率,但是也会带来一定的隐患.比如说两个线程同时往一个数据库表中插入不重复的数据,就可能会导致数据库中插入了相同的数据.今天我们就 ...

随机推荐

  1. RT_object

    以下图片来自"张世争"的微博  

  2. Go语言中的结构体:灵活性与可扩展性的重要角色

    1. 引言 结构体是Go语言中重要且灵活的概念之一.结构体的使用使得我们可以定义自己的数据类型,并将不同类型的字段组合在一起,实现更灵活的数据结构.本文旨在深入介绍Go语言中的结构体,揭示其重要性和灵 ...

  3. 洛谷 P8179 Tyres

    滴叉题/se/se 题意 直接复制了 有 \(n\) 套轮胎,滴叉需要用这些轮胎跑 \(m\) 圈.使用第 \(i\) 套轮胎跑的第 \(j\) 圈(对每套轮胎单独计数)需要 \(a_i+b_i(j- ...

  4. spingmvc配置AOP 之 非注解方式

    spingmvc配置AOP有两种方式,一种是利用注解的方式配置,另一种是XML配置实现. 应用注解的方式配置: 先在maven中引入AOP用到的依赖 <dependency> <gr ...

  5. python笔记:第三章使用字符串

    1.1 字符串的基本操作 对序列的操作都适用于字符串,但字符串是不可变的,所以元素赋值和切片赋值都是非法的 1.2 设置字符串的格式 方法一: 使用%来设置字符串 format = 'Hello, % ...

  6. Centos7中搭建Redis6集群操作步骤

    目录 下载安装包 解压安装装包 安装依赖 安装 创建目录 设置配置文件 创建启动服务 制作启动文件 启动并验证Redis 开放防火墙端口 创建集群 集群其他操作 注意 下载安装包 # 进入软件下载目录 ...

  7. ZEGO 即构音乐场景降噪技术解析

    随着线上泛娱乐的兴起,语聊房.在线 KTV 以及直播等场景在人们的日常生活中占据越来越重要的地位,用户对于音质的要求也越来越高,因此超越传统语音降噪算法的 AI 降噪算法应运而生,所以目前各大 RTC ...

  8. Lakehouse: A New Generation of Open Platforms that Unify Data Warehousing and Advanced Analytics

    在Delta Lake官网上提到的一篇新一代湖仓架构的论文. 这篇论文由Databricks团队2021年发表于CIDR会议. 这个会议是对sigmod和vldb会议的补充. 可以看到这篇论文和前一篇 ...

  9. [CF 1780B] GCD Partition

    B. GCD Partition 题意 : 给一个长度为n的序列, 并将其分成连续的k块(k > 1), 得到序列b, 使得 \(gcd(b_{1}, b_{2}, b_{3}, ..., b_ ...

  10. 反射(Java Reflection)

    Java反射机制概述 Reflection(反射)是Java被视为动态语言的关键,反射机制允许程序在执行期借助于ReflectionAPI取得任何类的内部信息,并能直接操作任意对象的内部属性及方法. ...