天天看點

線程、多線程和線程池面試專題

線程、多線程和線程池面試專題

極力推薦文章:歡迎收藏

Android 幹貨分享

線程、多線程和線程池面試專題
閱讀五分鐘,每日十點,和您一起終身學習,這裡是程式員Android

本篇文章主要介紹

Android

開發中的部分知識點,通過閱讀本篇文章,您将收獲以下内容:

1、開啟線程的三種方式?

1)繼承

Thread

類,重寫

run()

方法,在

run()

方法體中編寫要完成的任務

new Thread().start();

2)實作

Runnable

接口,實作

run()

方法

new Thread(new MyRunnable()).start();

3)實作

Callable

接口

MyCallable

類,實作

call()

方法,使用

FutureTask

類來包裝

Callable

對象,使用

FutureTask

對象作為

Thread

對象的

target

建立并啟動線程;調用

FutureTask

對象的

get()

方法來獲得子線程執行結束後的傳回值。

FutureTask<Integer> ft = new FutureTask<Integer>(new MyCallable());

new Thread(ft).start();
           

2、run()和start()方法差別

run()

方法隻是線程的主體方法,和普通方法一樣,不會建立新的線程。

隻有調用

start()

方法,才會啟動一個新的線程,新線程才會調用

run()

方法,線程才會開始執行。

3、如何控制某個方法允許并發通路線程的個數?

建立

Semaphore

變量,

Semaphore semaphore = new Semaphore(5, true);

當方法進入時,請求一個信号,如果信号被用完則等待,方法運作完,釋放一個信号,釋放的信号新的線程就可以使用。

4、在Java中wait和seelp方法的不同

wait()

方法屬于

Object

類,調用該方法時,線程會放棄對象鎖,隻有該對象調用

notify()

方法後本線程才進入對象鎖定池準備擷取對象鎖進入運作狀态。

sleep()

方法屬于

Thread

類,

sleep()

導緻程式暫停執行指定的時間,讓出

CPU

,但它的監控狀态依然儲存着,當指定時間到了又會回到運作狀态,

sleep()

方法中線程不會釋放對象鎖。

5、談談wait/notify關鍵字的了解

notify:

喚醒在此對象螢幕上等待的單個線程

notifyAll():

通知所有等待該競争資源的線程

wait:

釋放

obj

的鎖,導緻目前的線程等待,直接其他線程調用此對象的

notify()

notifyAll()

方法

當要調用

wait()

notify()/notifyAll()

方法時,一定要對競争資源進行加鎖,一般放到

synchronized(obj)

代碼中。當調用

obj.notify/notifyAll

後,調用線程依舊持有

obj

鎖,是以等待線程雖被喚醒,但仍無法獲得

obj

鎖,直到調用線程退出

synchronized

塊,釋放

obj

鎖後,其他等待線程才有機會獲得鎖繼續執行。

6、什麼導緻線程阻塞?

(1)一般線程阻塞

1)線程執行了

Thread.sleep(int millsecond)

方法,放棄

CPU

,睡眠一段時間,一段時間過後恢複執行;

2)線程執行一段同步代碼,但無法獲得相關的同步鎖,隻能進入阻塞狀态,等到擷取到同步鎖,才能恢複執行;

3)線程執行了一個對象的

wait()

方法,直接進入阻塞态,等待其他線程執行

notify()/notifyAll()

操作;

4)線程執行某些

IO

操作,因為等待相關資源而進入了阻塞态,如

System.in

,但沒有收到鍵盤的輸入,則進入阻塞态。

5)線程禮讓,

Thread.yield()

方法,暫停目前正在執行的線程對象,把執行機會讓給相同或更高優先級的線程,但并不會使線程進入阻塞态,線程仍處于可執行态,随時可能再次分得

CPU

時間。線程自閉,

join()

方法,在目前線程調用另一個線程的

join()

方法,則目前線程進入阻塞态,直到另一個線程運作結束,目前線程再由阻塞轉為就緒态。

6)線程執行

suspend()

使線程進入阻塞态,必須

resume()

方法被調用,才能使線程重新進入可執行狀态。

7、線程如何關閉?

  1. 使用标志位

2)使用

stop()

方法,但該方法就像關掉電腦電源一樣,可能會發生預料不到的問題

3)使用中斷

interrupt()

public class Thread {
    // 中斷目前線程
    public void interrupt();
    // 判斷目前線程是否被中斷
    public boolen isInterrupt();
    // 清除目前線程的中斷狀态,并傳回之前的值
    public static boolen interrupted();   
}
           

但調用

interrupt()

方法隻是傳遞中斷請求消息,并不代表要立馬停止目标線程。

8、講一下java中的同步的方法

