問題引出
大家可能聽過「Automic」原子類,這些類在多線程下可以保證線程安全。比如想要實作自增,多線程下會出現少增加的情況。
public class VolatileAtomicTest {
public static int num = 0;
public static void increase() {
num++;
}
public static void main(String[] args) throws InterruptedException {
Thread[] threads = new Thread[10];
for (int i = 0; i < threads.length; i++) {
threads[i] = new Thread(() -> {
for (int j = 0; j < 1000; j++) {
increase();
}
});
threads[i].start();
}
for (Thread thread : threads) {
thread.join();
}
System.out.println(num);
}
}
上面代碼建立了10個線程,這10個線程要依次循環1000次,每次增加1,最後要求。
但實際情況是
![](https://img.laitimes.com/img/9ZDMuAjOiMmIsIjOiQnIsIiZpdmLzADN1UDOzETMxMDOwAjMwIzLc52YucWbp5GZzNmLn9Gbi1yZtl2Lc9CX6MHc0RHaiojIsJye.gif)
多線程情況下會出現第一個線程還在運算,第二個線程就運算完并覆寫了第一個線程的值運算結果,是以會出現與預期不符的結果。原因在
public static void increase() {
num++;
}
num++
我們可以拆解為
int a = num + 1; //步驟1
num = a; //步驟2
如果線程一隻執行到步驟1,還沒執行到步驟2,線程二這時執行了步驟2。那麼num的值就是線程2計算的值,而線程一的值就覆寫了。
如果我們保證這兩步的原子性(操作一體,不能被其他線程插入)就可以得到預期結果。我們在這個方法下面加鎖即可。
public synchronized static void increase() {
num++;
}
或者
static Lock lock = new ReentrantLock();
public static void increase() {
try {
lock.lock();
num++;
} finally {
lock.unlock();
}
}
![](https://img.laitimes.com/img/9ZDMuAjOiMmIsIjOiQnIsIiZpdmLzADN1UDOzETMxMDOwAjMwIzLc52YucWbp5GZzNmLn9Gbi1yZtl2Lc9CX6MHc0RHaiojIsJye.gif)
但是,上面的方法都使用了鎖,在多線程下對性能還是有影響的。我們可以使用無鎖化的原子類,實作原子自增。
@Test
public void test() throws InterruptedException {
Thread[] threads = new Thread[10];
for (int i = 0; i < threads.length; i++) {
threads[i] = new Thread(() -> {
for (int j = 0; j < 1000; j++) {
num = atomicInteger.incrementAndGet();
}
});
threads[i].start();
}
for (Thread thread : threads) {
thread.join();
}
System.out.println(num);
}
原子類
上圖就是Java原子類的全家桶,主要是通過CAS + 自旋實作的。這裡我們主要說說
AtomicInteger
,來看看
incrementAndGet()
方法。Java8增加了一些類,優化自選帶來的性能問題。
/**
* Atomically increments by one the current value.
*
* @return the previous value
*/
public final int getAndIncrement() {
return unsafe.getAndAddInt(this, valueOffset, 1);
}
/ setup to use Unsafe.compareAndSwapInt for updates
private static final Unsafe unsafe = Unsafe.getUnsafe();
發現用了
Unsafe
的
getAndAddInt
方法。至于
Unsafe
,底層也是作業系統的類,可以直接修改作業系統記憶體,或者排程線程。看着名字就知道不建議程式員使用它。
public final int getAndAddInt(Object var1, long var2, int var4) {
int var5;
do {
var5 = this.getIntVolatile(var1, var2);//從該對象對應位址取出變量值
} while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));
return var5;
}
-
:var1
Object
-
:var2
valueOffset
-
: 目前該變量在記憶體中的值var5
-
: 需要寫進去的值var5 + var4
這裡就是通過CAS修改值,不斷循環,知道修改成功。但在高并發情況下,存在一些問題:
❝
高并發量的情況下,由于真正更新成功的線程占少數,容易導緻循環次數過多,浪費時間,并且浪費線程資源。
由于需要保證變量真正的共享,「緩存一緻性」開銷變大。
❞
getIntVolatile()
方法是系統的本地方法
public native int getIntVolatile(Object var1, long var2);
compareAndSwapInt()
也是本地方法,這裡就是常說的CAS了。
public final native boolean compareAndSwapInt(Object var1, long var2, int var4, int var5);
手寫 AtomicInteger
AtomicInteger
「首先定義幾個變量」
private volatile int value;
private static long offset;//偏移位址
private static Unsafe unsafe;
我們定義了
value
值,用來儲存目前的值。offset偏移位址,在類初始化的時候,計算出value變量在對象中的偏移。
Unsafe
類,直接作業系統記憶體。
「初始化變量」
// 通過Unsafe計算出value變量在對象中的偏移
static {
try {
Field theUnsafeField = Unsafe.class.getDeclaredField("theUnsafe");
theUnsafeField.setAccessible(true);
unsafe = (Unsafe) theUnsafeField.get(null);
Field field = MyAtomicInteger.class.getDeclaredField("value");
offset = unsafe.objectFieldOffset(field);//獲得偏移位址
} catch (Exception e) {
e.printStackTrace();
}
}
我們反射擷取
Unsafe
的
theUnsafe
字段,自定義的
value
字段,還有偏移位址
offset
。
「自增方法」
public void increment(int num) {
int tempValue;
do {
tempValue = unsafe.getIntVolatile(this, offset);//拿到值
} while (!unsafe.compareAndSwapInt(this, offset, tempValue, value + num));//CAS自旋
}
「測試結果」
public class MyAtomTest {
public static void main(String[] args) {
Thread[] threads = new Thread[10];
MyAtomicInteger atomicInteger = new MyAtomicInteger();
for (int i = 0; i < 10; i++) {
threads[i] = new Thread(() -> {
for (int j = 0; j < 1000; j++) {
atomicInteger.increment(1); //自增1
}
});
threads[i].start();
}
for (int i = 0; i < threads.length; i++) {
try {
threads[i].join();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("x=" + atomicInteger.get());
}
}
「Atomic」原子類解決了高并發下線程安全問題,但是高并發下也帶來了性能問題,如果你的項目需要使用原子類,并且性能要求高,可以使用「Java8」中的原子類。
往期推薦
- 我寫出這樣幹淨的代碼,老闆直誇我
- 雲南麗江旅遊攻略
- 使用ThreadLocal怕記憶體洩漏?
- Java進階之路思維導圖
- 程式員必看書籍推薦
- 3萬字的Java後端面試總結(附PDF)
掃碼二維碼,擷取更多精彩。或微信搜Lvshen_9,可背景回複擷取資料
1.回複"java" 擷取java電子書;
2.回複"python"擷取python電子書;
3.回複"算法"擷取算法電子書;
4.回複"大資料"擷取大資料電子書;
5.回複"spring"擷取SpringBoot的學習視訊。
6.回複"面試"擷取一線大廠面試資料
7.回複"進階之路"擷取Java進階之路的思維導圖
8.回複"手冊"擷取阿裡巴巴Java開發手冊(嵩山終極版)
9.回複"總結"擷取Java後端面試經驗總結PDF版
10.回複"Redis"擷取Redis指令手冊,和Redis專項面試習題(PDF)
另:點選【我的福利】有更多驚喜哦。