Java 多線程、線程安全、線程間通信

創建線程

Java使用 java.lang.Thread 類代表線程,所有的線程對象都必須是Thread類或其子類的實例。

Thread類

public class MyThread extends Thread{
    public MyThread(String name) {
        //調用父類的String參數的構造方法,指定線程的名稱
        super(name);
    }

    /**
     * 重寫run方法,完成該線程執行的邏輯
     */
    @Override
    public void run() {
        try {
            sleep(10);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        for(int i = 0; i < 100; i++) {
            System.out.println(getName() + i);
        }
    }
}
public class ThreadTest {
    public static void main(String[] args) {
        //創建自定義線程對象
        MyThread mt1 = new MyThread("a");
        MyThread mt2 = new MyThread("b");
        MyThread mt3 = new MyThread("c");
        //開啓新線程
        mt1.start();
        mt2.start();
        mt3.start();
        //主線程中的for循環
        for (int i = 0; i < 20; i++) { 
       		System.out.println("主線程:"+i); 
        }
    }
}

程序啓動運行main時候,java虛擬機啓動一個進程,主線程main在main()調用時候被創建。隨着調用mt的對象的 start方法,另外三個新的線程也啓動了,這樣,整個應用就在多線程下運行。

Runnable接口

public class MyRunnable implements Runnable{
    @Override
    public void run() {
        try {
            Thread.sleep(100);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        for(int i = 0; i < 10; i++) {
            System.out.println(Thread.currentThread().getName() + i);
        }
    }
}
public class RunableTest {
    public static void main(String[] args) {
        MyRunnable mr = new MyRunnable();

        new Thread(mr, "a").start();
        new Thread(mr, "b").start();
        new Thread(mr, "c").start();
    }
}

通過實現Runnable接口,使得該類有了多線程類的特徵。run()方法是多線程程序的一個執行目標。所有的多線程 代碼都在run方法裏面。Thread類實際上也是實現了Runnable接口的類。

在啓動的多線程的時候,需要先通過Thread類的構造方法Thread(Runnable target) 構造出對象,然後調用Thread 對象的start()方法來運行多線程代碼。

實際上所有的多線程代碼都是通過運行Thread的start()方法來運行的。因此,不管是繼承Thread類還是實現 Runnable接口來實現多線程,最終還是通過Thread的對象的API來控制線程的,熟悉Thread類的API是進行多線程 編程的基礎。

實現Runnable接口比繼承Thread類所具有的優勢

  • 適合多個相同的程序代碼的線程去共享同一個資源
  • 可以避免java中的單繼承的侷限性
  • 增加程序的健壯性,實現解耦操作,代碼可以被多個線程共享,代碼和線程獨立
  • 線程池只能放入實現Runable或Callable類線程,不能直接放入繼承Thread的類。

在java中,每次程序運行至少啓動2個線程。一個是main線程,一個是垃圾收集線程。因爲每當使用 java命令執行一個類的時候,實際上都會啓動一個JVM,每一個JVM其實在就是在操作系統中啓動了一個進程。

線程安全

如果有多個線程在同時運行,而這些線程可能會同時運行這段代碼。程序每次運行結果和單線程運行的結果是一樣 的,而且其他的變量的值也和預期的是一樣的,就是線程安全的。
在多線程場景下,如果沒有對資源加鎖,會導致共享資源的訪問出錯,下面模擬售票演示線程不安全:

public class Ticket implements Runnable {
    private int ticket = 100;
    @Override
    public void run() {
        while (true) {
            if (ticket > 0) {
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                String name = Thread.currentThread().getName();
                System.out.println(name + "正在賣:" + ticket);
                ticket--;
            }
        }
    }
}
public class Test1 {
    public static void main(String[] args) {
        Ticket ticket = new Ticket();

        Thread t1 = new Thread(ticket, "窗口1");
        Thread t2 = new Thread(ticket, "窗口2");
        Thread t3 = new Thread(ticket, "窗口3");

        t1.start();
        t2.start();
        t3.start();
    }
}

部分結果:
在這裏插入圖片描述

線程同步

當我們使用多個線程訪問同一資源的時候,且多個線程中對資源有寫操作,就容易出現線程安全問題。
爲了保證每個線程都能正常執行原子操作,Java引入了線程同步機制。有三種方式完成同步操作:

