你真的了解 synchronized 嗎?

線程安全概念

當(dāng)多個線程訪問某一個類(對象或方法)時,這個對象始終都能表現(xiàn)出正確的行為,那么這個類(對象或方法)就是線程安全的。
synchronized:可以在任意對象及方法上加鎖,而加鎖的這段代碼稱為"互斥區(qū)"或"臨界區(qū)"

方法鎖

public class MyThread extends Thread{
    
    private int count = 5 ;
    
    //synchronized加鎖
    public synchronized void run(){
        count--;
        System.out.println(this.currentThread().getName() + " count = "+ count);
    }
    public static void main(String[] args) {
        MyThread myThread = new MyThread();
        Thread t1 = new Thread(myThread,"t1");
        Thread t2 = new Thread(myThread,"t2");
        Thread t3 = new Thread(myThread,"t3");
        Thread t4 = new Thread(myThread,"t4");
        Thread t5 = new Thread(myThread,"t5");
        t1.start();
        t2.start();
        t3.start();
        t4.start();
        t5.start();
    }
}


當(dāng)多個線程訪問 myThreadrun 方法時,以排隊的方式進行處理(這里排隊是按照 CPU 分配的先后順序而定的),一個線程想要執(zhí)行synchronized 修飾的方法里的代碼需要做兩種操作:
1、嘗試獲得鎖
2、如果拿到鎖,執(zhí)行synchronized代碼體內(nèi)容;拿不到鎖,這個線程就會不斷的嘗試獲得這把鎖,直到拿到為止,而且是多個線程同時去競爭這把鎖。(也就是會有鎖競爭的問題)

對象鎖

public class MultiThread {

    private static int num = 0;
    
