Java實現多執行緒【同步】的三種方式
阿新 • • 發佈:2022-01-07
多執行緒之間對同一共享資源進行操作,容易出現執行緒安全問題,解決方案就是把共享資源加鎖,從而實現執行緒同步,使任意時刻只能有一個執行緒操作共享資源。Java 有 3 種方式可以實現執行緒同步,為了更清晰的描述方案,我以兩個視窗賣火車票為例進行介紹 3 種執行緒同步的方案。本篇部落格目的在於總結 Java 多執行緒同步的知識點,以便在平時工作中用到的時候,可以快速上手。
方案一、採用同步程式碼塊
同步程式碼塊格式:
//需要確保多個執行緒使用的是同一個鎖物件
synchronized (鎖物件) {
多條語句操作共享資料的程式碼
}
程式碼演示:
public class Ticket implements Runnable { //火車票的總數量 private int ticket = 50; //鎖物件 private Object obj = new Object(); @Override public void run() { while (true) { //同步程式碼塊:多個執行緒必須使用同一個鎖物件 synchronized (obj) { if (ticket <= 0) { break; } else { try { Thread.sleep(100); } catch (Exception ex) { ex.printStackTrace(); } ticket = ticket - 1; System.out.println(Thread.currentThread().getName() + "正在賣票,還剩下 " + ticket + " 張票"); } } } } } public class TicketDemo { public static void main(String[] args) { /* 不能採用這種方式,因為這樣相當於每個執行緒使用不同的物件,沒有共享資源 Ticket ticket1 = new Ticket(); Ticket ticket2 = new Ticket(); Thread t1 = new Thread(ticket1); Thread t2 = new Thread(ticket2);*/ //例項化一個物件,讓所有執行緒都使用這一個物件 Ticket ticket = new Ticket(); Thread t1 = new Thread(ticket); Thread t2 = new Thread(ticket); t1.setName("視窗一"); t2.setName("視窗二"); t1.start(); t2.start(); } }
同步程式碼塊:這種實現方案允許一個類中存在多個鎖物件。
如果想讓多個執行緒即使訪問多個不同的程式碼塊,也要統一排隊等待的話,可以讓多個程式碼塊使用同一個鎖物件。
如果想讓多個執行緒訪問不同的程式碼塊互不影響,但是訪問同一個程式碼塊需要排隊等待的話,可以讓多個程式碼塊分別使用不同的鎖物件。
方案二、採用同步方法
同步方法的格式:
//同步方法的鎖物件是其所在類的例項化物件本身 this 修飾符 synchronized 返回值型別 方法名 (方法引數) { 方法體 } //同步靜態方法的鎖物件是其所在的類的 類名.Class 修飾符 static synchronized 返回值型別 方法名 (方法引數) { 方法體 }
同步方法的程式碼演示:
public class Ticket implements Runnable { private static int ticketCount = 50; @Override public void run() { while (true) { //這裡先休眠 100 毫秒,為了讓多個執行緒都有機會搶奪共享資源 try { Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } //使用同步方法 boolean result = synchronizedMthod(); if (result) { break; } } } //同步方法的鎖物件就是 this 本身 private synchronized boolean synchronizedMthod() { if (ticketCount <= 0) { return true; } else { ticketCount = ticketCount - 1; System.out.println(Thread.currentThread().getName() + "正在賣票,還剩下 " + ticketCount + " 張票"); return false; } } } public class TicketDemo { public static void main(String[] args) { //例項化一個物件,讓所有執行緒都使用這一個物件 Ticket ticket = new Ticket(); Thread t1 = new Thread(ticket,"視窗一"); Thread t2 = new Thread(ticket,"視窗二"); t1.start(); t2.start(); } }
同步靜態方法的程式碼演示:
//為了證明同步靜態方法的鎖物件是其所在的類的 類名.Class
//這裡針對兩個視窗執行緒,分別採用不同的同步方式來證明
//視窗一執行緒,採用同步靜態方法
//視窗二執行緒,採用同步程式碼塊,但是使用的是當前類的 類名.Class 作為鎖物件
//最終可以發現【視窗一執行緒】和【視窗二執行緒】能夠實現執行緒同步
public class Ticket implements Runnable {
private static int ticketCount = 50;
@Override
public void run() {
while (true) {
//視窗一執行緒,使用同步靜態方法
if ("視窗一".equals(Thread.currentThread().getName())) {
//同步方法
boolean result = synchronizedMthod();
if (result) {
break;
}
}
//視窗二執行緒,使用同步程式碼塊,但是鎖物件是當前類的 類名.Class
if ("視窗二".equals(Thread.currentThread().getName())) {
//同步程式碼塊
synchronized (Ticket.class) {
if (ticketCount <= 0) {
break;
} else {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
ticketCount--;
System.out.println(Thread.currentThread().getName() +
"正在賣票,還剩下 " + ticketCount + " 張票");
}
}
}
}
}
//同步靜態方法
private static synchronized boolean synchronizedMthod() {
if (ticketCount <= 0) {
return true;
} else {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
ticketCount--;
System.out.println(Thread.currentThread().getName() +
"正在賣票,還剩下 " + ticketCount + " 張票");
return false;
}
}
}
public class TicketDemo {
public static void main(String[] args) {
//例項化一個物件,讓所有執行緒都使用這一個物件
Ticket mr = new Ticket();
Thread t1 = new Thread(mr, "視窗一");
Thread t2 = new Thread(mr, "視窗二");
t1.start();
t2.start();
}
}
同步方法:這種方案會導致同一個例項物件中的所有的同步方法的鎖物件都是 this ,因此多個執行緒即使訪問該例項物件中不同的同步方法時,也必須統一排隊等待。
同步靜態方法:這種方案導致同一個類中所有的同步靜態方法的鎖物件都是當前的 類名.Class ,因此多個執行緒即使訪問該類中不同的同步靜態方法時,也必須統一排隊等待。
方案三、採用 Lock 鎖物件例項
JDK5以後提供了一個新的鎖物件 Lock,但是 Lock 是介面不能直接例項化,因此必須採用它的實現類 ReentrantLock 來實現執行緒同步。ReentrantLock 有兩個方法:
方法名 | 說明 |
---|---|
void lock() | 對多執行緒要訪問的共享資原始碼加鎖 |
void unlock() | 對多執行緒要訪問的共享資原始碼解鎖 |
程式碼演示:
public class Ticket implements Runnable {
private int ticket = 100;
private ReentrantLock lock = new ReentrantLock();
@Override
public void run() {
while (true) {
//這裡先休眠 100 毫秒,為了讓多個執行緒都有機會搶奪共享資源
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
lock.lock(); //加鎖
if (ticket <= 0) {
break;
} else {
ticket--;
System.out.println(Thread.currentThread().getName() +
"正在賣票,還剩下 " + ticket + " 張票");
}
lock.unlock(); //解鎖
}
}
}
public class TicketDemo {
public static void main(String[] args) {
//例項化一個物件,讓所有執行緒都使用這一個物件
Ticket ticket = new Ticket();
Thread t1 = new Thread(ticket,"視窗一");
Thread t2 = new Thread(ticket,"視窗二");
t1.start();
t2.start();
}
}
這種方案,跟同步程式碼塊一樣,一個類中可以存在多個鎖物件。只不過需要自己手動進行加鎖和解鎖。
到此為止,三種執行緒同步的方案已經介紹完畢,每種方案各有優缺點,大家可以根據實際需要,選擇使用不同的方案。