Java並發編程:線程和鎖的使用與解析


線程的使用

  新建線程

  新建一個線程有兩種方法:繼承Thread類,然后重寫run方法;實現Runnable接口,然后實現run方法。實際上Thread類也是實現的Runnable接口,再加上類只能單繼承,所以推薦使用Runnable接口。示例如下:

 

class Demo1 implements Runnable{
    @Override
    public void run() {
        //新建線程需要執行的邏輯
    }
}

 

class Demo2 extends Thread{
    @Override
    public void run() {
        //新建線程需要執行的邏輯
    }
}

  對於Thread類,當然可以使用匿名內部類來簡化寫法:

Thread thread=new Thread(){
    public void run(){
        //新建線程需要執行的邏輯
    }
};
//Lambda表達式簡化后
Thread thread=new Thread(()->{
    //需要執行的邏輯
});

新建完一個線程后,就可以用對象實例來啟動線程,啟動后就會執行我們重寫后的run方法:

thread.start();

  此外,Thread類有個非常重要的構造方法:

public Thread(Runnable target) {
    init(null, target, "Thread-" + nextThreadNum(), 0);
}

  可見,傳入的是一個Runnable類型的參數。為什么需要這個構造函數?因為Runnable接口只有一個run方法,如果我們直接實例化實現了這個接口的類,然后調用run方法,其實就和普通的類沒有區別,並沒有另外一個線程去執行run方法。說白了,Runnable並不是新建了一個線程,而只是線程里面執行任務的一種類型。在Java並發編程里,我們總是說的任務,很多時候就是Runnable類型的。所以我們還是需要把實現了Runnable接口的類的實例傳入Thread的構造函數,然后通過start方法去調用Runnable的run方法。

//新建一個任務(Demo1實現了Runnable接口)
Demo1 task=new Demo1;
//新建一個線程並傳入需要執行的任務
Thread thread=new Thread(task);
//啟動線程執行任務
thread.start();

  線程的其他方法

  熟悉了線程的創建,再簡單了解一下操作線程的其他方法。

  stop方法:作用是終止線程,但不推薦使用,因為它是強制結束線程,不管線程執行到了哪一步,很容易造成錯誤數據,引起數據不一致的問題。

  interrupt方法:作用和stop類似,但是並不會那么粗魯的終止線程,如果只調用這一個方法並不會中斷線程,它還需要配合一個方法使用:

class Demo implements Runnable {
    @Override
    public void run() {
        //通過isInterrupted方法判斷當前線程是否需要停止,不需要停止就執行邏輯代碼
        while (!Thread.currentThread().isInterrupted()){
            //邏輯
        }
    }
}
public class Use {
    public static void main(String[] args) throws InterruptedException {
        Demo task = new Demo ();
        Thread thread=new Thread(task);
        thread.start();
        //通知thread可以終止了
        thread.interrupt();
    }
}

  wait方法和notify方法:這兩個方法放在一起說,是因為它們需要配合使用。簡單提一下synchronized ,這個會在在鎖里面講。synchronized大概的作用就是:代碼塊里的代碼,同時只能由一個線程去執行,如何確保只有一個線程去執行?誰擁有鎖誰就有資格執行。任何對象都可以調用wait方法,如obj.wait,它的意思就是讓當前線程在obj上等待並釋放當前線程占用的鎖。obj.notify就是喚醒在obj上等待的線程並重新嘗試獲取鎖。下面演示一下簡單的使用:

public class Use {
    //一定要確保等待和喚醒是同一個對象,用類鎖也可以,至於什么是類鎖可以看后面synchronized部分
    static Object object=new Object();
    static int i = 0;
    static class Demo1 implements Runnable {
        @Override
        public void run() {
            synchronized (object){
                for(int j=0;j<10000;j++){
                    i++;
                    if(i==5000){
                        //1.因為t1先啟動並進入同步代碼塊,所以首先輸出5000
                        System.out.println();
                        try {
                            //釋放鎖並等待
                            object.wait();
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    }
                }
                //3.被喚醒后接着執行完剩余的代碼,輸出20000
                System.out.println(i);
            }
        }
    }
    static class Demo2 implements Runnable{
        @Override
        public void run() {
            synchronized (object){
                for(int j=0;j<10000;j++){
                    i++;
                }
                //2.獲取到t1釋放的鎖,執行完代碼后輸出15000並喚醒object上等待的線程
                System.out.println(i);
                object.notify();
            }
        }
    }

    public static void main(String[] args) throws InterruptedException {
        Demo1 task1 = new Demo1();
        Demo2 task2 = new Demo2();
        Thread thread1=new Thread(task1);
        Thread thread2=new Thread(task2);
        thread1.start();
        thread2.start();
    }
}

