# 多線程詳解
## 核心概念
- 線程就是獨立的執行路徑
- 在程序運行時,即使沒有自己創建線程,后臺也會有多個線程,如主線程, gc線程;
- main()稱之為主線程,為系統的入口,用于執行整個程序;
- 在一個進程中,如果開辟了多個線程,線程的運行由調度器安排調度,調度器是與操作系統緊密相關的,先后順序是不能人為的干預的。
- 對同一份資源操作時,會存在資源搶奪的問題,需要加入并發控制;
- 線程會帶來額外的開銷,如cpu調度時間,并發控制開銷。
- 每個線程在自己的工作內存交互,內存控制不當會造成數據不一致
## Thread類
**線程開啟不一定立即執行,由CPU調度執行**
創建一個新的執行線程有兩種方法。
1. 一個是將一個類聲明為`Thread`的子類。 這個子類應該重寫`run`類的方法`Thread` 。然后可以分配并啟動子類的實例。
2. 另一種方法來創建一個線程是聲明實現類`Runnable`接口。 那個類然后實現了`run`方法。 然后可以分配類的實例,在創建`Thread`時作為參數傳遞,并啟動。
然后,以下代碼將創建一個線程并啟動它運行:
> ```
> PrimeRun p = new PrimeRun(143);
> new Thread(p).start();
> ```
### **方式一**
```java
//創建線程方式一:繼承Thread類,重寫run()方法,調用start開啟線程
public class One extends Thread{
@Override
public void run(){
//run方法線程體
for (int i = 0; i < 10; i++) {
System.out.println("數字---" + i);
}
}
public static void main(String[] args) {
//main線程,主線程
//創建一個線程對象
One one = new One();
//調用start()方法開啟線程
one.start();
for (int i = 0; i < 1000; i++) {
System.out.println("線程---" + i);
}
}
}
```
### **方式二**
```java
//創建線程方式二:實現runnable接口,重寫run方法,執行線程需要丟入runnable接口實現類,調用start方法。
public class Two implements Runnable{
@Override
public void run() {
//run方法線程體
for (int i = 0; i < 100; i++) {
System.out.println("數字---" + i);
}
}
public static void main(String[] args) {
//創建runnable接口的實現類對象
Two two = new Two();
//創建線程對象,通過線程對象來開啟我們的線程,代理
// Thread thread = new Thread(two);
// thread.start();
new Thread(two).start();
for (int i = 0; i < 100; i++) {
System.out.println("線程--" + i);
}
}
}
```
## 并發任務
初識并發:
```java
public class Ticket implements Runnable{
//票數
private int tickNums = 10;
@Override
public void run() {
while (true){
if (tickNums<=0){
break;
}
//模擬延遲
try {
Thread.sleep(200);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "-->拿到了第" + tickNums-- + "張票");
}
}
public static void main(String[] args) {
Ticket ticket = new Ticket();
new Thread(ticket,"阿冰").start();
new Thread(ticket,"黃牛").start();
new Thread(ticket,"小澤").start();
}
}
1.png (20.39 KB, 下載次數: 69)
下載附件
2022-3-5 01:33 上傳
在運行過程中會出現多個線程同時處理同一個資源的現象這個時候就需要并發。
**處理并發任務的安全性:隊列+鎖。**
### 龜兔賽跑例程
```java
import java.util.Random;
//模擬龜兔賽跑
public class Race implements Runnable{
private static String winner;
@Override
public void run() {
Random random = new Random();
int time = random.nextInt();
for (int i = 0; i <= 100; i++) {
//模擬兔子睡覺
if (Thread.currentThread().getName().equals("兔子") && i % time == 0) {
try {
Thread.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//判斷比賽是否結束
boolean flag = gameOver(i);
//如果比賽結束了就停止程序
if (flag){
break;
}
System.out.println(Thread.currentThread().getName() + "-->跑了" + i + "步");
}
}
//判斷是否完成比賽
private boolean gameOver(int steps) {
if (winner != null) {//已經存在勝利者
return true;
}else {
if (steps>=100){
winner = Thread.currentThread().getName();
System.out.println("winner is " + winner);
return true;
}
}
return false;
}
public static void main(String[] args) {
Race race = new Race();
new Thread(race,"兔子").start();
new Thread(race,"烏龜").start();
}
}
```
## 靜態代理模式
```java
public class StaticType {
public static void main(String[] args) {
WedingCompany wedingCompany = new WedingCompany(new You());
wedingCompany.HappyMarry();
}
}
interface Marry{
void HappyMarry();
}
//真實對象,你去結婚
class You implements Marry{
@Override
public void HappyMarry() {
System.out.println("結婚了,超級開心!");
}
}
//代理角色,幫助你結婚
class WedingCompany implements Marry{
private Marry target; //代理誰-->真實目標
public WedingCompany(Marry target){
this.target = target;
}
@Override
public void HappyMarry() {
before();
this.target.HappyMarry();
after();
}
private void before() {
System.out.println("結婚之前");
}
private void after() {
System.out.println("結婚之后");
}
}
```
靜態代理模式總結:
- 真實對象和代理對象都要實現同一個接口
- 代理對象要代理真實角色
好處:
- 代理對象可以做很多真實對象做不了的事情
- 真實對象專注做自己的事情
## Lambda表達式
```java
/*
推導Lambda表達式
*/
public class ClassLambda {
//3.靜態內部類
static class Like2 implements ILike {
@Override
public void lambda() {
System.out.println("I like lambda2!");
}
}
public static void main(String[] args) {
ILike like = new Like();
like.lambda();
like = new Like2();
like.lambda();
//4.局部內部類
class Like3 implements ILike{
@Override
public void lambda() {
System.out.println("I like lambda3!");
}
}
like = new Like3();
like.lambda();
//5.匿名內部類,沒有類的名稱,必須借助接口或者父類
like = new ILike() {
@Override
public void lambda() {
System.out.println("I like lambda4!");
}
};
like.lambda();
//6.用lambda簡化
like = ()->{
System.out.println("I like lambda5!");
};
like.lambda();
}
}
//1.定義一個函數式接口
interface ILike{
void lambda();
}
//2.實現類
class Like implements ILike {
@Override
public void lambda() {
System.out.println("I like lambda!");
}
}
```
**簡化Lambda**
```java
public class TestLambda {
public static void main(String[] args) {
//lambda表示簡化
Ilove ilove = (int a) -> {
System.out.println("I love you!" + a);
};
//簡化1.參數類型
ilove = (a) -> {
System.out.println("1.I love you!" + a);
};
//簡化2.簡化括號
ilove = a -> {
System.out.println("2.I love you!" + a);
};
//簡化3.簡化花括號
ilove = a -> System.out.println("3.I love you!" + a);
ilove.love(520);
}
}
interface Ilove{
void love(int a);
}
```
總結:
- lambda表達式只能有一行代碼的情況下才能簡化成為一行,如果有多行,那么就用代碼塊包裹。
- 前提是接口時函數式接口。
- 多個參數也可以去掉參數類型,要去掉都去掉,必須加上括號。
## Stop方法:線程停止
```java
//測試stop
//1.建議線程正常停止--->利用次數,不建議死循環
//2.建議使用標志位--->設置一個標志位
//3.不要使用stop或者destroy等過時或者JDK不建議使用的方法
public class TestStop implements Runnable {
//1.設置一個標志位
private boolean flag = true;
@Override
public void run() {
int i = 0;
while (flag){
System.out.println("run Thread..." + i++);
}
}
//2.設置一個公開的方法停止線程,轉換標志位
public void stop(){
this.flag = false;
}
public static void main(String[] args) {
TestStop testStop = new TestStop();
new Thread(testStop).start();
for (int i = 0; i < 100; i++) {
System.out.println("main" + i);
if (i == 90){
//調用stop方法切換標志位,讓線程停止
testStop.stop();
System.out.println("線程該停止了。");
}
}
}
}
```
## Sleep方法:線程休眠
```java
//計時器
public class TimeDown {
public static void main(String[] args) {
try {
Down();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public static void Down() throws InterruptedException {
int number = 10;
while (true) {
Thread.sleep(1000);
System.out.println(number--);
if (number <= 0)
break;
}
}
}
```
```java
import java.text.SimpleDateFormat;
import java.util.Date;
//打印當前系統時間
public class SystemTime {
public static void main(String[] args) {
Date startTime = new Date(System.currentTimeMillis()); //獲取當前時間
while (true){
try {
Thread.sleep(1000);
System.out.println(new SimpleDateFormat("YYYY/MM/DD\nHH:mm:ss\n").format(startTime));
startTime = new Date(System.currentTimeMillis()); //更新當前時間
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
```
每個對象都有一把鎖,sleep方法不會釋放鎖。
## Yield方法:線程禮讓
- 禮讓線程,讓當前正在執行的線程暫停,但不阻塞
- 將線程從運行態轉化為就緒態
- 讓CPU重新調度,禮讓不一定成功,看CPU心情
```java
public class TestYield {
public static void main(String[] args) {
MyYield myYield = new MyYield();
new Thread(myYield,"a").start();
new Thread(myYield,"b").start();
}
}
class MyYield implements Runnable{
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + "-->start");
Thread.yield();
System.out.println(Thread.currentThread().getName() + "-->end");
}
}
```
## Join方法:線程強制執行
- Join合并線程,待線程執行完成后,在執行其他線程,其他線程阻塞
- 不建議使用,容易導致進程阻塞
```java
public class TestJoin implements Runnable{
@Override
public void run() {
for (int i = 0; i < 10; i++) {
System.out.println("VIP插隊" + i);
}
}
public static void main(String[] args) throws InterruptedException {
//啟動我們的線程
TestJoin testJoin = new TestJoin();
Thread thread = new Thread(testJoin);
thread.start();
//主線程
for (int i = 0; i < 100; i++) {
if (i == 10){
thread.join();
}
System.out.println("main" + i);
}
}
}
```
## 線程的狀態
線程可以處于以下狀態之一:
- [`NEW`](../../java/lang/Thread.State.html#NEW)
尚未啟動的線程處于此狀態。
- [`RUNNABLE`](../../java/lang/Thread.State.html#RUNNABLE)
在Java虛擬機中執行的線程處于此狀態。
- [`BLOCKED`](../../java/lang/Thread.State.html#BLOCKED)
被阻塞等待監視器鎖定的線程處于此狀態。
- [`WAITING`](../../java/lang/Thread.State.html#WAITING)
正在等待另一個線程執行特定動作的線程處于此狀態。
- [`TIMED_WAITING`](../../java/lang/Thread.State.html#TIMED_WAITING)
正在等待另一個線程執行動作達到指定等待時間的線程處于此狀態。
- [`TERMINATED`](../../java/lang/Thread.State.html#TERMINATED)
已退出的線程處于此狀態。
**線程狀態觀測**
```java
//觀測線程的狀態
public class TestState {
public static void main(String[] args) {
Thread thread = new Thread(()->{
for (int i = 0; i < 5; i++) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("結束");
});
//觀測狀態
Thread.State state = thread.getState();
System.out.println(state); //NEW
//觀察重啟后
thread.start();
state = thread.getState();
System.out.println(state);//RUNNABLE
while (state != Thread.State.TERMINATED){//只要線程不終止,就一直輸出狀態
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
state = thread.getState(); //更新線程狀態
System.out.println(state); //輸出狀態
}
}
}
```
## 守護優先級
```java
public class TestPriority {
public static void main(String[] args) {
System.out.println(Thread.currentThread().getName() +
"-->" +
Thread.currentThread().getPriority());
MyPriority myPriority = new MyPriority();
Thread t1 = new Thread(myPriority);
Thread t2 = new Thread(myPriority);
Thread t3 = new Thread(myPriority);
Thread t4 = new Thread(myPriority);
t1.start();
t2.setPriority(2);
t2.start();
t3.setPriority(6);
t3.start();
t4.setPriority(Thread.MAX_PRIORITY);
t4.start();
}
}
class MyPriority implements Runnable{
@Override
public void run() {
System.out.println(Thread.currentThread().getName() +
"-->" +
Thread.currentThread().getPriority());
}
}
```
## synchronized鎖機制
- 由于我們可以通過 private 關鍵字來保證數據對象只能被方法訪問,所以我們只需要針對方法提出一套機制,這套機制就是synchronized關鍵字,它包括兩種用法:
- synchronized方法
- synchronized塊
```java
//同步方法:
public synchronized void method(int args){} //synchronized默認鎖的是this.
```
- synchronized方法控制對“對象”的訪問,每個對象對應一把鎖,每個synchronized方法都必須獲得調用該方法的對象的鎖才能執行,否則線程會阻塞,方法一旦執行,就獨占該鎖,直到該方法返回才釋放鎖,后面被阻塞的線程才能獲得這個鎖,繼續執行
**同步塊**
```java
synchronized (Obj){}
```
- Obj 稱之為同步監視器
- Obj 可以是任何對象(增刪改的對象),但是推薦使用共享資源作為同步監視器
- 同步方法中無需指定同步監視器,因為同步方法的同步監視器就是this ,就是
這個對象本身,或者是class [反射中講解]
- 同步監視器的執行過程
1. 第一個線程訪問,鎖定同步監視器,執行其中代碼.
2. 第二個線程訪問,發現同步監視器被鎖定, 無法訪問.
3. 第一個線程訪問完畢,解鎖同步監視器.
4. 第二個線程訪問, 發現同步監視器沒有鎖,然后鎖定并訪問
```java
public class Bank {
public static void main(String[] args) {
Account account = new Account(100, "存錢");
GetMoney You = new GetMoney(account, 50, "我");
GetMoney Wife = new GetMoney(account, 100, "老婆");
Wife.start();
You.start();
}
}
//賬戶
class Account {
int SumMoney;
String name;
public Account(int sumMoney, String name) {
this.SumMoney = sumMoney;
this.name = name;
}
}
//取錢
class GetMoney extends Thread {
Account account; //賬戶
int TakeMoney; //取走了多少錢
int nowMoney; //手里有多少錢
public GetMoney(Account account, int TakeMoney, String name){
super(name);
this.account = account;
this.TakeMoney = TakeMoney;
}
//取錢
@Override
public synchronized void run(){
//鎖的對象是賬戶
synchronized (account) {
//判斷有沒有錢
if (account.SumMoney - TakeMoney < 0) {
System.out.println(Thread.currentThread().getName() + "錢不夠,沒有取到錢");
return;
}
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
//卡內余額
account.SumMoney = account.SumMoney - TakeMoney;
//你手里的錢
nowMoney = nowMoney + TakeMoney;
System.out.println(this.getName() + "手里的錢:" + nowMoney);
System.out.println(account.name + "余額的錢還有" + account.SumMoney);
}
}
}
```
## 死鎖
- 從JDK 5.0開始, Java提供了更強大的線程同步機制一通過顯式定 義同步鎖對象來實現同步。同步鎖使用Lock對象充當
- java.til.concurrent.locks.Lock接C是控制多個線程對共享資源進行訪問的工具。鎖提供了對共享資源的獨占訪問,每次只能有-個線程對Lock對象加鎖,線程開始訪問共享資源之前應先獲得L ock對象
- ReentrantLock類實現了Lock ,它擁有與synchronized相同的并發性和內存語義,在實現線程安全的控制中,比較常用的是ReentrantLock,可以顯式加鎖、釋放鎖。
```java
import java.util.concurrent.locks.ReentrantLock;
public class TestLock {
public static void main(String[] args) {
wantTicket buy = new wantTicket();
new Thread(buy,"a").start();
new Thread(buy,"b").start();
new Thread(buy,"c").start();
}
}
class wantTicket implements Runnable{
int TicketNum = 10;
private final ReentrantLock lock = new ReentrantLock();
@Override
public void run() {
while (true){
try {//上鎖
lock.lock();
if (TicketNum > 0){
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "-->拿到了第" + TicketNum-- + "張票");
}else {
break;
}
}finally {
lock.unlock();
}
}
}
}
```
- Lock是顯式鎖(手動開啟和關閉鎖,別忘記關閉鎖) synchronized是隱式鎖, 出了作用域自動釋放
- Lock只有代碼塊鎖,synchronized有代碼塊鎖和方法鎖
- 使用Lock鎖,JVM將花費較少的時間來調度線程,性能更好。并且具有更好的擴展性(提供更多的子類)
- 優先使用順序:
- Lock >同步代碼塊(已經進入了方法體,分配了相應資源) >同步方法(在方法體之外)
### 生產者消費者問題
| 方法名 | 作用 |
| --- | --- |
| wait() | 表示線程一直等待 ,直到其他線程通知,與sleep不同,會釋放鎖 |
| wait(long timeout) | 指定等待的毫秒數 |
| notify | 喚醒一個處于等待狀態的線程 |
| notifyAll | 喚醒同一個對象上所有調用wait()方法的線程,優先級別高的線程優先調度 |
```java
//測試:生產者消費者模型-->利用緩沖區:管程法
//生產者,消費者,產品,緩沖區
public class TestPC {
public static void main(String[] args) {
SynContainer container = new SynContainer();
new Productor(container).start();
new Consumer(container).start();
}
}
//生產者
class Productor extends Thread{
SynContainer container;
public Productor(SynContainer container){
this.container = container;
}
@Override
public void run() {
for (int i = 0; i < 100; i++) {
container.push(new Chicken(i));
System.out.println("生產了" +i +"只雞");
}
}
}
//消費者
class Consumer extends Thread{
SynContainer container;
public Consumer(SynContainer container){
this.container = container;
}
@Override
public void run() {
for (int i = 0; i < 100; i++) {
System.out.println("消費了-->" + container.pop().id + "只雞");
}
}
}
//產品
class Chicken{
int id;
public Chicken(int id) {
this.id = id;
}
}
//緩沖區
class SynContainer{
//需要一個容器大小
Chicken[] chickens= new Chicken[10];
//容器計數器
int count = 0;
//生產者生產產品
public synchronized void push(Chicken chicken){
//如果容器滿了,我們就需要等待消費
if (count == chickens.length){
//通知消費者消費,生產等待
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}else {
//如果沒有滿,我們就需要丟入產品
chickens[count] = chicken;
count++;
//可以通知消費者消費了
this.notifyAll();
}
}
//消費者消費產品
public synchronized Chicken pop(){
//判斷能否消費
if (count == 0){
//等待生產者生產,消費者等待
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//如果可以消費
count--;
Chicken chicken = chickens[count];
//吃完了,通知生產者生產
this.notifyAll();
return chicken;
}
}
```
```java
//測試生產者消費者問題2-->信號燈法,標志位解決
public class TestPC2 {
public static void main(String[] args) {
TV tv = new TV();
new Player(tv).start();
new Watcher(tv).start();
}
}
//生產者-->演員
class Player extends Thread{
TV tv;
public Player(TV tv){
this.tv = tv;
}
@Override
public void run() {
for (int i = 0; i < 20; i++) {
if (i % 2 == 0){
this.tv.play("河南衛視");
}else {
this.tv.play("春節聯歡晚會");
}
}
}
}
//消費者-->觀眾
class Watcher extends Thread{
TV tv;
public Watcher(TV tv){
this.tv = tv;
}
@Override
public void run() {
for (int i = 0; i < 20; i++) {
tv.watch();
}
}
}
//產品-->節目
class TV {
//演員表演,觀眾等待 T
//觀眾觀看,演員等待 F
String voice; //表演的節目
boolean flag = true;
//表演
public synchronized void play(String voice){
if (!flag){
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("演員表演了:" + voice);
//通知觀眾觀看
this.notifyAll();//通知喚醒
this.voice = voice;
this.flag = !this.flag;
}
//觀看
public synchronized void watch(){
if (flag){
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("觀看了:" + voice);
//通知演員表演
this.notifyAll();
this.flag = !this.flag;
}
}
```
|