四、線程池原理分析
1.阻塞隊列與非阻塞隊列
1.1阻塞隊列與非阻塞隊列的區別:
(1)從空的阻塞隊列中讀取元素,將會阻塞,知道其他線程插入元素到這個隊列中。
(2)往滿的隊列中添加元素,同樣也會阻塞,知道有線程從隊列中取出元素或者隊列中的元素被清除。
1.2下面列舉幾個常見的隊列:
(1)ArrayDeque, (數組雙端隊列)
(2)PriorityQueue, (優先級隊列)
(3)ConcurrentLinkedQueue, (基於鏈表的併發隊列)
(4)DelayQueue, (延期阻塞隊列)(阻塞隊列實現了BlockingQueue接口)
(5)ArrayBlockingQueue, (基於數組的併發阻塞隊列)
(6)LinkedBlockingQueue, (基於鏈表的FIFO阻塞隊列)
(7)LinkedBlockingDeque, (基於鏈表的FIFO雙端阻塞隊列)
(8)PriorityBlockingQueue, (帶優先級的無界阻塞隊列)
(9)SynchronousQueue (併發同步阻塞隊列)
1.3ConcurrentLinkedQueue
ConcurrentLinkedQueue :是一個基於高併發場景下的隊列,通過無鎖的方式實現了高併發下的高性能。他是一個聚集鏈接節點的無界安全隊列。不允許有null元素。
ConcurrentLinkedQueue重要方法:
add 和offer() 都是加入元素的方法(在ConcurrentLinkedQueue中這倆個方法沒有任何區別)
poll() 和peek() 都是取頭元素節點,區別在於前者會刪除元素,後者不會。
/**
*
* @author johson
*阻塞隊列和非阻塞隊列
*阻塞隊列的最大好處就是能防止隊列容器溢出,防止數據丟失
*/
public class test01 {
public static void main(String[] args){
//新建一個非阻塞式隊列,無界的
ConcurrentLinkedQueue<String> concurrentLinkedQueue = new ConcurrentLinkedQueue<>();
concurrentLinkedQueue.add("張三"); //add()調用了offer方法
//concurrentLinkedQueue.offer("王麻子");
concurrentLinkedQueue.add("李四");
concurrentLinkedQueue.add("王五");
System.out.println("peek()方法showtime");
//peek()是獲取但不刪除
System.out.println(concurrentLinkedQueue.peek());
System.out.println(concurrentLinkedQueue.peek());
System.out.println(concurrentLinkedQueue.peek());
System.out.println("pool()方法showtime");
//一次性只能獲取一個值,poll()是獲取一個刪除一個
System.out.println(concurrentLinkedQueue.poll());
System.out.println(concurrentLinkedQueue.poll());
System.out.println(concurrentLinkedQueue.poll());
System.out.println(concurrentLinkedQueue.size());
}
}
peek()方法與poll()方法運行對比:
1.4BlockingQueue
阻塞隊列(BlockingQueue)是一個支持兩個附加操作的隊列。這兩個附加的操作是:
(1)在隊列爲空時,獲取元素的線程會等待隊列變爲非空。
(2)當隊列滿時,存儲元素的線程會等待隊列可用。
阻塞隊列常用於生產者和消費者的場景,生產者是往隊列裏添加元素的線程,消費者是從隊列裏拿元素的線程。阻塞隊列就是生產者存放元素的容器,而消費者也只從容器裏拿元素。
阻塞隊列的簡單實驗:
/**
*
* @author johson
* 有界隊列,阻塞隊列
*/
public class test02 {
//阻塞式隊列:存隊列的時候,如果滿了。就會等待;取隊列的時候,如果沒有值或者取不到值也會等待
public static void main(String[] args) throws InterruptedException{
//新建一個大小爲3的阻塞隊列
BlockingQueue<String> blockingDeque = new ArrayBlockingQueue<String>(3);
//添加非阻塞隊列
blockingDeque.offer("張三");
//添加阻塞式隊列,如果隊列已經滿了,就會等待3秒,如果還是滿的就會結束插入操作
blockingDeque.offer("李四",3,TimeUnit.SECONDS);
System.out.println(blockingDeque.poll());
System.out.println(blockingDeque.poll(3,TimeUnit.SECONDS));
//獲取阻塞式隊列,過了三秒沒有獲取到就不再等待
System.out.println(blockingDeque.poll(3,TimeUnit.SECONDS));
}
}
利用BlockingQueue實現生產者與消費者實驗:
/**
* 生產者線程
* @author johson
*
*/
class ProduceThread implements Runnable{
private BlockingQueue<String> blockingQueue;
private volatile boolean flag = true;
//計數器,原子類
AtomicInteger atomicInteger = new AtomicInteger();
public ProduceThread(BlockingQueue<String> blockingQueue) {
this.blockingQueue = blockingQueue;
}
@Override
public void run() {
System.out.println("生產者線程啓動");
try {
while(flag){
//基於線程安全的一個自增方法
String data = atomicInteger.incrementAndGet()+"";
boolean offer = blockingQueue.offer(data,2,TimeUnit.SECONDS);
if(offer){
System.out.println("生產者入列成功,data=" + data);
}
else {
System.out.println("生產者入列失敗,data=" + data);
}
Thread.sleep(1000);
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//停止方法
public void stop(){
this.flag =false;
}
}
/**
* 消費者線程
* @author johson
*
*/
class ConsumerThread implements Runnable{
private BlockingQueue<String> blockingQueue;
private volatile boolean flag = true;
public ConsumerThread(BlockingQueue<String> blockingQueue) {
this.blockingQueue = blockingQueue;
}
@Override
public void run() {
try {
while (flag) {
String data = blockingQueue.poll(2, TimeUnit.SECONDS);
if(data == null){
System.out.println("消費者超過2秒時間沒有獲取到信息");
flag = true;
return ;
}
System.out.println("消費者獲取到data="+data);
}
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}finally {
System.out.println("消費者已經停止");
}
}
}
public class test03 {
public static void main(String[] args) {
BlockingQueue<String> blockingQueue = new LinkedBlockingQueue<String>(10);
ProduceThread produceThread = new ProduceThread(blockingQueue);
ConsumerThread consumerThread = new ConsumerThread(blockingQueue);
Thread t1 = new Thread(produceThread);
Thread t2 = new Thread(consumerThread);
t1.start();
t2.start();
try {
Thread.sleep(10000);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
produceThread.stop();
}
}
實驗結果:
2.線程池
2.1線程池的作用
線程池是爲了突然大量爆發的線程而設計的,通過有限的幾個固定線程爲大量的操所服務,減少了線程創建和銷燬的時間,從而提高效率。
2.2線程池能夠帶來3個好處
第一:降低資源消耗。通過重複利用已創建的線程降低線程創建和銷燬造成的消耗。
第二:提高響應速度。當任務到達時,任務可以不需要等到線程創建就能立即執行。複用原來的線程(但是run()方法體不一樣了)
第三:提高線程的可管理性。線程是稀缺資源,如果無限制地創建,不僅會消耗系統資源,還會降低系統的穩定性,使用線程池可以進行統一分配、調優和監控。但是,要做到合理利用。
2.3線程池四種創建方式
Java通過Executors(jdk1.5併發包)提供四種線程池,分別爲:
(1)newCachedThreadPool創建一個可緩存線程池,如果線程池長度超過處理需要,可靈活回收空閒線程,若無可回收,則新建線程。
/**
* newCachedThreadPool創建一個可緩存線程池
* @author johson
*
*/
public class test04 {
public static void main(String[] args) {
//創建了一個可緩存的線程池重複利用
ExecutorService newCachedThreadPool = Executors.newCachedThreadPool();
for(int i = 0;i < 20;i++){
final int t = i;
newCachedThreadPool.execute(new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName()+" "+t);
}
});
}
}
}
(2)newFixedThreadPool 創建一個定長線程池,可控制線程最大併發數,超出的線程會在隊列中等待。
/**
* newFixedThreadPool 創建一個定長線程池
* @author johson
*
*/
public class test05 {
public static void main(String[] args) {
ExecutorService newCachedThreadPool = Executors.newFixedThreadPool(3);
for(int i = 0;i < 20;i++){
final int t = i;
newCachedThreadPool.execute(new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName()+" "+t);
}
});
}
}
}
(3)newScheduledThreadPool 創建一個定長線程池,支持定時及週期性任務執行。
/**
* newScheduledThreadPool 創建一個定長線程池,支持定時及週期性任務執行。
* @author johson
*
*/
public class test06 {
public static void main(String[] args) {
ScheduledExecutorService newCachedThreadPool = Executors.newScheduledThreadPool (10);
for(int i = 0;i < 20;i++){
final int t = i;
newCachedThreadPool.schedule(new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName()+" "+t);
}
},3,TimeUnit.SECONDS);//3秒後執行調度
/* newCachedThreadPool.execute(new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName()+" "+t);
}
});*/
}
}
}
(4)newSingleThreadExecutor 創建一個單線程化的線程池,它只會用唯一的工作線程來執行任務,保證所有任務按照指定順序(FIFO, LIFO, 優先級)執行。
/**
* newSingleThreadExecutor 創建一個單線程化的線程池
* @author johson
*
*/
public class test07 {
public static void main(String[] args) {
ExecutorService newCachedThreadPool = Executors.newSingleThreadExecutor();
for(int i = 0;i < 10;i++){
final int t = i;
newCachedThreadPool.execute(new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName()+" "+t);
}
});
}
}
}
2.4線程池原理分析
常用的四個線程池都是通過ThreadPoolExecutor()封裝的
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue) {
this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue,
Executors.defaultThreadFactory(), defaultHandler);
}
corePoolSize:核心線程數
maximumPoolSize:最大線程數
核心線程數與最大線程數的區別:核心線程數是指實際用的線程數,最大線程數是指最多能創建的線程數
最多支持的線程數=最大線程數+隊列長度
2.5自定義線程池
/**
* 自定義線程池
* @author johson
*
*/
class TaskThread implements Runnable{
private String threadName;
public TaskThread(String threadName) {
this.threadName = threadName;
}
@Override
public void run() {
System.out.println(Thread.currentThread().getName()+" "+threadName);
}
}
public class test08 {
public static void main(String[] args) {
//核心線程數爲1(最多運行的線程數),最大線程數爲2(最多隻能創建幾個線程),線程空閒超時時間,分鐘還是秒,隊列爲3個
ThreadPoolExecutor executor = new ThreadPoolExecutor(
1, 2, 60L, TimeUnit.SECONDS, new LinkedBlockingQueue<Runnable>(3));
//任務1創建線程,正在執行
executor.execute(new TaskThread("任務1"));
//任務2存放在隊列緩存中,因爲只有一個核心線程
executor.execute(new TaskThread("任務2"));
//任務3存放在隊列緩存中,因爲只有一個核心線程
executor.execute(new TaskThread("任務3"));
executor.execute(new TaskThread("任務4"));
//再創建一個線程,因爲最大能創建的線程數爲2
executor.execute(new TaskThread("任務5"));
//任務6會報錯,拒絕策略爲線程數>最大線程數+隊列長度
//executor.execute(new TaskThread("任務6"));
}
}
3.合理配置線程池
3.1 IO密集
表示該任務需要大量的IO,即大量的阻塞。
3.2 CPU密集
表示該任務需要大量的運算,而沒有阻塞,CPU一直全速運行。
CPU密集任務只有在真正的多核CPU上纔可能得到加速(通過多線程),而在單核CPU上,無論你開幾個模擬的多線程,該任務都不可能得到加速,因爲CPU總的運算能力就那些。
CPU密集型時,任務可以少配置線程數,大概和機器的cpu核數相當,這樣可以使得每個線程都在執行任務
IO密集型時,大部分線程都阻塞,故需要多配置線程數,2*cpu核數