  需要注意的是,如果有多個線程在obj等待,只執行一次obj.notify的話,它是隨機從obj等待列表中選擇一個線程喚醒的,如果要喚醒所有等待線程,可以使用obj.notifyAll不管wait、notify還是notifyAll只能在synchronized代碼塊中使用,否則會報IllegalMonitorStateException異常。Java之所以這么規定,是確保不會發生Lost Wake Up問題,也就是喚醒丟失。上面那個例子中使用了同步代碼塊,所以不會發生這種問題。試想一種情況,如果沒有synchronized確保線程是有秩序執行的,當t2線程先喚醒了object上的對象,t1線程后暫停的,那么t1是不是就永遠會暫停下去,t2notify相當於丟失了,這就是Lost wake up

  join方法:作用是讓指定線程加入當前線程。為了節約篇幅還是以interrupt方法的代碼為例,如果在main方法里調用thread.join(),那么主線程就會等待thread線程執行完才接着執行。其實這就和單線程的效果差不多了。如果有時候thread線程執行時間太長,為了不影響其他線程,我們可以在join方法里傳入一個時間,單位是毫秒,當過了這個時間不管thread線程有沒有執行完,主線程都會接着執行。join方法其實是通過wait方法實現的,注意這個wait是被加入線程等待,而不是加入的線程等待。貼一下源碼,邏輯很簡單就不復述了,如果join不傳入參數,millis默認就是0:

public final synchronized void join(long millis)
throws InterruptedException {
    long base = System.currentTimeMillis();
    long now = 0;
    if (millis < 0) {
        throw new IllegalArgumentException("timeout value is negative");
    }
    if (millis == 0) {
        while (isAlive()) {
            wait(0);
        }
    } else {
        while (isAlive()) {
            long delay = millis - now;
            if (delay <= 0) {
                break;
            }
            wait(delay);
            now = System.currentTimeMillis() - base;
        }
    }
}

  yield方法:這個方法會讓出當前線程的CPU,讓出后還會接着去爭奪,但是還能不能爭奪到就不一定了。一般優先級比較低的線程,為了節約資源可以適當調用這個方法。

 

線程安全

  如何保證線程安全?無非就是鎖的爭奪。誰擁有了鎖,誰才有資格執行。為什么只讓一個線程執行代碼?這就與Java的內存模型有關了。不了解的可以看其他資料,也可以看看我的另外一篇博客:https://www.cnblogs.com/lbhym/p/12458990.html,最后一節就是講Java內存模型的。簡單的說:線程共享的資源是放在一個共享內存區域的。當線程A去操作一個共享變量時,它會先把這個變量拷貝到自己私有的內存空間,然后進行操作,最后把操作后的值賦值到共享內存中的變量。如果在賦值之前另外一個線程B剛剛更新了這個值,那么線程A的操作就把線程B的操作給覆蓋了,而線程B渾然不知,接着執行它的邏輯,這就造成了數據不一致的情況。所以我們必須加上一把鎖,確保同一時間只能由一個線程來修改這個變量。

  關鍵字synchronized

  關鍵字synchronized的作用前面已經提到過了,下面給個簡單的示例:

class Demo implements Runnable {
    static int i = 0;
    @Override
    public void run() {
        //小括號中的Demo.class就是鎖,大括號內的代碼同時只能由一個線程執行
       synchronized (Demo.class){
           for(int j=0;j<10000;j++) {
               i++;
           }
       }
    }

}
public class Use {
    public static void main(String[] args) throws InterruptedException {
        Demo task = new Demo();
        Thread thread1=new Thread(task);
        Thread thread2=new Thread(task);
        thread1.start();
        thread2.start();
        //讓兩個線程加入主線程,這樣就可以輸出執行后的i了
        thread1.join();
        thread2.join();
        System.out.println(Demo.i);//輸出20000,如果去掉同步代碼塊,i絕對小於20000
    }
}

  其實這個關鍵字的作用很好理解,關鍵在於,小括號里面有什么實際意義,它與Lock有什么區別?

  首先synchronized和Lock都是Java里面的鎖機制,前者用起來更加方便,后者功能更多。方便在哪?進入代碼塊前自動獲取鎖,如果鎖已經被占,則會等待。執行完同步代碼塊中的內容,自動釋放鎖。而Lock需要手動加鎖解鎖,接下來會講。

  接着說說synchronized具體用法,小括號里就是鎖對象。有一點需要注意,synchronized鎖的是對象,而不是里面的代碼,誰擁有指定的鎖誰就能執行里面的代碼。明白這一點有助於理解下面的內容。

  synchronized的鎖分為類鎖和對象鎖。它們的區別就是作用域的不同。

  首先說說對象鎖怎么用以及它的特點:

//對象鎖:
synchronized(this){...}
synchronized(類的實例){...}
//修飾在void前也是對象鎖
public synchronized void run(){...}

