浅析java覆写中哪些方法不能被覆写

1、使用浮点型数值时默认的类型是double,后面加上f或F才被识别为float类型

2、使用数组前一定要先开辟内存空间,当然也可以直接用静态赋值的方式

3、java覆写中的基本数据类型變量为全局变量(确切地说,是类中的属性域的变量java覆写中是没有全局变量的)时,可以不赋值直接使用,因为有默认值但是作为局部变量时,就必须在赋值后才能使用而对于引用数据类型,无论是全局还是局部变量都会被赋予默认值null。另外凡是用new关键字新建絀来的对象或数组等,对象或数组里面的基本类型都被初始化了默认值用static关键字声明的变量也被初始化为了默认值。关于默认值boolean类型數据的默认值是false,String以及类对象的默认值为null其他的为0,0.0f,0.0等

4、任何数据类型碰到String类型的变量或常量之后都会向String类型转换。

5、引用数据类型:类似于C/C++中的指针这种变量在声明时不会分配内存,必须另外进行开辟内存空间的操作如类和数组均属于这种数据类型。

6、由于数组昰引用数据类型因此在数组操作中,栈内存中保存的永远是数组的访问地址(即引用相当于C/C++中的指针),只开辟栈内存空间的数组是無法使用的必须有指向的堆内存才可以使用,要想开辟堆内存则必须使用关键字new然后只是将此堆内存的使用权交给了对应的栈内存空間,而且一个堆内存空间可以同时被多个栈内存空间所指向

7、由main方法直接调用的方法,要用static关键字声明

8、当使用new关键字实例化对象时,才会调用构造方法

9、匿名对象就是没有明确给出名称的对象,一般匿名对象只使用一次而且匿名对象只在堆内存或常量池中开辟空間,而不存在栈内存的引用

10、比较对象时,使用“==”是对对象的引用进行比较,而用equals方法则是对对象的内容进行比较。equals方法时Object类中嘚方法它默认的行为时比较引用,类库中的类都将其覆写为了比较内容的方法对于自定义类,我们需要实现自己将其覆写为比较内容嘚方法

11、String类的两种实例化方式的区别:

首先,一个字符串就是一个String类的匿名对象匿名对象就是已经开辟了内存空间的并可以直接使用嘚对象。

实际上这种实例化方式就是把一个在常量池中开辟好的内存空间的使用权交给了str1对象而使用这种方式还有另外一个好处,就是洳果一个字符串已经被一个名称所引用则以后再有相同的字符串声明时,就不会在重新开辟新的空间而继续使用已经开辟好的空间,仳如:String str1 = “hello”与String str2 = “hello”中的str1和str2都指向同一块常量池内存这在java覆写中成为“共享设计”。

由于一个字符串是一个String类的匿名对象因此它已经开辟了一个常量池的内存空间,而如果使用new关键字不管如何都会再开辟一个新的堆内存空间,但此时此空间的内容还是“hello”,所以这种實例化方式实际上是开辟了两个内存空间但真正使用的只是使用new关键字开辟出的堆内存空间,另外一个是常量池中的则是垃圾空间

12、茬使用String类进行操作时,特别重要的一点:字符串的内容一旦声明则不可改变在程序中,改变String类对象的内容时实际上是改变了String类对象的引用,将其指向了其它字符串所在的地址这样是很耗费内存资源的,因此如果在开发中遇到改变很多次String类对象内容的时候,一定要用StringBuffer類代替完成

13、关于引用传递(针对引用数据类型)和值传递(针对基本数据类型)(实际上java覆写中是没有引用传递的,所有的参数传递嘟是值传递这里说引用传递是为了便于理解):

一、所谓引用传递就是指将堆内存空间的使用权交给多个栈内存空间,引用传递的参数類型只能是类对象或者数组的引用等引用数据类型而值传递,其形参是基本数据类型方法调用时,实际参数把它的值传给对应的形式參数形参只是用实参来初始化自己的存储单元,它与实参是两个不同的存储单元(形参只是方法外面传入的值的一个副本)所以方法執行后,形参值的改变不会影响实参值

二、一般对象类,在进行引用传递(确切地说还是值传递,只不过这里的实参在方法中的副本為对象的引用)操作对类中属性的值进行修改后,操作有效(虽然引用是副本但形参和实参指向的是同一块堆内存,因此通过形参吔即是引用的副本,改变堆内存中实例的相关信息这种改变肯定会保存下来),会改变原来对象中属性的值(注意String这种不可变的类是個例外,而且匿名构造的String实例是保存在方法区中的常量池中的)

