Java之多執行緒、執行緒池
多執行緒
執行緒:執行緒是程序中的一個執行單元,負責當前程序中程式的執行,一個程序中至少一個執行緒(main),但是可以有多個執行緒的,則這個程式稱為多執行緒程式。每一個執行緒執行不同的任務,多執行緒程式並不能提高程式的執行速度,可以提高程式的執行效率。
CPU處理可以分為分時排程和搶佔式排程,分時排程:即執行緒是輪流被CPU處理器所處理,處理時間大致相同,搶佔式排程:即讓優先順序最高的執行緒使用CPU,如果優先順序相同,則會隨機選擇一個執行緒。Java就是使用搶佔式排程。
主執行緒:
Java虛擬機器是從程式中的main方法開始執行,按照程式程式碼的順序來執行,一直到程式結束,這個執行緒就是主執行緒。
但是在實際開發中單執行緒程式並不能實際解決問題,往往需要有多個執行緒,分別執行不同的任務,並且同時進行,且互不干擾,則就需要藉助多執行緒技術。
多執行緒:
在API中可以查到建立執行緒的類被稱為Thread類,此類繼承Object並實現Runnable類。
多執行緒執行時,在記憶體中是如何執行的呢?
多執行緒執行時,Java虛擬機器先找到main方法,使其進入棧中,若程式中含有建立執行緒的程式碼,並啟動執行緒,如x.start() ,則這個新執行緒進入另一個新的棧中,當執行執行緒的任務結束後,執行緒在棧記憶體中自動釋放,當所有的執行緒結束後,則程序結束。
Thread類獲取執行緒名稱方法:
返回該執行緒的名稱。
返回對當前正在執行的執行緒物件的引用。
例項:
class MyThread extends Thread { //繼承Thread MyThread(String name){ super(name); } //複寫其中的run方法 public void run(){ for (int i=1;i<=20 ;i++ ){ System.out.println(Thread.currentThread().getName()+",i="+i); } } } class ThreadDemo { public static void main(String[] args) { //建立兩個執行緒任務 MyThread d = new MyThread(); MyThread d2 = new MyThread(); d.run();//沒有開啟新執行緒, 在主執行緒呼叫run方法 d2.start();//開啟一個新執行緒,新執行緒呼叫run方法//d.getName(); } }
建立執行緒有兩個方法:
1. 通過建立一個繼承Thread類的子類,重寫Thread類中的run() 方法,並新建該子類的一個物件,然後再呼叫start()方法即可。
2. 通過宣告實現Runnable介面的類,並實現run() 方法,然後新建該子類的一個物件,呼叫start() 方法。
建立執行緒方式一:
1 定義一個類繼承Thread。
2 重寫run方法。
3 建立子類物件,就是建立執行緒物件。
4 呼叫start方法,開啟執行緒並讓執行緒執行,同時還會告訴jvm去呼叫run方法。例項:
public class Demo01 {
public static void main(String[] args) {
//建立自定義執行緒物件
MyThread mt = new MyThread("新的執行緒!");
//開啟新執行緒
mt.start();
//在主方法中執行for迴圈
for (int i = 0; i < 10; i++) {
System.out.println("main執行緒!"+i);
}
}
}
Thread繼承類程式碼:
public class MyThread extends Thread {
//定義指定執行緒名稱的構造方法
public MyThread(String name) {
//呼叫父類的String引數的構造方法,指定執行緒的名稱
super(name);
}
/**
* 重寫run方法,完成該執行緒執行的邏輯
*/
@Override
public void run() {
for (int i = 0; i < 10; i++) {
System.out.println(getName()+":正在執行!"+i);
}
}
}
程式碼中的start()方法:使該執行緒開始執行;Java 虛擬機器呼叫該執行緒的
run
方法。
建立執行緒方式二:
建立執行緒的步驟。
1、定義類實現Runnable介面。
2、覆蓋介面中的run方法。。
3、建立Thread類的物件
4、將Runnable介面的子類物件作為引數傳遞給Thread類的建構函式。
5、呼叫Thread類的start方法開啟執行緒。
例項:public class Demo02 {
public static void main(String[] args) {
//建立執行緒執行目標類物件
MyRunnable runn = new MyRunnable();
//將Runnable介面的子類物件作為引數傳遞給Thread類的建構函式
Thread thread = new Thread(runn);
Thread thread2 = new Thread(runn);
//開啟執行緒
thread.start();
thread2.start();
for (int i = 0; i < 10; i++) {
System.out.println("main執行緒:正在執行!"+i);
}
}
}
介面實現類:public class MyRunnable implements Runnable{
//定義執行緒要執行的run方法邏輯
@Override
public void run() {
for (int i = 0; i < 10; i++) {
System.out.println("我的執行緒:正在執行!"+i);
}
}
}
兩種方法進行比較:
第二種方式實現Runnable介面避免了單繼承的侷限性,所以較為常用。實現Runnable介面的方式,更加的符合面向物件,執行緒分為兩部分,一部分執行緒物件,一部分執行緒任務。繼承Thread類,執行緒物件和執行緒任務耦合在一起。一旦建立Thread類的子類物件,既是執行緒物件,有又有執行緒任務。實現runnable介面,將執行緒任務單獨分離出來封裝成物件,型別就是Runnable介面型別。Runnable介面對執行緒物件和執行緒任務進行解耦。
執行緒安全:
如果多個執行緒在同時執行,這些執行緒可能會同時運行同一個數據,則可能會出現錯誤。
電影院要賣票,我們模擬電影院的賣票過程。假設要播放的電影是 “功夫熊貓3”,本次電影的座位共100個(本場電影只能賣100張票)。
我們來模擬電影院的售票視窗,實現多個視窗同時賣 “功夫熊貓3”這場電影票(多個視窗一起賣這100張票)。例項:
public class ThreadDemo {
public static void main(String[] args) {
//建立票物件
Ticket ticket = new Ticket();
//建立3個視窗
Thread t1 = new Thread(ticket, "視窗1");
Thread t2 = new Thread(ticket, "視窗2");
Thread t3 = new Thread(ticket, "視窗3");
t1.start();
t2.start();
t3.start();
}
}
public class Ticket implements Runnable {
//共100票
int ticket = 100;
@Override
public void run() {
//模擬賣票
while(true){
if (ticket > 0) {
//模擬選坐的操作
try {
Thread.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "正在賣票:" + ticket--);
}
}
}
視窗1正在賣票:3
視窗2正在賣票:2
視窗3正在賣票:1
視窗2正在賣票:-1
視窗1正在賣票:0
執行結果發現:上面程式出現了問題
票出現了重複的票
錯誤的票 0、-1
解析:在上述程式碼while語句的if語句內,存線上程安全隱患,比如當前票數還剩下1張,視窗1、2、3正在搶票,若視窗1搶到了CPU的處理片,進入if語句內,進入try語句內,則程式進入休眠狀態,視窗2或3其中之一搶到了CPU的處理片,再次進入if語句內,滿足ticket > 0的條件,也再次進入了休眠狀態,當視窗1過了sleep時間後,執行剩餘操作,將ticket--,則ticket = 0,然後視窗2過了sleep時間後,再次執行剩餘操作,將ticket--,則ticket = -1,所以出現了上述結果。Java提供了執行緒同步機制,能夠解決執行緒安全問題。
執行緒安全的解決辦法:
1. 執行緒同步
2. Lock介面
1.執行緒同步:
執行緒同步的方式有兩種:
方式1:同步程式碼塊
方式2:同步方法
方式一同步程式碼塊:
在程式碼塊宣告時加上synchronized關鍵字。synchronized(鎖物件){
可能會產生執行緒安全問題的程式碼
}
同步程式碼塊中的鎖物件可以是任意的物件;但多個執行緒時,要使用同有個鎖物件才能保證執行緒安全。
例項:
public class Ticket implements Runnable {
//共100票
int ticket = 100;
//定義鎖物件
Object lock = new Object();
@Override
public void run() {
//模擬賣票
while(true){
//同步程式碼塊
synchronized (lock){
if (ticket > 0) {
//模擬電影選坐的操作
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "正在賣票:" + ticket--);
}
}
}
}
}
在有執行緒任務的程式碼塊中用synchronized(鎖物件)來包裹起來。這樣就可以解決執行緒安全問題,但前提是當多個執行緒同時使用一個執行緒任務時,必須保證鎖物件唯一,不然仍然會出現安全問題。方式二同步方法:
同步方法:在方法宣告上加上synchronized關鍵字,可以線上程程式碼run方法中寫一個用synchronized關鍵字修飾的函式,如:
public synchronized void method(){
可能會產生執行緒安全問題的程式碼
}
同步方法中的鎖物件是 this
public class Ticket implements Runnable {
//共100票
int ticket = 100;
//定義鎖物件
Object lock = new Object();
@Override
public void run() {
//模擬賣票
while(true){
//同步方法
method();
}
}
//同步方法,鎖物件this
public synchronized void method(){
if (ticket > 0) {
//模擬選坐的操作
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "正在賣票:" + ticket--);
}
}
}
靜態同步方法: 在方法宣告上加上staticsynchronized
public static synchronized void method(){
可能會產生執行緒安全問題的程式碼
}
靜態同步方法中的鎖物件是類名.class
2.Lock介面:
Lock
實現提供了比使用 synchronized
方法和語句可獲得的更廣泛的鎖定操作。
void lock()
獲取鎖。
釋放鎖。
例項:更簡單。
public class Ticket implements Runnable {
//共100票
int ticket = 100;
//建立Lock鎖物件
Lock ck = new ReentrantLock();
@Override
public void run() {
//模擬賣票
while(true){
//synchronized (lock){
ck.lock();
if (ticket > 0) {
//模擬選坐的操作
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "正在賣票:" + ticket--);
}
ck.unlock();
//}
}
}
}
死鎖:
同步鎖使用的弊端:當執行緒任務中出現了多個同步(多個鎖)時,如果同步中嵌套了其他的同步。這時容易引發一種現象:程式出現無限等待,這種現象我們稱為死鎖。這種情況能避免就避免掉。
synchronzied(A鎖){
synchronized(B鎖){
}
}
等待喚醒機制:
在開始講解等待喚醒機制之前,有必要搞清一個概念——執行緒之間的通訊:多個執行緒在處理同一個資源,但是處理的動作(執行緒的任務)卻不相同。通過一定的手段使各個執行緒能有效的利用資源。而這種手段即——等待喚醒機制。
比如:實現一功能,input為一執行緒,output為一執行緒,main為主執行緒,實現input對成員變數賦值,output獲取成員變數值。但有一個問題是當input給一個成員變數賦值後,output開始獲取成員變數值,此時還沒有獲取完全,緊接著input給下一個成員變數賦值,這就導致了output獲取的值可能是input第二次給成員變數賦的值,要想解決此辦法,必須讓input和output這兩個執行緒實現通訊,保證一邊input賦值後,等待output獲取值,當其獲取值後,給input一個訊號,使其再賦值。才能保證不會出現亂值。
等待喚醒機制所涉及到的方法:
wait() :等待,將正在執行的執行緒釋放其執行資格 和 執行權,並存儲到執行緒池中。
notify():喚醒,喚醒執行緒池中被wait()的執行緒,一次喚醒一個,而且是任意的。
notifyAll(): 喚醒全部:可以將執行緒池中的所有wait() 執行緒都喚醒。
1.當input發現Resource中沒有資料時,開始輸入,輸入完成後,叫output來輸出。如果發現有資料,就wait();
2.當output發現Resource中沒有資料時,就wait() ;當發現有資料時,就輸出,然後,叫醒input來輸入資料。public class Resource {
private String name;
private String sex;
private boolean flag = false;
public synchronized void set(String name, String sex) {
if (flag)
try {
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
// 設定成員變數
this.name = name;
this.sex = sex;
// 設定之後,Resource中有值,將標記該為 true ,
flag = true;
// 喚醒output
this.notify();
}
public synchronized void out() {
if (!flag)
try {
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
// 輸出執行緒將資料輸出
System.out.println("姓名: " + name + ",性別: " + sex);
// 改變標記,以便輸入執行緒輸入資料
flag = false;
// 喚醒input,進行資料輸入
this.notify();
}
}
public class Input implements Runnable {
private Resource r;
public Input(Resource r) {
this.r = r;
}
@Override
public void run() {
int count = 0;
while (true) {
if (count == 0) {
r.set("小明", "男生");
} else {
r.set("小花", "女生");
}
// 在兩個資料之間進行切換
count = (count + 1) % 2;
}
}
}
public class Output implements Runnable {
private Resource r;
public Output(Resource r) {
this.r = r;
}
@Override
public void run() {
while (true) {
r.out();
}
}
}
public class ResourceDemo {
public static void main(String[] args) {
// 資源物件
Resource r = new Resource();
// 任務物件
Input in = new Input(r);
Output out = new Output(r);
// 執行緒物件
Thread t1 = new Thread(in);
Thread t2 = new Thread(out);
// 開啟執行緒
t1.start();
t2.start();
}
}
執行緒池
執行緒池概念:
執行緒池,其實就是一個容納多個執行緒的容器,其中的執行緒可以反覆的使用,省去了頻繁建立執行緒物件的操作,無需反覆建立執行緒而消耗過多資源。
我們詳細的解釋一下為什麼要使用執行緒池?
在java中,如果每個請求到達就建立一個新執行緒,開銷是相當大的。在實際使用中,建立和銷燬執行緒花費的時間和消耗的系統資源都相當大,甚至可能要比在處理實際的使用者請求的時間和資源要多的多。除了建立和銷燬執行緒的開銷之外,活動的執行緒也需要消耗系統資源。如果在一個jvm裡建立太多的執行緒,可能會使系統由於過度消耗記憶體或“切換過度”而導致系統資源不足。為了防止資源不足,需要採取一些辦法來限制任何給定時刻處理的請求數目,儘可能減少建立和銷燬執行緒的次數,特別是一些資源耗費比較大的執行緒的建立和銷燬,儘量利用已有物件來進行服務。
執行緒池主要用來解決執行緒生命週期開銷問題和資源不足問題。通過對多個任務重複使用執行緒,執行緒建立的開銷就被分攤到了多個任務上了,而且由於在請求到達時執行緒已經存在,所以消除了執行緒建立所帶來的延遲。這樣,就可以立即為請求服務,使用應用程式響應更快。另外,通過適當的調整執行緒中的執行緒數目可以防止出現資源不足的情況。
使用執行緒池方式:
1. Runnable介面
2. Callable介面
方式一:
例項:
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
/*
* JDK1.5新特性,實現執行緒池程式步驟:
* 1.使用工廠類 Executors中的靜態方法建立執行緒物件,指定執行緒的個數
* static ExecutorService newFixedThreadPool(int 個數) 返回執行緒池物件
* 返回的是ExecutorService介面的實現類 (執行緒池物件)
* 2.建立Runnable類的物件,作為submit方法中的引數
* 介面實現類物件,呼叫方法submit (Ruunable r) 提交執行緒執行任務
*
*/
public class ThreadPoolDemo {
public static void main(String[] args) {
//呼叫工廠類的靜態方法,建立執行緒池物件
//返回執行緒池物件,是返回的介面
ExecutorService es = Executors.newFixedThreadPool(2);
//呼叫介面實現類物件es中的方法submit提交執行緒任務
//將Runnable介面實現類物件,傳遞
es.submit(new ThreadPoolRunnable());//採用匿名物件
es.submit(new ThreadPoolRunnable());
es.submit(new ThreadPoolRunnable());
}
}
public class ThreadPoolRunnable implements Runnable {
public void run(){
System.out.println(Thread.currentThread().getName()+" 執行緒提交任務");
}
}
方式二:
例項:
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
/*
* 實現執行緒程式的第三個方式,實現Callable介面方式
* 實現步驟
* 1.工廠類 Executors靜態方法newFixedThreadPool方法,建立執行緒池物件
* 執行緒池物件ExecutorService介面實現類,呼叫方法submit提交執行緒任務
* 2.建立Callable介面實現類的物件作為submit的引數 submit(Callable c)
*/
public class ThreadPoolDemo1 {
public static void main(String[] args)throws Exception {
ExecutorService es = Executors.newFixedThreadPool(2);
//提交執行緒任務的方法submit方法返回 Future介面的實現類
Future<String> f = es.submit(new ThreadPoolCallable());
String s = f.get();
System.out.println(s);
}
}
import java.util.concurrent.Callable;
public class ThreadPoolCallable implements Callable<String>{
public String call(){
return "abc";
}
}
採用此方法的優點在於:
Callable介面返回結果並且可能丟擲異常的任務,該介面類中的方法call(),可以返回任意型別資料,會丟擲異常,但是Runnable介面類中存放任務的方法run(),返回型別為void型別。建議使用第二種方式。