程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> JAVA編程 >> 關於JAVA >> Java多線程根底——對象及變量並發訪問

Java多線程根底——對象及變量並發訪問

編輯:關於JAVA

Java多線程根底——對象及變量並發訪問。本站提示廣大學習愛好者:(Java多線程根底——對象及變量並發訪問)文章只能為提供參考,不一定能成為您想要的結果。以下是Java多線程根底——對象及變量並發訪問正文


在開發多線程順序時,假如每個多線程處置的事情都不一樣,每個線程都互不相關,這樣開發的進程就十分輕松。但是很多時分,多線程順序是需求同時訪問同一個對象,或許變量的。這樣,一個對象同時被多個線程訪問,會呈現處置的後果和預期不分歧的能夠。因而,需求理解如何對對象及變量並發訪問,寫出線程平安的順序,所謂線程平安就是處置的對象及變量的時分是同步處置的,在處置的時分其他線程是不會攪擾。本文將從以下幾個角度論述這個問題。一切的代碼都在char02

  1. 關於辦法的同步處置
  2. 關於語句塊的同步處置
  3. 對類加鎖的同步處置
  4. 保證可見性的關鍵字——volatile
關於辦法的同步處置

關於一個對象的辦法,假如有兩個線程同時訪問,假如不加控制,訪問的後果會出人意料。所以我們需求對辦法停止同步處置,讓一個線程先訪問,等訪問完畢,在讓另一個線程去訪問。關於要處置的辦法,用synchronized修飾該辦法。我們上面看一下比照的例子。
首先是沒有同步修飾的辦法,看看會有什麼預料之外的事情

public class HasSelfPrivateNum {
    private int num = 0;
    public void addI(String username){
        try{
            if (username.equals("a")){
                num = 100;
                System.out.println("a set over!");
                Thread.sleep(2000);
            }else {
                num = 200;
                System.out.println("b set over!");
            }
            System.out.println(username + "  num=" + num);
        }catch (Exception e){
            e.printStackTrace();
        }
    }
}

public class SelfPrivateThreadA  extends Thread{
    private HasSelfPrivateNum num;
    public SelfPrivateThreadA(HasSelfPrivateNum num){
        this.num = num;
    }
    @Override
    public void run() {
        super.run();
        num.addI("a");
    }
}

public class SelfPrivateThreadB extends Thread{
    private HasSelfPrivateNum num;
    public SelfPrivateThreadB(HasSelfPrivateNum num){
        this.num = num;
    }
    @Override
    public void run() {
        super.run();
        num.addI("b");
    }
}

測試的辦法如下:

public class HasSelfPrivateNumTest extends TestCase {
    public void testAddI() throws Exception {
        HasSelfPrivateNum numA = new HasSelfPrivateNum();
//        HasSelfPrivateNum numB = new HasSelfPrivateNum();
        SelfPrivateThreadA threadA = new SelfPrivateThreadA(numA);
        threadA.start();
        SelfPrivateThreadB threadB = new SelfPrivateThreadB(numA);
        threadB.start();

        Thread.sleep(1000 * 3);
    }

}

在這個對象中,有一個成員變量num, 假如username是a,則num應該等於100,假如是b,則num應該等於200,threadA與threadB同時去訪問addI辦法,預期的後果應該是a num=100 b num=200。但是實踐的後果如下:

a set over!
b set over!
b  num=200
a  num=200

這是為什麼呢?由於threadA先調用addI辦法,但是由於傳入的參數的是a,所示ThreadA線程休眠2s,這是B線程也曾經調用了addI辦法,然後將num的值改為了200,這是輸入語句輸入的是b改之後的num的值也就是200,a的值被b再次修正掩蓋了。
這個辦法是線程不平安的,我們給這個辦法添加synchronized,修正如下:

 synchronized public void addI(String username){
        try{
            if (username.equals("a")){
                num = 100;
                System.out.println("a set over!");
                Thread.sleep(2000);
            }else {
                num = 200;
                System.out.println("b set over!");
            }
            System.out.println(username + "  num=" + num);
        }catch (Exception e){
            e.printStackTrace();
        }
    }

其他中央堅持不變,如今我們在看一下,後果:

a set over!
a  num=100
b set over!
b  num=200

這個後果是不是就契合預期的後果,調用的順序也是分歧的。
synchronized可以保證多線程調用同一個對象的辦法的時分,是同步停止的,留意是同一個對象,也就是說synchronized的辦法是對象鎖,鎖住的是對象,假如是不同的對象,就沒有這個線程不平安的問題。我們在下面的修正的根底上,去掉
synchronized,然後修正測試辦法,讓兩個線程調用不同對象的辦法,修正如下:

