天天看點

變量和類的線程安全變量的線程安全常見線程安全類

變量的線程安全

成員變量和靜态變量是否線程安全?

如果它們沒有共享,則線程安全

如果它們被共享了,根據它們的狀态是否能夠改變,又分兩種情況

如果隻有讀操作,則線程安全

如果有讀寫操作,則這段代碼是臨界區,需要考慮線程安全

局部變量是否線程安全?

局部變量是線程安全的

但局部變量引用的對象則未必

如果該對象沒有逃離方法的作用通路,它是線程安全的

如果該對象逃離方法的作用範圍,需要考慮線程安全 (經過逃逸分析)

局部變量線程安全分析

package com.dongguo.sync;

/**
 * @author Dongguo
 * @date 2021/9/11 0011-14:29
 * @description:
 */
public class SafeDemo {
    public static void test1() {
        int i = 10;
        i++;
    }

    public static void main(String[] args) {

    }
}
           

每個線程調用 test1() 方法時局部變量 i,會在每個線程的棧幀記憶體中被建立多份,是以不存在共享

位元組碼檔案

public static void test1();
    descriptor: ()V
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=1, locals=1, args_size=0
         0: bipush        10   //讀取10
         2: istore_0			//指派給i
         3: iinc          0, 1	//執行i++
         6: return
      LineNumberTable:
        line 10: 0
        line 11: 3
        line 12: 6
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            3       4     0     i   I
           

如圖

變量和類的線程安全變量的線程安全常見線程安全類

局部變量的引用稍有不同

成員變量線程安全分析

package com.dongguo.sync;

import java.util.ArrayList;

/**
 * @author Dongguo
 * @date 2021/9/11 0011-14:45
 * @description:
 */
public class UnSafeDemo {
    static final int THREAD_NUMBER = 2;
    static final int LOOP_NUMBER = 200;

    public static void main(String[] args) {
        ThreadUnsafe test = new ThreadUnsafe();
        for (int i = 1; i <= THREAD_NUMBER; i++) {
            new Thread(() -> {
                test.method1(LOOP_NUMBER);
            }, "t" + i).start();
        }
    }
}

class ThreadUnsafe {
    ArrayList<String> list = new ArrayList<>();

    public void method1(int loopNumber) {
        for (int i = 0; i < loopNumber; i++) {
            // { 臨界區, 會産生競态條件
            method2();
            method3();
            // } 臨界區
        }
    }

    private void method2() {
        list.add("1");
    }

    private void method3() {
        list.remove(0);
    }
}
運作結果
Exception in thread "t2" java.lang.IndexOutOfBoundsException: Index: 0, Size: 0
	at java.util.ArrayList.rangeCheck(ArrayList.java:659)
	at java.util.ArrayList.remove(ArrayList.java:498)
	at com.dongguo.sync.ThreadUnsafe.method3(UnSafeDemo.java:41)
	at com.dongguo.sync.ThreadUnsafe.method1(UnSafeDemo.java:31)
	at com.dongguo.sync.UnSafeDemo.lambda$main$0(UnSafeDemo.java:18)
	at java.lang.Thread.run(Thread.java:748)
           

分析:

無論哪個線程中的 method2 引用的都是同一個對象中的 list 成員變量

method3 與 method2 分析相同

變量和類的線程安全變量的線程安全常見線程安全類

将 list 修改為局部變量

package com.dongguo.sync;

import java.util.ArrayList;

/**
 * @author Dongguo
 * @date 2021/9/11 0011-14:45
 * @description:
 */
public class UnSafeDemo {
    static final int THREAD_NUMBER = 2;
    static final int LOOP_NUMBER = 200;

    public static void main(String[] args) {
        ThreadUnsafe test = new ThreadUnsafe();
        for (int i = 1; i <= THREAD_NUMBER; i++) {
            new Thread(() -> {
                test.method1(LOOP_NUMBER);
            }, "t" + i).start();
        }
    }
}