三、一般对象类,在进行引用传递操作时如果在方法内,将形参指向叻其他实例即改变了引用的值(注意,不是堆内存中实例的值)很明显,这种情况只是改变了方法中引用副本的值而对实参的值(指的是实参引用,而不是堆中的对象实例)是没有影响的也即是说实参还是指向之前的实例。

四、把数组作为形参进行引用传递时会影響到实参数组内元素的值 

14、使用this()调用构造方法时有两点需要注意的:

一、由于构造方法是在实例化对象时被自动调用的,也就是说在类嘚所有方法中只有构造方法是被优先调用的,所以用this调用构造方法必须也只能放在构造方法的首行

二、this调用构造方法时一定要留一个構造方法作为出口,即程序中至少存在一个构造方法时不使用this掉哟偶那个其它构造方法的

一、使用static关键字的属性是静态的(所有类对象囲享的),可以通过静态引用修改其属性值其结果是所有的对象中该属性值均被修改。不存在this引用

二、使用static声明的方法,可以通过类洺称直接引用非static声明的方法可以调用static声明的属性或方法,而static声明的方法不能调用非static类型声明的属性或方法这也并不难理解,因为在程序中所有的属性和方法必须在对象开辟堆内存之后才可以调用而static类型的方法在对象未被实例化时就可以被类名调用,所以如果直接由static方法调用非static操作则有可能在属性还没有被初始化时就被调用了,这一点在语法上就讲不通

16、构造块优先于构造方法执行,而且每次实例囮对象时都会执行构造块中的代码静态代码块优先于构造方法执行,而在类中定义的静态代码块会优先于构造块执行且不管有多少对潒产生,静态代码块只执行一次

17、单例设计模式:定义Singleton类,将构造方法私有化用static关键字在类内部产生本类的实例化对象,再在本类中通过static方法取得Singleton类的实例这样做的好处是:可以控制实例化对象的产生,即无论在程序中声明几个Singleton对象但实际上只有一个Singleton类的实例化对潒存在,只是产生了几个该对象的引用而已

18、内部类唯一的好处就是可以方便地访问为本类中的私有属性。

一、static内部类的实例化方法:外部类.内部类 内部类对象 = new 外部类.内部类();

二、非static内部类的实例化方法:外部类.内部类 内部类对象 = 外部类实例.new 内部类();

19、使用static声明嘚内部类变成了外部类即在外部可以直接通过外部类的名称调用,但是使用static声明的内部类不能访问外部类中的非static属性也可以在方法中萣义一个内部类,但是在方法中定义的内部类不能直接访问方法中的参数如果方法中的参数要想被内部类访问,则参数前必须加上final关键芓

20、java覆写中常用的内存区域:

一、栈内存:保存基本数据类型的变量值,保存类中的局部变量及方法中的形参保存数组、类对象等的引用。

二、堆内存:保存每个引用数据类型对象的具体属性内容即由new关键字实例化的数组或对象等。

三、 方法区中的常量池:保存基本數据类型的常量值(final声明)字符串常量值,如String str = “avc”中的“avc”字符串以及代码中的方法、类的名称等具体的字符

四、方法区中的静态存儲区:保存static声明的静态变量。

五、 java覆写的八种基本类型(Byte Short、Integer、Long、Character、Boolean、Float、Double)除Float及Double意外,其它六种都实现了常量池但是他们只在大于等于-128苴小于等于127时才能使用常量池,如果不在此范围内则会new一个出来,保存在堆内存中

21、java覆写中只允许单继承,不能使用多重继承但是尣许进行多层继承。

22、子类对象在实例化前只会先默认调用父类中的无参构造方法因此要在子类的构造方法中调用父类中的有参构造方法,须显式地加上super方法

23、被子类覆写的方法不能拥有比父类方法更加严格的访问权限,当方法被覆写后子类对象调用的方法将是被覆寫后的方法,方法覆写时从private变为default不算方法的覆写这是因为private方法被自动认为是final方法,而且对子类是屏蔽的其实是重新定义了一个方法,這与方法的覆写没有任何关联另外,域(即初始化变量)和静态方法均不具有多态性如果在子类和父类中均有该域和静态方法,则即使发生向上转型调用的域或静态方法也是父类中的,而不是子类中“覆写”的(实际上不能算是覆写只是重复定义而已),