之是以需要同步,因為在多線程并發控制,當多個線程同時操作一個可共享的資源時,如果沒有采取同步機制,将會導緻資料不準确,是以需要加入同步鎖,確定在該線程沒有完成操作前被其他線程調用,進而保證該變量的唯一一性和準确性。

1)synchronized修飾同步代碼塊或方法

由于

java

的每個對象都有一個内置鎖,用此關鍵字修飾方法時,内置鎖會保護整個方法。在調用該方法前,需獲得内置鎖,否則就處于陰塞狀态。

2)volatile修飾變量

保證變量線上程間的可見性,每次線程要通路

volatile

修飾的變量時都從記憶體中讀取,而不緩存中,這樣每個線程通路到的變量都是一樣的。且使用記憶體屏障。

3)ReentrantLock重入鎖,它常用的方法有ReentrantLock():

建立一個

ReentrantLock

執行個體

lock()

獲得鎖

unlock()

釋放鎖

4)使用局部變量ThreadLocal實作線程同步

每個線程都會儲存一份該變量的副本,副本之間互相獨立,這樣每個線程都可以随意修改自己的副本,而不影響其他線程。常用方法

ThreadLocal()

建立一個線程本地變量;

get()

傳回此線程局部的目前線程副本變量;

initialValue()

傳回此線程局部變量的目前線程的初始值;

set(T value)

将此線程變量的目前線程副本中的值設定為

value

5) 使用原子變量

AtomicInteger

,常用方法

AtomicInteger(int value)

建立個有給定初始值的

AtomicInteger

整數;

addAndGet(int data)

以原子方式将給定值與目前值相加

6)使用阻塞隊列實作線程同步

例如

LinkedBlockingQueue<E>

9、如何保證線程安全?

線程安全性展現在三方法:

1)原子性:

提供互斥通路,同一時刻隻能有一個線和至資料進行操作。

JDK

中提供了很多

atomic

類,如

AtomicInteger\AtomicBoolean\AtomicLong

,它們是通過

CAS

完成原子性。

JDK

提供鎖分為兩種:

synchronized

依賴

JVM

實作鎖,該關鍵字作用對象的作用範圍内同一時刻隻能有一個線程進行操作。另一種

LOCK

,是

JDK

提供的代碼層面的鎖,依賴

CPU

指令,代表性是

ReentrantLock

2)可見性:

一個線程對主記憶體的修改及時被其他線程看到。

JVM

提供了

synchronized

volatile

volatile

的可見性是通過記憶體屏障和禁止重排序實作的,

volatile

會在寫操作時,在寫操作後加一條

store

屏障指令,将本地記憶體中的共享變量值重新整理到主記憶體;會在讀操作時,在讀操作前加一條

load

指令,從記憶體中讀取共享變量。

3)有序性:

指令沒有被編譯器重排序。

可通過

volatile、synchronized、Lock

保證有序性。

10、兩個程序同時要求寫或者讀,能不能實作?如何防止程序的同步?

我認為可以實作,比如兩個程序都讀取月曆程序資料是沒有問題,但同時寫,應該會有沖突。

可以使用共享記憶體實作程序間資料共享。

11、線程間操作List

多線程數量的問題,一般情況下,多線程數量要等于機器

CPU

核數

-1

.

1.如何讓n個線程順序周遊含有n個元素的List集合
import java.util.ArrayList;
import java.util.List;
import org.apache.commons.lang3.ArrayUtils;
 
public class Test_4 {
    /**
     * 多線程處理list
     *
     * @param data  資料list
     * @param threadNum  線程數
     */
    public synchronized void handleList(List<String> data, int threadNum) {
        int length = data.size();
        int tl = length % threadNum == 0 ? length / threadNum : (length
                / threadNum + 1);
 
        for (int i = 0; i < threadNum; i++) {
            int end = (i + 1) * tl;
            HandleThread thread = new HandleThread("線程[" + (i + 1) + "] ",  data, i * tl, end > length ? length : end);
            thread.start();
        }
    }
 
    class HandleThread extends Thread {
        private String threadName;
        private List<String> data;
        private int start;
        private int end;
 
        public HandleThread(String threadName, List<String> data, int start, int end) {
            this.threadName = threadName;
            this.data = data;
            this.start = start;
            this.end = end;
        }
        
        public void run() {
            List<String> subList = data.subList(start, end)/*.add("^&*")*/;
            System.out.println(threadName+"處理了"+subList.size()+"條!");
        }
 
    }
 
    public static void main(String[] args) {
        Test_4 test = new Test_4();
        // 準備資料
        List<String> data = new ArrayList<String>();
        for (int i = 0; i < 6666; i++) {
            data.add("item" + i);
        }
        test.handleList(data, 5);
        System.out.println(ArrayUtils.toString(data));
    }
}
           

