JAVA基礎-synchronized關鍵字
synchronized關鍵字也叫作互斥鎖或者同步。
這個關鍵字的存在是爲了解決編程中的線程安全問題的,而線程安全問題出現的主要原因一般爲:多個線程操作同一個對象的數據,也就是同時操作共享變量的值。
synchronized的出現解決了這個問題,互斥鎖的含義爲,當一個線程操作一個對象的時候,對該對象增加一個鎖,任何其他線程都處在等待狀態,不可以對該對象進行操作。當持有鎖的線程執行完畢後,會釋放持有鎖,其他等待線程共同競爭鎖資源。
同時,synchronized關鍵字還可以保證線程的變化對其他線程可見,保證共享變量的可見性,也就是volatile功能。
synchronized關鍵字的應用
1、修飾實例方法:對當前實例對象加鎖,執行同步代碼前獲取當前實例對象的控制權。
2、修飾靜態方法:對當前類對象加鎖,執行同步代碼前獲取當前類對象的控制權。
3、修飾代碼塊:對任意指定對象加鎖,執行同步代碼前獲取指定對象的控制權。
修飾實例方法:
/**
* synchronized
*
* Created by j on 2018/4/26.
*/
public class SynchronizedTest {
static int i = 0;
public synchronized void add(){
i++;
}
public static void main(String[] args) throws InterruptedException {
SynchronizedTest synchronizedTest = new SynchronizedTest();
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
for (int i = 0; i < 100000; i++ ){
synchronizedTest.add();
}
}
});
Thread thread1 = new Thread(new Runnable() {
@Override
public void run() {
for (int i = 0; i < 100000; i++ ){
synchronizedTest.add();
}
}
});
thread.start();
thread1.start();
// 確保線程執行完畢
while (Thread.activeCount() > 2) {
Thread.yield();
}
System.out.println(i);
}
}
上述代碼可以看到,新建了兩個線程分別調用同步方法add() 由於兩個線程鎖的都是synchronizedTest對象,所以可以保證線程運行完畢,i的值爲200000。得出結論修飾實例方法是對實例對象進行加鎖。
接下來看一段代碼:
/**
* synchronized
*
* Created by j on 2018/4/26.
*/
public class SynchronizedTest {
static int i = 0;
public synchronized void add(){
i++;
}
public static void main(String[] args) throws InterruptedException {
SynchronizedTest synchronizedTest = new SynchronizedTest();
SynchronizedTest synchronizedTest1 = new SynchronizedTest();
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
for (int i = 0; i < 100000; i++ ){
synchronizedTest.add();
}
}
});
Thread thread1 = new Thread(new Runnable() {
@Override
public void run() {
for (int i = 0; i < 100000; i++ ){
synchronizedTest1.add();
}
}
});
thread.start();
thread1.start();
// 確保線程執行完畢
while (Thread.activeCount() > 2) {
Thread.yield();
}
System.out.println(i);
}
}
對之前的代碼做了輕微的改動,兩個線程分別對兩個實例對象加鎖,這時當線程運行結束,結果不一定會是200000,得到了198478結果,說明兩個線程用的是不同的鎖,無法保證線程安全。
修飾靜態方法:
/**
* synchronized
*
* Created by j on 2018/4/26.
*/
public class SynchronizedTest {
static int i = 0;
public static synchronized void add(){
i++;
}
public static void main(String[] args) throws InterruptedException {
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
for (int i = 0; i < 100000; i++ ){
add();
}
}
});
Thread thread1 = new Thread(new Runnable() {
@Override
public void run() {
for (int i = 0; i < 100000; i++ ){
add();
}
}
});
thread.start();
thread1.start();
// 確保線程執行完畢
while (Thread.activeCount() > 2) {
Thread.yield();
}
System.out.println(i);
}
}
同步方法添加了static關鍵字,說明對當前類對象加鎖。靜態方法不屬於任何一個實例。
但是如果一個線程A調用靜態同步方法,另一個線程B調用實例同步方法,並且對同一個變量進行操作的時候,會發生線程安全問題,因爲:靜態方法鎖的是類對象,實例方法鎖的是實例對象,是兩個不同的鎖。
代碼舉例:
/**
* synchronized
*
* Created by j on 2018/4/26.
*/
public class SynchronizedTest {
static int i = 0;
public static synchronized void add(){
i++;
}
public synchronized void add1(){
i++;
}
public static void main(String[] args) throws InterruptedException {
SynchronizedTest test = new SynchronizedTest();
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
for (int i = 0; i < 100000; i++ ){
test.add1();
}
}
});
Thread thread1 = new Thread(new Runnable() {
@Override
public void run() {
for (int i = 0; i < 100000; i++ ){
add();
}
}
});
thread.start();
thread1.start();
// 確保線程執行完畢
while (Thread.activeCount() > 2) {
Thread.yield();
}
System.out.println(i);
}
}
修飾代碼塊:
public void add(){
synchronized (SynchronizedTest.class){
i++;
}
}
public void add1(){
synchronized (this){
i++;
}
}
當只需要對一部分代碼進行同步操作時,可以用synchronized修飾代碼片段,鎖定的對象可以是實例鍍錫或者this當前實例對象,也可以是XX.class類對象。
synchronized重入性
synchronized是給對象添加一個互斥鎖,當一個線程持有對象鎖時,其他操作該對象的線程將處於阻塞狀態。但是當一個線程持有對象鎖,然後再次請求自己持有對象鎖的臨界資源時,就是重入鎖,可以請求成功。也就是說,在一個synchronized方法執行時,可以調用該對象的另一個synchronized方法。
/**
* synchronized
*
* Created by j on 2018/4/26.
*/
public class SynchronizedTest {
static int i = 0;
public void add(){
synchronized (SynchronizedTest.class){
i++;
}
}
public void add1(){
synchronized (this){
add();
}
}
public static void main(String[] args) throws InterruptedException {
SynchronizedTest test = new SynchronizedTest();
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
for (int i = 0; i < 100000; i++ ){
test.add();
}
}
});
Thread thread1 = new Thread(new Runnable() {
@Override
public void run() {
for (int i = 0; i < 100000; i++ ){
test.add();
}
}
});
thread.start();
thread1.start();
// 確保線程執行完畢
while (Thread.activeCount() > 2) {
Thread.yield();
}
System.out.println(i);
}
}
synchronized底層原理
JVM中的同步是通過持有和退出Monitor(監視器/管程)對象來實現的。
無論是顯式同步(同步代碼塊)或者是隱式同步(同步方法)都是這樣。
顯式同步與隱式同步的區別:
顯式同步:通過明確的代碼指令monitorenter(同步開始)和monitorexit(同步結束)來實現。
隱式同步:通過讀取常量池中方法的ACC_SYNCHRONIZED標識來實現。
synchronized同步代碼塊原理:
public void synchronizedAdd(){
synchronized (this){
i++;
}
}
首先,對上面這個同步塊代碼進行反編譯,javap -c SynchronizedTest.class
可以得到這個方法的指令代碼。
其中第3行和第13行可以看到是通過monitorenter和monitorexit指令來實現同步。
當執行monitorenter指令時,當前線程獲取鎖對象的monitor持有權,當monitor持有計數器爲0時,線程獲得鎖成功,計數器+1。如果在運行時,調用該對象其他鎖方法,此時爲重入狀態,計數器再次+1。當同步代碼執行完畢,計數器歸0。其他線程將會試圖獲取monitor。
我們可以看到19行多了一個monitorexit,是爲了當程序發生異常時,來釋放monitor的。否則其他線程將無法獲得monitor。
public void synchronizedAdd();
Code:
0: aload_0
1: dup
2: astore_1
3: monitorenter
4: getstatic #3 // Field i:I
7: iconst_1
8: iadd
9: putstatic #3 // Field i:I
12: aload_1
13: monitorexit
14: goto 22
17: astore_2
18: aload_1
19: monitorexit
20: aload_2
21: athrow
22: return
Exception table:
from to target type
4 14 17 any
17 20 17 any
synchronized同步方法原理:
同步方法,使用的是隱式同步方法,不通過指令來開始或結束同步(持有或釋放monitor)。
同步方法通過方法常量池方法表結構中的ACC_SYNCHRONIZED標識區分是否爲同步方法,如果在方法調用時發現ACC_SYNCHRONIZED被設置了,那麼線程首先會獲得monitor,其他線程無法獲得這個monitor並處於阻塞狀態。當方法執行完成時,釋放monitor。如果在同步方法期間拋出異常,並且沒有捕捉處理,那麼該線程所持有的monitor在拋到同步方法之外時釋放。
同步方法指令:
public synchronized void synchronizedAdd();
Code:
0: getstatic #2 // Field i:I
3: iconst_1
4: iadd
5: putstatic #2 // Field i:I
8: return
現在在實際的編程中已經幾乎很少使用synchronized關鍵字了,可以用更多的方式或封裝類來替代。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.