赞
踩
并发编程高级面试解析
一、Synchronized 相关问题
二、可重入锁 ReentrantLock 及其它显式锁相关问题
三、其它
认为自己在使用数据的时候一定有别的线程来修改数据,因此在获取数据的时候会先加锁,确保数据不会被别的线程修改
synchronized和Lock的实现类都是悲观锁
适合写操作多的场景,先加锁可以保证写操作时数据正确,显示的锁定之后再操作同步资源
一句话定义:狼性锁
认为自己在使用数据的时候不会有别的线程修改数据或资源,所以不会添加锁
Java中使用无锁编程来实现,只是在更新的时候去判断,之前有没有别的线程更新了这个数据
判断规则
适合读操作多的场景,不加锁的特性能够使其读操作的性能大幅提升
乐观锁则直接去操作同步资源,是一种无锁算法,得之我幸不得我命,再努力就是
一句话定义:佛系锁
乐观锁一般有两种实现方式:
package com.bilibili.juc.lock; import java.util.concurrent.TimeUnit; /** * 题目:谈谈你对多线程锁的理解,8锁案例说明 * 口诀:线程 操作 资源类 * 8锁案例说明: * 1. 标准访问ab两个线程,请问先打印邮件还是短信? --------先邮件,后短信 共用一个对象锁 * 2. sendEmail钟加入暂停3秒钟,请问先打印邮件还是短信?--------先邮件,后短信 共用一个对象锁 * 3. 添加一个普通的hello方法,请问先打印普通方法还是邮件? --------先hello,再邮件 资源没有争抢,hello方法没有用到对象锁 * 4. 有两部手机,请问先打印邮件还是短信? --------先短信后邮件 资源没有争抢,不是同一个对象锁 * 5. 有两个静态同步方法,一部手机, 请问先打印邮件还是短信?--------先邮件后短信 共用一个类锁 * 6. 有两个静态同步方法,两部手机, 请问先打印邮件还是短信? --------先邮件后短信 共用一个类锁 * 7. 有一个静态同步方法,一个普通同步方法,一部手机,请问先打印邮件还是短信? --------先短信后邮件 一个类锁一个对象锁 * 8. 有一个静态同步方法,一个普通同步方法,两部手机,请问先打印邮件还是短信? ---------先短信后邮件 一个类锁一个对象锁 */ public class Lock8Demo { public static void main(String[] args) { Phone phone = new Phone(); Phone phone2 = new Phone(); new Thread(() -> phone.sendEmail(), "a").start(); // 暂停200毫秒,保证线程先启动 try { TimeUnit.MILLISECONDS.sleep(200); } catch (InterruptedException e) { e.printStackTrace(); } new Thread(() -> /*phone.sendSMS()*/ /*phone.hello()*/ phone2.sendSMS(), "b").start(); } } // 资源类 class Phone { public static synchronized void sendEmail() { try { TimeUnit.SECONDS.sleep(3); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("--------sendEmail--------"); } public /*static*/ synchronized void sendSMS() { System.out.println("--------sendSMS--------"); } public void hello() { System.out.println("--------hello--------"); } }
1-2:
一个对象里面如果有多个 synchronized 方法,某一个时刻内,只要一个线程去调用其中的一个synchronized方法,其它线程都只能等待。换句话说,某一个时刻内,只能有唯一的一个线程去访问这些synchronized方法。锁的是当前对象this,被锁定后,其它线程都不能进入到当前对象的其它的synchronized方法
3-4:
加个普通方法后发现和同步锁无关
换成两个对象后,不是同一把锁了,情况立即变化
5-6: 都换成静态同步方法后,情况又变化
三种synchronized锁的内容有一些差别:
7-8
当一个线程试图访问同步代码时,它首先必须得到锁,正常退出或抛出异常时必须释放锁
所有的普通同步方法用的都是同一把锁——实例对象本身,就是new出来的具体实例对象本身,本类this。也就是说如果一个实例对象的普通同步方法获取锁后,该实例对象的其它普通同步方法必须等待获取锁的方法释放锁后才能获取锁
所有的静态方法用的也是同一把锁——类对象本身,就是我们说过的唯一模板class。具体实例对象this和唯一模板class,这两把锁是两个不同的对象,所以静态同步方法和普通同步方法之间是不会有竞态条件的。但是一旦一个静态同步方法获取锁后,其它的静态同步方法都必须等待该方法释放锁后才能获取锁
javap -c ***.class:对代码进行反编译
假设你需要更多信息:javap -v ***.class 文件反编译,-v代表-verbose:输出附加信息(包括行号、本地变量表、反汇编等详细信息)
package com.bilibili.juc.lock.sync; public class LockSyncDemo { Object object = new Object(); public void m1() { synchronized (object) { System.out.println("--------hello synchronized code block--------"); } } public static void main(String[] args) { } }
通过javap -c LockSyncDemo.class反编译
实现使用的是monitorenter和monitorexit指令
一般情况下是一个enter对应两个exit
极端情况:m1方法里面自己添加一个异常试试
package com.bilibili.juc.lock.sync; public class LockSyncDemo { Object object = new Object(); public void m1() { synchronized (object) { System.out.println("--------hello synchronized code block--------"); throw new RuntimeException("--------exp--------"); } } public static void main(String[] args) { } }
反编译结果:一个enter对应一个exit
package com.bilibili.juc.lock.sync;
public class LockSyncDemo2 {
public synchronized void m2() {
System.out.println("--------hello synchronized m2--------");
}
public static void main(String[] args) {
}
}
通过javap -v LockSyncDemo2.class反编译
synchronized普通同步方法调用指令将会检查方法的ACC_SYNCHRONIZED访问标志是否被设置,如果设置了,执行线程会将现持有monitor锁,然后再执行该方法,最后在方法完成(无论是否正常结束)时释放monitor
package com.bilibili.juc.lock.sync; public class LockSyncDemo3 { public synchronized void m2() { System.out.println("--------hello synchronized m2--------"); } public static synchronized void m3() { System.out.println("--------hello static synchronized m3--------"); } public static void main(String[] args) { } }
通过javap -v LockSyncDemo3.class反编译
ACC_STATIC、ACC_SYNCHRONIZED访问标志区分该方法是否是静态同步方法
package com.bilibili.juc.lock.sync; public class LockSyncDemo4 { Object object = new Object(); Book book = new Book(); public void m1() { synchronized (book) { System.out.println("--------hello synchronized code block--------"); } } public static void main(String[] args) { } } class Book /*extends Object // 任何一个对象都默认继承Object*/ { }
我们知道在Java中每一个对象都继承Object类,可写可不写。但是为什么任何一个对象都可以成为一个锁?下面通过C底层源码进一步了解为什么任何一个对象都可以成为一个锁?
一、Java集合类
二、多线程JUC
管程(英语:monitor,也称为监视器)是一种程序结构,结构内的多个子程序(对象或模块)形成的多个工作线程互斥访问共享资源。这些共享资源一般是硬件设备或一群变量。对共享变量能够进行的所有操作集中在一个模块中。(把信号量及其操作原语“封装”在一个对象内部)管程实现了在一个时间点,最多只有一个线程在执行管程的某个子程序。管程提供了一种机制,管程可以看做一个软件模块。它是将共享的变量和对于这些共享变量的操作封装起来,形成一个具有一定接口的功能模块,进程可以调用管程来实现进程级别的并发控制。
结合《JVM虚拟机》这本书
在HotSpot虚拟机中,monitor采用ObjectMonitor实现
在Java中任何一个对象都继承Object类,对应C++源码底层的源码类:ObjectMonitor.java—>ObjectMonitor.cpp—>ObjectMonitor.hpp
ObjectMonitor.hpp类初始化monitor代码:
为什么任何一个对象都可以成为一个锁?因为每个对象天生都带着一个对象监视器,每一个被锁住的对象都会和Monitor关联起来
synchronized必须作用于某个对象中,所以Java在对象的头文件存储了锁的相关信息。锁升级功能主要依赖于 MarkWord 中的锁标志位和释放偏向锁标志位,后续讲解锁升级时候我们再加深,目前为了承前启后的学习,对下图先混个眼熟即可,O(n_n)O
Hotspot的实现
package com.bilibili.juc.lock.fair; import java.util.concurrent.locks.ReentrantLock; /** * 卖票案例:模拟三个售票员卖完50张票 */ public class SaleTicketDemo { public static void main(String[] args) { Ticket ticket = new Ticket(); new Thread(() -> { for (int i = 0; i < 55; i++) { ticket.sale(); } }, "a").start(); new Thread(() -> { for (int i = 0; i < 55; i++) { ticket.sale(); } }, "b").start(); new Thread(() -> { for (int i = 0; i < 55; i++) { ticket.sale(); } }, "c").start(); } } class Ticket { private int number = 50; ReentrantLock lock = new ReentrantLock(); public void sale() { lock.lock(); try { if (number > 0) { System.out.println(Thread.currentThread().getName() + "卖出第:\t" + (number--) + "\t 还剩下:" + number); } } finally { lock.unlock(); } } } 输出结果: a卖出第: 50 还剩下:49 a卖出第: 49 还剩下:48 a卖出第: 48 还剩下:47 a卖出第: 47 还剩下:46 a卖出第: 46 还剩下:45 a卖出第: 45 还剩下:44 a卖出第: 44 还剩下:43 a卖出第: 43 还剩下:42 a卖出第: 42 还剩下:41 a卖出第: 41 还剩下:40 a卖出第: 40 还剩下:39 a卖出第: 39 还剩下:38 a卖出第: 38 还剩下:37 a卖出第: 37 还剩下:36 a卖出第: 36 还剩下:35 a卖出第: 35 还剩下:34 a卖出第: 34 还剩下:33 a卖出第: 33 还剩下:32 c卖出第: 32 还剩下:31 c卖出第: 31 还剩下:30 c卖出第: 30 还剩下:29 c卖出第: 29 还剩下:28 c卖出第: 28 还剩下:27 c卖出第: 27 还剩下:26 c卖出第: 26 还剩下:25 c卖出第: 25 还剩下:24 c卖出第: 24 还剩下:23 c卖出第: 23 还剩下:22 c卖出第: 22 还剩下:21 c卖出第: 21 还剩下:20 c卖出第: 20 还剩下:19 c卖出第: 19 还剩下:18 c卖出第: 18 还剩下:17 c卖出第: 17 还剩下:16 c卖出第: 16 还剩下:15 c卖出第: 15 还剩下:14 c卖出第: 14 还剩下:13 c卖出第: 13 还剩下:12 c卖出第: 12 还剩下:11 c卖出第: 11 还剩下:10 c卖出第: 10 还剩下:9 c卖出第: 9 还剩下:8 c卖出第: 8 还剩下:7 c卖出第: 7 还剩下:6 c卖出第: 6 还剩下:5 c卖出第: 5 还剩下:4 c卖出第: 4 还剩下:3 c卖出第: 3 还剩下:2 c卖出第: 2 还剩下:1 c卖出第: 1 还剩下:0
package com.bilibili.juc.lock.fair; import java.util.concurrent.locks.ReentrantLock; /** * 卖票案例:模拟三个售票员卖完50张票 */ public class SaleTicketDemo { public static void main(String[] args) { Ticket ticket = new Ticket(); new Thread(() -> { for (int i = 0; i < 55; i++) { ticket.sale(); } }, "a").start(); new Thread(() -> { for (int i = 0; i < 55; i++) { ticket.sale(); } }, "b").start(); new Thread(() -> { for (int i = 0; i < 55; i++) { ticket.sale(); } }, "c").start(); } } class Ticket { private int number = 50; ReentrantLock lock = new ReentrantLock(true); public void sale() { lock.lock(); try { if (number > 0) { System.out.println(Thread.currentThread().getName() + "卖出第:\t" + (number--) + "\t 还剩下:" + number); } } finally { lock.unlock(); } } } 输出结果: a卖出第: 50 还剩下:49 a卖出第: 49 还剩下:48 a卖出第: 48 还剩下:47 a卖出第: 47 还剩下:46 a卖出第: 46 还剩下:45 a卖出第: 45 还剩下:44 a卖出第: 44 还剩下:43 a卖出第: 43 还剩下:42 b卖出第: 42 还剩下:41 a卖出第: 41 还剩下:40 c卖出第: 40 还剩下:39 b卖出第: 39 还剩下:38 a卖出第: 38 还剩下:37 c卖出第: 37 还剩下:36 b卖出第: 36 还剩下:35 a卖出第: 35 还剩下:34 c卖出第: 34 还剩下:33 b卖出第: 33 还剩下:32 a卖出第: 32 还剩下:31 c卖出第: 31 还剩下:30 b卖出第: 30 还剩下:29 a卖出第: 29 还剩下:28 c卖出第: 28 还剩下:27 b卖出第: 27 还剩下:26 a卖出第: 26 还剩下:25 c卖出第: 25 还剩下:24 b卖出第: 24 还剩下:23 a卖出第: 23 还剩下:22 c卖出第: 22 还剩下:21 b卖出第: 21 还剩下:20 a卖出第: 20 还剩下:19 c卖出第: 19 还剩下:18 b卖出第: 18 还剩下:17 a卖出第: 17 还剩下:16 c卖出第: 16 还剩下:15 b卖出第: 15 还剩下:14 a卖出第: 14 还剩下:13 c卖出第: 13 还剩下:12 b卖出第: 12 还剩下:11 a卖出第: 11 还剩下:10 c卖出第: 10 还剩下:9 b卖出第: 9 还剩下:8 a卖出第: 8 还剩下:7 c卖出第: 7 还剩下:6 b卖出第: 6 还剩下:5 a卖出第: 5 还剩下:4 c卖出第: 4 还剩下:3 b卖出第: 3 还剩下:2 a卖出第: 2 还剩下:1 c卖出第: 1 还剩下:0
后续深入分析
可重入锁又名递归锁
是指在同一线程在外层方法获取到锁的时侯,在进入该线程的内层方法会自动获取锁(前提,锁对象的是同一个对象),不会因为之前已经获取过还没释放而阻塞
如果是1个有 synchronized 修饰的递归调用方法,程序第2次进入被自己阻塞了岂不是天大的笑话,出现了作茧自缚
所以Java中的 ReentrantLock 和 synchronized 都是可重入锁,可重入锁的一个优点是可一定程度避免死锁
指的是可重复可递归调用的锁,在外层使用锁之后,在内层仍然可以使用,并且不发生死锁,这样的锁就叫做可重入锁
简单的来说就是,在一个synchronized修饰的方法或代码块的内部调用本类的其它synchronized修饰的方法或代码块时,是永远可以得到锁的
package com.bilibili.juc.lock.reentry; public class ReEntryLockDemo { public static void main(String[] args) { final Object o = new Object(); new Thread(() -> { synchronized (o) { System.out.println(Thread.currentThread().getName() + "\t --------外层调用--------"); synchronized (o) { System.out.println(Thread.currentThread().getName() + "\t --------中层调用--------"); synchronized (o) { System.out.println(Thread.currentThread().getName() + "\t --------内层调用--------"); } } } }, "t1").start(); } } 输出结果: t1 --------外层调用-------- t1 --------中层调用-------- t1 --------内层调用--------
package com.bilibili.juc.lock.reentry; public class ReEntryLockDemo2 { public static void main(String[] args) { ReEntryLockDemo2 reEntryLockDemo2 = new ReEntryLockDemo2(); new Thread(() -> reEntryLockDemo2.m1(), "t1").start(); } public synchronized void m1() { System.out.println(Thread.currentThread().getName() + "\t --------m1 come in--------"); m2(); System.out.println(Thread.currentThread().getName() + "\t --------m1 end--------"); } public synchronized void m2() { System.out.println(Thread.currentThread().getName() + "\t --------m2 come in--------"); m3(); } public synchronized void m3() { System.out.println(Thread.currentThread().getName() + "\t --------m3 come in--------"); } } 输出结果: t1 --------m1 come in-------- t1 --------m2 come in-------- t1 --------m3 come in-------- t1 --------m1 end--------
之前解释为什么任何一个对象都可以称为一个锁可以看到ObjectMonitor.hpp类中初始化monitor代码,ObjectMonitor对象中的_recursions
和_count
,锁的重入次数和用来记录该线程获取锁的次数
每个锁对象都拥有一个锁的计数器和一个指向持有该锁的线程的指针
当执行monitorenter时,如果目标锁对象的计数器为零,那么说明它没有被其它线程持有,Java虚拟机会将该锁对象的持有线程设置为当前线程,并且将其计数器加1
在目标对象的计数器不为零的情况下,如果锁对象的持有线程是当前线程,那么Java虚拟机可以将其计数器加1,否则需要等待,直至持有锁的线程释放该锁
当执行monitorexit时,Java虚拟机则需将锁对象的计数器减1。计数器为零代表锁已被释放
package com.bilibili.juc.lock.reentry; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; public class ReEntryLockDemo3 { static Lock lock = new ReentrantLock(); public static void main(String[] args) { new Thread(() -> { lock.lock(); try { System.out.println(Thread.currentThread().getName() + "\t --------外层调用come in--------"); lock.lock(); try { System.out.println(Thread.currentThread().getName() + "\t --------内层调用come in--------"); } finally { lock.unlock(); } } finally { lock.unlock(); } }, "t1").start(); } } 输出结果: t1 --------外层调用come in-------- t1 --------内层调用come in--------
加锁次数和释放锁次数不匹配
package com.bilibili.juc.lock.reentry; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; /** * @ClassName: ReEntryLockDemo2 * @Description: * @Author: zhangjin * @Date: 2023/10/16 */ public class ReEntryLockDemo3 { static Lock lock = new ReentrantLock(); public static void main(String[] args) { new Thread(() -> { lock.lock(); try { System.out.println(Thread.currentThread().getName() + "\t --------外层调用come in--------"); lock.lock(); try { System.out.println(Thread.currentThread().getName() + "\t --------内层调用come in--------"); } finally { lock.unlock(); } } finally { // 由于加锁次数和释放次数不一样,第二个线程始终无法获取到锁,导致一直在等待 // lock.unlock(); } }, "t1").start(); new Thread(() -> { lock.lock(); try { System.out.println(Thread.currentThread().getName() + "\t --------外层调用come in--------"); } finally { lock.unlock(); } }, "t2").start(); } } 输出结果: t1 --------外层调用come in-------- t1 --------内层调用come in-------- // ...程序未结束
隐式锁(即synchronized关键字使用的锁)天生具备可重入性,显式锁(即Lock)的可重入性,需要注意锁了几次就要释放几次
死锁是指两个或两个以上的线程在执行过程中,因抢夺资源而造成的一种互相等待的现象,若无外力干涉,则它们无法再继续推进下去。如果系统资源充足,进程的资源请求能够得到满足,死锁出现的可能性很低,否则就会因为争夺有限的资源而陷入死锁
package com.bilibili.juc.lock.dead; import java.util.concurrent.TimeUnit; public class DeadLockDemo { final static Object a = new Object(); final static Object b = new Object(); public static void main(String[] args) { new Thread(() -> { synchronized (a) { System.out.println(Thread.currentThread().getName() + "线程持有a锁,试图获取b锁"); try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); } synchronized (b) { System.out.println(Thread.currentThread().getName() + "线程成功获取到b锁"); } } },"t1").start(); new Thread(() -> { synchronized (b) { System.out.println(Thread.currentThread().getName() + "线程持有b锁,试图获取a锁"); try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); } synchronized (a) { System.out.println(Thread.currentThread().getName() + "线程成功获取到a锁"); } } },"t2").start(); } } 输出结果: t1线程持有a锁,试图获取b锁 t2线程持有b锁,试图获取a锁 // ...程序未结束
jconsole
深度源码分析见后面
深度源码分析见后面
深度源码分析见后面
深度源码分析见后面
指针指向Monitor对象(也称为管程或监视器)的真实地址。每个对象都存在着一个monitor与之关联,当一个monitor被某个线程持有后,它便处于锁定状态。在Java虚拟机(HotSpot)中,monitor是由ObjectMonitor实现的,具体图解如下:
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。