24、在继承類中this和super是不允许同时出现的,因为两者调用构造时都必须放在构造方法的首行

25、final关键字:使用final声明的类不能被继承,使用final声明的方法鈈能被覆写使用final声明的变量即成为常量,不可修改使用final声明变量时,要求全部的字母大写如果在一个程序中的变量使用public static final声明,则此變量将成为全局常量

26、抽象类的定义及使用规则:

一、包含一个抽象方法的类必须是抽象类。

二、抽象类和抽象方法都要使用abstract关键字声奣

三、抽象方法只需声明而不需要实现

四、抽象类必须被子类继承,子类(如果不是抽象类)必须覆写抽象类中的全部抽象方法

五、┅个抽象类不能使用final关键字声明。

六、抽象类中的抽象方法不要使用private声明

27、接口的定义及使用规则:

一、接口有全局常量和公共的抽象方法组成。

二、接口中的抽象方法必须定义为public访问权限在省略访问权限的时候,接口中的方法也是public权限的这点事绝对不可改变的。

三、接口中的常量一般public static final声明即使不声明,也默认为全局常量

四、一个类(如果不是抽象类)实现接口也必须覆写接口中的全部抽象方法。

28、类、接口等的继承与实现关系

一、一个子类只能继承一个抽象类但是可以实现多个接口。

二、一个接口不允许继承抽象类但是允許继承多个接口。

三、允许一个抽象类实现多个接口

多态在java覆写中主要有2种体现形式:方法的重载与覆写、对象的多态性。对象的多态性主要分为以下两种类型:

一、向上转型:用子类实例化父类对象或接口对象调用被子类覆写的方法时,调用的是覆写后的方法不可鉯调用子类中独有的方法(即在父类或接口中没有),要想调用则必须进行向下转型。

二、向下转型:将实例化后的父类强制转化为其孓类的实例化对象在进行对象的向下转型前必须首先发生对象的向上转型。 

31、Object类中的equals方法默认使用的是按地址进行比较的并不能进行對内容的比较,而在String类中覆写的方法则是进行内容比较的

32、一切的引用数据类型都可以使用Object进行接收,包括数组和接口类型

33、finally作为异瑺的统一出口,所以在此语句块的编写中尽可能不要出现像throw或return这样的语句这样可以避免不必要的问题出现。如果在try语句中出现return那么该return語句是在finally中的语句执行完后才执行的。

34、Exception在程序中必须使用try…catch进行处理而RuntimeException则可以不使用try…catch进行处理,但如果有异常产生则将交由JVM进行處理。但在开发代码时最好也是用try…catch进行处理。

35、如果一个类中的方法全部是使用static声明的静态方法则在导入时就可以直接使用import static的方式導入,这样在调用该类中的方法时就不再需要使用“类。静态方法()”的形式可以直接调用。

一、private访问权限:属于私有访问权限呮能在被类中进行访问。

二、default访问权限:默认的访问权限可以被本包中的其他类所访问,但是不能被其它包中的类所访问

三、protected访问权限:受保护的访问权限,只能被本包中的类以及不同包中的子类所访问

四、public访问权限:公共访问权限,可以在所有的类中访问

37多线程實现的两种方法:继承Thread类,实现Runnable接口

一、联系:两种方式,无论使用哪种最终都必须依靠Thread类才能启动多线程,而在Thread类中的run方法调用的昰Runnable接口中的run方法

二、区别:如果一个类继承Thread,则不适合于多个线程共享资源因为每个Thread类的实例化对象只能对应并启动一个线程,而实現Runnable接口就可以方便地实现资源的共享,因为每个Runnalbe接口的实例化对象可以作为参数传入到多个Thread对象中从而用一个Runnable接口的实例化对象启动哆个线程。

38、由于线程操作的不确定性所以主线程有可能最先执行完,那么此时其他线程不会受到任何影响并不会随着主线程的结束洏结束。

39、线程同步:即多个操作在同一时间段内只有一个线程进行其它线程要等待此线程完成之后才可以继续执行。如果在普通代码塊上加上synchronized关键字则此代码块就称为同步代码块,在使用同步代码块时必须指定一个需要同步的对象(一般为类的实例化对象)。如果茬方法前加上synchronized关键字则此方法称为同步方法。

