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

实验是在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()函数是静态的)。









  • 16
    点赞
  • 18
    收藏
    觉得还不错? 一键收藏
  • 6
    评论
超级有影响力的Java面试题大全文档 1.抽象: 抽象就是忽略一个主题中与当前目标无关的那些方面,以便更充分地注意与当前目标有关的方面。抽象并不打算了解全部问题,而只是选择其中的一部分,暂不用部分细节。抽象包括两个方面,一是过程抽象,二是数据抽象。 2.继承:  继承是一种联结类的层次模型,并且允许和鼓励类的重用,它提供了一种明确表述共性的方法。对象一个新类可以从现有的类中派生,这个过程称为类继承。新类继承了原始类的特性,新类称为原始类的派生类(子类),而原始类称为新类的基类(父类)。派生类可以从它的基类那里继承方法和实例变量,并且类可以修改或增加新的方法使之更适合特殊的需要。 3.封装:  封装是把过程和数据包围起来,对数据的访问只能通过已定义的界面。面向对象计算始于这个基本概念,即现实世界可以被描绘成一系列完全自治、封装的对象,这些对象通过一个受保护的接口访问其他对象。 4. 多态性:  多态性是指允许不同类的对象对同一消息作出响应。多态性包括参数化多态性和包含多态性。多态性语言具有灵活、抽象、行为共享、代码共享的优势,很好的解决了应用程序函数同名问题。 5、String是最基本的数据类型吗?  基本数据类型包括byte、int、char、long、float、double、boolean和short。  java.lang.String类是final类型的,因此不可以继承这个类、不能修改这个类。为了提高效率节省空间,我们应该用StringBuffer类 6、int 和 Integer 有什么区别  Java 提供两种不同的类型:引用类型和原始类型(或内置类型)。Int是java的原始数据类型,Integer是java为int提供的封装类。Java为每个原始类型提供了封装类。 原始类型 封装类 boolean Boolean char Character byte Byte short Short int Integer long Long float Float double Double  引用类型和原始类型的行为完全不同,并且它们具有不同的语义。引用类型和原始类型具有不同的特征和用法,它们包括:大小和速度问题,这种类型以哪种类型的数据结构存储,当引用类型和原始类型用作某个类的实例数据所指定的缺省值。对象引用实例变量的缺省值为 null,而原始类型实例变量的缺省值与它们的类型有关。 7、String 和StringBuffer的区别  JAVA平台提供了两个类:String和StringBuffer,它们可以储存和操作字符串,即包含多个字符的字符数据。这个String类提供了数值不可改变的字符串。而这个StringBuffer类提供的字符串进行修改。当你知道字符数据要改变的候你就可以使用StringBuffer。典型地,你可以使用 StringBuffers来动态构造字符数据。 8、运行异常与一般异常有何异同?  异常表示程序运行过程中可能出现的非正常状态,运行异常表示虚拟机的通常操作中可能遇到的异常,是一种常见运行错误。java编译器要求方法必须声明抛出可能发生的非运行异常,但是并不要求必须声明抛出未被捕获的运行异常。 9、说出Servlet的生命周期,并说出Servlet和CGI的区别。  Servlet被服务器实例化后,容器运行其init方法,请求到达运行其service方法,service方法自动派遣运行与请求对应的doXXX方法(doGet,doPost)等,当服务器决定将实例销毁的调用其destroy方法。 与cgi的区别在于servlet处于服务器进程中,它通过多线程方式运行其service方法,一个实例可以服务于多个请求,并且其实例一般不会销毁,而CGI对每个请求都产生新的进程,服务完成后就销毁,所以效率上低于servlet。 10、说出ArrayList,Vector, LinkedList的存储性能和特性  ArrayList 和Vector都是使用数组方式存储数据,此数组元素数大于实际存储的数据以便增加和插入元素,它们都允许直接按序号索引元素,但是插入元素要涉及数组元素移动等内存操作,所以索引数据快而插入数据慢,Vector由于使用了synchronized方法(线程安全),通常性能上较ArrayList差,而LinkedList使用双向链表实现存储,按序号索引数据需要进行前向或后向遍历,但是插入数据只需要记录本项的前后项即可,所以插入速度较快。 11、EJB是基于哪些技术实现的?并说出SessionBean和EntityBean的区别,StatefulBean和StatelessBean的区别。 EJB包括Ses

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 6
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值