多線程:概述;實現多線程的方式;線程控制;線程安全問題

多線程

一、進程和線程

​ 進程:進程就是正在運行的程序,是系統進行資源分配和調用的獨立單位。每一個進程都有它自己的內存空間和系統資源。
​ 多進程:因爲單核CPU在某個時間點上只能做一件事情,計算機是在多個進程間做着頻繁切換,且切換速度很快,所以,我們感覺多個進程在同時進行,其實並不是同時執行的。多進程的作用不是提高執行速度,而是提高CPU的使用率。
​ 線程:在一個進程內部又可以執行多個任務,而這每一個任務我們就可以看成是一個線程。線程是CPU調度的基本單位。
​ 多線程:Java使用的線程模式是搶佔式調度,即多個線程搶佔CPU的時間片(執行權),線程的優先級高的搶佔的概率相對較大,優先級相同的,搶佔的概率的相等,具有隨機性,不確定性;多線程可以解決負載均衡問題,充分利用CPU的資源。
​ 併發:指在同一時刻只能有一條指令執行,但多個進程指令被快速的輪換執行,使得在宏觀上具有多個進程同時執行的效果,但在微觀上並不是同時執行的,只是把時間分成若干段,使多個進程快速交替的執行。
​ 並行:指在同一時刻,有多條指令在多個處理器上同時執行。所以無論從微觀還是從宏觀來看,二者都是一起執行的。

二、多線程實現

Thread類的基本獲取和設置方法
	public final String getName() //獲取線程名稱
	public final void setName(String name) //設置線程名稱
	public static Thread currentThread()//獲取當前執行的線程

1、方式一

public class Blog {
    public static void main(String[] args) {
        //創建MyThread的對象
        MyThread th1 = new MyThread();
        MyThread th2 = new MyThread();
        th1.setName("線程1");
        //通過對象調用start()方法開啓線程
        th1.start();
        th2.setName("線程2");//設置線程名稱
        th2.start();
    }
}
//定義一個類繼承Thread類
class MyThread extends Thread{
    //重寫run()方法
    @Override
    public void run() {
        //獲取當前線程的名稱
        String name = Thread.currentThread().getName();
        System.out.println(name+":需要執行的代碼!");
    }
}
 /*注意:
        a: 開啓線程調用的是start()方法,不是run()方法,調用run()方法僅僅只是將run方法執行一遍,並沒有開啓線程;
        b: run()方法中一般執行比較耗時的代碼
*/

2、方式二

public class Blog1 {
    public static void main(String[] args) {
        RunnableImpl runnable = new RunnableImpl();
        //將Runnable子類對象作爲參數傳給Thread,獲取Thread的對象
        Thread th1 = new Thread(runnable);
        th1.setName("線程1");//設置線程名稱
        //也可通過構造方法設置線程名稱
        Thread th2 = new Thread(runnable, "線程2");
        //開啓線程
        th1.start();
        th2.start();
    }
}
//定義一個類實現Runnable接口
class RunnableImpl implements Runnable{
    @Override
    public void run() {
        String name = Thread.currentThread().getName();
        System.out.println(name+"需要執行的代碼");
    }
}

3、方式三

import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;

public class Blog2 {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        MyCallable myCallable = new MyCallable();
        FutureTask<Integer> task = new FutureTask<>(myCallable);
        //將task對象傳遞給Thread有參構造,獲取Thread對象
        Thread th1 = new Thread(task,"線程1");
        th1.start();//開啓線程
        Integer sum = task.get();//獲取call方法返回值
        System.out.println(sum);//49995000
    }
}
//定義一個類實現Callable接口
class MyCallable implements Callable<Integer>{

    //實現call()方法
    @Override
    public Integer call() throws Exception {
        int sum=0;
        for (int i = 0; i < 10000; i++) {
            sum+=i;
        }
        return sum;
    }
}

三、線程控制

