簡(jiǎn)單聊一聊公平鎖和非公平鎖,Parallel并行流
大家好,我是哪吒。
上一章提到了一個(gè)關(guān)于 i++ 和 ++i 的面試題打趴了所有人,最終方案是在兩個(gè)方法上添加synchronized關(guān)鍵字,從而避免i++的線程安全問題,不過,這樣真的好嗎?在所有有線程安全的方法都添加synchronized?
答案是顯而易見的,不行。
synchronized會(huì)極大的降低程序的性能,導(dǎo)致整個(gè)程序幾乎只能支持單線程操作,性能顯著降低。
那么,如何解決呢?
一、降低鎖的粒度,將synchronized關(guān)鍵字不放在方法上了,改為synchronized代碼塊。
鎖的粒度更小了,也解決了這個(gè)問題,確實(shí)可以的。
package com.guor.thread;
public class SynchronizedTest2 {
int a = 1;
int b = 1;
public void add() {
System.out.println("add start");
synchronized (this) {
for (int i = 0; i < 10000; i++) {
a++;
b++;
}
}
System.out.println("add end");
}
public synchronized void compare() {
System.out.println("compare start");
synchronized (this) {
for (int i = 0; i < 10000; i++) {
boolean flag = a < b;
if (flag) {
System.out.println("a=" + a + ",b=" + b + "flag=" + flag + ",a < b = " + (a < b));
}
}
}
System.out.println("compare end");
}
public static void main(String[] args) {
SynchronizedTest2 synchronizedTest = new SynchronizedTest2();
new Thread(() -> synchronizedTest.add()).start();
new Thread(() -> synchronizedTest.compare()).start();
}
}
為了更好的優(yōu)化,有的時(shí)候可以將synchronized代碼塊變?yōu)閰^(qū)分讀寫場(chǎng)景的讀寫鎖,也可以考慮悲觀鎖和樂觀鎖的區(qū)分。
對(duì)于讀寫場(chǎng)景比較多的情況,可以使用ReentrantReadWriteLock區(qū)分讀寫,再次降低鎖的粒度,提高程序的性能。
ReentrantReadWriteLock 還可以選擇提供了公平鎖,在沒有明確必須使用公平鎖的情況下,盡量不要使用公平鎖,公平鎖會(huì)使程序性能降低很多很多。
二、先區(qū)分一下公平鎖和非公平鎖
- 公平鎖:多個(gè)線程按照申請(qǐng)鎖的順序去獲得鎖,線程會(huì)直接進(jìn)入隊(duì)列去排隊(duì),永遠(yuǎn)都是隊(duì)列的第一個(gè)得到鎖。
- 非公平鎖:多個(gè)線程去獲取鎖的時(shí)候,會(huì)直接去嘗試獲取,獲取不到,進(jìn)入等待隊(duì)列,如果能獲取到,就直接獲取到鎖。
簡(jiǎn)單來(lái)說(shuō),公平鎖(誰(shuí)先排隊(duì),誰(shuí)先執(zhí)行),非公平鎖(不用排隊(duì),每個(gè)人都有機(jī)會(huì))。
1、公平鎖
有一天早上,云韻、美杜莎、小醫(yī)仙結(jié)伴去買醬香拿鐵,到了咖啡店,先排隊(duì),一個(gè)一個(gè)來(lái)。不一會(huì),哪吒來(lái)了,也買醬香拿鐵,只能在末尾排隊(duì)。這個(gè)就是公平鎖。
2、非公平鎖
但是呢?第二天早上,哪吒又去買醬香拿鐵,上一次去晚了沒買到(線程被餓死了),這次急了,要插隊(duì)買,不講武德。終于喝上了心心念念的醬香拿鐵,這個(gè)就是非公平鎖。
3、公平鎖的優(yōu)缺點(diǎn):
- 優(yōu)點(diǎn):所有線程都會(huì)獲取到鎖,只是一個(gè)時(shí)間的問題,不會(huì)出現(xiàn)有線程被餓死的情況;
- 缺點(diǎn):吞吐量會(huì)下降很多,隊(duì)列里只有第一個(gè)線程能獲取到鎖,其他的線程都會(huì)阻塞,cpu喚醒阻塞線程的開銷會(huì)增大。
4、非公平鎖的優(yōu)缺點(diǎn):
- 優(yōu)點(diǎn):可以減少CPU喚醒線程的開銷,整體的吞吐效率會(huì)高點(diǎn),CPU也不必取喚醒所有線程,會(huì)減少喚起線程的數(shù)量。
- 缺點(diǎn):如果運(yùn)氣不好,會(huì)出現(xiàn)一致獲取不到鎖的情況,會(huì)被活活的餓死。
三、是否對(duì)癥下藥
我們都知道,靜態(tài)字段屬于類,類級(jí)別的鎖才能保護(hù);非靜態(tài)字段屬于類實(shí)例,實(shí)例級(jí)別的鎖才能保護(hù)。
先看一下下面的代碼:
import lombok.Data;
import java.util.stream.IntStream;
@Data
public class LockTest {
public static void main(String[] args) {
IntStream.rangeClosed(1, 100000).parallel().forEach(i -> new LockTest().increase());
System.out.println(time);
}
private static int time = 0;
public synchronized void increase() {
time++;
}
}
在LockTest類中定義一個(gè)靜態(tài)變量time,定義一個(gè)非靜態(tài)方法increase(),實(shí)現(xiàn)time++自增。先累加10萬(wàn)次,測(cè)試一下??纯词欠裼芯€程安全的問題。
這...不對(duì)啊,上一節(jié)在介紹高并發(fā)下i++線程安全問題的時(shí)候,synchronized 是好使的啊。
今天這是怎么了?再運(yùn)行一次,結(jié)果依然如此,不等于100000
先來(lái)分析一下。
在非靜態(tài)的方法上加synchronized,只能確保多個(gè)線程無(wú)法執(zhí)行同一個(gè)實(shí)例的increase()方法,卻不能保證不同實(shí)例的increase()方法。靜態(tài)的變量time,在多個(gè)線程中共享,所以會(huì)出現(xiàn)線程安全的問題,synchronized失效了。
那么,將synchronized改為靜態(tài)方法是不是就可以了,試一下。
有兩種寫法,一種是直接將方法改為靜態(tài)方法,一種是使用synchronized代碼塊。
private static Object obj= new Object();
public void increase() {
synchronized (obj) {
time++;
}
}
四、IntStream.rangeClosed是干嘛的?
很多小伙伴,可能會(huì)好奇,這個(gè)是干什么的,干了5年后端代碼開發(fā)了,沒見過這玩意兒。
IntStream是一種特殊的stream,用來(lái)提供對(duì)int相關(guān)的stream操作。
IntStream.rangeClosed:生成某個(gè)數(shù)字范圍內(nèi)的數(shù)字集合的stream。
比如上面代碼中的IntStream.rangeClosed(1, 100000).parallel().forEach(i -> new LockTest().increase());。
- range:不包含10000
- rangeClosed:包含10000
五、parallel是干嘛的?
1、parallel()是什么
Stream.parallel() 方法是 Java 8 中 Stream API 提供的一種并行處理方式。在處理大量數(shù)據(jù)或者耗時(shí)操作時(shí),使用 Stream.parallel() 方法可以充分利用多核 CPU 的優(yōu)勢(shì),提高程序的性能。
Stream.parallel() 方法是將串行流轉(zhuǎn)化為并行流的方法。通過該方法可以將大量數(shù)據(jù)劃分為多個(gè)子任務(wù)交由多個(gè)線程并行處理,最終將各個(gè)子任務(wù)的計(jì)算結(jié)果合并得到最終結(jié)果。使用 Stream.parallel() 可以簡(jiǎn)化多線程編程,減少開發(fā)難度。
需要注意的是,并行處理可能會(huì)引入線程安全等問題,需要根據(jù)具體情況進(jìn)行選擇。
2、舉一個(gè)簡(jiǎn)單的demo
定義一個(gè)list,然后通過parallel() 方法將集合轉(zhuǎn)化為并行流,對(duì)每個(gè)元素進(jìn)行i++,最后通過 collect(Collectors.toList()) 方法將結(jié)果轉(zhuǎn)化為 List 集合。
使用并行處理可以充分利用多核 CPU 的優(yōu)勢(shì),加快處理速度。
public class StreamTest {
public static void main(String[] args) {
List<Integer> list = new ArrayList<>();
for (int i = 0; i < 10; i++) {
list.add(i);
}
System.out.println(list);
List<Integer> result = list.stream().parallel().map(i -> i++).collect(Collectors.toList());
System.out.println(result);
}
}
我勒個(gè)去,什么情況?
這是大部分開發(fā)人員都會(huì)犯的小錯(cuò)誤,在上篇中提到過,i++ 返回原來(lái)的值,++i 返回加1后的值。這誰(shuí)都知道,可是,寫的時(shí)候,就不一定了,因?yàn)槟懔?xí)慣了i++,寫順手了,寫的時(shí)候也是心不在焉,一蹴而就了。
i++改了++i即可。
3、parallel()的優(yōu)缺點(diǎn)
優(yōu)點(diǎn):
- 充分利用多核 CPU 的優(yōu)勢(shì),提高程序的性能;
- 可以簡(jiǎn)化多線程編程,減少開發(fā)難度。
缺點(diǎn):
- 并行處理可能會(huì)引入線程安全等問題,需要根據(jù)具體情況進(jìn)行選擇;
- 并行處理需要付出額外的開銷,例如線程池的創(chuàng)建和銷毀、線程切換等,對(duì)于小數(shù)據(jù)量和簡(jiǎn)單計(jì)算而言,串行處理可能更快。
4、何時(shí)使用parallel()?
在實(shí)際開發(fā)中,應(yīng)該根據(jù)數(shù)據(jù)量、計(jì)算復(fù)雜度、硬件等因素綜合考慮。
比如:
- 數(shù)據(jù)量較大,有1萬(wàn)個(gè)元素;
- 計(jì)算復(fù)雜度過大,需要對(duì)每個(gè)元素進(jìn)行復(fù)雜的計(jì)算;
- 硬件夠硬,比如多核CPU。