day06面向對象(多線程、同步和線程狀態)
一.多線程
1.線程類Thread的介紹
Thread類代表Java中的線程類
構造方法:
public Thread():無參構造
public Thread(String name):帶有線程名字的構造方法
public Thread(Runnable r):Runnable構造
public Thread(Runnable r, String name):帶有Runnable和線程名字的構造方法
成員方法:
public String getName():獲取線程名字
public void setName(String name):修改線程名字
public void start():開啓線程
public void run():線程執行任務
靜態方法:
public static void sleep(long 毫秒值):讓當前線程“休眠”指定的時間
public static Thread currentThread():獲取到當前線程對象
通用的獲取線程名字的方式:
Thread.currentThread().getName();
2.多線程的原理
a.時序圖
start()在main方法中一調用,就彈棧,創建對應的線程的棧(start()方法是底層實現的,不是Java寫的,Java幹 預不了內存操作)
b.內存圖
每個線程都有獨立的棧(線程開啓成功後出現),所有線程共享一個堆(Java進程的堆)
3.創建新的線程方式二*******************
API的描述:
另一種方法是聲明Runnable接口的類
該類然後實現run方法,然後可以分配該類的實例
在創建Thread時作爲一個參數來傳遞並啓動
a.創建實現類 實現Runnable接口
b.實現類重寫run方法
c.創建實現類對象
d.創建Thread同時傳入實現類對象
e.啓動線程對象
public class MyRunnable implements Runnable {
@Override
public void run() {
for (int i = 0; i < 20; i++) {
System.out.println(Thread.currentThread().getName()+".." + i);
}
}
}
public class TestDemo {
public static void main(String[] args) {
//3.實現類對象,任務對象
MyRunnable mr = new MyRunnable();
//4.創建線程對象,傳入實現類對象
Thread t = new Thread(mr,"小花");
//5.開啓線程
t.start();
//主線程
for (int i = 0; i < 20; i++) {
System.out.println(Thread.currentThread().getName()+"..."+i);
}
}
}
4.兩種創建線程方式的比較
一種稱爲繼承方式 一種稱爲實現方式
a.實現方式避免了單繼承的不足
b.實現方式線程對象和任務對象是鬆耦合的,繼承方式是緊耦合的
c.可以讓多個線程執行同一個任務
d.線程池只接受實現方式的任務對象
總結:在開發中儘量使用實現方式創建線程對象
5.使用匿名內部類創建線程******************
public static void main(String[] args) {
//使用匿名內部類快速創建線程
//1.繼承方式
new Thread(){
@Override
public void run() {
for (int i = 0; i < 100; i++) {
System.out.println(Thread.currentThread().getName() + "...." + i);
}
}
}.start();
//2.實現方式
new Thread(new Runnable(){
@Override
public void run() {
for (int i = 0; i < 100; i++) {
System.out.println(Thread.currentThread().getName() + "...." + i);
}
}
}).start();
//主線程
for (int i = 0; i < 100; i++) {
System.out.println(Thread.currentThread().getName() + "...." + i);
}
}
}
二.線程安全************
1.線程安全問題出現的原因
a.單線程永遠不會有安全問題
b.多線程同時執行,執行同一個任務,操作同一個共享資源,纔可能有安全問題
2.線程安全問題的案例分析:賣票案例**********
產生重複數據的原因:
一個線程執行完賣票,沒有來得及減票,被另一個線程搶走CPU
產生0,-1非法數據的原因:
當時剩下最後一張票時,多個線程都通過if的判斷,導致賣出0和-1張
/**
* 賣票任務
*/
public class MyTask implements Runnable {
int count = 100; //100張票
@Override
public void run() {
while (true) {
if (count > 0) {
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"賣出第"+count+"張票");
count--;
}
}
}
}
public class TestDemo {
public static void main(String[] args) {
//1.創建賣票任務對象
MyTask mt = new MyTask();
//2.創建線程,傳入任務
Thread t1 = new Thread(mt,"小美");
Thread t2 = new Thread(mt,"小花花");
Thread t3 = new Thread(mt,"馬兒扎哈");
//3.開啓
t1.start();
t2.start();
t3.start();
//安全問題出現了:
//a.重複數據
//b.非法數據 0 -1
}
}
3.線程同步
讓多句代碼只能由某個線程進入執行,當這個線程沒有執行完畢之前,其他線程不能執行
4.三種線程同步的方式
a.同步代碼塊(開發中最最最最常用!!!!)
格式:
synchronized(鎖對象){
需要同步的哪些代碼
}
注意:鎖對象可以是任意對象,但是必須是一個對象
b.同步方法:(默認就是用當前任務對象當鎖,不能更改synchronized(this))
格式:
public synchronized void method(){
需要同步的哪些代碼
}
注意:
a.同步方法也需要鎖對象,同步方法也需要鎖對象,默認使用this作爲鎖對象
b.如果同步方法是靜態方法,默認使用當前類的字節碼文件作爲鎖對象
c.Lock鎖(面向對象,接口)
格式:
Lock lock = new reentrantLock();
lock.lock(); // 加鎖
需要同步的代碼
lock.unlock(); // 解鎖
三.線程的狀態*************
1.線程的六種狀態(槍鎖操作是CPU分配的)
a.新建狀態NEW:至今尚未啓動的線程處於這種狀態(剛剛創建的線程,還沒有調用start())
b.可運行狀態RUNNABLE:正在Java虛擬機中執行的線程處於這種狀態(可運行,就緒)(創建後的線程,調用了start())
c.鎖阻塞狀態BLOCKED:受阻塞並等待某個監視器鎖的線程處於這種狀態(需要鎖但是鎖被其他線程持有)(其他線程釋放鎖當前線程搶到鎖就進入可執行狀態)
d.無限等待WAITING:無限期地等待另一個線程來執行某一特定操作的線程處於這種狀態
注意:1)當前線程必須持有鎖對象
2)調用鎖對象的wait方法
3)當前會自動把鎖釋放後進入無限等待
喚醒:1)其他線程持有鎖對象(必須是剛剛無限等待線程釋放的那個鎖對象)
2)調用鎖對象的notify方法,線程狀態變爲鎖阻塞狀態,需要等待鎖被其他線程釋放搶到鎖後才能可執行
e.限時等待(休眠)TIMED_WAITING:等待另一個線程來執行取決於指定等待時間的操作的線程處於這種狀態(調用Thread.sleep(毫秒值))(限時等待的時間完畢才能恢復)
f.消亡狀態(退出狀態)TERMINATED:已退出的線程處於這種狀態
四.等待喚醒機制****************
1.等待喚醒機制(wait和notify)
public class WaitAndNotifyDemo01 {
public static void main(String[] args) throws InterruptedException {
//定義一個鎖對象
Object obj = new Object();
//線程1
new Thread(new Runnable() {
@Override
public void run() {
synchronized (obj){
System.out.println("線程1搶到了鎖對象...");
System.out.println("線程1即將進入無限等待...");
//進入無限等待
try {
obj.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("線程1從無限等待中醒來了...");
}
}
}).start();
for (int i = 0; i < 5; i++) {
Thread.sleep(1000);
System.out.println(i);
}
//線程2
new Thread(new Runnable() {
@Override
public void run() {
synchronized (obj) {
System.out.println("線程2搶到鎖對象了...");
System.out.println("線程2即將喚醒線程1...");
obj.notify();
//多加幾行
System.out.println("線程2繼續執行...");
System.out.println("線程2繼續執行...");
System.out.println("線程2繼續執行...");
System.out.println("線程2繼續執行...");
System.out.println("線程2繼續執行...");
System.out.println("線程2繼續執行...");
System.out.println("線程2繼續執行...");
System.out.println("線程2繼續執行...");
}
}
}).start();
}
}
2.生產者與消費者問題(代碼演示)*******************
有兩個線程
一個線程稱爲生產者線程
另一個稱爲消費者線程
public class WaitAndNotifyDemo02 {
public static void main(String[] args) {
//1.創建包子對象
BaoZi bz = new BaoZi();
Object obj = new Object();
//2.兩個線程
//生產者線程
new Thread(new Runnable() {
@Override
public void run() {
while (true) {
synchronized (obj) {
//如果有包子,那麼進入無限等待
if (bz.flag == true) { //此處可以優化爲 if(bz.flag)
try {
obj.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//做包子
System.out.println("沒有包子,我就做一個...");
bz.flag = true;
//喚醒消費者
obj.notify();
}
}
}
}).start();
//消費者線程
new Thread(new Runnable() {
@Override
public void run() {
while (true) {
synchronized (obj) {
//如果沒有包子,那麼進入無限等
if (bz.flag == false) { //此處可優化 if(!bz.flag)
try {
obj.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//吃包子
System.out.println("有包子,好吃...");
bz.flag = false;
//喚醒生產者
obj.notify();
}
}
}
}).start();
}
}
總結:
-[] 能夠描述Java中多線程運行原理(時序圖、內存圖)
-[] 能夠使用繼承類的方法創建線程
-[] 能夠使用實現接口的方式船艦多線程
-[] 能夠說出實現接口方式的好處
-[] 能夠解釋安全問題的出現的原因
-[] 能夠使用同步代碼塊解決線程安全問題
-[] 能夠使用同步方法解決線程安全問題
-[] 能夠說出線程6個狀態的名稱(名字,切換)
-[] 能夠理解等待喚醒案例(生產者和消費者案例)