深入解析Java进程同步:synchronized()实现同步与互斥及this和static的区别

在 Java 里,不存在像 PV 操作那样的方法,也不存在进程互斥相关的方法。JAVA 的进程同步借助 synchronized()来达成。需明确,JAVA 的 synchronized()方法类似操作系统概念里的互斥内存块。在 JAVA 的 Object 类型中,都携带有一个内存锁。当有线程获取到该内存锁后,其他线程便无法访问该内存,以此实现 JAVA 中简单的同步以及互斥操作。明白这个原理,就能理解为何 synchronized(this)与 synchronized(static XXX)有区别。synchronized 是针对内存区块申请内存锁。this 关键字代表类的一个对象,所以其内存锁是针对相同对象进行互斥操作。而 static 成员属于类专有,其内存空间为该类所有成员所共有。这就使得 synchronized()对 static 成员加锁时,相当于对类加锁,即在该类的所有成员间实现互斥,在同一时间只有一个线程可访问该类的实例。如果只是单纯地想要在 JAVA 中实现线程互斥,知晓这些基本就已经足够了。然而,如果需要让线程之间相互唤醒,那就需要借助 Object.wait()以及 Object.nofity()了。

深入解析Java进程同步:synchronized()实现同步与互斥及this和static的区别

Obj.wait()需要与 Obj.notify()一起使用,并且必须与 synchronized(Obj)配合。也就是说,wait 和 notify 是针对已经获取了 Obj 锁的情况进行操作的,从语法层面来讲,Obj.wait()和 Obj.notify 必须在 synchronized(Obj){...}这个语句块内部。从功能方面而言,wait 意味着线程在获取到对象锁之后,会主动将对象锁释放掉,与此同时,本线程会进入休眠状态。只有当有其他线程调用对象的 notify()方法来唤醒该线程时,它才能够再次获取对象锁,并继续执行后续操作。而相应的 notify()则是对对象锁的唤醒这一操作。需要注意的是,notify()调用之后,不会立即释放对象锁。只有在相应的 synchronized(){}语句块执行结束且自动释放锁之后,JVM 才会在 wait()对象锁的线程中随机选取一个线程,赋予其对象锁并唤醒该线程,使其继续执行。这样就实现了在线程间的同步和唤醒操作。Thread.sleep()能暂停当前线程并释放 CPU 控制权。Object.wait()在释放 CPU 的同时,也会释放对象锁的控制。二者都可以实现暂停当前线程并释放相关控制权的功能,主要区别在于对对象锁控制的释放与否。

仅仅在概念上理解清楚是不够的,必须在实际的例子中进行测试,这样才能更好地理解。关于 Object.wait()和 Object.notify()的应用,最经典的例子当属三线程打印 ABC 的问题了,这是一道较为经典的面试题,题目有如下要求:

创建三个线程,其中一个线程用于打印 10 次 A;另一个线程用于打印 10 次 B;还有一个线程用于打印 10 次 C。需要这些线程同时运行,并且按照交替的方式打印 10 次 ABC。利用 Object 的 wait()和 notify()方法能够很便捷地解决这个问题。代码如下:

Java 代码

公共类 MyThreadPrinter2 实现了 Runnable 接口。

private String name;

private Object prev;

private Object self;

MyThreadPrinter2 是一个私有构造函数,它接受一个名为 name 的字符串、一个名为 prev 的对象以及一个名为 self 的对象作为参数。

this.name = name;

this.prev = prev;

this.self = self;

@Override

