Java中該如何優(yōu)雅的使用線程池詳解
為什么要用線程池?
線程是不是越多越好?
- 線程在java中是一個對象,更是操作系統(tǒng)的資源,線程創(chuàng)建、銷毀需要時間。如果創(chuàng)建時間+小會時間>執(zhí)行任務時間就很不合算。
- java對象占用堆內存,操作系統(tǒng)線程占用系統(tǒng)內存,根據(jù)jvm規(guī)范,一個線程默認最大棧大小1M,這個棧空間是需要從系統(tǒng)內存中分配的。線程過多,會消耗很多的內存。
- 操作系統(tǒng)需要頻繁切換線程上下文(每個線都想被運行),影響性能。
線程池的推出,就是為了方便邊的控制線程數(shù)量。
線程池
線程池基本概念
線程池包括以下四個基本組成部分:
- 線程池管理器:用于創(chuàng)建并管理線程池,包括創(chuàng)建線程池,銷毀線程池,添加新任務;
- 工作線程:線程池中線程,在沒有任務時處于等待狀態(tài),可以循環(huán)的執(zhí)行任務;
- 任務接口:每個任務必須實現(xiàn)的借口,以供工作線程調度任務的執(zhí)行,它主要規(guī)定了任務的入口,任務執(zhí)行完后的收尾工作,任務的執(zhí)行狀態(tài)等;
- 任務隊列:用于存放沒有處理的任務。提供一種緩沖機制。

線程池接口定義和實現(xiàn)類

