- 線程的使用方式
1. 創(chuàng)建后臺線程執(zhí)行任務(wù),大多數(shù)人(包括我)都會直接選擇
new Thread()
//或者
new Thread(new Runnable())
之后用start()來啟動線程。跟代碼會發(fā)現(xiàn)start()會執(zhí)行start0()這個native方法,虛擬機調(diào)用run方法。有Runnable就會調(diào)用傳入的runnable的run()實現(xiàn),否則就會執(zhí)行Thread中的run()方法。
2. ThreadFactory:工廠模式,方便做一些統(tǒng)一的初始化操作:
ThreadFactory factory=new ThreadFactory(){
@Override
public Thread newThread(Runnable r){
return new Thread(r);
}
}
Runnable runnable =new Runnable(){
//```
}
Thread thread=factory.newThread(runnable);
thread.start();
Thread thread1=factory.newThread(runnable);
thread1.start();
3. Executor:最常用到但是卻很少用的方法:
Runnable runnable =new Runnable(){
@Override
public void run(){
//```
}
}
Executor executor=Executors.newCachedThreadPool();
executor.executor(runnable);
executor.executor(runnable);
(ExecutorService)executor.shutdown();
至少對于我,看起來是很少用,那為什么說最常用呢?
AsyncTask,Cursor,Rxjava其實也是使用Executor進(jìn)行線程操作的。
可以來看一下Executor,只是一個接口來通過execute()來指定線程工作
public interface Executor {
void execute(Runnable command);
}
對于Executor的擴展:ExecutorServive/Executors Executors.newCachedThreadPool()返回的又是一個ExecutorSevice extends Executor,對Executor做了一些擴展,主要關(guān)注的是shutdown()(保守型的結(jié)束)/shotdownNow()(立即結(jié)束),還有Future相關(guān)的submit(),這些后面會說。
newCachedThreadPool() 創(chuàng)建了一個帶緩存的線程池,自動的進(jìn)行線程的創(chuàng)建,緩存,回收操作。
還有幾個別的方法:
newSingleThreadExecutor() 單一線程,用途較少。
newFixedThreadPool() 固定大小的線程池。 比如需要創(chuàng)建批量任務(wù)。 newScheduledThreadPool() 指定時間表,做個延時或者指定時間執(zhí)行。
如果你想自定義線程池的時候就可以參考這幾個方法在app初始化的時候直接new ThreadPoolExecutor了。 線程完成后結(jié)束:就可以直接添加任務(wù)后執(zhí)行shutdown()。關(guān)于線程的結(jié)束,寫在了后面的[從及基本的啟動/結(jié)束開始。
ThreadPoolExecutor()的幾個參數(shù)
return new ThreadPoolExecutor(
0, Integer.MAX_VALUE,60L, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>());
}
public ThreadPoolExecutor(
int corePoolSize,//初始線程池的大小/創(chuàng)建線程執(zhí)行結(jié)束后回收到多少線程后不再回收
int maximumPoolSize,//線程上線
long keepAliveTime,//保持線程不被回收的等待時間
TimeUnit unit,
BlockingQueue<Runnable> workQueue,//阻塞隊列
ThreadFactory threadFactory,
RejectedExecutionHandler handler) {
//```
}
寫到這里的時候,maximumPoolSize這個參數(shù),想起之前在自己寫一些圖片加載、緩存的時候,開的線程總是會用CPU核心數(shù)來限制一下,比如2*CPU_CORE,以前不懂,會覺得大概是每個核分一個?
現(xiàn)在學(xué)到的:首先肯定不是為了一個核心來一個線程,畢竟一個cpu跑N多個線程,哪能就那么剛剛好一個核心一個。
大概是可以保證代碼在不同的機器上的CPU調(diào)度積極性差不多,比如單核的,就創(chuàng)建兩個線程,8核心的,就是16個線程。不然寫8個線程,單核心機器運行可能就會比較卡,8核心機器運行又會太少。
4. callable 可以簡單描述為有返回值的后臺線程,安卓端比較少用,就簡單記錄下,畢竟AsyncTask、Handler、RxJava都比這個好用
Callable callable = new Callable<String>() {
@Override
public String call() throws Exception {
try {
Thread.sleep(1500);
} catch (InterruptedException e) {
e.printStackTrace();
}
return 清韻";
}
};
ExecutorService executor = Executors.newCachedThreadPool();
Future<String> future = executor.submit(callable);
try {
String result = future.get();
} catch (ExecutionException | InterruptedException e) {
e.printStackTrace();
}
是不是看起來也不是很麻煩?甚至還有一丟丟好用? 但是這個Future會阻塞線程,如果在主線程中使用的話,就需要不停地來查看后臺是否執(zhí)行結(jié)束
while (true) {
if (future.isDone()){
try {
String result = future.get();
} catch (ExecutionException | InterruptedException e) {
e.printStackTrace();
}
try{
Thread.sleep(1000);//模擬主線程的任務(wù),過一秒來看一看
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
ok,線程的常見使用方式就基本上說完了。使用的時候我們會經(jīng)常遇到多個線程操作同一個資源的問題,比如A給B轉(zhuǎn)賬的同時,B,C又同時給A轉(zhuǎn)賬,那么應(yīng)該怎么處理呢?這時候就要說到第二節(jié)的線程安全問題了。
- 線程安全/同步問題:
1. 產(chǎn)生原因:
操作系統(tǒng)對于cpu使用的時間片機制,導(dǎo)致某段代碼某個線程在執(zhí)行中會被暫停,其他線程繼續(xù)執(zhí)行同一段代碼/操作同一個數(shù)據(jù)(資源)的時候,可能帶來的數(shù)據(jù)錯誤。
eg:
class Test{
private int x=0;
private int y=0;
private void ifEquals(int val){
x=val;
y=val
if(x!=y){
System.out.println("x: "+x+",y: "+y);
}
}
public void testThread(){
new Thread(){
public void run(){
for(int i=0;i<1_000_000_000;i++){
ifEquals(i);
}
}
}.start();
new Thread(){
public void run(){
for(int i=0;i<1_000_000_000;i++){
ifEquals(i);
}
}
}.start();
}
}
這還能不相等?
產(chǎn)生的原因其實簡單,上面也簡述過。就是在執(zhí)行testThread的時候,兩個線程同時操作ifEquals方法:
- 線程1在操作到i=10,當(dāng)前x=val=10時候被切換線程,此時y=val還未被線程1進(jìn)行賦值
- 線程2進(jìn)行執(zhí)行代碼,當(dāng)進(jìn)行到x=100,y=100,然后線程再次切換
- 線程1執(zhí)行y=10,此時x的值已經(jīng)是線程2修改過的100了,就會導(dǎo)致x!=y。
2. 解決辦法:Synchronized
那么知道了原因,解決的思路就變得簡單,x,y這兩步操作應(yīng)該是變成一步操作即可。或者說一次ifEquals方法變成一步操作。所以JAVA提供了synchronized關(guān)鍵字,把這個關(guān)鍵字加在ifEquals這個方法,就使得其變成原子操作。 會對這個方法添加一個監(jiān)視器Monitor這樣在線程1未執(zhí)行完畢的時候,monitor不會被釋放,即使線程切換,線程2訪問到這個方法的時候,由于monitor未被釋放就會進(jìn)入排隊等待,不會執(zhí)行這個方法。
關(guān)于Synchronized關(guān)鍵字:現(xiàn)在給ifEquals增加Synchronized關(guān)鍵以后,上述代碼再增加下面這個delVal()方法在線程中調(diào)用,x,y的值會出現(xiàn)問題嗎?依然會。
所以,看起來是對于方法的保護(hù),實際上是對資源的保護(hù),比如上面的例子,我們希望的其實并不是保護(hù)ifEquals方法,而是x,y的資源。
private void delVal(int val){
x=val-1;
y=val-1;
}
String name;
private Synchronized void setName(String val){
name=val;
}
3. 關(guān)于監(jiān)視器Monitor
另一個問題就是在保護(hù)x/y的時候,同時也需要保護(hù)name的時候,如上對于兩個方法都加上Synchronized的時候也不方便,此時會導(dǎo)致當(dāng)一個線程只是訪問到ifEquals方法的時候,另一個線程不能訪問setName。這就與我們一個線程操作x、y,另一個線程同步操作name的預(yù)期不符。原因是對方法添加synchronized會把整個Test類對象當(dāng)做Monitor進(jìn)行監(jiān)視,也就是這些方法都會被同一個monitor進(jìn)行監(jiān)視。
那為什么兩個方法操作的不是同一個資源,還會被保護(hù)呢?因為monitor不會對方法進(jìn)行檢查,實際上我們synchronize方法的原因也不是為了讓方法不能被另一個線程調(diào)用,而是為了保護(hù)資源。這時候,synchronize方法就不符合我們多線程操作的預(yù)期,就需要我們自己手動來進(jìn)行操作。所以就需要引入Synchronized代碼塊。
final Object numMonitor=new Object();
final Object nameMonitor=new Object();
//代碼塊
private void ifEquals(int val){
synchronize(this){
x=val;
y=val
if(x!=y){
System.out.println("x: "+x+",y: "+y);
}
}
}
//代碼塊
private void delVal(int val){
synchronize(this){
x=val-1;
y=val-1;
}
}
//指定monitor
private Synchronized void setName(String val){
synchronize(nameMonitor){
name=val;
}
}
public void testThread(){
//``````
}
synchronize(Object object),允許你指定object作為monitor來進(jìn)行監(jiān)視,比如我們可以把上面的this,換成numMonitor,這個時候,name和x、y就是兩個monitor進(jìn)行,互不影響了。
synchronize的另一個作用:線程之間對于監(jiān)視資源的數(shù)據(jù)同步
4.靜態(tài)方法的synchronize
給方法加,默認(rèn)的monitor就是當(dāng)前的Test.Class,這個類,不是這個對象 2.代碼塊內(nèi)部無法使用synchronize(this),應(yīng)為這是靜態(tài)方法并不存在這個this。可以使用synchronize(Test.Class)
5. volatile
private volatile int a;
相當(dāng)于一個小型的synchronized,使得對的操作具有原子性和同步性。多個線程進(jìn)行讀寫操作不會導(dǎo)致內(nèi)存中的數(shù)據(jù)亂改
但是對于double、long,由于比較長,那么它本身不像int,其沒有原子性
對于基本類型有效,對于對象,只有保證本身的賦值操作有效(Dog dog=new Dog(“wangwang”)有效,對于dog.name="miaomiao"無效 )
-
我們都知道(int)a++ 實際上就是兩步操作,所以volatile 并不能保證a++的安全
- int temp=a+1;
- a=temp;
6. 保證a++使用AtomicXXX
AtomicInteger a=new ActomicInteger(0);
a.getAndIncrement();
7. Synchronize和運行速度
先解釋下java虛擬機下面的數(shù)據(jù)操作:比如ifEquals這個方法,x、y讀到內(nèi)存中,并不是cpu直接操作內(nèi)存中的數(shù)據(jù),而是由cpu單獨給線程一個存儲空間進(jìn)行操作。我們都知道,現(xiàn)在用的內(nèi)存條速度,比起cpu的操作速度有著極大的速度差,就像硬盤和內(nèi)存的巨大速度差一樣,如果代碼都是從硬盤執(zhí)行,然后操作數(shù)據(jù)再寫回硬盤,肯定無法忍受。對于cpu也是一樣,ram的讀寫實在是太慢了,這時候就像使用內(nèi)存來彌補速度差一樣,使用cpu的高速cache,來彌補內(nèi)存和cpu總線之間的速度差。基于以上的描述
開始操作的時候,
- x=0,y=0;
- thread1進(jìn)行:x=5,y=5結(jié)束(在線程的cpu cache中),還沒有寫入內(nèi)存的時候,
- thread2進(jìn)行數(shù)據(jù)讀取,x=0,y=0;
synchronize關(guān)鍵字,就會保證cpu讀取賦值以后,再寫回內(nèi)存中。來保證數(shù)據(jù)的正確
但是帶來的結(jié)果就是,如果沒有cpu緩存操作,這個x=5,y=5的操作會變得很慢。其實從上面的代碼運行時間也能很明顯的看出區(qū)別,testThread()方法的執(zhí)行時間,在有沒有對方法添加synchronize時會相差非常明顯。所以雖然會帶來線程之間的數(shù)據(jù)同步問題,當(dāng)前的cache還是很有必要的。
8. 關(guān)于安全
很多時候我們在說安全,線程安全,網(wǎng)絡(luò)安全,數(shù)據(jù)安全,但是其實這是不一樣的“安全”:
Safety 保證不會被改錯,改壞的安全,比如Thread Safety
Security 不被侵犯安全 https 的S
關(guān)于鎖
死鎖
死鎖是我們最常遇到,或者是最常聽到的一種鎖。其實產(chǎn)生的原因也很簡單,就是互相持有(對方的“鑰匙”)導(dǎo)致互相等待:
private Synchronized void setName(String val){
synchronize(nameMonitor){
name=val;
synchronize(numMonitor){
x=val;
y=val
if(x!=y){
System.out.println("x: "+x+",y: "+y);
}
}
}
}
}
private void ifEquals(int val){
synchronize(numMonitor){
x=val;
y=val
if(x!=y){
System.out.println("x: "+x+",y: "+y);
synchronize(nameMonitor){
name="haha";
}
}
}
}
Thread1 執(zhí)行 ifEquals,numMonitor,然后cpu進(jìn)行線程進(jìn)行切換到Thread2 執(zhí)行setName,持有nameMonitor,然后往下執(zhí)行的時候,發(fā)現(xiàn)numMonitor被持有,Thread2進(jìn)行等待,切換回Thread1,Thread1發(fā)現(xiàn)繼續(xù)執(zhí)行,但是nameMonitor被持有,進(jìn)入等待,這樣兩個線程就變成了互相持有對方需要的monitor(鑰匙)進(jìn)入互相等待,也就是死鎖。
樂觀悲觀鎖
跟線程安全不是很相關(guān)的鎖,更多的是,數(shù)據(jù)庫相關(guān),并不是線程相關(guān)。
比如數(shù)據(jù)庫進(jìn)行數(shù)據(jù)修改,需要先取出數(shù)據(jù)進(jìn)行操作,再往里寫,就會出現(xiàn)A操作寫數(shù)據(jù),B操作也寫同一個數(shù)據(jù),比如小明給我轉(zhuǎn)賬100,A操作出我的余額X+100,正要寫入數(shù)據(jù)庫:余額X+100,小王給我轉(zhuǎn)賬1并且先一步寫入了X+1,此時如果A操作繼續(xù)寫入余額X+100就很明顯是錯誤的了。
解決這個問題的方式兩種:
- 悲觀鎖:對讀寫操作加鎖,A操作進(jìn)行結(jié)束之前,B操作進(jìn)行等待。是不是跟synchronize操作看起來一樣?
- 樂觀鎖:拿到數(shù)據(jù)的時候不加鎖,A操作進(jìn)行寫入時,發(fā)現(xiàn)數(shù)據(jù)庫數(shù)據(jù)已經(jīng)跟取出時候有了變化,那么重新計算再寫入。
讀寫鎖lock
比如Test中,增加一個方法
private Lock lock=new ReentrantLock();
private void reset(){
lock.lock();
//```
lock.unlock();
}
看起來功能跟synchronized差不多?但是這么麻煩用你干啥?但是其實想一下,之前說線程同步的時候,都是在寫數(shù)據(jù)的時候出現(xiàn)問題,單純的讀取數(shù)據(jù)并不會出現(xiàn)問題,只有在寫入的時候,別人讀寫會導(dǎo)致出現(xiàn)問題,如果線程1讀取數(shù)據(jù)中切換線程,線程2也不能讀取,就會有性能的浪費,讀寫鎖就可以解決這個問題:
private ReentrantReadWriteLock lock=new ReentrantReadWriteLock();
private ReentrantReadWriteLock.ReadLock readLock=lock.readLock();
private ReentrantReadWriteLock.WriteLock writeLock=lock.writeLock();
private void ifEquals(int val){
writeLock.lock();
try{
x=val;
y=val;
}finally{
writeLock.unlock()
}
}
private readData(){
readLock.lock();
try{
System.out.println("x: "+x+",y: "+y);
}finally{
readLock.unlock();
}
}
這樣,有線程在調(diào)用ifEquals()的時候,別人不能讀也不能寫,readData()的時候,別人能跟我一起讀取數(shù)據(jù)。就有利于性能的提升。
-
線程之間的交互
1. 從及基本的啟束動/結(jié)開始:
Thread thread = new Thread() {
@Override
public void run() {
for (int i = 0; i < 1_000_000_000; i++) {// 模擬一段耗時操作
Log.d("========>", "找湯圓");
}
}
};
thread.start();
Thread.sleep(100);
thread.stop();
這樣就在主線程完成了子線程的開始和終止,就基本的交互就是這樣。但是用的時候會發(fā)現(xiàn),stop()。喵喵喵?不是很好用嗎?為什么劃線不建議用了呢?
因為不可控,Thread.stop會直接結(jié)束,而不管你內(nèi)部正在進(jìn)行什么操作(實際上主線程也確實不知道子線程在做什么),這樣就帶來了不可控性。
但是比如我明知道進(jìn)行A操作以后,后面的線程做的工作已經(jīng)無意義了,需要節(jié)省資源終止線程要怎么做呢? 用thread.interrupt(),但是這個interrupt只是做了一個標(biāo)記,如果僅僅使用interrupt是沒有任何作用的,還需要子線程自己根據(jù)這個中斷狀態(tài)進(jìn)行操作:
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
for (int i = 0; i < 1_000_000_000; i++) {
if (isInterrupted()) {//不改變interrupt狀態(tài)
// if(Thread.interrupted()) //這個方法會在使用之后重置interrupt的狀態(tài)
//做線程結(jié)束的收尾工作
return;
}
Log.d("========>", "找湯圓"+i);
}
}
});
thread.start();
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
thread.interrupt();
看到這個interrupt是不是會覺得這個詞在線程操作中有點熟悉?哎(二聲),這不是剛好是上面兩行sleep的時候要catch的InterruptedException么?為什么我就想讓線程sleep一下,要加入個try/catch呢?
原因有兩個:
- sleep方法會檢測當(dāng)前的interrupt的狀態(tài),如果當(dāng)前線程已經(jīng)被interrupt,則會拋InterruptedException
- 因為我們在使用interrupt的時候,需要注意interrupt會直接喚醒sleep,重置interrupt的狀態(tài)
//對于Thread.interrupt()方法的部分注釋
* If this thread is blocked in an invocation of the wait() or join() or sleep(),
* methods of this class, then its interrupt status will be cleared and it
* will receive an {@link InterruptedException}.
所以中斷線程的時候需要考慮一下進(jìn)行處理:
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
for (int i = 0; i < 1_000_000_000; i++) {
if (Thread.interrupted()) {//false,100毫秒后才會變成ture
//進(jìn)行自己的interrupt處理
return;
}
try {
Thread.sleep(2000);//睡得時候被執(zhí)行interrupt,會直接喚醒進(jìn)入中斷異常,
//如果不在下面catch進(jìn)行處理,interrupt的值又會被重置,導(dǎo)致外部調(diào)用的interrupt相當(dāng)于沒有發(fā)生
} catch (InterruptedException e) {
e.printStackTrace();
//收尾工作
return;
}
Log.d("========>", "清韻"+i);
}
}
});
thread.start();
try {
Thread.sleep(100);//這里是主線程睡了,
//跟上面的子線程的sleep不一樣哦,只是為了模擬start()和interrupt之間中間有個時間差
} catch (InterruptedException e) {
e.printStackTrace();
}
thread.interrupt();
那所以安卓中我只是想單純的讓線程sleep一下,不需要外部interrupt時候提供支持,不想寫try/catch不行嗎?
SystemClock.sleep(100);//小哥哥,了解一下這個
2. wait(),join(),yield()的使用(Android較少用到):
- wait(),比如上面講到的死鎖中,存在的情況,雙方互相需要操作資源的時候發(fā)現(xiàn)monitor不在手里,wait總是要和個synchronized一起出現(xiàn),因為wait出現(xiàn)就是為了使用共同的資源
String name=null;
private synchronized void setName(){
name="清韻";
}
private synchronized String getName(){
return name;
}
private void main(){
new Thread() {
@Override
public void run() {
//一些操作
setName();
}
}.start();
new Thread() {
@Override
public void run() {
//一些操作
getname();
}
}.start();
}
由于兩個Thread不知道誰先執(zhí)行完,所以可能出現(xiàn)getName先執(zhí)行,但是getName獲取空的話又沒法進(jìn)行操作,這時候怎么辦呢?
private synchronized String getName(){
while(name==null){}//一直干等著,直到name不為空
return name;
}
但是這是個synchronized方法又會持有跟setname一樣的monitor,setName也被鎖住了成了死鎖,那怎么做?
private synchronized String getName(){
while(name==null){//使用wait的標(biāo)準(zhǔn)配套就是while判斷,而不是if,因為wait會被interrupt喚醒
try{
wait();//object方法
}catch(InterruptedException e){
//
e.printStackTrace();
}
}//干等著,直到不為空
return name;
}
private synchronized void setName(){
name="清韻";
notifyAll();//object方法,把monitor上的所有在等待的線程全部喚醒去看一下是否滿足執(zhí)行條件了
}
或者是進(jìn)入頁面,需要請求多個接口,根據(jù)接口的數(shù)據(jù)來設(shè)置頁面設(shè)置數(shù)據(jù),也是類似的情況。不過實際上一個Rxjava的zip操作就能解決大多數(shù)問題了。 實際上寫了這么多,這種需求Rxjava的zip操作就解決了...
- join() 可能會存在一個thread1在執(zhí)行的過程中,需要thread0來執(zhí)行,等完全結(jié)束后再繼續(xù)執(zhí)行該線程
private void main(){
new Thread() {
@Override
public void run() {
//一些操作
try{
thread0.join();//相當(dāng)于自動notify的wait
//Thread.yield();
}catch(InterruptedException e){
e.printStackTrace();
}
//一些操作
getname();
}
};
}
- yield() 極少用,了解就行了,參考上面注釋掉的那行代碼,讓出本次的cpu執(zhí)行時間片給同優(yōu)先級的線程。