public class HasSelfPrivateNumTest extends TestCase {
    public void testAddI() throws Exception {
        HasSelfPrivateNum numA = new HasSelfPrivateNum();
        HasSelfPrivateNum numB = new HasSelfPrivateNum();
        SelfPrivateThreadA threadA = new SelfPrivateThreadA(numA);
        threadA.start();
        SelfPrivateThreadB threadB = new SelfPrivateThreadB(numA);
        threadB.start();
        Thread.sleep(1000 * 3);
    }
}

後果如下:

b set over!
b  num=200
a set over!
a  num=100

由於threadB是不需求休眠的,所以兩個線程同時調用的時分,一定是B線程先出後果,這個後果是契合預期的。但是這樣是無法證明synchronized是對象鎖的,只能闡明不同線程訪問不同對象是不會呈現線程不平安的狀況的。在補充一個例子來證明:同一個對象,有兩個同步辦法,但是兩個線程辨別調用其中一個同步辦法,假如前往的後果不是同時呈現的,則闡明是對象鎖,即鎖住了一個對象,該對象的其他辦法也要等該對象鎖釋放,才干調用。

public class MyObject {

    synchronized public void methodA(){
        try{
            System.out.println("begin methodA threadName=" + Thread.currentThread().getName()+
                                " begin time =" + System.currentTimeMillis());
            Thread.sleep(5000);
            System.out.println("end");
        }catch (InterruptedException e){
            e.printStackTrace();
        }
    }

    synchronized public void methodB(){
        try{
            System.out.println("begin methodB threadName=" + Thread.currentThread().getName() +
                                " begin time =" + System.currentTimeMillis());
            Thread.sleep(5000);
            System.out.println("end");
        }catch (InterruptedException e){
            e.printStackTrace();
        }
    }
}

public class SynchronizedMethodThread extends Thread{

    private MyObject object;

    public SynchronizedMethodThread(MyObject object){
        this.object = object;
    }

    @Override
    public void run() {
        super.run();
        if(Thread.currentThread().getName().equals("A")){
            object.methodA();
        }else{
            object.methodB();
        }
    }
}

測試辦法如下:

public class SynchronizedMethodThreadTest extends TestCase {
    public void testRun() throws Exception {
        MyObject object = new MyObject();
        SynchronizedMethodThread a = new SynchronizedMethodThread(object);
        a.setName("A");
        SynchronizedMethodThread b = new SynchronizedMethodThread(object);
        b.setName("B");

        a.start();
        b.start();

        Thread.sleep(1000 * 15);
    }

}

A,B兩個線程辨別調用methodA與methodB, 兩個辦法也打印出了他們的開端和完畢時間。
後果如下:

begin methodA threadName=A begin time =1483603953885
end
begin methodB threadName=B begin time =1483603958886
end

可以看出兩個辦法是同步伐用,一前一後,後果無穿插。闡明synchronized修飾辦法添加確實實是對象鎖。
這樣,用synchronized修飾的辦法,都需求多線程同步伐用,但是沒用他修飾的辦法,多線程還是直接去調用的。也就是說,雖然多線程會同步伐用synchronized修飾的辦法,但是在一個線程同步伐用辦法的時分,其他線程能夠先調用了非同步辦法,這個在某些時分會有問題。比方呈現髒讀。
A線程先同步伐用了set辦法,但是能夠在set的進程中呈現了等候,然後其他線程在get的時分,數據是set還沒有執行完的數據。看如下代碼:

public class PublicVar {

    public String username = "A";
    public String password = "AA";

    synchronized public void setValue(String username,String password){
        try{
            this.username = username;
            Thread.sleep(3000);
            this.password = password;
            System.out.println("setValue method thread name=" + Thread.currentThread().getName() + " username="
                                + username + " password=" + password);
        }catch (InterruptedException e){
            e.printStackTrace();
        }
    }

    public void getValue(){
        System.out.println("getValue method thread name=" + Thread.currentThread().getName() + " username=" + username
                            + " password=" + password);
    }
}

public class PublicVarThreadA extends Thread {

    private PublicVar publicVar;
    public PublicVarThreadA(PublicVar publicVar){
        this.publicVar = publicVar;
    }

    @Override
    public void run() {
        super.run();
        publicVar.setValue("B","BB");
    }
}

看測試的例子:

