在看《java并发编程实战》时,讲可重入锁时,子类改写父类的synchronized方法,然后调用父类中的synchronized方法,如果内置锁不是可重入的将导致死锁。
public class Widget{ public synchronized void doSomething(){ ··· } } public class LoggingWidget extends Widget{ public synchronized void doSomething(){ ··· } } 复制代码
我们知道synchronized修饰实例方法,锁对象是this对象。子类中的synchronized应该锁住的是子类对象,父类中的synchronized方法应该锁住的是父类对象。两个方法的锁对象根本不同,所以根本不需要可重入锁。那为什么这里说需要可重入锁?
1.书中写错了
2.其实子类和父类的锁对象为同一个,那么到底是子类对象还是父类对象?
由于synchronized修饰实例方法,锁对象是当前实例对象,我们分别在父类和子类中打印一下this对象,看一下它们究竟是什么。
public class Father{ public synchronized void test(){ System.out.println("father'this="+this); System.out.println("father'super"+super.toString()); } } public class Son extends Father{ @override public synchronized void test(){ System.out.println("son's this="+this); System.out.println("son's super="+super.toString()); super.test(); } } public static void main(String[] args){ Son son = new Son(); son.test(); } //结果 son's this=Son@39b43cbc son's super=Son@39b43cbc father's this=Son@39b43cbc father's super=Son@39b43cbc 复制代码
我们可以看到当用子类调用父类方法时,子类父类的this都是指向同一个引用那么子类和父类的锁对象为同一个,并且为子类对象。 我们用实例验证一下,
//改进一下代码 public class Father{ public synchronized void test(){ System.out.println("father test"); } public synchronized void test2(){ System.out.println("father test2"); while(true); } } public class Son extends Father{ @override public synchronized void test(){ System.out.println("son test"); } @override public void test2(){ super.test2(); } } public static void main(String[] args) { Son son = new Son(); Thread thread1= new Thread(()->{ son.test2(); }); Thread thread2 = new Thread(()->{ son.test(); }); thread1.start(); try { Thread.sleep(1000);//这里可能休眠的时线程1,但是不重要,只要线程1能先启动,获得锁就好 } catch (InterruptedException e) { e.printStackTrace(); } thread2.start(); } //结果 son test2 father test2 //因为thread1中son通过没有被synchronized修饰的test2()调用父类被synchronized修饰的test2()方法 ,如果父类synchronized获取的锁不是和子类不是同一个,那么,thread2通过son调用被synchronized修饰 的test()方法,必然能够进入该方法,因为锁对象不是同一个,然而根据验证结果,锁对象为同一个,并且为子类对象。 复制代码
在上边的实验中,我们看到this和super都指向同一个引用。我们通常理解的是this指向的是本实例对象,super是父类实例对象的一个引用。然而为什么this和super指向了同一个引用?
//对于this,jls中这样描述 When used as a primary expression, the keyword this denotes a value that is a reference to the object for which the instance method or default method was invoked (§15.12), or to the object being constructed. The value denoted by this in a lambda body is the same as the value denoted by this in the surrounding context. //对于super的一些描述 The form super.Identifier refers to the field named Identifier of the current object, but with the current object viewed as an instance of the superclass of the current class. 复制代码
可以看出,this表示一个指向调用当前实例方法的那个对象的引用,而super上边的解释并不明显,我们再看一个其它的解释
The usage of the super reference when applied to overridden methods of a superclass is special; it tells the method resolution system to stop the dynamic method search at the superclass, instead of at the most derived class (as it otherwise does). 复制代码
就是说super具有阻止动态调用的过程。 而这也是为什么有了this关键后,还有super关键字,并且super和this 指向同一个引用。 通过实例来看一下
public class Son extends Father{ public void test(){ this.test2(); super.test2(); } } public class Father{ public void test(){ } public void test2(){ System.out.println("father test2"); } } main()方法: Son son = new Son(); son.test2(); //结果 father test2 father test2 //字节码 public void test(); descriptor: ()V flags: (0x0001) ACC_PUBLIC Code: stack=1, locals=1, args_size=1 0: aload_0 1: invokevirtual #2 // Method test2:()V 4: aload_0 5: invokespecial #3 // Method Father.test2:()V 8: return LineNumberTable: line 4: 0 line 5: 4 line 6: 8 } 复制代码
我们可以看到通过this,super都可以调用父类中的方法或者成员变量
因为对象在堆中保存的实例数据包含了它继承过来的实例数据,所以子类对象中保存了父类的信息,以前的对super的理解“指向父类对象的一个引用”其实是错误的,这里根本就没有父类对象,所谓的父类对象,其实是子类实例数据中继承过来的数据。也就是说在堆中只有一个对象,并没有父类对象。
所以super和this只能指向一个对象,那就是子类对象(其实应该是表示一个指向调用当前实例方法的那个对象的引用)。
那么既然子类已经有父类的实例数据,直接通过this调用即可,那么为什么还要super呢,并且super和this指向的都是同一个对象?
仔细观察上边的字节码就会发现,
this.test2()->invokevirtual super.test2()->invokespecial 复制代码
this是动态调用的,supers是在编译期就已经知道的调用哪一个方法。
所以this,super的目的就是:防止存在方法重写时的调用混乱,this关键字调用方法要经历一个动态分析过程,而super关键字调用的变量或方法是确定的,也就是继承过来的实例数据中的父类的成员变量或成员方法
了解jvm都知道方法中的第一个参数实际是this,我们通过子类调用父类方法,传进去的this实际是子类的this
//jvms中的解释 Note that methods called using the invokespecial instruction always pass this to the invoked method as its first argument. As usual, it is received in local variable 0. 复制代码
//伪代码 public class Father{ public void test(){ } } public Class Son extends Father{ public void test(){ //实际上test参数中的第一个为this,jvm自动帮我们加入的(我这里显式的写上),所以在父类test方法中获取的是同一个this super.test(this); } } 复制代码
那么问题来了,super和this极其相似,那么jvm是否在方法的参数列表中也自动的添加了super呢?
答案是否定的
public void test2(){ } //字节码 public void test2(); descriptor: ()V flags: (0x0001) ACC_PUBLIC Code: //这里 locals=1,说明只有一个变量那就是this stack=0, locals=1, args_size=1 0: return LineNumberTable: line 5: 0 } 复制代码
public class son extends Father{ int a=10; @Override public void test() { System.out.println("son test"); System.out.println("super.a="+super.a); System.out.println("((Father)this).a="+((Father)this).a); super.test2(); ((Father)this).test2(); } @Override public void test2() { System.out.println("son test2"); } } public class Father{ int a=20; public void test(){ System.out.println("father test"); } public void test2(){ System.out.println("father test2"); } } main(): new Son().test(); //结果 son test super.a=20 ((Father)this).a=20 father test2 son test2 复制代码
这里其实是一个动态绑定的结果。
动态绑定的关键点:
子类在方法区维护了一个虚方法表(Vtable,在连接阶段完成),所有对象共享一个vtable
1.如果子类没有重写父类的方法,子vtable直接指向父vtable 2.如果子类重写了父类的方法,子vtable和父vtable索引相同方便查找 复制代码
虚方法表的使用,每个对象就不需要额外的指针空间来保存对每个方法的引用,提高了效率。
以上的分析有的部分为个人根据字节码的理解,对jvm内存分区可能存在一些误解,希望大佬能帮忙指导。