Java 對象鎖和類鎖全面解析
最近工作有用到一些多線程的東西,之前吧,有用到synchronized同步塊,不過是別人怎麼用就跟著用,並沒有搞清楚鎖的概念。最近也是遇到一些問題,不搞清楚鎖的概念,很容易碰壁,甚至有些時候自己連用沒用對都不知道。
今天把一些疑惑都解開了,寫篇文章分享給大家,文章還算比較全面。當然可能有小寶鴿理解得不夠深入透徹的地方,如果說得不正確還望指出。
看之前有必要跟某些猿友說一下,如果看一遍沒有看明白呢,也沒關係,當是了解一下,等真正使用到了,再回頭看。
本文主要是將synchronized關鍵字用法作為例子來去解釋Java中的對象鎖和類鎖。特別的是希望能幫大家理清一些概念。
一、synchronized關鍵字
synchronized關鍵字有如下兩種用法:
1、 在需要同步的方法的方法簽名中加入synchronized關鍵字。
synchronized public void getValue() {
+ Thread.currentThread().getName() + " username=" + username
+ " password=" + password);
}
上面的代碼修飾的synchronized是非靜態方法,如果修飾的是靜態方法(static)含義是完全不一樣的。具體不一樣在哪裡,後面會詳細說清楚。
synchronized static public void getValue() {
+ Thread.currentThread().getName() + " username=" + username
+ " password=" + password);
}
2、使用synchronized塊對需要進行同步的代碼段進行同步。
public void serviceMethod() {
try {
synchronized (this) {
Thread.sleep(2000);
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
上面的代碼塊是synchronized (this)用法,還有synchronized (非this對象)以及synchronized (類.class)這兩種用法,這些使用方式的含義也是有根本的區別的。我們先帶著這些問題繼續往下看。
二、Java中的對象鎖和類鎖
小寶鴿似乎並沒有辦法用清晰簡短的語言來描述對象鎖和類鎖的概念。即便能用簡單的語句概況,也會顯得抽象。猿友們耐心看完自然會明白。
之前網上有找一些相關資料,有篇博客是這樣描述的(看的是轉載的,原創連接我也不知道):
一段synchronized的代碼被一個線程執行之前,他要先拿到執行這段代碼的許可權,
在Java裡邊就是拿到某個同步對象的鎖(一個對象只有一把鎖);
如果這個時候同步對象的鎖被其他線程拿走了,他(這個線程)就只能等了(線程阻塞在鎖池等待隊列中)。
取到鎖後,他就開始執行同步代碼(被synchronized修飾的代碼);
線程執行完同步代碼後馬上就把鎖還給同步對象,其他在鎖池中等待的某個線程就可以拿到鎖執行同步代碼了。
這樣就保證了同步代碼在統一時刻只有一個線程在執行。
這段話,除了最後一句,講得都是挺合理的。」這樣就保證了同步代碼在統一時刻只有一個線程在執行。」這句話顯然不對,synchronized並非保證同步代碼同一時刻只有一個線程執行,同步代碼同一時刻應該可以有多個線程執行。
上面提到鎖,這裡先引出鎖的概念。先來看看下面這些啰嗦而必不可少的文字。
多線程的線程同步機制實際上是靠鎖的概念來控制的。
在Java程序運行時環境中,JVM需要對兩類線程共享的數據進行協調:
1)保存在堆中的實例變數
2)保存在方法區中的類變數
這兩類數據是被所有線程共享的。
(程序不需要協調保存在Java 棧當中的數據。因為這些數據是屬於擁有該棧的線程所私有的。)
這裡插播一下廣告:關於JVM內存,如果想了解可以看看博主的另外一篇文章:
方法區(Method Area)與Java堆一樣,是各個線程共享的內存區域,它用於存儲已被虛擬機載入的類信息、常量、靜態變數、即時編譯器編譯後的代碼等數據。雖然Java虛擬機規範把方法區描述為堆的一個邏輯部分,但是它卻有一個別名叫做Non-Heap(非堆),目的應該是與Java堆區分開來。
棧:在Java中,JVM中的棧記錄了線程的方法調用。每個線程擁有一個棧。在某個線程的運行過程中,如果有新的方法調用,那麼該線程對應的棧就會增加一個存儲單元,即幀(frame)。在frame中,保存有該方法調用的參數、局部變數和返回地址。
堆是JVM中一塊可自由分配給對象的區域。當我們談論垃圾回收(garbage collection)時,我們主要回收堆(heap)的空間。
Java的普通對象存活在堆中。與棧不同,堆的空間不會隨著方法調用結束而清空。因此,在某個方法中創建的對象,可以在方法調用結束之後,繼續存在於堆中。這帶來的一個問題是,如果我們不斷的創建新的對象,內存空間將最終消耗殆盡。
在java虛擬機中,每個對象和類在邏輯上都是和一個監視器相關聯的。
對於對象來說,相關聯的監視器保護對象的實例變數。
對於類來說,監視器保護類的類變數。
(如果一個對象沒有實例變數,或者一個類沒有變數,相關聯的監視器就什麼也不監視。)
為了實現監視器的排他性監視能力,java虛擬機為每一個對象和類都關聯一個鎖。代表任何時候只允許一個線程擁有的特權。線程訪問實例變數或者類變數不需鎖。
但是如果線程獲取了鎖,那麼在它釋放這個鎖之前,就沒有其他線程可以獲取同樣數據的鎖了。(鎖住一個對象就是獲取對象相關聯的監視器)
類鎖實際上用對象鎖來實現。當虛擬機裝載一個class文件的時候,它就會創建一個java.lang.Class類的實例。當鎖住一個對象的時候,實際上鎖住的是那個類的Class對象。
一個線程可以多次對同一個對象上鎖。對於每一個對象,java虛擬機維護一個加鎖計數器,線程每獲得一次該對象,計數器就加1,每釋放一次,計數器就減 1,當計數器值為0時,鎖就被完全釋放了。
java編程人員不需要自己動手加鎖,對象鎖是java虛擬機內部使用的。
在java程序中,只需要使用synchronized塊或者synchronized方法就可以標誌一個監視區域。當每次進入一個監視區域時,java 虛擬機都會自動鎖上對象或者類。
三、synchronized關鍵字各種用法與實例
看完了」二、Java中的對象鎖和類鎖」,我們再來結合」一、synchronized關鍵字」裡面提到的synchronized用法。
事實上,synchronized修飾非靜態方法、同步代碼塊的synchronized (this)用法和synchronized (非this對象)的用法鎖的是對象,線程想要執行對應同步代碼,需要獲得對象鎖。
synchronized修飾非靜態方法以及同步代碼塊的synchronized (類.class)用法鎖的是類,線程想要執行對應同步代碼,需要獲得類鎖。
因此,事實上synchronized關鍵字可以細分為上面描述的五種用法。
本文的實例均來自於《Java多線程編程核心技術》這本書裡面的例子。
1、我們先看看非線程安全實例(Run.java):
public class Run {
public static void main(String[] args) {
HasSelfPrivateNum numRef = new HasSelfPrivateNum();
ThreadA athread = new ThreadA(numRef);
athread.start();
ThreadB bthread = new ThreadB(numRef);
bthread.start();
}
}
class HasSelfPrivateNum {
private int num = 0;
public void addI(String username) {
try {
if (username.equals("a")) {
num = 100;
Thread.sleep(2000);
} else {
num = 200;
}
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
class ThreadA extends Thread {
private HasSelfPrivateNum numRef;
public ThreadA(HasSelfPrivateNum numRef) {
super();
this.numRef = numRef;
}
@Override
public void run() {
super.run();
numRef.addI("a");
}
}
class ThreadB extends Thread {
private HasSelfPrivateNum numRef;
public ThreadB(HasSelfPrivateNum numRef) {
super();
this.numRef = numRef;
}
@Override
public void run() {
super.run();
numRef.addI("b");
}
}
運行結果為:
a set over!
b set over!
b num=200
a num=200
修改HasSelfPrivateNum如下,方法用synchronized修飾如下:
class HasSelfPrivateNum {
private int num = 0;
synchronized public void addI(String username) {
try {
if (username.equals("a")) {
num = 100;
Thread.sleep(2000);
} else {
num = 200;
}
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
運行結果是線程安全的:
b set over!
b num=200
a set over!
a num=100
實驗結論:兩個線程訪問同一個對象中的同步方法是一定是線程安全的。本實現由於是同步訪問,所以先列印出a,然後列印出b
這裡線程獲取的是HasSelfPrivateNum的對象實例的鎖——對象鎖。
2、多個對象多個鎖
就上面的實例,我們將Run改成如下:
public class Run {
public static void main(String[] args) {
HasSelfPrivateNum numRef1 = new HasSelfPrivateNum();
HasSelfPrivateNum numRef2 = new HasSelfPrivateNum();
ThreadA athread = new ThreadA(numRef1);
athread.start();
ThreadB bthread = new ThreadB(numRef2);
bthread.start();
}
}
運行結果為:
a set over!
b set over!
b num=200
a num=200
這裡是非同步的,因為線程athread獲得是numRef1的對象鎖,而bthread線程獲取的是numRef2的對象鎖,他們並沒有在獲取鎖上有競爭關係,因此,出現非同步的結果
這裡插播一下:同步不具有繼承性
3、同步塊synchronized (this)
我們先看看代碼實例(Run.java)
public class Run {
public static void main(String[] args) {
ObjectService service = new ObjectService();
ThreadA a = new ThreadA(service);
a.setName("a");
a.start();
ThreadB b = new ThreadB(service);
b.setName("b");
b.start();
}
}
class ObjectService {
public void serviceMethod() {
try {
synchronized (this) {
Thread.sleep(2000);
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
class ThreadA extends Thread {
private ObjectService service;
public ThreadA(ObjectService service) {
super();
this.service = service;
}
@Override
public void run() {
super.run();
service.serviceMethod();
}
}
class ThreadB extends Thread {
private ObjectService service;
public ThreadB(ObjectService service) {
super();
this.service = service;
}
@Override
public void run() {
super.run();
service.serviceMethod();
}
}
運行結果:
這樣也是同步的,線程獲取的是同步塊synchronized (this)括弧()裡面的對象實例的對象鎖,這裡就是ObjectService實例對象的對象鎖了。
需要注意的是synchronized (){}的{}前後的代碼依舊是非同步的
4、synchronized (非this對象)
我們先看看代碼實例(Run.java)
public class Run {
public static void main(String[] args) {
Service service = new Service("xiaobaoge");
ThreadA a = new ThreadA(service);
a.setName("A");
a.start();
ThreadB b = new ThreadB(service);
b.setName("B");
b.start();
}
}
class Service {
String anyString = new String();
public Service(String anyString){
this.anyString = anyString;
}
public void setUsernamePassword(String username, String password) {
try {
synchronized (anyString) {
+ "在" + System.currentTimeMillis() + "進入同步塊");
Thread.sleep(3000);
+ "在" + System.currentTimeMillis() + "離開同步塊");
}
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
class ThreadA extends Thread {
private Service service;
public ThreadA(Service service) {
super();
this.service = service;
}
@Override
public void run() {
service.setUsernamePassword("a", "aa");
}
}
class ThreadB extends Thread {
private Service service;
public ThreadB(Service service) {
super();
this.service = service;
}
@Override
public void run() {
service.setUsernamePassword("b", "bb");
}
}
不難看出,這裡線程爭奪的是anyString的對象鎖,兩個線程有競爭同一對象鎖的關係,出現同步
現在有一個問題:一個類裡面有兩個非靜態同步方法,會有影響么?
答案是:如果對象實例A,線程1獲得了對象A的對象鎖,那麼其他線程就不能進入需要獲得對象實例A的對象鎖才能訪問的同步代碼(包括同步方法和同步塊)。不理解可以細細品味一下!
5、靜態synchronized同步方法
我們直接看代碼實例:
public class Run {
public static void main(String[] args) {
ThreadA a = new ThreadA();
a.setName("A");
a.start();
ThreadB b = new ThreadB();
b.setName("B");
b.start();
}
}
class Service {
synchronized public static void printA() {
try {
+ "在" + System.currentTimeMillis() + "進入printA");
Thread.sleep(3000);
+ "在" + System.currentTimeMillis() + "離開printA");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
synchronized public static void printB() {
+ System.currentTimeMillis() + "進入printB");
+ System.currentTimeMillis() + "離開printB");
}
}
class ThreadA extends Thread {
@Override
public void run() {
Service.printA();
}
}
class ThreadB extends Thread {
@Override
public void run() {
Service.printB();
}
}
運行結果:
兩個線程在爭奪同一個類鎖,因此同步
6、synchronized (class)
對上面Service類代碼修改成如下:
class Service {
public static void printA() {
synchronized (Service.class) {
try {
+ "在" + System.currentTimeMillis() + "進入printA");
Thread.sleep(3000);
+ "在" + System.currentTimeMillis() + "離開printA");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public static void printB() {
synchronized (Service.class) {
+ "在" + System.currentTimeMillis() + "進入printB");
+ "在" + System.currentTimeMillis() + "離開printB");
}
}
}
運行結果:
兩個線程依舊在爭奪同一個類鎖,因此同步
需要特別說明:對於同一個類A,線程1爭奪A對象實例的對象鎖,線程2爭奪類A的類鎖,這兩者不存在競爭關係。也就說對象鎖和類鎖互補干預內政
靜態方法則一定會同步,非靜態方法需在單例模式才生效,但是也不能都用靜態同步方法,總之用得不好可能會給性能帶來極大的影響。另外,有必要說一下的是Spring的bean默認是單例的。
想要系統學習Java知識 加入學習群一四四九零一零七六 可以免費學習java還有大量學習乾貨哦
※歡樂頌五美的衛浴間,哪個入你眼?
※C語言必背18個經典程序
※程序員必須投資的十件事,你忽略了哪些?
※Java內存模型-volatile
TAG:IT技術java交流 |
※AI發威!或能幫人類鎖定第一個地外人類家園
※潛艇對抗魚類鎖定的聲誘餌都有哪些?答案揭曉:四種類型一一盤點
※在遠離地球的宇宙空間如何定位?人類鎖定更強大的天體
※小黑熊被人類鎖住10多年,最近它命運出現轉機,動容!