public void run() {

int count = 10;

while (count > 0) {

synchronized (prev) {

synchronized (self) {

System.out.print(name);

count--;

self.notify();

try {

prev.wait();

当捕获到 InterruptedException 异常时:

e.printStackTrace();

公共静态的 void 主方法(字符串数组 args)抛出异常。

Object a = new Object();

Object b = new Object();

Object c = new Object();

创建了一个名为 MyThreadPrinter2 的对象 pa,其参数为字符串 "A"、变量 c 和变量 a。

创建了一个名为 pb 的 MyThreadPrinter2 对象,其参数为 "B" 以及变量 a 和 b 。

创建了一个名为 pc 的 MyThreadPrinter2 对象,其参数为 "C"、b 和 c,即 pc = new MyThreadPrinter2("C", b, c);

new Thread(pa).start();

new Thread(pb).start();

new Thread(pc).start(); }

首先来解释其整体思路,从大的方向来看,这个问题属于三线程间的同步唤醒操作。其主要目的是让 ThreadA->ThreadB->ThreadC->ThreadA 这样循环执行三个线程。为了能够控制线程执行的顺序,就必须明确唤醒和等待的顺序。因此,每一个线程都必须同时持有两个对象锁,这样才能继续执行。一个对象锁是 prev,意味着这是前一个线程所拥有的对象锁。同时,还有一个对象锁是自身的对象锁。主要思想是,要控制执行顺序,就得先持有 prev 锁,也就是前一个线程需释放自身对象锁,然后去申请自身对象锁,当两者都具备时进行打印。之后先调用 self.notify()来释放自身对象锁,以唤醒下一个等待线程,接着调用 prev.wait()释放 prev 对象锁,从而终止当前线程,等待循环结束后再次被唤醒。运行上述代码,能够看到三个线程循环打印 ABC,总共进行 10 次。程序运行的主要过程如下:首先是 A 线程开始运行,它持有 C 和 A 对象的锁,然后释放 A 和 C 锁,进而唤醒 B 线程。接着,B 线程等待 A 锁,之后申请 B 锁,接着打印 B,再释放 B 和 A 锁,从而唤醒 C 线程。最后,C 线程等待 B 锁,接着申请 C 锁,然后打印 C,再释放 C 和 B 锁,最终唤醒 A 线程。看起来似乎没什么问题,然而如果你仔细去思考,就会察觉到有问题,这个问题在于初始条件。三个线程按照 A、B、C 的顺序来启动,按照之前的思考,先是 A 唤醒 B,接着 B 唤醒 C,然后 C 再唤醒 A。

但是这种假设依赖于JVM中线程调度、执行的顺序。具体而言,在 main 主线程启动 ThreadA 之后,要等到 ThreadA 执行完毕且在 prev.wait()等待的过程中,才切换回线程去启动 ThreadB。当 ThreadB 执行完毕且在 prev.wait()等待时,再切换回主线程去启动 ThreadC。只有当 JVM 按照这样的线程运行顺序来执行,才能够确保输出的结果是正确的。而这依赖于JVM的具体实现。考虑一种情况:主线程启动 A 后开始执行 A,在这个过程中又切回主线程,接着启动了 ThreadB 和 ThreadC。此时,因为 A 线程还没有释放 self.notify,所以 B 需要在 synchronized(prev)处等待。然而,就在这时,C 却调用 synchronized(prev)获取了对 b 的对象锁。这样,A 调用完之后,ThreadB 同时获取了 prev 也就是 a 的对象锁,此时 ThreadC 的执行条件已经满足,会打印 C。接着 ThreadC 释放 c 以及 b 的对象锁,这时 ThreadB 具备了运行条件,会打印 B。这样一来,循环就变成了 ACBACB 了。这种情况,可以通过在 run 中主动释放 CPU 来进行模拟,代码如下:

Java代码

public void run() {

int count = 10;

while (count > 0) {

synchronized (prev) {

synchronized (self) {

System.out.print(name);

count--;

try{

Thread.sleep(1);

在捕获到 InterruptedException 异常的情况下:

e.printStackTrace();

self.notify();

try {

prev.wait();

} catch (InterruptedException e) {

e.printStackTrace();

运行之后的打印结果变为了 ACBACB。为了规避这种跟 JVM 调度相关的不确定性,需要让 A、B、C 这三个线程按照确定的顺序启动,最终的代码如下:

Java代码

public class MyThreadPrinter2 implements Runnable {

private String name;

private Object prev;

private Object self;

private MyThreadPrinter2(String name, Object prev, Object self) {

this.name = name;

this.prev = prev;

this.self = self;

@Override

public void run() {

int count = 10;

while (count > 0) {

synchronized (prev) {

synchronized (self) {

System.out.print(name);

count--;

try{

Thread.sleep(1);

catch (InterruptedException e){

e.printStackTrace();

self.notify();

try {

prev.wait();

} catch (InterruptedException e) {

e.printStackTrace();

public static void main(String[] args) throws Exception {

Object a = new Object();

Object b = new Object();

Object c = new Object();

MyThreadPrinter2 pa = new MyThreadPrinter2("A", c, a);

MyThreadPrinter2 pb = new MyThreadPrinter2("B", a, b);

MyThreadPrinter2 pc = new MyThreadPrinter2("C", b, c);

new Thread(pa).start();

Thread.sleep(10);

new Thread(pb).start();

Thread.sleep(10);

new Thread(pc).start();

Thread.sleep(10);

这样能将该问题完美解决。从这个例子能看出,很多理论和概念,像 Obj.wait()、Obj.notify()等,理解起来较为容易,但在实际应用中,这里常常是出现问题的地方。需要深入理解,并在解决问题的过程中不断强化对概念的掌握。

本文源自微信公众号 mjw-java,若想获取更多内容,可关注该微信公众号 mjw-java 或者进行访问。

小板凳已备好。

#term_id=100172309

你可能想看:
« 上一篇
下一篇 »

相关推荐

java中什么是冒泡,冒泡算法

2026年09月06日

27阅读

java中冒号,java双冒号

2026年09月06日

26阅读

java中的->,java中的是什么意思

2026年09月06日

24阅读

java中的super用法,Java中super的说明

2026年09月06日

28阅读

java中反射的应用,java中反射的应用场景

2026年09月06日

25阅读

java中开关的定义,Java关键字有哪些

2026年09月06日

26阅读