1、設置線程優先級

	public final int getPriority() //獲取線程的優先級
	public final void setPriority(int newPriority)//設置線程的優先級,默認優先級爲5,最高優先級爲10,最低爲1
	//注意:線程優先級高只是搶佔CPU時間片的概率大,而不是該線程就一定比其他線程先搶佔,多線程搶佔CPU執行權具有隨機性,不確定性,一兩次運行不能說明問題。
    //演示
        th1.setPriority(Thread.MIN_PRIORITY);//設置爲最低優先級
        int priority1 = th1.getPriority();
        System.out.println(priority1);//1

		th2.setPriority(Thread.MAX_PRIORITY);//設置爲最低優先級
        int priority2 = th2.getPriority();
        System.out.println(priority2);//10

		//獲取主線程的優先級
        int priority = Thread.currentThread().getPriority();
        System.out.println(priority);//5

2、線程休眠

public static void sleep(long millis) //讓線程休眠millis毫秒

3、加入線程

public final void join()	//等待該線程執行完畢了以後,其他線程才能再次執行;注意事項: 在線程啓動之後,再調用該方法
//演示
	//可以實現讓多個線程串行執行
        th1.start();
        th1.join();
        th2.start();
        th2.join();
        th3.start();
        th3.join();    			

4、禮讓線程

public static void yield()	//暫停當前正在執行的線程對象,並執行其他線程。

注意:線程禮讓是暫停當前正在執行的線程,由於暫停的時間是相當短的,如果在這個線程暫停完畢以後,其他的線程還沒有搶佔到CPU的執行權,那麼這個時候這個線程就會再次和其他線程搶佔CPU的執行權.

5、守護線程

public final void setDaemon(boolean on):
	//將該線程標記爲守護線程或用戶線程。當所有的用戶線程都結束之後,Java 虛擬機就會退出,不管是否還有未完成的守護線程。 
	//注意:該方法必須在啓動線程前調用。 

6、中斷線程

public final void stop():		//停止線程的運行(已過時)
public void interrupt():		//中斷線程的阻塞狀態
//演示
public class Blog1 {
    public static void main(String[] args){
        RunnableImpl runnable = new RunnableImpl();
        Thread th1 = new Thread(runnable,"線程1");

        th1.start();
        th1.interrupt();//中斷此線程阻塞狀態
    }
}

class RunnableImpl implements Runnable{
    @Override
    public void run() {
        try {
            Thread.sleep(1000);//讓線程休眠1000毫秒
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        String name = Thread.currentThread().getName();
        System.out.println(name+"需要執行的代碼");
    }
}
運行結果:

java.lang.InterruptedException: sleep interrupted  //中斷異常:休眠被interrupted終止
at java.lang.Thread.sleep(Native Method)
at myblog1.RunnableImpl.run(Blog1.java:17)
at java.lang.Thread.run(Thread.java:748)
線程1需要執行的代碼

Process finished with exit code 0

7、線程間的等待喚醒機制

Object 類中
	void wait ()  在其他線程調用此對象的 notify () 方法或 notifyAll () 方法前,導致當前線程等待。    
	void wait (long timeout) 在其他線程調用此對象的 notify () 方法或 notifyAll () 方法,或者超過指定的時間量前,導致當前線程等待。
	void notify () 喚醒在此對象監視器上等待的單個線程。
	void notifyAll ()  喚醒在此對象監視器上等待的所有線程。
public class MyTest {
    public static void main(String[] args) {
        //生產線程生產一個資源,通知消費線程消費一個資源,
        //消費線程消費完一個資源,再通知生產線程生產一個資源
        Student student = new Student();
        new SetThread(student).start();
        new GetThread(student).start();
    }
}

class SetThread extends Thread{
    private static Student student;
    private static int i;
    public SetThread(Student student) {
        this.student=student;
    }

