這是并發(fā)系列第10篇文章。
創(chuàng)新互聯(lián)是一家專業(yè)提供廉江企業(yè)網(wǎng)站建設(shè),專注與成都做網(wǎng)站、網(wǎng)站設(shè)計(jì)、外貿(mào)營銷網(wǎng)站建設(shè)、HTML5、小程序制作等業(yè)務(wù)。10年已為廉江眾多企業(yè)、政府機(jī)構(gòu)等服務(wù)。創(chuàng)新互聯(lián)專業(yè)的建站公司優(yōu)惠進(jìn)行中。
當(dāng)多個線程去訪問同一個類(對象或方法)的時候,該類都能表現(xiàn)出正常的行為(與自己預(yù)想的結(jié)果一致),那我們就可以所這個類是線程安全的。
看一段代碼:
package com.itsoku.chat04;
/**
* 微信公眾號:javacode2018,獲取年薪50萬課程
*/
public class Demo1 {
static int num = 0;
public static void m1() {
for (int i = 0; i < 10000; i++) {
num++;
}
}
public static class T1 extends Thread {
@Override
public void run() {
Demo1.m1();
}
}
public static void main(String[] args) throws InterruptedException {
T1 t1 = new T1();
T1 t2 = new T1();
T1 t3 = new T1();
t1.start();
t2.start();
t3.start();
//等待3個線程結(jié)束打印num
t1.join();
t2.join();
t3.join();
System.out.println(Demo1.num);
/**
* 打印結(jié)果:
* 25572
*/
}
}
Demo1中有個靜態(tài)變量num,默認(rèn)值是0,m1()方法中對num++執(zhí)行10000次,main方法中創(chuàng)建了3個線程用來調(diào)用m1()方法,然后調(diào)用3個線程的join()方法,用來等待3個線程執(zhí)行完畢之后,打印num的值。我們期望的結(jié)果是30000,運(yùn)行一下,但真實(shí)的結(jié)果卻不是30000。上面的程序在多線程中表現(xiàn)出來的結(jié)果和預(yù)想的結(jié)果不一致,說明上面的程序不是線程安全的。
線程安全是并發(fā)編程中的重要關(guān)注點(diǎn),應(yīng)該注意到的是,造成線程安全問題的主要誘因有兩點(diǎn):
因此為了解決這個問題,我們可能需要這樣一個方案,當(dāng)存在多個線程操作共享數(shù)據(jù)時,需要保證同一時刻有且只有一個線程在操作共享數(shù)據(jù),其他線程必須等到該線程處理完數(shù)據(jù)后再進(jìn)行,這種方式有個高尚的名稱叫互斥鎖,即能達(dá)到互斥訪問目的的鎖,也就是說當(dāng)一個共享數(shù)據(jù)被當(dāng)前正在訪問的線程加上互斥鎖后,在同一個時刻,其他線程只能處于等待的狀態(tài),直到當(dāng)前線程處理完畢釋放該鎖。在 Java 中,關(guān)鍵字 synchronized可以保證在同一個時刻,只有一個線程可以執(zhí)行某個方法或者某個代碼塊(主要是對方法或者代碼塊中存在共享數(shù)據(jù)的操作),同時我們還應(yīng)該注意到synchronized另外一個重要的作用,synchronized可保證一個線程的變化(主要是共享數(shù)據(jù)的變化)被其他線程所看到(保證可見性,完全可以替代volatile功能),這點(diǎn)確實(shí)也是很重要的。
那么我們把上面的程序做一下調(diào)整,在m1()方法上面使用關(guān)鍵字synchronized,如下:
public static synchronized void m1() {
for (int i = 0; i < 10000; i++) {
num++;
}
}
然后執(zhí)行代碼,輸出30000,和期望結(jié)果一致。
所謂實(shí)例對象鎖就是用synchronized修飾實(shí)例對象的實(shí)例方法,注意是實(shí)例方法,不是靜態(tài)方法,如:
package com.itsoku.chat04;
/**
* 微信公眾號:javacode2018,獲取年薪50萬課程
*/
public class Demo2 {
int num = 0;
public synchronized void add() {
num++;
}
public static class T extends Thread {
private Demo2 demo2;
public T(Demo2 demo2) {
this.demo2 = demo2;
}
@Override
public void run() {
for (int i = 0; i < 10000; i++) {
this.demo2.add();
}
}
}
public static void main(String[] args) throws InterruptedException {
Demo2 demo2 = new Demo2();
T t1 = new T(demo2);
T t2 = new T(demo2);
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println(demo2.num);
}
}
main()方法中創(chuàng)建了一個對象demo2和2個線程t1、t2,t1、t2中調(diào)用demo2的add()方法10000次,add()方法中執(zhí)行了num++,num++實(shí)際上是分3步,獲取num,然后將num+1,然后將結(jié)果賦值給num,如果t2在t1讀取num和num+1之間獲取了num的值,那么t1和t2會讀取到同樣的值,然后執(zhí)行num++,兩次操作之后num是相同的值,最終和期望的結(jié)果不一致,造成了線程安全失敗,因此我們對add方法加了synchronized來保證線程安全。
注意:m1()方法是實(shí)例方法,兩個線程操作m1()時,需要先獲取demo2的鎖,沒有獲取到鎖的,將等待,直到其他線程釋放鎖為止。
synchronize作用于實(shí)例方法需要注意:
當(dāng)synchronized作用于靜態(tài)方法時,鎖的對象就是當(dāng)前類的Class對象。如:
package com.itsoku.chat04;
/**
* 微信公眾號:javacode2018,獲取年薪50萬課程
*/
public class Demo3 {
static int num = 0;
public static synchronized void m1() {
for (int i = 0; i < 10000; i++) {
num++;
}
}
public static class T1 extends Thread {
@Override
public void run() {
Demo3.m1();
}
}
public static void main(String[] args) throws InterruptedException {
T1 t1 = new T1();
T1 t2 = new T1();
T1 t3 = new T1();
t1.start();
t2.start();
t3.start();
//等待3個線程結(jié)束打印num
t1.join();
t2.join();
t3.join();
System.out.println(Demo3.num);
/**
* 打印結(jié)果:
* 30000
*/
}
}
上面代碼打印30000,和期望結(jié)果一致。m1()方法是靜態(tài)方法,有synchronized修飾,鎖用于與Demo3.class對象,和下面的寫法類似:
public static void m1() {
synchronized (Demo4.class) {
for (int i = 0; i < 10000; i++) {
num++;
}
}
}
除了使用關(guān)鍵字修飾實(shí)例方法和靜態(tài)方法外,還可以使用同步代碼塊,在某些情況下,我們編寫的方法體可能比較大,同時存在一些比較耗時的操作,而需要同步的代碼又只有一小部分,如果直接對整個方法進(jìn)行同步操作,可能會得不償失,此時我們可以使用同步代碼塊的方式對需要同步的代碼進(jìn)行包裹,這樣就無需對整個方法進(jìn)行同步操作了,同步代碼塊的使用示例如下:
package com.itsoku.chat04;
/**
* 微信公眾號:javacode2018,獲取年薪50萬課程
*/
public class Demo5 implements Runnable {
static Demo5 instance = new Demo5();
static int i = 0;
@Override
public void run() {
//省略其他耗時操作....
//使用同步代碼塊對變量i進(jìn)行同步操作,鎖對象為instance
synchronized (instance) {
for (int j = 0; j < 10000; j++) {
i++;
}
}
}
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(instance);
Thread t2 = new Thread(instance);
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println(i);
}
}
從代碼看出,將synchronized作用于一個給定的實(shí)例對象instance,即當(dāng)前實(shí)例對象就是鎖對象,每次當(dāng)線程進(jìn)入synchronized包裹的代碼塊時就會要求當(dāng)前線程持有instance實(shí)例對象鎖,如果當(dāng)前有其他線程正持有該對象鎖,那么新到的線程就必須等待,這樣也就保證了每次只有一個線程執(zhí)行i++;操作。當(dāng)然除了instance作為對象外,我們還可以使用this對象(代表當(dāng)前實(shí)例)或者當(dāng)前類的class對象作為鎖,如下代碼:
//this,當(dāng)前實(shí)例對象鎖
synchronized(this){
for(int j=0;j<1000000;j++){
i++;
}
}
//class對象鎖
synchronized(Demo5.class){
for(int j=0;j<1000000;j++){
i++;
}
}
分析代碼是否互斥的方法,先找出synchronized作用的對象是誰,如果多個線程操作的方法中synchronized作用的鎖對象一樣,那么這些線程同時異步執(zhí)行這些方法就是互斥的。如下代碼:
package com.itsoku.chat04;
/**
* 微信公眾號:javacode2018,獲取年薪50萬課程
*/
public class Demo6 {
//作用于當(dāng)前類的實(shí)例對象
public synchronized void m1() {
}
//作用于當(dāng)前類的實(shí)例對象
public synchronized void m2() {
}
//作用于當(dāng)前類的實(shí)例對象
public void m3() {
synchronized (this) {
}
}
//作用于當(dāng)前類Class對象
public static synchronized void m4() {
}
//作用于當(dāng)前類Class對象
public static void m5() {
synchronized (Demo6.class) {
}
}
public static class T extends Thread{
Demo6 demo6;
public T(Demo6 demo6) {
this.demo6 = demo6;
}
@Override
public void run() {
super.run();
}
}
public static void main(String[] args) {
Demo6 d1 = new Demo6();
Thread t1 = new Thread(() -> {
d1.m1();
});
t1.start();
Thread t2 = new Thread(() -> {
d1.m2();
});
t2.start();
Thread t3 = new Thread(() -> {
d1.m2();
});
t3.start();
Demo6 d2 = new Demo6();
Thread t4 = new Thread(() -> {
d2.m2();
});
t4.start();
Thread t5 = new Thread(() -> {
Demo6.m4();
});
t5.start();
Thread t6 = new Thread(() -> {
Demo6.m5();
});
t6.start();
}
}
分析上面代碼:
synchronized除了用于線程同步、確保線程安全外,還可以保證線程間的可見性和有序性。從可見性的角度上將,關(guān)鍵字synchronized可以完全替代關(guān)鍵字volatile的功能,只是使用上沒有那么方便。就有序性而言,由于關(guān)鍵字synchronized限制每次只有一個線程可以訪問同步塊,因此,無論同步塊內(nèi)的代碼如何被亂序執(zhí)行,只要保證串行語義一致,那么執(zhí)行結(jié)果總是一樣的。而其他訪問線程,又必須在獲得鎖后方能進(jìn)入代碼塊讀取數(shù)據(jù),因此,他們看到的最終結(jié)果并不取決于代碼的執(zhí)行過程,有序性問題自然得到了解決(換言之,被關(guān)鍵字synchronized限制的多個線程是串行執(zhí)行的)。
線程進(jìn)入synchronized修飾的代碼中時,synchronized代碼塊內(nèi)部使用到的共享變量在當(dāng)前線程的工作內(nèi)存中都會被清空,會從主內(nèi)存中獲取,當(dāng)synchronized代碼塊結(jié)束的時候,代碼塊內(nèi)部修改的共享變量都會強(qiáng)制刷新到主存儲中,所以是可見的。
關(guān)于synchronized可以保證可見性的,上個例子:
package com.itsoku.chat05;
import java.util.concurrent.TimeUnit;
/**
* 微信公眾號:javacode2018,獲取年薪50萬課程
*/
public class Demo4 {
static boolean flag = false;
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread() {
@Override
public void run() {
System.out.println(this.getName() + " start");
while (true) {
synchronized (this) {
if (flag) {
break;
}
}
}
System.out.println(this.getName() + " exit");
}
};
t1.setName("t1");
t1.start();
Thread t2 = new Thread() {
@Override
public void run() {
System.out.println(this.getName() + " start");
synchronized (this) {
while (true) {
if (flag) {
break;
}
}
}
System.out.println(this.getName() + " exit");
}
};
t2.setName("t2");
t2.start();
TimeUnit.SECONDS.sleep(2);
flag = true;
}
}
運(yùn)行結(jié)果:
t1線程可以正常結(jié)束,t2線程無法結(jié)束,說明主線程中flag修改之后已經(jīng)被刷新到了主內(nèi)存了,t1可以看到主內(nèi)存中中flag最新的值。
t1線程中有個while循環(huán),循環(huán)內(nèi)部有個synchronized塊,前面提到過,進(jìn)入synchronized時,塊內(nèi)部用到的變量在當(dāng)前線程的工作內(nèi)存中都會被清空,所以每次進(jìn)入塊中第一次訪問flag的時候,都會從主內(nèi)存中獲取,然后復(fù)制到工作內(nèi)存中,所以t1可以正常結(jié)束。
t2線程中while循環(huán)在synchronized內(nèi)部,循環(huán)內(nèi)部第一次訪問flag的時候會從主內(nèi)存中獲取最新的值,后面再次訪問的時候會從工作內(nèi)存中獲取,所以獲取到flag一直未false,程序無法結(jié)束。
分享名稱:java高并發(fā)系列-第10天:線程安全和synchronized關(guān)鍵字
鏈接地址:http://m.rwnh.cn/article2/ipgpoc.html
成都網(wǎng)站建設(shè)公司_創(chuàng)新互聯(lián),為您提供定制網(wǎng)站、企業(yè)建站、網(wǎng)站設(shè)計(jì)、品牌網(wǎng)站制作、搜索引擎優(yōu)化、域名注冊
聲明:本網(wǎng)站發(fā)布的內(nèi)容(圖片、視頻和文字)以用戶投稿、用戶轉(zhuǎn)載內(nèi)容為主,如果涉及侵權(quán)請盡快告知,我們將會在第一時間刪除。文章觀點(diǎn)不代表本網(wǎng)站立場,如需處理請聯(lián)系客服。電話:028-86922220;郵箱:631063699@qq.com。內(nèi)容未經(jīng)允許不得轉(zhuǎn)載,或轉(zhuǎn)載時需注明來源: 創(chuàng)新互聯(lián)