同步問題的引出
需求:多個線程同時賣票
class MyThread implements Runnable {
private int ticket = 10;
@Override
public void run() {
while (this.ticket > 0)
{
try {
Thread.sleep(200);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"還剩下"+this.ticket--+"票");
}
}
}
public class TestThread {
public static void main(String[] args) {
MyThread myThread = new MyThread();
new Thread(myThread,"黃牛1").start();
new Thread(myThread,"黃牛2").start();
new Thread(myThread,"黃牛3").start();
}
}
Files\Java\jdk1.8.0_181\jre\lib\rt.jar;E:\Java\code\out\production\code" www.bit.java.TestThread
黃牛3還剩下10票
黃牛1還剩下9票
黃牛2還剩下10票
黃牛3還剩下8票
黃牛1還剩下6票
黃牛2還剩下7票
黃牛3還剩下5票
黃牛2還剩下3票
黃牛1還剩下4票
黃牛2還剩下2票
黃牛3還剩下1票
黃牛1還剩下0票
黃牛2還剩下-1票
Process finished with exit code 0
這個時候我們發現,票數竟然出現負數,這種問題我們稱之爲不同步操作。
不同步的唯一好處是處理速度快(多個線程併發執行)
同步處理
所謂的同步指的是所有的線程不是一起進入到方法中執行,而是按照順序一個一個進來。
synchronized處理同步問題
如果要想實現這把"鎖"的功能,可以採用關鍵字synchronized來處理。
使用synchronized關鍵字處理有兩種模式:同步代碼塊、同步方法
- 同步代碼塊:在方法中使用
synchronized(對象)
,一般可以鎖定當前對象this。表示同一時刻只有一個線程能夠進入同步代碼塊,但是多個線程可以同時進入方法。
class MyThread implements Runnable {
private int ticket = 1000;
@Override
public void run() {
for(int i = 0;i< 1000;i++){
synchronized (this){
if(this.ticket > 0)
{
try {
Thread.sleep(20);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"還剩下"+this.ticket--+"票");
}
}
}
}
}
public class TestThread {
public static void main(String[] args) {
MyThread myThread = new MyThread();
new Thread(myThread,"黃牛1").start();
new Thread(myThread,"黃牛2").start();
new Thread(myThread,"黃牛3").start();
}
}
- 同步方法:在
方法上加synchronized
,表示此時只有一個線程能夠進入同步方法。
class MyThread implements Runnable {
private int ticket = 1000;
@Override
public void run() {
for(int i = 0;i< 1000;i++){
SellTicket(this.ticket);
}
}
private synchronized void SellTicket(int ticket){
if(this.ticket > 0)
{
try {
Thread.sleep(20);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"還剩下"+this.ticket--+"票");
}
}
}
public class TestThread {
public static void main(String[] args) {
MyThread myThread = new MyThread();
new Thread(myThread,"黃牛1").start();
new Thread(myThread,"黃牛2").start();
new Thread(myThread,"黃牛3").start();
}
}
關於synchronized的額外說明
先來看一段代碼:觀察synchronized鎖多對象
class Sync {
public synchronized void test() {
System.out.println("test方法開始,當前線程爲 "+Thread.currentThread().getName());
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("test方法結束,當前線程爲 "+Thread.currentThread().getName());
}
}
class MyThread extends Thread{
@Override
public void run() {
Sync sync = new Sync();
sync.test();
}
}
public class TestThread {
public static void main(String[] args) {
for(int i = 0;i < 3;i++){
new Thread(new MyThread()).start();
}
}
}
通過上述代碼以及運行結果我們可以發現,沒有看到synchronized起到作用,三個線程同時運行test()方法。
實際上,synchronized(this)
以及非static的synchronized方法
,只能防止多個線程同時執行同一個對象的同步代碼段。即synchronized鎖住的是括號裏的對象,而不是代碼。對於非static的synchronized方法,鎖的就是對象本身也就是this。
當synchronized鎖住一個對象後,別的線程如果也想拿到這個對象的鎖,就必須等待這個線程執行完成釋放鎖,才能再次給對象加鎖,這樣才達到線程同步的目的。即使兩個不同的代碼段,都要鎖同一個對象,那麼這兩個代碼段也不能在多線程環境下同時運行。
那麼,如果真要鎖住這段代碼,要怎麼做?
- 鎖同一個對象
class Sync {
public synchronized void test() {
System.out.println("test方法開始,當前線程爲 "+Thread.currentThread().getName());
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("test方法結束,當前線程爲 "+Thread.currentThread().getName());
}
}
class MyThread extends Thread{
private Sync sync;
public MyThread(Sync sync){
this.sync = sync;
}
@Override
public void run() {
this.sync.test();
}
}
public class TestThread {
public static void main(String[] args) {
Sync sync = new Sync();
for(int i = 0;i < 3;i++){
new Thread(new MyThread(sync)).start();
}
}
}
- 讓synchronized鎖這個類對應的Class對象—全局鎖
class Sync {
public synchronized void test() {
synchronized (Sync.class){
System.out.println("test方法開始,當前線程爲 "+Thread.currentThread().getName());
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("test方法結束,當前線程爲 "+Thread.currentThread().getName());
}
}
}
class MyThread extends Thread{
@Override
public void run() {
Sync sync = new Sync();
sync.test();
}
}
public class TestThread {
public static void main(String[] args) {
for(int i = 0;i < 3;i++){
new Thread(new MyThread()).start();
}
}
}
上面代碼用synchronized(Sync.class)
實現了全局鎖的效果。因此,如果要想鎖的是代碼段,鎖住多個對象的同一方法,使用這種全局鎖,鎖的是類而不是this。
static synchronized方法
,static方法可以直接類名加方法名調用,方法中無法使用this,所以它鎖的不是this,而是類的Class對象,所以,static synchronized方法也相當於全局鎖,相當於鎖住了代碼段。
class Sync {
public static synchronized void test() {
System.out.println("test方法開始,當前線程爲 "+Thread.currentThread().getName());
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("test方法結束,當前線程爲 "+Thread.currentThread().getName());
}
}
class MyThread extends Thread{
@Override
public void run() {
Sync sync = new Sync();
sync.test();
}
}
public class TestThread {
public static void main(String[] args) {
for(int i = 0;i < 3;i++){
new Thread(new MyThread()).start();
}
}
}
synchronized實現原理
同步代碼塊底層實現
先來看一段簡單的代碼:
public class Test{
private static Object object = new Object();
public static void main(String[] args) {
synchronized (object) {
System.out.println("hello world");
}
}
}
下面我們使用javap反編譯後看看生成的部分字節碼
...
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=3, args_size=1
0: getstatic #2 // Field object:Ljava/lang/Object;
3: dup
4: astore_1
5: monitorenter // 瞪大眼睛看這裏!!!
6: getstatic #3 // Field java/lang/System.out:Ljava/io/PrintStream;
9: ldc #4 // String hello world
11: invokevirtual #5 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
14: aload_1
15: monitorexit // 瞪大眼睛看這裏!!!
16: goto 24
19: astore_2
20: aload_1
21: monitorexit // 瞪大眼睛看這裏!!!
22: aload_2
23: athrow
24: return
Exception table:
from to target type
6 16 19 any
19 22 19 any
...
執行同步代碼塊後首先要先執行monitorenter
指令,退出的時候monitorexit
指令。通過分析之後可以看出,使用 Synchronized進行同步,其關鍵就是必須要對對象的監視器monitor進行獲取,當線程獲取monitor後才能繼續往下執行,否則就只能等待。而這個獲取的過程是互斥的,即同一時刻只有一個線程可以獲取到該對象的monitor監視器。
上述字節碼中包含一個monitorenter
指令以及多個monitorexit
指令。這是因爲Java虛擬機需要確保所獲得的鎖在正常執行路徑,以及異常執行路徑上都能夠被解鎖。
同步方法底層實現
public synchronized void test();
descriptor: ()V
flags: ACC_PUBLIC, ACC_STATIC, ACC_SYNCHRONIZED
Code:
stack=2, locals=0, args_size=0
0: getstatic #3 // Field java/lang/System.out:Ljava/io/PrintStream;
3: ldc #4 // String hello world
5: invokevirtual #5 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
8: return
LineNumberTable:
line 471: 0
line 472: 8
當使用synchronized標記方法時,字節碼會出現一個訪問標記ACC_SYNCHRONIZED
。該標記表示在進入方法時,JVM需要進行monitorenter
操作。在退出方法時,無論是正常返回,還是向調用者拋異常,JVM需要進行monitorexit
操作。
這裏 monitorenter
和 monitorexit
操作所對應的鎖對象是隱式的。對於實例方法來說,這兩個操作對應的鎖對象是 this;對於靜態方法來說,這兩個操作對應的鎖對象則是所在類的 Class 實例。
當JVM執行monitorenter時,如果目標對象monitor的計數器爲0,表示此時該對象沒有被其他線程所持有。此時JVM會將該鎖對象的持有線程設置爲當前線程,並且將monitor計數器+1。
在目標鎖對象的計數器不爲0的情況下,如果鎖對象的持有線程是當前線程,JVM可以將計數器再次+1(可重入鎖);否則需要等待,直到持有線程是釋放線程。
當執行monitorexit時,JVM需將鎖對象計數器-1。當計數器減爲0時,代表該鎖以及被釋放掉,喚醒所有正在等待的線程去競爭該鎖。
之所以採用這種計數器的方式,是爲了允許同一個線程重複獲取同一把鎖。舉個例子,如果一個 Java 類中擁有多個 synchronized 方法,那麼這些方法之間的相互調用,不管是直接的還是間接的,都會涉及對同一把鎖的重複加鎖操作。因此,我們需要設計這麼一個可重入的特性,來避免編程裏的隱式約束。
證明同一個對象的同步方法再次獲得鎖時不能獲取成功
class MyThread extends Thread{
public synchronized void A(){
while(true){}
}
public synchronized void B(){
System.out.println(Thread.currentThread().getName()+",線程B...");
}
@Override
public void run() {
A();
B();
}
}
public class TestThread {
public static void main(String[] args) {
MyThread myThread = new MyThread();
new Thread(myThread,"A").start();
new Thread(myThread,"B").start();
}
}
證明同一個線程再次獲得鎖時可以獲取成功,而其他線程獲取鎖會阻塞(即鎖的可重入性)
class MyThread extends Thread{
public synchronized void A(){
while(true){
System.out.println(Thread.currentThread().getName()+",線程A...");
B();
}
}
public synchronized void B(){
System.out.println(Thread.currentThread().getName()+",線程B...");
}
@Override
public void run() {
A();
B();
}
}
public class TestThread {
public static void main(String[] args) {
MyThread myThread = new MyThread();
new Thread(myThread,"A").start();
new Thread(myThread,"B").start();
}
}