    @Override
    public void run() {
        while (true) {
            synchronized (student) {
                if(student.flag){
                    try {
                        student.wait();//有資源,當前線程進入等待狀態,等待消費線程消費資源,並釋放鎖
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                //模擬生產資源
                if(i%2==0){
                    student.name="張三";
                    student.age=23;
                }else {
                    student.name="李四";
                    student.age=24;
                }
                i++;
                student.flag=true;//上面生產了資源,修改標記
                student.notify();//喚醒消費線程等待狀態,讓其消費資源
            }
        }
    }
}

class GetThread extends Thread{
    private static Student student;
    public GetThread(Student student) {
        this.student=student;
    }

    @Override
    public void run() {
        while (true) {
            synchronized (student) {
                if(!student.flag){
                    try {
                        student.wait();//沒有資源,當前線程進入等待狀態,等待生產線程生產資源,並釋放鎖
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                System.out.println(student.name+"=="+student.age);//打印模擬消費資源
                student.flag=false;//上面了消費資源,修改標記
                student.notify();//喚醒生產線程等待狀態,讓其生產資源
            }
        }
    }
}

class Student{
    public String name;
    public int age;
    public boolean flag=false;//true表示有資源(即學生信息),false表示沒有資源
}

運行結果:

張三==23
李四==24
張三==23
李四==24
張三==23
...(死循環)

8、線程狀態轉換圖

在這裏插入圖片描述

四、線程安全問題

判斷一個多線程應用程序是否有問題的標準:

  • a: 是否是多線程環境
  • b: 是否存在共享數據
  • c: 是否存在多條語句同時操作共享數據

解決問題:

由於我們要使用多線程環境,和共享數據,所以只能對"多條語句同時操作共享數據"這部分進行處理。

1、解決方式一:使用同步代碼塊處理

public class Blog1 {
    public static void main(String[] args){
        RunnableImpl runnable = new RunnableImpl();
        new Thread(runnable,"窗口1").start();
        new Thread(runnable,"窗口2").start();
        new Thread(runnable,"窗口3").start();
    }
}

class RunnableImpl implements Runnable{
    private static int num=100;//定義票的數量
    private static Object obj=new Object();//定義共享鎖對象
    @Override
    public void run() {
        while (true){
            //同步代碼塊作用:當一個線程正在執行同步代碼塊時,其他線程必須等待其執行完,才能執行此代碼塊
            synchronized (obj){
                try {
                    Thread.sleep(30);//讓線程休眠30毫秒,模擬網絡延遲
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                if(num>=1){
                    System.out.println(Thread.currentThread().getName()+"已出售第"+(num--)+"張票!");
                }else {
                    break;
                }
            }
        }
    }
}
同步代碼塊格式:
synchronized(鎖對象){ //這裏的鎖對象可以爲任意對象
		需要同步的代碼;
	}

同步的好處:解決了多線程的安全問題。

同步的弊端:當線程相當多時,因爲每個線程都會去判斷同步上的鎖,這是很耗費資源的,無形中會降低程序的運行效率。

2、解決方式二:使用同步方法

public class Blog1 {
    public static void main(String[] args) {
        RunnableImpl runnable = new RunnableImpl();
        new Thread(runnable, "窗口1").start();
        new Thread(runnable, "窗口2").start();
        new Thread(runnable, "窗口3").start();
    }
}

class RunnableImpl implements Runnable {
    private static int num = 100;//定義票的數量

    @Override
    public void run() {
        while (true) {
                sell();
            if (num < 1) {
                break;
            }
        }
    }

    //同步方法
    private synchronized void sell() {
        if (num>=1) {
            try {
                Thread.sleep(30);//讓線程休眠30毫秒,模擬網絡延遲
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            String name = Thread.currentThread().getName();
            System.out.println(name + "已出售第" + (num--) + "張票!");
        }
    }
}

​ 注意:同步方法和同步代碼塊類似,不過使用鎖對象不同,同步代碼塊鎖對象爲任意對象,而同步方法鎖對象是this,靜態同步方法鎖對象是字節碼文件(A.class)

3、解決方式三:使用Lock鎖

import java.util.concurrent.locks.ReentrantLock;

public class Blog2 {
    public static void main(String[] args) {
        MyThread myThread = new MyThread();
        new Thread(myThread, "窗口1").start();
        new Thread(myThread, "窗口2").start();
        new Thread(myThread, "窗口3").start();
    }
}

class MyThread implements Runnable {
    private static int num = 100;//定義票的數量
    static ReentrantLock lock = new ReentrantLock();//創建鎖對象

    @Override
    public void run() {
        while (true) {
            lock.lock();//開啓鎖
            try {
                Thread.sleep(30);//讓線程休眠30毫秒,模擬網絡延遲
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            if (num >= 1) {
                System.out.println(Thread.currentThread().getName() + "已出售第" + (num--) + "張票!");
            } else {
                //此處也應關閉鎖,因爲程序執行到這,會直接跳出循環,不執行最後的關鎖語句,若不關閉鎖,則會出現死鎖現象
                lock.unlock();
                break;
            }
            lock.unlock();//關閉鎖
        }
    }
}

拓展:死鎖現象

public class MyTest {
    public static void main(String[] args) {
        new MyThread(true).start();
        new MyThread(false).start();
    }
}
interface ObjectUtils{
    Object objA=new Object();
    Object objB=new Object();
}
class MyThread extends Thread{
    boolean flag;
    public MyThread(boolean flag) {
        this.flag=flag;
    }

    @Override
    public void run() {
        if(flag){
            synchronized (ObjectUtils.objA){
                System.out.println("true====ObjA 進來了");
                synchronized (ObjectUtils.objB){
                    System.out.println("true====ObjB進來了");
                }
            } //objA不釋放,下面的objA同步代碼塊執行不了
        }else{
            synchronized (ObjectUtils.objB) {
                System.out.println("false====ObjB 進來了");
                synchronized (ObjectUtils.objA) {
                    System.out.println("false====ObjA 進來了");
                }
            }//objB不釋放,上面的objB同步代碼塊執行不了
        }
    }
}

4、volatile關鍵字

JVM的內存模型:
Java內存模型規定了所有的變量都存儲在主內存中。每條線程中還有自己的工作內存,線程的工作內存中保存了被該線程所使用到的變量(這些變量是從主內存中拷貝而來)。
線程對變量的所有操作(讀取,賦值)都必須在工作內存中進行。
不同線程之間也無法直接訪問對方工作內存中的變量,線程間變量值的傳遞均需要通過主內存來完成。

volatile關鍵字:當多個線程操作共享數據時,可以保證內存中的數據可見。即相當於若工作內存中的變量發生改變,則會立即刷回主內存。

5、CAS算法

	概述:CAS (Compare-And-Swap) 是一種硬件對併發的支持,針對多處理器操作而設計的處理器中的一種特殊指令,用於管理對共享數據的併發訪問。CAS 是一種無鎖的非阻塞算法的實現。
	java.util.concurrent.atomic 包下提供了一些原子操作的常用類:
		 AtomicBoolean 、 AtomicInteger 、 AtomicLong 、 AtomicReference
		 AtomicIntegerArray 、 AtomicLongArray
		 AtomicMarkableReference
		 AtomicReferenceArray 
import java.util.concurrent.atomic.AtomicInteger;

public class MyTest1 {
    public static void main(String[] args) {
        for (int i = 0; i < 10; i++) {
            new Thread(new MyRunnable()).start();
        }
    }
}
class MyRunnable implements Runnable{
    static AtomicInteger i=new AtomicInteger(1);//給原子操作數i賦初值1
    @Override
    public void run() {
        while (true) {
            try {
                Thread.sleep(20);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            //讓i自加1,並打印
            System.out.println(Thread.currentThread().getName()+":"+i.getAndIncrement());
        }
    }
}

運行結果:

Thread-0:1
Thread-2:2
Thread-1:3
Thread-3:4
Thread-7:5
Thread-5:6
    ...
發佈了55 篇原創文章 · 獲贊 31 · 訪問量 1萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章