多線程編程基礎
Java多線程
本章內容來自尚學堂Java300集視頻第二季多線程部分總結
- 程序、進程、線程
程序:指令集 靜態概念
進程:操作系統調度程序 動態概念
線程:在進程內多條執行路徑
一個進程中的線程共享相同的內存單元/內存地址空間->可以訪問相同的變量和對象,而且它們從同一堆中分配對象->通信、數據交換、同步操作
由於線程間的通信是同一地址空間上進行的,所以不需要額外的通信機制
區別 | 進程 | 線程 |
---|---|---|
根本區別 | 作爲資源分配的單位 | 調度和執行的單位 |
開銷 | 每個進程都有獨立的代碼和數據空間(進程上下文),進程間的切換會有較大開銷。 | 線程可以看成是輕量級進程,同一類線程共享代碼和數據空間,每個線程有獨立的運行棧和程序計數器(PC),線程切換開銷小。 |
所處環境 | 在操作系統中能同時運行多個任務(程序) | 在同一應用程序中有多個順序流同時執行 |
分配內存 | 系統在運行的時候會爲咩哥進程分配不同的內存區域 | 除了CPU之外,不會爲線程分配內存(線程所使用的資源是它所屬的進程的資源),線程組只能共享資源 |
包含關係 | 沒有線程的進程是可以被看作單線程的,如果一個進程內擁有多個線程,則執行過程不是一條線的,而是多條線程共同完成的。 | 線程是進程的一部分 |
創建線程
繼承Thread + 重寫run(線程體)
package com.zjn.thread.create;
/**
* 模擬龜兔賽跑
* 1.創建多線程,繼承Thhread + 重寫run(線程體)
* 2.使用線程:創建子類對象 + 調用對象.start()方法 線程啓動
*/
public class Rabbit extends Thread{
@Override
public void run() {
for(int i=0;i<100;i++){
System.out.println("兔子跑了"+i+"步");
}
}
}
class Tortoise extends Thread {
@Override
public void run() {
for(int i=0;i<100;i++){
System.out.println("烏龜跑了"+i+"步");
}
}
}
package com.zjn.thread.create;
public class RabbitApp {
public static void main(String[] args) {
//創建子類對象
Rabbit rab = new Rabbit();
Tortoise tor = new Tortoise();
//調用start方法
rab.start();
tor.start();
}
}
控制檯輸出
兔子跑了0步
兔子跑了1步
兔子跑了2步
兔子跑了3步
兔子跑了4步
兔子跑了5步
烏龜跑了0步
兔子跑了6步
...
繼承Thread類方式的缺點:如果我們的類已經從一個類繼承,則無法再繼承Thread類
通過Runnable接口實現多線程
優點:可以同時實現繼承。實現Runnable接口方式要通用一些
使用Runnable 創建線程
-
避免單繼承的侷限性
-
便於共享資源
-
類實現Runnable接口+重寫run() -->真實角色類
-
啓動多線程 使用靜態代理
1) 創建真實角色
2)創建代理角色
3) 調用start()方法
package com.zjn.thread.create;
public class Programmer implements Runnable{
@Override
public void run() {
for (int i=0;i<1000;i++){
System.out.println("一邊敲代碼");
}
}
}
package com.zjn.thread.create;
public class ProgrammerApp {
public static void main(String[] args) {
Programmer pro = new Programmer();
Thread proxy = new Thread(pro);
proxy.start();
for (int i=0;i<1000;i++){
System.out.println("一邊聊天");
}
}
}
控制檯輸出:
...
一邊聊天
一邊聊天
一邊聊天
一邊聊天
一邊聊天
一邊敲代碼
一邊敲代碼
一邊敲代碼
一邊敲代碼
...
靜態代理設計模式
package com.zjn.thread.create;
/**
* 靜態代理 設計模式
* 1.真實角色
* 2.代理角色:持有真實角色的引用
* 3.二者實現相同接口
*/
public class StaticProxy {
public static void main(String[] args) {
//創建真實角色
You you = new You();
//創建代理角色+真實角色的引用
WeddingCompany company = new WeddingCompany();
//執行任務
company.marry();
}
}
interface Marry{
public abstract void marry();
}
//真實角色
class You implements Marry{
@Override
public void marry() {
System.out.println("you and 嫦娥結婚了");
}
}
//代理角色
class WeddingCompany implements Marry{
private Marry you;
public WeddingCompany(Marry you) {
this.you = you;
}
public WeddingCompany() {
}
private void before(){
System.out.println("佈置豬窩");
}
private void after(){
System.out.println("鬧玉兔");
}
@Override
public void marry() {
before();
you.marry();
after();
}
}
感覺靜態代理模式是否也應用於了測試用例部分,比如Junit的設置前置與後置條件。
通過Calllable接口實現多線程
Callable和Future接口
Callable和Runnable有幾點不同:
1.Callable規定的方法是call(),Runnable規定的方法是run()
2.call方法可以拋出異常,run方法不能拋出異常
3.Callable任務執行後可返回值,運行Callable任務可拿到一個Future對象,而Runnable任務是不能返回值的,Future表示異步計算結果,它提供了檢查計算是否完成的方法,以等待計算的完成,並檢索計算的結果。
通過Future對象可瞭解任務執行情況,可取消任務的執行,還可獲取任務執行的結果
package com.zjn.thread.create;
import java.util.concurrent.*;
/**
* 使用callable創建線程
*/
public class Call {
public static void main(String[] args) throws ExecutionException, InterruptedException {
//創建線程
ExecutorService ser = Executors.newFixedThreadPool(1);
Race tortoise = new Race("老烏龜",1000);
Race rabbit = new Race("小兔子",500);
//獲取值
Future<Integer> result1 = ser.submit(tortoise);
Future<Integer> result2 = ser.submit(rabbit);
Thread.sleep(2000);//2秒
//停止線程體循環
tortoise.setFlag(false);
rabbit.setFlag(false);
int num1 = result1.get();
int num2 = result2.get();
System.out.println("烏龜跑了-->"+num1+"步");
System.out.println("兔子跑了-->"+num2+"步");
//停止服務
ser.shutdown();
}
}
class Race implements Callable<Integer>{
private String name;//名稱
private long time;//延時時間
private boolean flag = true;
private int step = 0;//步
@Override
public Integer call() throws Exception {
while (flag){
Thread.sleep(time);//延時
step++;
}
return 1000;
}
...//構造方法以及setter、getter類
線程的狀態和方法
新生狀態 就緒狀態 運行狀態 阻塞狀態 死亡狀態
停止線程
1.自然終止:線程體正常執行完畢
2.外部干涉:
- 線程類中 定義 線程體使用的標識
2)線程體使用該標識
- 提供對外的方法改變該標識
4)外部根據條件調用該方法
阻塞
1.join:合併線程
package com.zjn.thread.status;
/**
* join:合併線程
*/
public class JoinDemo1 extends Thread{
public static void main(String[] args) throws InterruptedException {
JoinDemo1 demo = new JoinDemo1();
Thread t = new Thread(demo);
t.start();
//cpu調度運行
for (int i=0;i<100;i++){
if(50==i){
t.join();//main阻塞
}
System.out.println("main... "+i);
}
}
@Override
public void run() {
for(int i=0;i<100;i++) {
System.out.println("join..."+i);
}
}
}
2.yield:暫停自己的線程
package com.zjn.thread.status;
public class YieldDemo01 extends Thread {
public static void main(String[] args) {
JoinDemo1 demo = new JoinDemo1();
Thread t = new Thread(demo);
t.start();
//cpu調度運行
for (int i=0;i<100;i++){
if(i%20==0){
//暫停本線程main
Thread.yield();
}
System.out.println("main... "+i);
}
}
@Override
public void run() {
for (int i=0;i<1000;i++) {
System.out.println("yield..."+i);
}
}
}
3.sleep:休眠,不釋放鎖。排他鎖
1)與時間相關:倒計時
package com.zjn.thread.status;
import java.text.SimpleDateFormat;
import java.util.Date;
/**
* 倒計時
* 1.倒數10個數,1秒內打印一個
* 2.倒計時
*/
public class SleepDemo01 {
public static void main(String[] args) throws InterruptedException {
Date endTime = new Date(System.currentTimeMillis()+10*1000);
long end = endTime.getTime();
while(true){
//輸出
System.out.println(new SimpleDateFormat("mm:ss").format(endTime));
//構建下一秒時間
endTime = new Date(endTime.getTime()-1000);
//等待一秒
Thread.sleep(1000);
//10秒以內繼續,否則退出
if(end-10000>endTime.getTime()){
break;
}
}
}
public static void test1() throws InterruptedException {
int num = 10;
while(true) {
System.out.println(num);
Thread.sleep(1000);//暫停
if(num<0){
break;
}
}
}
}
2)模擬網絡延時
package com.zjn.thread.status;
/**
* Sleep模擬網絡延時,線程不安全
*/
public class SleepDemo02 {
public static void main(String[] args) {
//真實角色
com.zjn.thread.create.Web12306 web = new com.zjn.thread.create.Web12306();
//代理
Thread t1 = new Thread(web,"路人甲");
Thread t2 = new Thread(web,"黃牛乙");
Thread t3 = new Thread(web,"工程師");
t1.start();
t2.start();
t3.start();
}
}
class Web12306 implements Runnable {
private int num = 50;
@Override
public void run() {
while(true){
if (num < 0){
break; //跳出循環
}
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"搶到了"+num--);
}
}
}
- 線程基本信息和優先級
package com.zjn.thread.info;
/**
* 優先級:概率,不是絕對的優先級,沒有絕對先後順序
* MAX_PRIORITY 10
* NORM_PRIORITY 5 默認
* MIN_PRIORITY 1
*/
public class InfoDemo02 {
public static void main(String[] args) throws InterruptedException {
MyThread it = new MyThread();
Thread p1 = new Thread(it,"挨踢1");
MyThread it2 = new MyThread();
Thread p2=new Thread(it2,"挨踢2");
p1.setPriority(Thread.MIN_PRIORITY);//設置優先級
p2.setPriority(Thread.MAX_PRIORITY);
p1.start();
p2.start();
Thread.sleep(1000);
it.stop();
it2.stop();
}
}
package com.zjn.thread.info;
/**
* Thread currentThread() :當前線程
* setName() :設置名稱
* getName() :獲取名稱
* isAlive()
*/
public class InfoDemo01 {
public static void main(String[] args) throws InterruptedException {
MyThread it = new MyThread();
Thread proxy = new Thread(it,"挨踢");
proxy.setName("test");
System.out.println(proxy.getName());
System.out.println(Thread.currentThread().getName());//main
proxy.start();
System.out.println("啓動後的狀態"+proxy.isAlive());
Thread.sleep(200);
it.stop();
Thread.sleep(100);
System.out.println("停止後的狀態"+proxy.isAlive());
}
}
package com.zjn.thread.info;
public class MyThread implements Runnable {
private boolean flag = true;
private int num = 0;
@Override
public void run() {
while(flag) {
System.out.println(Thread.currentThread().getName()+"-->"+ num++);
}
}
public void stop(){
this.flag = !this.flag;
}
}
線程的同步和死鎖問題
同步:併發,多個線程訪問同一份資源 確保資源安全 -> 線程安全
Synchronize -> 同步
一、同步塊
synchronized(引用類型|類|class){
}
死鎖:過多的同步會造成死鎖
package com.zjn.thread.syn;
public class SynDemo01 {
public static void main(String[] args) {
//真實角色
com.zjn.thread.create.Web12306 web = new com.zjn.thread.create.Web12306();
//代理
Thread t1 = new Thread(web,"路人甲");
Thread t2 = new Thread(web,"黃牛乙");
Thread t3 = new Thread(web,"工程師");
t1.start();
t2.start();
t3.start();
}
}
class Web12306 implements Runnable {
private int num = 50;
private boolean flag = true;
@Override
public void run() {
while (flag) {
test2();
}
}
//鎖定範圍不正確
public void test4(){
//a,b,c
synchronized (this) {
if (num < 0) {
flag=false; //跳出循環
return;
}
}
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "搶到了" + num--);
}
//線程安全 鎖定正確
public void test2(){
if (num < 0) {
flag=false; //跳出循環
return;
}
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "搶到了" + num--);
}
//線程不安全
public synchronized void test1(){
if (num <= 0) {
flag=false; //跳出循環
}
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "搶到了" + num--);
}
//線程安全 鎖定正確
public void test5(){
//a,b,c
synchronized ((Integer)num) {
if (num < 0) {
flag=false; //跳出循環
return;
}
}
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "搶到了" + num--);
}
}
單例設計模式
1.懶漢式
2.餓漢式
package com.zjn.thread.syn;
/**
* 單例設計模式:外部使用時,確保一個類只有一個對象
*/
public class SynDemo02 {
public static void main(String[] args) {
// //單線程中地址空間一樣
// Jvm jvm1 = Jvm.getInstance();
// Jvm jvm2 = Jvm.getInstance();
// System.out.println(jvm1);
// System.out.println(jvm2);
// //多線程地址空間不同
// JvmThread thread1 = new JvmThread(100);
// JvmThread thread2 = new JvmThread(500);
// thread1.start();
// thread2.start();
//加入同步,地址空間一樣
JvmThread thread1 = new JvmThread(100);
JvmThread thread2 = new JvmThread(500);
thread1.start();
thread2.start();
}
}
class JvmThread extends Thread{
private long time;
public JvmThread() {
}
public JvmThread(long time) {
this.time = time;
}
@Override
public void run() {
System.out.println(Thread.currentThread().getName()+"-->創建:"+Jvm.getInstance(time));
}
}
/**
* 單例設計模式
* 確保一個類只有一個對象
* 懶漢式-使用時才創建對象
* 1。構造器私有化,避免外部直接創建對象
* 2。聲明一個私有的靜態變量
* 3.創建一個對外的公共的靜態方法訪問該變量,如果變量沒有對象,創建該對象
*/
class Jvm {
//聲明一個私有的靜態變量,靜態的同一份資源
private static Jvm instance = null;
//構造器私有化,避免外部直接創建對象
private Jvm(){
}
//double checking雙重檢查提高已經存在對象的訪問效率
public static Jvm getInstance(long time) {
if (null==instance){
//其他線程進入的時候如果發現已經有對象就不用重新創建,直接獲取
synchronized (Jvm.class){
if (null == instance) {
try {
Thread.sleep(time);//延時,放大錯誤
} catch (InterruptedException e) {
e.printStackTrace();
}
instance = new Jvm();
}
}
}
return instance;
}
//synchronized鎖類的字節碼信息
public static Jvm getInstance3(long time) {
//a b --> 效率不高,進來都需要等待,存在對象也需要等待
synchronized (Jvm.class){
if (null == instance) {
try {
Thread.sleep(time);//延時,放大錯誤
} catch (InterruptedException e) {
e.printStackTrace();
}
instance = new Jvm();
}
return instance;
}
}
public static synchronized Jvm getInstance2(long time) {
if (null == instance) {
try {
Thread.sleep(time);//延時,放大錯誤
} catch (InterruptedException e) {
e.printStackTrace();
}
instance = new Jvm();
}
return instance;
}
//創建一個對外的公共的靜態方法訪問該變量,如果變量沒有對象,創建該對象
public static Jvm getInstance1(long time) {
if (null == instance) {
try {
Thread.sleep(time);//延時,放大錯誤
} catch (InterruptedException e) {
e.printStackTrace();
}
instance = new Jvm();
}
return instance;
}
}
package com.zjn.thread.syn;
/**
* 單例創建的方式
* 1。懶漢式
* 1)構造器私有化
* 2)聲明私有的靜態屬性
* 3)對外提供訪問屬性的靜態方法,確保該對象存在
*/
public class MyJvm {
private static MyJvm instance;
private MyJvm(){
}
public static MyJvm getInstance(){
if(null==instance){ // 提高效率
synchronized (MyJvm.class){
if(null==instance){ //安全檢查
instance = new MyJvm();
}
}
}
return instance;
}
}
/**
* 餓漢式-線程安全
* 1)構造器私有化
* 2)聲明私有的靜態屬性,同時創建該對象
* 3)對外提供訪問屬性的靜態方法
*/
class MyJvm2 {
//類加載時創建
private static MyJvm2 instance = new MyJvm2();
private MyJvm2(){
}
public static MyJvm2 getInstance(){
return instance;
}
}
/**
* 提高效率
* 類在使用的時候加載,JVMholder延緩了加載時間
*/
class MyJvm3 {
private static class JVMholder{
//類加載時創建
private static MyJvm3 instance = new MyJvm3();
}
private MyJvm3(){
}
public static MyJvm3 getInstance(){
return JVMholder.instance;
}
}
死鎖
package com.zjn.thread.syn;
/**
* 過多的同步方法可能造成死鎖
*/
public class SynDemo03 {
public static void main(String[] args) {
Object g = new Object();
Object m = new Object();
//兩個線程訪問同樣資源
Test t1 = new Test(g,m);
Test2 t2 = new Test2(g,m);
Thread proxy = new Thread(t1);
Thread proxy2 = new Thread(t2);
proxy.start();
proxy2.start();
class Test implements Runnable{
Object goods;
Object money;
public Test(Object goods, Object money) {
this.goods = goods;
this.money = money;
}
@Override
public void run() {
while (true){
test();
}
}
public void test(){
synchronized (goods){
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (money){
}
}
System.out.println("一手給錢");
}
}
class Test2 implements Runnable{
Object goods;
Object money;
public Test2(Object goods, Object money) {
this.goods = goods;
this.money = money;
}
@Override
public void run() {
while (true){
test();
}
}
public void test(){
synchronized (money){
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (goods){
}
}
System.out.println("一手給貨");
}
}
生產者消費者模式-信號燈法
Producer-consumer problem生產者-消費者問題,也稱爲有限緩衝問題,是一個多線程同步問題的經典案例。該問題描述了兩個共享固定大小緩衝區的線程-即所謂生產者和消費者——在實際運行時會發生的問題,生產者的主要作用是生成一定量的數據放到緩衝區中,然後重複此過程。與此同時,消費者也在緩衝區消耗這些數據。該問題的關鍵就是要保證生產者不會在緩衝區滿時加入數據,消費者也不會在緩衝區空時消耗數據。
要解決該問題,就必須讓生產者在緩衝區滿時休眠(要麼乾脆放棄數據)。等到下次消費者消耗緩衝區中的數據的時候,生產者才能被喚醒,開始往緩衝區中添加數據。同樣,也可以讓消費者在緩衝區空時進入休眠,等到生產者往緩衝區添加數據之後,再喚醒消費者。通常常用的方法有信號燈法、管程等。如果解決方法不夠完善,則容易出現死鎖的情況。出現死鎖時,兩個線程都會陷入休眠,等待對方喚醒自己。
package com.zjn.thread.pro;
public class App {
public static void main(String[] args) {
//共同的資源
Movie m = new Movie();
//多線程
Player p = new Player(m);
Watcher w = new Watcher(m);
new Thread(p).start();
new Thread(w).start();
}
}
package com.zjn.thread.pro;
/**
* 一個場景,共同的資源
* 生產者消費者模式 信號燈法
* wait():等待,釋放鎖。sleep不釋放鎖
* notify()/notifyAll():喚醒
* 與synchronized一起使用
*/
public class Movie {
private String pic;
//信號燈
//flag=true生產者生產,消費者等待,生產完成後通知消費
//flag=flag生產者等待,消費者消費,消費完成後通知生產
private boolean flag = true;
/**
* 播放
* @param pic
*/
public synchronized void play(String pic){
if(!flag){//生產者等待
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//開始生產
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("生產了"+pic);
//生產完畢
this.pic = pic;
//通知消費
this.notify();
//生產者停下
this.flag = false;
}
public synchronized void watch(){
if (flag){//消費者等待
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//開始消費
try {
Thread.sleep(200);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("消費了"+pic);
//消費完畢
//通知生產
this.notifyAll();
this.flag = true;
}
}
package com.zjn.thread.pro;
/**
* 生產者
*/
public class Player implements Runnable{
private Movie m;
public Player(Movie m) {
this.m = m;
}
@Override
public void run() {
for(int i=0;i<20;i++){
if(i%2==0){
m.play("左青龍");
}else{
m.play("右白虎");
}
}
}
}
package com.zjn.thread.pro;
/**
* 消費者
*/
public class Watcher implements Runnable{
private Movie m;
public Watcher(Movie m) {
this.m = m;
}
@Override
public void run() {
for(int i=0;i<20;i++){
m.watch();
}
}
}
任務調度
Timer定時器類
TimerTask任務類
通過java timer timertask:(spring的任務調度就是通過他們來實現的)
在這種實現方式中,Timer類實現的是類似鬧鐘的功能,也就是定時或是每隔一定時間觸發一次線程。其實,Timer類本身實現的就是一個線程,只是這個線程是用來實現調用其他線程的。而TimerTask類是一個抽象類,該類實現了Runnable接口,所以按照前面的介紹,該類具備多線程的能力。
在這種實現方式中,通過繼承TimerTask使該類獲得多線程的能力,將需要多線程執行的代碼書寫在run方法內部,然後通過Timer類啓動線程的執行
在實際使用時,一個Timer可以啓動任意多個TimerTask實現的線程,但是多個線程之間會存在阻塞。所以如果多個線程之間如果需要完全獨立運行的話,最好還是一個Timer啓動一個TimerTask實現。
package com.zjn.thread.schedule;
import java.util.Date;
import java.util.Timer;
import java.util.TimerTask;
/**
* schedule(TimerTask task, Date time)
* schedule(TimerTask task, Date time, long period)
*/
public class TimeDemo01 {
public static void main(String[] args) {
Timer timer = new Timer();
//每隔200毫秒運行一次
timer.schedule(new TimerTask() {
@Override
public void run() {
System.out.println("so easy...");
}
},new Date(System.currentTimeMillis()+1000),200);
}
}
Python多線程
啓動線程
# 創建線程
x = threading.Thread(target=thread_function, args=(1,))
# 啓動線程
x.start()
守護進程和線程
要告訴一個線程等待另一個線程結束,請調用.join()
x.join()
啓動多線程
使用threading.Thread
線程的運行順序由操作系統確定,可能很難預測。它可能(並且可能會)因運行而異
threads = list()
for index in range(3):
x = threading.Thread(target=thread_function, args=(index,))
threads.append(x)
x.start()
for index, thread in enumerate(threads):
thread.join()
使用ThreadPoolExecutor
作爲上下文管理器,使用該with語句來管理池的創建和銷燬
import concurrent.futures
with concurrent.futures.ThreadPoolExecutor(max_workers=3) as executor:
executor.map(thread_function, range(3))
競爭問題
當兩個或多個線程訪問共享的數據或資源時,可能會發生爭用情況。
Lock
一次只允許一個線程進入代碼的read-modify-write部分。最常見的方法是Lock在Python中調用。在其他一些語言中,相同的想法稱爲mutex。Mutex來自互斥,這正是a的Lock功能。
由with語句鎖定和釋放:
class FakeDatabase:
def __init__(self):
self.value = 0
self._lock = threading.Lock()
def locked_update(self, name):
with self._lock:
local_copy = self.value
local_copy += 1
time.sleep(0.1)
self.value = local_copy