最近幾個月終於有大把時間總結這兩年來所學
2019.5.29
前言
java中的多線程包括下面兩點
- 多線程怎麼用
- 線程安全
區分幾個概念
區別一下
進程、線程、CPU線程、操作系統線程
進程:操作系統中一塊獨立的區域,和操作系統獨立開,數據不共享,相互隔離。
線程:工作在進程中的工作單元,可以共享資源。
CPU線程:CPU在硬件級別同時能做的事情(注意是硬件層面,而非軟件上做的時間切片)。有做過單片機的裸機代碼的同學,對這個概念應該會非常熟悉。
操作系統線程:模仿的CPU線程,把CPU的線程做進一步拆分,分爲一個個的時間切片,輪詢的做各種不同的工作
區別線程和進程,本質上來說,線程(thread)和進程(Process)不是一個概念,進程本身是一個運行的程序,而線程是程序中的工作單元。Thread 的英文本意是棉毛線的意思,而Process是過程,工序的意思。對比一下兩者的英文意思,可以很好地理解兩個概念。
線程池的大小和CPU的核心數。一般線程池的大小和CPU的核心數要成正相關
java中實現多線程
簡要列舉一下java中常用的多線程的實現。
- Thread
創建一個thread,重寫run方法。然後thread.start();
static void thread() {
Thread thread = new Thread() {
@Override
public void run() {
System.out.println("use thread");
}
};
thread.start();
}
- runnable
static void runnable() {
Runnable runnable = new Runnable() {
@Override
public void run() {
System.out.println("use runnable");
}
};
Thread thread = new Thread(runnable);
thread.start();
}
Runnable 和 Thread兩個方法關聯是很緊密的,我最近看了看源碼,發現,Thread裏面的run方法是長這個樣子的
public void run() {
if (this.target != null) {
this.target.run();
}
}
然後我們看到裏面有一個target,這個target就是 Thread thread = new Thread(runnable);
裏面的runnable
。
所以對於Thread和Runnable兩個方法來說,我們如果重寫了Runnable,就會運行runnable的run方法,如果寫了Thread的run方法,就會運行Thread的run方法。如果兩個都寫了,兩個都會運行。
- ThreadFactory
static void threadFactory() {
ThreadFactory factory = new ThreadFactory() {
AtomicInteger count = new AtomicInteger(0);
// int count = 0;
@Override
public Thread newThread(Runnable r) {
// count++;
return new Thread(r, "Thread-" + count.incrementAndGet());
}
};
Runnable runnable = new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + " started!");
}
};
Thread thread = factory.newThread(runnable);
thread.start();
Thread thread1 = factory.newThread(runnable);
thread1.start();
}
這種方法用的比較少,簡單看看就可以。
- Executor 線程池
這是非常非常非常常用的方法。有四種常用的方法
- 基本用法
static void executor() {
Runnable runnable = new Runnable() {
@Override
public void run() {
System.out.println("Thread with Runnable started!");
}
};
Executor executor = Executors.newCachedThreadPool();
executor.execute(runnable);
}
- 基本構造方法
ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue)
變量含義分別爲:
核心線程數;
最大線程數;
keep alive 的時間;單位;
裝runnable的隊列
幾個變量的判斷優先級爲:corePoolSize > workQueue > maximumPoolSize;
解釋一下:
當前線程數 < corePoolSize,新任務來臨,無論是否有空的線程任務,都會優先創建核心線程;
corePoolSize < 當前線程數 < maximumPoolSize,只有當workQueue滿了,纔會創建新的線程去做任務,否則都將默認排隊執行;
- 下面就這個構造基本的構造方法來介紹一下我們常用的四中線程池。
a.newCachedThreadPool
:
new ThreadPoolExecutor(0, 2147483647, 60L, TimeUnit.SECONDS, new SynchronousQueue());
//核心線程數:0;最大線程數:無窮大;等待時間:60S
從名字也可以看出,這是可緩存線程池,如果線程池長度超過處理需要,可靈活回收空閒線程,如果沒有空線程等待回收,則新建線程。
b.newSingleThreadPool
:
new ThreadPoolExecutor(1, 1, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue());
//核心線程數:1;最大線程數1;永不回收
特點:線程一直存在,隨時可以拿來用。可以用來做取消功能。
c.newFixedThreadPool
new ThreadPoolExecutor(32, 32, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue());
//核心線程數:自定義;最大線程數:核心線程數;不回收
做集中的事情,比如下載文件,訴求就是,我需要馬上用很多線程,而且就用這麼一下,用完了我就不要了。
d.newSchedualThreadPool
:
new ThreadPoolExecutor(32, 2147483647, 0L, TimeUnit.NANOSECONDS, new ScheduledThreadPoolExecutor.DelayedWorkQueue());
可以設置定時,以及週期性執行任務。
- callable:一個有返回值的runnable
後臺任務在後臺執行,返回值返回給誰?和Future類配合使用,將返回的值給future的get方法。如下面的程序段所示
注意,Future.get是一個阻塞方法,會一直等到callable執行完成,纔會返回數據。
static void callable() {
Callable<String> callable = new Callable<String>() {
@Override
public String call() {
try {
Thread.sleep(1500);
} catch (InterruptedException e) {
e.printStackTrace();
}
return "Done";
}
};
ExecutorService executor = Executors.newCachedThreadPool();
Future<String> future = executor.submit(callable);
try {
String result = future.get();
System.out.println("result: " + result);
} catch (InterruptedException | ExecutionException e) {
e.printStackTrace();
}
}
synchronized
- 原子操作:CPU級別,只執行一次的代碼。不可被拆分
- synchronized的互斥
爲了保護方法或者代碼塊內的數據,重點是保護數據。我們關注的不是方法,而是資源,只有處理同樣數據方法,才需要做互斥操作
每一個synchronized都有對應一個monitor, 只有相同的monitor才具有互斥性。 非靜態方法的monitor是類的實例化對象。靜態方法的monitor是類。
public static synchronized void methodA(){
//do something
}
public synchronized void methodB(){
//do something
}
private final Object monitorC = new Object();
public void methodC(){
synchronized (monitorC){
//do something
}
}
看上面的一段代碼,methodA/methodB/methodC三個方法,都加了synchronized,但是他們的monitor分別是:className.class,this,monitorC,所以三個方法可以同時被調用。我們的鎖,是針對每個monitor進行鎖的,兩個方法在同一個monitor下,可以達到鎖的作用,如果不在一個monitor下,就達不到鎖的作用。同時,對於不是操作同樣數據的方法,是沒有必要加互斥鎖的
- 死鎖
- 兩個鎖一起使用的時候,會出現死鎖。比如下面這中情況
public class DeadLock {
private final Object monitor1 = new Object();
private final Object monitor2 = new Object();
private void setName(int name){
synchronized (monitor1){
name = 1;
//線程1運行到此處的時候,被線程2打斷,跑到setAge中
synchronized (monitor2){
name = 2;
}
}
}
private void setAge(int age){
synchronized (monitor2){
age = 1;
//線程是運行到此處,就會出現死鎖
synchronized (monitor1){
age = 2;
}
}
}
}
上面的代碼中,註釋裏面有詳細說明。monitor1和monitor2在相互等待對方運行完成,導致死鎖產生。
- 樂觀鎖、悲觀鎖
關於數據庫讀寫的問題。比如賬戶餘額,用於存入100塊,需要先讀出原來的金額,然後+100。但是該操作可能會同時進行,比如兩個人同時給你轉賬。
樂觀鎖:寫入數據前,先覈對一下數據有沒有變化,如果有變化,那麼就加一個鎖。
悲觀鎖:無論有沒有人寫,都先把把鎖加上。
volatile
- 相當於一個輕量級的synchronized。保證修飾的變量具有同步性,即被修飾的變量被賦值時,具有原子性。無法解決++的問題。
其他
- 解決++的非原子問題:使用Atomic
看看上面的threadFactory的示例代碼中。
- 讀寫鎖
上面有說道,同步方法本質是爲了保護方法中處理的數據。所以我們在讀寫方法中,使用讀寫鎖來解決synchronized過重的問題
public class ReadWriteLockDemo {
private ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
private ReentrantReadWriteLock.ReadLock readLock = lock.readLock();
private ReentrantReadWriteLock.WriteLock writeLock = lock.writeLock();
private int x = 0;
private void write() {
writeLock.lock();
try {
x++;
} finally {
writeLock.unlock();
}
}
private void read(int time) {
readLock.lock();
try {
for (int i = 0; i < time; i++) {
System.out.print(x + " ");
}
System.out.println();
} finally {
readLock.unlock();
}
}
}
- 以上爲線程的相關簡要整理。
再另外,以上都是自己平時所學整理,如果有錯誤,歡迎留言或者添加微信批評指出,一起學習,共同進步,愛生活,愛技術。