Synchronized在多執行緒中使用得比較多的,這兩天看了下慕課網相關課程,在此總結下其使用和原理
作用:
Synchronized據有可重入,不可中斷性,能夠保證在同一時刻最多只有一個執行緒執行該段程式碼,以達到保證併發安全的效果,內部是通過monitor來加鎖和解鎖的。
地位:
1.Synchronized是java的關鍵字,被java語言原生支援。
2.是最基本的互斥同步手段。
3.是併發程式設計中的元老級角色,是併發程式設計的必學內容。
兩個執行緒同時a++,如果不加鎖,最後結果會比預計的少
public class DisappearRequest {
int i = 0;
private Runnable runnable = new Runnable() {
@Override
public void run() {
for (int j = 0; j < 100000; j++) {
i++;
}
}
};
@Test
public void test01() throws InterruptedException {
Thread thread = new Thread(runnable);
Thread thread2 = new Thread(runnable);
thread.start();
thread2.start();
thread.join();
thread2.join();
System.out.println(i);
}
}
執行上面程式碼,期望結果是200000,但是每次執行結果都是小於這個數的。
原因:
i++不是一個原子操作,實際上包含三個動作
1.讀取i;
2.將i加1;
3.將i的值寫入到記憶體中。
在多執行緒環境下這樣是不安全的。在解決問題之前,還是先來學習下Synchronized的用法。
物件鎖
包括方法鎖(預設鎖物件爲this當前範例物件)和同步程式碼塊鎖(自己指定的鎖物件)
類鎖
指synchronized修飾靜態的方法或指定鎖物件爲class物件
public Object lock = new Object();
//程式碼塊鎖
public Runnable runnable = new Runnable() {
@Override
public void run() {
synchronized (lock) { //this或者自建物件
System.out.println("我是物件鎖的程式碼塊形式,我叫:" + Thread.currentThread().getName());
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + ":執行結束");
}
}
};
@Test
public void test01() {
Thread t1 = new Thread(runnable);
Thread t2 = new Thread(runnable);
t1.start();
t2.start();
while (t1.isAlive() || t2.isAlive()) {
}
System.out.println("finished");
}
物件鎖只對同一個物件的方法起到作用,如果是不同對象同一個方法,鎖會失效
//方法鎖
public Runnable runnable2 = new Runnable() {
@Override
public synchronized void run() {
System.out.println("我是物件鎖的方法修飾符形式,我叫:" + Thread.currentThread().getName());
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + ":執行結束");
}
};
@Test
public void test01() {
Thread t1 = new Thread(runnable2);
Thread t2 = new Thread(runnable2);
t1.start();
t2.start();
while (t1.isAlive() || t2.isAlive()) {
}
System.out.println("finished");
}
方法鎖形式,鎖物件預設爲this
概念:java類可能有很多個物件,但只有一個class物件
本質:所以所謂的類鎖,不過是Class物件而已。
class RunnableTest implements Runnable{
@Override
public void run() {
method();
}
public static synchronized void method(){
System.out.println("我是物件鎖的靜態方法修飾符形式,我叫:" + Thread.currentThread().getName());
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + ":執行結束");
}
}
@Test
public void test01() {
Thread t1 = new Thread(new RunnableTest());
Thread t2 = new Thread(new RunnableTest());
t1.start();
t2.start();
while (t1.isAlive() || t2.isAlive()) {
}
System.out.println("finished");
}
如果不加static,RunnableTest裏面的method方法可以同時執行
//class物件鎖
public Runnable runnable5 = new Runnable() {
@Override
public void run() {
synchronized (SynchronizedTest.class){
System.out.println("我是物件鎖的class物件修飾符形式,我叫:" + Thread.currentThread().getName());
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + ":執行結束");
}
}
};
@Test
public void test01() {
Thread t1 = new Thread(runnable5);
Thread t2 = new Thread(runnable5);
t1.start();
t2.start();
while (t1.isAlive() || t2.isAlive()) {
}
System.out.println("finished");
}
物件鎖和類鎖學完了,前面的例子就可以很容易的解決了,這裏就不多說了。
前面的物件鎖已經學習過
前面的類鎖的靜態方法同步鎖已經學過,如果不加靜態的,那麼兩個執行緒執行毫無影響,不受幹擾
加了靜態方法就變成類鎖,會一個一個執行
public class SynchronizedYesAndNoTest {
private Runnable runnable = new Runnable(){
@Override
public void run() {
if(Thread.currentThread().getName().equals("Thread-0")){
method();
}else {
method2();
}
}
};
public synchronized void method(){
System.out.println("我是加鎖的方法,我叫:" + Thread.currentThread().getName());
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + ":執行結束");
}
public void method2(){
System.out.println("我是沒加鎖的方法,我叫:" + Thread.currentThread().getName());
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + ":執行結束");
}
@Test
public void test01() {
Thread t1 = new Thread(runnable);
Thread t2 = new Thread(runnable);
t1.start();
t2.start();
while (t1.isAlive() || t2.isAlive()) {
}
System.out.println("finished");
}
}
非同步方法不受影響
public class SynchronizedDifferentMethod {
private Runnable runnable = new Runnable(){
@Override
public void run() {
if(Thread.currentThread().getName().equals("Thread-0")){
method();
}else {
method2();
}
}
};
public synchronized void method(){
System.out.println("我是加鎖的方法method,我叫:" + Thread.currentThread().getName());
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + ":執行結束");
}
public synchronized void method2(){
System.out.println("我是加鎖的方法method2,我叫:" + Thread.currentThread().getName());
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + ":執行結束");
}
@Test
public void test01() {
Thread t1 = new Thread(runnable);
Thread t2 = new Thread(runnable);
t1.start();
t2.start();
while (t1.isAlive() || t2.isAlive()) {
}
System.out.println("finished");
}
}
一個一個執行
public class SynchronizedStaticAndNormal {
private Runnable runnable = new Runnable(){
@Override
public void run() {
if(Thread.currentThread().getName().equals("Thread-0")){
method();
}else {
method2();
}
}
};
public static synchronized void method(){
System.out.println("我是靜態加鎖的方法method,我叫:" + Thread.currentThread().getName());
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + ":執行結束");
}
public synchronized void method2(){
System.out.println("我是非靜態加鎖的方法method2,我叫:" + Thread.currentThread().getName());
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + ":執行結束");
}
@Test
public void test01() {
Thread t1 = new Thread(runnable);
Thread t2 = new Thread(runnable);
t1.start();
t2.start();
while (t1.isAlive() || t2.isAlive()) {
}
System.out.println("finished");
}
}
不受影響,同時進行
public class SynchronizedException {
private Runnable runnable = new Runnable(){
@Override
public void run() {
if(Thread.currentThread().getName().equals("Thread-0")){
method();
}else {
method2();
}
}
};
public synchronized void method(){
System.out.println("我是方法method,我叫:" + Thread.currentThread().getName());
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
throw new RuntimeException();
// System.out.println(Thread.currentThread().getName() + ":執行結束");
}
public synchronized void method2(){
System.out.println("我是方法method2,我叫:" + Thread.currentThread().getName());
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + ":執行結束");
}
@Test
public void test01() {
Thread t1 = new Thread(runnable);
Thread t2 = new Thread(runnable);
t1.start();
t2.start();
while (t1.isAlive() || t2.isAlive()) {
}
System.out.println("finished");
}
}
拋出異常後,jvm會自動釋放鎖
1.一把鎖只能同時被一個執行緒獲取,沒有拿到鎖的執行緒必須等待(對應第1,5種情況)
2.每個範例都對應有自己的一把鎖,不同範例之前互不影響;例外:鎖物件是*.class以及synchronized修飾的是static方法的時候,所有物件共用同一把類鎖(對應第2,3,4,6種情況)
3.無論是正常執行完畢或者拋出異常,都會釋放鎖。
定義:指的是同一個執行緒的外層函數獲得鎖之後,內層函數可以直接再次獲取到鎖。(Synchronized,Lock)
好處:避免死鎖、提升封裝性
粒度:執行緒而非呼叫(用3種情況來說明和pthread的區別)
public class SynchronizedRecursion {
private int a = 0;
private synchronized void method(){
System.out.println("這是method1,a = " + a);
if(a==0){
a ++;
method();
}
}
@Test
public void test01(){
method();
}
}
遞回呼叫自己可以成功
public class SynchronizedOtherMethod {
private synchronized void method(){
System.out.println("這是method1");
method2();
}
private synchronized void method2(){
System.out.println("這是method2");
}
@Test
public void test01(){
method();
}
}
public class SynchronizedSuperClass {
@Test
public void test01(){
TestClass testClass = new TestClass();
testClass.method();
}
}
class SuperClass{
public synchronized void method(){
System.out.println("我是父類別的方法");
}
}
class TestClass extends SuperClass{
@Override
public synchronized void method() {
super.method();
System.out.println("我是子類的方法");
}
}
一旦這個鎖已經被別人獲取到了,如果我還想獲得,我只能選擇等待或者阻塞,直到別的執行緒釋放這個鎖。如果別人永遠不釋放鎖,那麼我就只能永遠等待下去。
相比之下,Lock可以擁有中斷的能力,第一點,如果我覺得我等的時間太長,有權中斷現在已經獲取到鎖的執行緒的執行;第二點,如果我等待的時間太長了不想再等了,也可以退出。
獲取和釋放的時機:內建鎖,每個java物件可以用做實現同步的鎖,這個鎖就是內建鎖,或者稱做監視鎖。
表達成lock的形式
public class SynchronizedToLock {
Lock lock = new ReentrantLock();
public synchronized void method1(){
System.out.println("我是Synchronized形式的鎖");
}
public void method2(){
lock.lock();
try{
System.out.println("我是lock形式的鎖");
}finally {
lock.unlock();
}
}
@Test
public void test01(){
method1();
method2();
}
}
深入JVM看位元組碼
javac 編譯成class檔案
javap [-verbose] 反編譯
//java檔案
public class Decompilation {
private Object object = new Object();
public void insert(Thread thread){
synchronized (object){
}
}
}
//反編譯之後的檔案
Last modified 2020-8-9; size 503 bytes
MD5 checksum 1832d1176898be312930160e30a29bf2
Compiled from "Decompilation.java"
public class com.example.threadlocaldemo.Decompilation
minor version: 0
major version: 52
flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
#1 = Methodref #2.#20 // java/lang/Object."<init>":()V
#2 = Class #21 // java/lang/Object
#3 = Fieldref #4.#22 // com/example/threadlocaldemo/Decompilation.object:Ljava/lang/Object;
#4 = Class #23 // com/example/threadlocaldemo/Decompilation
#5 = Utf8 object
#6 = Utf8 Ljava/lang/Object;
#7 = Utf8 <init>
#8 = Utf8 ()V
#9 = Utf8 Code
#10 = Utf8 LineNumberTable
#11 = Utf8 insert
#12 = Utf8 (Ljava/lang/Thread;)V
#13 = Utf8 StackMapTable
#14 = Class #23 // com/example/threadlocaldemo/Decompilation
#15 = Class #24 // java/lang/Thread
#16 = Class #21 // java/lang/Object
#17 = Class #25 // java/lang/Throwable
#18 = Utf8 SourceFile
#19 = Utf8 Decompilation.java
#20 = NameAndType #7:#8 // "<init>":()V
#21 = Utf8 java/lang/Object
#22 = NameAndType #5:#6 // object:Ljava/lang/Object;
#23 = Utf8 com/example/threadlocaldemo/Decompilation
#24 = Utf8 java/lang/Thread
#25 = Utf8 java/lang/Throwable
{
public com.example.threadlocaldemo.Decompilation();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=3, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: aload_0
5: new #2 // class java/lang/Object
8: dup
9: invokespecial #1 // Method java/lang/Object."<init>":()V
12: putfield #3 // Field object:Ljava/lang/Object;
15: return
LineNumberTable:
line 3: 0
line 5: 4
public void insert(java.lang.Thread);
descriptor: (Ljava/lang/Thread;)V
flags: ACC_PUBLIC
Code:
stack=2, locals=4, args_size=2
0: aload_0
1: getfield #3 // Field object:Ljava/lang/Object;
4: dup
5: astore_2
6: monitorenter //monitorenter和monitorexit兩個指令實現加鎖和釋放鎖
7: aload_2
8: monitorexit
9: goto 17
12: astore_3
13: aload_2
14: monitorexit
15: aload_3
16: athrow
17: return
Exception table:
from to target type
7 9 12 any
12 15 12 any
LineNumberTable:
line 8: 0
line 10: 7
line 11: 17
StackMapTable: number_of_entries = 2
frame_type = 255 /* full_frame */
offset_delta = 12
locals = [ class com/example/threadlocaldemo/Decompilation, class java/lang/Thread, class java/lang/Object ]
stack = [ class java/lang/Throwable ]
frame_type = 250 /* chop */
offset_delta = 4
monitorenter和monitorexit兩個指令實現加鎖和釋放鎖
1.jvm負責跟蹤物件被加鎖的次數
2.執行緒第一次給物件加鎖的時候,計數變爲1。每當這個相同的執行緒在此物件上再次獲得鎖時,計數會遞增(可重入)。
3.每當任務離開時,計數遞減,當計數爲0的時候,鎖會被完全釋放。
Synchonized物件釋放鎖前,任何物件的修改都會被寫入到主記憶體,保證每次執行都是可靠的,保證可見性
效率低:鎖的釋放情況少、試圖獲得鎖時不能設定超時、不能中斷一個正在試圖獲得鎖的執行緒
不夠靈活(讀寫鎖更靈活):加鎖和釋放的時機單一,每個鎖僅有單一的條件(某個物件),可能是不夠的
無法知道是否成功獲取到鎖
1.使用注意點:鎖物件不能爲空(鎖資訊是儲存在物件頭中,鎖爲空,那麼鎖資訊就沒有了)、作用域不宜過大、避免死鎖
2.如何選擇Lock和Synchronized關鍵字
如果可以兩個都不要用,而是選擇java.util.courrent包中的類,優先Synchronized,避免出錯的發生
3.多執行緒存取同步方法的各種情況
1.多個執行緒等待同一個