    /** static */
    public static synchronized void printNum(String tag){
        try {
            
            if(tag.equals("a")){
                num = 100;
                System.out.println("tag a, set num over!");
                Thread.sleep(1000);
            } else {
                num = 200;
                System.out.println("tag b, set num over!");
            }
            
            System.out.println("tag " + tag + ", num = " + num);
            
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
    
    //注意觀察run方法輸出順序
    public static void main(String[] args) {
        
        //倆個不同的對象
        final MultiThread m1 = new MultiThread();
        final MultiThread m2 = new MultiThread();
        
        Thread t1 = new Thread(new Runnable() {
            @Override
            public void run() {
                m1.printNum("a");
            }
        });
        
        Thread t2 = new Thread(new Runnable() {
            @Override 
            public void run() {
                m2.printNum("b");
            }
        });     
        
        t1.start();
        t2.start();
        
    }
}

輸出結(jié)果:

方法printNum()未加static 后的輸出結(jié)果:

tag b, set num over!
tag a, set num over!
tag b, num = 200
tag a, num = 100

在方法printNum()加上static 后的輸出結(jié)果:

tag a, set num over!
tag a, num = 100
tag b, set num over!
tag b, num = 200

1、在本例中關(guān)鍵字 synchronized 取得的鎖都是對象鎖,而不是把一段代碼(方法)當(dāng)做鎖,所以代碼中哪個線程先執(zhí)行synchronized關(guān)鍵字的方法,哪個線程就持有該方法所屬對象的鎖(Lock
2、在靜態(tài)方法上加 synchronized關(guān)鍵字,表示鎖定.class類,類一級別的鎖(獨占.class類)。

對象鎖異步同步的問題

public class MyObject {

    public synchronized void method1(){
        try {
            System.out.println(Thread.currentThread().getName());
            Thread.sleep(4000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
    
    /** synchronized */
    public void method2(){
            System.out.println(Thread.currentThread().getName());
    }
    
    public static void main(String[] args) {
        
        final MyObject mo = new MyObject();
        
        /**
         * 分析:
         * t1線程先持有object對象的Lock鎖,t2線程可以以異步的方式調(diào)用對象中的非synchronized修飾的方法
         * t1線程先持有object對象的Lock鎖,t2線程如果在這個時候調(diào)用對象中的同步(synchronized)方法則需等待,也就是同步
         */
        Thread t1 = new Thread(new Runnable() {
            @Override
            public void run() {
                mo.method1();
            }
        },"t1");
        
        Thread t2 = new Thread(new Runnable() {
            @Override
            public void run() {
                mo.method2();
            }
        },"t2");
        
        t1.start();
        t2.start();
        
    }
    
}

輸出結(jié)果:

method2()未加synchronized關(guān)鍵字:

t2
t1

method2()加上synchronized關(guān)鍵字:

t1
t2

在本例中
1、method2()方法未加synchronized關(guān)鍵字, t1 線程先持有object對象的Lock鎖,t2 線程可以以異步的方式調(diào)用對象中的非synchronized修飾的方法 2、如果在method2()方法上加上synchronized關(guān)鍵字此時,t1 線程先持有object對象的Lock`鎖,t2 線程如果在這個時候調(diào)用對象中的同步(synchronized)方法則需等待,也就是同步

臟讀、原子性問題

在某些情況下業(yè)務(wù)整體需要使用完整的synchronized,保持業(yè)務(wù)的原子性。

public class DirtyRead {

    private String username = "loofer";
    private String password = "123";
    
    public synchronized void setValue(String username, String password){
        this.username = username;
        
        try {
            Thread.sleep(2000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        
        this.password = password;
        
        System.out.println("setValue最終結(jié)果:username = " + username + " , password = " + password);
    }
    
    public void getValue(){
        System.out.println("getValue方法得到:username = " + this.username + " , password = " + this.password);
    }
    
    
    public static void main(String[] args) throws Exception{
        
        final DirtyRead dr = new DirtyRead();
        Thread t1 = new Thread(new Runnable() {
            @Override
            public void run() {
                dr.setValue("z3", "456");       
            }
        });
        t1.start();
        Thread.sleep(1000);
        
        dr.getValue();
    }
    
}

輸出結(jié)果:

getValue方法得到:username = z3 , password = 123
setValue最終結(jié)果:username = z3 , password = 456

由于使用了sleep()導(dǎo)致getValue()方法先執(zhí)行,原本我們期望的應(yīng)該是setValue()方法先執(zhí)行,并且 password 應(yīng)該都為 456 才對

synchronized的重入

例子1

public class SyncDubbo1 {

    public synchronized void method1(){
        System.out.println("method1..");
        method2();
    }
    public synchronized void method2(){
        System.out.println("method2..");
        method3();
    }
    public synchronized void method3(){
        System.out.println("method3..");
    }
    
    public static void main(String[] args) {
        final SyncDubbo1 sd = new SyncDubbo1();
        Thread t1 = new Thread(new Runnable() {
            @Override
            public void run() {
                sd.method1();
            }
        });
        t1.start();
    }
}

例子2

public class SyncDubbo2 {

    static class Main {
        public int i = 10;
        public synchronized void operationSup(){
            try {
                i--;
                System.out.println("Main print i = " + i);
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
    
    static class Sub extends Main {
        public synchronized void operationSub(){
            try {
                while(i > 0) {
                    i--;
                    System.out.println("Sub print i = " + i);
                    Thread.sleep(100);      
                    this.operationSup();
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
    
    public static void main(String[] args) {
        
        Thread t1 = new Thread(new Runnable() {
            @Override
            public void run() {
                Sub sub = new Sub();
                sub.operationSub();
            }
        });
        
        t1.start();
    }
}

輸出結(jié)果:

例子 1:

method1..
method2..
method3..

例子2:

Sub print i = 9
Main print i = 8
Sub print i = 7
Main print i = 6
Sub print i = 5
Main print i = 4
Sub print i = 3
Main print i = 2
Sub print i = 1
Main print i = 0

synchronized 異常

public class SyncException {

    private int i = 0;
    public synchronized void operation(){
        while(true){
            try {
                i++;
                Thread.sleep(100);
                System.out.println(Thread.currentThread().getName() + " , i = " + i);
                if(i == 20){
                    //Integer.parseInt("a");
                    throw new RuntimeException();
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
    
    public static void main(String[] args) {
        
        final SyncException se = new SyncException();
        Thread t1 = new Thread(new Runnable() {
            @Override
            public void run() {
                se.operation();
            }
        },"t1");
        t1.start();
    }
}

輸出結(jié)果

t1 , i = 1
t1 , i = 2
t1 , i = 3
t1 , i = 4
t1 , i = 5
t1 , i = 6
t1 , i = 7
t1 , i = 8
t1 , i = 9
Exception in thread "t1" java.lang.RuntimeException
    at com.loofer.base.sync005.SyncException.operation(SyncException.java:18)
    at com.loofer.base.sync005.SyncException$1.run(SyncException.java:32)
    at java.lang.Thread.run(Thread.java:745)
t1 , i = 10

這里我們除了 main 線程外還有一個 t1 線程,當(dāng) t1 中拋出一個異常時,由于我們在operation()方法上加了鎖,所以我們整個應(yīng)用程序最后退出了,對于這種問題一般情況下可以采取 continue 寫入日志發(fā)布告警,讓應(yīng)用程序繼續(xù)執(zhí)行。

線程細節(jié)問題

使用 synchronized 聲明的方法在某些情況下是有弊端的,比如 A 線程調(diào)用同步的方法執(zhí)行一個很長時間的任務(wù),那么 B 線程必須等待比較長的時間才能執(zhí)行,這樣的情況下可以使用 synchronized去優(yōu)化代碼執(zhí)行時間,也就是通常所說的減小鎖的粒度。

鎖對象的改變的問題

鎖對象的改變問題,當(dāng)使用一個對象進行加鎖的時候,要注意對象本身發(fā)生改變的時候,那么持有的鎖就不同。如果對象本身不發(fā)生改變,那么依然是同步的,即使是對象的屬性發(fā)生了改變。

public class ChangeLock {

    private String lock = "lock";
    
    private void method(){
        synchronized (lock) {
            try {
                System.out.println("當(dāng)前線程 : "  + Thread.currentThread().getName() + "開始");
                lock = "change lock";
                Thread.sleep(2000);
                System.out.println("當(dāng)前線程 : "  + Thread.currentThread().getName() + "結(jié)束");
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
    
    public static void main(String[] args) {
    
        final ChangeLock changeLock = new ChangeLock();
        Thread t1 = new Thread(new Runnable() {
            @Override
            public void run() {
                changeLock.method();
            }
        },"t1");
        Thread t2 = new Thread(new Runnable() {
            @Override
            public void run() {
                changeLock.method();
            }
        },"t2");
        t1.start();
        try {
            Thread.sleep(100);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        t2.start();
    }
}

輸出結(jié)果

當(dāng)前線程 : t1開始
當(dāng)前線程 : t2開始
當(dāng)前線程 : t1結(jié)束
當(dāng)前線程 : t2結(jié)束

在本例中由于 lock 成員變量在線程執(zhí)行的過程中被改變了,導(dǎo)致鎖失效了

死鎖問題

在設(shè)計程序時就應(yīng)該避免雙方相互持有對方的鎖的情況

public class DeadLock implements Runnable{

    private String tag;
    private static Object lock1 = new Object();
    private static Object lock2 = new Object();
    
    public void setTag(String tag){
        this.tag = tag;
    }
    
    @Override
    public void run() {
        if(tag.equals("a")){
            synchronized (lock1) {
                try {
                    System.out.println("當(dāng)前線程 : "  + Thread.currentThread().getName() + " 進入lock1執(zhí)行");
                    Thread.sleep(2000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                synchronized (lock2) {
                    System.out.println("當(dāng)前線程 : "  + Thread.currentThread().getName() + " 進入lock2執(zhí)行");
                }
            }
        }
        if(tag.equals("b")){
            synchronized (lock2) {
                try {
                    System.out.println("當(dāng)前線程 : "  + Thread.currentThread().getName() + " 進入lock2執(zhí)行");
                    Thread.sleep(2000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                synchronized (lock1) {
                    System.out.println("當(dāng)前線程 : "  + Thread.currentThread().getName() + " 進入lock1執(zhí)行");
                }
            }
        }
    }
    
    public static void main(String[] args) {
        
        DeadLock d1 = new DeadLock();
        d1.setTag("a");
        DeadLock d2 = new DeadLock();
        d2.setTag("b");
         
        Thread t1 = new Thread(d1, "t1");
        Thread t2 = new Thread(d2, "t2");
         
        t1.start();
        try {
            Thread.sleep(500);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        t2.start();
    }   
}

輸出結(jié)果

當(dāng)前線程 : t1 進入lock1執(zhí)行
當(dāng)前線程 : t2 進入lock2執(zhí)行

在程序運行的運行時啟動了兩個線程,每個線程中都持有 lock1、lock2 兩個鎖,當(dāng) t1 運行時持有了 lock1 當(dāng) t2 運行的時候 lock2 被持有了,當(dāng) t1 運行到后面去嘗試獲得 lock2 就會一直等待,而 t2 繼續(xù)運行嘗試去獲得 t1 發(fā)現(xiàn)被占用了也進入等待,這時就進入了互相等待的狀態(tài)。也就是我們所說的死鎖。

同一對象屬性的修改不會影響鎖的情況

public class ModifyLock {
    
    private String name ;
    private int age ;
    
    public String getName() {
        return name;
    }
    public void setName(String name) {
        this.name = name;
    }
    public int getAge() {
        return age;
    }
    public void setAge(int age) {
        this.age = age;
    }
    
    public synchronized void changeAttributte(String name, int age) {
        try {
            System.out.println("當(dāng)前線程 : "  + Thread.currentThread().getName() + " 開始");
            this.setName(name);
            this.setAge(age);
            
            System.out.println("當(dāng)前線程 : "  + Thread.currentThread().getName() + " 修改對象內(nèi)容為: " 
                    + this.getName() + ", " + this.getAge());
            
            Thread.sleep(2000);
            System.out.println("當(dāng)前線程 : "  + Thread.currentThread().getName() + " 結(jié)束");
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
    
    public static void main(String[] args) {
        final ModifyLock modifyLock = new ModifyLock();
        Thread t1 = new Thread(new Runnable() {
            @Override
            public void run() {
                modifyLock.changeAttributte("張三", 20);
            }
        },"t1");
        Thread t2 = new Thread(new Runnable() {
            @Override
            public void run() {
                modifyLock.changeAttributte("李四", 21);
            }
        },"t2");
        
        t1.start();
        try {
            Thread.sleep(100);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        t2.start();
    }
}

輸出結(jié)果

當(dāng)前線程 : t1 開始
當(dāng)前線程 : t1 修改對象內(nèi)容為: 張三, 20
當(dāng)前線程 : t1 結(jié)束
當(dāng)前線程 : t2 開始
當(dāng)前線程 : t2 修改對象內(nèi)容為: 李四, 21
當(dāng)前線程 : t2 結(jié)束

使用synchronized代碼塊減小鎖的粒度,提高性能

public class Optimize {

    public void doLongTimeTask(){
        try {
            
            System.out.println("當(dāng)前線程開始:" + Thread.currentThread().getName() + 
                    ", 正在執(zhí)行一個較長時間的業(yè)務(wù)操作,其內(nèi)容不需要同步");
            Thread.sleep(2000);
            
            synchronized(this){
                System.out.println("當(dāng)前線程:" + Thread.currentThread().getName() + 
                    ", 執(zhí)行同步代碼塊,對其同步變量進行操作");
                Thread.sleep(1000);
            }
            System.out.println("當(dāng)前線程結(jié)束:" + Thread.currentThread().getName() +
                    ", 執(zhí)行完畢");
            
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
    
    public static void main(String[] args) {
        final Optimize otz = new Optimize();
        Thread t1 = new Thread(new Runnable() {
            @Override
            public void run() {
                otz.doLongTimeTask();
            }
        },"t1");
        Thread t2 = new Thread(new Runnable() {
            @Override
            public void run() {
                otz.doLongTimeTask();
            }
        },"t2");
        t1.start();
        t2.start();
        
    }
}

輸出結(jié)果

當(dāng)前線程開始:t1, 正在執(zhí)行一個較長時間的業(yè)務(wù)操作,其內(nèi)容不需要同步
當(dāng)前線程開始:t2, 正在執(zhí)行一個較長時間的業(yè)務(wù)操作,其內(nèi)容不需要同步
當(dāng)前線程:t1, 執(zhí)行同步代碼塊,對其同步變量進行操作
當(dāng)前線程:t2, 執(zhí)行同步代碼塊,對其同步變量進行操作
當(dāng)前線程結(jié)束:t1, 執(zhí)行完畢
當(dāng)前線程結(jié)束:t2, 執(zhí)行完畢

字符串加鎖

public class StringLock {

    public void method() {
        //new String("字符串常量")
        synchronized (new String("字符串常量")) {
            try {
                while(true){
                    System.out.println("當(dāng)前線程 : "  + Thread.currentThread().getName() + "開始");
                    Thread.sleep(1000);     
                    System.out.println("當(dāng)前線程 : "  + Thread.currentThread().getName() + "結(jié)束");
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
    
    public static void main(String[] args) {
        final StringLock stringLock = new StringLock();
        Thread t1 = new Thread(new Runnable() {
            @Override
            public void run() {
                stringLock.method();
            }
        },"t1");
        Thread t2 = new Thread(new Runnable() {
            @Override
            public void run() {
                stringLock.method();
            }
        },"t2");
        
        t1.start();
        t2.start();
    }
}

輸出結(jié)果

當(dāng)前線程 : t1開始
當(dāng)前線程 : t1結(jié)束
當(dāng)前線程 : t1開始
當(dāng)前線程 : t1結(jié)束
...

t1 永遠持有鎖,t2 永遠進不去
synchronized 代碼塊對字符串的鎖,注意String常量池的緩存功能

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

推薦閱讀更多精彩內(nèi)容