在Java的多線程中有兩種編程模型:異步編程模型和同步編程模型。
假設t1和t2爲兩個線程,則
異步編程模型: t1線程執行t1的,t2線程執行t2的,兩個線程之間誰也不等誰
同步編程模型:t1線程和t2線程執行,當t1線程必須等t2線程之行結束之後,t1線程才能執行,這是同步編程模型。
爲什麼要引入同步呢?
爲了數據的安全。儘管應用程序的使用率降低,但是爲了保證數據是安全的,必須加入線程同步機制。 線程同步機制使程序變成了(等同於)單線程。
什麼時候要同步呢?
第一、必須是多線程環境
第二、共享同一個數據 [ 注意:該數據必須是類中的變量(也即實例變量)對應的數據]
第三、共享的數據涉及到修改操作
必須同時滿足上面的三個條件才用同步模型。
以下示例演示採用異步編程對共享數據進行修改操作,看會出現什麼問題?
/*
* 以下程序演示取款例子,在不使用線程同步機制,
* 多線程同時對同一個賬戶進行操作,會出現什麼問題?
*
*/
public class Test {
public static void main(String[] args) throws InterruptedException {
// 創建一個公共賬戶
Account act = new Account("num-001", 1000.0);
MyRunnable myRunnable = new MyRunnable(act);
// 創建兩個線程,對同一個賬戶取款
Thread t1 = new Thread(myRunnable);
Thread t2 = new Thread(myRunnable);
t1.start();
t2.start();
}
}
class MyRunnable implements Runnable {
Account act;
public MyRunnable(Account act) {
this.act = act;
}
@Override
public void run() {
act.withdraw(100.0);
System.out.println("取款100.0成功,餘額爲:" + act.getBalance());
}
}
class Account {
private String actnum;
private Double balance;
public Account(String actnum, double balance) {
this.actnum = actnum;
this.balance = balance;
}
public String getActnum() {
return actnum;
}
public void setActnum(String actnum) {
this.actnum = actnum;
}
public Double getBalance() {
return balance;
}
public void setBalance(Double balance) {
this.balance = balance;
}
// 對外提供一個取款的方法
public void withdraw(double money) {
// 計算餘額
double after = balance - money;
// 延遲
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
// 更新
this.setBalance(after);
}
}
運行結果如圖 6-1 所示:
圖 6-1 不使用同步對共享數據進行修改操作會出現“髒讀”
使用同步可以解決上述問題。同步的方式大概可以分爲兩種:同步方法和同步語句塊。下面分別用這兩種方式解決上述問題。
先用同步語句塊解決,對上面的代碼做如下修改
//把需要同步的代碼放到同步語句塊中
synchronized (this) {
// 計算餘額
double after = balance - money;
// 延遲
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
// 更新
this.setBalance(after);
}
運行結果如圖 6-2 所示
圖 6-2 使用同步語句塊解決
然後用同步方法解決,對上面的代碼做如下修改
// 對外提供一個取款的方法,在方法上加上synchronized關鍵詞
public synchronized void withdraw(double money) {
// 計算餘額
double after = balance - money;
// 延遲
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
// 更新
this.setBalance(after);
}
運行結果如圖 6-3 所示
圖 6-3 使用同步方法解決
使用synchronized(this)語句塊同步的原理:t1線程和t2線程
t1線程在執行過程中,遇到了synchronized關鍵字,就會去找this的對象鎖, 如果找到this對象鎖,則進入同步語句塊中執行程序。當同步語句塊中的代碼執行結束後, t1線程歸還this的對象鎖。
在t1線程執行同步語句塊的過程中,如果t2線程也過來執行以下代碼,也遇到synchronized 關鍵字,所以也去找this的對象鎖,但是該對象鎖被t1線程持有,只能在這等待this對象的歸還。
使用synchronized方法同步的原理:t1線程和t2線程
每個對象都有一個鎖(也稱監控器monitor),它是對象生來就有的東西(因此你不必爲此寫任何代碼)。
t1線程在執行過程中,遇到了synchronized方法,就會去找this的對象鎖, 如果找到this對象鎖,這個對象就被鎖住了。當同步方法執行結束後,t1線程歸還這個對象的對象鎖,在方法返回並且解鎖之前,誰也不能調用同一個對象的其它synchronized方法。
在t1線程執行同步方法中的代碼過程中,如果t2線程也過來執行以下代碼,也遇到synchronized 關鍵字,所以也去找this的對象鎖,但是該對象鎖被t1線程持有,只能在這等待this對象的歸還。
同步方法又能細分爲:普通同步方法和靜態同步方法。雖然這兩種方法同步效果一樣,但是本質卻是不同的:synchronized關鍵字加到static靜態方法上是給Class類上鎖,而synchronized關鍵字加到普通方法上是給對象上鎖。下面用一個示例驗證不是同一個鎖
/*
* 驗證靜態同步方法和普通同步方法的本質區別:
* 靜態同步方法是給Class類上鎖,普通同步方法是給對象上鎖。
*/
public class Test {
public static void main(String[] args) throws InterruptedException {
Service service = new Service();
MyRunnableA myRunnableA = new MyRunnableA(service);
Thread a = new Thread(myRunnableA);
a.setName("A");
a.start();
MyRunnableB myRunnableB = new MyRunnableB(service);
Thread b = new Thread(myRunnableB);
b.setName("B");
b.start();
MyRunnableC myRunnableC = new MyRunnableC(service);
Thread c = new Thread(myRunnableC);
c.setName("C");
c.start();
}
}
class MyRunnableA implements Runnable {
private Service service;
public MyRunnableA(Service service) {
this.service = service;
}
@Override
public void run() {
service.printA();
}
}
class MyRunnableB implements Runnable {
private Service service;
public MyRunnableB(Service service) {
this.service = service;
}
@Override
public void run() {
service.printB();
}
}
class MyRunnableC implements Runnable {
private Service service;
public MyRunnableC(Service service) {
this.service = service;
}
@Override
public void run() {
service.printC();
}
}
/*
* 提供兩個靜態同步方法和一個普通同步方法,通過輸出結果(異步或同步)來判斷是否是不同的鎖
*/
class Service {
public synchronized static void printA() {
try {
System.out.println(
"線程名稱爲:" + Thread.currentThread().getName() + "在" + System.currentTimeMillis() + "進入printA");
// 設置延遲
Thread.sleep(3000);
System.out.println(
"線程名稱爲:" + Thread.currentThread().getName() + "在" + System.currentTimeMillis() + "離開printA");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public synchronized static void printB() {
System.out.println("線程名稱爲:" + Thread.currentThread().getName() + "在" + System.currentTimeMillis() + "進入printB");
System.out.println("線程名稱爲:" + Thread.currentThread().getName() + "在" + System.currentTimeMillis() + "離開printB");
}
public synchronized void printC() {
System.out.println("線程名稱爲:" + Thread.currentThread().getName() + "在" + System.currentTimeMillis() + "進入printC");
System.out.println("線程名稱爲:" + Thread.currentThread().getName() + "在" + System.currentTimeMillis() + "離開printC");
}
}
運行結果如圖 6-4 所示:
圖 6-4 方法printC()爲異步執行
異步的原因是持有不同的鎖,printC()方法是對象鎖,printA()方法是Class鎖,而Class鎖可以對類的所有對象實例起作用。下面用一個示例進行驗證
/*
* 驗證Class鎖可以對類的所有對象實例起作用
*/
public class Test {
public static void main(String[] args) throws InterruptedException {
// 設置兩個不同的對象實例
Service service1 = new Service();
Service service2 = new Service();
MyRunnableA myRunnableA = new MyRunnableA(service1);
Thread a = new Thread(myRunnableA);
a.setName("A");
a.start();
MyRunnableB myRunnableB = new MyRunnableB(service2);
Thread b = new Thread(myRunnableB);
b.setName("B");
b.start();
}
}
class MyRunnableA implements Runnable {
private Service service;
public MyRunnableA(Service service) {
this.service = service;
}
@Override
public void run() {
service.printA();
}
}
class MyRunnableB implements Runnable {
private Service service;
public MyRunnableB(Service service) {
this.service = service;
}
@Override
public void run() {
service.printB();
}
}
/*
* 提供兩個靜態同步方法
*/
class Service {
public synchronized static void printA() {
try {
System.out.println(
"線程名稱爲:" + Thread.currentThread().getName() + "在" + System.currentTimeMillis() + "進入printA");
// 設置延遲
Thread.sleep(3000);
System.out.println(
"線程名稱爲:" + Thread.currentThread().getName() + "在" + System.currentTimeMillis() + "離開printA");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public synchronized static void printB() {
System.out.println("線程名稱爲:" + Thread.currentThread().getName() + "在" + System.currentTimeMillis() + "進入printB");
System.out.println("線程名稱爲:" + Thread.currentThread().getName() + "在" + System.currentTimeMillis() + "離開printB");
}
}
運行結果如同 6-5 所示
圖 6-5 雖然是不同對象,但靜態同步方法還是同步執行
同步語句塊又能細分爲:同步synchronized(class)語句塊、同步synchronized(this)語句塊、同步synchronized(非this對象x)語句塊和同步synchronized(string)語句塊。
同步synchronized(this)語句塊在上面已經演示過,下面不再演示。
同步synchronized(class)語句塊的作用其實和synchronized static方法的作用一樣,下面用一個示例進行演示
對圖 6-5 上面的代碼做以下更改
class Service {
public static void printA() {
synchronized (Service.class) {
try {
System.out.println(
"線程名稱爲:" + Thread.currentThread().getName() + "在" + System.currentTimeMillis() + "進入printA");
// 設置延遲
Thread.sleep(3000);
System.out.println(
"線程名稱爲:" + Thread.currentThread().getName() + "在" + System.currentTimeMillis() + "離開printA");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public synchronized static void printB() {
synchronized (Service.class) {
System.out.println(
"線程名稱爲:" + Thread.currentThread().getName() + "在" + System.currentTimeMillis() + "進入printB");
System.out.println(
"線程名稱爲:" + Thread.currentThread().getName() + "在" + System.currentTimeMillis() + "離開printB");
}
}
}
運行結果如同 6-6 所示
圖 6-6 和靜態同步方法的作用一樣,也是同步執行
同步synchronized(string)語句塊會因爲String常量池的特性而帶來問題,因此同步synchronized 語句塊都不使用String作爲鎖對象,而改用其它,比如new Object()實例化一個Object對象,但它並不放入緩存中。
在JVM中具有String常量池緩存的功能,所以如圖 6-7 所示的結果爲true。
圖 6-7 Sting常量池緩存
同步synchronized(非this對象x)語句塊具有一定的優點:如果在一個類中有很多個synchronized 方法,這時雖然能實現同步,但會受到阻塞,所以影響運行效率;但如果使用同步語句塊非this對象,則synchronized (非this對象x)語句塊中的程序與同步方法是異步的,不與其他鎖this同步方法爭搶this鎖,則可以大大提高運行效率。
“synchronized (非this對象x)”格式的寫法是將x對象本身作爲“對象監視器”,這樣就可以得出以下3個結論:
- 當多個線程同時執行synchronized (x){}同步語句塊時呈同步效果。
- 當其他線程執行x對象中synchronized 同步方法時呈同步效果。
- 當其他線程執行x對象方法裏面的synchronized (this)語句塊時也呈同步效果。
注意:如果其他線程調用不加synchronized 關鍵字的方法時,還是異步調用。
驗證第一個結論:當多個線程同時執行synchronized (x){}同步語句塊時呈同步效果,示例如下
/*驗證當多個線程同時執行synchronized (x){}同步語句塊時呈同步效果
*/
public class Test {
public static void main(String[] args) throws InterruptedException {
Service service = new Service();
Object object = new Object();
ThreadA a = new ThreadA(service, object);
a.setName("A");
a.start();
ThreadB b = new ThreadB(service, object);
b.setName("B");
b.start();
}
}
class ThreadA extends Thread {
private Service service;
private Object object;
public ThreadA(Service service, Object object) {
super();
this.service = service;
this.object = object;
}
@Override
public void run() {
super.run();
service.testMethod1(object);
}
}
class ThreadB extends Thread {
private Service service;
private Object object;
public ThreadB(Service service, Object object) {
super();
this.service = service;
this.object = object;
}
@Override
public void run() {
super.run();
service.testMethod1(object);
}
}
class Service {
public void testMethod1(Object object) {
synchronized (object) {
try {
System.out.println("testMethod1--getLock time:" + System.currentTimeMillis() + " run ThreadName:"
+ Thread.currentThread().getName());
Thread.sleep(2000);
System.out.println("testMethod1--releaseLock time:" + System.currentTimeMillis() + " run ThreadName:"
+ Thread.currentThread().getName());
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
運行結果如圖 6-8 所示
圖 6-8 同步調用
同步的原因是使用了同一個“對象監視器”,如果使用不同的“對象監視器”就會異步執行。
對上面的代碼作以下更改:
public class Test {
public static void main(String[] args) throws InterruptedException {
Service service = new Service();
ThreadA a = new ThreadA(service, new Object());
a.setName("A");
a.start();
ThreadB b = new ThreadB(service, new Object());
b.setName("B");
b.start();
}
}
運行結果如圖 6-9 所示
圖 6-9 異步調用
驗證第二個結論:當其他線程執行x對象中synchronized 同步方法時呈同步效果,示例如下
public class Test {
public static void main(String[] args) throws InterruptedException {
Service service = new Service();
MyObject object = new MyObject();
ThreadA a = new ThreadA(service, object);
a.setName("A");
a.start();
ThreadB b = new ThreadB(service, object);
b.setName("B");
b.start();
}
}
class ThreadA extends Thread {
private Service service;
private MyObject object;
public ThreadA(Service service, MyObject object) {
super();
this.service = service;
this.object = object;
}
@Override
public void run() {
super.run();
service.testMethod1(object);
}
}
class ThreadB extends Thread {
private Service service;
private MyObject object;
public ThreadB(Service service, MyObject object) {
super();
this.service = service;
this.object = object;
}
@Override
public void run() {
super.run();
object.speedPrintString();
}
}
class Service {
public void testMethod1(MyObject object) {
synchronized (object) {
try {
System.out.println("testMethod1--getLock time:" + System.currentTimeMillis() + " run ThreadName:"
+ Thread.currentThread().getName());
Thread.sleep(5000);
System.out.println("testMethod1--releaseLock time:" + System.currentTimeMillis() + " run ThreadName:"
+ Thread.currentThread().getName());
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
class MyObject {
public synchronized void speedPrintString() {
System.out.println("speedPrintString--getLock time:" + System.currentTimeMillis() + " run ThreadName:"
+ Thread.currentThread().getName());
System.out.println("-------------------");
System.out.println("speedPrintString--releaseLock time:" + System.currentTimeMillis() + " run ThreadName:"
+ Thread.currentThread().getName());
}
}
運行結果如圖 6-10 所示
圖 6-10 同步效果
驗證第三個結論:當其他線程執行x對象方法裏面的synchronized (this)語句塊時也呈同步效果,示例如下
對上面的代碼作以下更改
class MyObject {
public void speedPrintString() {
synchronized (this) {
System.out.println("speedPrintString--getLock time:" + System.currentTimeMillis() + " run ThreadName:"
+ Thread.currentThread().getName());
System.out.println("-------------------");
System.out.println("speedPrintString--releaseLock time:" + System.currentTimeMillis() + " run ThreadName:"
+ Thread.currentThread().getName());
}
}
}
運行結果如圖 6-11 所示
圖 6-11 也是同步效果