進程和線程都是一個時間段的描述,是CPU工做時間段的描述,不過是顆粒大小不一樣。 java
他們主要區別是:進程不共享內存,線程能夠共享內存。 引用知乎地址Java中線程的建立有兩種方式: 1.經過繼承Thread類,重寫Thread的run()方法,將線程運行的邏輯放在其中。c++
2.經過實現Runnable接口,實例化Thread類。數組
咱們一般使用第二種,由於能夠複用Runnable,更容易實現資源共享,能多個線程同時處理一個資源。緩存
// 1
public class MyRunnable implements Runnable {
@Override
public void run() {
System.out.println("this is a Runnable");
}
}
// 2
public class MyThread extends Thread {
@Override
public void run() {
super.run();
System.out.println("this is thread");
}
}
// 具體使用
public class Main {
public static void main(String[] args) {
// 第一種
Thread thread1 = new Thread(new MyRunnable());
thread1.start();
// 第二種
MyThread thread2 = new MyThread();
thread2.start();
}
}
複製代碼
而實際Android開發工做中,以上兩種都不用,咱們一般使用Android提供的Handler和java.util包裏的Executor。安全
Executor 是一個接口,execute執行Runnable。bash
public interface Executor {
/**
* Executes the given command at some time in the future. The command
* may execute in a new thread, in a pooled thread, or in the calling
* thread, at the discretion of the {@code Executor} implementation.
*
* @param command the runnable task
* @throws RejectedExecutionException if this task cannot be
* accepted for execution
* @throws NullPointerException if command is null
*/
void execute(Runnable command);
}
複製代碼
看下使用:多線程
val executor: Executor = Executors.newCachedThreadPool()
executor.execute { }
複製代碼
點進去newCachedThreadPool,發現返回的是一個ExecutorService。ExecutorService就是Executor的實現了。架構
return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
60L, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>());
}
複製代碼
ExecutorService有兩個方法:併發
void shutdown();
是指再也不添加任務,執行完已有任務後結束。 List<Runnable> shutdownNow();
是當即調用線程的interrupt()結束全部的線程。app
上面看到Executors裏面new的是ThreadPoolExecutor,咱們看下ThreadPoolExecutor的構造方法:
//五個參數的構造函數
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue)
//六個參數的構造函數-1
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory)
//六個參數的構造函數-2
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
RejectedExecutionHandler handler)
//七個參數的構造函數
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler)
複製代碼
corePoolSize
: 該線程池中核心線程數最大值核心線程:在建立完線程池以後,核心線程先不建立,在接到任務以後建立核心線程。而且會一直存在於線程池中(即便這個線程啥都不幹),有任務要執行時,若是核心線程沒有被佔用,會優先用核心線程執行任務。數量通常狀況下設置爲CPU核數的二倍便可。
maximumPoolSize
: 該線程池中線程總數最大值線程總數=核心線程數+非核心線程數。
非核心線程:簡單理解,即核心線程都被佔用,但還有任務要作,就建立非核心線程。
keepAliveTime
: 非核心線程閒置超時時長這個參數能夠理解爲,任務少,但池中線程多,非核心線程不能白養着,超過這個時間不工做的就會被幹掉,可是核心線程會保留。
TimeUnit
: keepAliveTime的單位TimeUnit是一個枚舉類型,其包括:
NANOSECONDS:1微毫秒 = 1微秒 / 1000
MICROSECONDS:1微秒 = 1毫秒 / 1000
MILLISECONDS:1毫秒 = 1秒 /1000
SECONDS:秒
MINUTES:分
HOURS:小時
DAYS:天
複製代碼
BlockingQueue workQueue
: 線程池中的任務隊列默認狀況下,任務進來以後先分配給核心線程執行,核心線程若是都被佔用,並不會馬上開啓非核心線程執行任務,而是將任務插入任務隊列等待執行,核心線程會從任務隊列取任務來執行,任務隊列能夠設置最大值,一旦插入的任務足夠多,達到最大值,纔會建立非核心線程執行任務。
常見的workQueue有四種:
SynchronousQueue:這個隊列接收到任務的時候,會直接提交給線程處理,而不保留它,若是全部線程都在工做怎麼辦?那就新建一個線程來處理這個任務!因此爲了保證不出現<線程數達到了maximumPoolSize而不能新建線程>的錯誤,使用這個類型隊列的時候,maximumPoolSize通常指定成Integer.MAX_VALUE,即無限大。
LinkedBlockingQueue:這個隊列接收到任務的時候,若是當前已經建立的核心線程數小於線程池的核心線程數上限,則新建線程(核心線程)處理任務;若是當前已經建立的核心線程數等於核心線程數上限,則進入隊列等待。因爲這個隊列沒有最大值限制,即全部超過核心線程數的任務都將被添加到隊列中,這也就致使了maximumPoolSize的設定失效,由於總線程數永遠不會超過corePoolSize
ArrayBlockingQueue:能夠限定隊列的長度,接收到任務的時候,若是沒有達到corePoolSize的值,則新建線程(核心線程)執行任務,若是達到了,則入隊等候,若是隊列已滿,則新建線程(非核心線程)執行任務,又若是總線程數到了maximumPoolSize,而且隊列也滿了,則發生錯誤,或是執行實現定義好的飽和策略。
DelayQueue:隊列內元素必須實現Delayed接口,這就意味着你傳進去的任務必須先實現Delayed接口。這個隊列接收到任務時,首先先入隊,只有達到了指定的延時時間,纔會執行任務。
ThreadFactory threadFactory
-> 建立線程的工廠能夠用線程工廠給每一個建立出來的線程設置名字。通常狀況下無須設置該參數。
RejectedExecutionHandler handler
-> 飽和拒絕策略這是當任務隊列和線程池都滿了時所採起的應對策略,默認是AbordPolicy。
AbordPolicy
:表示沒法處理新任務,並拋出 RejectedExecutionException 異常。此外還有3種策略,它們分別以下。
CallerRunsPolicy
:用調用者所在的線程來處理任務。此策略提供簡單的反饋控制機制,可以減緩新任務的提交速度。
DiscardPolicy
:不能執行的任務,並將該任務刪除。
DiscardOldestPolicy
:丟棄隊列最近的任務,並執行當前的任務。
Executors類爲咱們提供的四種簡單建立線程池的方法:
private val fix = Executors.newFixedThreadPool(4)
private val cache = Executors.newCachedThreadPool()
private val single = Executors.newSingleThreadExecutor()
private val scheduled = Executors.newScheduledThreadPool(4)
複製代碼
其實就是調用不一樣的ThreadPoolExecutor的構造方法。下面一個一個分析:
FixedThreadPool
public static ExecutorService newFixedThreadPool(int nThreads) {
return new ThreadPoolExecutor(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>());
}
複製代碼
FixedThreadPool的corePoolSize和maximumPoolSize都設置爲參數nThreads,也就是隻有固定數量的核心線程,不存在非核心線程。keepAliveTime爲0L表示多餘的線程馬上終止,由於不會產生多餘的線程,因此這個參數是無效的,也就是說線程不會被回收一直保存在線程池。FixedThreadPool的任務隊列採用的是LinkedBlockingQueue。通常咱們設置爲cpu核心數+1。
private val fix = Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors() + 1)
FixThreadPool其實就像一堆人排隊上公廁同樣,能夠無數多人排隊,可是廁所位置就那麼多,並且沒人上時,廁所閒置着也不會搬走。
SingleThreadPool
public static ExecutorService newSingleThreadExecutor() {
return new FinalizableDelegatedExecutorService
(new ThreadPoolExecutor(1, 1,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>()));
}
複製代碼
咱們能夠看到總線程數和核心線程數都是1,因此就只有一個核心線程。該線程池才用鏈表阻塞隊列LinkedBlockingQueue,先進先出原則,因此保證了任務的按順序逐一進行。
SingleThreadPool能夠理解爲公廁裏只有一個坑位,先來先上。
CachedThreadPool
public static ExecutorService newCachedThreadPool() {
return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
60L, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>());
}
複製代碼
CachedThreadPool的corePoolSize是0,maximumPoolSize是Int的最大值,也就是說CachedThreadPool沒有核心線程,所有都是非核心線程,而且沒有上限。keepAliveTime是60秒,就是說空閒線程等待新任務60秒,超時則銷燬。此處用到的隊列是阻塞隊列SynchronousQueue,這個隊列沒有緩衝區,因此其中最多隻能存在一個元素,有新的任務則阻塞等待。
適用於頻繁IO的操做,由於他們的任務量小,可是任務基數很是龐大,使用核心線程處理的話,數量建立方面就很成問題。
CachedThreadPool有點像去衝浪,由於海洋無限大,隨時去都有位置衝浪,一我的衝完60秒內能夠免費給下一我的玩。超過60秒衝浪板就被商家回收。
public ScheduledThreadPoolExecutor(int corePoolSize) {
super(corePoolSize, Integer.MAX_VALUE,
DEFAULT_KEEPALIVE_MILLIS, MILLISECONDS,
new DelayedWorkQueue());
}
複製代碼
能夠看出corePoolSize是傳進來的固定值,maximumPoolSize無限大,由於採用的隊列DelayedWorkQueue是無解的,因此maximumPoolSize參數無效。若是運行的線程達到了corePoolSize時,則將任務添加到DelayedWorkQueue中。DelayedWorkQueue會將任務進行排序,先要執行的任務會放在隊列的前面。在跟此前介紹的線程池不一樣的是,當執行完任務後,會將ScheduledFutureTask中的time變量改成下次要執行的時間並放回到DelayedWorkQueue中。
ScheduledThreadPool主要用於執行定時任務以及有固定週期的重複任務。
Callable是java1.5添加進來的一個加強版本。相似於Runnable,卻又有差別:
下面看下使用:
val executor: ExecutorService = Executors.newSingleThreadExecutor()
val future: Future<String> = executor.submit(MyCallable())
try {
val string: String = future.get()
} catch (e: ExecutionException) {
}
executor.shutdown()
複製代碼
class MyCallable() : Callable<String> {
override fun call(): String {
return "done"
}
}
複製代碼
由於硬件架構,會致使一些問題,特別在多線程的時候更爲突出:
緩存一致性問題:在多處理器系統中,每一個處理器都有本身的高速緩存,而它們又共享同一主內存(MainMemory)。基於高速緩存的存儲交互很好地解決了處理器與內存的速度矛盾,可是也引入了新的問題:緩存一致性(CacheCoherence)。當多個處理器的運算任務都涉及同一塊主內存區域時,將可能致使各自的緩存數據不一致的狀況,若是真的發生這種狀況,那同步回到主內存時以誰的緩存數據爲準呢?爲了解決一致性的問題,須要各個處理器訪問緩存時都遵循一些協議,在讀寫時要根據協議來進行操做,這類協議有MSI、MESI(IllinoisProtocol)、MOSI、Synapse、Firefly及DragonProtocol,等等。
指令重排序問題:爲了使得處理器內部的運算單元能儘可能被充分利用,處理器可能會對輸入代碼進行亂序執行(Out-Of-Order Execution)優化,處理器會在計算以後將亂序執行的結果重組,保證該結果與順序執行的結果是一致的,但並不保證程序中各個語句計算的前後順序與輸入代碼中的順序一致。所以,若是存在一個計算任務依賴另外一個計算任務的中間結果,那麼其順序性並不能靠代碼的前後順序來保證。與處理器的亂序執行優化相似,Java虛擬機的即時編譯器中也有相似的指令重排序(Instruction Reorder)優化。
線程間通訊必需要通過主內存。
以下,若是線程A與線程B之間要通訊的話,必需要經歷下面2個步驟:
1)線程A把本地內存A中更新過的共享變量刷新到主內存中去。
2)線程B到主內存中去讀取線程A以前已更新過的共享變量。
當對象和變量被存放在計算機中各類不一樣的內存區域中時,就可能會出現一些具體的問題。Java內存模型創建所圍繞的問題:在多線程併發過程當中,如何處理多線程讀同步問題與可見性(多線程緩存與指令重排序)、多線程寫同步問題與原子性。
Java內存模型(即Java Memory Model,簡稱JMM)自己是一種抽象的概念,並不真實存在,它描述的是一組規則或規範,經過這組規範定義了程序中各個變量(包括實例字段,靜態字段和構成數組對象的元素)的訪問方式。
多線程讀同步與可見性 線程對共享變量修改的可見性。當一個線程修改了共享變量的值,其餘線程可以馬上得知這個修改。
原子性 指一個操做是按原子的方式執行的。要麼該操做不被執行;要麼以原子方式執行,即執行過程當中不會被其它線程中斷。
有序性 有序性是指對於單線程的執行代碼,咱們老是認爲代碼的執行是按順序依次執行的,這樣的理解並無毛病,畢竟對於單線程而言確實如此,但對於多線程環境,則可能出現亂序現象,由於程序編譯成機器碼指令後可能會出現指令重排現象,重排後的指令與原指令的順序未必一致,要明白的是,在Java程序中,假若在本線程內,全部操做都視爲有序行爲,若是是多線程環境下,一個線程中觀察另一個線程,全部操做都是無序的,前半句指的是單線程內保證串行語義執行的一致性,後半句則指指令重排現象和工做內存與主內存同步延遲現象。
volatile關鍵字有以下兩個做用
//線程1
boolean stop = false;
while(!stop){
doSomething();
}
//線程2
stop = true;
複製代碼
若是線程2改變了stop的值,線程1必定會中止嗎?不必定。當線程2更改了stop變量的值以後,可是還沒來得及寫入主存當中,線程2轉去作其餘事情了,那麼線程1因爲不知道線程2對stop變量的更改,所以還會一直循環下去。
可是用volatile修飾以後就變得不同了:
//線程1
volatile boolean stop = false;
while(!stop){
doSomething();
}
//線程2
stop = true;
複製代碼
第一:使用volatile關鍵字會強制將修改的值當即寫入主存;
第二:使用volatile關鍵字的話,當線程2進行修改時,會致使線程1的工做內存中緩存變量stop的緩存行無效(反映到硬件層的話,就是CPU的L1或者L2緩存中對應的緩存行無效);
第三:因爲線程1的工做內存中緩存變量stop的緩存行無效,因此線程1再次讀取變量stop的值時會去主存讀取。
那麼在線程2修改stop值時(固然這裏包括2個操做,修改線程2工做內存中的值,而後將修改後的值寫入內存),會使得線程1的工做內存中緩存變量stop的緩存行無效,而後線程1讀取時,發現本身的緩存行無效,它會等待緩存行對應的主存地址被更新以後,而後去對應的主存讀取最新的值。
那麼線程1讀取到的就是最新的正確的值
這也就是內存模型JMM的內存可見性。
private volatile int inc = 0;
void count() {
inc++;
}
void add() {
new Thread() {
@Override
public void run() {
for (int j = 0; j < 100_00_00; j++) {
count();
}
System.out.println(inc);
}
}.start();
new Thread() {
@Override
public void run() {
for (int j = 0; j < 100_00_00; j++) {
count();
}
System.out.println(inc);
}
}.start();
}
複製代碼
看這段代碼,2個線程分別加一百萬次。結果會打印出兩百萬次嗎?不會的。可能有的人就會有疑問,不對啊,上面是對變量inc進行自增操做,因爲volatile保證了可見性,那麼在每一個線程中對inc自增完以後,在其餘線程中都能看到修改後的值啊,因此有兩個線程分別進行了一百萬次操做,那麼最終inc的值應該是兩百萬啊。
這裏面就有一個誤區了,volatile關鍵字能保證可見性沒有錯,可是上面的程序錯在沒能保證原子性。可見性只能保證每次讀取的是最新的值,可是volatile沒辦法保證對變量的操做的原子性。
inc++;
實際上是兩個步驟,先加加,而後再賦值。不是原子性操做,因此volatile不能保證線程安全。
synchronized是Java中的關鍵字,是利用鎖的機制來實現同步的。Synchronized的做用主要有三個:
synchronized 能夠修飾方法和代碼塊,進入synchronized修飾的方法或者代碼塊的線程,就會獲取monitor對象,monitor也就是Java裏的對象鎖。
下面看下經典的賣票案例:
class Ticket implements Runnable {
/* 五百張票 */
private int tickets = 500;
@Override
public void run() {
while (true) {
//同步鎖
synchronized (this) {
if (tickets > 0) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.printf("%s窗口正在賣出第%d張票!\n", Thread.currentThread().getName(), tickets--);
} else {
System.out.printf("%s窗口已售罄\n", Thread.currentThread().getName());
System.exit(0);
}
}
}
}
}
複製代碼
public static void main(String[] args) {
Ticket ticket = new Ticket();
Thread thread1= new Thread(ticket);
Thread thread2 = new Thread(ticket);
Thread thread3 = new Thread(ticket);
thread1.start();
thread2.start();
thread3.start();
}
複製代碼
3個線程賣500張票。利用synchronized實現線程安全,下面修改下實現:
class Ticket {
/* 五百張票 */
private int tickets = 500;
public void sellTckets() {
while (true) {
//同步鎖
synchronized (this) {
if (tickets > 0) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.printf("%s窗口正在賣出第%d張票!\n", Thread.currentThread().getName(), tickets--);
} else {
System.out.printf("%s窗口已售罄\n", Thread.currentThread().getName());
System.exit(0);
}
}
}
}
}
複製代碼
public static void main(String[] args) {
final Ticket ticket = new Ticket();
Thread thread1= new Thread(){
@Override
public void run() {
ticket.sellTckets();
}
};
Thread thread2 = new Thread(){
@Override
public void run() {
ticket.sellTckets();
}
};
Thread thread3 = new Thread(){
@Override
public void run() {
ticket.sellTckets();
}
};
thread1.start();
thread2.start();
thread3.start();
}
複製代碼
同樣的線程安全,多線程賣票,可是如今我不只要賣票,還要訂餐,賣票和訂餐是兩個互不干涉的操做,可是由於 synchronized (this)拿到的是同一個對象鎖,因此若是線程1在賣票,那麼線程2就不能拿到對象鎖去訂餐:
class Ticket {
/* 二百張票 */
private int tickets = 200;
/* 二百份盒飯 */
private int foods = 200;
public void sellTckets() {
while (true) {
//同步鎖
synchronized (this) {
if (tickets > 0) {
try {
Thread.sleep(50);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.printf("%s窗口正在賣出第%d張票!\n", Thread.currentThread().getName(), tickets--);
} else {
System.out.printf("%s窗口車票已售罄\n", Thread.currentThread().getName());
System.exit(0);
}
}
}
}
public void sellFoods() {
while (true) {
//同步鎖
synchronized (this) {
if (foods > 0) {
try {
Thread.sleep(50);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.printf("%s窗口正在賣出第%d份盒飯!\n", Thread.currentThread().getName(), foods--);
} else {
System.out.printf("%s窗口盒飯已售罄\n", Thread.currentThread().getName());
System.exit(0);
}
}
}
}
複製代碼
那麼怎麼能多線程訂票的同時,別的線程也能夠訂餐呢?用不一樣的對象便可:
class Ticket {
private int tickets = 200;
private int foods = 200;
Object object1 = new Object();
Object object2 = new Object();
public void sellTickets() {
while (true) {
//同步鎖
synchronized (object1) {
if (tickets > 0) {
try {
Thread.sleep(50);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.printf("%s窗口正在賣出第%d張票!\n", Thread.currentThread().getName(), tickets--);
} else {
System.out.printf("%s窗口車票已售罄\n", Thread.currentThread().getName());
System.exit(0);
}
}
}
}
public void sellFoods() {
while (true) {
//同步鎖
synchronized (object2) {
if (foods > 0) {
try {
Thread.sleep(50);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.printf("%s窗口正在賣出第%d份盒飯!\n", Thread.currentThread().getName(), foods--);
} else {
System.out.printf("%s窗口盒飯已售罄\n", Thread.currentThread().getName());
System.exit(0);
}
}
}
}
}
複製代碼
這就像你家裏2個臥室,門鎖是同樣的鎖因此都用同一把鑰匙。老王拿着鑰匙進入主臥反鎖了門睡覺,你想去次臥睡,可是鑰匙被老王拿進主臥了。你去不了次臥。只能等他出來把鑰匙給你。怎麼能你倆都去睡覺呢?那就配兩把鑰匙。老王拿着主臥的鑰匙去了主臥,你拿着次臥的鑰匙去次臥睡。