0.背景
文章目录层级较多,参考我这篇文章来进行展开,方便阅读:博客园SimpleMemory主题如何浮动目录显示
参考文章:
synchronized的底层是基于Java的监视器monitor的。
在Java中,monitor
(监视器)是用来实现线程同步的一种基本机制。
它是一个控制结构,内置在每个Java对象中,用于解决多线程之间的互斥和协作问题。
我们先要知道以下2点:
- 每个对象可以有1个与之关联的锁(也称为互斥锁或monitor锁),这把锁是在对象被用作同步时由 JVM 动态管理的。
- 每个对象都有3个用于线程间通信的方法
- wait()
- notify()
- notifyAll()
查看Object
类,可以看到这几个方法。
其中,notify()
、notifyAll()
、wait(long timeout)
都是native
的,而wait()
和wait(long timeout, int nanos)
则是基于wait(long timeout)的重载。
public final native void notify();
public final native void notifyAll();
public final native void wait(long timeout) throws InterruptedException;
public final void wait() throws InterruptedException {
wait(0);
}
public final void wait(long timeout, int nanos) throws InterruptedException {
if (timeout < 0) {
throw new IllegalArgumentException("timeout value is negative");
}
if (nanos < 0 || nanos > 999999) {
throw new IllegalArgumentException(
"nanosecond timeout value out of range");
}
if (nanos > 0) {
timeout++;
}
wait(timeout);
}
由于Object
是顶层父类,所以任意变量实例化后,都会自动继承其中的方法,wait()、notify()、notifyAll()亦是如此,任何实例对象都会自动具备。
1.概念
在HotSpot虚拟机中,monitor是由ObjectMonitor实现的,源码由C++实现,位于HotSpot虚拟机源码ObjectMonitor.hpp文件中
src/share/vm/runtime/objectMonitor.hpp
在Java中,监视器(monitor)是与对象关联的,而不是与类、接口或抽象类直接关联。
这里的知识搜了下比较复杂,我们可以先这么简单的理解:
所有对象都有潜在的能力关联监视器,但并不是说每个对象都默认地与一个监视器实体积极关联。
对象的监视器(或说锁)只在必要时,即对象被用于synchronized
时,才被激活和使用。
类比我们常规的加锁操作,锁这玩意,是一个外部的东西。
比如老生常谈的厕所这个例子,厕所门上的锁,相对于厕所本身,是个外部的、第三方的东西,我们依据它来裁定使用权。
两个线程出现问题了,涉及到竞态,我们就要寻求一个外部的东西来加锁,这个外部的东西,就是监视器。
这是一种思想,那我们抽象下,不用监视器,我们只要用一个外部的、第三方的能拿来判定的东西就好了。
比如说,分布式环境下,两个程序需要竞争了,我们是不是开始寻求一个外部的裁判redis,来,你们谁能给我拿到,谁就可以操作。
嗯,又比如我两都要去参加王者荣耀的线下比赛,但是我们都是打野位置,到底谁去?嗯 ,简单的方法就是今晚看谁先上2100分,这就是外部的一个判定条件。
就加了个synchronized关键字啊,都没有写锁的代码,当时是底层来帮你处理啦。
2.锁和锁的对象
我们可以先简单的归纳出一个基本的运行模式:
- 当一个线程尝试进入一个由
synchronized
关键字同步的代码块或方法时,它需要获取那个对象的监视器。 - 如果监视器是空闲的,则线程获取它,并进入同步代码块。
- 如果监视器已经被另一个线程持有,则当前线程将阻塞,直到监视器被释放。
2.1 锁定
当使用
synchronized
关键字时,锁定某个对象或类的Class
对象。
实际上意味着任何线程想要执行该对象或类中的同步代码部分,它必须首先获得那个特定对象或 Class
对象背后的锁。
什么是锁定,锁定就是说,你不能用了,你得等解锁后才能继续。
它是一个状态,代表着已经上锁了,不能乱用。
锁定xx,到底蕴含着什么意思啊?
锁定的关键,在于你锁定谁了,你就必须持有谁背后的那把锁,才能继续操作。
关键字
持有
,看到锁定,我们就要思考,谁持有?
回顾下,synchronized
主要作用于3个地方:
- 实例方法
- 静态方法
- 同步代码块
2.1.1 实例方法
当
synchronized
关键字用于实例方法时,它会锁定调用该方法的对象(通常称为this
对象)。
这意味着,要执行这个同步方法,线程必须首先获得这个对象的锁。
public class Example {
public synchronized void instanceMethod() {
// code
}
}
实例方法,必然是由某个实例对象调用的,假设这个实例对象是A。
假设我们有线程1和2要使用这个对象A的instanceMethod()
方法。
现在,线程1获取到这个对象A上的锁后,线程1就锁定对象A,这个时候,线程2未持有对象A的锁,所以,线程2无法执行。
2.1.2 静态方法
当
synchronized
关键字用于静态方法时,它会锁定这个类的所有对象共享的Class
对象。
因为静态方法是属于类的而不是某个对象,所以用类本身作为锁的对象。
public class Example {
public static synchronized void staticMethod() {
// code
}
}
静态方法,是直接由类进行调用的。
假设我们有线程1和2要使用这个Example类的staticMethod()
方法。
现在,线程1获取到这个Example类的锁后,线程1就锁定Example类,这个时候,线程2未持有Example类的锁,所以,线程2无法执行。
2.1.3 同步代码块
这种方式允许你指定锁定的对象,提供更细粒度的锁控制。
可以是任何对象,例如一个特定的对象实例、类的 Class
对象,或者是方法中的任何其他对象。
public class Example {
private final Object lock = new Object();
public void method() {
synchronized(lock) {
// synchronized block code
}
}
}
同步代码块,可能位于一个实例方法中,也可能位于一个静态方法中,比如下图这样。
那么,还是两个线程1和线程2,他们运行method1()
和method2()
会怎么样呢?
-
method1()
- 这个方法使用一个非静态同步代码块,锁定的是
lock
对象。 lock
是一个实例变量,不同的实例将会拥有不同的lock
对象。- 如果两个线程使用
同一个实例
调用method1()
,它们将会互相阻塞,因为它们试图获取同一个lock
对象的监视器。 - 如果每个线程使用
不同的实例
调用method1()
,它们将不会互相阻塞,因为每个实例的lock
对象是不同的,它们各自锁定自己的锁对象。
- 这个方法使用一个非静态同步代码块,锁定的是
-
method2()
- 这个方法使用一个静态同步代码块,锁定的是
LOCK2
对象。 LOCK2
是一个静态变量,所有的 类实例共享同一个LOCK2
对象。- 无论哪个线程或哪个实例调用
method2()
,它们都会试图获取同一个LOCK2
对象的监视器。 - 意味着全局只有一个线程可以在任何时刻进入
method2()
的同步代码块,如果有多个线程尝试调用method2()
,它们将会因为LOCK2
而互相阻塞。
- 这个方法使用一个静态同步代码块,锁定的是
2.2 扩展
上面的例子看下来,好像懂了,又好像没懂。到底有没有一个简单点的方法论,能让我判断xx线程能不能执行啊?
关键点在于,你要找到这把锁到底锁在哪,当前线程能拿到吗?
跟我重复背诵:1.锁在哪 2.能拿到吗
跟我重复背诵:1.锁在哪 2.能拿到吗
跟我重复背诵:1.锁在哪 2.能拿到吗
2.2.1 demo1
现在再来结合另一个例子演示下
package cn.yang37.thread;
import lombok.extern.slf4j.Slf4j;
/**
* @description:
* @class: MySync
* @author: [email protected]
* @date: 2024/5/15 11:06
* @version: 1.0
*/
@Slf4j
public class MySync {
public static void main(String[] args) {
Sync sync = new Sync();
new Thread(
() -> {
sync.m1();
}, "t1")
.start();
new Thread(
() -> {
sync.m2();
}, "t2")
.start();
log.info("mmmmmmmmmmmmmm");
}
}
@Slf4j
class Sync {
public void m1() {
try {
// Thread.sleep(1000);
log.info("11111111111111");
} catch (Exception e) {
log.error("error...", e);
}
}
public void m2() {
log.info("22222222222222");
}
}
main方法中启动了2个线程,分别运行了m1、m2两个方法,这个时候,没有使用synchronized
关键字,可以看到输出是无序的,依赖于cpu对t1、t2、main这3个线程的调度顺序。
- m、2、1
- 2、1、m
- 2、m、1
- 1、m、2
2.2.1.1 添加sleep
尝试为m1方法添加sleep(1000),可以看到,此时会最后输出。
因为线程1、2近似同时启动,由于线程1中会休眠1s,故最后输出信息。
2.2.1.2 m1和m2添加synchronized
将m1和m2变成同步方法,现在再来运行,注意,m1中会sleep。
现在可以看到,111将会固定比222先输出。
方法:看锁定的是谁,当前线程中能拿到吗?
此时,synchronized
锁定的是当前对象,线程1和线程2都是访问的sync对象。
Sync sync = new Sync();
线程1启动后,立马占用了sync这个对象的锁。此时,哪怕线程2想要运行,也拿不到锁,得等线程1先运行完释放。
所以现象就是,222肯定得等111输出完释放锁。
2.2.1.3 m2移除synchronized
现在,又可以看到222比111先输出了。
还是刚才的点,因为m2不是同步方法,不需要获取锁,所以哪怕m1占用了也没事。
2.2.1.4 同时调用m1
此时,由于t1先持有锁,t2必须等待,所以,t2固定后输出111。
2.2.1.5 不同的对象调用m1
此时,由于线程1和2持有的锁不同,一个是sync对象的,一个是sync2对象的,所以不会等待,可以看到t2线程先输出的情况。
因为没有竞争,t1先输出也是正常的。
2.2.1.6 修改为静态方法
虽然会告警不应该通过实例对象调用静态方法,但是此处为了演示,我们先忽略。
此时,线程1固定线程2先输出,哪怕它们用的对象不同,因为,此时锁定的是Sync.class
这个类,线程1和2是存在竞争的。
2.2.1.7 代码块
调整为静态代码块,使用this。
this啥意思,就是调用方法的这个玩意,这里是谁,就是sync这个对象,所以,t1固定比t2先输出。
2.2.1.7 代码块中使用同一个对象
为m1添加入参,锁对象由入参传入。
此时t1固定比t2先输出,因为它们都使用的o1对象。
2.2.1.8 代码块中使用不同的对象
调整传入的锁对象分别为o1和o2,可以看到,此时t2可能比t1先输出。
t1比t2先输出,也是正常的。
因为,此时锁定的对象是不同的,分别是o1和o2,所以,不存在竞争关系。
2.2.2 demo2
代码里搜索了下synchronized
,咱也不知道这是哪个类,主打一个随机展示。
看到这里有个synchronized
,如何分析?
可以看到里面的fSymbolTable
,所以关键点就是,fSymbolTable这个玩意咱们的线程1和线程2如何访问的。
找到这个fSymbolTable
的来源,哦原来这是个成员变量,从构造方法传入的。
看下这几个构造方法:
public SynchronizedSymbolTable(SymbolTable symbolTable) {
fSymbolTable = symbolTable;
} // <init>(SymbolTable)
// construct synchronized symbol table of default size
public SynchronizedSymbolTable() {
fSymbolTable = new SymbolTable();
} // init()
// construct synchronized symbol table of given size
public SynchronizedSymbolTable(int size) {
fSymbolTable = new SymbolTable(size);
} // init(int)
哦,无参没有就新建,有就用这个,同时支持一个传递size的。
那么问题的关键点就是,多个线程能否同时访问到SynchronizedSymbolTable中的fSymbolTable。
protected SymbolTable fSymbolTable;
我们可以看到方法都是非static的,即实例方法。
实例方法怎么用,实例方法肯定是new出来的对象用的,那么,new出来的这个SynchronizedSymbolTable对象,内部的fSymbolTable是不是唯一的?
显然,每个类中的fSymbolTable这个变量是唯一的,多个线程通过同一个SynchronizedSymbolTable对象访问时,访问到的fSymbolTable是固定的。
故,我们可以有以下结论:
这个SynchronizedSymbolTable的addSymbol()方法是线程安全的
多个线程执行addSymbol()方法时,会锁住SynchronizedSymbolTable中的fSymbolTable,此时别的线程无法操作。
来来来,上上最新的CHATGpt 4o
,让它分析下。
2.2.2.1 整体角度
1.方法定义
public String addSymbol(String symbol) {
synchronized (fSymbolTable) {
return fSymbolTable.addSymbol(symbol);
}
}
2.并发安全
- 同步机制:该方法使用
synchronized
关键字对fSymbolTable
对象加锁,确保同一时间只有一个线程能够执行addSymbol
方法,防止并发问题。 - 锁的对象:锁住的是
fSymbolTable
实例,即实际的SymbolTable
对象。
3.竞争关系举例
-
竞争关系:多个线程同时调用
addSymbol
方法时,只有一个线程能获取到fSymbolTable
的锁,其它线程必须等待锁的释放。 -
实际例子
-
场景:假设有两个线程
Thread A
和Thread B
,都试图将不同的符号添加到SymbolTable
中。 -
操作流程
-
Thread A
调用addSymbol
,获取fSymbolTable
的锁。 -
在
Thread A
完成addSymbol
操作之前,Thread B
也调用addSymbol
,但必须等待Thread A
释放锁。 -
Thread A
释放锁后,Thread B
获取锁并执行addSymbol
方法。
-
-
4.实际应用例子
假设一个编译器使用 SynchronizedSymbolTable
来存储唯一的标识符。多个线程解析代码并将标识符添加到符号表:
java复制代码SynchronizedSymbolTable symbolTable = new SynchronizedSymbolTable();
// Thread A
new Thread(() -> {
symbolTable.addSymbol("variableA");
}).start();
// Thread B
new Thread(() -> {
symbolTable.addSymbol("variableB");
}).start();
在上述例子中,Thread A
和 Thread B
会竞争 fSymbolTable
的锁,确保每次只有一个线程能够向符号表中添加符号,从而保证并发安全。
2.2.2.2 成员变量角度
每个 SynchronizedSymbolTable
实例中的 fSymbolTable
变量是唯一的。换句话说,fSymbolTable
是每个 SynchronizedSymbolTable
对象的成员变量,每个实例都有自己的 fSymbolTable
对象。
1.实例唯一性
-
每个
SynchronizedSymbolTable
实例在创建时,会初始化自己的fSymbolTable
变量,指向一个新的或现有的SymbolTable
对象。 -
例如:
SynchronizedSymbolTable table1 = new SynchronizedSymbolTable(); SynchronizedSymbolTable table2 = new SynchronizedSymbolTable();
2.锁的作用
synchronized (fSymbolTable)
对象锁定fSymbolTable
的实例,确保每次只有一个线程可以访问该SymbolTable
实例的方法。- 因此,锁的范围仅限于每个
SynchronizedSymbolTable
实例的fSymbolTable
,不会影响其他SynchronizedSymbolTable
实例。