Java 多線程編程(上)

  • 2019 年 11 月 4 日
  • 筆記

學習一時爽,一直學習一直爽

  Hello,大家好,我是 もうり,一個從無到有的技術+語言小白。

https://blog.csdn.net/weixin_44510615/article/details/102617286

Java多線程往往決定Java水平

在 Java 中實現多線程有兩種手段,一種是繼承 Thread 類,另一種就是實現 Runnable 接口。

Java 提供了三種創建線程的方法:

  • 通過實現 Runnable 接口;
  • 通過繼承 Thread 類本身;
  • 通過 Callable 和 Future 創建線程。

實現 Runnable 接口

  • main 入口函數創建線程
  • 線程執行的 run 方法,所以繼承 Runnable 接口,改寫 run 方法

RunnabeDemo.java

/** * @author: 毛利 */class RunnableDemo implements Runnable {    private Thread t;    private String threadName;    RunnableDemo( String name) {        threadName = name;        System.out.println("Creating " +  threadName );    }    public void run() {        System.out.println("Running " +  threadName );        try {            for(int i = 4; i > 0; i--) {                System.out.println("Thread: " + threadName + ", " + i);                // 讓線程睡眠一會0.05                Thread.sleep(50);            }        }catch (InterruptedException e) {            System.out.println("Thread " +  threadName + " interrupted.");        }        System.out.println("Thread " +  threadName + " exiting.");    }    public void start () {        System.out.println("Starting " +  threadName );        if (t == null) {            t = new Thread (this, threadName);            t.start ();        }    }}

TestThread.java

/** * @author: 毛利 */public class TestThread {    public static void main(String args[]) {        RunnableDemo R1 = new RunnableDemo( "Thread-1");        R1.start();        RunnableDemo R2 = new RunnableDemo( "Thread-2");        R2.start();    }}

通過繼承 Thread 類本身

單線程

/** * @author: 毛利 */public class demo1 extends Thread {    // 覆蓋 run 方法    @Override    public void run() {        for (int i = 0; i < 5; i++) {            try {                Thread.sleep(100);            } catch (InterruptedException e) {                // TODO Auto-generated catch block                e.printStackTrace();            }            System.out.println(this.getName() + ",i=" + i);        }    }    public static void main(String[] args) {        // 程序開始,執行的線程名字叫做main        System.out.println("程序開始,執行的線程名字叫做" + Thread.currentThread().getName());        // 創建demo1對象        demo1 demo1 = new demo1();        // 執行run方法        demo1.start();        /*        Thread-0,i=0        Thread-0,i=1        Thread-0,i=2        Thread-0,i=3        Thread-0,i=4         */    }}

如果我再添加一共 demo2 線程

demo1 demo2 = new demo1();demo2.start();

發現了

竟然是同步運行的

是時候吹吹 synchronized修飾符,難度使用 synchronized修飾符就一定同步了?

現在使用繼承 Runnable 方法來看看毛利是不是窮人

/** * @author: 毛利 */public class demo2 implements Runnable{    /*    創建生產者和消費者的概念     */    // 一開始毛利沒有錢的    private int count = 0;    // 毛利我掙錢    public synchronized void addMoney(){        double money = Math.floor(Math.random()*100);        count += money;        System.out.println( "毛利我掙了"+ money + "元");    }    // 毛利我花錢    public synchronized void subMoney(){        double money =  Math.floor(Math.random()*100);        if (count - money < 0 ){            count -= money;            System.out.println("毛利你又花了"+ money + "你現在負債啊啊,準備喝西北風了");        }        else {            count -= money;            System.out.println("毛利又花了" + money + ",準備吃土了");        }    }    // 看看毛利口袋    public void lookMoney(){        System.out.println("毛利現在口袋只有"+count);    }    public static void main(String[] args) {        demo2 maoli = new demo2();        maoli.run();    }    @Override    public void run() {        for (int j =0 ; j<10;j++){            addMoney();            subMoney();            lookMoney();        }    }}

運行結果如下,果然我還是一個窮人,天天喝西北風

毛利我掙了79.0元毛利你又花了95.0你現在負債啊啊,準備喝西北風了毛利現在口袋只有-16毛利我掙了2.0元毛利你又花了35.0你現在負債啊啊,準備喝西北風了毛利現在口袋只有-49毛利我掙了2.0元毛利你又花了14.0你現在負債啊啊,準備喝西北風了毛利現在口袋只有-61毛利我掙了34.0元毛利你又花了26.0你現在負債啊啊,準備喝西北風了毛利現在口袋只有-53毛利我掙了36.0元毛利你又花了63.0你現在負債啊啊,準備喝西北風了毛利現在口袋只有-80毛利我掙了57.0元毛利你又花了7.0你現在負債啊啊,準備喝西北風了毛利現在口袋只有-30毛利我掙了67.0元毛利你又花了66.0你現在負債啊啊,準備喝西北風了毛利現在口袋只有-29毛利我掙了68.0元毛利你又花了63.0你現在負債啊啊,準備喝西北風了毛利現在口袋只有-24毛利我掙了76.0元毛利你又花了96.0你現在負債啊啊,準備喝西北風了毛利現在口袋只有-44毛利我掙了38.0元毛利你又花了80.0你現在負債啊啊,準備喝西北風了毛利現在口袋只有-86

這次程序是一個一個運行的。不是一段一段的

這次毛利創建兩個線程看看毛利是否吃土

public static void main(String[] args) throws InterruptedException {        demo2 maoli = new demo2();        // maoli.run();        Thread thread1 = new Thread(maoli);        Thread thread2 = new Thread(maoli);        //start() 本身就是執行run        thread1.start();        thread2.start();        thread1.join();        thread2.join();        System.out.println("終於知道毛利是一個窮光蛋了");    }

兩個線程同步運行

thread1.start();thread2.start(); 意思是開啟了 thread1 和 thread2,也就是兩個子線程都開始執行 run 方法。

最後毛利果然還是喝西北風

thread1.join();thread2.join(); 意思是 thread1 子線程執行完再執行主線程,thread2 子線程執行完再執行主線程. 最後輸出毛利是窮光蛋

毛利我要專業點說說 synchronized

  • synchronized

如果一個對象對多個線程可見,則對該對象變量的所有讀取和寫入都是通過同步方法完成的。

通俗:能夠保證你在同一時刻最多只有一個線程執行該段代碼,以達到保證並發安全的效果。

synchronized 是 Java 的關鍵字,是最基本的互斥同步手段,是並發編程必學內容。

並發後果

Java 並發一道好題

/** * @author: 毛利 */public class demo3 implements Runnable {    // main 方法靜態,所以num必須是加static    static int num = 0;    @Override    public void run() {        for (int i = 0 ;i<10000;i++){            num++;        }    }    public static void main(String[] args) throws InterruptedException {        demo3 main = new demo3();        Thread thread1 = new Thread(main);        Thread thread2 = new Thread(main);        thread1.start();        thread2.start();        thread1.join();        thread2.join();        System.out.println(num);    }}

代碼如下創建兩個線程跑 run 方法,最後我想知道這個 num 到底是多少?

2 個線程,10000*2=20000

NO? 納里?

如果兩個線程存在一種現象不是同步所有讀取和寫入,那麼就不可能是 20000

答案是 0-20000 之間的任何一個數

加上 synchronized 問題就解決了

@Overridepublic synchronized void run() {    for (int i = 0 ;i<10000;i++){        num++;    }}

當然還可以不用在函數入口加, 添加 synchronized(this)一樣的

@Overridepublic  void run() {    synchronized(this) {        for (int i = 0; i < 10000; i++) {            num++;        }    }}

當兩個並發線程訪問同一個對象 object 中的這個 synchronized(this) 同步代碼塊時,一個時間內只能有一個線程得到執行。另一個線程必須等待當前線程執行完這個代碼塊以後才能執行該代碼塊。

上面的代碼修飾的 synchronized 是非靜態方法,如果修飾的是靜態方法(static)含義是完全不一樣的。

但是如果我 new 兩個對象,這樣就又 2 個 object 的對象鎖,答案又變了

/** * @author: 毛利 */public class demo3 implements Runnable {    // main 方法靜態,所以num必須是加static    static int num = 0;    @Override    public  void run() {        synchronized(this) {            for (int i = 0; i < 10000; i++) {                num++;            }        }    }    public static void main(String[] args) throws InterruptedException {        // new兩個對象        Thread thread1 = new Thread(new demo3());        Thread thread2 = new Thread(new demo3());        thread1.start();        thread2.start();        thread1.join();        thread2.join();        System.out.println(num);        // 0-20000    }}

是時候拋出對象鎖,不過先了解下類鎖

  • 類鎖:在代碼中的方法上加了 static 和 synchronized 的鎖,或者 synchronized(xxx.class)

類鎖對應的關鍵字是 static sychronized,是一個全局鎖,無論多少個對象否共享同一個鎖(也可以鎖定在該類的 class 上或者是 classloader 對象上),同樣是保障同一個時刻多個線程同時訪問同一個 synchronized 塊,當一個線程在訪問時,其他的線程等待。

  • 私有鎖:在類內部聲明一個私有屬性如 private Object lock,在需要加鎖的代碼段 synchronized(lock)
  • 對象鎖:在代碼中的方法上加了 synchronized 的鎖,或者 synchronized(this) 的代碼段 方法鎖和私有鎖:都屬於對象鎖

對象鎖

對象鎖對應 synchronized 關鍵字,當多個線程訪問多個實例時,它們互不干擾,每個對象都擁有自己的鎖,如果是單例模式下,那麼就是變成和類鎖一樣的功能。

對象鎖防止在同一個時刻多個線程訪問同一個對象的 synchronized 塊。如果不是同一個對象就沒有這樣子的限制。

Synchronized(對象鎖)和 Static Synchronized(類鎖)的區別

/** * @author: 毛利 */public class demo4 {    //  Synchronized(對象鎖)    public synchronized void test1() {        int i = 5;        while (i-- > 0) {            System.out.println(Thread.currentThread().getName() + " : " + i);            try {                Thread.sleep(500);            } catch (InterruptedException e) {            }        }    }    // Static Synchronized(類鎖)    public static synchronized void test2() {        int i = 5;        while (i-- > 0) {            System.out.println(Thread.currentThread().getName() + " : " + i);            try {                Thread.sleep(500);            } catch (InterruptedException e) {            }        }    }    public static void main(String[] args) {        demo4 demo4 = new demo4();        // 採用Runable 複寫 run        Thread test1 = new Thread(new Runnable() {            public void run() {                demo4.test1();            }        }, "test1");        Thread test2 = new Thread(new Runnable() {            public void run() {                demo4.test2();            }        }, "test2");        test1.start();        test2.start();    }    /*    test2 : 4    test1 : 4    test2 : 3    test1 : 3    test2 : 2    test1 : 2    test1 : 1    test2 : 1    test2 : 0    test1 : 0     */}
  • Static Synchronized(類鎖)比 Synchronized(對象鎖)訪問快

下面使用同一個對象的多個實例

public static void main(String[] args) {        demo4 demo4 = new demo4();        // 採用Runable 複寫 run        Thread test1 = new Thread(new Runnable() {            public void run() {                demo4.test1();            }        }, "test1");        Thread test2 = new Thread(new Runnable() {            public void run() {                demo4.test1();            }        }, "test2");        Thread test3 = new Thread(new Runnable() {            public void run() {                demo4.test2();            }        }, "test3");        Thread test4 = new Thread(new Runnable() {            public void run() {                demo4.test2();            }        }, "test4");        test1.start();        test2.start();        test3.start();        test4.start();    }

果然 test2 和 test4 不會馬上調用

test1 : 4test3 : 4test3 : 3test1 : 3test3 : 2test1 : 2test3 : 1test1 : 1test1 : 0test3 : 0test2 : 4test4 : 4test4 : 3test2 : 3test2 : 2test4 : 2test2 : 1test4 : 1test4 : 0test2 : 0

現在使用多個對象來調用

public static void main(String[] args) {        // 採用Runable 複寫 run        Thread test1 = new Thread(new Runnable() {            public void run() {                new demo4().test1();            }        }, "test1");        Thread test2 = new Thread(new Runnable() {            public void run() {                new demo4().test1();            }        }, "test2");        Thread test3 = new Thread(new Runnable() {            public void run() {                new demo4().test2();            }        }, "test3");        Thread test4 = new Thread(new Runnable() {            public void run() {                new demo4().test2();            }        }, "test4");        test1.start();        test2.start();        test3.start();        test4.start();    }

test4 是不會馬上調用的,因為是類鎖,不是對象鎖

test1 : 4test3 : 4test2 : 4test3 : 3test1 : 3test2 : 3test2 : 2test3 : 2test1 : 2test1 : 1test2 : 1test3 : 1test2 : 0test1 : 0test3 : 0test4 : 4test4 : 3test4 : 2test4 : 1test4 : 0

對象鎖防止在同一個時刻多個線程訪問同一個對象的 synchronized 塊。

如果不是同一個對象就沒有這樣子的限制。但是類鎖就不同了。

synchronized 是對類的當前實例進行加鎖,防止其他線程同時訪問該類的該實例的所有 synchronized 塊,注意這裡是 「類的當前實例」, 類的兩個不同實例就沒有這種約束了。