-
執行緒使用
1. 建立後臺執行緒執行任務,大多數人(包括我)都會直接選擇
new Thread()
//或者
new Thread(new Runnable())
複製程式碼
之後用start()來啟動執行緒。跟程式碼會發現start()會執行start0()這個native方法,虛擬機器呼叫run方法。有Runnable就會呼叫傳入的runnable的run()實現,否則就會執行Thread中的run()方法。
2. ThreadFactory:工廠模式,方便做一些統一的初始化操作:
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進行執行緒操作的。
可以來看一下Executor,只是一個介面來通過execute()來指定執行緒工作
public interface Executor {
void execute(Runnable command);
}
複製程式碼
對於Executor的擴充套件:ExecutorServive/Executors
Executors.newCachedThreadPool()返回的又是一個ExecutorSevice extends Executor,對Executor做了一些擴充套件,主要關注的是shutdown()(保守型的結束)/shotdownNow()(立即結束),還有Future相關的submit(),這些後面會說。
newCachedThreadPool() 建立了一個帶快取的執行緒池,自動的進行執行緒的建立,快取,回收操作。
還有幾個別的方法:
newSingleThreadExecutor() 單一執行緒,用途較少。
newFixedThreadPool() 固定大小的執行緒池。 比如需要建立批量任務。
newScheduledThreadPool() 指定時間表,做個延時或者指定時間執行。
如果你想自定義執行緒池的時候就可以參考這幾個方法在app初始化的時候直接new ThreadPoolExecutor了。
執行緒完成後結束:就可以直接新增任務後執行shutdown()。關於執行緒的結束,寫在了後面的執行緒結束。
ThreadPoolExecutor()的幾個引數
public static ExecutorService newCachedThreadPool() {
return new ThreadPoolExecutor(
0, Integer.MAX_VALUE,60L, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>());
}
public ThreadPoolExecutor(
int corePoolSize,//初始執行緒池的大小/建立執行緒執行結束後回收到多少執行緒後不再回收
int maximumPoolSize,//執行緒上線
long keepAliveTime,//保持執行緒不被回收的等待時間
TimeUnit unit,
BlockingQueue<Runnable> workQueue,//阻塞佇列
ThreadFactory threadFactory,
RejectedExecutionHandler handler) {
//```
}
複製程式碼
寫到這裡的時候,maximumPoolSize這個引數,想起之前在自己寫一些圖片載入、快取的時候,開的執行緒總是會用CPU核心數來限制一下,比如2*CPU_CORE,以前不懂,會覺得大概是每個核分一個?
現在學到的:首先肯定不是為了一個核心來一個執行緒,畢竟一個cpu跑N多個執行緒,哪能就那麼剛剛好一個核心一個。
大概是可以保證程式碼在不同的機器上的CPU排程積極性差不多,比如單核的,就建立兩個執行緒,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會阻塞執行緒,如果在主執行緒中使用的話,就需要不停地來檢視後臺是否執行結束
while (true) {
if (future.isDone()){
try {
String result = future.get();
} catch (ExecutionException | InterruptedException e) {
e.printStackTrace();
}
try{
Thread.sleep(1000);//模擬主執行緒的任務,過一秒來看一看
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
複製程式碼
關於執行緒的結束,上面只是簡單說了下使用shutdown(),其實
-
執行緒安全/同步問題:
大致的產生原因:作業系統對於cpu使用的時間片機制,導致某段程式碼某個執行緒在執行中會被暫停,其他執行緒繼續執行同一段程式碼/操作同一個資料(資源)的時候,可能帶來的資料錯誤。
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();
}
}
複製程式碼
這還能不相等?
產生的原因其實簡單,上面也簡述過。就是在執行testThread的時候,兩個執行緒同時操作ifEquals方法:
- 執行緒1在操作到i=10,當前x=val=10時候被切換執行緒,此時y=val還未被執行緒1進行賦值
- 執行緒2進行執行程式碼,當進行到x=100,y=100,然後執行緒再次切換
- 執行緒1執行y=10,此時x的值已經是執行緒2修改過的100了,就會導致x!=y。
那麼知道了原因,解決的思路就變得簡單,x,y這兩步操作應該是變成一步操作即可。或者說一次ifEquals方法變成一步操作。所以JAVA提供了synchronized關鍵字,把這個關鍵字加在ifEquals這個方法,就使得其變成原子操作。 會對這個方法新增一個監視器Monitor這樣線上程1未執行完畢的時候,monitor不會被釋放,即使執行緒切換,執行緒2訪問到這個方法的時候,由於monitor未被釋放就會進入排隊等待,不會執行這個方法。
關於Synchronized關鍵字:現在給ifEquals增加Synchronized關鍵以後,上述程式碼再增加下面這個delVal()方法線上程中呼叫,x,y的值會出現問題嗎?依然會。
所以,看起來是對於方法的保護,實際上是對資源的保護,比如上面的例子,我們希望的其實並不是保護ifEquals方法,而是x,y的資源。
private void delVal(int val){
x=val-1;
y=val-1;
}
String name;
private Synchronized void setName(String val){
name=val;
}
複製程式碼
另一個問題就是在保護x/y的時候,同時也需要保護name的時候,如上對於兩個方法都加上Synchronized的時候也不方便,此時會導致當一個執行緒只是訪問到ifEquals方法的時候,另一個執行緒不能訪問setName。這就與我們一個執行緒操作x、y,另一個執行緒同步操作name的預期不符。原因是對方法新增synchronized會把整個Test類物件當做Monitor進行監視,也就是這些方法都會被同一個monitor進行監視。
那為什麼兩個方法操作的不是同一個資源,還會被保護呢?因為monitor不會對方法進行檢查,實際上我們synchronize方法的原因也不是為了讓方法不能被另一個執行緒呼叫,而是為了保護資源。這時候,synchronize方法就不符合我們多執行緒操作的預期,就需要我們自己手動來進行操作。所以就需要引入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來進行監視,比如我們可以把上面的this,換成numMonitor,這個時候,name和x、y就是兩個monitor進行,互不影響了。
synchronize的另一個作用:執行緒之間對於監視資源的資料同步
先解釋下java虛擬機器下面的資料操作:比如ifEquals這個方法,x、y讀到記憶體中,並不是cpu直接操作記憶體中的資料,而是由cpu單獨給一個儲存空間進行操作。我們都知道,現在用的記憶體條速度,比起cpu的操作速度有著極大的速度差,就像硬碟和記憶體的巨大速度差一樣,如果程式碼都是從硬碟執行,然後運算元據再寫回硬碟,肯定無法忍受。對於cpu也是一樣,ram的讀寫實在是太慢了,這時候就像使用記憶體來彌補速度差一樣,使用cpu的高速cache,來彌補記憶體和cpu匯流排之間的速度差。 基於以上的描述
開始操作的時候,
- x=0,y=0;
- thread1進行:x=5,y=5結束(線上程的cpu cache中),還沒有寫入記憶體的時候,
- thread2進行資料讀取,x=0,y=0;
synchronize關鍵字,就會保證cpu讀取賦值以後,再寫回記憶體中。來保證資料的正確
複製程式碼
但是帶來的結果就是,如果沒有cpu快取操作,這個x=5,y=5的操作會變得很慢。其實從上面的程式碼執行時間也能很明顯的看出區別,testThread()方法的執行時間,在有沒有對方法新增synchronize時會相差非常明顯。所以雖然會帶來執行緒之間的資料同步問題,當前的cache還是很有必要的。
安全
Safety 保證不會被改錯,改壞的安全,比如Thread Safety
Security 不被侵犯安全 https 的S
複製程式碼
死鎖
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 執行 ifEquals,numMonitor,然後cpu進行執行緒進行切換到Thread2 執行setName,持有nameMonitor,然後往下執行的時候,發現numMonitor被持有,Thread2進行等待,切換回Thread1,Thread1發現繼續執行,但是nameMonitor被持有,進入等待,這樣兩個執行緒就變成了互相持有對方需要的monitor進入互相等待,也就是死鎖。
樂觀悲觀鎖
跟執行緒安全不是很相關的鎖,更多的是,資料庫相關,並不是執行緒相關。
比如資料庫進行資料修改,需要先取出資料進行操作,再往裡寫,就會出現A操作寫資料,B操作也寫同一個資料,比如小明給我轉賬100,A操作出我的餘額X+100,正要寫入資料庫:餘額X+100,小王給我轉賬1並且先一步寫入了X+1,此時如果A操作繼續寫入餘額X+100就很明顯是錯誤的了。
解決這個問題的方式兩種:
- 悲觀鎖:對讀寫操作加鎖,A操作進行結束之前,B操作進行等待。是不是跟synchronize操作看起來一樣?
- 樂觀鎖:拿到資料的時候不加鎖,A操作進行寫入時,發現資料庫資料已經跟取出時候有了變化,那麼重新計算再寫入。
靜態方法的synchronize
1.給方法加,預設的monitor就是當前的Test.Class,這個類,不是這個物件 2.程式碼塊內部無法使用synchronize(this),應為這是靜態方法並不存在這個this。可以使用synchronize(Test.Class)
volatile
private volatile int a;
- 相當於一個小型的synchronized,使得對的操作具有原子性和同步性。多個執行緒進行讀寫操作不會導致記憶體中的資料亂改
- 但是對於double、long,由於比較長,那麼它本身不像int,其沒有原子性
- 對於基本型別有效,對於物件,只有保證本身的賦值操作有效(Dog dog=new Dog(“wangwang”)有效,對於dog.name="miaomiao"無效 )
- 我們都知道(int)a++ 實際上就是兩步操作,所以volatile 並不能保證a++的安全
- int temp=a+1;
- a=temp;
保證a++使用AtomicXXX
AtomicInteger a=new ActomicInteger(0);
a.getAndIncrement();
複製程式碼
讀寫鎖lock
比如Test中,增加一個方法
private Lock lock=new ReentrantLock();
private void reset(){
lock.lock();
//```
lock.unlock();
}
複製程式碼
但是如果在中間的方法中,出現了異常,後面的lock.unlock()無法執行,那就會導致一直被鎖(Monitor遇到異常會自動解開),所以需要手動處理:
private void reset(){
lock.lock();
try{
//```
}finally{
lock.unlock();
}
}
複製程式碼
看起來功能跟synchronized差不多?但是這麼麻煩用你幹啥?但是其實想一下,之前說執行緒同步的時候,都是在寫資料的時候出現問題,單純的讀取資料並不會出現問題,只有在寫入的時候,別人讀寫會導致出現問題,如果執行緒1讀取資料中切換執行緒,執行緒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();
}
}
複製程式碼
這樣,有執行緒在呼叫ifEquals()的時候,別人不能讀也不能寫,readData()的時候,別人能跟我一起讀取資料。就有利於效能的提升。
-
執行緒之間的互動
1. 從及基本的啟動/結束開始:
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();
複製程式碼
這樣就在主執行緒完成了子執行緒的開始和終止,就基本的互動就是這樣。但是用的時候會發現,stop()。喵喵喵?不是很好用嗎?為什麼劃線不建議用了呢?
因為不可控,Thread.stop會直接結束,而不管你內部正在進行什麼操作(實際上主執行緒也確實不知道子執行緒在做什麼),這樣就帶來了不可控性。
但是比如我明知道進行A操作以後,後面的執行緒做的工作已經無意義了,需要節省資源終止執行緒要怎麼做呢? 用thread.interrupt(),但是這個interrupt只是做了一個標記,如果僅僅使用interrupt是沒有任何作用的,還需要子執行緒自己根據這個中斷狀態進行操作:
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
for (int i = 0; i < 1_000_000_000; i++) {
if (isInterrupted()) {//不改變interrupt狀態
// if(Thread.interrupted()) //這個方法會在使用之後重置interrupt的狀態
//做執行緒結束的收尾工作
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方法會檢測當前的interrupt的狀態,如果當前執行緒已經被interrupt,則會拋InterruptedException
- 因為我們在使用interrupt的時候,需要注意interrupt會直接喚醒sleep,重置interrupt的狀態
//對於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}.
複製程式碼
所以中斷執行緒的時候需要考慮一下進行處理:
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
//進行自己的interrupt處理
return;
}
try {
Thread.sleep(2000);//睡得時候被執行interrupt,會直接喚醒進入中斷異常,
//如果不在下面catch進行處理,interrupt的值又會被重置,導致外部呼叫的interrupt相當於沒有發生
} 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(),比如上面講到的死鎖中,存在的情況,雙方互相需要操作資源的時候發現monitor不在手裡,wait總是要和個synchronized一起出現,因為wait出現就是為了使用共同的資源
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不知道誰先執行完,所以可能出現getName先執行,但是getName獲取空的話又沒法進行操作,這時候怎麼辦呢?
private synchronized String getName(){
while(name==null){}//一直乾等著,直到name不為空
return name;
}
複製程式碼
但是這是個synchronized方法又會持有跟setname一樣的monitor,setName也被鎖住了成了死鎖,那怎麼做?
private synchronized String getName(){
while(name==null){//使用wait的標準配套就是while判斷,而不是if,因為wait會被interrupt喚醒
try{
wait();//object方法
}catch(InterruptedException e){
//
e.printStackTrace();
}
}//乾等著,直到不為空
return name;
}
private synchronized void setName(){
name="湯圓";
notifyAll();//object方法,把monitor上的所有在等待的執行緒全部喚醒去看一下是否滿足執行條件了
}
複製程式碼
或者是進入頁面,需要請求多個介面,根據介面的資料來設定頁面設定資料,也是類似的情況。不過實際上一個Rxjava的zip操作就能解決大多數問題了。 實際上寫了這麼多,這種需求Rxjava的zip操作就解決了...
- join() 可能會存在一個thread1在執行的過程中,需要thread0來執行,等完全結束後再繼續執行該執行緒
private void main(){
new Thread() {
@Override
public void run() {
//一些操作
try{
thread0.join();//相當於自動notify的wait
//Thread.yield();
}catch(InterruptedException e){
e.printStackTrace();
}
//一些操作
getname();
}
};
}
複製程式碼
- yield() 極少用,瞭解就行了,參考上面註釋掉的那行程式碼,讓出本次的cpu執行時間片給同優先順序的執行緒。
感謝&參考:扔物線