文章目錄
線程安全的概述
多線程訪問了共享數據,會產生線程安全的問題
只有多線程會產生安全問題
線程安全產生的原理
線程安全解決方法
第一種方法——同步代碼塊
/*
賣票案例出現了線程安全問題,邁出了不存在的票和重複的票
解決線程安全的一種方式:使用同步代碼塊
格式:
synchronized(鎖對象){
可能會出現線程安全的代碼(訪問了共享數據的代碼)
}
注意:
1. 同步代碼塊中的鎖對象,可以是任意的對象
2. 必須保證多個線程使用的鎖對象是同一個
3. 鎖對象作用:
把同步代碼塊所著,只讓一個線程在同步代碼塊中執行
*/
public class RunnableImpl implements Runnable {
// 定義一個多個線程共享的票源
private int ticket = 100;
// 創建一個鎖對象,必須創建在run的外面
Object obj = new Object();
@Override
public void run() {
while(true){
synchronized (obj){
if (ticket>0){
// 提高安全問題出現的概率,讓程序睡眠
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 票存在,賣票
System.out.println(Thread.currentThread().getName()+"-->正在賣第"+ticket+"張票");
ticket--;
}
}
}
}
}
/*
模擬買票
創建三個線程,同時開啓,對共享的票進行出售
*/
public class Demo01Ticket {
public static void main(String[] args) {
RunnableImpl run = new RunnableImpl();
Thread t1 = new Thread(run);
Thread t2 = new Thread(run);
Thread t3 = new Thread(run);
t1.start();
t2.start();
t3.start();
}
}
同步代碼塊的原理
第二種方法——同步方法
/*
第二種方式:使用同步方法
使用步驟:
1. 把訪問了共享數據的代碼取出來,放到一個方法中
2. 在方法上添加synchronized修飾符
格式:定義方法的格式
修飾符 synchronized 返回值類型 方法名(參數列表){
可能會出現線程安全的代碼(訪問了共享數據的代碼)
}
*/
public class RunnableImpl implements Runnable {
// 定義一個多個線程共享的票源
private int ticket = 100;
// 創建一個鎖對象,必須創建在run的外面
Object obj = new Object();
@Override
public void run() {
while(true){
payTicket();
}
}
// 定義一個同步方法
public synchronized void payTicket(){
if (ticket>0) {
// 提高安全問題出現的概率,讓程序睡眠
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 票存在,賣票
System.out.println(Thread.currentThread().getName() + "-->正在賣第" + ticket + "張票");
ticket--;
}
}
}
同步方法也會把方法內部的代碼鎖住,只讓一個線程執行
同步方法的鎖對象是誰? 就是實現類對象new Runnable(),也就是this
第三種方法——靜態同步方法(瞭解)
public class RunnableImpl implements Runnable {
// 定義一個多個線程共享的票源
private static int ticket = 100;//靜態同步方法訪問的變量也必須是靜態的
// 創建一個鎖對象,必須創建在run的外面
Object obj = new Object();
@Override
public void run() {
while(true){
payTicketStatic();
}
}
/*
靜態同步方法的鎖對象是誰?
不能是this,this是創建對象之後產生的,靜態方法優先於對象,
靜態方法的鎖對象是本類的class屬性——class文件對象(反射)
*/
public static synchronized void payTicketStatic(){
if (ticket>0) {
// 提高安全問題出現的概率,讓程序睡眠
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 票存在,賣票
System.out.println(Thread.currentThread().getName() + "-->正在賣第" + ticket + "張票");
ticket--;
}
}
}
第四種方法——Lock鎖
/*
解決線程安全的第四種解決方案:Lock鎖
java.util.concurrent.locks
Lock實現提供了比使用synchronized方法和語句可獲得更廣泛的鎖定操作
Lock接口種的方法:
void lock() 獲取鎖。
void unlock() 釋放鎖。
java.util.concurrent.locks.ReentrantLock implements Lock接口
使用步驟:
1. 在成員位置創建一個ReentrantLock對象
2. 在可能會出現安全問題的代碼前調用Lock接口種的方法Lock獲取鎖
3. 在可能會出現安全問題的代碼後調用Lock接口種的方法unLock釋放鎖
*/
public class RunnableImpl implements Runnable {
// 定義一個多個線程共享的票源
private int ticket = 100;
// 1. 在成員位置創建一個ReentrantLock對象
Lock l = new ReentrantLock();
@Override
public void run() {
while(true){
// 2. 在可能會出現安全問題的代碼前調用Lock接口種的方法Lock獲取鎖
l.lock();
if (ticket>0) {
// 提高安全問題出現的概率,讓程序睡眠
try {
Thread.sleep(10);
// 票存在,賣票
System.out.println(Thread.currentThread().getName() + "-->正在賣第" + ticket + "張票");
ticket--;
} catch (InterruptedException e) {
e.printStackTrace();
}finally {
// 無論程序是否異常,否會把鎖釋放
//3. 在可能會出現安全問題的代碼後調用Lock接口種的方法unLock釋放鎖
l.unlock();
}
}
}
}
}
線程狀態
TimeWaiting線程狀態圖
Blocked狀態
Waiting(無限等待)
等待喚醒案例代碼實現
/*
等待喚醒案例:線程之間的通信
創建一個顧客線程(消費者):告知老闆要的包子的種類和數量,調用wait方法,進入到WAITING狀態(無限等待)
創建一個老闆線程(生產者):花了5秒做包子,做好包子後,調用notify方法,喚醒顧客喫包子
注意:
顧客和老闆線程必須使用同步代碼塊包裹起來,保證等待和喚醒只能由一個在執行
同步使用的鎖對象必須保證唯一
只有鎖對象才能調用wait和notify方法
Object類種的方法
void wait()
在其他線程調用此對象的 notify() 方法或 notifyAll() 方法前,導致當前線程等待。
void notify()
喚醒在此對象監視器上等待的單個線程。
*/
public class Demo01WaitAndNotify {
public static void main(String[] args) {
Object obj = new Object();
// 顧客的線程
new Thread(){
@Override
public void run() {
// 保證等待和喚醒的線程只能有一個執行,需要使用同步技術
synchronized (obj){
System.out.println("告知老闆要的包子的種類和數量");
try {
obj.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
// 喚醒之後執行的代碼
System.out.println("開喫");
}
}
}.start();
// 老闆的線程
new Thread(){
@Override
public void run() {
try {
Thread.sleep(5000);//畫5s做包子
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (obj){
System.out.println("告知顧客,可以喫包子");
obj.notify();
}
}
}.start();
}
}
進入到TimeWaiting(計時等待)有兩種方式
- 使用sleep(long m)方法,在毫秒值結束之後,線程睡醒進入到Runnable/Blocked狀態
- 使用wait(long m)方法如果在毫秒值結束後還沒有被notify喚醒,就會自動醒來,線程睡醒進入到Runnable/Blocked狀態
喚醒方法:
void notify() 喚醒在此對象監視器上等待的單個線程。有多個等待線程時隨機喚醒一個
void notifyAll() 喚醒在此對象監視器上等待的所有線程。
線程間通信——等待喚醒機制
線程池
頻繁創建線程和銷燬線程需要時間
/*
線程池:JDK1.5之後提供的
java.util.concurrent.Executors:線程池的工廠類,用來生成線程池
Executors類種的靜態方法
static ExecutorService newFixedThreadPool(int nThreads) 創建一個可重用固定線程數的線程池,以共享的無界隊列方式來運行這些線程。
返回值:
ExecutorService接口:返回的是ExecutorService接口的實現類對象,我們可以使用ExecutorService接口接收(面向接口編程)
java.util.concurrent.ExecutorService:線程池接口
用來從線程池獲取線程,調用start方法,執行線程任務
submit(Runnable task) 提交一個 Runnable 任務用於執行,並返回一個表示該任務的 Future。
關閉線程池的方法:
void shutdown() 啓動一次順序關閉,執行以前提交的任務,但不接受新任務。
線程池的使用步驟:
1. 使用線程池的工廠類Executors裏面提供的newFixedThreadPool生產一個指定線程數量的線程池
2. 創建一個類,實現Runnable接口,重寫run方法,設置線程任務
3. 調用ExecutorService中的submit,傳遞線程任務(實現類),開啓線程,執行run方法
4. 調用ExecutorService中的shutdown,銷燬線程池(不建議使用)
*/
public class Demo01ThreadPool {
public static void main(String[] args) {
ExecutorService es = Executors.newFixedThreadPool(2);
es.submit(new RunnableImpl());//pool-1-thread-1創建了一個新的線程執行
// 線程池會一直開啓,會自動把線程歸還給線程池
es.submit(new RunnableImpl());//pool-1-thread-2創建了一個新的線程執行
es.submit(new RunnableImpl());//pool-1-thread-2創建了一個新的線程執行
}
}