多線程入門
先大致瞭解下進程與線程
程序運行起來叫進程
進程包含若干線程(默認含有主線程、gc線程)
一、創建線程的三個方法:
- 繼承Thread類
package threadTest;
/**
* 創建線程方式一
* 繼承Thread類
* 重寫run()方法
* 調用start()方法啓動線程
*/
public class ThreadTest extends Thread {
public static void main(String[] args) {
Thread thread = new ThreadTest();
thread.start();
for (int i=0;i<1000;i++){
System.out.println("main線程運行中"+i);
}
}
@Override
public void run() {
for (int i=0;i<100;i++){
System.out.println("Thread子線程運行中"+i);
}
}
}
- 實現Runnable接口
package threadTest;
/**
* 創建線程方式二
* 實現Runnable接口
* 實現run()方法
* 創建Thread類傳入Runnable實現類對象
* 調用Thread的start()方法
*/
public class RunnableTest implements Runnable {
@Override
public void run() {
for (int i = 0; i < 100; i++) {
System.out.println("Runnable創建子線程運行中"+i);
}
}
public static void main(String[] args) {
Thread thread = new Thread(new RunnableTest());
thread.start();
for (int i = 0; i < 1000; i++) {
System.out.println("main線程運行中"+i);
}
}
}
- 實現Callable接口(需要返回值類型,該方法目前僅做了解)
package threadTest;
import java.util.concurrent.*;
/**
* 創建線程方式三
* 實現Callable接口(擁有返回值)
* 實現call方法,需要拋出異常
* 創建Callable實現類對象
* 創建執行服務:ExecutorService ser = Executors.newFixedThreadPool()
* 提交執行線程:Future result = ser.submit(new CallableTest)
* 獲取結果:boolean res = result.get();
* 關閉服務:ser.shutdownNow();
*/
public class CallableTest implements Callable<Boolean> {
@Override
public Boolean call() throws Exception {
for (int i = 0; i < 100; i++) {
System.out.println("Callable創建子線程運行中"+i);
}
return true;
}
public static void main(String[] args) throws ExecutionException, InterruptedException {
CallableTest callableTest = new CallableTest();
ExecutorService ser = Executors.newFixedThreadPool(1);
Future<Boolean> result = ser.submit(callableTest);
for (int i = 0; i < 1000; i++) {
System.out.println("main線程運行中"+i);
}
boolean res = result.get();
ser.shutdownNow();
}
}
Thread方式與Runnable方式比較:
其一:
- Runnable方式可以將程序代碼與數據進行有效的分離
- Thread方式則代碼與數據具有較高的耦合性
其二:
- Runnable方式可以避免由於Java單繼承所帶來的侷限性
- Thread只能夠創建已繼承的打單個Thread方式
二、Lambda表達式:
函數式接口:任何接口如果只包含唯一一個抽象方法,那麼它就是一個函數式接口
package threadTest;
/**
* Lambda表達式
* 前提條件需要一個函數式接口
* 優點:避免內部類定義過多,簡化代碼
* 僅留下核心邏輯
*/
public class LambdaTest {
public static void main(String[] args) {
LambdaInterface lambdaInterface;
lambdaInterface =()->{
System.out.println("Lambda表達式重寫使用測試");
};
lambdaInterface.run();
}
}
interface LambdaInterface{
void run();
}
class LambdaImpl implements LambdaInterface{
@Override
public void run() {
System.out.println("Lambda表達式使用測試");
}
}
三、線程狀態:
線程的生命週期和狀態轉換:
線程生命週期共有五個階段:
- 新建狀態:新創建的對象所處狀態,此時不能運行,但是JVM爲其分配了內存,就和普通java對象一樣
- 就緒狀態:線程對象調用start()方法後所處狀態,此時可運行進入可運行池中,等待CPU調度
- 運行狀態:此時線程獲取CPU使用權,開始執行run()方法
- 阻塞狀態:在某些特殊情況下會放棄CPU使用權,進入阻塞狀態
- 舉例:
- 當線程試圖獲取某個對象的同步鎖時,如果鎖被其他線程持有
- 當線程調用阻塞式的IO方法時
- 調用了某個對象的wait()方法
- 調用了Thread的sleep()方法
- 調用了另一個線程的join()方法
- tip:線程只能從阻塞狀態到就緒狀態,不能直接進入運行狀態
- 死亡狀態:線程run()方法執行完畢或拋出未捕獲的Exception、錯誤Error時線程就會進入死亡狀態。此時線程不可運行,也不可轉換到其他狀態
四、線程的調度:
在計算機中,線程調度有兩種模型:分時調度模型和搶佔式調度模型
分時調度:
線程輪流獲取CPU使用權,平均分配每個線程佔用的時間片
搶佔式調度:
讓可運行池中優先級較高的線程優先佔用CPU,若優先級相同則隨機選擇。JVM默認採用搶佔式調度
4.1 線程休眠
靜態方法sleep(long millis):
- 該方法可以讓當前正在執行的線程暫停,進入休眠等待狀態
- 該方法拋出InterruptedException異常,使用時需要拋出或捕獲
package ThreadMethodTest;
import threadTest.RunnableTest;
public class SleepTest implements Runnable{
@Override
public void run() {
try {
for (int i = 0; i < 100; i++) {
if(i==50){
Thread.sleep(1000);
}
System.out.println("Runnable創建子線程運行中,50會休眠"+i);
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public static void main(String[] args) {
Thread thread = new Thread(new SleepTest());
thread.start();
for (int i = 0; i < 1000; i++) {
System.out.println("main線程正運行中"+i);
}
}
}
4.2 線程讓步:
靜態方法yield():
將當前正在執行的前程暫停,轉換爲就緒狀態,讓CPU重新調度一次
package ThreadMethodTest;
public class YieldTest implements Runnable {
@Override
public void run() {
for (int i = 0; i < 100; i++) {
System.out.println("Runnable創建的子線程在運行"+i);
if(i==50){
Thread.yield();
}
}
}
public static void main(String[] args) {
Thread thread = new Thread(new YieldTest());
thread.start();
for (int i = 0; i < 1000; i++) {
System.out.println("main線程正在運行"+i);
}
}
}
4.3 線程插隊:
join():
- 在某個線程中調用其他線程的join()方法,調用的線程將被阻塞,直到join()方法加入的線程執行完它纔會執行
- 需要拋出或處理異常InterruptedException
package ThreadMethodTest;
public class JoinTest implements Runnable {
@Override
public void run() {
for (int i = 0; i < 100; i++) {
//Thread.currentThread().getName()獲取當前執行線程的名字
System.out.println(Thread.currentThread().getName()+"正在執行中"+i);
if(i==50){
}
}
}
public static void main(String[] args) throws InterruptedException {
Thread thread1 = new Thread(new JoinTest());
thread1.start();
for (int i = 0; i < 100; i++) {
if(i==50){
thread1.join();
}
System.out.println("main線程執行"+i);
}
}
}
五、多線程同步:
多線程可以提高程序的效率,但是也會引發一些安全問題。例如:售票時,如果多個線程同時取同一張票,就可能導致錯誤。
引發錯誤的代碼:
package synchronizedTest;
public class ErrorTest implements Runnable{
private int tickets = 10;
@Override
public void run() {
try {
while (tickets>0){
Thread.sleep(100);
System.out.println(Thread.currentThread().getName()+"當前售出第"+(tickets--)+"張票");
}
}catch (Exception e){
e.printStackTrace();
}
}
public static void main(String[] args) {
ErrorTest errorTest = new ErrorTest();
Thread thread1 = new Thread(errorTest,"售票員1");
Thread thread2 = new Thread(errorTest,"售票員2");
Thread thread3 = new Thread(errorTest,"售票員3");
thread1.start();
thread2.start();
thread3.start();
}
}
可以看出一張票可能被售出多次,甚至可能會出現票爲負數的情況。這是因爲當線程A正在取票,但是票的數量還未減1時,線程B也要取票,這樣就導致取出了重複的票,顯然這是不正確的,所以我們需要進行同步,來避免問題的出現
5.1 同步代碼塊:
爲保證共享資源在任何時刻都只能有一個線程訪問,Java提供了同步機制。當多個線程使用同一個共享資源時,可以將處理共享資源的代碼放置在一個代碼塊中,使用synchronized關鍵字修飾,稱爲同步代碼塊
synchronized(lock){
//操縱共享資源的代碼塊
}
lock是一個鎖對象,它是同步代碼塊的關鍵。默認情況下lock爲1,表示可以訪問。如果當前有線程正在訪問共享資源,則lock爲0。不允許新的線程訪問共享資源,使新線程進入阻塞狀態。只有正在訪問的線程離開後lock會重新置爲1,允許訪問。
對上面的錯誤代碼進行修改:
package synchronizedTest;
public class ErrorTest_yes implements Runnable{
private int tickets = 10;
private Object lock = new Object();
@Override
public void run() {
synchronized (lock){
try {
while (tickets>0) {
Thread.sleep(100);
System.out.println(Thread.currentThread().getName()
+ "當前售出第" + (tickets--) + "張票");
}
}catch (Exception e){
e.printStackTrace();
}
}
}
public static void main(String[] args) {
ErrorTest_yes yes = new ErrorTest_yes();
Thread thread1 = new Thread(yes,"售票員A");
Thread thread2 = new Thread(yes,"售票員B");
Thread thread3 = new Thread(yes,"售票員C");
thread1.start();
thread2.start();
thread3.start();
}
}
tip:同步代碼塊中lock對象可以是任意類型的對象,但是多個線程共享的對象必須是唯一的。lock對象的創建不能放在run方法中,這樣的話每一個線程都會擁有一個自己的鎖,無法起到同步作用。
5.2 同步方法:
被synchronized修飾的方法就是同步方法,可以實現與同步代碼塊相同的功能
//synchronized 返回值 方法名([參數1,...]){}
使用同步方法修改上面的錯誤代碼:
package synchronizedTest;
public class ErrorTest_yes implements Runnable{
private int tickets = 10;
private Object lock = new Object();
@Override
public void run() {
saleTicket();
}
private synchronized void saleTicket(){
try {
while (tickets>0) {
Thread.sleep(100);
System.out.println(Thread.currentThread().getName()
+ "當前售出第" + (tickets--) + "張票");
}
}catch (Exception e){
e.printStackTrace();
}
}
public static void main(String[] args) {
ErrorTest_yes yes = new ErrorTest_yes();
Thread thread1 = new Thread(yes,"售票員A");
Thread thread2 = new Thread(yes,"售票員B");
Thread thread3 = new Thread(yes,"售票員C");
thread1.start();
thread2.start();
thread3.start();
}
}
- 通過上面的代碼大家可以看出也實現了同步。但是同步方法沒有傳入lock對象啊,他是怎麼進行同步的呢?
答:其實同步方法的鎖就是this對象。例如上代碼,因爲同步方法是被線程共享的,所以所有的線程都使用同一個yes對象,自然也就可以使用this來保證同步效果
- 那麼問題又來了?如果我們**用靜態同步方法呢?**這時候是沒有this的,他又是如何同步的呢?
答:靜態同步方法的鎖是靜態方法所在的類的Class對象。因爲在Java類加載機制中,類只被創建一次。所以也就可以被用來作爲鎖對象了
5.3 死鎖問題:
有這樣一個場景:一箇中國人和一個美國人在一起喫飯,美國人拿了中國人的筷子,
中國人拿了美國人的刀叉,兩個人開始爭執不休: .
中國人:“你先給我筷子,我再給你刀叉!”
美國人:“你先給我刀叉,我再給你筷子!”
…
結果可想而知:兩個人都喫不到飯。類似的問題還有哲學家就餐問題。有興趣大家可以自行了解。此處不做贅述
代碼模擬死鎖問題:
package synchronizedTest;
public class DeadLock implements Runnable {
private static Object chopsticks = new Object(); //筷子的鎖
private static Object knifeAndFork = new Object(); //刀叉的鎖
private boolean flag; //flag帶表是美國人還是中國人
public DeadLock(boolean flag){
this.flag = flag;
}
@Override
public void run() {
if(flag){ //當前說老美
//筷子鎖對象上的同步代碼塊
synchronized (chopsticks){
System.out.println("把叉子給我,我就把筷子給你");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (knifeAndFork){ //開始伸手要刀叉
System.out.println("雙標老美拿到刀叉");
}
}
}else{
//刀叉對象的同步代碼塊
synchronized (knifeAndFork) {
System.out.println("把筷子給我,我就把叉子給你");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (chopsticks) {
System.out.println("偉大的中國人民拿到筷子");
}
}
}
}
public static void main(String[] args) {
DeadLock American = new DeadLock(true);
DeadLock Chinese = new DeadLock(false);
//創建並開啓兩個線程
new Thread(American,"雙標美").start();
new Thread(Chinese,"博愛中").start();
}
}
由上面代碼可以看出雙方互不鬆手,程序陷入死鎖。所以在編程中我們需要避免死鎖問題的發生
5.6 多線程通信
經典例子:生產者和消費者問題。
假設有一個場景:有一個倉庫,生產者往裏面放貨物,消費者從裏面取貨物。如果倉庫滿了,如何讓生產者停下後通知消費者取貨。如果倉庫空了,如何停止取貨,讓消費者通知生產者生產?
代碼模擬一下:
package communicationTest;
/**
* 定義一個倉庫類
*/
public class Storage {
//數據存儲數組
private int[] cells = new int[10];
//inPos表示存入時數組下標,outPos表示取出時數組下標
private int inPos;
private int outPos;
public void put(int num){
cells[inPos] = num;
System.out.println("在cells["+inPos+"]中放入數據--"+cells[inPos]);
inPos++;
//每當數據已經放滿就從0位置重新開始放數據
if(inPos == cells.length){
inPos=0; //當inPos爲數組長度時,將其置爲0
}
}
//定義一個get方法從數組中取出數據
public void get(){
int data = cells[outPos];
System.out.println("在cells["+outPos+"]中取出數據--"+cells[outPos]);
outPos++; //取完讓元素位置++
//每當數據已經取完就從0位置重新開始取數據
if(outPos==cells.length){
outPos=0;
}
}
}
生產者和消費者類:
package communicationTest;
/**
* 生產者和消費者類
* 生產者不斷生產
* 消費者不斷消費
*/
class Input implements Runnable {
private Storage st;
private int flag=100;
private int num;
Input(Storage st){
this.st = st;
}
@Override
public void run() {
while ((flag--)>0){
st.put(num++);
}
}
}
class Output implements Runnable {
private Storage st;
private int flag=100;
Output(Storage st){
this.st = st;
}
@Override
public void run() {
while ((flag--)>0){
st.get();
}
}
}
public class InputAndOutput{
public static void main(String[] args) {
//創建一個倉庫對象
Storage st = new Storage();
Input input = new Input(st);
Output output = new Output(st);
new Thread(input).start();
new Thread(output).start();
}
}
根據運行結果能夠發現,已經被放過數據還未被取出的位置又被重複放上數據,這是錯誤的。
那麼如何解決問題呢?
此時就需要讓線程之間彼此通信。Object類中提供了wait()、notify()、notifyAll()方法用於解決線程間的通信問題
- wait():使當前線程放棄同步鎖並進入等待,直到其他線程進入此同步鎖,並調用notify()方法,或notifyAll()方法喚醒該線程爲止
- notify():喚醒此同步鎖上等待的第一個調用wait()方法的線程
- notifyAll():喚醒此同步鎖上調用wait()方法的所有線程
注意:以上三個方法的調用者都應該是同步鎖對象,如果不是則會拋出異常
對上面Storage代碼的修改
package communicationTest;
/**
* 定義一個倉庫類
*/
public class Storage {
//數據存儲數組
private int[] cells = new int[10];
//inPos表示存入時數組下標,outPos表示取出時數組下標
private int inPos;
private int outPos;
private int count; //存入或取出數據的數量
public synchronized void put(int num) {
if(count==cells.length){
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
cells[inPos] = num;
System.out.println("在cells["+inPos+"]中放入數據--"+cells[inPos]);
inPos++;
count++; //放入一個元素count++
//如果已經放到最後一個位置,則從頭開始放。(模擬循環隊列)
if (inPos==cells.length){
inPos=0;
}
this.notify();
}
//定義一個get方法從數組中取出數據
public synchronized void get() {
if(count==0){ //如果已經全部取出
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
int data = cells[outPos];
System.out.println("在cells["+outPos+"]中取出數據--"+data);
cells[outPos]=-1; //代表此處無元素
outPos++; //取完讓元素位置++
count--;
if(outPos==cells.length){
outPos=0;
}
this.notify(); //表示倉庫已經可以放貨物,通知生產者生產
}
}
此時便不會出現重複放入元素或重複取出元素的情況