1.執行緒和程式
1.1執行緒和程式的區別
- 程式 它是記憶體中的一段獨立的空間,可以負責當前應用程式的執行。當前這個程式負責排程當前程式中的所有執行細節(作業系統為程式分配一塊獨立的執行空間);
- 執行緒 它是位於程式中,負責當前程式中的某個具備獨立執行資格的空間(程式為執行緒分配一塊獨立執行的空間); 程式是負責某個程式的執行,執行緒是負責程式中某個獨立功能的執行,一個程式至少要包含一個執行緒。
- 多執行緒 在一個程式中可以開啟多個執行緒,讓多個執行緒同時去完成某項任務。使用多執行緒的目的是為了提高程式的執行效率。
1.2執行緒執行狀態
通過Thread類或Runnable介面建立執行緒物件之後進入初始狀態;呼叫start方法進入可執行狀態(就緒狀態),此時並不是真正的執行,只是代表已經做好了執行前的各項裝備;如果此執行緒獲取到cpu的時間片,則進入到真正的可執行狀態,執行run方法裡面的業務邏輯;如果run方法執行完畢或呼叫stop方法則執行緒執行結束,進入死亡狀態;在執行狀態時呼叫不同方法也會進入其他不同狀態,如果呼叫強制執行方法join或休眠方法將進入等待狀態,時間到後自動進入就緒狀態,隨時準備獲取cpu時間片;如果看到synchronized則進入同步佇列等待狀態,或者如果呼叫了wait方法則進入等待狀態,等待狀態的執行緒必須要通過notify喚醒才可進入等待狀態,如果其它執行緒執行完畢,本執行緒拿到同步鎖則進入就緒狀態,等待獲取cpu時間片。某個執行緒是否會執行只能看它能否爭搶到cpu時間片,但是通過調高優先順序來讓執行緒更大概率的被優先執行。 參考文件:https://mp.weixin.qq.com/s?src=11×tamp=1513562547&ver=581&signature=30FEkCCQvF3E1tt67vYVym5tRNsSk3d8HGe0v9TAonJmhLh4-53fDEBbgwNFOlgp5rAlGFAJQXYnviaFRwiQ9NmbtIWnZGpotGcuV0Ok*3WzWxg4X6e2mxU0JrgbRb&new=1
2.多執行緒
多執行緒執行的原理是:cpu線上程中做時間片的切換。cpu負責程式的執行,在每個時間點它其實只能執行一個程式而不是多個程式,不停的在多個程式之間高速切換,而一個程式其實就是一個程式即多個執行緒,說到底其實就是cpu在多個執行緒之間不停的做高速切換,而開多個執行緒就是不讓cpu歇著,最大程度的壓榨它來為程式服務。實現多執行緒有三種方式:繼承Thread類;實現Runnable介面;使用執行緒池。
2.1繼承Thread類
public class MyExtendsThread extends Thread {
String flag;
public MyExtendsThread(String flag){
this.flag = flag;
}
@Override
public void run(){
String name = Thread.currentThread().getName();
System.out.println("執行緒"+name+"開始工作了...");
Random random = new Random();
for (int i = 0;i < 20;i++){
try {
Thread.sleep(random.nextInt(10)*100);
System.out.println(name+"============="+flag);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public static void main(String[] args) {
Thread t0 = new MyExtendsThread("t0");
Thread t1 = new MyExtendsThread("t1");
t0.start();
t1.start();
// t0.run();
// t1.run();
}
}
複製程式碼
呼叫執行緒要用start方法,而不是run方法,使用run方法只是呼叫方法,實際執行的還是Main執行緒,而呼叫start方法可以明顯的看到執行緒爭搶。
2.2實現Runnable介面
public class MyThreadImplementRunnable implements Runnable {
int x;
public MyThreadImplementRunnable(int x) {
this.x = x;
}
@Override
public void run() {
String name = Thread.currentThread().getName();
System.out.println("執行緒"+name+"開始執行");
Random random = new Random();
for(int i = 0;i<20;i++){
try {
Thread.sleep(random.nextInt(10)*100);
System.out.println(name+"============="+x);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public static void main(String[] args) {
Thread t1 = new Thread(new MyThreadImplementRunnable(1),"執行緒1");
Thread t2 = new Thread(new MyThreadImplementRunnable(2),"執行緒2");
t1.start();
t2.start();
}
}
複製程式碼
2.3實現Callable介面
- 建立實現Callable介面的類MyThreadImplementCallable;
- 建立一個類物件:MyThreadImplementCallable callable = new MyThreadImplementCallable("測試");
- 由Callable建立一個FutureTask物件: FutureTask futureTask = new FutureTask(callable); 注意:FutureTask是一個包裝器,它通過接受Callable來建立,它同時實現了Future和Runnable介面。
- 由FutureTask建立一個Thread物件: Thread thread = new Thread(futureTask);
- 啟動執行緒: thread.start();
- 獲取任務執行緒執行結果 futureTask.get(); 注意:實現Callable介面的執行緒可以獲得任務執行緒的執行結果;實現Runnable介面的執行緒無法獲取任務執行緒執行的結果。
public class MyThreadImplementCallable implements Callable<String> {
String name;
public MyThreadImplementCallable(String name) {
this.name = name;
}
@Override
public String call() throws Exception {
Thread thread = Thread.currentThread();
System.out.println(thread.getName()+"開始工作==============");
Random random = new Random();
Thread.sleep(random.nextInt(5)*100); //模擬執行業務
return name+":執行完成";
}
public static void main(String[] args) throws Exception{
MyThreadImplementCallable callable = new MyThreadImplementCallable("測試");
FutureTask<String> futureTask = new FutureTask<String>(callable);
Thread thread = new Thread(futureTask);
thread.start();
String result = futureTask.get(); //獲取任務執行緒執行結果
System.out.println("執行緒的執行結果:"+result);
}
}
複製程式碼
2.4使用執行緒池
見下面的執行緒池專講。 參考文件:https://www.cnblogs.com/langtianya/archive/2013/03/14/2959713.html
3.同步
3.1synchronized關鍵字
public class MySynchronized {
public static void main(String[] args){
final MySynchronized synchronized1 = new MySynchronized();
final MySynchronized synchronized2 = new MySynchronized();
new Thread("thread1"){
@Override
public void run(){
synchronized (synchronized1){
try {
System.out.println(this.getName()+":start");
Thread.sleep(1000);
System.out.println(this.getName()+":wake up");
System.out.println(this.getName()+":end");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}.start();
new Thread("thread2"){
@Override
public void run() {
synchronized (synchronized1){ //爭搶同一把鎖時,執行緒1沒釋放之前,執行緒2只能等待
// synchronized (synchronized2){ //如果不是一把鎖,可以看到兩句話交叉列印,發生爭搶
System.out.println(this.getName()+":start");
System.out.println(this.getName()+":end");
}
}
}.start();
}
}
複製程式碼
synchronized是java中的關鍵字,屬於java語言的內建特性。如果一個程式碼塊使用synchronized修飾,則這塊程式碼是同步的,當一個執行緒獲取到這個鎖並且開始執行時,其它執行緒只能一直眼睜睜的等著這個執行緒執行然後釋放鎖,其中釋放鎖只有兩種原因:1.執行緒正常執行完畢;2.執行緒執行時發生異常,jvm自動將鎖釋放。可以看到使用synchronized關鍵字之後每個時刻只會有一個執行緒執行程式碼塊裡面的共享程式碼,執行緒安全;缺點也很明顯,其它執行緒只能等鎖釋放,資源浪費嚴重。
3.2Lock介面
- lock和synchronized的區別: Lock不是Java語言內建的,它是一個介面,通過這個介面可以實現同步訪問,synchronized是Java語言的關鍵字,是內建特性;Lock和synchronized有一點非常大的不同,採用synchronized不需要使用者去手動釋放鎖,當synchronized方法或者synchronized程式碼塊執行完之後,系統會自動讓執行緒釋放對鎖的佔用;而Lock則必須要使用者去手動釋放鎖,如果沒有主動釋放鎖,就有可能導致出現死鎖現象。 Lock是一個介面,它裡面有如下方法:
public interface Lock {
void lock();
void lockInterruptibly() throws InterruptedException;
boolean tryLock();
boolean tryLock(long time, TimeUnit unit) throws InterruptedException;
void unlock();
}
複製程式碼
lock()、tryLock()、tryLock(long time, TimeUnit unit)、lockInterruptibly()是用來獲取鎖的。 unLock()方法是用來釋放鎖的。
- lock就是用來獲取鎖的,前面說到如果採用Lock,必須主動去釋放鎖。即使發生異常,程式也不會自動釋放鎖,因此一般來說,使用Lock必須在try{}catch{}塊中進行,並且將釋放鎖的操作放在finally塊中進行,以保證鎖一定被被釋放,防止死鎖的發生。
public class MyLock {
private static ArrayList<Integer> arrayList = new ArrayList<Integer>();
private static Lock lock = new ReentrantLock();
public static <E> void main(String[] args) {
new Thread() {
@Override
public void run() {
Thread thread = Thread.currentThread();
lock.lock(); //獲取鎖
try {
System.out.println(thread.getName() + "得到了鎖");
for (int i = 0; i < 5; i++) {
arrayList.add(i);
}
} catch (Exception e) {
} finally {
System.out.println(thread.getName() + "釋放了鎖");
lock.unlock(); //釋放鎖
}
};
}.start();
new Thread() {
@Override
public void run() {
Thread thread = Thread.currentThread();
lock.lock();
try {
System.out.println(thread.getName() + "得到了鎖");
for (int i = 0; i < 5; i++) {
arrayList.add(i);
}
} catch (Exception e) {
} finally {
System.out.println(thread.getName() + "釋放了鎖");
lock.unlock();
}
};
}.start();
}
}
複製程式碼
- tryLock()表示用來嘗試獲取鎖,如果獲取成功,則返回true,如果獲取失敗(即鎖已被其他執行緒獲取),則返回false,也就說這個方法無論如何都會立即返回,在拿不到鎖時不會一直等待。
- tryLock(long time, TimeUnit unit)方法和tryLock()方法類似,區別在於這個方法在拿不到鎖時會等待一定的時間,在時間期限之內如果還拿不到鎖,就返回false。如果一開始就拿到鎖或者在等待期間內拿到了鎖,則返回true。
//觀察現象:一個執行緒獲得鎖後,另一個執行緒取不到鎖,不會一直等待
public class MyTryLock {
private static List<Integer> arrayList = new ArrayList<Integer>();
private static Lock lock = new ReentrantLock();
public static void main(String[] args) {
new Thread("執行緒1") {
@Override
public void run() {
Thread thread = Thread.currentThread();
boolean tryLock = lock.tryLock();
System.out.println(thread.getName()+"======="+tryLock);
if(tryLock){
try {
System.out.println(thread.getName() + "得到了鎖");
for(int i = 0;i < 20;i++){
arrayList.add(i);
}
Thread.sleep(1000);
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();
System.out.println(thread.getName() + "釋放了鎖");
}
}
}
}.start();
new Thread("執行緒2") {
@Override
public void run() {
Thread thread = Thread.currentThread();
boolean tryLock = lock.tryLock();
System.out.println(thread.getName()+"======="+tryLock);
if(tryLock){
try {
System.out.println(thread.getName() + "得到了鎖");
for(int i = 0;i < 20;i++){
arrayList.add(i);
}
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();
System.out.println(thread.getName() + "釋放了鎖");
}
}
}
}.start();
}
}
複製程式碼
執行緒1和執行緒2共享成員變數arrayList,當執行緒1獲取鎖的時候,執行緒2就獲取不到鎖,沒辦法執行它的業務邏輯,只有等執行緒1執行完畢,釋放了鎖,執行緒2才能獲取鎖,執行它的程式碼,進而保證了執行緒安全。
- lockInterruptibly()方法比較特殊,當通過這個方法去獲取鎖時,如果執行緒正在等待獲取鎖,則這個執行緒能夠響應中斷,即中斷執行緒的等待狀態。 注意,當一個執行緒獲取了鎖之後,是不會被interrupt()方法中斷的。 因此當通過lockInterruptibly()方法獲取某個鎖時,如果不能獲取到,只有進行等待的情況下,是可以中斷的。而用synchronized修飾的話,當一個執行緒處於等待某個鎖的狀態,是無法被中斷的,只有一直等待下去。
- Lock介面的實現類——ReentrantLock 直接使用lock介面的話,我們需要實現很多方法,不方便,ReentrantLock是唯一實現了Lock介面的類,並且ReentrantLock提供了更多的方法,ReentrantLock,意思是“可重入鎖”,使用它可以建立Lock物件。
- ReadWriteLock也是一個介面,在它裡面只定義了兩個方法:
public interface ReadWriteLock {
Lock readLock();
Lock writeLock();
}
複製程式碼
一個用來獲取讀鎖,一個用來獲取寫鎖。也就是說將檔案的讀寫操作分開,分成2個鎖來分配給執行緒,從而使得多個執行緒可以同時進行讀操作。
- ReentrantReadWriteLock裡面提供了很多豐富的方法,不過最主要的有兩個方法:readLock()和writeLock()用來獲取讀鎖和寫鎖,使用這個讀寫鎖操作的結果就是:要麼執行的全是讀操作,結束完之後全執行寫操作,中間不會交叉讀寫。
/**
* @author 劉俊重
* 如果有一個執行緒已經佔用了讀鎖,則此時其他執行緒如果要申請寫鎖,則申請寫鎖的執行緒會一直等待釋放讀鎖。
* 如果有一個執行緒已經佔用了寫鎖,則此時其他執行緒如果申請寫鎖或者讀鎖,則申請的執行緒會一直等待釋放寫鎖。
*/
public class MyReentrantReadWriteLock {
ReentrantReadWriteLock readWriteLock = new ReentrantReadWriteLock();
public static void main(String[] args) {
final MyReentrantReadWriteLock myTest = new MyReentrantReadWriteLock();
new Thread("執行緒1"){
@Override
public void run(){
myTest.read(Thread.currentThread());
myTest.writer(Thread.currentThread());
}
}.start();
new Thread("執行緒2"){
@Override
public void run(){
myTest.read(Thread.currentThread());
myTest.writer(Thread.currentThread());
}
}.start();
}
/**
* @Description 讀方法
* @Author 劉俊重
* @Date 2017/12/18
*/
private void read(Thread thread){
readWriteLock.readLock().lock();
try {
long start = System.currentTimeMillis();
while (System.currentTimeMillis()-start<=1){
System.out.println(thread.getName()+"===正在執行讀操作");
}
} catch (Exception e) {
e.printStackTrace();
} finally {
readWriteLock.readLock().unlock();
System.out.println(thread.getName()+"==釋放讀鎖");
}
}
/**
* @Description 寫方法
* @Author 劉俊重
* @Date 2017/12/18
*/
private void writer(Thread thread){
readWriteLock.writeLock().lock();
try {
long start = System.currentTimeMillis();
while (System.currentTimeMillis()-start<=1){
System.out.println(thread.getName()+"===正在執行寫操作");
}
} catch (Exception e) {
e.printStackTrace();
} finally {
readWriteLock.writeLock().unlock();
System.out.println(thread.getName()+"==釋放寫鎖");
}
}
}
複製程式碼
Lock和Synchronized的選擇:
- Lock是一個介面,而sysnchrinized是java關鍵字,屬於內建的語言實現;
- synchronized關鍵字程式執行完成之後或出現異常時會釋放鎖,使用lock不會自動釋放鎖,只能自己使用unlock釋放,否則會引起死鎖,最好在finally中釋放;
- 使用lock可以使用trylock方法判斷有沒有獲得鎖,使用synchronized無法判斷;
- 使用lock可以讓等待鎖的執行緒中斷,使用synchronized無法讓執行緒中斷,只能一直等待下去;
- 使用lock可以提高多執行緒讀操作的效率。 結論:如果競爭的資源不激烈,則使用synchronized和lock效率差不多;如果有大量執行緒同時競爭,則lock要遠遠優於synchronized。
4.volatile關鍵字
程式執行時有主記憶體,每個執行緒工作時也有自己的工作記憶體。當一個執行緒開始工作時會從主記憶體中拷貝一個變數的副本到工作記憶體中,在工作記憶體中操作完副本時再更新回主記憶體。當存在多執行緒時,如果工作記憶體A處理完還沒來得及更新回主記憶體之前,工作記憶體B就從主記憶體中拉取了這個變數,那麼很明顯這個變數並不是最新的資料,會出現問題。怎麼解決呢?可以使用volatile,volatile有個最顯著的特性就是對它所修飾變數具有可見性,什麼意思呢,就是當一個執行緒修改了變數的值,新的值會立刻(馬上)同步到主記憶體中,其它執行緒使用時拉取到的就是最新的變數值。儘管volatile能保證變數的可見性,但並不能保證執行緒安全,因為它不能保證原子性。要想執行緒安全還是要用同步或者鎖。 有一篇文件寫volatile寫的很好,貼一下:http://dwz.cn/76TMGW
5.執行緒池
JDK1.5之後引入了高階併發特性,在java.util.concurrent包中,是專門用於多執行緒併發程式設計的,充分利用了現代計算機多處理器和多核心的功能以編寫大規模併發應用程式。主要包含原子量、併發集合、同步器、可重入鎖,並對執行緒池的建立提供了強力的支援。
5.1執行緒池的5種建立方式
- Single Thread Executor : 只有一個執行緒的執行緒池,所有提交的任務都是順序執行; 程式碼: Executors.newSingleThreadExecutor()
- Cached Thread Pool : 執行緒池裡有很多執行緒需要同時執行,老的可用執行緒將被新的任務觸發重新執行,如果執行緒超過60秒內沒執行,那麼將被終止並從池中刪除; 程式碼:Executors.newCachedThreadPool()
- Fixed Thread Pool : 擁有固定執行緒數的執行緒池,如果沒有任務執行,那麼執行緒會一直等待, 程式碼: Executors.newFixedThreadPool(4) 在建構函式中的引數4是執行緒池的大小,你可以隨意設定,最好設定成和cpu的核數量保持一致,獲取cpu的核數量int cpuNums = Runtime.getRuntime().availableProcessors();
- Scheduled Thread Pool : 用來排程即將執行的任務的執行緒池,可能不是直接執行, 每隔多久執行一次,屬於策略型的。 程式碼:Executors.newScheduledThreadPool()
- Single Thread Scheduled Pool : 只有一個執行緒,用來排程任務在指定時間執行,程式碼:Executors.newSingleThreadScheduledExecutor() 示例程式碼如下:
public static void main(String[] args) {
ExecutorService newSingleThreadExecutor = Executors.newSingleThreadExecutor();
ExecutorService newCachedThreadPool = Executors.newCachedThreadPool();
//獲取cpu核心數
int num = Runtime.getRuntime().availableProcessors();
ExecutorService newFixedThreadPool = Executors.newFixedThreadPool(num);
ScheduledExecutorService newScheduledThreadPool = Executors.newScheduledThreadPool(8);
ScheduledExecutorService newSingleThreadScheduledExecutor = Executors.newSingleThreadScheduledExecutor();
}
複製程式碼
5.2執行緒池的使用
說到執行緒池使用之前再強調一下Runnable的孿生兄弟——Callable,他們兩個很像,只是Runnable的run方法不會有任何返回結果,主執行緒無法獲得任務執行緒的返回值;但是Callable的call方法可以返回結果,但是主執行緒在獲取時是被阻塞,需要等待任務執行緒返回才能拿到結果,所以Callable比Runnable更強大,那麼怎麼獲取到這個執行結果呢?答案是Future,使用Future可以獲取到Callable執行的結果。 現在開始說執行緒池怎麼使用,也有兩種方式,一種Runnable的,一種Callable的:
- 提交 Runnable ,任務完成後 Future 物件返回 null,呼叫excute,提交任務, 匿名Runable重寫run方法, run方法裡是業務邏輯。 示例程式碼:
public class TestPoolWithRunnable {
public static void main(String[] args) throws Exception{
ExecutorService pool = Executors.newFixedThreadPool(4);
for (int i=0;i<10;i++){
Future<?> submit = pool.submit(new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + "開始執行");
}
});
System.out.println("執行結果:"+submit.get()); //所有的執行結果全是null
}
pool.shutdown(); //關閉執行緒池
}
}
複製程式碼
- 提交 Callable,該方法返回一個 Future 例項表示任務的狀態,呼叫submit提交任務, 匿名Callable,重寫call方法, 有返回值, 獲取返回值會阻塞,一直要等到執行緒任務返回結果。
/**
* @author 劉俊重
* Callable 跟Runnable的區別:
* Runnable的run方法不會有任何返回結果,所以主執行緒無法獲得任務執行緒的返回值
* Callable的call方法可以返回結果,但是主執行緒在獲取時是被阻塞,需要等待任務執行緒返回才能拿到結果
*/
public class TestPoolWithCallable {
public static void main(String[] args) throws Exception{
ExecutorService pool = Executors.newFixedThreadPool(4);
for(int i=0;i<10;i++){
Future<String> future = pool.submit(new Callable<String>() {
@Override
public String call() throws Exception {
Thread.sleep(500);
return "===="+Thread.currentThread().getName();
}
});
//從Future中get結果,這個方法是會被阻塞的,一直要等到執行緒任務返回結果
System.out.println("執行結果:"+future.get());
}
pool.shutdown();
}
}
複製程式碼
如何解決獲取執行結果阻塞的問題? 在使用future.get()方法獲取結果時,這個方法是阻塞的,怎麼提高效率呢?如果在不要求立馬拿到執行結果的情況下,可以先將執行結果放在佇列裡面,待程式執行完畢之後在獲取每個執行緒的執行結果,示例程式碼如下:
public class TestThreadPool {
public static void main(String[] args) throws Exception{
Future<?> submit = null;
//建立快取執行緒池
ExecutorService cachePool = Executors.newCachedThreadPool();
//用來存在Callable執行結果
List<Future<?>> futureList = new ArrayList<Future<?>>();
for(int i = 0;i<10;i++){
//cachePool提交執行緒,Callable,Runnable無返回值
//submit = cachePool.submit(new TaskCallable(i));
submit = cachePool.submit(new TaskRunnable(i));
//把這些執行結果放到list中,後面再取可以避免阻塞
futureList.add(submit);
}
cachePool.shutdown();
//列印執行結果
for(Future f : futureList){
boolean done = f.isDone();
System.out.println(done?"已完成":"未完成");
System.out.println("執行緒返回結果:"+f.get());
}
}
}
複製程式碼
把submit放在list集合中,執行緒直線完畢之後再取。
6.java併發程式設計總結
6.1不使用執行緒池的缺點
直接使用new Thread().start()的方式,對於一般場景是沒問題的,但如果是在併發請求很高的情況下,就會有隱患:
- 新建執行緒的開銷。執行緒雖然比程式要輕量許多,但對於JVM來說,新建一個執行緒的代價還是很大的,決不同於新建一個物件。
- 資源消耗量。沒有一個池來限制執行緒的數量,會導致執行緒的數量直接取決於應用的併發量。
- 穩定性。當執行緒數量超過系統資源所能承受的程度,穩定性就會成問題。
6.2執行緒池的型別
不管是通過Executors建立執行緒池,還是通過Spring來管理,都得知道有哪幾種執行緒池:
- FixedThreadPool:定長執行緒池,提交任務時建立執行緒,直到池的最大容量,如果有執行緒非預期結束,會補充新執行緒;
- CachedThreadPool:可變執行緒池,它猶如一個彈簧,如果沒有任務需求時,它回收空閒執行緒,如果需求增加,則按需增加執行緒,不對池的大小做限制;
- SingleThreadExecutor:單執行緒。處理不過來的任務會進入FIFO佇列等待執行;
- SecheduledThreadPool:週期性執行緒池。支援執行週期性執行緒任務 其實,這些不同型別的執行緒池都是通過構建一個ThreadPoolExecutor來完成的,所不同的是corePoolSize,maximumPoolSize,keepAliveTime,unit,workQueue,threadFactory這麼幾個引數。
6.3執行緒池飽和策略
由以上執行緒池型別可知,除了CachedThreadPool其他執行緒池都有飽和的可能,當飽和以後就需要相應的策略處理請求執行緒的任務,比如,達到上限時通過ThreadPoolExecutor.setRejectedExecutionHandler方法設定一個拒絕任務的策略,JDK提供了AbortPolicy、CallerRunsPolicy、DiscardPolicy、DiscardOldestPolicy幾種策略。