柚子快報(bào)激活碼778899分享:Kotlin多線程
柚子快報(bào)激活碼778899分享:Kotlin多線程
目錄
線程的使用
線程的創(chuàng)建
例一:創(chuàng)建線程并輸出Hello World
Thread對(duì)象的用法
start()
join()
interrupt()
線程安全
原子性
可見性
有序性
線程鎖
ReentrantLock
ReadWriteLock
線程的使用
Java虛擬機(jī)中的多線程可以1:1映射至CPU中,即一個(gè)CPU線程跑一個(gè)任務(wù),這叫并行,也可以N:1地運(yùn)行,即一個(gè)CPU線程交替跑多個(gè)任務(wù),看起來是同時(shí)地。這兩種方法都叫并發(fā)
線程的創(chuàng)建
kotlin中,可以通過kotlin.concurrent包下的thread函數(shù)創(chuàng)建一個(gè)線程:
fun thread(
start: Boolean = true,
isDaemon: Boolean = false,
contextClassLoader: ClassLoader? = null,
name: String? = null,
priority: Int = -1,
block: () -> Unit
): Thread
該函數(shù)接收6個(gè)參數(shù),必須定義block參數(shù),因?yàn)樗蔷€程的執(zhí)行函數(shù):
start: 如果為真,則立即執(zhí)行isDaemon: 如果為真,則會(huì)創(chuàng)建守護(hù)線程。當(dāng)所有正在運(yùn)行的線程都是守護(hù)線程時(shí),Java虛擬機(jī)將自動(dòng)退出contextClassLoader: 線程中所使用的類加載器,又叫上下文類加載器。如果不指定類加載器,則會(huì)使用系統(tǒng)的類加載器name: 線程的名字priority: 線程的優(yōu)先級(jí)。只有該參數(shù)大于0時(shí)才有效。線程的優(yōu)先級(jí)在1-10之間,默認(rèn)為5. 線程的優(yōu)先級(jí)的最大值、最小值、默認(rèn)值被分別定義在java.long包下Thread類的靜態(tài)變量MAX_PRIORITY、MIN_PRIORITY和NORM_PRIORITY內(nèi)block: 一個(gè)回調(diào)函數(shù),無參數(shù),無返回值,線程運(yùn)行調(diào)用此方法
該函數(shù)返回一個(gè)java.long包下的Thread對(duì)象,表示創(chuàng)建的線程。
因?yàn)樵摵瘮?shù)的前五個(gè)參數(shù)都有默認(rèn)值,因此可以使用kotlin的語法糖,簡(jiǎn)化thread的用法:
thread {
println("Hello World")
}
例一:創(chuàng)建線程并輸出Hello World
import kotlin.concurrent.thread
fun main() {
thread {
println("Hello World")
}
}
這就是這個(gè)例子的全部代碼了,是不是非常簡(jiǎn)單?
在main方法里,創(chuàng)建了一個(gè)線程,線程執(zhí)行時(shí)打印Hello World.
Thread對(duì)象的用法
我們提到,thread函數(shù)會(huì)返回一個(gè)Thread對(duì)象,那么,如何使用這個(gè)Thread對(duì)象呢?
首先,Thread類是用Java寫的,所以它的函數(shù)原型是Java形式的
start()
Thread對(duì)象中有start()方法,表示執(zhí)行線程:
public void start()
如果我們?cè)趖hread方法中設(shè)置start參數(shù)為false,那么我們可以通過調(diào)用start()方法執(zhí)行線程:
import kotlin.concurrent.thread
fun main() {
val th = thread(start = false) {
println("Hello World")
}
println("準(zhǔn)備啟動(dòng)線程")
th.start()
}
執(zhí)行結(jié)果:
準(zhǔn)備啟動(dòng)線程
Hello World
join()
join()方法等待線程執(zhí)行結(jié)束:
public final void join()
throws InterruptedException
如我們可以這樣使用:
import kotlin.concurrent.thread
fun main() {
val th = thread {
Thread.sleep(1000)
println("th執(zhí)行完成")
}
th.join()
println("main執(zhí)行完成")
}
?執(zhí)行結(jié)果如下:
th執(zhí)行完成
main執(zhí)行完成
因此,join成功是main線程等待th線程結(jié)束
如果我們?nèi)サ魌h.join(),則輸出:
main執(zhí)行完成
th執(zhí)行完成
這就是join()的基本用法
另外,如果當(dāng)前線程(調(diào)用join()方法的線程)被任何線程中斷,則拋出InterruptedException
異常,并不再等待:
import kotlin.concurrent.thread
fun main() {
val th = thread {
val th2 = thread {
Thread.sleep(1000)
println("th2執(zhí)行完成")
}
try {
th2.join()
}catch (e: InterruptedException){
println("中斷")
}
println("th執(zhí)行完成")
}
th.interrupt()
}
?執(zhí)行結(jié)果:
中斷
th執(zhí)行完成
th2執(zhí)行完成
因?yàn)閙ain線程創(chuàng)建了th線程,th線程又創(chuàng)建了th2線程。th線程調(diào)用join()方法等待th2線程時(shí),main線程中斷了th線程,因此th線程中的join()方法停止等待,執(zhí)行完成。之后,th2線程才執(zhí)行完成
interrupt()
interrupt()中斷線程。調(diào)用該方法時(shí),將會(huì)把指定線程的Thread.interrupted()方法的返回值設(shè)為true,因此,要中斷線程需要檢測(cè)這個(gè)值。
public void interrupt()
?其用法如下:
import kotlin.concurrent.thread
fun main() {
val th = thread {
while (true){
if (Thread.interrupted()) break
}
println("th被中斷")
}
Thread.sleep(1000)
println("準(zhǔn)備中斷線程")
th.interrupt()
}
輸出:
準(zhǔn)備中斷線程
th被中斷
線程安全
線程安全必須同時(shí)滿足原子性、可見性和有序性:
原子性
考慮這么一個(gè)代碼:
import kotlin.concurrent.thread
fun main() {
var tmp = 0
val th1 = thread {
Thread.sleep(200)
tmp++
}
val th2 = thread {
Thread.sleep(200)
tmp++
}
th1.join()
th2.join()
println(tmp)
}
其中,tmp被增加了2次,因此應(yīng)該返回2,可是我的輸出結(jié)果為:
1
這是為什么呢?
我們知道,自增語句分三步:讀取、增加、寫入。在兩個(gè)線程同時(shí)執(zhí)行的時(shí)候,可能會(huì)出現(xiàn)類似以下情況:
時(shí)間第一個(gè)線程第二個(gè)線程1讀取tmp變量(0)2計(jì)算tmp+1的值3讀取tmp變量(0)4寫入tmp+1的值到tmp變量(1)5計(jì)算tmp+1的值6寫入tmp+1的值到tmp變量(1)
因此,由于線程之間并發(fā)運(yùn)行,最終tmp的值為1。
之所以自增語句會(huì)出現(xiàn)這樣的問題,是因?yàn)樽栽稣Z句需要3塊時(shí)間才能完成,不能一口氣直接完成。如果自增可以直接完成,在非并行的情況下,就會(huì)出現(xiàn)以下情況:
時(shí)間第一個(gè)線程第二個(gè)線程1tmp自增2tmp自增
這樣就不會(huì)有沖突了。
我們稱這種直接完成而不被其他線程打斷的操作叫原子操作,在kotlin中可以通過java.util.concurrent.atomic定義的支持原子操作的類,實(shí)現(xiàn)原子操作:
import java.util.concurrent.atomic.AtomicInteger
import kotlin.concurrent.thread
fun main() {
val tmp = AtomicInteger(0)
val th1 = thread {
Thread.sleep(200)
tmp.incrementAndGet()
}
val th2 = thread {
Thread.sleep(200)
tmp.incrementAndGet()
}
th1.join()
th2.join()
println(tmp.get())
}
注意:原子操作不適合并行時(shí)的問題,但由于現(xiàn)代電腦CPU少線程多的現(xiàn)狀,大部分的情況都可以使用原子操作:
一個(gè)12核CPU有將近4000個(gè)線程
可見性
由于現(xiàn)代設(shè)備的線程有自己的緩存,有些時(shí)候當(dāng)一個(gè)變量被修改后,其他線程可能看不到修改的信息,因此就會(huì)產(chǎn)生線程安全問題:
import kotlin.concurrent.thread
fun main() {
var boolean = true
val th1 = thread {
Thread.sleep(200)
boolean = false
println("已經(jīng)將boolean設(shè)為false")
}
val th2 = thread {
println("等待boolean為false")
while (boolean){}
}
th1.join()
th2.join()
println("線程執(zhí)行完畢")
}
執(zhí)行結(jié)果:
等待boolean為false
已經(jīng)將boolean設(shè)為false
(無限循環(huán))
?這是因?yàn)?,?dāng)boolean被修改時(shí),th2不能及時(shí)獲得boolean的變化,所以跳不出循環(huán),出現(xiàn)了可見性問題。我們可以通過Thread.yield()方法同步變量在線程內(nèi)和進(jìn)程內(nèi)的數(shù)據(jù):
import kotlin.concurrent.thread
fun main() {
var boolean = true
val th1 = thread {
Thread.sleep(200)
boolean = false
println("已經(jīng)將boolean設(shè)為false")
}
val th2 = thread {
println("等待boolean為false")
while (boolean){
Thread.yield()
}
}
th1.join()
th2.join()
println("線程執(zhí)行完畢")
}
執(zhí)行結(jié)果:
等待boolean為false
已經(jīng)將boolean設(shè)為false
線程執(zhí)行完畢
注意,Thread.yield()方法的真實(shí)作用是告訴調(diào)度器當(dāng)前線程愿意放棄對(duì)處理器的使用,直到處理器重新調(diào)用這個(gè)線程,可以用以下表格來說明:
因此,Thread.yield()方法就可以抽空在合適的時(shí)機(jī)同步變量的數(shù)據(jù),實(shí)現(xiàn)線程的可見性。
我們前面舉的變量自增的例子也有可能是因?yàn)榫€程的可見性問題導(dǎo)致的。
有序性
我們?cè)趯懘a時(shí),往往認(rèn)為程序是按順序運(yùn)行的,其實(shí)并不是。如果前后兩個(gè)指令沒有任何關(guān)聯(lián),處理器可能會(huì)先運(yùn)行寫在后面的省時(shí)指令,后運(yùn)行寫在前面的費(fèi)時(shí)指令,這樣可以起到節(jié)省資源的效果。在單線程中,這沒有問題,但在多線程中,就出現(xiàn)了問題:
import kotlin.concurrent.thread
fun main() {
var a = 0
var b = 0
var x = -1
var y = -1
var count = 0
while (true) {
a = 0
b = 0
x = -1
y = -1
val th1 = thread {
b = 1
x = a
return@thread
}
val th2 = thread {
a = 1
y = b
return@thread
}
th1.join()
th2.join()
count++
if (x == 0 && y == 0){
println("第$count 次,($x,$y)")
break
}
}
}
輸出:
第100010 次,(0,0)
按照正常的邏輯,這個(gè)程序的運(yùn)行過程應(yīng)該是類似這樣的:
時(shí)間第一個(gè)線程第二個(gè)線程1b=12a=13x=a(1)4y=b(1)
或
時(shí)間第一個(gè)線程第二個(gè)線程1b=12x=a(0)3a=14y=b(1)
或
時(shí)間第一個(gè)線程第二個(gè)線程1a=12y=b(0)3b=14x=a(1)
無論如何,x和y都不可能同時(shí)為0,可是為什么原程序中,x和y都為0呢?
只有一種可能,類似這樣:
x和y的賦值語句被處理器提到了前面,因此出現(xiàn)了有序性的問題?
@Volatile注解可以保證指定變量的可見性和有序性:
import kotlin.concurrent.thread
@Volatile
var a = 0
@Volatile
var b = 0
@Volatile
var x = -1
@Volatile
var y = -1
fun main() {
var count = 0
while (true) {
a = 0
b = 0
x = -1
y = -1
val th1 = thread {
b = 1
x = a
return@thread
}
val th2 = thread {
a = 1
y = b
return@thread
}
th1.join()
th2.join()
count++
if (x == 0 && y == 0) {
println("第$count 次,($x,$y)")
break
}
}
}
運(yùn)行結(jié)果:
(無限循環(huán))
?可見,@Volatile注解保證了其有序性。這個(gè)注解保證可見性和有序性的原理如下:
可見性:給變量上一個(gè)Load屏障,每次讀取數(shù)據(jù)的時(shí)候被強(qiáng)制從進(jìn)程中讀取最新的數(shù)據(jù);同時(shí)上一個(gè)Store屏障,強(qiáng)制使每次修改之后強(qiáng)制刷新進(jìn)程中的數(shù)據(jù)有序性:通過禁止重排屏障禁止指令重排:
StoreStore屏障:禁止StoreStore屏障的前后Store寫操作重排LoadLoad屏障:禁止LoadLoad屏障的前后Load讀操作進(jìn)行重排LoadStore屏障:禁止LoadStore屏障的前面Load讀操作跟LoadStore屏障后面的Store寫操作重排StoreLoad屏障:禁止LoadStore屏障前面的Store寫操作跟后面的Load/Store 讀寫操作重排
線程鎖
我們可以通過線程鎖保證線程安全:
如果在操作對(duì)象之前,線程先聲明:“這個(gè)對(duì)象是我的”,當(dāng)另一個(gè)線程也想操作這個(gè)對(duì)象時(shí),發(fā)現(xiàn)已經(jīng)有人聲明過了,那么它就等待,直到那個(gè)發(fā)布聲明的線程又發(fā)了一個(gè)“這個(gè)對(duì)象不是我的了”的聲明。當(dāng)然,這兩個(gè)聲明其實(shí)起到了一個(gè)鎖的作用,當(dāng)聲明“這個(gè)對(duì)象是我的”時(shí),對(duì)象就被上了鎖,當(dāng)聲明“這個(gè)對(duì)象不是我的了”時(shí),對(duì)象的鎖就被解開了
當(dāng)然,上鎖和解鎖這一過程都必須保證原子性、可見性和有序性
我們可以如下修改代碼:
import kotlin.concurrent.thread
class MyMutex{
private var mutex: Boolean = false
@Synchronized
fun lock(){
while (mutex){} // 等待解鎖
mutex = true // 上鎖
return
}
@Synchronized
fun unlock(){
mutex = false // 解鎖
return
}
}
fun main() {
var tmp = 0
val mutex = MyMutex()
val th1 = thread {
Thread.sleep(200)
mutex.lock()
tmp++
mutex.unlock()
}
val th2 = thread {
Thread.sleep(200)
mutex.lock()
tmp++
mutex.unlock()
}
th1.join()
th2.join()
println(tmp)
}
這里面使用了@Synchronized注解,可以保證方法的原子性、可見性和有序性。在這里面保證了上鎖和解鎖的原子性、可見性和有序性。
@Synchronized注解是這么保證方法的原子性、可見性和有序性的:
原子性:在方法執(zhí)行前加鎖,執(zhí)行后解鎖,這樣一個(gè)方法同時(shí)只能有一個(gè)線程使用可見性:在方法執(zhí)行時(shí)給方法內(nèi)的每個(gè)變量上一個(gè)Load屏障,每次讀取數(shù)據(jù)的時(shí)候被強(qiáng)制從進(jìn)程中讀取最新的數(shù)據(jù);同時(shí)上一個(gè)Store屏障,強(qiáng)制使每次修改之后強(qiáng)制刷新進(jìn)程中的數(shù)據(jù)有序性:通過禁止重排屏障禁止指令重排:
StoreStore屏障:禁止StoreStore屏障的前后Store寫操作重排LoadLoad屏障:禁止LoadLoad屏障的前后Load讀操作進(jìn)行重排LoadStore屏障:禁止LoadStore屏障的前面Load讀操作跟LoadStore屏障后面的Store寫操作重排StoreLoad屏障:禁止LoadStore屏障前面的Store寫操作跟后面的Load/Store 讀寫操作重排
當(dāng)然,我們上面的代碼只是簡(jiǎn)單實(shí)現(xiàn)了一個(gè)線程鎖,kotlin中可以使用自帶的線程鎖:
ReentrantLock
ReentrantLock是一個(gè)遞歸互斥,在Java中叫可重入鎖,允許同一個(gè)線程多次上鎖,相應(yīng)的,同一個(gè)線程上鎖多少次,就要解鎖多少次。為什么要允許線程多次上鎖呢?
我們來看以下代碼:
import kotlin.concurrent.thread
class MyMutex{
private var mutex: Boolean = false
@Synchronized
fun lock(){
while (mutex){} // 等待解鎖
mutex = true // 上鎖
return
}
@Synchronized
fun unlock(){
mutex = false // 解鎖
return
}
}
fun main() {
val mutex1 = MyMutex()
val mutex2 = MyMutex()
val th1 = thread {
mutex1.lock()
println("th1 locked mutex1") // 模擬操作受mutex1保護(hù)的資源
Thread.sleep(200)
mutex2.lock()
println("th1 locked mutex2") // 模擬操作受mutex2保護(hù)的資源
mutex1.unlock()
println("th1 unlocked mutex1")
Thread.sleep(200)
mutex2.unlock()
println("th2 unlocked mutex2")
return@thread
}
val th2 = thread {
mutex2.lock()
println("th1 locked mutex2") // 模擬操作受mutex2保護(hù)的資源
Thread.sleep(200)
mutex1.lock()
println("th1 locked mutex1") // 模擬操作受mutex1保護(hù)的資源
mutex2.unlock()
println("th1 unlocked mutex2")
Thread.sleep(200)
mutex1.unlock()
println("th2 unlocked mutex1")
return@thread
}
th1.join()
th2.join()
println("線程執(zhí)行完畢")
}
運(yùn)行結(jié)果:
th1 locked mutex1
th1 locked mutex2
(無限循環(huán))
為什么會(huì)無限循環(huán)呢?因?yàn)閠h1鎖定mutex1后想要鎖定mutex2,卻發(fā)現(xiàn)mutex2被th2鎖定;而th2鎖定mutex2后想要鎖定mutex1,卻發(fā)現(xiàn)mutex1被th1鎖定,因此出現(xiàn)了無限循環(huán)的問題。我們稱這種問題為死鎖
使用遞歸互斥可以有效避免死鎖問題:
import java.util.concurrent.locks.ReentrantLock
import kotlin.concurrent.thread
fun main() {
val mutex = ReentrantLock()
val th1 = thread {
mutex.lock()
println("th1 locked mutex1")
Thread.sleep(200) // 模擬操作受mutex1保護(hù)的資源
mutex.lock()
println("th1 locked mutex2") // 模擬操作受mutex2保護(hù)的資源
mutex.unlock()
println("th1 unlocked mutex1")
Thread.sleep(200)
mutex.unlock()
println("th2 unlocked mutex2")
return@thread
}
val th2 = thread {
mutex.lock()
println("th1 locked mutex2") // 模擬操作受mutex2保護(hù)的資源
Thread.sleep(200)
mutex.lock()
println("th1 locked mutex1") // 模擬操作受mutex1保護(hù)的資源
mutex.unlock()
println("th1 unlocked mutex2")
Thread.sleep(200)
mutex.unlock()
println("th2 unlocked mutex1")
return@thread
}
th1.join()
th2.join()
println("線程執(zhí)行完畢")
}
其中,代碼
val mutex = ReentrantLock()
表示創(chuàng)建一個(gè)可重入鎖,這個(gè)對(duì)象的lock()和unlock()方法分別表示上鎖和解鎖
柚子快報(bào)激活碼778899分享:Kotlin多線程
精彩內(nèi)容
本文內(nèi)容根據(jù)網(wǎng)絡(luò)資料整理,出于傳遞更多信息之目的,不代表金鑰匙跨境贊同其觀點(diǎn)和立場(chǎng)。
轉(zhuǎn)載請(qǐng)注明,如有侵權(quán),聯(lián)系刪除。