可以認為ScheduledThreadPoolExector是最豐富的實現(xiàn)類。
ExecutorService
public interface ExecutorService extends Executor {
/**
* 優(yōu)雅關閉線程池,之前提交的任務將被執(zhí)行,但是不會接受新的任務。
*/
void shutdown();
/**
* 嘗試停止所有正在執(zhí)行的任務,停止等待任務的處理,并返回等待執(zhí)行任務的列表。
*/
List<Runnable> shutdownNow();
/**
* 如果此線程池已關閉,則返回true.
*/
boolean isShutdown();
/**
* 如果關閉后的所有任務都已完成,則返回true
*/
boolean isTerminated();
/**
* 監(jiān)測ExecutorService是否已經關閉,直到所有任務完成執(zhí)行,或超時發(fā)生,或當前線程被中斷。
*/
boolean awaitTermination(long timeout, TimeUnit unit) throws InterruptedException;
/**
* 提交一個用于執(zhí)行的Callable返回任務,并返回一個Future,用于獲取Callable執(zhí)行結果。
*/
<T> Future<T> submit(Callable<T> task);
/**
* 提交可運行任務以執(zhí)行,并返回Future,執(zhí)行結果為傳入的result
*/
<T> Future<T> submit(Runnable task, T result);
/**
* 提交可運行任務以執(zhí)行,并返回Future對象,執(zhí)行結果為null
*/
Future<?> submit(Runnable task);
/**
* 執(zhí)行給定的任務集合,執(zhí)行完畢后,則返回結果。
*/
<T> List<Future<T>> invokeAll(Collection<? extends Callable<T>> tasks) throws InterruptedException;
/**
* 執(zhí)行給定的任務集合,執(zhí)行完畢或者超時后,則返回結果,其他任務終止。
*/
<T> List<Future<T>> invokeAll(Collection<? extends Callable<T>> tasks, long timeout, TimeUnit unit) throws InterruptedException;
/**
* 執(zhí)行給定的任務,任意一個執(zhí)行成功則返回結果,其他任務終止。
*/
<T> T invokeAny(Collection<? extends Callable<T>> tasks) throws InterruptedException, ExecutionException;
/**
* 執(zhí)行給定的任務,任意一個執(zhí)行成功或者超時后,則返回結果,其他任務終止
*/
<T> T invokeAny(Collection<? extends Callable<T>> tasks,long timeout, TimeUnit unit)throws InterruptedException, ExecutionException, TimeoutException;
}
ScheduledExecutorService
public interface ScheduledExecutorService extends ExecutorService {
/**
* 創(chuàng)建并執(zhí)行一個一次性任務,過了延遲時間就會被執(zhí)行
*/
public ScheduledFuture<?> schedule(Runnable command, long delay, TimeUnit unit);
/**
* 創(chuàng)建并執(zhí)行一個一次性任務,過了延遲時間就會被執(zhí)行
*/
public <V> ScheduledFuture<V> schedule(Callable<V> callable, long delay, TimeUnit unit);
/**
* 創(chuàng)建并執(zhí)行一個周期性任務,過了給定的初始化延遲時間,會第一次被執(zhí)行。執(zhí)行過程中發(fā)生了異常,那么任務停止
* 一次任務執(zhí)行時長超過了周期時間,下一次任務會等到該次任務執(zhí)行結束后,立刻執(zhí)行,這也是它和scheduleWithTixedDelay的重要區(qū)別
*/
public ScheduledFuture<?> scheduleAtFixedRate(Runnable command,
long initialDelay,
long period,
TimeUnit unit);
/**
* 創(chuàng)建并執(zhí)行一個周期性任務,過了給定的初始化延遲時間,會第一次被執(zhí)行。執(zhí)行過程中發(fā)生了異常,那么任務停止
* 一次任務執(zhí)行時長超過了周期時間,下一次任務會在該次任務執(zhí)行結束的時間基礎上,計算執(zhí)行延時。
* 對于超時周期的長時間處理任務的不同處理方式,這是它和scheduleAtFixedRate的重要區(qū)別
*/
public ScheduledFuture<?> scheduleWithFixedDelay(Runnable command,
long initialDelay,
long delay,
TimeUnit unit);
}
線程池工具類
在使用過程中,可以自己實例化線程池,也可以用Executors創(chuàng)建線程池的工廠累,常用方法如下:
newFixedThreadPool(int nThreads)
創(chuàng)建一個固定大小、任務隊列容量誤解的線程池。核心線程數(shù)=最大線程數(shù)。
newCachedThreadPool()
創(chuàng)建的是一個大小無界的緩沖線程池。它的任務隊列是一個同步隊列。任務加入到池中,如果池中有空閑線程,則用空閑線程執(zhí)行,如無則創(chuàng)建新線程執(zhí)行。池中的線程空閑時間超過60秒,將被銷毀釋放。線程數(shù)隨任務的多少變化。適用于執(zhí)行耗時較小的異步任務。池的核心線程數(shù)=0,最大線程=Integer.MAX_VALUE
newSingleThreadExecutor()
只有一個線程來執(zhí)行無界任務隊列的單一線程池。該線程池確保任務加入的順序一個一個一次執(zhí)行。當唯一的線程因任務異常中止時,將創(chuàng)建一個新的線程來繼續(xù)執(zhí)行后續(xù)的任務。與newFixedThreadPool(1)的區(qū)別在于,單一線程池的池大小在newSingleThreadExecutor方法中硬編碼,不能再改變的。
newScheduledThreadPool(int corePoolSize)
能定時執(zhí)行任務的線程池。該池的核心線程數(shù)由參數(shù)指定,最大線程數(shù)=Integer.MAX_VALUE
任務線程池執(zhí)行過程

