修飾實例方法,對於普通同步方法,鎖定是目前的實例物件
修飾靜態方法,對於靜態同步方法,鎖是目前的Class物件
修飾方法程式碼區塊,對於同步方法區塊,鎖定是synchronized括號裡面配置的物件!
當一個執行緒試圖存取同步程式碼區塊的時候,就必須得到鎖,完成後(或出現異常),就必須釋放鎖。那麼鎖究竟存在什麼地方呢?我們一塊來探究!
不過,相信,既然大家能夠找到這篇文章,相信大家對他的使用早已了熟於心,我們對於使用,以及為什麼多線程情況下,數據會出現錯亂情況,不做詳細的解釋!只把他的幾種使用方式列出,供參考!
修飾實例方法,對於普通同步方法,鎖是目前的實例物件
這個沒得說,使用的同一個實例,加入上synchronized後,執行緒需要排隊,完成一個原子操作,但是注意前提是使用的同一個實例
,他才會生效!
正例:
<code>/**<br> * @author huangfu<br> */<br>public class ExploringSynchronized implements Runnable {<br> /**<br> * 共享资源(临界资源)<br> */<br> static int i=0;<br> public synchronized void add(){<br> i++;<br> }<br><br> @Override<br> public void run() {<br> for (int j = 0; j add();<br> }<br> }<br><br> public static void main(String[] args) throws InterruptedException {<br> ExploringSynchronized exploringSynchronized = new ExploringSynchronized();<br> Thread t1 = new Thread(exploringSynchronized);<br> Thread t2 = new Thread(exploringSynchronized);<br> t1.start();<br> t2.start();<br> //join 主线程需要等待子线程完成后在结束<br> t1.join();<br> t2.join();<br> System.out.println(i);<br><br> }<br>}</code>
反例:
<code>/**<br> * @author huangfu<br> */<br>public class ExploringSynchronized implements Runnable {<br> /**<br> * 共享资源(临界资源)<br> */<br> static int i=0;<br> public synchronized void add(){<br> i++;<br> }<br><br> @Override<br> public void run() {<br> for (int j = 0; j add();<br> }<br> }<br><br> public static void main(String[] args) throws InterruptedException {<br> Thread t1 = new Thread(new ExploringSynchronized());<br> Thread t2 = new Thread(new ExploringSynchronized());<br> t1.start();<br> t2.start();<br> //join 主线程需要等待子线程完成后在结束<br> t1.join();<br> t2.join();<br> System.out.println(i);<br><br> }<br>}</code>
這種,即使你在方法上加上了synchronized也無濟於事,因為,對於普通同步方法,鎖是當前的實例物件!實例物件都不一樣了,那麼他們之間的鎖自然就不是同一個!
修飾靜態方法,對於靜態同步方法,鎖定是目前的Class物件
從定義上可以看出來,他的鎖是類別對象,那麼也就是說,以上面那個類別為例:普通方法的鎖定對象就是new ExploringSynchronized()
而靜態方法對應的鎖定對象就是ExploringSynchronized.class
所以對於靜態方法添加同步鎖,即使你重新建立一個實例,它拿到的鎖還是同一個!
<code>package com.byit.test;<br><br>/**<br> * @author huangfu<br> */<br>public class ExploringSynchronized implements Runnable {<br> /**<br> * 共享资源(临界资源)<br> */<br> static int i=0;<br> public synchronized static void add(){<br> i++;<br> }<br><br> @Override<br> public void run() {<br> for (int j = 0; j add();<br> }<br> }<br><br> public static void main(String[] args) throws InterruptedException {<br> Thread t1 = new Thread(new ExploringSynchronized());<br> Thread t2 = new Thread(new ExploringSynchronized());<br> t1.start();<br> t2.start();<br> //join 主线程需要等待子线程完成后在结束<br> t1.join();<br> t2.join();<br> System.out.println(i);<br><br> }<br>}</code>
當然,結果是我們期待的 200000
修飾方法程式碼區塊,對於同步方法區塊,鎖定是synchronized括號裡面配置的物件!
<code>package com.byit.test;<br><br>/**<br> * @author huangfu<br> */<br>public class ExploringSynchronized implements Runnable {<br> /**<br> * 锁标记<br> */<br> private static final String LOCK_MARK = "LOCK_MARK";<br> /**<br> * 共享资源(临界资源)<br> */<br> static int i=0;<br> public void add(){<br> synchronized (LOCK_MARK){<br> i++;<br> }<br> }<br><br> @Override<br> public void run() {<br> for (int j = 0; j add();<br> }<br> }<br><br> public static void main(String[] args) throws InterruptedException {<br> Thread t1 = new Thread(new ExploringSynchronized());<br> Thread t2 = new Thread(new ExploringSynchronized());<br> t1.start();<br> t2.start();<br> //join 主线程需要等待子线程完成后在结束<br> t1.join();<br> t2.join();<br> System.out.println(i);<br><br> }<br>}</code>
對於同步程式碼區塊,括號裡面是什麼,鎖定物件就是什麼,裡面可以使用this 字串 物件等等!
java中synchronized
的實作是基於進入和退出的Monitor
物件實現的,無論是明確同步(修飾程式碼區塊,有明確的monitorenter
和monitorexit
指令)還是隱式同步(修飾方法體)!
要注意的是,只有修飾程式碼區塊的時候,才是基於monitorenter
和monitorexit
指令來實現的;修飾方法的時候,是透過另一個種方式實現的!我會放到後面說!
在了解整個實作底層之前,我還是希望你能夠大致了解一下物件在記憶體中的結構詳情!
實例變數:存放類別的屬性資料訊息,包括父類別的屬性訊息,如果是陣列的實例部分還包括陣列的長度,這部分記憶體按4位元組對齊。
填充資料:由於虛擬機器要求物件起始位址必須是8位元組的整數倍。填充資料不是必須存在的,僅僅是為了位元組對齊,這點了解即可。
這兩個概念,我們簡單理解就好!我們今天不去探究對象的構成原理!我們著重探究一下對象頭,他對我們理解鎖尤為重要!
一般而言,synchronized
所使用的鎖存在於物件頭裡面!如果是數組對象,則虛擬機器使用3個字寬存儲對象,如果是非數組對象,則使用兩個字寬存儲對象頭!字虛擬機裡面1字寬等於4位元組!主要結構是Mark Word
和Class Metadata Address
組成,結構如下:
虛擬機位數 | 頭物件結構 | 說明 |
---|---|---|
#32/64bit | Mark Word | 存儲物件的hashCode、鎖定資訊或分代年齡或GC標誌等資訊 |
#32/64bit | Class Metadata Address | 儲存到隊形類型資料的指標 |
32/64bit(陣列) | Aarray length | 陣列的長度 |
透過上述表格能夠看出 鎖定資訊
存在於 Mark Word
內,那麼 Mark Word 內又是如何組成的呢?
鎖定狀態 | 25bit | #4bit | 1bit是否是偏向鎖定 | 2bit鎖定標誌位元 |
---|---|---|---|---|
無鎖定狀態 | 物件的hashcode | 物件的分代年齡 | 0 | 01 |
在运行起见,mark Word 里存储的数据会随着锁的标志位的变化而变化。mark Word可能变化为存储一下四种数据
Java SE 1.6为了减少获得锁和释放锁带来的消耗,引入了偏向锁
和轻量级锁
,从之前上来就是重量级锁到1.6之后,锁膨胀升级的优化,极大地提高了synchronized
的效率;
锁一共有4中状态,级别从低到高:
这几个状态会随着锁的竞争,逐渐升级。锁可以升级,但是不能降级,其根本的原因就是为了提高获取锁和释放锁的效率!
那么,synchronized是又如何保证的线程安全的呢?或许我们需要从字节码寻找答案!
<code>package com.byit.test;<br><br>/**<br> * @author Administrator<br> */<br>public class SynText {<br> private static String A = "a";<br> public int i ;<br><br> public void add(){<br> synchronized (A){<br> i++;<br> }<br><br> }<br>}</code>
反编译的字节码
<code>Compiled from "SynText.java"<br>public class com.byit.test.SynText {<br> public int i;<br><br> public com.byit.test.SynText();<br> Code:<br> 0: aload_0<br> 1: invokespecial #1 // Method java/lang/Object."<init>":()V<br> 4: return<br><br> public void add();<br> Code:<br> 0: getstatic #2 // Field A:Ljava/lang/String;<br> 3: dup<br> 4: astore_1<br> 5: monitorenter<br> 6: aload_0<br> 7: dup<br> 8: getfield #3 // Field i:I<br> 11: iconst_1<br> 12: iadd<br> 13: putfield #3 // Field i:I<br> 16: aload_1<br> 17: monitorexit<br> 18: goto 26<br> 21: astore_2<br> 22: aload_1<br> 23: monitorexit<br> 24: aload_2<br> 25: athrow<br> 26: return<br> Exception table:<br> from to target type<br> 6 18 21 any<br> 21 24 21 any<br><br> static {};<br> Code:<br> 0: ldc #4 // String a<br> 2: putstatic #2 // Field A:Ljava/lang/String;<br> 5: return<br>}<br></init></code>
省去不必要的,简化在简化
<code> 5: monitorenter<br> ...<br> 17: monitorexit<br> ...<br> 23: monitorexit</code>
从字节码中可知同步语句块的实现使用的是monitorenter
和 monitorexit
指令,其中monitorenter
指令指向同步代码块的开始位置,monitorexit
指令则指明同步代码块的结束位置,当执行monitorenter
指令的时候,线程将试图获取对象所所对应的monitor
特权,当monitor的的计数器为0的时候,线程就可以获取monitor
,并将计数器设置为1.去锁成功!如果当前线程已经拥有monitor特权,则可以直接进入方法(可重入锁),计数器+1;如果其他线程已经拥有了monitor特权,那么本县城将会阻塞!
拥有monitor特权的线程执行完成后释放monitor,并将计数器设置为0;同时执行monitorexit
指令;不要担心出现异常无法执行monitorexit
指令;为了保证在方法异常完成时 monitorenter 和 monitorexit 指令依然可以正确配对执行,编译器会自动产生一个异常处理器,这个异常处理器声明可处理所有的异常,它的目的就是用来执行 monitorexit 指令。从字节码中也可以看出多了一个monitorexit指令,它就是异常结束时被执行的释放monitor 的指令。
同步代码块的原理了解了,那么同步方法如何解释?不急,我们不妨来反编译一下同步方法的状态!
javap -verbose -p SynText > 3.txt
代码
<code>package com.byit.test;<br><br>/**<br> * @author huangfu<br> */<br>public class SynText {<br> public int i ;<br><br> public synchronized void add(){<br> i++;<br><br> }<br>}</code>
字节码
<code>Classfile /D:/2020project/byit-myth-job/demo-client/byit-demo-client/target/classes/com/byit/test/SynText.class<br> Last modified 2020-1-6; size 382 bytes<br> MD5 checksum e06926a20f28772b8377a940b0a4984f<br> Compiled from "SynText.java"<br>public class com.byit.test.SynText<br> minor version: 0<br> major version: 52<br> flags: ACC_PUBLIC, ACC_SUPER<br>Constant pool:<br> #1 = Methodref #4.#17 // java/lang/Object."<init>":()V<br> #2 = Fieldref #3.#18 // com/byit/test/SynText.i:I<br> #3 = Class #19 // com/byit/test/SynText<br> #4 = Class #20 // java/lang/Object<br> #5 = Utf8 i<br> #6 = Utf8 I<br> #7 = Utf8 <init><br> #8 = Utf8 ()V<br> #9 = Utf8 Code<br> #10 = Utf8 LineNumberTable<br> #11 = Utf8 LocalVariableTable<br> #12 = Utf8 this<br> #13 = Utf8 Lcom/byit/test/SynText;<br> #14 = Utf8 syncTask<br> #15 = Utf8 SourceFile<br> #16 = Utf8 SynText.java<br> #17 = NameAndType #7:#8 // "<init>":()V<br> #18 = NameAndType #5:#6 // i:I<br> #19 = Utf8 com/byit/test/SynText<br> #20 = Utf8 java/lang/Object<br>{<br> public int i;<br> descriptor: I<br> flags: ACC_PUBLIC<br><br> public com.byit.test.SynText();<br> descriptor: ()V<br> flags: ACC_PUBLIC<br> Code:<br> stack=1, locals=1, args_size=1<br> 0: aload_0<br> 1: invokespecial #1 // Method java/lang/Object."<init>":()V<br> 4: return<br> LineNumberTable:<br> line 6: 0<br> LocalVariableTable:<br> Start Length Slot Name Signature<br> 0 5 0 this Lcom/byit/test/SynText;<br><br> public synchronized void syncTask();<br> descriptor: ()V<br> flags: ACC_PUBLIC, ACC_SYNCHRONIZED<br> Code:<br> stack=3, locals=1, args_size=1<br> 0: aload_0<br> 1: dup<br> 2: getfield #2 // Field i:I<br> 5: iconst_1<br> 6: iadd<br> 7: putfield #2 // Field i:I<br> 10: return<br> LineNumberTable:<br> line 10: 0<br> line 11: 10<br> LocalVariableTable:<br> Start Length Slot Name Signature<br> 0 11 0 this Lcom/byit/test/SynText;<br>}<br>SourceFile: "SynText.java"<br></init></init></init></init></code>
简化,在简化
<code> public synchronized void syncTask();<br> descriptor: ()V<br> flags: ACC_PUBLIC, ACC_SYNCHRONIZED<br> Code:<br> stack=3, locals=1, args_size=1<br> 0: aload_0<br> 1: dup</code>
我们能够看到 flags: ACC_PUBLIC, ACC_SYNCHRONIZED
这样的一句话
从字节码中可以看出,synchronized修饰的方法并没有monitorenter指令和monitorexit指令,取得代之的确实是ACC_SYNCHRONIZED标识,该标识指明了该方法是一个同步方法,JVM通过该ACC_SYNCHRONIZED访问标志来辨别一个方法是否声明为同步方法,从而执行相应的同步调用。这便是synchronized锁在同步代码块和同步方法上实现的基本原理。
那么在JAVA6之前,为什么synchronized
会如此的慢?
那是因为,操作系统实现线程之间的切换需要系统内核从用户态切换到核心态!这个状态之间的转换,需要较长的时间,时间成本高!所以这也就是synchronized慢的原因!
在这之前,你需要知道什么是锁膨胀!他是JAVA6之后新增的一个概念!是一种针对之前重量级锁的一种性能的优化!他的优化,大部分是基于经验上的一些感官,对锁来进行优化!
研究发现,大多数情况下,锁不仅不存在多线程竞争,而且还总是由一条线程获得!因为为了减少锁申请的次数!引进了偏向锁!在没有锁竞争的情况下,如果一个线程获取到了锁,那么锁就进入偏向锁的模式!当线程再一次请求锁时,无需申请,直接获取锁,进入方法!但是前提是没有锁竞争的情况,存在锁竞争,锁会立即膨胀,膨胀为轻量级锁!
偏向锁失败,那么锁膨胀为轻量级锁!此时锁机构变为轻量级锁结构!他的经验依据是:“绝大多数情况下,在整个同步周期内,不会存在锁的竞争”,故而,轻量级锁适合,线程交替进行的场景!如果在同一时间出现两条线程对同一把锁的竞争,那么此时轻量级锁就不会生效了!但是,jdk官方为了是锁的优化性能更好,轻量级锁失效后,并不会立即膨胀为重量级锁!而是将锁转换为自旋锁状态!
轻量级锁失败后,为了是避免线程挂起,引起内核态的切换!为了优化,此时线程会进入自选状态!他可能会进行几十次,上百次的空轮训!为什么呢?又是经验之谈!他们认为,大多数情况下,线程持有锁的时间都不会太长!做几次空轮训,就能大概率的等待到锁!事实证明,这种优化方式确实有效!最后如果实在等不到锁!没办法,才会彻底升级为重量级锁!
jvm在进行代码编译时,会基于上下文扫描;将一些不可能存在资源竞争的的锁给消除掉!这也是JVM对于锁的一种优化方式!不得不感叹,jdk官方的脑子!举个例子!在方法体类的局部变量对象,他永远也不可能会发生锁竞争,例如:
<code>/**<br> * @author huangfu<br> */<br>public class SynText {<br> public static void add(String name1 ,String name2){<br> StringBuffer sb = new StringBuffer();<br> sb.append(name1).append(name2);<br> }<br><br> public static void main(String[] args) {<br> for (int i = 0; i add("w"+i,"q"+i);<br> }<br> }<br>}</code>
不能否认,StringBuffer
是线程安全的!但是他永远也不会被其他线程引用!故而,锁失效!故而,被消除掉!
以上是Java中的synchronized關鍵字是用來實現執行緒同步的的詳細內容。更多資訊請關注PHP中文網其他相關文章!