class ThreadUnsafe {
    public void method1(int loopNumber) {
        ArrayList<String> list = new ArrayList<>();
        for (int i = 0; i < loopNumber; i++) {
            // { 臨界區, 會産生競态條件
            method2(list);
            method3(list);
            // } 臨界區
        }
    }

    private void method2(ArrayList<String> list) {
        list.add("1");
    }

    private void method3(ArrayList<String> list) {
        list.remove(0);
    }
}
           

那麼就不會有上述問題了

分析:

list 是局部變量,每個線程調用時會建立其不同執行個體,沒有共享

而 method2 的參數是從 method1 中傳遞過來的,與 method1 中引用同一個對象

method3 的參數分析與 method2 相同

變量和類的線程安全變量的線程安全常見線程安全類

常見線程安全類

String

Integer

StringBuffer

Random

Vector

Hashtable

java.util.concurrent 包下的類

這裡說它們是線程安全的是指,多個線程調用它們同一個執行個體的某個方法時,是線程安全的。也可以了解為

Hashtable table = new Hashtable();
new Thread(() -> {
    table.put("key", "value1");
}).start();
new Thread(() -> {
    table.put("key", "value2");
}).start();
           

它們的每個方法是原子的

即Hashtable的put方法時線程安全的 ,使用synchronized同步方法

public synchronized V put(K key, V value) {
    // Make sure the value is not null
    if (value == null) {
        throw new NullPointerException();
    }

    // Makes sure the key is not already in the hashtable.
    Entry<?,?> tab[] = table;
    int hash = key.hashCode();
    int index = (hash & 0x7FFFFFFF) % tab.length;
    @SuppressWarnings("unchecked")
    Entry<K,V> entry = (Entry<K,V>)tab[index];
    for(; entry != null ; entry = entry.next) {
        if ((entry.hash == hash) && entry.key.equals(key)) {
            V old = entry.value;
            entry.value = value;
            return old;
        }
    }

    addEntry(hash, key, value, index);
    return null;
}
           

線程安全類方法的組合

分析下面代碼是否線程安全?

package com.dongguo.sync;

import java.util.Hashtable;

/**
 * @author Dongguo
 * @date 2021/9/11 0011-15:05
 * @description:
 */
public class UnSafeDemo2 {

    public static void main(String[] args) {
        
    }

    public static void putValue(String value) {
        Hashtable table = new Hashtable();
        // 線程1,線程2
        if (table.get("key") == null) {
            table.put("key", value);
        }
    }
}
           

hashtable的get()、put()都是線程安全的

但是在使用get()和put()之間,有可能其他的線程也會執行get()、put()

變量和類的線程安全變量的線程安全常見線程安全類

不可變類線程安全性

String、Integer 等都是不可變類,因為其内部的狀态不可以改變,是以它們的方法都是線程安全的

有同學或許有疑問,String 有 replace,substring 等方法【可以】改變值啊,那麼這些方法又是如何保證線程安

全的呢?

以substring 為例

public String substring(int beginIndex) {
    if (beginIndex < 0) {
        throw new StringIndexOutOfBoundsException(beginIndex);
    }
    int subLen = value.length - beginIndex;
    if (subLen < 0) {
        throw new StringIndexOutOfBoundsException(subLen);
    }
    return (beginIndex == 0) ? this : new String(value, beginIndex, subLen);
}

    public String(char value[], int offset, int count) {
        if (offset < 0) {
            throw new StringIndexOutOfBoundsException(offset);
        }
        if (count <= 0) {
            if (count < 0) {
                throw new StringIndexOutOfBoundsException(count);
            }
            if (offset <= value.length) {
                this.value = "".value;
                return;
            }
        }
        // Note: offset or count might be near -1>>>1.
        if (offset > value.length - count) {
            throw new StringIndexOutOfBoundsException(offset + count);
        }
        this.value = Arrays.copyOfRange(value, offset, offset+count);
    }
           

(beginIndex == 0) ? this : new String(value, beginIndex, subLen);

當beginIndex =0,傳回這個String對象如果beginIndex !=0建立一個新的String對象

Arrays.copyOfRange(value, offset, offset+count); 将複制的char[]指派給這個新的String對象