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