1、線程概述
1.1、基本概念
進程:正在運行的程序,負責了這個程序的內存空間分配,代表了內存 中的執行區域。
線程:就是在一個進程中負責一個執行路徑。
多線程:就是在一個進程中多個執行路徑同時執行。
電腦上的程序同時在運行,“多任務”操作系統能同時運行多個進程(程序),但實際是由於CUP分時機制的作用,使每個進程都能循環獲得自己的CUP時間片,由於輪換速度非常快,使得所有程序好象是在“同時”運行一樣。 與其說是快速的切換進程,還不如說是線程進行着CUP的資源爭奪戰。
1.2、多線程的利弊
1.2.1、 多線程好處
1、解決一個進程裏可以同時執行多個任務;
2、提高了資源的利用率(不是效率);
1.2.2.、多線程弊端
1、降低了一個進程中線程的執行頻率;
2、對線程進行管理需要額爲的CUP開銷(多線程的使用會給系統帶來上下文切換的額外負擔);
3、線程死鎖,較長時間的等待或資源爭奪以及死鎖等;
4、線程安全問題;
2、線程的創建方式
2.1、繼承Thread類,方式一
步驟:
1、自定義一個類,並繼承Thread類;
2、重寫父類的run()方法,把自定義線程的任務代碼寫在run()方法裏;
3、創建Thread類的子類對象,並調用start()方法啓動線程;
public class BuildThread01 extends Thread{
@Override
public void run() {
for(int i=0;i<100;i++){
System.out.println("自定義線程中的方法-->"+i);
}
}
public static void main(String[] args) {
BuildThread01 thrad = new BuildThread01();
thrad.start();
for (int i = 0; i < 100; i++) {
System.out.println("主線程中的方法-->"+i);
}
}
}
注意要點:
1、不能試着直接調用Thread子類對象的run()方法來啓動線程,如果直接調用run()方法,相當於被當成一個普通的方法;
2、調用start()啓動線程,線程一旦開啓,就會執行run()方法裏的代碼;
這裏額外說一點,main()方法裏除了自定義線程的那一個外,還有兩個線程,一個是main的主線程,另一個是GC垃圾回收器的線程。所以一個main函數裏至少有兩個線程;
有一個問題:重寫父類run()方法的作用是什麼:
每個線程都有自己的任務代碼,JVM創建主線程的任務代碼就是main()函數裏的代碼,自定義線程的任務代碼就是寫在run()方法中的,所以自定義線程負責了run()方法中的代碼。
2.2、類實現Runnable接口,方式一
步驟:
1、自定義一個類實現Runnable接口;
2、實現Runnable接口的run()方法,把自定義線程的任務代碼寫在run()方法裏;
3、創建Runnable實現類對象;
4、創建Thread類的對象,並且把Runnable實現類的對象作爲實參傳遞。
5、調用Tread類對象的start()方法來啓動線程;
public class BuildThread02 implements Runnable{
@Override
public void run() {
for (int i = 0; i <50; i++) {
System.out.println(Thread.currentThread().getName()+"--->"+i);
}
}
public static void main(String[] args) {
//創建Runnable實現類的對象
BuildThread02 thread02 = new BuildThread02();
//創建Thread類的對象, 把Runnable實現類對象作爲實參傳遞。
Thread thread = new Thread(thread02,"狗娃");
//調用thread對象的start方法開啓線程。
thread.start();
for (int i = 0; i <50; i++) {
System.out.println(Thread.currentThread().getName()+"--->"+i);
}
}
}
注意:
1、thread02是Runnable實現類的對象,是不具備start()方法的。
2.3、提問
問題一:Runable實現類的對象是線程對象嗎?
Runable實現類的對象並不是線程對象,它只不過是一個實現了Runable接口的普通對象,不具備start()方法;只有Thread或者thread的子類,纔是線程對象,具備start()方法;
問題二:爲什麼要把Runable實現類的對象作爲實參傳遞給Thread對象?作用是什麼?
這個我們先看一下Thread thread = new Thread(thread02,"狗娃");
Thread()構造方法的源碼:
public Thread(Runnable target, String name) {
init(null, target, name, 0);
}
注意:構造方法裏將Runable實現類的對象名稱叫做target;
然後我們再看Thread的run()方法的源碼:
@Override
public void run() {
if (target != null) {
target.run();
}
}
run()方法裏target正是Runable實現類的對象,也就是:thread02 ,所以target.run();
就等於thread02.run()
;
那麼我們現在來回答這個問題:把Runnable實現類的對象作爲實參傳遞給Thread對象,作用就是把Runnable實現類的對象的run方法作爲了線程的任務代碼去執行了。
問題三:這兩種方法一般使用哪種?
一般使用第二種,實現Runnable接口,因爲java是單繼承,多實現的。
3、線程的一般方法
Thread(String name) 初始化線程的名字
setName(String name) 設置線程對象的名字
getName() 獲取線程對象的名字
sleep(long time) 線程睡眠的指定毫秒數,注意:sleep爲靜態方法,那個線程執行了sleep方法,就是那個線程進入睡眠
currentThread() 返回當前線程對象,注意:currentThread爲靜態方法,那個線程執行了currentThread方法,返回的就是那個線程的對象
getPriority() 返回當前線程的優先級,默認優先級爲5,線程的優先級範圍:[1,10];優先級越大,執行的概率越高
下面我貼一下代碼,作爲簡單使用這幾個方法的例子:
public class ThreadMethods extends Thread{
public ThreadMethods(String threadName) {
super(threadName);
}
@Override
public void run() { // 子類拋出的異常,小於等於父類的異常。
System.out.println("自定義線程的名稱01-》"+this.getName());// this.getName() == Thread.currentThread().getName()
System.out.println("自定義線程的名稱02-》"+Thread.currentThread().getName());
/* for(int i=0 ;i<50;i++){
System.out.println("自定義線程---》"+i);
}*/
try {
//子類拋出的異常,小於等於父類的異常。父類的run()沒有拋出異常,所以子類的run()只能捕獲異常
Thread.sleep(1000);
} catch (InterruptedException e) {
}
}
public static void main(String[] args) throws InterruptedException {
ThreadMethods thread = new ThreadMethods("自定義線程");
Thread.sleep(100);
thread.setPriority(6); // 設置自定義線程的優先級
thread.start();
thread.setName("狗蛋");
System.out.println("自定義線程的名稱-》"+thread.getName());
System.out.println("主線程線程的名稱-》"+Thread.currentThread().getName());
System.out.println("自定義線程的優先級-》"+thread.getPriority());
System.out.println("主線程線程的優先級-》"+Thread.currentThread().getPriority());
// for (int i = 0; i < 50; i++) {
// System.out.println("主方法的線程--->"+i);
// }
}
}
注意:
這裏我簡單講一下main()中Thread.sleep(100);
的異常處理方法與自定義線程Thread.sleep(100);
異常處理方式不一樣,main()中是拋異常,自定義線程中是try-catch
因爲java中:子類拋出的異常,小於等於父類的異常。父類的run()沒有拋出異常,所以子類的run()只能捕獲異常。
4、線程生命週期
這裏我就直接上一張生命週期圖吧
5、線程安全問題
5.1、線程安全問題的由因
嗯,,,我先講一下線程安全問題的出現,看下面代碼:
class SaleTicket extends Thread {
public SaleTicket(String name) {
super(name);
}
static int sum = 50
// static Object o = new Object();
@Override
public void run() {
while (true) {
if (sum <= 0) {
System.out.println("票售完了...");
break;
}
System.out.println(Thread.currentThread().getName() + "售出了第" + sum + "號票");
sum--;
}
}
}
public class ThreadSafes01 {
public static void main(String[] args) {
SaleTicket sale01 = new SaleTicket("一號窗口");
SaleTicket sale02 = new SaleTicket("二號窗口");
SaleTicket sale03 = new SaleTicket("三號窗口");
sale01.start();
sale02.start();
sale03.start();
}
}
先大概講一下代碼,這片代碼是模仿車子窗口售票,創建了三個自定義線程,分別取名爲一、二、三號窗口,上面這個代碼是存在線程安全問題的,我截圖一部分運行的結果圖:
車站售的票是不能重複的,但是圖中運行結果我們可以看到一號跟二號窗口都出售了43號票,這就是線程安全問題了。
簡單講一下出現這一問題的步驟:
1、當一號線程獲取到了CUP的執行權,它走啊走,終於走到了System.out.println(Thread.currentThread().getName() + "售出了第" + sum + "號票");
這一塊代碼,打印出了:一號窗口售出了第43號票;但就在這時,當一號線程還來不及執行sum--;
它的執行權就被二號線程奪取了,所以這裏要記得票數sum還是等於43。
2、現在二號線程拿到了CPU執行權,也是走啊走,在沒有被其他線程奪去執行權的情況下,也是到了System.out.println(Thread.currentThread().getName() + "售出了第" + sum + "號票");
這個位置,打印出了:二號窗口售出了第43號票;
基於上面步驟的解析,那麼線程安全問題出現的條件是什麼呢?
1、有兩個或者兩個以上的線程;
2、多個線程共享一個資源;
3、共享資源由多條代碼組成;這條件不難理解,試想一下啊,假如我們的共享資源只有一句System.out.println(“哈哈哈哈”);
那執行完不就完了嗎,哪兒那麼多事。
5.2、線程安全問題的解決
sun公司提供了線程同步機制來幫我們解決線程安全問題,其同步機制方法有兩種:同步代碼塊,同步函數;
5.2.1、同步代碼塊
格式:
synchronized (鎖對象) {
共享資源(代碼)
}
至於具體的用法,我們以之前的模擬賣車票的代碼爲例,將線程共享的那一片代碼,放進synchronized 的代碼塊裏,如下:
while (true) {
synchronized ("鎖") {
if (sum <= 0) {
System.out.println("票售完了...");
break;
}
System.out.println(Thread.currentThread().getName() + "售出了第" + sum + "號票");
sum--;
}
}
這裏要注意同步代碼塊的範圍,不能連同while (true) {}
這一個區域一同放入同步代碼塊中,如果放入的話,結果就是,只要任何一個線程進入同步代碼塊,就會將所有的票出售完,才釋放鎖對象。所以同步代碼塊同步的範圍是需要根據自己實際的業務進行分析的。
再上一個整體的代碼:
class SaleTicket extends Thread {
public SaleTicket(String name) {
super(name);
}
static int sum = 50;
@Override
public void run() {
while (true) {
synchronized ("鎖") {
if (sum <= 0) {
System.out.println("票售完了...");
break;
}
System.out.println(Thread.currentThread().getName() + "售出了第" + sum + "號票");
sum--;
}
}
}
}
public class ThreadSafes01 {
public static void main(String[] args) {
SaleTicket sale01 = new SaleTicket("一號窗口");
SaleTicket sale02 = new SaleTicket("二號窗口");
SaleTicket sale03 = new SaleTicket("三號窗口");
sale01.start();
sale02.start();
sale03.start();
}
}
同步代碼塊注意事項:
1、鎖對象可以是任意對象;每個對象內部都維護得有狀態碼synchronized()
就是根據這一狀態碼進行同步判斷的。
2、鎖對象得是各個線程共享且唯一的,比如用static
修飾的對象;我們來做一個假設:假設多線程的鎖對象不是唯一且共享的,那麼就是每個線程自己內部都維護了各自的鎖對象,就相當於每個對象都具有一把鑰匙,那麼每個線程就可以隨意打開synchronized()
這把鎖,不用考慮其他線程的感受。所以鎖對象得是各個線程共享且唯一的。
3、在同步代碼塊中調用sleep()
方法,並不會釋放鎖對象,而是當線程的睡眠時間結束,並且執行完同步代碼塊中的方法時,纔會釋放鎖對象。
4、只有當真的存在線程安全時,才設置同步代碼塊,不然會影響效率。
我在上面的代碼中,使用的鎖對象是"鎖"
這樣的字符串對象,這是最簡單的鎖對象。因爲"鎖"
這個已經在字符串常量池中生成,共享且唯一。
5.2.2、同步函數
同步函數:被synchronized
修飾的函數,爲同步函數。
我先直接講一下同步函數的注意要點:
1、使用synchronized
修飾的方法爲非靜態的方法時,鎖對象爲this
對象,當前函數的調用者;
2、使用synchronized
修飾的方法爲靜態方法時,鎖對象爲當前函數所屬對象的字節碼文件(class);
3、同步函數同步機制的鎖對象是固定的,不可以隨意更改;
4、同步函數,同步的是整個方法,所以方法裏的所有代碼都會被同步;
再上一片代碼,來講第一條要點----->
@Override
public synchronized void run() {
while (true) {
if (sum <= 0) {
System.out.println("票售完了...");
break;
}
System.out.println(Thread.currentThread().getName() + "售出了第" + sum + "號票");
sum--;
}
}
我們使用synchronized
修飾非靜態的run()
方法,根據第一條的定義:**使用synchronized
修飾的方法爲非靜態的方法時,鎖對象爲this
對象;**而這裏的this
正是每個正在執行的線程對象,也就是說鎖對象並不是唯一且共享的,所以我們的代碼出現的線程安全問題,如下的執行結果:
然後再上一條代碼解釋第二條:
class SaleTicket extends Thread {
@Override
public void run() {
Ticket();
}
public static synchronized void Ticket() {
while (true) {
if (sum <= 0) {
System.out.println("票售完了...");
break;
}
System.out.println(Thread.currentThread().getName() + "售出了第" + sum + "號票");
sum--;
}
}
}
上面這塊代碼來跑售票是不規範的,無論怎麼跑都是一個窗口將票全部售完,我借用這個代碼講一下鎖對象是哪一個,看一下定義:使用synchronized
修飾的方法爲靜態方法時,鎖對象爲當前函數所屬對象的字節碼文件(class);Ticket()
所屬類是SaleTicket
,所以**Ticket()
的鎖對象就是SaleTicket
的字節碼文件;
6、線程死鎖
先直接上圖,然後再解釋一下圖:
圖片的意思是:狗蛋和狗娃,要看電視,只有同時擁有遙控器和電池才能打開電視。狗蛋搶到了電池,狗娃搶到了遙控器。狗蛋要狗娃給他遙控器,而狗娃要狗蛋給他電池,於是兩人就僵持住了,,,
基於這個情況我們用代碼模擬:
class ThreadLock extends Thread{
public ThreadLock(String name){
super(name);
}
@Override
public void run() {
if("狗娃".equals(Thread.currentThread().getName())){
synchronized ("遙控器") {
System.out.println("狗娃拿到了遙控器了,馬上拿電池...");
synchronized ("電池") {
System.out.println("狗娃拿到了遙控器和電池,正在在愉快的看電視...");
}
}
}else if ("狗蛋".equals(Thread.currentThread().getName())) {
synchronized ("電池") {
System.out.println("狗蛋拿到了電池了,馬上拿遙控器...");
synchronized ("遙控器") {
System.out.println("狗蛋拿到了遙控器和電池,正在在愉快的看電視...");
}
}
}
}
}
public class DeadLock {
public static void main(String[] args) {
ThreadLock thread01 = new ThreadLock("狗娃");
ThreadLock thread02 = new ThreadLock("狗蛋");
thread01.start();
thread02.start();
}
}
這一片代碼執行後有兩種情況,一種是正常的運行,另一種則是陷入了死鎖;
我們先看正常的結果:
我們再看一種死鎖的結果:
這一結果就是狗娃在等電池,狗蛋在等遙控器的僵持狀態。我們來看一下導致這一狀態的原因:
首先我們創建了兩個線程狗娃和狗蛋,併線程的任務代碼裏有兩個鎖對象:遙控器
和電池
;
然後我們來走一下代碼:
1、狗娃先獲得CUP的執行權,走啊走啊,走,走到了System.out.println("狗娃拿到了遙控器了,馬上拿電池...");
這個位置,佔用着遙控器
這個鎖對象,當他正要準備通過電池
這個鎖對象去拿電池的時候,他的執行權被狗蛋搶走了。這時記住了,狗娃任然佔用着遙控器
這個鎖對象;
2、狗蛋拿到的執行權,也是走啊走啊,走,這時由於狗娃並沒有佔用着電池
這一鎖對象,所有狗蛋輕鬆的通過電池
這個鎖對象拿到了電池。但是正當狗蛋去拿遙控器的時候,狗娃正佔用着遙控器
這個鎖對象,所以狗蛋拿不到遙控器,並佔用着電池
這個鎖對象。而狗娃也拿不到電池,因爲狗蛋並未釋放電池
這個鎖對象。就有了上面的死鎖結果。
從上面的運行結果我們可以看出,死鎖並不是一定會發生的,而是概率問題;
出現線程死鎖的根本原因
1、存在兩個或者兩個以上的線程
2、存在兩個或者兩個以上的共享資源
出現線程死鎖的解決方法
沒有,儘量避免
7、線程之間的通訊
線程通訊:一個線程完成任務,去通知另外的線程進行其他的任務
線程通訊方法:
1、wait():調用wait()方法的線程,將釋放鎖對象進入等待狀態,並且只有當其他線程調用notify()方法才能被喚醒;
2、notify():喚醒線程池中等待線程中的一個,不能喚醒指定的線程,一般先等待先喚醒;
3、notifyAll():喚醒線程池中所以的等待線程;
wait()、notify()的注意事項:
1、wait()和notify()方法是屬於Object對象的;
2、wait()和notify()必須在同步代碼塊或者同步函數中才能使用;
3、wait()和notify()只能由鎖對象調用;
4、調用了notify()方法,即使線程池中沒有等待的線程也沒有關係;
提問:爲什麼wait()和notify()只能由鎖對象調用?
1、一個線程執行了wait()方法,那麼該線程就會進入到一個一鎖對象爲標識符的線程池中等待;
2、一個線程執行了notify()方法,那麼就會喚醒以鎖對象爲標識符的線程池中等待的一個線程;
不同的鎖調用wait()和notify()方法就創建了不同的線程池,notify()只能喚醒同一線程池裏的線程;
我們舉一個官方線程通訊的例子:生產者生產一個產品,消費者就消費一個產品,產品是生產者與消費者共享的;
使用代碼模擬上述需要:
// 產品
class Product{
String name;
double price;
boolean flag = false;//是否有生產的產品,默認爲無
}
// 生產者
class Producer extends Thread{
Product p; // 維護了產品
public Producer(Product p,String name) {
super(name);
this.p = p;
}
@Override
public void run() {
int i = 0;
while(true){
synchronized (p) {
try {
if(!p.flag){ // 還未生產
if(i%2==0){
p.name = "橘子";
p.price = 4.5;
}else {
p.name = "蘋果";
p.price = 2.0;
}
System.out.println(Thread.currentThread().getName()+"生產了"+p.name+"<--->價格是:"+p.price);
p.flag = true; // 已經生產產品,將判斷改爲有產品
p.notify(); // 生產完畢,喚醒消費者去消費
i++;
}else {
p.wait(); // 生產者進入等待狀態,即等待消費者去消費
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
// 消費者
class Customers extends Thread{
Product p; // 維護了產品
public Customers(Product p,String name) {
super(name);
this.p = p;
}
@Override
public void run() {
while (true) {
synchronized (p) {
try {
if (p.flag) {//判斷是否有生產的產品
System.out.println(Thread.currentThread().getName()+"消費了"+p.name+"<--->花費了"+p.price);
p.flag = false;// 已經消費產品,將判斷改爲無產品
p.notify(); // 喚醒生產者去生產
}else {
p.wait(); // 消費者進入等待狀態,即等待生產者去生產
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
public class ThreadCommunication {
public static void main(String[] args) {
Product p = new Product();
Customers customers = new Customers(p,"消費者");
Producer producer = new Producer(p,"生產者");
producer.start();
customers.start();
}
}
上面代碼一些詳情在備註裏說明:
1、分別有產品、消費者、生產者三個對象,並且消費者、生產者內部維護了產品這一對象;
2、由於產品是消費者和生產者共享的,所以在消費者、生產者的構造方法裏有產品這一形參,在main方法中將產品對象作爲實參傳入到消費者、生產者中;
3、由於產品對象p是消費者、生產者共享的,所以消費者、生產者中的鎖對象統一爲產品P;