天天看點

java學習筆記(十二)-- 線程同步與死鎖

一 線程的同步   ***************

同步問題:每一個線程對象輪番搶占共享資源帶來的問題

首先看一段代碼:

class MyThread implements Runnable{
    private int ticket=10;
    @Override
    public void run() {
        while (this.ticket>0){
            try{
                //模拟網絡延遲
                Thread.sleep(200);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName()+
",還有"+this.ticket--+"張票");
        }
    }
}
public class Test2 {
    public static void main(String[] args) throws InterruptedException {
        MyThread mt=new MyThread();
        new Thread(mt,"黃牛A").start();
        new Thread(mt,"黃牛B").start();
        new Thread(mt,"黃牛C").start();
    }
}
這時會發現運作到最後時會出現如下結果,當隻剩下一張票的時候,卻被三個人拿去同時買:
......
黃牛B,還有1張票
黃牛C,還有0張票
黃牛A,還有-1張票
           

上面出現現象就可以稱之為不同步操作,其唯一好處就是處理速度快(多個線程并發執行)

1 同步處理

1.1 使用synchronize關鍵字(内建鎖,JDK1.0作為關鍵字提供的同步手段)來處理同步問題  對象鎖一定要清楚鎖的對象

synchronize處理同步有兩種模式:同步代碼塊,同步方法

同步代碼塊:(推薦使用,鎖粒度較細)要使用同步代碼塊必須設定一個鎖定的對象,一般可以鎖目前對象this

同一時刻隻能有一個線程進入代碼塊,方法内仍然是多線程

synchronized(this){

    //需要同步代碼塊

}

class MyThread implements Runnable{
    private int ticket=1000;
    @Override
    public void run() {
        for(int i=0;i<1000;i++){
            synchronized (this){
                if(this.ticket>0){
                    System.out.println(Thread.currentThread().getName()+
",還有"+this.ticket--+"張票");
                }
            }
        }
    }
}
public class Test2 {
    public static void main(String[] args) throws InterruptedException {
        MyThread mt=new MyThread();
        new Thread(mt,"黃牛A").start();
        new Thread(mt,"黃牛B").start();
        new Thread(mt,"黃牛C").start();
    }
}
會發現執行結果中沒有負數:
......
黃牛B,還有3張票
黃牛B,還有2張票
黃牛B,還有1張票
           

同步方法:在方法上添加synchronize關鍵字,表示此方法隻有一個線程能進入。隐式鎖對象,this

同一時刻隻有一個線程能進入此方法

範例:

class MyThread implements Runnable{
    private int ticket=1000;
    @Override
    public void run() {
        for(int i=0;i<1000;i++){
            if(this.ticket>0){
                this.sala();
            }
        }
    }
    public synchronized void sala(){
        if(this.ticket>0){
            try {
                //模拟網絡延遲
                Thread.sleep(20);
            }catch (InterruptedException e){
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName()+
",還有"+this.ticket--+"張票");
        }
    }
}
public class Test2 {
    public static void main(String[] args) throws InterruptedException {
        MyThread mt=new MyThread();
        new Thread(mt,"黃牛A").start();
        new Thread(mt,"黃牛B").start();
        new Thread(mt,"黃牛C").start();
    }
}
會發現執行結果中沒有負數:
......
黃牛C,還有3張票
黃牛A,還有2張票
黃牛A,還有1張票
           

同步的缺點:雖然可以保證資料的完整性(線程安全操作),但是其執行的速度回很慢  StringBuilder 和 StringBuffer

  • StringBuffer  JDK1.0 ,采用同步處理,線程安全,效率較低
  • StringBuilder   JDK1.5,采用異步處理,線程不安全,效率較高

1.2 關于synchronized的額外說明

①實際上synchronized(this)

非static的synchronized方法,隻能防止多個線程同時執行同一個對象的同步代碼塊。

synchronized叫對象鎖(預設隻能鎖一個對象),鎖的是對象本身即this。

範例:觀察synchronized的額外說明

