Java多線程(四) 解決多線程安全——synchronized
- Java多線程(四) 解決多線程安全——synchronized
-
- synchronized的使用
- synchronized 是重量型鎖
- synchronized 原理和例子
-
- synchronized 作用于執行個體方法
- synchronized 作用于代碼塊
- synchronized作用于靜态方法
- synchronized 可重入鎖
在上一篇文章 《Java多線程(三) 多線程不安全的典型例子》中說到了多線程不安全的問題以及三個典型例子,在這一篇中講解一中保證多線程安全的一種方式——synchronized關鍵字。
synchronized的使用
synchronized相當于給對象上鎖或者給類上鎖,這樣防止其他線程通路共享資源,進而保護多線程的安全。synchronized的原理是它使用了flag标記ACC_SYN-CHRONIZED,執行線程先持有同步鎖,然後執行方法,最後在方法完成時才釋放鎖。
synchronized主要有三種用法:
- 修飾執行個體方法:作用于目前對象執行個體加鎖,進入同步代碼前要獲得目前對象執行個體的鎖。
synchronized void method() {
//業務代碼
}
- 修飾靜态方法:也就是給目前類加鎖,會作用于類的所有對象執行個體 ,進入同步代碼前要獲得目前 class 的鎖。因為靜态成員不屬于任何一個執行個體對象,是類成員( static 表明這是該類的一個靜态資源,不管 new 了多少個對象,隻有一份),是以,如果一個線程 A 調用一個執行個體對象的非靜态 synchronized 方法,而線程 B 需要調用這個執行個體對象所屬類的靜态 synchronized 方法,是允許的,不會發生互斥現象,因為通路靜态 synchronized 方法占用的鎖是目前類的鎖,而通路非靜态 synchronized 方法占用的鎖是目前執行個體對象鎖。
synchronized void staic method() {
//業務代碼
}
- 修飾代碼塊:指定加鎖對象,對給定對象/類加鎖。synchronized(this / object) 表示進入同步代碼庫前要獲得給定對象的鎖。synchronized(類.class) 表示進入同步代碼前要獲得目前 class 的鎖
synchronized(this) {
//業務代碼
}
總的來說:
synchronized 關鍵字加到 static 靜态方法和 synchronized(class) 代碼塊上都是是給 Class 類上鎖。
synchronized 關鍵字加到執行個體方法上是給對象執行個體上鎖。
synchronized 是重量型鎖
Synchronized是通過對象内部的一個叫做螢幕鎖(monitor)來實作的。但是螢幕鎖本質又是依賴于底層的作業系統的Mutex Lock來實作的。而作業系統實作線程之間的切換這就需要從使用者态轉換到核心态,這個成本非常高,狀态之間的轉換需要相對比較長的時間,這就是為什麼Synchronized效率低的原因。是以,這種依賴于作業系統Mutex Lock所實作的鎖我們稱之為“重量級鎖”。
synchronized 原理和例子
在 Java 中,synchronized可以保證在同一個時刻,隻有一個線程可以執行某個方法或者某個代碼塊(主要是對方法或者代碼塊中存在共享資料的操作),此外synchronized的另外一個重要的作用是synchronized可保證一個線程的變化(主要是共享資料的變化)被其他線程所看到(保證可見性,完全可以替代Volatile功能),這點确實也是很重要的。
synchronized 作用于執行個體方法
第一個例子舉一個很簡單的對一個類的共享資源加一的操作,為了保證多線程安全,使用synchronized修飾increase()。對于增加i值來說,他并不是一個原子操作,因為第一步要讀取i值,第二步要對他進行加一操作,是以需要進行互斥操作。synchronized修飾的是執行個體方法increase,在這樣的情況下,目前線程的鎖便是執行個體對象instance。當一個線程正在通路一個對象的 synchronized 執行個體方法,那麼其他線程不能通路該對象的所有 synchronized 方法,因為一個對象隻有一把鎖,當一個線程擷取了該對象的鎖之後,其他線程無法擷取該對象的鎖,是以無法通路該對象的synchronized執行個體方法,這樣的方式也就保護了多線程的安全,不過其他線程還是可以通路該執行個體對象的其他非synchronized方法。
public class syntest {
public static void main(String[] args) throws InterruptedException {
AccountingSync instance=new AccountingSync();
Thread t1=new Thread(instance);
Thread t2=new Thread(instance);
t1.start();
t2.start();
t1.join();
t2.join();
}
}
class AccountingSync implements Runnable{
static int i=0;
public synchronized void increase() throws InterruptedException {
i++;
Thread.sleep(300);
System.out.println(Thread.currentThread().getName()+"增加了i值,它的值為 "+i);
}
@Override
public void run() {
for(int j=0;j<100;j++){
try {
increase();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
(省略中間的)
synchronized 作用于代碼塊
這裡舉的例子是上一篇《Java多線程(三) 多線程不安全的典型例子》的第一個不安全的買票例子,這裡依舊是三個人買票,一共三十張票,不同的是這次使用synchronized對買票的代碼塊進行上鎖。當編寫的方法體比較大時,同時存在一些比較耗時的操作,而需要同步的代碼又隻有一小部分,如果直接對整個方法進行同步操作,可能會得不償失,此時我們可以使用同步代碼塊的方式對需要同步的代碼進行包裹,這樣就無需對整個方法進行同步操作了。将synchronized作用于一個給定的執行個體對象t,即目前執行個體對象就是鎖對象,每次當線程進入synchronized包裹的代碼塊時就會要求目前線程持有instance執行個體對象鎖,如果目前有其他線程正持有該對象鎖,那麼新到的線程就必須等待。當然除了t作為對象外,我們還可以使用this對象(代表目前執行個體)或者目前類的class對象作為鎖。
class Ticket implements Runnable{
private int alltickets = 30;
private boolean flag = true;
@Override
public void run() {
while(alltickets>0) {
synchronized (this) {
try {
Thread.sleep(300);
buy();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
public void buy() throws InterruptedException {
if(this.alltickets<=0)
{
System.out.println("沒票可買了"+Thread.currentThread().getName());
this.flag = false;
return;
}
else
{
Thread.sleep(100);
System.out.println(Thread.currentThread().getName()+"買了第"+this.alltickets--+"張票 ");
}
}
}
public class testThread {
public static void main(String[] args) throws InterruptedException, ExecutionException {
Ticket t = new Ticket();
new Thread(t, "小華同學").start();
new Thread(t, "小明同學").start();
new Thread(t, "黃牛").start();
}
可以看到這次買票過程是有序的,并且沒有出現買到重複票的問題
synchronized作用于靜态方法
當synchronized作用于靜态方法時,其鎖就是目前類的class對象鎖。由于靜态成員不專屬于任何一個執行個體對象,是類成員,是以通過class對象鎖可以控制靜态成員的并發操作。需要注意的是如果一個線程A調用一個執行個體對象的非static的synchronized方法,而線程B需要調用這個執行個體對象所屬類的靜态 synchronized方法,是允許的,不會發生互斥現象,因為通路靜态 synchronized 方法占用的鎖是目前類的class對象,而通路非靜态 synchronized 方法占用的鎖是目前執行個體對象鎖。synchronized關鍵字修飾的是靜态方法,其鎖對象是目前類的class對象。注意代碼中的increase2方法是執行個體方法,其對象鎖是目前執行個體對象,如果别的線程調用該方法,将不會産生互斥現象,畢竟鎖對象不同,但我們應該意識到這種情況下可能會發現線程安全問題(操作了共享靜态變量i)。
public class syntest {
public static void main(String[] args) throws InterruptedException {
Thread t1=new Thread(new StaticTest());
Thread t2=new Thread(new StaticTest());
t1.start();t2.start();
}
}
class StaticTest implements Runnable{
static int i=0;
/**
* 作用于靜态方法,鎖是目前class對象,也就是StaticTest類對應的class對象
*/
public static synchronized void increase() throws InterruptedException {
i++;
Thread.sleep(300);
System.out.println(Thread.currentThread().getName()+"增加了i值,它的值為 "+i);
}
/**
* 非靜态,通路時鎖不一樣不會發生互斥
*/
public synchronized void increase2(){
i++;
}
@Override
public void run() {
for(int j=0;j<100;j++){
try {
increase();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
synchronized 可重入鎖
關鍵字synchronized擁有重入鎖的功能,即在使用synchronized時,當一個線程得到了一個對象鎖後,再次請求此對象鎖時是可以得到該對象鎖的,意思是在一個synchronized方法或者代碼塊中,調用這個類的其他synchronized方法或者代碼塊時是可以做到的。
public class syntest {
public static void main(String[] args) throws InterruptedException {
Thread t1=new Thread(new StaticTest());
t1.start();
}
}
class StaticTest implements Runnable{
public synchronized void method1()
{
System.out.println("method1");
method2();
}
public synchronized void method2()
{
System.out.println("method2");
method3();
}
public synchronized void method3()
{
System.out.println("method3");
}
@Override
public void run() {
method1();
}
}