路是自己選的
- 多線程
- 概念
- 優缺點
- 多線程創建
- 線程的五種狀態
- 阻塞狀態
- 線程基本信息
- 線程同步
- 線程死鎖
- 生產者和消費者模式
一、概念
1、程序
Java源程序和字節碼文件被稱爲"程序",是一個靜態的概念。
2、進程
執行中的程序叫做進程(Process),是一個動態的概念。
-
進程是程序的一次動態執行過程, 佔用特定的地址空間.
-
每個進程由3部分組成:cpu,data,code。每個進程都是獨立的,保有自己的cpu時間,代碼和數據,即便用同一份程序產生好幾個進程,它們之間還是擁有自己的這3樣東西。
-
多任務(Multitasking)操作系統將CPU時間動態地劃分給每個進程,操作系統同時執行多個進程,每個進程獨立運行。以進程的觀點來看,它會以爲自己獨佔Cpu的使用權
進程的查看
- Windows系統: Ctrl+Alt+Del
- Unix系統: ps or top
3、線程
- 線程是進程中一個“單一的連續控制流程” (a single sequential flflow of control)執行路徑。線程也可以達到同一份程序產生好幾個進程的效果,但是不同的線程之間可以有某種程度上的資源共享,所以線程又被稱爲輕量級進程(lightweight process)。
- Threads run at the same time, independently of one another
- 一個進程可以擁有多個並行的線程
- 一個進程中的線程共享相同的內存單元/內存地址空間可以訪問相同的變量和對象,而且它們從同一堆中分配對象通信、數據交換、同步操作
- 由於線程間的通信是在同一地址空間上進行的,所以不需要額外的通信機制,這就使得通信更簡便而且信息傳遞的速度也更快。
程序是指令的集合,代碼的集合;而進程是動態的概念,當程序在執行時,系統分配進程;多線程是在同一進程下,充分利用資源 ,多條執行路徑,共享資源 (cpu data code)。
注意:很多線程是模擬出來的,真正的多線程是指有多個 cpu,即多核,如服務器。如 果是模擬出來的多線程,即一個 cpu 的情況下,在同一個時間點,cpu 只能執行一個代碼, 因爲切換的很快,所以就有同時執行的錯覺。但是現在的電腦一般都是多核的。
二、優缺點
1、優點
資源利用率更好;程序設計在某些情況下更簡單;程序響應更快
2、缺點
設計複雜
三、多線程的創建和啓動
1、Thread子類
1.1、創建
public class MyThread extends Thread{
@Override // 重寫父類方法
public void run() {
System.out.println("MyThread...");
}
}
1.2、啓動
public class Test1 {
public static void main(String[] args) {
Thread thread = new MyThread();
thread.start(); // 開啓線程
}
}
2、Runnable接口
2.1、創建
public class MyThread implements Runnable{
@Override // 重寫父類方法
public void run() {
System.out.println("Runnable...");
}
}
2.2、啓動
public class Test1 {
public static void main(String[] args) {
Thread thread = new Thread(new MyThread());
thread.start();
}
}
3、匿名內部類
public class Test1 {
public static void main(String[] args) {
new Thread(new Runnable() {
@Override
public void run() {
System.out.println("線程");
}
}).start();
}
}
4、Lambda表達式
public class Test1 {
public static void main(String[] args) {
new Thread(()->System.out.println("線程")) .start();
}
}
四、線程的五種狀態
- 新生狀態
- 就緒狀態
- 運行狀態
- 阻塞狀態
- 死亡狀態
五、阻塞狀態
1、sleep
當一個線程執行代碼的時候調用了sleep方法後,線程處於睡眠狀態,需要設置一個睡眠時間,此時有其他線程需要執行時就會造成線程阻塞,而且sleep方法被調用之後,線程不會釋放鎖對象,也就是說鎖還在該線程手裏,CPU執行權還在自己手裏,等睡眠時間一過,該線程就會進入就緒狀態,典型的“佔着茅坑不拉屎”;
public class Test2 {
public static void main(String[] args) {
new Thread(new Runnable() {
@Override
public void run() {
for(int i = 0;i<10;i++) {
if(i%2==0) {
try {
Thread.sleep(500); // 阻塞500毫秒,500毫秒走完就進入就緒狀態
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
System.out.println("A:"+i);
}
}
}).start();
}
}
2、yield
當一個線程正在運行時,調用了yield方法之後,該線程會將執行權禮讓給同等級的線程或者比它高一級的線程優先執行,此時該線程有可能只執行了一部分而此時把執行權禮讓給了其他線程,這個時候線程會進入就緒狀態,等待系統調度,這就很”中國化的線程“了,比較講究謙讓;
public class Test2 {
public static void main(String[] args) {
new Thread(new Runnable() {
@Override
public void run() {
for(int i = 0;i<100;i++) {
if(i%2==0) {
Thread.yield(); // 禮讓,讓出cpu的使用權,直接進入就緒狀態,等待系統的調度
}
System.out.println("A:"+i);
}
}
}).start();
new Thread(new Runnable() {
@Override
public void run() {
for(int i = 0;i<100;i++) {
System.out.println("B:"+i);
}
}
}).start();
}
}
3、join
當一個線程正在運行時,調用了一個join方法,此時在哪個線程裏調用join方法,哪個線程就會進入阻塞狀態,等待調用join方法的線程執行完畢,纔會再次執行自己。就好比插隊,本來該執行自己的,但是別的找關係,插了個隊,只能把自己的阻塞,等待插隊的走完,才能再次走自己的。
public class Test2 {
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
for(int i = 0;i<10;i++) {
System.out.println("AAAA:"+i);
}
}
});
t1.start();
for(int i = 0;i<10;i++) {
if(i==2) { // 在i==2的時候,t1插個隊,阻塞main線程,執行t1
t1.join();
}
System.out.println("Main:"+i);
}
}
}
六、線程基本信息
public class Test3 {
public static void main(String[] args) {
System.out.println(Thread.currentThread()); // 獲取當前線程
System.out.println(Thread.currentThread().isAlive()); // 線程是否還或者(未死亡)
System.out.println(Thread.currentThread().getPriority()); // 獲取線程優先級,默認線程優先級爲5
Thread.currentThread().setPriority(10); // 設置線程優先級[0,10]
System.out.println(Thread.currentThread().getName()); // 獲取線程名字
Thread.currentThread().setName("Ahh"); // 設置線程名字
}
}
七、線程同步
由於同一進程的多個線程共享同一片存儲空間,在帶來方便的同時,也帶來了訪問衝突這個嚴重的問題。Java語言提供了專門機制以解決這種衝突,有效避免了同一個數據對象被多個線程同時訪問。
public class Ticket {
public int count;
public Ticket(int count) {
this.count = count;
}
}
public class TicketConsumer implements Runnable{
private Ticket ticket;
public TicketConsumer(int count) {
super();
this.ticket = new Ticket(count);
}
@Override
public void run() {
while(true) {
try {
Thread.sleep(100); // 模擬搶票延遲
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
if((ticket.count)>0) {
System.out.println(Thread.currentThread().getName()+"搶到第"+ticket.count--+"張票");
}else {
break;
}
}
}
}
public class Test4 {
public static void main(String[] args) {
TicketConsumer ticket = new TicketConsumer(10); // 共10張票
Thread t1 = new Thread(ticket,"張三");
Thread t2 = new Thread(ticket,"李四");
t1.start();
t2.start();
}
}
運行結果 |
---|
張三搶到第10張票 李四搶到第9張票 張三搶到第8張票 李四搶到第7張票 張三搶到第6張票 李四搶到第5張票 張三搶到第4張票 李四搶到第4張票 李四搶到第3張票 張三搶到第2張票 李四搶到第1張票 張三搶到第1張票 |
可以看到結果不對,共10張票,但是出現了2次重複車票,這就是因爲不同步問題,就上面例子來說,李四搶到了第4張車票,但是可能就在出票的的時候,系統分配給李四的時間片用完了,這時李四就會進入阻塞狀態,這時候張三也來搶票,搶到了第四張,張三搶完了,系統分配給張三的時間片也用完了,張三進入阻塞狀態,然後解除李四的阻塞繼續執行,但是李四並不知道,第四張票已經被張三搶了,仍舊會搶第四張車票,這就是爲什麼重複搶到了車票。
Java中有一個關鍵字可以解決這種問題,那就是synchronized
1、synchronized同步
public class TicketConsumer implements Runnable{
private Ticket ticket;
public TicketConsumer(int count) {
super();
this.ticket = new Ticket(count);
}
@Override
public void run() {
while(true) {
try {
Thread.sleep(100); // 模擬搶票延遲
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
synchronized (ticket) {
if((ticket.count)>0) {
System.out.println(Thread.currentThread().getName()+"搶到第"+ticket.count--+"張票");
}else {
break;
}
}
}
}
}
運行結果 |
---|
張三搶到第10張票 李四搶到第9張票 張三搶到第8張票 李四搶到第7張票 張三搶到第6張票 李四搶到第5張票 李四搶到第4張票 張三搶到第3張票 張三搶到第2張票 李四搶到第1張票 |
八、線程死鎖
死鎖是指兩個或兩個以上的線程在執行過程中,由於競爭資源或者由於彼此通信而造成的一種阻塞的現象,若無外力作用,它們都將無法推進下去。此時稱系統處於死鎖狀態或系統產生了死鎖,這些永遠在互相等待的進程稱爲死鎖進程。
public class Test5 {
static String a = "A";
static String b = "B";
public static void main(String[] args) {
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
synchronized (a) {
System.out.println(Thread.currentThread().getName()+"拿到了a,準備拿b");
try {
Thread.sleep(100);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
synchronized (b) {
System.out.println();
}
}
}
});
Thread t2 = new Thread(new Runnable() {
@Override
public void run() {
synchronized (b) {
System.out.println(Thread.currentThread().getName()+"拿到了b,準備拿a");
try {
Thread.sleep(100);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
synchronized (a) {
System.out.println();
}
}
}
});
t1.start();
t2.start();
}
}
運行結果 |
---|
Thread-0拿到了a,準備拿b Thread-1拿到了b,準備拿a |
Tips:上述程序一直沒有結束
上述就是一個死鎖,t1
想先拿a,然後拿b
,t2
想先拿b
然後拿a
t1
和t2
兩個線程,如果t1
先執行,t1
拿到了a
,然後t1
就會sleep(100)
毫秒進入阻塞狀態,t2
就會執行,t2
拿到了b
,然後t2
進入阻塞狀態,t1
解除阻塞狀態繼續拿b
,但是因爲b
已經被t2
拿了,t1
就會等待t2
釋放b
,然後才能繼續執行,但是同樣t2
也在等待t1
釋放a
,兩者就會僵持下去,這就形成了死鎖
解決死鎖:
- 死鎖往往是程序邏輯問題,修改程序邏輯
- 儘量不要同時持有兩個對象鎖
九、生產者和消費者模式
在常見的多線程問題解決中,同步問題的典型示例是“生產者-消費者”模型,也就是生產者線程只負責生產,消費者線程只負責消費,在消費者發現無內容可消費時則睡覺
public class Factory {
List<Integer> list = new ArrayList<Integer>();
public synchronized void a() {
if(list.size()!=10) { // 不滿足則一直生產
list.add(1);
try {
Thread.sleep(300);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
System.out.println("生產:"+list.size());
}else { // 滿足執行
this.notify(); // 叫醒另外一個線程幹活
try {
this.wait(); // 睡覺
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
public synchronized void b() {
if(list.size()!=0) { // 一直消費
list.remove(list.size()-1);
try {
Thread.sleep(300);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
System.out.println("消費:"+list.size());;
}else { // 消費完畢
this.notify(); // 叫醒另外一個線程幹活
try {
this.wait(); // 睡覺
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
}
public class Test6 {
static List<Integer> list = new ArrayList<Integer>();
public static void main(String[] args) {
Factory f = new Factory();
// 生產者
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
while(true) {
f.a();
}
}
});
// 消費者
Thread t2 = new Thread(new Runnable() {
@Override
public void run() {
while(true) {
f.b();
}
}
});
t1.start();
t2.start();
}
}