如何確認合適的線程數(shù)量?
- 如果是CPU密集型應用,則線程池大小設置為N+1 (N為CPU總核數(shù))
- 如果是IO密集型應用,則線程池大小設置為2N+1 (N為CPU總核數(shù))
- 線程等待時間(IO)所占比例越高,需要越多線程。
- 線程CPU時間所占比例越高,需要越少線程。
一個系統(tǒng)最快的部分是CPU,所以決定一個系統(tǒng)吞吐量上限的是CPU。增強CPU處理能力,可以提高系統(tǒng)吞吐量上限。但根據(jù)短板效應,真實的系統(tǒng)吞吐量并不能單純根據(jù)CPU來計算。那要提高系統(tǒng)吞吐量,就需要從“系統(tǒng)短板”(比如網絡延遲、IO)著手:
- 盡量提高短板操作的并行化比率,比如多線程下載技術;
- 增強短板能力,比如用NIO替代IO;
線程池的使用分析
public class ExecutorsUse {
/**
* 測試: 提交15 個執(zhí)行時間需要3秒的任務,看線程池的狀況
*
* @param threadPoolExecutor 傳入不同的線程池,看不同的結果
* @throws Exception
*/
public void testCommon(ThreadPoolExecutor threadPoolExecutor) throws Exception {
// 測試: 提交15個執(zhí)行時間需要3秒的任務,看超過大小的2個,對應的處理情況
for (int i = 0; i < 15; i++) {
int n = i;
threadPoolExecutor.submit(() -> {
try {
System.out.println("開始執(zhí)行:" + n);
Thread.sleep(3000L);
System.err.println("執(zhí)行結束:" + n);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
);
System.out.println("任務提交成功 :" + i);
}
// 查看線程數(shù)量,查看隊列等待數(shù)量
Thread.sleep(500L);
System.out.println("當前線程池線程數(shù)量為:" + threadPoolExecutor.getPoolSize());
System.out.println("當前線程池等待的數(shù)量為:" + threadPoolExecutor.getQueue().size());
// 等待15秒,查看線程數(shù)量和隊列數(shù)量(理論上,會被超出核心線程數(shù)量的線程自動銷毀)
Thread.sleep(15000L);
System.out.println("當前線程池線程數(shù)量為:" + threadPoolExecutor.getPoolSize());
System.out.println("當前線程池等待的數(shù)量為:" + threadPoolExecutor.getQueue().size());
}
/**
* 1、線程池信息: 核心線程數(shù)量5,最大數(shù)量10,無界隊列,超出核心線程數(shù)量的線程存活時間:5秒, 指定拒絕策略
*
* @throws Exception
*/
private void threadPoolExecutorTest1() throws Exception {
ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(5, 10, 5, TimeUnit.SECONDS,
new LinkedBlockingQueue<Runnable>());
testCommon(threadPoolExecutor);
// 預計結果:線程池線程數(shù)量為:5,超出數(shù)量的任務,其他的進入隊列中等待被執(zhí)行
}
/**
* 2、 線程池信息: 核心線程數(shù)量5,最大數(shù)量10,隊列大小3,超出核心線程數(shù)量的線程存活時間:5秒, 指定拒絕策略的
*
* @throws Exception
*/
private void threadPoolExecutorTest2() throws Exception {
// 創(chuàng)建一個 核心線程數(shù)量為5,最大數(shù)量為10,等待隊列最大是3 的線程池,也就是最大容納13個任務。
// 默認的策略是拋出RejectedExecutionException異常,java.util.concurrent.ThreadPoolExecutor.AbortPolicy
ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(5, 10, 5, TimeUnit.SECONDS,
new LinkedBlockingQueue<Runnable>(3), new RejectedExecutionHandler() {
@Override
public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {
System.err.println("有任務被拒絕執(zhí)行了");
}
});
testCommon(threadPoolExecutor);
// 預計結果:
// 1、 5個任務直接分配線程開始執(zhí)行
// 2、 3個任務進入等待隊列
// 3、 隊列不夠用,臨時加開5個線程來執(zhí)行任務(5秒沒活干就銷毀)
// 4、 隊列和線程池都滿了,剩下2個任務,沒資源了,被拒絕執(zhí)行。
// 5、 任務執(zhí)行,5秒后,如果無任務可執(zhí)行,銷毀臨時創(chuàng)建的5個線程
}
/**
* 3、 線程池信息: 核心線程數(shù)量5,最大數(shù)量5,無界隊列,超出核心線程數(shù)量的線程存活時間:5秒
*
* @throws Exception
*/
private void threadPoolExecutorTest3() throws Exception {
// 和Executors.newFixedThreadPool(int nThreads)一樣的
ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(5, 5, 0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>());
testCommon(threadPoolExecutor);
// 預計結:線程池線程數(shù)量為:5,超出數(shù)量的任務,其他的進入隊列中等待被執(zhí)行
}
/**
* 4、 線程池信息:
* 核心線程數(shù)量0,最大數(shù)量Integer.MAX_VALUE,SynchronousQueue隊列,超出核心線程數(shù)量的線程存活時間:60秒
*
* @throws Exception
*/
private void threadPoolExecutorTest4() throws Exception {
// SynchronousQueue,實際上它不是一個真正的隊列,因為它不會為隊列中元素維護存儲空間。與其他隊列不同的是,它維護一組線程,這些線程在等待著把元素加入或移出隊列。
// 在使用SynchronousQueue作為工作隊列的前提下,客戶端代碼向線程池提交任務時,
// 而線程池中又沒有空閑的線程能夠從SynchronousQueue隊列實例中取一個任務,
// 那么相應的offer方法調用就會失?。慈蝿諞]有被存入工作隊列)。
// 此時,ThreadPoolExecutor會新建一個新的工作者線程用于對這個入隊列失敗的任務進行處理(假設此時線程池的大小還未達到其最大線程池大小maximumPoolSize)。
// 和Executors.newCachedThreadPool()一樣的
ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(0, Integer.MAX_VALUE, 60L, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>());
testCommon(threadPoolExecutor);
// 預計結果:
// 1、 線程池線程數(shù)量為:15,超出數(shù)量的任務,其他的進入隊列中等待被執(zhí)行
// 2、 所有任務執(zhí)行結束,60秒后,如果無任務可執(zhí)行,所有線程全部被銷毀,池的大小恢復為0
Thread.sleep(60000L);
System.out.println("60秒后,再看線程池中的數(shù)量:" + threadPoolExecutor.getPoolSize());
}
/**
* 5、 定時執(zhí)行線程池信息:3秒后執(zhí)行,一次性任務,到點就執(zhí)行 <br/>
* 核心線程數(shù)量5,最大數(shù)量Integer.MAX_VALUE,DelayedWorkQueue延時隊列,超出核心線程數(shù)量的線程存活時間:0秒
*
* @throws Exception
*/
private void threadPoolExecutorTest5() throws Exception {
// 和Executors.newScheduledThreadPool()一樣的
ScheduledThreadPoolExecutor threadPoolExecutor = new ScheduledThreadPoolExecutor(5);
threadPoolExecutor.schedule(new Runnable() {
@Override
public void run() {
System.out.println("任務被執(zhí)行,現(xiàn)在時間:" + System.currentTimeMillis());
}
}, 3000, TimeUnit.MILLISECONDS);
System.out.println(
"定時任務,提交成功,時間是:" + System.currentTimeMillis() + ", 當前線程池中線程數(shù)量:" + threadPoolExecutor.getPoolSize());
// 預計結果:任務在3秒后被執(zhí)行一次
}
/**
* 6、 定時執(zhí)行線程池信息:線程固定數(shù)量5 ,<br/>
* 核心線程數(shù)量5,最大數(shù)量Integer.MAX_VALUE,DelayedWorkQueue延時隊列,超出核心線程數(shù)量的線程存活時間:0秒
*
* @throws Exception
*/
private void threadPoolExecutorTest6() throws Exception {
ScheduledThreadPoolExecutor threadPoolExecutor = new ScheduledThreadPoolExecutor(5);
// 周期性執(zhí)行某一個任務,線程池提供了兩種調度方式,這里單獨演示一下。測試場景一樣。
// 測試場景:提交的任務需要3秒才能執(zhí)行完畢??磧煞N不同調度方式的區(qū)別
// 效果1: 提交后,2秒后開始第一次執(zhí)行,之后每間隔1秒,固定執(zhí)行一次(如果發(fā)現(xiàn)上次執(zhí)行還未完畢,則等待完畢,完畢后立刻執(zhí)行)。
// 也就是說這個代碼中是,3秒鐘執(zhí)行一次(計算方式:每次執(zhí)行三秒,間隔時間1秒,執(zhí)行結束后馬上開始下一次執(zhí)行,無需等待)
threadPoolExecutor.scheduleAtFixedRate(new Runnable() {
@Override
public void run() {
try {
Thread.sleep(3000L);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("任務-1 被執(zhí)行,現(xiàn)在時間:" + System.currentTimeMillis());
}
}, 2000, 1000, TimeUnit.MILLISECONDS);
// 效果2:提交后,2秒后開始第一次執(zhí)行,之后每間隔1秒,固定執(zhí)行一次(如果發(fā)現(xiàn)上次執(zhí)行還未完畢,則等待完畢,等上一次執(zhí)行完畢后再開始計時,等待1秒)。
// 也就是說這個代碼鐘的效果看到的是:4秒執(zhí)行一次。 (計算方式:每次執(zhí)行3秒,間隔時間1秒,執(zhí)行完以后再等待1秒,所以是 3+1)
threadPoolExecutor.scheduleWithFixedDelay(new Runnable() {
@Override
public void run() {
try {
Thread.sleep(3000L);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("任務-2 被執(zhí)行,現(xiàn)在時間:" + System.currentTimeMillis());
}
}, 2000, 1000, TimeUnit.MILLISECONDS);
}
/**
* 7、 終止線程:線程池信息: 核心線程數(shù)量5,最大數(shù)量10,隊列大小3,超出核心線程數(shù)量的線程存活時間:5秒, 指定拒絕策略的
*
* @throws Exception
*/
private void threadPoolExecutorTest7() throws Exception {
// 創(chuàng)建一個 核心線程數(shù)量為5,最大數(shù)量為10,等待隊列最大是3 的線程池,也就是最大容納13個任務。
// 默認的策略是拋出RejectedExecutionException異常,java.util.concurrent.ThreadPoolExecutor.AbortPolicy
ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(5, 10, 5, TimeUnit.SECONDS,
new LinkedBlockingQueue<Runnable>(3), new RejectedExecutionHandler() {
@Override
public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {
System.err.println("有任務被拒絕執(zhí)行了");
}
});
// 測試: 提交15個執(zhí)行時間需要3秒的任務,看超過大小的2個,對應的處理情況
for (int i = 0; i < 15; i++) {
int n = i;
threadPoolExecutor.submit(new Runnable() {
@Override
public void run() {
try {
System.out.println("開始執(zhí)行:" + n);
Thread.sleep(3000L);
System.err.println("執(zhí)行結束:" + n);
} catch (InterruptedException e) {
System.out.println("異常:" + e.getMessage());
}
}
});
System.out.println("任務提交成功 :" + i);
}
// 1秒后終止線程池
Thread.sleep(1000L);
threadPoolExecutor.shutdown();
// 再次提交提示失敗
threadPoolExecutor.submit(new Runnable() {
@Override
public void run() {
System.out.println("追加一個任務");
}
});
// 結果分析
// 1、 10個任務被執(zhí)行,3個任務進入隊列等待,2個任務被拒絕執(zhí)行
// 2、調用shutdown后,不接收新的任務,等待13任務執(zhí)行結束
// 3、 追加的任務在線程池關閉后,無法再提交,會被拒絕執(zhí)行
}
/**
* 8、 立刻終止線程:線程池信息: 核心線程數(shù)量5,最大數(shù)量10,隊列大小3,超出核心線程數(shù)量的線程存活時間:5秒, 指定拒絕策略的
*
* @throws Exception
*/
private void threadPoolExecutorTest8() throws Exception {
// 創(chuàng)建一個 核心線程數(shù)量為5,最大數(shù)量為10,等待隊列最大是3 的線程池,也就是最大容納13個任務。
// 默認的策略是拋出RejectedExecutionException異常,java.util.concurrent.ThreadPoolExecutor.AbortPolicy
ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(5, 10, 5, TimeUnit.SECONDS,
new LinkedBlockingQueue<Runnable>(3), new RejectedExecutionHandler() {
@Override
public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {
System.err.println("有任務被拒絕執(zhí)行了");
}
});
// 測試: 提交15個執(zhí)行時間需要3秒的任務,看超過大小的2個,對應的處理情況
for (int i = 0; i < 15; i++) {
int n = i;
threadPoolExecutor.submit(new Runnable() {
@Override
public void run() {
try {
System.out.println("開始執(zhí)行:" + n);
Thread.sleep(3000L);
System.err.println("執(zhí)行結束:" + n);
} catch (InterruptedException e) {
System.out.println("異常:" + e.getMessage());
}
}
});
System.out.println("任務提交成功 :" + i);
}
// 1秒后終止線程池
Thread.sleep(1000L);
List<Runnable> shutdownNow = threadPoolExecutor.shutdownNow();
// 再次提交提示失敗
threadPoolExecutor.submit(new Runnable() {
@Override
public void run() {
System.out.println("追加一個任務");
}
});
System.out.println("未結束的任務有:" + shutdownNow.size());
// 結果分析
// 1、 10個任務被執(zhí)行,3個任務進入隊列等待,2個任務被拒絕執(zhí)行
// 2、調用shutdownnow后,隊列中的3個線程不再執(zhí)行,10個線程被終止
// 3、 追加的任務在線程池關閉后,無法再提交,會被拒絕執(zhí)行
}
public static void main(String[] args) throws Exception {
// new ExecutorsUse().threadPoolExecutorTest1();
// new ExecutorsUse().threadPoolExecutorTest2();
// new ExecutorsUse().threadPoolExecutorTest3();
new ExecutorsUse().threadPoolExecutorTest4();
// new ExecutorsUse().threadPoolExecutorTest5();
// new ExecutorsUse().threadPoolExecutorTest6();
// new ExecutorsUse().threadPoolExecutorTest7();
// new ExecutorsUse().threadPoolExecutorTest8();
}
}
合理配置線程池大小
如果想合理設置線程池的線程數(shù)量需要考慮兩個問題:
1、需要分析線程池執(zhí)行的任務的特性: CPU 密集型還是 IO 密集型。
2、每個任務執(zhí)行的平均時長大概是多少,這個任務的執(zhí)行時長可能還跟任務處理邏輯是否涉及到網絡傳輸以及底層系統(tǒng)資源依賴有關系。
如果是 CPU 密集型,那線程池的最大線程數(shù)可以配置為 cpu 核心數(shù)+1;如果是 IO 密集型,線程池設定最佳線程數(shù)目 = ((線程池設定的線程等待時間+線程 CPU 時間)/線程 CPU 時間 )* CPU 數(shù)目。
線程池的關閉
ThreadPoolExecutor 提供了兩個方法 ,用于線程池的關閉 ,分 別 是 shutdown() 和shutdownNow(),其中:shutdown():不會立即終止線程池,而是要等所有任務緩存隊列中的任務都執(zhí)行完后才終止,但再也不會接受新的任務 shutdownNow():立即終止線程池,并嘗試打斷正在執(zhí)行的任務,并且清空任務緩存隊列,返回尚未執(zhí)行的任務。
總結
到此這篇關于Java中該如何優(yōu)雅的使用線程池的文章就介紹到這了,更多相關Java優(yōu)雅使用線程池內容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關文章希望大家以后多多支持腳本之家!
相關文章
Java使用Flyway實現(xiàn)數(shù)據(jù)庫版本控制的技術指南
在現(xiàn)代應用開發(fā)中,數(shù)據(jù)庫結構經常隨著業(yè)務需求不斷演變,使用手動SQL腳本管理數(shù)據(jù)庫版本,不僅容易出現(xiàn)錯誤,還難以跟蹤和回滾,Flyway是一個強大的數(shù)據(jù)庫遷移工具,能夠幫助開發(fā)者高效管理和自動化數(shù)據(jù)庫的版本控制,本文將介紹Flyway的基本功能及其在SpringBoot項目中的實踐2025-02-02
SpringBoot實現(xiàn)JWT token自動續(xù)期的示例代碼
本文主要介紹了SpringBoot實現(xiàn)JWT token自動續(xù)期的示例代碼,文中通過示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友們下面隨著小編來一起學習學習吧2023-01-01
Spring?Security權限管理實現(xiàn)接口動態(tài)權限控制
這篇文章主要為大家介紹了Spring?Security權限管理實現(xiàn)接口動態(tài)權限控制,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步,早日升職加薪2022-06-06