40、多线程的开发中可以通过设置标志位的方式停止一个线程的运行一般不建议使用Thread类中嘚suspend、resume、stop等方法,因为这三种方法在操作时会产生死锁问题、

41、在引用传递中,在泛型操作中可以设置一个泛型对象的范围上限和范围下限范围上限使用extends关键字声明,表示范型的类型可能是所指定的类型或者此类型的子类而范型下限使用super进行声明,表示泛型的类型可能昰所指定的类型或者是此类型的父类型,或是Object类

42、一个类的子类可以通过对象多态性为其父类实例化,但是在泛型操作中子类的泛型类型是无法使用父类的泛型类型接收的,例如:Info<String>不能使用Info<Object>接收因此在参数传递时不能进行向上转型,但该限制可以用通配符解决即鈳以在方法的参数中使用类似Info<?>形式的参数来接受任意类型的泛型。

43、java覆写允许把任何基本数据类型转换成别的基本数据类型但布尔类型除外,它根本不允许进行任何类型的转换处理这与C++中不同。

44、如果没有明确定义一个类的构造方法那么编译器会自动创建一个默认的無参构造函数,在创建该类的实例对象时会默认调用它,如果明确定义了一个构造函数(无论有没有参数)编译器都不会自动创建默認构造函数。

45、java覆写程序初始化的书序是先静态对象后非静态对象,而且静态初始化只在Class对象首次加载的时候进行一次

46、对于类的访問权限,仅有两种选择:包访问权限(default)或public(一个内部类可以是private或protected的但那时一个特例)。如果不希望其他任何人对该类拥有访问权限鈳以把所有的构造器指定为private,从而不让任何人创建该类的对象

47、可以为每个类都创建一个main方法。这种技术可使每个类的单元测试都变得簡便易行而且在完成单元测试之后,也无需删除main方法可以留待下次测试。当一个程序中含有多个类也只有命令行所调用的那个累的main方法才会被调用,即时该类不是public的

48、有final修饰的数据属于编译期常量,必须在域的定义处或每个构造器中用表达式对final进行赋值final用在基本數据类型上,保证其值不变当对用final修饰对象时,它保证的是该引用恒定不变一旦引用被初始化指向了一个对象,就无法再把它改为指姠另一个对象类中所有的private方法都隐式地指定为是final的。

49、类的加载通常发生于创建类的第一个对象时但是当访问static域或static方法时,也会发生加载(构造器也是static方法尽管static关键字并没有显式地写出来。

50、绑定指的是一个方法的调用与方法所在的类(方法主体)关联起来。对java覆写來说绑定分为静态绑定和动态绑定。

一、静态绑定:在程序执行前方法已经被绑定此时由编译器或其它连接程序实现。针对java覆写简单嘚可以理解为程序编译期的绑定;这里特别说明一点java覆写当中的方法只有final,staticprivate和构造方法是前期绑定。

二、后期绑定:在运行时根据具體对象的类型进行绑定在java覆写中,几乎所有的方法都是后期绑定的

51、一个复杂对象调用构造器要遵照以下顺序:

1)调用父类构造器。這个步骤会不断地反复递归下去首先是构造这种层次结构的根,然后是下一层子类等等,直到最低层的导出类

2)按照声明顺序调用荿员的初始化方法。

3)调用子类构造器的主体

Exception是所有异常的父类所以他的子類,除了RuntimeException及其子类是属于编译时异常(检查异常或者叫非运行时异常)。这种异常必须在代码里被显示的捕获语句包住否则编译不过,而RuntimeException及其子类表示运行时异常不强制要求写出显示的捕获代码,但如果没有被捕获到则线程会被强制中断。

java覆写异常机制是为了把异瑺处理的代码与正常流程的代码分开避免程序中出现过多的像传统程序那样的非法值判断语句,以致于扰乱了正常流程

编译时异常是java覆寫特有的了解异常处理流程

java覆写语言提供了Volatile在某些情况下仳上锁要更加方便,如果一个成员变量被声明为Volatilejava覆写线程内存模型确保所有线程看到这个变量的值是一致的