public class PublicVarThreadATest extends TestCase {
    public void testRun() throws Exception {
        PublicVar publicVarRef = new PublicVar();
        PublicVarThreadA threadA = new PublicVarThreadA(publicVarRef);
        threadA.start();
        Thread.sleep(40);
        publicVarRef.getValue();
        Thread.sleep(1000 * 5);

    }

}

等待的後果應該是"A","AA",或許是"B","BB",但是後果是:

getValue method thread name=main username=B password=AA
setValue method thread name=Thread-0 username=B password=BB

所以,關於同一個對象中的數據讀與取,都需求用synchronized修飾才干同步。髒讀一定會呈現在操作對象狀況下,多線程"爭搶"對象的後果。
上面,說一些同步辦法其他特性,當一個線程失掉一個對象鎖的時分,他再次懇求對象鎖,一定會再次失掉該對象的鎖。這往往呈現在一個對象辦法裡調用這個對象的另一個辦法,而這兩個辦法都是同步的。這樣設計是有緣由,由於假如不能再次取得這個對象鎖的話,很容易形成死鎖。這種直接獲取鎖的方式稱之為可重入鎖。
Java中的可重入鎖支持在承繼中運用,也就是說可以在子類的同步辦法中調用父類的同步辦法。
上面,看個例子:

public class FatherSynService {

    public int i = 10;
    synchronized public void operateIMainMethod(){
        try{
            i--;
            System.out.println("main print i=" +i);
            Thread.sleep(100);
        }catch (InterruptedException e){
            e.printStackTrace();
        }
    }
}

public class SonSynService extends FatherSynService{

    synchronized public void operateISubMethod(){
        try{
            while (i > 0){
                i--;
                System.out.println("sub print i=" + i);
                Thread.sleep(1000);
                this.operateIMainMethod();
            }
        }catch (InterruptedException e){
            e.printStackTrace();
        }
    }
}

public class SonSynTread extends Thread{
    @Override
    public void run() {
        super.run();
        SonSynService son = new SonSynService();
        son.operateISubMethod();
    }
}

測試的例子如下:

public class SonSynTreadTest extends TestCase {
    public void testRun() throws Exception {
        SonSynTread thread = new SonSynTread();
        thread.start();

        Thread.sleep(1000 * 10);
    }
}

後果就是i是延續輸入的。這闡明,當存在父子類承繼關系時,子類是完全可以經過"可重入鎖"調用父類的同步辦法的。但是在承繼關系中,同步是不會被承繼的,也就是說假如父類的辦法是同步的辦法,但是子類在覆寫該辦法的時分,沒有加同步的修飾,則子類的辦法不算是同步辦法。
關於同步辦法還有一點,就是同步辦法呈現未捕捉的異常,則自動釋放鎖。

關於語句塊的同步處置

關於下面的同步辦法而言,其實是有些弊端的,假如同步辦法是需求執行一個很長時間的義務,那麼多線程在排隊處置同步辦法時就會等候很久,但是一個辦法中,其實並不是一切的代碼都需求同步處置的,只要能夠會發作線程不平安的代碼才需求同步。這時,可以采用synchronized來修飾語句塊讓關鍵的代碼停止同步。用synchronized修飾同步塊,其格式如下:

synchronized(對象){
    //語句塊
}

這裡的對象,可以是以後類的對象this,也可以是恣意的一個Object對象,或許直接承繼自Object的對象,只需保證synchronized修飾的對象被多線程訪問的是同一個,而不是每次調用辦法的時分都是重生成就就可以。但是特別留意String對象,由於JVM有String常量池的緣由,所以相反內容的字符串實踐上就是同一個對象,在用同步語句塊的時分盡能夠不必String。
上面,看一個例子來闡明同步語句塊的用法和與同步辦法的區別:

public class LongTimeTask {
    private String getData1;
    private String getData2;

    public void doLongTimeTask(){
        try{
            System.out.println("begin task");
            Thread.sleep(3000);
            String privateGetData1 = "長時間處置義務後從近程前往的值 1 threadName=" + Thread.currentThread().getName();
            String privateGetData2 = "長時間處置義務後從近程前往的值 2 threadName=" + Thread.currentThread().getName();

            synchronized (this){
                getData1 = privateGetData1;
                getData2 = privateGetData2;
            }

            System.out.println(getData1);
            System.out.println(getData2);
            System.out.println("end task");
        }catch (InterruptedException e){
            e.printStackTrace();
        }
    }
}

public class LongTimeServiceThreadA extends Thread{

    private LongTimeTask task;
    public LongTimeServiceThreadA(LongTimeTask task){
        super();
        this.task = task;
    }

