文章目錄
票務中心案例
票 100
售票途徑 (多線程)
// 票務中心
public class Ticket implements Runnable{
// 票的數量爲 100 張
int count = 100;
@Override
public void run() {
// 循環賣票
while (true){
if(count > 0){
// 爲了讓問題更加突顯
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"\t第"+count+"張");
}else{
break;
}
// 每循環一次, 總票數 -1
count--;
}
}
}
測試類
public class TicketDemo {
public static void main(String[] args) {
Ticket t = new Ticket();
Thread t1 = new Thread(t,"網絡售票");
Thread t2 = new Thread(t,"售票窗口");
Thread t3 = new Thread(t,"黃牛黨");
t1.start();
t2.start();
t3.start();
}
}
運行結果分析
- 重複票 一張票賣出兩次
- 賣票順序不同
- 負數票
產生該問題的原因
CPU 選擇線程的隨機性
思考解決方式
在容易出現問題的代碼上 上鎖
- 容易出現問題的代碼
一個售票員 每賣出一張票, 必須重新設置該票的總數, 中間的過程不允許被其它售票員強行中斷
一旦中斷, 很容易發生 線程安全問題
代碼鎖的格式
同步代碼鎖
synchronized (鎖對象){
// 容易發生問題的代碼
}
對鎖對象的要求是, 多個線程的鎖對象必須是同一個
代碼鎖演示
@Override
public void run() {
while(true){
synchronized (this){
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
if(count <= 0){
break;
}
System.out.println(Thread.currentThread().getName()+"\t第"+count+"張");
count--;
}
}
}
同步方法鎖
成員方法
public void run() {
while(true){
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
sellTicket();
}
}
public synchronized void sellTicket(){
if(count <= 0){
return;
}
System.out.println(Thread.currentThread().getName()+"\t第"+count+"張");
count--;
}
synchronized 存在一定有鎖對象存在
成員方法 的鎖對象爲 this
靜態方法
只是把成員方法 添加靜態修飾符
靜態方法 的鎖對象爲 該類的字節碼對象
使用同步代碼鎖優劣分析
優勢
- 可以避免線程安全問題的出現
劣勢
- 必須等待一個線程運行完畢同步代碼塊 之後, 其他線程纔可以運行
導致效率變低
速度和安全 對立的雙方
對比之前的對象
ArrayList 和 Vector synchronized
StringBuffer (synchronized) 和 StringBuilder
學習完同步之後再看線程狀態
同步代碼鎖, 把一塊區域的代碼 加鎖, 一次只能允許一條線程 進入加鎖的代碼
提款機, 從一個人進入提款機面前, 插卡, 輸入密碼 , 輸入提款金額, 把錢放進錢包, 退卡
其他線程 在排隊等待 即使CPU 把執行權給了 線程, 但是該線程也無法進入上鎖區域
lock鎖
lock 鎖基本介紹
Lock實現提供比使用synchronized方法和語句可以獲得的更廣泛的鎖定操作。 它們允許更靈活的結構化,可能具有完全不同的屬性,並且可以支持多個相關聯的對象Condition 。
鎖是用於通過多個線程控制對共享資源的訪問的工具。 通常,鎖提供對共享資源的獨佔訪問:一次只能有一個線程可以獲取鎖,並且對共享資源的所有訪問都要求首先獲取鎖
基本使用語法
public void sellTicket(){
// 獲得鎖。
lock.lock();
if(count <= 0){
return;
}
System.out.println(Thread.currentThread().getName()+"\t第"+count+"張");
count--;
// 釋放鎖
lock.unlock();
}
問題分析
萬一在 lock() 和 unlock() 之間 出現了 異常
將不會執行 unlock() 釋放鎖
代碼優化方式
public void sellTicket(){
// 獲得鎖。
lock.lock();
try{
if(count <= 0){
return;
}
System.out.println(Thread.currentThread().getName()+"\t第"+count+"張");
count--;
}finally{
// 釋放鎖
lock.unlock();
}
}
和 synchronized 對比
1)Lock是一個接口,而synchronized是Java中的關鍵字,synchronized是內置的語言實現,synchronized是在JVM層面上實現的,不但可以通過一些監控工具監控synchronized的鎖定,而且在代碼執行時出現異常,JVM會自動釋放鎖定,但是使用Lock則不行,lock是通過代碼實現的,要保證鎖定一定會被釋放,就必須將 unLock()放到finally{} 中;
2)synchronized在發生異常時,會自動釋放線程佔有的鎖,因此不會導致死鎖現象發生;而Lock在發生異常時,如果沒有主動通過unLock()去釋放鎖,則很可能造成死鎖現象,因此使用Lock時需要在finally塊中釋放鎖;
3)Lock可以讓等待鎖的線程響應中斷,線程可以中斷去幹別的事務,而synchronized卻不行,使用synchronized時,等待的線程會一直等待下去,不能夠響應中斷;
4)通過Lock可以知道有沒有成功獲取鎖,而synchronized卻無法辦到。
5)Lock可以提高多個線程進行讀操作的效率。
在性能上來說,如果競爭資源不激烈,兩者的性能是差不多的,而當競爭資源非常激烈時(即有大量線程同時競爭),此時Lock的性能要遠遠優於synchronized。所以說,在具體使用時要根據適當情況選擇。
死鎖問題
// 場景模擬
張三 和 李四
public class MyThread extends Thread{
public static final Object left = new Object();
public static final Object right = new Object();
public boolean boo ;
public MyThread(){}
public MyThread(boolean boo) {
this.boo = boo;
}
@Override
public void run() {
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
// true 張三開始從左向右走
if(boo){
while (true){
leftRight();
}
}else{
// false 李四開始從右向左走
while(true){
rightLeft();
}
}
}
public void leftRight(){
synchronized (left){
System.out.println("張三進入了左側房間!");
synchronized (right){
System.out.println("張三進入了右側房間!");
}
}
}
public void rightLeft(){
synchronized (right){
System.out.println("李四進入了右側房間!");
synchronized (left){
System.out.println("李四進入了左側房間!");
}
}
}
}
測試
public class ThreadDemo {
public static void main(String[] args) {
MyThread t1 = new MyThread(true);
t1.start();
MyThread t2 = new MyThread(false);
t2.start();
}
}
線程池
線程的生命週期
用戶使用線程的步驟爲
創建線程 => 啓動線程 => 可運行=運行 => 阻塞或銷燬.....
把線程看做是一輛自行車
用戶 想要從家去公司, 想要一輛自行車
買自行車 => 騎自行車 => 銷燬自行車
程序的運行結束 是需要銷燬已經創建的線程對象的, 根據自行車的案例, 現實生活中有共享單車, 可以很好地優化線程的獲取方式
租車 => 用車 => 還車
租用線程 => 使用線程 => 歸還線程
線程池
Executor
Interface Executor
界面提供了一種將任務提交從每個任務的運行機制分解的方式,包括線程使用,調度等的Executor
Executors
static ThreadFactory defaultThreadFactory()
返回用於創建新線程的默認線程工廠。
static ExecutorService newCachedThreadPool()
創建一個根據需要創建新線程的線程池,但在可用時將重新使用以前構造的線程。
static ExecutorService newCachedThreadPool(ThreadFactory threadFactory)
創建一個根據需要創建新線程的線程池,但在可用時將重新使用以前構造的線程,並在需要時使用提供的ThreadFactory創建新線程。
static ExecutorService newFixedThreadPool(int nThreads)
創建一個線程池,該線程池重用固定數量的從共享無界隊列中運行的線程。
static ExecutorService newFixedThreadPool(int nThreads, ThreadFactory threadFactory)
創建一個線程池,重用固定數量的線程,從共享無界隊列中運行,使用提供的ThreadFactory在需要時創建新線程。
static ScheduledExecutorService newScheduledThreadPool(int corePoolSize)
創建一個線程池,可以調度命令在給定的延遲之後運行,或定期執行。
static ScheduledExecutorService newScheduledThreadPool(int corePoolSize, ThreadFactory threadFactory)
創建一個線程池,可以調度命令在給定的延遲之後運行,或定期執行。
案例
public class Demo1_Pool {
public static void main(String[] args) {
Ticket ticket = new Ticket();
// 線程池本質上就是一個容器, 該容器存放的內容是 線程
// 用戶使用線程, 從該容器中自動獲取
ExecutorService pool = Executors.newCachedThreadPool();
pool.submit(ticket);
pool.submit(ticket);
pool.submit(ticket);
}
}
優勢
- 不需要創建線程類 , 直接使用線程池, 實現業務邏輯和線程分離
一個人 開了一個 淘寶網店賣零食, 需要組建自己的快遞團隊嗎?
人負責好自己的主業就可以了, 不要在快遞上分心, 直接把業務打包給快遞公司負責
需要很多輛自行車, 直接找共享單車合作
線程池就是屬於第三方公司
- 使用線程池, 可以節約資源, 提高系統效率
同一輛車 可以提供給多人重複使用
線程之間的通訊問題
之前的賣票案例中 多個線程共同爭搶CPU資源
但是線程之間不存在依賴關係
兩個賣票窗口之間是相互獨立的, 雖然有共享資源存在, 兩者之間不存在先後關係!
通訊問題引入
存在依賴關係的兩個線程之間 比如超市案例 共享同一個超市共享資源的兩個線程 , 供貨商線程 消費者線程, 如果供貨商沒有供貨,消費者將無法消費, 消費者線程依賴於 供貨商線程, 一旦存在依賴關係, 需要線程之間進行通訊
案例資源
共享資源類
public class SharedData {
private char c;
// 是否存在 已經生產完成的字符
private boolean isProduced = false;
// 生產字一個符的方法
public void putShareChar(char c){
this.c = c;
// 修改狀態
isProduced = true;
System.out.println("生產了一個字符! "+c);
}
public char getShareChar(){
// 調用該方法, 是從共享數據中 拿取一個字符
// 把字符取出之後, 修改狀態
isProduced = false;
System.out.println("消費了一個字符"+c);
return c;
}
}
生產者類
public class Producer extends Thread{
private SharedData sd;
public Producer(SharedData sd) {
this.sd = sd;
}
@Override
public void run() {
// 生產者 循環 向倉庫 放入字符, 生產商品放入倉庫
for(char ch = 'A' ; ch <= 'D' ; ch++){
// 時間差
try {
Thread.sleep((long)(Math.random()*3000));
} catch (InterruptedException e) {
e.printStackTrace();
}
sd.putShareChar(ch);
}
}
}
消費者類
public class Consumer extends Thread{
private SharedData sd;
public Consumer(SharedData sd) {
this.sd = sd;
}
@Override
public void run() {
// 消費者去買東西
char ch;
do{
// 時間差
try {
Thread.sleep((long)(Math.random()*3000));
} catch (InterruptedException e) {
e.printStackTrace();
}
ch = sd.getShareChar();
}while(ch != 'D');
}
}
測試類
public class Demo {
public static void main(String[] args) {
// 共享資源對象
SharedData sd = new SharedData();
// 生產者線程
Producer pro = new Producer(sd);
Consumer con = new Consumer(sd);
pro.start();
con.start();
}
}
沒有同步, 沒線程之間也沒有通訊
生產者還未生產, 消費者就已經開始消費了
案例優化
public class SharedData {
private char c;
// 是否存在 已經生產完成的字符
private boolean isProduced = false;
// 生產字一個符的方法
public synchronized void putShareChar(char c) {
if(isProduced){
//
System.out.println("倉庫的商品 還未銷售完畢, 生產者停止生產");
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
this.c = c;
// 修改狀態
isProduced = true;
System.out.println("生產了一個字符! "+c);
this.notify();
}
public synchronized char getShareChar(){
// 只有當 產品有的時候, 纔可以買,
if(!isProduced){
// 沒有的時候只能等
System.out.println("生產者還未生產完畢, 請等待");
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
// 調用該方法, 是從共享數據中 拿取一個字符
// 把字符取出之後, 修改狀態
isProduced = false;
System.out.println("消費了一個字符"+c);
this.notify();
return c;
}
}
wait 和 notify
wait 等待 掛起
需要被喚醒
notify() 喚醒的方法 可以喚醒正在等待的線程
實際上是屬於 Object 類的兩個方法
調用兩個方法的主體是 共享資源對象, 因爲任何類都可以作爲共享資源而存在
wait() 和 sleep() 的區別
wait() 在 線程暫停執行之後 會釋放鎖
sleep() 在線程暫停執行之後, 不會釋放鎖
類似於 銀行提款機,張三在提款機中取錢, 突發疾病 在提款機旁 暈倒
wait() 在暈倒之前把門打開
sleep() 直接暈倒
線程和IO流的綜合案例
使用原始方式複製多文件
package s0805;
import java.io.*;
public class Demo1_copy {
/*
// 1- 判斷 目標文件夾是否存在, 不存在創建
// 2- 創建 源文件 輸入流對象
// 3- 創建 目標文件 輸出流對象
// 4- 使用包裝流 buffered
// 5- 使用 for循環 多次進行文件複製
// 6- 關閉資源
*/
public void copyFiles(String targetDir,String ...files) throws Exception {
// 1- 判斷 目標文件夾是否存在, 不存在創建
File dir = new File(targetDir);
if(!dir.exists()){
dir.mkdirs();
}
// 2- 創建 源文件 輸入流對象
// 使用 for循環
for(String file : files){
File sourceFile = new File(file);
FileInputStream fis = new FileInputStream(sourceFile);
// 3- 創建 目標文件 輸出流對象
File targetFile = new File(targetDir,sourceFile.getName());
FileOutputStream fos = new FileOutputStream(targetFile);
// 4- 使用包裝流 buffered
BufferedInputStream bis = new BufferedInputStream(fis);
BufferedOutputStream bos = new BufferedOutputStream(fos);
// 5- 多次進行文件複製
byte [] bys = new byte[1024];
int len;
// 文件開始複製之前 打印一句話
System.out.println(sourceFile.getName()+"開始複製");
while( (len = bis.read(bys)) != -1){
bos.write(bys,0,len);
}
System.out.println(sourceFile.getName()+"複製完成");
// 6- 關閉資源
bis.close();
bos.close();
}
}
}
測試類
public class Demo2_copytest {
public static void main(String[] args) throws Exception {
Demo1_copy copy = new Demo1_copy();
String dir = "D:\\testCopy";
String f1 = "D:\\55- java 軟件\\Mysql\\mysql-installer-community-5.7.19.0.msi";
String f2 = "D:\\55- java 軟件\\Mysql\\Navicat for MySQL.rar";
String f3 = "D:\\55- java 軟件\\IDEA\\ideaIU-2017.2.6.exe";
copy.copyFiles(dir,f1,f2,f3);
}
}
使用多線程 完成多文件複製案例
需求分析
// 1- 新建線程類, 該類主要負責 一個文件的複製工作
// 2- 線程類需要兩個屬性, 分別表示 源文件, 和目標文件夾
// 3- 在run 方法中 把文件複製的代碼 補充完畢, (注意必須內部處理異常)
// 4- 在測試類中, 多文件複製, 每一個文件都需要開啓一條線程
// 5- 使用 線程池來優化 線程的邏輯和效率
線程類
package s0805.copyThread;
import java.io.*;
/**
* 該線程 主要功能是 每一條線程 負責一個文件的複製拷貝工作
*/
public class CopyRunnable implements Runnable{
// 源文件
private String file;
// 目標文件夾
private String targetDir;
public CopyRunnable(){}
public CopyRunnable(String file, String targetDir) {
this.file = file;
this.targetDir = targetDir;
}
@Override
public void run() {
BufferedInputStream bis = null;
BufferedOutputStream bos = null;
try{
File sourceFile = new File(file);
FileInputStream fis = new FileInputStream(sourceFile);
// 3- 創建 目標文件 輸出流對象
File targetFile = new File(targetDir,sourceFile.getName());
FileOutputStream fos = new FileOutputStream(targetFile);
// 4- 使用包裝流 buffered
bis = new BufferedInputStream(fis);
bos = new BufferedOutputStream(fos);
// 5- 多次進行文件複製
byte [] bys = new byte[1024];
int len;
// 文件開始複製之前 打印一句話
System.out.println(sourceFile.getName()+"開始複製");
while( (len = bis.read(bys)) != -1){
bos.write(bys,0,len);
}
System.out.println(sourceFile.getName()+"複製完成");
}catch(Exception e){
e.printStackTrace();
}finally{
// 6- 關閉資源
if(bis != null){
try {
bis.close();
} catch (IOException e) {
bis = null;
}
}
if(bos != null){
try {
bos.close();
} catch (IOException e) {
bos = null;
}
}
}
}
}
文件複製類
public void copyFiles(String targetDir,String ...files) throws Exception {
// 1- 判斷 目標文件夾是否存在, 不存在創建
File dir = new File(targetDir);
if(!dir.exists()){
dir.mkdirs();
}
// 2- 創建 源文件 輸入流對象
// 使用 for循環
for(String file : files){
// 1- 創建線程對象
CopyRunnable copyRunnable = new CopyRunnable(file, targetDir);
// 2- 使用線程池 來執行多條線程
ExecutorService executorService = Executors.newCachedThreadPool();
executorService.submit(copyRunnable);
}
}
測試類不變