  如果synchronized里指定的是對象鎖,那么在創建task時,不同的實例對象就是不同的鎖。大家可以在上面示例代碼的基礎上,再用Demo類實例化一個task2,然后用thread去執行它,接着把synchronized小括號里的鎖換成this,也就是對象鎖,會發現輸出的i小於20000。因為tasktask2完全就是不同的鎖,兩個線程並不沖突,這就是為什么上面強調,鎖的是對象,而不是里面的代碼。

  再說說類鎖的用法和特點:

//類鎖
synchronized(類名.class){...}
//修飾在靜態方法前也是類鎖,run方法里直接調用handler就行
private synchronized static void handler(){...}

  上面的示例代碼就是一個類鎖,即使實例化兩個不同的對象,提交給兩個線程執行后,輸出結果肯定是20000,也就是說它們是同步的。

  最后說一點,同一個類中,類鎖和對象鎖依舊是不同的鎖,它們之間互不干擾,不是同步的。舉個例子:

class Demo implements Runnable {
    static int i = 0;
    @Override
    public void run() {
          run2();
          run3();
    }
    //類鎖
    private synchronized static void run2(){
        for(int j=0;j<10000;j++) {
            i++;
        }
    }
    //對象鎖
    private synchronized void run3(){
        for(int j=0;j<10000;j++) {
            i++;
        }
    }
}

  main方法就不貼了,記得實例化一個task2給thread2執行。最后的輸出結果肯定小於40000,如果把run3改成靜態方法,也就是變成類鎖,輸出結果就是40000了。

   接口Lock

  Lock接口下提供了一套功能更完整的鎖機制。如果項目中線程的競爭並不激烈,使用synchronized完全足夠,如果競爭很激烈,還需要其他一些功能,這時候就可以嘗試一下Lock提供的鎖了。

  ReentrantLock:可重入鎖

  簡單的示例如下,說明也在注釋當中:

class ReenterLock implements Runnable {
    //可重入鎖,意思是:在同一個線程中,可以對lock多次加鎖,當然也必須解鎖對應次數
    //那么Lock下的鎖是類鎖還是對象鎖,取決於鎖對象是類變量還是普通的全局變量,加上static就是類鎖,反之就是對象鎖
    static ReentrantLock lock = new ReentrantLock();
    static int i = 0;
    @Override
    public void run() {
        lock.lock();
        for (int j=0;j<10000;j++){
            i++;
        }
        lock.unlock();
    }
}
public class 可重入鎖 {
    public static void main(String[] args) throws InterruptedException {
        ReenterLock task = new ReenterLock();
        Thread thread1=new Thread(task);
        Thread thread2=new Thread(task);
        thread1.start();
        thread2.start();
        thread1.join();
        thread2.join();
        System.out.println(ReenterLock.i);
    }
}

  可重入鎖除了以上加鎖、解鎖的基本功能外,還有其他一些功能:

  lockInterruptibly方法和interrupt方法:后者在線程中已經出現過一次了,雖然名字一樣,功能也差不多,但是作用對象不一樣。如果我們的線程在加鎖也就是獲取鎖時,用的是lockInterruptibly方法,如果在等待一段時間后,還沒獲取到鎖,那么就可以通過interrupt方法通知這個線程不用等了。這兩個方法配合使用,在設置合理的等待時間后,可以避免死鎖的發生。但需要注意,被通知放棄獲取鎖的線程會釋放自己的資源,結束執行任務。

