Java Concurrency in Practice中对象锁重入问题的理解
2015-08-07 20:07
766 查看
原因:Java Concurrency in Practice 中文版21页讲解了关于对象锁的重入的问题,一直没有读懂作者给的例子,今天琢磨了好久,找到了一个可以说服自己的理由……
1 原书内容如下:
当某个线程请求一个由其他线程持有的锁时,发出请求的线程就会阻塞。然而,由于内置锁是可重入的,因此如果摸个线程试图获得一个已经由它自己持有的锁,那么这个请求就会成功。“重入”意味着获取锁的操作的粒度是“线程”,而不是调用。重入的一种实现方法是,为每个锁关联一个获取计数值和一个所有者线程。当计数值为0时,这个锁就被认为是没有被任何线程所持有,当线程请求一个未被持有的锁时,JVM将记下锁的持有者,并且将获取计数值置为1,如果同一个线程再次获取这个锁,计数值将递增,而当线程退出同步代码块时,计数器会相应地递减。当计数值为0时,这个锁将被释放。重入进一步提升了加锁行为的封装性,因此简化了面向对象并发代码的开发。分析如下程序:
public class Father { public synchronized void doSomething(){ ...... } } public class Child extends Father { public synchronized void doSomething(){ ...... super.doSomething(); } }
子类覆写了父类的同步方法,然后调用父类中的方法,此时如果没有可重入的锁,那么这段代码件产生死锁。由于Father和Child中的doSomething方法都是synchronized方法,因此每个doSomething方法在执行前都会获取Child对象实例上的锁。如果内置锁不是可重入的,那么在调用super.doSomething时将无法获得该Child对象上的互斥锁,因为这个锁已经被持有,从而线程会永远阻塞下去,一直在等待一个永远也无法获取的锁。重入则避免了这种死锁情况的发生。
2 我的问题如下:
说实话,读这本书时,我本着一个学生对作者无比崇敬的心情战战兢兢的欣赏着,生怕自己看不懂。当我看完了作者的文字内容之后,除了认同和佩服之外,没有产生任何的疑问。可是当我看到作者的示例代码是,我完全搞不懂了,心情一落千丈。我一度对作者的水平产生了怀疑,但是觉得不太可能是作者的问题,因为我深知自己的水平有多低,应该是我的问题。怀疑如下:假设一个线程t1调用了Childl类某个实例c1的doSomething方法,那么在成功调用之前t1应该先获得c1的锁。接下来调用Father(super:大家都知道,在创建c1需要先创建其父类的实例f1,super作为f1的引用)类某个实例f1的doSomething方法,那么在成功调用之前t1应该先获得f1的锁。也就是说t1线程先后或得了两个不同对象的锁,这怎么能叫重入呢?
3 我的探索如下:
1. 第一步探索class _Father{ public synchronized void dosomething(){ System.out.println("the dosomething method of father"); } public synchronized void mydosomething() throws InterruptedException{ System.out.println("the mysomething method of father"); Thread.sleep(3000); } } public class _JavaConcurrency_01 extends _Father{ public synchronized void dosomething() { System.out.println("the dosomething method of son"); super.dosomething(); } public void mydosomething() throws InterruptedException { super.mydosomething(); } public static void main(String[] args) throws InterruptedException { final _JavaConcurrency_01 s1 = new _JavaConcurrency_01(); // 启动t1执行son.mydosomething方法(不需要锁) // 继续调用super.mydosomething方法,获取到了f1(f1在疑问中阐述)的锁,并让t1续修3秒钟 new Thread(new Runnable() { public void run() { try { s1.mydosomething(); } catch (InterruptedException e) { } } }, "t1").start(); // 确保t1线程先执行 Thread.sleep(100); // 主线程中son.dosomething方法中需要调用f1的dosomething方法,f1的锁被t1抢占,必须等t1释放锁之后主线程才能进入 s1.dosomething(); } }
预测结果:
the mysomething method of father
the dosomething method of son
三秒之后打印下面内容
the dosomething method of father
预测结果分析:
t1调用son.mydosomething方法时不需要获取s1对象的锁,但是son.mydosomething方法中调用了super.mydosomething()方法,获取到f1实例的锁,打印“the mysomething method of father”,然后停顿三秒钟。
主线程停顿100毫秒后执行s1.dosomething方法,该方法需要获取s1实例的锁(获取成功,因为t1没有获取s1实例的锁),打印“the dosomething method of son”,接下来调用super.dosomething,需要获取f1实例的锁(获取失败,f1已经被t1获取,三秒后才会释放f1的锁),主线程阻塞,三秒之后打印“the dosomething method of father”
真实结果:
the mysomething method of father
三秒之后打印下面内容
the dosomething method of son
the dosomething method of father
结果分析:
看到真实结果后,觉得自己太傻太无知了。在真实结果面前,我好像感觉到了一丝丝真相的味道:好像t1在执行_Father类中的mydosomething方法时获得是实例s1的锁并不是f1的锁,也就是说作者说的没错。
2. 第二步探索
这一次我干脆一不做二不休,直接在_JavaConcurrency_01 中创建了一个Father类的实例f1来代替所有super。
class _Father{ public synchronized void dosomething(){ System.out.println("the dosomething method of father"); } public synchronized void mydosomething() throws InterruptedException{ System.out.println("the mysomething method of father"); Thread.sleep(3000); } } public class _JavaConcurrency_01 extends _Father{ _Father f1 = new _Father(); public synchronized void dosomething() { System.out.println("the dosomething method of son"); f1.dosomething(); } public void mydosomething() throws InterruptedException { f1.mydosomething(); } public static void main(String[] args) throws InterruptedException { final _JavaConcurrency_01 s1 = new _JavaConcurrency_01(); new Thread(new Runnable() { public void run() { try { s1.mydosomething(); } catch (InterruptedException e) { } } }, "t1").start(); Thread.sleep(100); s1.dosomething(); } }
预测结果:
the mysomething method of father
the dosomething method of son
三秒之后打印下面内容
the dosomething method of father
预测结果分析:
与第一步探索雷同
真实结果:
与预测结果完全一致,也就说我的说法好像不太对,真想打自己的脸。
3. 第三步探索
s1.mydosomething() ->super.mydosomething()
这次我要探索的是super.mydosomething()方法调用时,默认会传递一个“this”参数,而且this指向调用此方法的实例。我就是想看看这个默认的this指向了s1还是我说的f1。
class Father { public void doSomething() { System.out.print(this); } public String toString() { return "Father"; } } public class Child extends Father { public void doSomething() { super.doSomething(); } public String toString() { return "Son"; } public static void main(String[] args) { Child child = new Child(); child.doSomething(); } }
这里我就不再预测了,人得学会有自知之明呀,直接上结果,结果很可怕,至少我这么觉得,因为太无知,受不了一点惊吓。
真实结果
Son
结果分析,原来super.mydosomething()中默认的”this”参数指向了s1,我的天呢,作者说的一点都没有错呀,真实重入呀。
4. 第四步探索
赶紧利用 javap -verbose Child 命令看了看字节码命令是怎么执行的。
public static void main(java.lang.String[]); Code: Stack=2, Locals=2, Args_size=1 0: new #1; //class _1/Child 3: dup 4: invokespecial #23; //Method "<init>":()V 7: astore_1 8: aload_1 9: invokevirtual #24; //Method doSomething:()V 12: return
字节码命令描述:
0-7行创建了Child类型的一个实例,也就是s1。
astore_1,将操作数栈顶引用类型数值存入本地变量表的第二个(从零开始计数)本地变量位置,也就是把s1存入本地变量表的第一个位置。
aload_1,将s1在推入栈顶。
invokevirtual,执行实例方法doSomething,此时传入的“this”为栈顶元素s1。
public void doSomething(); Code: Stack=1, Locals=1, Args_size=1 0: aload_0 1: invokespecial #15; //Method _1/Father.doSomething:()V 4: return
现在我们来看关键的doSomething方法
0: aload_0,把this推入栈顶,这个“this”是s1的引用。
1: invokespecial #15; 调用父类方法,但是默认传入的“this”参数仍旧指向s1
4 请原谅我的无知:
最后,我找到了一个说服自己的理由,但是其中还是有好多问题,知其然不知其所以然,希望在未来的日子里可以慢慢解决这些问题,让自己变得有学问起来,哈哈。相关文章推荐
- struts2中的bean标签
- Java — 程序设计基础(Core Java I)
- jdk1.5新特性之自动装箱与拆箱
- jdk1.5新特性之增强for循环
- MyEclipse快捷键alt+/设置
- java注意事项
- Java — 开发环境(Core Java I)
- java关键字整理
- 2015届华为校园招聘机试题 (java实现)
- Eclipse工具条中添加下拉按钮
- 【JavaMail开发总结】开发前的配置
- HashMap和HashSet的区别
- Java程序设计之Constructor
- HashSet,TreeSet和LinkedHashSet的区别
- ,HashMap和Hashtable及HashSet的区别
- 使用Spring进行统一日志管理 + 统一异常管理
- Java IO流--IO包中的其他类
- java容器类---ArrayList
- springMVC获取request和response
- Java://Comparator、Comparable的用法(按照要求将set集合的数据进行排序输出):