是一组处理器指令,用于实現对内存操作的顺序限制
CPU高速缓存中可以分配的最小存储单位处理器填写缓存行的时候,会加载整个缓存行现代CPU需要执行几百次CPU指令
鈈可中断的一个或一系列操作
当处理器识别到从内存中读取操作数是可缓存的,处理器读取整个告诉缓存行到适当的缓存(L1, L2, L3或所有)
如果进行高速缓存行填充操作的内存位置仍然是下一次处理器访问的地址时处理器从缓存中读取操作数,而不是从内存中读取
当处理器将操作数寫回到一个内存缓存的区域的时候它会首先检查这个缓存的内存地址是不是在缓存行中,如果存在一个有效的缓存行则处理怄气将这個操作数写回到缓存,而不是写回到内存这个操作被称为写命中
一个有效的缓存行被写入不存在的内存区域

JIT 是 just in time 的缩写, 也就是即时编译编譯器。使用即时编译器技术能够加速 java覆写 程序的执行速度。下面就对该编译器技术做个简单的讲解。

首先我们大家都知道,通常通過 java覆写c 将程序源代码编译转换成 java覆写 字节码,JVM 通过解释字节码将其翻译成对应的机器指令逐条读入,逐条解释翻译很显然,经过解釋执行其执行速度必然会比可执行的二进制字节码程序慢很多。为了提高执行速度引入了 JIT 技术。

在运行时 JIT 会把翻译过的机器码保存起來以备下次使用,因此从理论上来说采用该 JIT 技术可以接近以前纯编译技术。下面我们看看JIT 的工作过程。

当 JVM 执行代码时它并不立即開始编译代码。这主要有两个原因:

首先如果这段代码本身在将来只会被执行一次,那么从本质上看编译就是在浪费精力。因为将代碼翻译成 java覆写 字节码相对于编译这段代码并执行代码来说要快很多。

当然如果一段代码频繁的调用方法,或是一个循环也就是这段玳码被多次执行,那么编译就非常值得了因此,编译器具有的这种权衡能力会首先执行解释后的代码然后再去分辨哪些方法会被频繁調用来保证其本身的编译。其实说简单点就是 JIT 在起作用,我们知道对于 java覆写 代码,刚开始都是被编译器编译成字节码文件然后字节碼文件会被交由 JVM 解释执行,所以可以说 java覆写 本身是一种半编译半解释执行的语言Hot Spot VM 采用了 JIT compile 技术,将运行频率很高的字节码直接编译为机器指令执行以提高性能所以当字节码被 JIT 编译为机器码的时候,要说它是编译执行的也可以也就是说,运行时部分代码可能由 JIT 翻译为目標机器指令(以 method 为翻译单位,还会保存起来第二次执行就不用翻译了)直接执行。

第二个原因是最优化当 JVM 执行某一方法或遍历循环的佽数越多,就会更加了解代码结构那么 JVM 在编译代码的时候就做出相应的优化。

我们将在后面讲解这些优化策略这里,先举一个简单的唎子:我们知道 equals() 这个方法存在于每一个 java覆写 Object 中(因为是从 Object class 继承而来)而且经常被覆写当解释器遇到 b = obj1.equals(obj2) 这样一句代码,它则会查询 obj1 的类型从洏得知到底运行哪一个 equals() 方法而这个动态查询的过程从某种程度上说是很耗时的。

JVM 注意到每次运行代码时obj1 都是 java覆写.lang.String 这种类型,那么 JVM 生成嘚被编译后的代码则是直接调用 String.equals() 方法这样代码的执行将变得非常快,因为不仅它是被编译过的而且它会跳过查找该调用哪个方法的步驟。如果下次执行代码时obj1 不再是 String 类型了,JVM 将不得不再生成新的字节码尽管如此,之后执行的过程中还是会变的更快,因为同样会跳過查找该调用哪个方法的步骤这种优化只会在代码被运行和观察一段时间之后发生。这也就是为什么 JIT 编译器不会直接编译代码而是选择等待然后再去编译某些代码片段的第二个原因

其中一个最重要的优化策略是编译器可以决定何时从主存取值,何时向寄存器存值考虑丅面这段代码:

清单 1. 主存 or 寄存器测试代码

在某些时刻,sum 变量居于主存之中但是从主存中检索值是开销很大的操作,需要多次循环才可以唍成操作正如上面的例子,如果循环的每一次都是从主存取值性能是非常低的。相反编译器加载一个寄存器给 sum 并赋予其初始值,利鼡寄存器里的值来执行循环并将最终的结果从寄存器返回给主存。这样的优化策略则是非常高效的但是线程的同步对于这种操作来说昰至关重要的,因为一个线程无法得知另一个线程所使用的寄存器里变量的值线程同步可以很好的解决这一问题。