  tryLock方法:除了上面那種外部通知放棄獲取鎖的方法外,還有一種限時等待的方法,tryLock有兩個參數,第一個是時間,第二個是時間類型。如果不傳入任何參數,獲取到鎖直接返回true,沒獲取到直接返回false。對的,tryLock和普通的lock方法不同,它返回的是Boolean類型,所以一般需要配合if判斷使用:

@Override
public void run() {
    try {
        if (lock.tryLock(10, TimeUnit.SECONDS)) {
            //邏輯代碼
        }
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
}

  公平鎖和非公平鎖:公平鎖的分配是公平的,先到先得。非公平鎖則是隨機分配鎖的,你先等待的不一定能先獲取到鎖。具體的是在ReenterLock構造函數中進行設置:

//構造函數傳入true就是公平鎖,默認情況下是非公平鎖
static ReentrantLock lock = new ReentrantLock(true);

  默認情況下采用非公平鎖,是因為公平鎖需要維護一個有序隊列,性能相較於非公平鎖是非常低的。

 

  Condition:可重入鎖的搭檔

  在synchronized代碼塊中,可以使用wait方法讓當前線程釋放鎖並等待,然后通過notify方法喚醒線程並嘗試重新獲取鎖。但是這兩個方法是作用在synchronized中的,前面也說過了。在可重入鎖也有類似的功能,下面舉個簡單的例子,會發現和synchronized中的wait和notify差不都:

public class Use {
    static ReentrantLock lock = new ReentrantLock();
    //創建的lock的condition對象
    static Condition condition = lock.newCondition();
    static int i = 0;
    static class Demo1 implements Runnable {
        @Override
        public void run() {
            //t1先進來加鎖(遇到一次特殊情況,t2后啟動的反而先獲取到鎖了)
            lock.lock();
            for (int j = 0; j < 10000; j++) {
                i++;
                if (i == 5000) {
                    //1.輸出5000
                    System.out.println(i);
                    try {
                        //釋放鎖並等待
                        condition.await();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
            //被喚醒后接着執行完剩下的代碼,並輸出20000
            System.out.println(i);
            lock.unlock();
        }
    }
    static class Demo2 implements Runnable {
        @Override
        public void run() {
            //獲取鎖
            lock.lock();
            for (int j = 0; j < 10000; j++) {
                i++;
            }
            System.out.println(i);
            //2.執行完后輸出15000,並喚醒等待的線程
            condition.signal();
            lock.unlock();
        }
    }

    public static void main(String[] args) throws InterruptedException {
        Demo1 task1 = new Demo1();
        Demo2 task2 = new Demo2();
        Thread thread1 = new Thread(task1);
        Thread thread2 = new Thread(task2);
        thread1.start();
        thread2.start();
    }
}

  await方法會使當前線程等待,同時釋放當前鎖,當其他線程中使用signal()方法或者signalAll()方法時,線程會重新獲得鎖並繼續執行。或者當線程被中斷時,也能跳出等待。這和Object.wait()方法相似。

  awaitUninterruptibly方法與await方法基本相同,但是它並不會在等待過程中響應中斷。

  singal方法用於喚醒一個在等待中的線程,singalAll方法會喚醒所有在等待中的線程。這和Obejct.notify()方法很類似。

 

  Semaphore:允許多個線程同時訪問

  前面提到的可重入鎖和同步代碼塊一次只能讓一個線程進入,而Semaphore可以指定多個線程,同時訪問一個資源。

public class Use {
    static int i = 0;
    //一次允許兩個線程進入
    static Semaphore sema = new Semaphore(2);
    static class Demo2 implements Runnable {
        @Override
        public void run() {
            try {
                //如果有多余的名額就允許一個線程進入
                sema.acquire();
                for(int j=0;j<10000;j++){
                    i++;
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            }finally {
                //當前線程執行完代碼並釋放一個名額
                sema.release();
            }
        }
    }

    public static void main(String[] args) throws InterruptedException {
        //Demo1 task1 = new Demo1();
        Demo2 task2 = new Demo2();
        Thread thread1 = new Thread(task2);
        Thread thread2 = new Thread(task2);
        thread1.start();
        thread2.start();
        thread1.join();
        thread2.join();
        System.out.println(i);//輸出小於20000,說明同時有兩個線程進去了,互相干擾了。
    }
}

  

  ReadWriteLock:讀寫鎖

  很多時候線程只是執行讀操作,並不會互相干擾,其實這個時候並不需要線程之間相互排斥。在數據庫里面讀寫鎖是比較常見的,在Java中,它們的邏輯其實是一樣的。只有讀和讀不會阻塞,有寫操作必然阻塞。

代碼篇幅太多了,就不再演示邏輯代碼了,下面是讀寫鎖的創建代碼:

static ReentrantReadWriteLock readWriteLock=new ReentrantReadWriteLock();
//讀鎖
static Lock readLock=readWriteLock.readLock();
//寫鎖
static Lock writLock=readWriteLock.writeLock();

  

  CountDownLatch:倒計數器

  一個實用的多線程工具類,說倒計數器可能有點不明白,其實就是等來指定數量的線程執行完后才執行接下來的代碼,看示例更清楚:

public class Use {
    //需要兩個線程執行完任務
    static CountDownLatch count = new CountDownLatch(2);
    static int i=0;
    static class Demo2 implements Runnable {
        @Override
        public void run() {
            synchronized (Use.class) {
                for (int j = 0; j < 10000; j++) {
                    i++;
                }
                //當前線程執行完任務,計數器+1
                count.countDown();
            }
        }
    }

    public static void main(String[] args) throws InterruptedException {
        //Demo1 task1 = new Demo1();
        Demo2 task2 = new Demo2();
        Thread thread1 = new Thread(task2);
        Thread thread2 = new Thread(task2);
        thread1.start();
        thread2.start();
        //等待指定數量的線程都執行完任務后才接着執行,相當於阻塞了當前的主線程,從而實現了join的功能
        count.await();
        System.out.println(i);//輸出20000
    }
}

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM