java高并發(fā)系列-第10天:線程安全和synchronized關(guān)鍵字

這是并發(fā)系列第10篇文章。

創(chuàng)新互聯(lián)是一家專業(yè)提供廉江企業(yè)網(wǎng)站建設(shè),專注與成都做網(wǎng)站、網(wǎng)站設(shè)計、外貿(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,運行一下,但真實的結(jié)果卻不是30000。上面的程序在多線程中表現(xiàn)出來的結(jié)果和預(yù)想的結(jié)果不一致,說明上面的程序不是線程安全的。

線程安全是并發(fā)編程中的重要關(guān)注點,應(yīng)該注意到的是,造成線程安全問題的主要誘因有兩點:

  1. 一是存在共享數(shù)據(jù)(也稱臨界資源)
  2. 二是存在多條線程共同操作共享數(shù)據(jù)

因此為了解決這個問題,我們可能需要這樣一個方案,當(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ào)整,在m1()方法上面使用關(guān)鍵字synchronized,如下:

public static synchronized void m1() {
    for (int i = 0; i < 10000; i++) {
        num++;
    }
}

然后執(zhí)行代碼,輸出30000,和期望結(jié)果一致。

synchronized主要有3種使用方式

  1. 修飾實例方法,作用于當(dāng)前實例,進(jìn)入同步代碼前需要先獲取實例的鎖
  2. 修飾靜態(tài)方法,作用于類的Class對象,進(jìn)入修飾的靜態(tài)方法前需要先獲取類的Class對象的鎖
  3. 修飾代碼塊,需要指定加鎖對象(記做lockobj),在進(jìn)入同步代碼塊前需要先獲取lockobj的鎖

synchronized作用于實例對象

所謂實例對象鎖就是用synchronized修飾實例對象的實例方法,注意是實例方法,不是靜態(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++實際上是分3步,獲取num,然后將num+1,然后將結(jié)果賦值給num,如果t2在t1讀取num和num+1之間獲取了num的值,那么t1和t2會讀取到同樣的值,然后執(zhí)行num++,兩次操作之后num是相同的值,最終和期望的結(jié)果不一致,造成了線程安全失敗,因此我們對add方法加了synchronized來保證線程安全。

注意:m1()方法是實例方法,兩個線程操作m1()時,需要先獲取demo2的鎖,沒有獲取到鎖的,將等待,直到其他線程釋放鎖為止。

synchronize作用于實例方法需要注意:

  1. 實例方法上加synchronized,線程安全的前提是,多個線程操作的是同一個實例,如果多個線程作用于不同的實例,那么線程安全是無法保證的
  2. 同一個實例的多個實例方法上有synchronized,這些方法都是互斥的,同一時間只允許一個線程操作同一個實例的其中的一個synchronized方法

synchronized作用于靜態(tài)方法

當(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++;
        }
    }
}

synchronized同步代碼塊

除了使用關(guān)鍵字修飾實例方法和靜態(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作用于一個給定的實例對象instance,即當(dāng)前實例對象就是鎖對象,每次當(dāng)線程進(jìn)入synchronized包裹的代碼塊時就會要求當(dāng)前線程持有instance實例對象鎖,如果當(dāng)前有其他線程正持有該對象鎖,那么新到的線程就必須等待,這樣也就保證了每次只有一個線程執(zhí)行i++;操作。當(dāng)然除了instance作為對象外,我們還可以使用this對象(代表當(dāng)前實例)或者當(dāng)前類的class對象作為鎖,如下代碼:

//this,當(dāng)前實例對象鎖
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)前類的實例對象
    public synchronized void m1() {
    }

    //作用于當(dāng)前類的實例對象
    public synchronized void m2() {
    }

    //作用于當(dāng)前類的實例對象
    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();
    }

} 

分析上面代碼:

  1. 線程t1、t2、t3中調(diào)用的方法都需要獲取d1的鎖,所以他們是互斥的
  2. t1/t2/t3這3個線程和t4不互斥,他們可以同時運行,因為前面三個線程依賴于d1的鎖,t4依賴于d2的鎖
  3. t5、t6都作用于當(dāng)前類的Class對象鎖,所以這兩個線程是互斥的,和其他幾個線程不互斥

synchronized可以確保變量的可見性

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;
    }
}

運行結(jié)果:
java高并發(fā)系列 - 第10天:線程安全和synchronized關(guān)鍵字

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://muchs.cn/article2/ipgpoc.html

成都網(wǎng)站建設(shè)公司_創(chuàng)新互聯(lián),為您提供定制網(wǎng)站企業(yè)建站、網(wǎng)站設(shè)計品牌網(wǎng)站制作、搜索引擎優(yōu)化域名注冊

廣告

聲明:本網(wǎng)站發(fā)布的內(nèi)容(圖片、視頻和文字)以用戶投稿、用戶轉(zhuǎn)載內(nèi)容為主,如果涉及侵權(quán)請盡快告知,我們將會在第一時間刪除。文章觀點不代表本網(wǎng)站立場,如需處理請聯(lián)系客服。電話:028-86922220;郵箱:631063699@qq.com。內(nèi)容未經(jīng)允許不得轉(zhuǎn)載,或轉(zhuǎn)載時需注明來源: 創(chuàng)新互聯(lián)

h5響應(yīng)式網(wǎng)站建設(shè)