2. List多線程并發讀取讀取現有的list對象

//測試讀取List的線程類,大概34秒
package com.thread.list;
 
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
 
public class Main {
    
    public static void main(String[] args) {
        
        List<String> list = new ArrayList<String>();
        Map<Long,Integer> map = new HashMap<Long,Integer>();

        for(int i = 0;i<1000;i++){
            list.add(""+i);
        }
        
        int pcount = Runtime.getRuntime().availableProcessors();        
        long start = System.currentTimeMillis();        
        
        for(int i=0;i<pcount;i++){
            
           Thread t = new MyThread1(list,map);
            map.put(t.getId(),Integer.valueOf(i));
            t.start();
            try {
                t.join();
            } catch (InterruptedException e) {              
                e.printStackTrace();
            }            
           // System.out.println(list.get(i));
        }        
        System.out.println("----"+(System.currentTimeMillis() - start));
    }    
}

//線程類
package com.thread.list;
 
import java.util.List;
import java.util.Map;
 
public class MyThread1 extends Thread {
 
    private List<String> list;
    private Map<Long,Integer> map;
    
    public MyThread1(List<String> list,Map<Long,Integer> map){
        this.list = list;
        this.map = map;
    }
    
    @Override
    public void run() {
        
        int pcount = Runtime.getRuntime().availableProcessors();
        int i = map.get(Thread.currentThread().getId());
        
        for(;i<list.size();i+=pcount){
            System.out.println(list.get(i));
        }              
    }    
}
           

3.多線程分段處理List集合

場景:大資料

List

集合,需要對

List

集合中的資料同标準庫中資料進行對比,生成新增,更新,取消資料

解決方案:

List

集合分段,

動态建立線程池

newFixedThreadPool

将對比操作在多線程中實作

public static void main(String[] args) throws Exception {

        // 開始時間
        long start = System.currentTimeMillis();
        List<String> list = new ArrayList<String>();

        for (int i = 1; i <= 3000; i++) {
            list.add(i + "");
        }
        // 每500條資料開啟一條線程
        int threadSize = 500;
        // 總資料條數
        int dataSize = list.size();
        // 線程數
        int threadNum = dataSize / threadSize + 1;
        // 定義标記,過濾threadNum為整數
        boolean special = dataSize % threadSize == 0;

        // 建立一個線程池
        ExecutorService exec = Executors.newFixedThreadPool(threadNum);
        // 定義一個任務集合
        List<Callable<Integer>> tasks = new ArrayList<Callable<Integer>>();
        Callable<Integer> task = null;
        List<String> cutList = null;

        // 确定每條線程的資料
        for (int i = 0; i < threadNum; i++) {
            if (i == threadNum - 1) {
                if (special) {
                    break;
                }
                cutList = list.subList(threadSize * i, dataSize);
            } else {
                cutList = list.subList(threadSize * i, threadSize * (i + 1));
            }
            // System.out.println("第" + (i + 1) + "組:" + cutList.toString());
            final List<String> listStr = cutList;
            task = new Callable<Integer>() {

                @Override
                public Integer call() throws Exception {
                    System.out.println(Thread.currentThread().getName() + "線程:" + listStr);
                    return 1;
                }
            };
            // 這裡送出的任務容器清單和傳回的Future清單存在順序對應的關系
            tasks.add(task);
        }

        List<Future<Integer>> results = exec.invokeAll(tasks);

        for (Future<Integer> future : results) {
            System.out.println(future.get());
        }

        // 關閉線程池
        exec.shutdown();
        System.out.println("線程任務執行結束");
        System.err.println("執行任務消耗了 :" + (System.currentTimeMillis() - start) + "毫秒");
    }
           

12、Java中對象的生命周期

1)建立階段(Created):

為對象配置設定存儲空間,開始構造對象,從超類到子類對

static

成員初始化;超類成員變量按順序初始化,遞歸調用超類的構造方法,子類成員變量按順序初始化,子類構造方法調用。

2)應用階段(In Use):

對象至少被一個強引用持有着。

3)不可見階段(Invisible):

程式運作已超出對象作用域

4)不可達階段(Unreachable):

該對象不再被強引用所持有

5)收集階段(Collected):

假設該對象重寫了

finalize()

方法且未執行過,會去執行該方法。

6)終結階段(Finalized):

對象運作完

finalize()

方法仍處于不可達狀态,等待垃圾回收器對該對象空間進行回收。

7)對象空間重新配置設定階段(De-allocated):

垃圾回收器對該對象所占用的記憶體空間進行回收或再配置設定,該對象徹底消失。

13、static synchronized 方法的多線程通路和作用

static synchronized

控制的是類的所有執行個體通路,不管

new

了多少對象,隻有一份,是以對該類的所有對象都加了鎖。限制多線程中該類的所有執行個體同時通路

JVM

中該類對應的代碼。

14、同一個類裡面兩個synchronized方法,兩個線程同時通路的問題

如果

synchronized

修飾的是靜态方法,鎖的是目前類的

class

對象,進入同步代碼前要獲得目前類對象的鎖;

普通方法,鎖的是目前執行個體對象,進入同步代碼前要獲得的是目前執行個體的鎖;

同步代碼塊,鎖的是括号裡面的對象,對給定的對象加鎖,進入同步代碼塊庫前要獲得給定對象鎖;

如果兩個線程通路同一個對象的

synchronized

方法,會出現競争,如果是不同對象,則不會互相影響。

15、volatile的原理

volatile

變量修飾的共享變量進行寫操作的時候會多一條彙編代碼,

lock addl $0x0,lock

字首的指令在多核處理器下會将目前處理器緩存行的資料會寫回到系統記憶體,這個寫回記憶體的操作會引起在其他

CPU

裡緩存了該記憶體位址的資料無效。同時

lock

字首也相當于一個記憶體屏障,對記憶體操作順序進行了限制。

16、synchronized原理

synchronized

通過對象的對象頭

(markword)

來實作鎖機制,

java

每個對象都有對象頭,都可以為

synchronized

實作提供基礎,都可以作為鎖對象,在位元組碼層面

synchronized

塊是通過插入

monitorenter monitorexit

完成同步的。持有

monitor

對象,通過進入、退出這個

Monitor

對象來實作鎖機制。

17、談談NIO的了解

NIO( New Input/ Output)

引入了一種基于通道和緩沖區的

I/O

方式,它可以使用

Native

函數庫直接配置設定堆外記憶體,然後通過一個存儲在

Java

堆的

DirectByteBuffer

對象作為這塊記憶體的引用進行操作,避免了在

Java

堆和

Native

堆中來回複制資料。 

NIO

是一種同步非阻塞的

IO

模型。同步是指線程不斷輪詢

IO

事件是否就緒,非阻塞是指線程在等待

IO

的時候,可以同時做其他任務。同步的核心就是

Selector,Selector

代替了線程本身輪詢

IO

事件,避免了阻塞同時減少了不必要的線程消耗;非阻塞的核心就是通道和緩沖區,當

IO

事件就緒時,可以通過寫道緩沖區,保證

IO

的成功,而無需線程阻塞式地等待。

18.ReentrantLock 、Lock、synchronized和volatile比較

1)volatile:

解決變量在多個線程間的可見性,但不能保證原子性,隻能用于修飾變量,不會發生阻塞。

volatile

能屏蔽編譯指令重排,不會把其後面的指令排到記憶體屏障之前的位置,也不會把前面的指令排到記憶體屏障的後面。多用于并行計算的單例模式。

volatile

規定

CPU

每次都必須從記憶體讀取資料,不能從

CPU

緩存中讀取,保證了多線程在多

CPU

計算中永遠拿到的都是最新的值。

2)synchronized:

互斥鎖,操作互斥,并發線程過來,串行獲得鎖,串行執行代碼。解決的是多個線程間通路共享資源的同步性,可保證原子性,也可間接保證可見性,因為它會将私有記憶體和公有記憶體中的資料做同步。可用來修飾方法、代碼塊。會出現阻塞。

synchronized

發生異常時,會自動釋放線程占有的鎖,是以不會導緻死鎖現象發生。非公平鎖,每次都是互相争搶資源。

3)lock

是一個接口,而

synchronized

java

中的關鍵字,

synchronized

是内置語言的實作。

lock

可以讓等待鎖的線程響應中斷。在發生異常時,如果沒有主動通過

unLock()

去釋放鎖,則可能造成死鎖現象,是以使用

Lock

時需要在

finally

塊中釋放鎖。

4)ReentrantLock

可重入鎖,鎖的配置設定機制是基于線程的配置設定,而不是基于方法調用的配置設定。

ReentrantLock

tryLock

方法,如果鎖被其他線程持有,傳回

false

,可避免形成死鎖。對代碼加鎖的顆粒會更小,更節省資源,提高代碼性能。

ReentrantLock

可實作公平鎖和非公平鎖,公平鎖就是先來的先擷取資源。

ReentrantReadWriteLock

用于讀多寫少的場合,且讀不需要互斥場景。

至此,本篇已結束,如有不對的地方,歡迎您的建議與指正。同時期待您的關注,感謝您的閱讀,謝謝!

線程、多線程和線程池面試專題