    @Override
    public void run() {
        super.run();
        CommonUtils.beginTime1 = System.currentTimeMillis();
        task.doLongTimeTask();
        CommonUtils.endTime1 = System.currentTimeMillis();
    }
}

public class LongTimeServiceThreadB extends Thread{

    private LongTimeTask task;
    public LongTimeServiceThreadB(LongTimeTask task){
        super();
        this.task = task;
    }

    @Override
    public void run() {
        super.run();
        CommonUtils.beginTime2 = System.currentTimeMillis();
        task.doLongTimeTask();
        CommonUtils.endTime2 = System.currentTimeMillis();
    }
}

測試的代碼如下:

public class LongTimeServiceThreadATest extends TestCase {

    public void testRun() throws Exception {
        LongTimeTask task = new LongTimeTask();
        LongTimeServiceThreadA threadA = new LongTimeServiceThreadA(task);
        threadA.start();

        LongTimeServiceThreadB threadB = new LongTimeServiceThreadB(task);
        threadB.start();

        try{
            Thread.sleep(1000 * 10);
        }catch (InterruptedException e){
            e.printStackTrace();
        }

        long beginTime = CommonUtils.beginTime1;
        if (CommonUtils.beginTime2 < CommonUtils.beginTime1){
            beginTime = CommonUtils.beginTime2;
        }

        long endTime = CommonUtils.endTime1;
        if (CommonUtils.endTime2 < CommonUtils.endTime1){
            endTime = CommonUtils.endTime2;
        }
        System.out.println("耗時:" + ((endTime - beginTime) / 1000));

        Thread.sleep(1000 * 20);
    }

}

後果如下:

begin task
begin task
長時間處置義務後從近程前往的值 1 threadName=Thread-1
長時間處置義務後從近程前往的值 2 threadName=Thread-1
end task
長時間處置義務後從近程前往的值 1 threadName=Thread-1
長時間處置義務後從近程前往的值 2 threadName=Thread-1
end task
耗時:3

兩個線程並發處置耗時義務只用了3s, 由於只在賦值的時分停止同步處置,同步語句塊以外的局部都是多個線程異步處置的。
上面,說一下同步語句塊的一些特性:

  1. 當多個線程同時執行synchronized(x){}同步代碼塊時呈同步效果。
  2. 當其他線程執行x對象中的synchronized同步辦法時呈同步效果。
  3. 當其他線程執行x對象中的synchronized(this)代碼塊時也出現同步效果。

細說一下每個特性,第一個特性下面的例子曾經論述了,就不多說了。第二個特性,由於同步語句塊也是對象鎖,一切當對x加鎖的時分,x對象內的同步辦法也出現同步效果,當x為this的時分,該對象內的其他同步辦法也要等候同步語句塊執行完,才干執行。第三個特性和下面x為this是不一樣的,第三個特性說的是,x對象中有一個辦法,該辦法中有一個synchronized(this)的語句塊的時分,也出現同步效果。即A線程調用了對x加鎖的同步語句塊的辦法,B線程在調用該x對象的synchronized(this)代碼塊是有先後的同步關系。

下面說同步語句塊比同步辦法在某些辦法中執行更無效率,同步語句塊還有一個優點,就是假如兩個辦法都是同步辦法,第一個辦法有限在執行的時分,第二個辦法就永遠不會被執行。這時可以對兩個辦法做同步語句塊的處置,設置不同的鎖對象,則可以完成兩個辦法異步執行。

對類加鎖的同步處置

和對象加鎖的同步處置分歧,對類加鎖的方式也有兩種,一種是synchronized修飾靜態辦法,另一種是運用synchronized(X.class)同步語句塊。在執行上看,和對象鎖分歧都是同步執行的效果,但是和對象鎖卻有實質的不同,對對象加鎖是訪問同一個對象的時分成同步的形態,不同的對象就不會。但是對類加鎖是用這個類的靜態辦法都是出現同步形態。
上面,看這個例子:

public class StaticService {
    synchronized public static void printA(){
        try{
            System.out.println(" 線程稱號為:" + Thread.currentThread().getName()
             + " 在 " + System.currentTimeMillis() + " 進入printA");
            Thread.sleep(1000 * 3);
            System.out.println(" 線程稱號為:" + Thread.currentThread().getName()
                    + " 在 " + System.currentTimeMillis() + " 分開printA");
        }catch (InterruptedException e){
            e.printStackTrace();
        }
    }

    synchronized public static void printB(){
        System.out.println(" 線程稱號為:" + Thread.currentThread().getName()
        + " 在 " + System.currentTimeMillis() +  " 進入printB");
        System.out.println(" 線程稱號為:" + Thread.currentThread().getName()
                + " 在 " + System.currentTimeMillis() +  " 分開printB");
    }