class Sync{
    public synchronized void test(){    //預設鎖的是目前對象
        System.out.println("test方法開始,目前線程"+Thread.currentThread().getName());
        try{
            Thread.sleep(1000);
        }catch (InterruptedException e){
            e.printStackTrace();
        }
        System.out.println("test方法結束,目前線程為"+Thread.currentThread().getName());
    }
}
class MyThread extends Thread{
  //使用static隻執行個體化一個Sync對象,這樣三個線程使用同一塊鎖
    //static Sync sync=new Sync();
    @Override
    public void run() {
        Sync sync=new Sync();
        sync.test();
    }
}
public class Test2 {
    public static void main(String[] args) {
        for(int i=0;i<3;i++){
            Thread thread=new MyThread();
            thread.start();
        }
    }
}
觀察結果,三個線程同時開始,同時結束:
test方法開始,目前線程Thread-0
test方法開始,目前線程Thread-1
test方法開始,目前線程Thread-2
test方法結束,目前線程為Thread-0
test方法結束,目前線程為Thread-1
test方法結束,目前線程為Thread-2
           

若要鎖住這段代碼還有三中思路:

範例: 鎖同一個對象

class Sync{
    public synchronized void test(){
        System.out.println("test方法開始,目前線程"+Thread.currentThread().getName());
        try{
            Thread.sleep(1000);
        }catch (InterruptedException e){
            e.printStackTrace();
        }
        System.out.println("test方法結束,目前線程為"+Thread.currentThread().getName());
    }
}
class MyThread extends Thread{
    private Sync sync;
    public MyThread(Sync sync){
        this.sync=sync;
    }
    @Override
    public void run() {
        this.sync.test();
    }
}
public class Test2 {
    public static void main(String[] args) {
        //執行個體化一個Sync對象
        Sync sync=new Sync();
        for(int i=0;i<3;i++){
            Thread thread=new MyThread(sync);
            thread.start();
        }
    }
}
           

對象鎖與全局鎖:

synchronized預設對象鎖,鎖的是目前對象而非代碼塊

全局鎖鎖的是真正代碼段,與對象無關!

實作全局鎖的兩種方式:

1.在同步代碼段鎖Class對象

2.使用static synchronized方法

範例:使用全局鎖,鎖的是類而不是this對象

class Sync{
    public void test(){
        //使用全局鎖,鎖的是以下代碼段,與對象無關
        synchronized (Sync.class){
            System.out.println("test方法開始,目前線程"+Thread.currentThread().getName());
            try{
                Thread.sleep(1000);
            }catch (InterruptedException e){
                e.printStackTrace();
            }
            System.out.println("test方法結束,目前線程為"+Thread.currentThread().getName());
        }
    }
}
class MyThread extends Thread{
    @Override
    public void run() {
        Sync sync = new Sync() ;
        sync.test();
    }
}
public class Test2 {
    public static void main(String[] args) {
        for(int i=0;i<3;i++){
            Thread thread=new MyThread();
            thread.start();
        }
    }
}
           

static synchronized方法,static方法可以直接類名加方法名調用,方法中無法使用this,是以它鎖的不是this,而是類 的Class對象,是以,static synchronized方法也相當于全局鎖,相當于鎖住了代碼段。

範例:使用static synchronized方法

class Sync{
    //此時鎖的為一下代碼塊,與對象無關
    public static  synchronized void test(){    //預設鎖的是目前對象
        System.out.println("test方法開始,目前線程"+Thread.currentThread().getName());
        try{
            Thread.sleep(1000);
        }catch (InterruptedException e){
            e.printStackTrace();
        }
        System.out.println("test方法結束,目前線程為"+Thread.currentThread().getName());
    }
}
class MyThread extends Thread{
    @Override
    public void run() {
        Sync sync=new Sync();
        sync.test();
    }
}
public class Test2 {
    public static void main(String[] args) {
        for(int i=0;i<3;i++){
            Thread thread=new MyThread();
            thread.start();
        }
    }
}
           

範例:看一段代碼( **** 重要 **** )典型面試題

class MyThread implements Runnable{
    private ThreadTest test;
        public MyThread(ThreadTest test){
        super();
        this.test=test;
    }
    @Override
    public void run() {
        if(Thread.currentThread().getName().equals("Thread-1")){
            this.test.testA();
        }else if(Thread.currentThread().getName().equals("Thread-2")){
            this.test.testB();
        }
    }
}
class ThreadTest{
    public synchronized void testA(){
        System.out.println(Thread.currentThread().getName()+"tsetA方法");
        while(true){}
    }
    public synchronized void testB(){
        System.out.println(Thread.currentThread().getName()+"tsetB方法");
    }
}
public class Test2 {
    public static void main(String[] args) throws InterruptedException {
        ThreadTest test=new ThreadTest();
        MyThread mythread=new MyThread(test);
        Thread thread1=new Thread(mythread);
        Thread thread2=new Thread(mythread);
        thread1.start();
        Thread.sleep(1000);
        thread2.start();
    }
}
           

線程2是否能進入到testB中嗎?

鎖的是目前對象,兩個同步方法鎖的是同一個對象

同一個對象,當一個類中有兩個同步方法時,有一個線程已經進了一個同步方法時,另外一個線程絕對不能進入另外的同步方法

如果有一個同步方法一個普通方法,另外一個線程可以進到普通方法

1.3 對象鎖(monitor)機制   ---  JDK6之前的synchronized(重量級鎖)

同步代碼塊:執行同步代碼塊後首先要執行monitorenter指令,退出時執行monitorexit指令。

使用内建鎖(synchronized)進行同步,關鍵在于擷取指定鎖對象monitor對象,當線程擷取monitor後才能繼續向下執行,否則就隻能等待。這個擷取過程是互斥的,即同一時刻隻有一個線程能夠擷取到對象monitor。

通常一個monitorenter指令會包含若幹個monitorexit指令。原因在于JVM需要確定在正常執行路徑以及異常執行路徑上都能正确的解鎖,任何情況下都能夠解鎖。

同步方法:當使用所有synchronized标記方法時,編譯後位元組碼中方法的通路标記多了一個 ACC_SYNCHRONIZED。該标記表示,進入該方法時,JVM需要進行monitorenter操作,退出該方法時,無論是否正常傳回,JVM均需要進行monitorexit操作。

當執行monitorenter時,如果目标鎖對象的monitor計數器為0,表示此對象沒有被任何其他對象所持有。此時JVM會将該鎖對象的持有線程設定為目前線程,并且有計數器+1;如果目标鎖對象計數器不為0,判斷鎖對象的持有線程是否是目前線程,如果是再次将計數器+1(鎖的可重入性),如果鎖對象的持有線程不是目前線程,目前線程需要等待,直至持有線程釋放鎖。

當執行monitorexit時,JVM會将鎖對象的計數器-1,當計數器減為0時,代表該鎖已經被釋放。

JDK1.5提供的Lock鎖(了解即可 )(任然是對象鎖)

範例:使用ReentrantLock進行同步處理,在需要上鎖的時候Lock,在finally中unLock

class MyThread implements Runnable{
    private int ticket=100;
    private Lock ticketLock=new ReentrantLock();
    @Override
    public void run() {
        for(int i=0;i<100;i++){
            try{
                ticketLock.lock();
                if(this.ticket>0) {
                System.out.println(Thread.currentThread().getName() +
"還剩下:" + this.ticket-- + "票");
                }
            }finally{
                //手工釋放鎖
                ticketLock.unlock();
            }
        }
    }
}
public class Test2 {
    public static void main(String[] args) throws InterruptedException {
        MyThread mt=new MyThread();
        Thread thread1=new Thread(mt,"黃牛1");
        Thread thread2=new Thread(mt,"黃牛2");
        Thread thread3=new Thread(mt,"黃牛3");
        thread1.setPriority(Thread.MIN_PRIORITY);
        thread2.setPriority(Thread.MAX_PRIORITY);
        thread3.setPriority(Thread.MAX_PRIORITY);
        thread1.start();
        thread2.start();
        thread3.start();
    }
}