  • 同步代碼塊
  • 同步方法
  • 鎖機制

同步代碼塊

synchronized關鍵字可以用於方法中的某個區塊中,表示只對這個區塊的資源進行互斥訪問。

synchronized(同步鎖) {
	需要同步操作的代碼
}

同步鎖是一個抽象的概念,鎖對象可以是任意類型,比如new Object()也可以。

public class Ticket implements Runnable {
    private int ticket = 100;

	// 一個對象,作爲一把鎖
    Object obj = new Object();

    @Override
    public void run() {
        while (true) {
        	// 也可以直接用this作爲鎖對象
           synchronized (obj) {
               if (ticket > 0) {
                   try {
                       Thread.sleep(100);
                   } catch (InterruptedException e) {
                       e.printStackTrace();
                   }
                   String name = Thread.currentThread().getName();
                   System.out.println(name + "正在賣:" + ticket);
                   ticket--;
               }
           }
        }
    }
}

同步方法

使用synchronized修飾的方法叫做同步方法,保證一個線程在執行方法的時候,其他線程阻塞等待。

public synchronized void method() {
	可能產生線程安全問題的代碼
}
public class Ticket implements Runnable {
    private int ticket = 100;

    @Override
    public void run() {
        while (true) {
            payTicket();
        }
    }

	// 鎖對象默認就是this
    public synchronized void payTicket() {
        if (ticket > 0) {
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            String name = Thread.currentThread().getName();
            System.out.println(name + "正在賣:" + ticket);
            ticket--;
        }
    }
}

同步方法payTicket()也可以設置爲靜態的,對於非靜態方法,同步鎖是this,對於靜態方法,使用當前方法所在類的字節碼對象(類名.class)。

Lock鎖

Lock鎖也稱同步鎖

  • public void lock() :加同步鎖
  • public void unlock() :釋放同步鎖
public class Ticket implements Runnable {
    private int ticket = 100;

    Lock l = new ReentrantLock();

    @Override
    public void run() {
        while (true) {
           l.lock();
            if (ticket > 0) {
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }finally{
                    String name = Thread.currentThread().getName();
                    System.out.println(name + "正在賣:" + ticket);
                    ticket--;
                    l.unlock();
                }
            }
        }
    }
}

線程間通信

多個線程併發執行時,在默認情況下,CPU是隨機性在線程之間進行切換,但是有時候我們希望它能夠有規律的執行,那麼多線程之間就需要一些協調通信來改變或控制CPU的隨機性。Java提供了等待喚醒機制來解決這個問題,具體來說就是多個線程依靠一個同步鎖,然後藉助於wait()notify()方法就可以實現線程間的協調通信。
同步鎖相當於中間人的作用,多個線程必須用同一個同步鎖,只有同一個鎖上的被等待線程,纔可以被持有該鎖的另一個線程喚醒,使用不同鎖的線程之間不能相互喚醒,也就是無法協調通信。

Java在Object類中提供了一些方法可以用來實現線程間的協調通信:

  • public final void wait():釋放當前線程的鎖
  • public final native void wait(long timeout):釋放當前線程的鎖,並等待timeout毫秒
  • public final native notify():喚醒持有同一個鎖的某個線程
  • public final native notifyAll():喚醒持有同一個鎖的所有線程

下面演示使用這幾個API實現多線程情況下,線程有序執行,下面的例子就是循環利用的就是Java中的線程等待喚醒機制。下面的例子就是兩個線程交替執行:

public class MyLock {
    public static Object o = new Object();
}
public class ThreadFor1 extends Thread{
    public void run() {
        for(int i = 0; i < 10; i++) {
            synchronized (MyLock.o) {
                System.out.println("thread1->"+i);

                // 先喚醒其他的一個線程
                MyLock.o.notify();
                try {
                    // 再等待
                    MyLock.o.wait();
                }catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}
public class ThreadFor2 extends Thread{
    public void run() {
        for(int i = 0; i < 10; i++) {
            synchronized (MyLock.o) {
                System.out.println("thread2->"+i);

                // 先喚醒其他的一個線程
                MyLock.o.notify();
                try {
                    // 再等待
                    MyLock.o.wait();
                }catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}
public class Test {
    public static void main(String[] args) {
        Thread t1 = new ThreadFor1();
        Thread t2 = new ThreadFor2();
        t1.start();
        t2.start();
    }
}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章