    synchronized public void printC(){
        System.out.println(" 線程稱號為:" + Thread.currentThread().getName()
                + " 在 " + System.currentTimeMillis() +  " 進入printC");
        System.out.println(" 線程稱號為:" + Thread.currentThread().getName()
                + " 在 " + System.currentTimeMillis() +  " 分開printC");
    }
}

測試辦法如下:

public class StaticServiceTest extends TestCase {

    public void testPrint() throws Exception{
        new Thread(new Runnable() {
            public void run() {
                StaticService.printA();
            }
        }).start();

        new Thread(new Runnable() {
            public void run() {
                StaticService.printB();
            }
        }).start();

        new Thread(new Runnable() {
            public void run() {
                new StaticService().printC();
            }
        }).start();

        Thread.sleep(1000 * 3);
    }

}

後果如下:

 線程稱號為:Thread-0 在 1483630533783 進入printA
 線程稱號為:Thread-2 在 1483630533783 進入printC
 線程稱號為:Thread-2 在 1483630533783 分開printC
 線程稱號為:Thread-0 在 1483630536786 分開printA
 線程稱號為:Thread-1 在 1483630536787 進入printB
 線程稱號為:Thread-1 在 1483630536787 分開printB

很分明的看出來,對類加鎖和對對象加鎖兩者辦法是異步執行的,而對類加鎖的兩個辦法是出現同步執行。
其特性也和同步對象鎖一樣。

關於同步加鎖的復雜運用的引見就到這裡了。最後還有留意一點,鎖對象鎖的是該對象的內存地址,其存儲的內容改動,並不會讓多線程並發的時分以為這是不同的鎖。所以改動鎖對象的內容,並不會同步生效。

保證可見性的關鍵字——volatile

在多線程爭搶對象的時分,處置該對象的變量的方式是在主內存中讀取該變量的值到線程公有的內存中,然後對該變量做處置,處置後將值在寫入到主內存中。下面舉的例子,之所以呈現後果與預期不分歧都是由於線程自己將值復制到自己的公有棧後修正後果而不知道其他線程的修正後果。假如我們不必同步的話,我們就需求一個能堅持可見的,知道其他線程修正後果的辦法。JDK提供了volatile關鍵字,來堅持可見性,關鍵字volatile的作用是強迫從公共堆棧中獲得變量的值,而不是從線程公有數據棧中獲得變量值。但是該關鍵字並不能保證原子性,以爭搶一個對象中的count變量來看下圖的詳細闡明:
變量在線程私有棧與主內存的關系

java 渣滓回收整理一文中,描繪了jvm運轉時辰內存的分配。其中有一個內存區域是jvm虛擬機棧,每一個線程運轉時都有一個線程棧,線程棧保管了線程運轉時分變量值信息。當線程訪問某一個對象時分值的時分,首先經過對象的援用找到對應在堆內存的變量的值,然後把堆內存變量的詳細值load到線程本地內存中,樹立一個變量正本,之後線程就不再和對象在堆內存變量值有任何關系,而是直接修正正本變量的值,在修正完之後的某一個時辰(線程加入之前),自動把線程變量正本的值回寫到對象在堆中變量。這樣在堆中的對象的值就發生變化了。

volatile在此進程中的詳細闡明如下:

read and load 從主存復制變量到以後任務內存
use and assign 執行代碼,改動共享變量值
store and write 用任務內存數據刷新主存相關內容
其中use and assign 可以屢次呈現
但是這一些操作並不是原子性,也就是 在read load之後,假如主內存count變量發作修正之後,線程任務內存中的值由於曾經加載,不會發生對應的變化,所以計算出來的後果會和預期不一樣關於volatile修飾的變量,jvm虛擬機只是保證從主內存加載到線程任務內存的值是最新的例如假設線程1,線程2 在停止read,load 操作中,發現主內存中count的值都是5,那麼都會加載這個最新的值在線程1堆count停止修正之後,會write到主內存中,主內存中的count變量就會變為6線程2由於曾經停止read,load操作,在停止運算之後,也會更新主內存count的變量值為6招致兩個線程及時用volatile關鍵字修正之後,還是會存在並發的狀況。

上述關於volatile的解析均摘自java中volatile關鍵字的含義

總結

至此,關於Java同步的知識就告一段落了,上文講的都是比擬深刻的用法,我放在github的代碼中有更多的例子,地址是:char02
關於多線程通訊的知識就放在了char03的代碼中。


  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved