(实验)Java一个线程用synchronized嵌套锁多个对象时调用wait()只释放wait函数关联的所对象还是释放所有锁对象

来源:互联网 发布:阿里云购买云服务器 编辑:程序博客网 时间:2024/06/02 01:17

实验是在JDK1.8下做的。


题目起的比较拗口,其实用代码说明起来更简单,如下所示:

public class MultiSynchronizedTest {    private static Object lock1 = new Object();    private static Object lock2 = new Object();    private static class Task1 implements Runnable {        @Override        public void run() {            synchronized (lock1) {                synchronized (lock2) {                    try {                        lock1.wait();                    } catch (InterruptedException e) {                        e.printStackTrace();                    }                }            }        }    }}

当一个线程运行Task1时,通过synchronized顺序获得了lock1和lock2的锁,然后在最里层调用锁(lock1或者lock2,下面以lock1为例)的wait()函数,然后按照教科书式的说法,线程进入waiting状态,释放锁,等别的线程调用notify()或者notifyAll()来再次唤醒到runnable状态。那么问题来了,释放锁是一个笼统的说法,到底是只释放wait()函数关联的对象锁(即lock1)还是释放线程当时持有的所有锁(即lock1和lock2)。


直观上来讲,我只调用了lock1.wait()函数,当然只释放lock1。而且我调用哪个对象的wait()就只释放哪个对象的锁,这样程序也更可控。在这里提前先告诉大家,经过实验,结果确实是上面讲的那样:一个线程通过synchronized嵌套锁住多个对象,然后在最里层调用wait()函数,只释放wait()函数关联的锁对象,而不是释放线程当时持有的全部锁


但是我们也可以直观说线程调用锁对象的wait()函数时,就是释放线程当时持有的所有锁嘛——要不通过wait()自身某种回调机制来释放,或者JVM使得线程进入waiting(或者time_waiting)状态时会统一把持有的锁都释放了。但是直观归直观,信息科学技术(拔的太高了?IT码活)永远是个实践出真知的领域,Object.wait()是个native函数,看明白原理要看cpp源码,JVM就更不用说了,在不研究源码的前提下,做个实验室最方便了。


下面我们用两把全局锁,两个线程和jstack、jps工具来验证下。完整的代码如下:

package com.jxshen.example.jdk.lock;public class MultiSynchronizedTest {    private static Object lock1 = new Object();    private static Object lock2 = new Object();    public static void main(String[] args) {        Runnable task1 = new Task1();        Thread thread1 = new Thread(task1, "task1");        thread1.start();        Runnable task2 = new Task2();        Thread thread2 = new Thread(task2, "task2");        thread2.start();    }    private static class Task1 implements Runnable {        @Override        public void run() {            synchronized (lock1) {                System.out.println("Task1 obtain lock1");                synchronized (lock2) {                    System.out.println("Task1 obtain lock2");                    try {                        lock1.wait();                    } catch (InterruptedException e) {                        e.printStackTrace();                    }                }            }        }    }    private static class Task2 implements Runnable {        @Override        public void run() {            try {                Thread.sleep(2000L);            } catch (InterruptedException e) {                e.printStackTrace();            }            synchronized (lock2) {                System.out.println("Task2 obtain lock2");            }        }    }}

lock1和lock2是两个静态全局锁,线程task1(代码里我把线程名和任务名取做一致,到时候看jstack方便点)顺序获得lock1和lock2,并在最里层马上调用lock1.wait()。为了保证线程task2在task1释放锁之后尝试获取锁,task2在一开始先sleep 2秒。注意线程task2尝试synchronized的锁一定要和task1调用wait()关联的锁不一样,否则task2马上能够获得锁。这里task2尝试获取lock2,如果线程调用wait()只释放关联的锁对象,那么task2获取不到lock2,会阻塞在那;否则task2获取lock2成功,马上打印出字符串。


运行上面的程序,从控制台(图1)可以看出线程task2并没有进入synchronized块。然后我们通过jstack工具看下这两个线程的具体状态。


图1


首先在命令行输入“jps -l”,获得图2所示:


图2

可以找到我们运行程序对应的进程号pid为27584。jps这个jdk工具可以查看当前用户java进程的简要状态,参数有l和v,具体用法可以网上查找。注意jps只能给出归属当前用户的java进程,要是想查找全部的java进程,windows下要利用下任务管理器,linux下需要top或者ps命令。


然后我们在命令行输入“jstack 27584”,获得图3所示:


图3

jstack具体展示了某个jvm进程在某时刻的堆栈快照。我们可以看到线程task1依次获取了两个锁,分别是0x00000000d5a5b500(lock1)和0x00000000d5a5b510(lock2)。随后线程task1进入了waiting(on object monitor)状态,等待的锁对象是0x00000000d5a5b500(lock1),对应代码里的lock1.wait()。


然后看线程task2,处于blocked(on object monitor)状态,等待的锁对象是0x00000000d5a5b510(lock2)。可以证明线程task1在wait后并没有释放掉所有的锁,只是释放了代码里调用wait()的锁。


其实这篇文章的主题是个很琐碎的细节,实际中遇到的情况很少,而且直观上也能想到答案。只是现在一些教材和网络文章中,很多对比sleep和wait,只是说“sleep不释放线程持有的锁,wait释放线程持有的所锁”,wait释放锁、wait释放锁、wait释放锁...(面试要应付的知识点重复三遍?)。很多初学者仅仅记住这个答案,会导致一些误解,其实准确的来说是obj.wait()函数只释放线程持有的obj锁,而不是释放线程所有的锁,或者说wait()函数之所以是成员函数而不是静态函数,就是只和具体的实体关联(大家可以换位思考下为什么Thread.sleep()函数是静态的)。









阅读全文
0 0