1、實現多線程
1.1 進程
進程:是正在運行的程序,是系統進行資源分配和調用的獨立單位
每一個進程都有它自己的內存空間和系統資源
1.2 線程
線程:是進程中的單個順序控制流,是一條執行路徑
單線程:一個進程如果只有一條執行路徑,則稱為單線程程序
多線程:一個進程如果有多條執行路徑,則稱為多線程程序
1.3 多線程的實現方式
方式1:繼承Thread類
- 定義一個類MyThread繼承Thread類
- 在MyThread類中重寫run()方法
- 創建MyThread類的對象
- 啟動線程
// 定義一個類MyThread繼承Thread類
public class MyThread extends Thread {
public MyThread() {
}
public MyThread(String name) {
super(name);
}
// 在MyThread類中重寫run()方法
@Override
public void run() {
for (int i = 0; i < 100; i++) {
System.out.println(this.getName()+":"+i);
}
}
}
public class MyThreadDemo {
public static void main(String[] args) {
// 創建MyThread類的對象
MyThread thread1 = new MyThread();
// run()並沒有真正啟動線程,run()是封裝線程執行的代碼,直接調用,相當於普通方法的調用
// thread1.run();
// 啟動線程
// void start() 導致此線程開始執行; Java虛擬機調用此線程的run方法
thread1.start();
}
}
兩個小問題:
-
為什么要重寫run()方法?
因為run()是用來封裝被線程執行的代碼
-
run()方法和start()方法的區別?
run():封裝線程執行的代碼,直接調用,相當於普通方法的調用
start():啟動線程;然后由JVM調用此線程的run()方法
方式2:實現Runnable接口
- 定義一個類MyRunnable實現Runnable接口
- 在MyRunnable類中重寫run()方法
- 創建MyRunnable類的對象
- 創建Thread類的對象,把MyRunnable對象作為構造方法的參數
- 啟動線程
// 定義一個類MyRunnable實現Runnable接口
public class MyRunnable implements Runnable {
// 在MyRunnable類中重寫run()方法
@Override
public void run() {
for (int i = 0; i < 100; i++) {
System.out.println(Thread.currentThread().getName()+":"+i);
}
}
}
public class MyRunnableDemo {
public static void main(String[] args) {
MyRunnable my = new MyRunnable();
// Thread(Runnable runnable):構造方法,傳入一個實現了Runnable接口的參數
Thread t1 = new Thread(my);
Thread t2 = new Thread(my);
// 啟動線程
t1.start();
t2.start();
}
}
實現Runnable接口的好處
- 避免了Java單繼承的局限性
- 適合多個相同程序的代碼去處理同一個資源的情況,把線程和程序的代碼、數據有效分離,較好的體現了面向對象的設計思想
1.4 設置和獲取線程名稱
Thread類中設置和獲取線程名稱的方法
- void setName(String name):將此線程的名稱更改為等於參數name
- String getName():返回此線程的名稱
- 通過構造方法也可以設置線程名稱
public class MyThread extends Thread {
public MyThread(String name){
super(name);
}
@Override
public void run() {
for (int i = 0; i < 20; i++) {
System.out.println(getName()+":"+i);
}
}
}
public class MyThreadDemo {
public static void main(String[] args) {
MyThread mt1 = new MyThread("劉備");
MyThread mt2 = new MyThread();
MyThread mt3 = new MyThread();
// 這里調用的是Thread類中的setName方法
mt2.setName("關羽");
mt3.setName("張飛");
mt2.start();
mt3.start();
}
}
如何獲取main()方法所在的線程名稱
- static Thread currentThread():返回當前正在執行的線程對象的引用
public class MyThreadDemo {
public static void main(String[] args) {
// static Thread currentThread():返回當前正在執行的線程對象的引用
System.out.println(Thread.currentThread().getName());
}
}
1.5 線程調度
線程有兩種調度模型
- 分時調度模型:所有線程輪流使用CPU的使用權,平均分配每個線程占用CPU的時間片
- 搶占式調度模型:優先讓優先級高的線程使用CPU,如果線程的優先級相同,那么會隨機選擇一個優先級高的線程獲取的CPU時間片相對多一些
Java使用的是搶占式調度模型
假如計算機只有一個CPU,那么CPU在某一個時刻只能執行一條指令,線程只有得到CPU時間片,也就是使用權,才可以執行指令。所以說多線程程序的執行是有隨機性的,因為誰搶到CPU的使用權是不一定的。
線程優先級
- Thread類中設置和獲取線程優先級的方法
- public final int getPriority():返回此線程的優先級
- public final void setPriority(int new Priority):更改此線程的優先級
- 線程默認優先級是5;線程優先級的范圍是1-10
- 線程優先級高僅僅表現線程獲取的CPU時間片的幾率高,但是要在次數比較多,或者多次運行的時候才能看到你想要的效果
public class MyThread extends Thread {
@Override
public void run() {
for (int i = 0; i < 100; i++) {
System.out.println(this.getName()+":"+i);
}
}
}
public class MyThreadDemo {
public static void main(String[] args) {
MyThread thread1 = new MyThread();
MyThread thread2 = new MyThread();
MyThread thread3 = new MyThread();
// 線程默認的優先級為5
System.out.println(thread1.getPriority()); // 5
System.out.println(thread2.getPriority()); // 5
System.out.println(thread3.getPriority()); // 5
// public final void setPriority(int new Priority):更改此線程的優先級
// IllegalArgumentException:拋出表示一種方法已經通過了非法或不正確的參數
// thread1.setPriority(10000);
// Thread中有三個靜態常量分別表示最高優先級、最低優先級和默認優先級
System.out.println(Thread.MAX_PRIORITY);// 10
System.out.println(Thread.MIN_PRIORITY);// 1
System.out.println(Thread.NORM_PRIORITY);// 5
// 設置正確的線程優先級
thread1.setPriority(10);
thread2.setPriority(1);
thread3.setPriority(3);
thread1.start();
thread2.start();
thread3.start();
}
}
1.6 線程控制
static void sleep(long millis):使當前正在執行的線程停留(暫停執行)指定的毫秒數
public class ThreadSleep extends Thread {
@Override
public void run() {
for (int i = 0; i < 100; i++) {
System.out.println(getName() + ":" + i);
try {
sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
public class ThreadSleepDemo {
public static void main(String[] args) {
ThreadSleep ts1 = new ThreadSleep();
ThreadSleep ts2 = new ThreadSleep();
ThreadSleep ts3 = new ThreadSleep();
ts1.setName("曹操");
ts2.setName("劉備");
ts3.setName("孫權");
ts1.start();
ts2.start();
ts3.start();
}
}
void join():等待這個線程死亡
public class ThreadJoin extends Thread {
@Override
public void run() {
for (int i = 0; i < 100; i++) {
System.out.println(getName()+":"+i);
}
}
}
public class ThreadJoinDemo {
public static void main(String[] args) {
ThreadJoin tj1 = new ThreadJoin();
ThreadJoin tj2 = new ThreadJoin();
ThreadJoin tj3 = new ThreadJoin();
tj1.setName("康熙");
tj2.setName("四阿哥");
tj3.setName("八阿哥");
tj1.start();
try {
tj1.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
tj2.start();
tj3.start();
}
}
void setDaemon(boolean on):將此線程標記為守護線程,當運行的線程都是守護線程時,JVM將退出
public class ThreadDaemon extends Thread {
@Override
public void run() {
for (int i = 0; i < 100; i++) {
System.out.println(getName()+":"+i);
}
}
}
public class ThreadDaemonDemo {
public static void main(String[] args) {
ThreadDaemon td1 = new ThreadDaemon();
ThreadDaemon td2 = new ThreadDaemon();
td1.setName("關羽");
td2.setName("張飛");
Thread thread = Thread.currentThread();
thread.setName("劉備");
td1.setDaemon(true);
td2.setDaemon(true);
td1.start();
td2.start();
for (int i = 0; i < 10; i++) {
System.out.println(thread.getName()+":"+i);
}
}
}
1.7 線程的生命周期
2、線程同步
2.1 買票
需求:
- 某電影院目前正在上映國產大片,共有100張票,而它有3個窗口買票,請設計一個程序模擬該電影院買票
思路:
-
定義一個SellTicket實現Runnable接口,里面定義一個成員變量private int tickets = 100;
-
在SellTicket類中重寫run()方法實現賣票,代碼步驟如下
- 判斷票數大於0,就賣票,並告知是哪個窗口賣的
- 賣了票之后,總票數要減一
- 票沒有了,也有可能有人來問,所以這里用死循環讓賣票動作一直執行
-
定義一個測試類SellTicketDemo,里面有main方法,代碼執行如下
-
創建SellTicket類的對象
-
創建三個Thread類的對象,把SellTicket對象作為構造方法的參數,並給出對應的窗口名
-
啟動線程
-
public class SellTicket implements Runnable {
private int tickets = 100;
@Override
public void run() {
while (true) {
if (tickets> 0) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "正在出售第"
+tickets+"張票");
tickets--;
}
}
}
}
public class SellTicketDemo {
public static void main(String[] args) {
SellTicket s = new SellTicket();
Thread t1 = new Thread(s,"一號窗口");
Thread t2 = new Thread(s,"二號窗口");
Thread t3 = new Thread(s,"三號窗口");
t1.start();
t2.start();
t3.start();
}
}
買票出現了問題
相同的票出現了多次
@Override
public void run() {
// 出現了相同的票
while (true) {
// tickets = 100;
// t1,t2,t3
// 假設t1線程搶到CPU的執行權
if (tickets> 0) {
// 通過sleep()方法來模擬出票時間
try {
Thread.sleep(100);
// t1線程休息100毫秒
// t2線程搶到了CPU的執行權,t2線程就開始執行,執行到這里時,t2線程休息100毫秒
// t3線程搶到了CPU的執行權,t3線程就開始執行,執行到這里時,t3線程休息100毫秒
} catch (InterruptedException e) {
e.printStackTrace();
}
// 假設線程按照順序醒過來
// t1搶到CPU的執行權,在控制台輸出:窗口1正在出售第100張票
System.out.println(Thread.currentThread().getName() + "正在出售第"
+tickets+"張票");
// t2搶到CPU的執行權,在控制台輸出:窗口2正在出售第100張票
// t3搶到CPU的執行權,在控制台輸出:窗口3正在出售第100張票
tickets--;
// 如果這三個線程還是按照順序來,這里就執行了3次--操作,最終票變為97
}
}
}
出現了負數的票
@Override
public void run() {
// 出現了負數的票
while (true) {
// tickets = 100;
// t1,t2,t3
// 假設t1線程搶到CPU的執行權
if (tickets > 0) {
// 通過sleep()方法來模擬出票時間
try {
Thread.sleep(100);
// t1線程休息100毫秒
// t2線程搶到了CPU的執行權,t2線程就開始執行,執行到這里時,t2線程休息100毫秒
// t3線程搶到了CPU的執行權,t3線程就開始執行,執行到這里時,t3線程休息100毫秒
} catch (InterruptedException e) {
e.printStackTrace();
}
// 假設線程按照順序醒過來
// t1搶到了CPU的執行權,在控制台輸出:窗口1正在出售第1張票
// 假設t1繼續擁有CPU的執行權,就會執行ticket--的操作,ticket= 0
// t2搶到了CPU的執行權,在控制台輸出:窗口2正在出售第0張票
// 假設t2繼續擁有CPU的執行權,就會執行ticket--的操作,ticket= -1
// t3搶到了CPU的執行權,在控制台輸出:窗口3正在出售第-1張票
// 假設t3繼續擁有CPU的執行權,就會執行ticket--的操作,ticket= -2
System.out.println(Thread.currentThread().getName() + "正在出售第"
+tickets+"張票");
tickets--;
}
}
}
問題原因
線程執行的隨機性導致的
2.2 買票案例數據安全問題
- 為什么出現問題?(這也是我們判斷多線程程序是否會有數據安全問題的標准)
- 是否是多線程環境
- 是否有數據共享
- 是否有多條語句操作共享數據
- 如何解決多線程的安全問題?
- 基本思路:讓程序沒有安全問題的環境
- 怎么實現呢?
- 把多條語句操作共享數據的代碼給鎖起來,讓任意時刻只能有一個線程執行即可
- Java提供了同步代碼塊的方式來解決
2.3 同步代碼塊
鎖多條語句操作共享數據,可以使用同步代碼塊實現
格式:
synchronized(任意對象){
多條語句操作共享數據的代碼塊
}
synchronized(任意對象):就相當於給代碼加鎖了,任意對象就可以看成是一把鎖
public class SellTicket implements Runnable {
private int tickets = 100;
// 這個是鎖對象,可以是任意對象
// 例如Student s = new Studnent();Integer i = new Integer()都是可以的
// 只不過在synchronized()中要輸入相同的對象,以代表被synchronized包裹的代碼是被同一把鎖鎖住的
Object obj = new Object();
@Override
public void run() {
while (true) {
// 傳入任意對象即可,也可以用匿名類的方法
// synchronized(new Object()){
synchronized (obj) {
if (tickets > 0) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "正在出售第"
+ tickets + "張票");
tickets--;
}
}
}
}
}
public class SellTicketDemo {
public static void main(String[] args) {
SellTicket s = new SellTicket();
Thread t1 = new Thread(s,"一號窗口");
Thread t2 = new Thread(s,"二號窗口");
Thread t3 = new Thread(s,"三號窗口");
t1.start();
t2.start();
t3.start();
}
}
同步的好處和弊端
- 好處:解決了多線程的數據安全問題
- 弊端:當線程很多時,因為每個線程都會去判斷同步上的鎖,很耗費資源,無形中會降低程序的運行效率
2.4 同步方法
同步方法:就是把synchronized關鍵字加到方法上
- 格式:修飾符 synchronized 返回值類型 方法名(方法參數){...}
public class SellTicket implements Runnable {
private int tickets = 100;
private int x = 0;
@Override
public void run() {
while (true) {
if(x%2 == 0){
// 這里使用this關鍵字獲取當前調用方法的對象
// 如果繼續使用object的話會因為用的不是同一把鎖導致沒有同步執行
synchronized (this) {
if (tickets > 0) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()
+ "正在出售第" + tickets + "張票");
tickets--;
}
}
}else{
sellTicket();
}
x++;
}
}
// 同步代碼鎖的對象是this
public synchronized void sellTicket() {
if (tickets > 0) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()
+ "正在出售第" + tickets + "張票");
tickets--;
}
}
}
public class SellTicketDemo {
public static void main(String[] args) {
SellTicket s = new SellTicket();
Thread t1 = new Thread(s,"一號窗口");
Thread t2 = new Thread(s,"二號窗口");
Thread t3 = new Thread(s,"三號窗口");
t1.start();
t2.start();
t3.start();
}
}
同步方法的鎖對象是什么?
- this
同步靜態方法:就是把synchronized關鍵字加到靜態方法上
- 格式:修飾符 static synchronized 返回值類型 方法名(方法參數){...}
public class SellTicket implements Runnable {
// 靜態方法調用靜態變量,所以講tickets用static修飾符修飾
private static int tickets = 100;
private int x = 0;
@Override
public void run() {
while (true) {
if(x%2 == 0){
// 靜態方法的鎖的對象是類名.class
synchronized (SellTicket.class) {
if (tickets > 0) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()
+ "正在出售第" + tickets + "張票");
tickets--;
}
}
}else{
sellTicket();
}
x++;
}
}
// 靜態方法的鎖的對象是類名.class
public static synchronized void sellTicket() {
if (tickets > 0) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()
+ "正在出售第" + tickets + "張票");
tickets--;
}
}
}
public class SellTicketDemo {
public static void main(String[] args) {
SellTicket s = new SellTicket();
Thread t1 = new Thread(s,"一號窗口");
Thread t2 = new Thread(s,"二號窗口");
Thread t3 = new Thread(s,"三號窗口");
t1.start();
t2.start();
t3.start();
}
}
同步靜態方法的鎖對象是什么
- 類名.class
2.5 線程安全的類
StringBuffer
- 線程安全,可變的字符序列。
- 從版本JDK5開始,這個類已經被一個等同的類補充了,它被設計為使用一個線程,StringBuilder。通常應該使用StringBuilder類,因為它支持所有相同的操作,但它更快,因為它不執行同步。
Vector
- 從Java2平台v1.2開始,該類改進了List接口,使其成為Java Collections Framework的成員。與新的集合實現不同,Vector被同步。如果不需要線程安全的實現,建議使用ArrayList代替Vector
Hashtable
- 該類實現了一個哈希表,它將鍵映射到值。任何非null對象都可以用作鍵值或值。
- 從Java 2平台v1.2開始,該類進行了改進,實現了Map接口,成為Java Collections Framework的成員。與新的集合實現不同,Hashtable被同步。如果不需線程安全的實現,建議用HashMap代替Hashtable。
/*
線程安全的類
StringBuffer
Vector
Hashtable
*/
public class ThreadDemo {
public static void main(String[] args) {
// 一般線程安全類中只用StringBuffer,因為ArrayList和HashMap可以通過Collections的指定方法返回一個同步列表
StringBuffer sb = new StringBuffer();
List<String> list = Collections.synchronizedList(new ArrayList<String>());
Vector<String> v = new Vector<>();
Map<String, String> map = Collections.synchronizedMap(new HashMap<String, String>());
Hashtable<String, String> ht = new Hashtable<>();
}
}
2.6 Lock鎖
雖然我們可以理解同步代碼塊和同步方法的鎖對象問題,但是我們並沒有直接看到在哪里加上了鎖,在哪里釋放了鎖
為了更清晰的表達如何加鎖和釋放鎖,JDK5以后提供了一個新的鎖對象Lock
Lock實現提供比使用synchronized方法和語句可以獲得更廣泛的鎖定操作
- Lock中提供了獲得鎖和釋放鎖的方法
- void lock():獲得鎖
- void unlock():釋放鎖
Lock是接口,不能直接實例化,這里采用它的實現類ReentrantLock來實例化
- Reentrantlock的構造方法
- ReentrantLock():創建一個ReentrantLock的實例
因為代碼運行中可能會出現異常,導致不能釋放鎖,所以使用try{}finally{}語句塊
public class SellTicket implements Runnable {
private static int tickets = 100;
// ReentrantLock():創建一個ReentrantLock的實例
private Lock lock = new ReentrantLock();
@Override
public void run() {
while (true) {
try {
// void lock():獲得鎖
lock.lock();
if (tickets > 0) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()
+ "正在出售第" + tickets + "張票");
tickets--;
}
} finally {
// void unlock():釋放鎖
lock.unlock();
}
}
}
}
public class SellTicketDemo {
public static void main(String[] args) {
SellTicket s = new SellTicket();
Thread t1 = new Thread(s,"一號窗口");
Thread t2 = new Thread(s,"二號窗口");
Thread t3 = new Thread(s,"三號窗口");
t1.start();
t2.start();
t3.start();
}
}
3、生產者消費者
3.1 生產者消費者模式概述
生產者消費者模式是一個十分經典的多線程協作的模式,弄懂生產者消費者問題能夠讓我們對多線程編程的理解更加深刻
所謂生產者消費者問題,實際上主要是包含了兩類線程:
- 一類是生產者線程用於生產數據
- 一類是消費者線程用於消費數據
為了解耦生產者和消費者的關系,通常會采用共享的數據區域,就像是一個倉庫
- 生產者生產數據之后直接放置在共享數據區中,並不需要關心消費者的行為
- 消費者只需要從共享數據區中去獲取數據,並不需要關心生產者的行為
- 生產者----→共享數據區域←----消費者
為了體現生產和消費過程中的等待和喚醒,Java就提供了幾個方法供我們使用,這幾個方法在Object類中
Object類的等待和喚醒方法:
- void wait() 導致當前線程等待,直到另一個線程調用該對象的notify()方法或notifyAll()方法
- void notify() 喚醒正在等待對象監視器的單個線程。
- void notifyAll() 喚醒正在等待對象監視器的所有線程。
3.2 生產着消費者案例
生產者消費者案例中包含的類:
- 奶箱類(Box):定義一個成員變量,表示第X瓶奶,提供存儲牛奶和獲取牛奶的操作
- 生產者類(Producer):實現Runnable接口,重寫run()方法,調用存儲牛奶的操作
- 消費者類(Customer):實現Runnable接口,重寫run()方法,調用獲取牛奶的操作
- 測試類(BoxDemo):里面有main方法,main方法中的代碼步驟如下
- 創建奶箱對象,這是共享數據區域
- 創建消費者對象,把奶箱對象作為構造方法參數傳遞,因為在這個類中要調用獲取牛奶的操作
- 創建兩個線程對象,分別把生產者對象和消費者對象作為構造方法參數傳遞
- 啟動線程
// 1.奶箱類(Box):定義一個成員變量,表示第X瓶奶,提供存儲牛奶和獲取牛奶的操作
public class Box {
private int milk = 0;
private static final int MAX_NUM = 50;
private static final int MIN_NUM = 0;
// IllegalMonitorStateException不擁有指定的監視器,方法不用synchronized修飾時拋出
// 需要添加同步關鍵字synchronized
// 寫了等待還要寫喚醒
public synchronized void getMilk() {
if (milk<=MIN_NUM) {
try {
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("消費者拿到第" + milk + "瓶奶");
milk--;
// 不寫喚醒最多各自執行一次之后,兩條進程全部進入等待狀態
notify();
}
public synchronized void setMilk() {
if (milk>=MAX_NUM) {
try {
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
milk++;
System.out.println("生產者將第" + milk + "瓶奶放入奶箱");
notify();
}
}
// 2.生產者類(Producer):實現Runnable接口,重寫run()方法,調用存儲牛奶的操作
public class Producer implements Runnable {
Box box;
public Producer() {
}
public Producer(Box box) {
this.box = box;
}
@Override
public void run() {
for (int i = 0; i < 100; i++) {
box.setMilk();
}
}
}
// 3.消費者類(Customer):實現Runnable接口,重寫run()方法,調用獲取牛奶的操作
public class Customer implements Runnable {
Box box;
public Customer() {
}
public Customer(Box box) {
this.box = box;
}
@Override
public void run() {
while (true) {
box.getMilk();
}
}
}
public class BoxDemo {
public static void main(String[] args) {
Box box = new Box();
Producer p = new Producer(box);
Customer c = new Customer(box);
Thread t1 = new Thread(p);
Thread t2 = new Thread(c);
t1.start();
t2.start();
}
}
小結:
- 共享數據區域的數據要進行監控,否則消費者會無限制的從共享數據區域拿到數據致使數據變成負數