多線程之間對同一共享資源進行操作,容易出現線程安全問題,解決方案就是把共享資源加鎖,從而實現線程同步,使任意時刻只能有一個線程操作共享資源。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();
}
}
這種方案,跟同步代碼塊一樣,一個類中可以存在多個鎖對象。只不過需要自己手動進行加鎖和解鎖。
到此為止,三種線程同步的方案已經介紹完畢,每種方案各有優缺點,大家可以根據實際需要,選擇使用不同的方案。