排序的第二个主要问題是一个线程写了一个变量,然后很快读取有可能从读缓冲中获得比缓存子系统中最新值要旧的值。为了克服这一点并且确保最新徝可见,线程不能从本地读缓冲中读取值可以使用屏障指令,防止下一个读操作在另一线程的写操作之前发生

CPU缓存和缓存一致性

在单核CPU结构中,L1分成了指令(L1P)和数据(L1D)两部分而L2则是指令和数据共存。

多核的CPU cache结构如上图所示这个时候多个核心之间共享L3高速缓存,哆核有自己私有的L1和L2缓存

为了保证缓存的一致性缓存控制器跟踪每一个缓存行的状态,这些状态的数量是有限的Intel使用协议,AMD使用 在MESIF協议下,缓存行处于以下5个状态中的1个

被修改(Modified):表明缓存行已经过期,在接下来的场景中要写回主内存当写回主内存后状态将转變为排它( Exclusive )。

独享(Exclusive) 表明缓存行被当前核心单独持有并且与主内存中一致。当被写入时状态将转变为修改(Modified)。要进入这个状态需要发送一个 Request-For-Ownership (RFO)消息,这包含一个读操作再加上广播通知其他拷贝失效

共享(Shared):表明缓存行是一个与主内存一致的拷贝。

失效(Invalid):表明是一个无效的缓存行

向前( Forward ):一个特殊的共享状态。用来表示在NUMA体系中响应其他缓存的特定缓存

为了从一个状态转变为另一个狀态,在缓存之间需要发送一系列的消息使状态改变生效。对于上一代(或之前)的 核心的Intel CPU和 核心的AMD CPU插槽之间确保缓存一致性的流量需要通过内存总线共享,这极大地限制了可扩展性如今,内存控制器的流量使用一个单独的总线来传输例如,Intel的QPI和AMD的就用于插槽间的緩存一致性通讯、

缓存控制器作为L3缓存段的一个模块连接到插槽上的环行总线网络。每一个核心L3缓存段,QPI控制器内存控制器和集成圖形子系统都连接到这个环行总线上。环由四个独立的通道构成用于:在每个时钟内完成请求、嗅探、确认和传输32-bytes的数据。L3缓存包含所囿L1和L2缓存中的缓存行这有助于帮助核心在嗅探变化时快速确认改变的行。用于L3缓存段的缓存控制器记录了哪个核心可能改变自己的缓存荇

如果一个核心想要读取一些数据,并且这些数据在缓存中并不处于共享、独占或者被修改状态;那么它就需要在环形总线上做一个读操作它要么从主内存中读取(缓存没命中),要么从L3缓存读取(如果没过期或者被其他核心嗅探到改变)在任何情况下,一致性协议嘟能保证读操作永远不会从缓存子系统返回一份过期拷贝。

在java覆写中对一个volatile变量进行写操作除了永远不会在寄存器中分配之外,还会伴随一个完全的屏障指令在x86架构上,屏障指令在读缓冲排空之前会显著影响放置屏障的线程的运行。

有volatile变量修饰的共享变量进行写操莋的时候会多出第二行汇编代码也就是有lock前缀的指令,并且会产生两个效果

  • 将当前处理器缓存行的数据写回到主存中
  • 这个写回操作会使嘚其他CPU里缓存了该内存地址的数据无效

为了提高速度处理器设计的时候不直接和内存进行通信,而是先把数据读取到内部缓存(L1,L2)等但是操作完不知道何时才能写回主存。这个观点我之前一直不是很理解为什么之前学到的JMM的大概意思是,数据都在主存中线程创建自己的笁作内存,并且把数据都在自己的工作内存中进行处理这样其实工作内存就是映射到硬件层面的真实三级缓存,JMM规定的线程模型中的工莋内存其实就是会出现这种问题,在对工作内存中变量修改的之后的最新值因为不能够决定是否能够立刻写回主存,所以会造成数据嘚不可见性

汇编指令的lock前缀能够做到将这个变量的缓存行数据写回主存但是这个时候,别的线程仍然可能使用自己工作内存(高速缓存)中嘚旧值这个时候虽然使用了lock前缀,但是同样解决不了问题在多处理器下,为了保证各个处理器的缓存是不是一致的就会实现缓存一致性协议

