實現同步的幾種方式
1.同步方法
即有synchronized關鍵字修飾的方法;
由於Java的每個物件都有一個內建鎖,當用此關鍵字修飾方法時,內建鎖會保護整個方法。在呼叫該方法之前,需要獲取內建鎖,否則就處於阻塞狀態。
2.同步程式碼塊
即有synchronized關鍵字修飾的語句塊;
程式碼如:
synchronized(object) {
}
注:同步是一種高開銷的操作,因此應該儘量減少同步的內容。通常沒有必要同步整個方法,使用synchronized程式碼塊同步關鍵程式碼即可。
3.使用volatile實現執行緒同步
(1)volatile關鍵字為域變數的訪問提供了一種免鎖機制;
(2)使用volatile修飾域,相當於告訴JVM,該域可能會被其它執行緒更新,因此每次使用該域時就要重新計算,而不是使用暫存器中的值;
(3)volatile不會提供任何原子操作,也不能用來修飾final型別的變數。
例項:如果一個銀行賬戶同時被兩個執行緒操作,一個取100塊,一個存錢100塊。假設賬戶原本有0塊,如果取錢執行緒和存錢執行緒同時發生,會出現什麼結果呢?
程式碼示例:
public class Test {
private volatile static int count = 0; //賬戶餘額
public static void main(String[] args) throws InterruptedException {
new TAdd().start();
new TSub().start();
}
public static void addMoney(int money) { //存錢
synchronized(Test.class) {
count+=money;
}
System.out.println(System.currentTimeMillis() + "存進" + money);
}
public static void subMoney(int money) { //取錢
synchronized(Test.class) {
if(count < money) {
System.out.println("餘額不足");
return;
}
count -= money;
}
System.out.println(System.currentTimeMillis() + "取出" + money);
}
public static void lookMoney() {
System.out.println("賬戶餘額:"+count);
}
static class TAdd extends Thread {
public void run() {
while(true) {
try {
Thread.sleep(1000);
addMoney(100);
lookMoney();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
static class TSub extends Thread {
public void run() {
while(true) {
try {
Thread.sleep(1000);
subMoney(100);
lookMoney();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
執行結果:
1545704383398存進100
1545704383398取出100
賬戶餘額:0
賬戶餘額:0
餘額不足
賬戶餘額:100
1545704384399存進100
賬戶餘額:100(出錯了)
。。。。。。。。
出錯原因是輸出語句並沒有被synchronized同步。另外,volatile不能保證原子性,因此volatile並不能代替synchronized,而在這個例子中,通過synchronized將count包裹起來解決了原子性的問題。volatile的原理是在每次執行緒要訪問volatile修飾的變數時都是從記憶體中讀取,而不是從快取中讀取,因此每個執行緒訪問到的變數值都是一樣的。
4.使用重入鎖實現執行緒同步
在Java5中新增了java.util.concurrent包來支援同步。ReentrantLock類是可重入、互斥、實現了Lock介面的鎖,它與使用synchronized方法和塊具有相同的基本行為和語義,並且擴充套件了其能力。
ReentrantLock類的常用方法有:
ReentrantLock():建立一個ReentrantLock例項;
lock():獲得鎖;
unlock():釋放鎖;
注:ReentrantLock還有一個可以建立公平鎖的構造方法,但由於會大幅降低程式執行效率,不推薦使用。
程式碼示例:
public class Test {
private volatile static int count = 0; //賬戶餘額
private static Lock lock = new ReentrantLock();
public static void main(String[] args) throws InterruptedException {
new TAdd().start();
new TSub().start();
}
public static void addMoney(int money) { //存錢
lock.lock();
try {
count+=money;
System.out.println(System.currentTimeMillis() + "存進" + money);
} finally {
lock.unlock();
}
}
public static void subMoney(int money) { //取錢
lock.lock();
try {
if(count < money) {
System.out.println("餘額不足");
return;
}
count -= money;
System.out.println(System.currentTimeMillis() + "取出" + money);
} finally {
lock.unlock();
}
}
public static void lookMoney() {
System.out.println("賬戶餘額:"+count);
}
static class TAdd extends Thread {
public void run() {
while(true) {
try {
Thread.sleep(1000);
addMoney(100);
lookMoney();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
static class TSub extends Thread {
public void run() {
while(true) {
try {
Thread.sleep(1000);
subMoney(100);
lookMoney();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
執行結果正常。
注:關於Lock物件和synchronized關鍵字的選擇
(1)如果synchronized能滿足使用者需求,就用synchronized,因為它能簡化程式碼;
(2)如果需要更高階的功能,就用ReentrantLock,此時需要注意及時釋放鎖,否則會出現死鎖,通常在finally中釋放鎖。
5.使用ThreadLocal實現執行緒同步
public class Test {
private static ThreadLocal<Integer> count = new ThreadLocal<Integer>(){
@Override
protected Integer initialValue() {
return 0;
}
};
public static void main(String[] args) throws InterruptedException {
new TAdd().start();
new TSub().start();
}
public static void addMoney(int money) { //存錢
count.set(count.get() + money);
System.out.println(System.currentTimeMillis() + "存進" + money);
}
public static void subMoney(int money) { //取錢
if(count.get() < money) {
System.out.println("餘額不足");
return;
}
count.set(count.get() - money);
System.out.println(System.currentTimeMillis() + "取出" + money);
}
public static void lookMoney() {
System.out.println("賬戶餘額:"+count.get());
}
static class TAdd extends Thread {
public void run() {
while(true) {
try {
Thread.sleep(1000);
addMoney(100);
lookMoney();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
static class TSub extends Thread {
public void run() {
while(true) {
try {
Thread.sleep(1000);
subMoney(100);
lookMoney();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
執行結果:
餘額不足
賬戶餘額:0
1545709620876存進100
賬戶餘額:100
餘額不足
賬戶餘額:0
1545709621876存進100
賬戶餘額:200
餘額不足
賬戶餘額:0
1545709622876存進100
賬戶餘額:300
餘額不足
賬戶餘額:0
1545709623878存進100
賬戶餘額:400
如果使用ThreadLocal管理變數,則每個使用該變數的執行緒都獲得該變數的一個副本,副本之間相互獨立,這樣每個執行緒都可以隨意更改自己的變數副本,而不會對其它執行緒產生影響。
ThreadLocal類的常用方法:
ThreadLocal():建立一個執行緒本地變數;
get():返回此執行緒區域性變數的當前執行緒副本中的值;
initialValue():返回此執行緒區域性變數的當前執行緒的初始值;
set(T value):將此執行緒區域性變數的當前執行緒副本中的值設定為value。