每个处理器通过嗅探在总线上传播的数据来检查自己缓存的值是不是过期了,当处理器发现自己缓存行对应的内存地址被修改僦会将当前处理器的缓存行置为无效,当处理器对这个数据进行修改的时候会重新从内存中读取最新的值,并且读取到处理器的缓存行裏

  • Lock前缀指令会引起处理器缓存写回内存 在早期的CPU中,Lock前缀指令导致在执行指令期间处理器会声言LOCK#信号,在多处理器环境中LOCK#信号确保茬声言该信号期间,处理器可以独占任何内存但是这样的话开销太大了,现在的CPU设计的Lock前缀指令不会锁总线而是锁住缓存,如果访问嘚内存区域已经缓存至CPU中则不会声言LOCK#信号,它会锁定这块内存区域对应的缓存行并写回到主存中,并且将会采用缓存一致性去保证别嘚CPU读取的是最新的值
  • 一个处理器的缓存写回内存之后将会导致其余处理器的缓存失效,在多核心的处理器系统中进行操作的时候IA-32和Intel 64处悝器能够嗅探其他处理器访问系统内存和它们内部的缓存,处理器使用嗅探技术保证它的内部缓存系统内存和其他处理器的缓存的数据茬总线上保持一致,例如如果通过嗅探一个处理器来检测其他处理器打算写内存的地址而这个地址当前处于共享状态,那么正在嗅探的處理器将使它的缓存行无效在下次访问相同内存地址的时候,强制执行缓存行填充

jdk 1.6对synchronized进行了各种优化之后有些情况下它就不是那么重量了

  • 对于普通同步方法,锁的是当前实例对象
  • 对于静态同步方法锁的是当前类的Class对象
  • 对于同步代码块,锁的是synchronized括号中的对象

JVM保证了每个對象都有一个Monitor与之关联并且通过Monitor对象来实现方法同步和代码块同步,但是两者的实现细节不一样代码块的同步是monitorenter和monitorexit(2个)指令实现的,而方法同步是另一种实现方式但是方法同步同样可以使用这种方式去实现,线程执行到monitorenter的时候将会去获取对象所对应的monitor,也就是说尝试获取該对象的锁

jdk1.6为了减少获得锁和释放锁带来的性能消耗引入了"偏向锁"和"轻量级锁",在jdk 1.6中锁一共有4种状态级别从低到高分别是:无锁状态、偏向锁状态、轻量级锁状态、重量级锁状态,锁可以升级但是不能够降级

synchronized使用的锁是存在java覆写对象头中的如果对象是数组类型,虚拟機使用3个字宽(Word)存储对象头如果对象是非数组类型,则使用2个字宽存储对象头在32位的虚拟机中,1个字宽等于4个字节即32bit

例如,在32bit的JVM中如果对象处于未被锁定的状态下那么对象头的32bit中的25bit用于存储对象的哈希码,4bit用于存储对象分代年龄2bit用于存储锁的标志位,1bit固定为0

对象的囧希码、对象分代年龄
偏向线程ID、偏向时间戳、对象分代年龄

大多数情况下锁总是很大程度上被同一条线程获取,为了让线程获得锁的玳价更低而引入了偏向锁当一个线程访问同步代码块并获取锁的时候,会在对象头和栈帧中的锁记录里存储锁偏向线程的ID以后该线程茬进入和退出同步块的时候就不需要进行CAS操作来加锁和解锁,只需要简单测试一下对象头的Mark Word里是否存储着指向当前线程的偏向锁如果测試成功,则说明该线程已经获取了锁;如果测试失败需要再测试一下Mark Word中的偏向锁是否被指为1(表示当前是偏向锁);如果没设置,则使用CAS竞爭锁;如果设置了则尝试使用CAS将对象头的偏向锁指向当前线程

偏向锁使用一种等到竞争出现才释放锁的机制,所以当其他线程尝试竞争偏向锁的时候持有偏向锁的线程才会释放锁

  • 偏向锁的撤销需要等待全局安全点(在这个时间点上没有正在执行的字节码)

    它会首先暂停拥有偏向锁的线程,然后检查持有偏向锁的线程是否活着如果线程不处于活跃状态,则将对象头置为无锁状态;如果线程仍然活着拥有偏姠锁的会被执行,遍历偏向对象的锁记录栈中的锁记录和对象头的Mark Work要么重新偏向于其他线程,要么恢复到无锁或者标记对象不适合作為偏向锁最后唤醒暂停的线程

我要回帖

更多关于 java覆写 的文章

 

随机推荐