nullhtml
用生活中的場景來比喻的話呢,就是假設你住在一個小區,這個小區就是一個操做系統,你家就是一個進程,你家的柴米油鹽是不跟其餘戶人家共享的,爲何?由於大家互相之間不要緊。這個柴米油鹽就是資源。 線程就是大家這個家的人,大家互相之間同時運行,能夠同時幹本身的事情。java
線程建立的方式主要包括:面試
/**
* 使用 Thread 類來定義工做
*/
static void thread() {
Thread thread = new Thread() {
@Override
public void run() {
System.out.println("Thread started!");
}
};
thread.start();
複製代碼
實現Runnable接口建立線程編程
/**
* 使用 Runnable 類來定義工做
*/
static void runnable() {
Runnable runnable = new Runnable() {
@Override
public void run() {
System.out.println("Thread with Runnable started!");
}
};
Thread thread = new Thread(runnable);
thread.start();
}
複製代碼
Callable是有返回值的Runnable。小程序
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(); //get是一個阻塞方法,雖然你換了個線程,可是你取數據的時候仍是會卡住
System.out.println("result: " + result);
} catch (InterruptedException | ExecutionException e) {
e.printStackTrace();
}
}
複製代碼
feature.get()是一個阻塞方法,那麼有沒有辦法不卡住線程呢? 答案是有的,那就是循環去查:緩存
Future<String> future = executor.submit(callable);
try {
while(!future.isDone){
//檢查是否已經完成,若是否,那麼可讓主線程去作其餘操做,不會被阻塞
}
String result = future.get(); //get是一個阻塞方法,雖然你換了個線程,可是你取數據的時候仍是會卡住
System.out.println("result: " + result);
} catch (InterruptedException | ExecutionException e) {
e.printStackTrace();
}
複製代碼
JDK 1.5後引入的Executor框架的最大優勢是把任務的提交和執行解耦。經過Executors 的工具類能夠建立如下類型的線程池: 安全
ExecutorService imageProcessor = Executor.newFixedThreadPool(); //我須要你立刻給我不少個線程,而後一旦用完我就不要了
List<Image> images; //圖片集合
for(Image image : images){
//處理圖片
improcessor.excutor(iamgeRunnable,image);
}
//等圖片處理完成後終止線程
imageProcessor.shutdown();
複製代碼
(2)cacheThreadPool 緩存線程池 當提交任務速度高於線程池中任務處理速度時,緩存線程池會不斷的建立線程 適用於提交短時間的異步小程序,以及負載較輕的服務器bash
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);
executor.execute(runnable);
executor.execute(runnable);
}
複製代碼
Executor接口裏面有兩個重要的方法,一個是shutdown,一個是shutdownNow。 他們兩個的區別是:shutdown再也不容許扔新的runnable進來。shutdownNow不僅是新的不容許,就算是正在執行的任務也不容許再繼續執行。服務器
🙋♀️闢謠: 網上有一個說法是:建立的線程池大小,取決於CPU的核數。 好比,你CPU有8個核,就建立8個線程,每一個線程分配給你一個核,這樣想一想頗有道理。可是實際上是沒道理的!你有8個核,你就佔了全部的核了嗎?不是這樣的。不過,你的線程數跟你的CPU掛鉤是有道理,它可讓你的軟件在不一樣機器上表現相對一致。 因此,你的線程數跟你的CPU掛鉤有道理,可是線程數=CPU核數就沒道理了。你們記住了吧。多線程
線程同步主要包括如下內容:
在說明synchronized爲何能保證線程安全以前,咱們先簡單過一下JVM內存模型。
(1)把工做內存1中更新過的共享變量刷新到主內存中 (2)將主內存中最新的共享變量的值更新到工做內存2中
若是一個線程對共享變量的修改,可以被其餘線程看到,那麼就說此時是可見的。
原子性也就是不可再分,不能再分爲分步操做。 好比:
int a =1 ;//是原子操做
a+= 1;//不是原子操做
a+=1 實際分爲三步:
1. 取出a = 1
2. 計算a + 1
3. 將計算結果寫入內存
複製代碼
在Java中,代碼書寫的順序並不等於代碼執行的順序。有時候,編譯器或者處理器爲了能提升程序性能,會對代碼指令進行重排序。
重排序不會給單線程帶來內存可見性問題,可是在進行多線程編程時,重排序可能會形成內存可見性問題。
舉個例子:
int num1= 1; //第一行代碼
int num2 = 2; //第二行代碼
int sum = num1 + num2; //第三行代碼
//在進行重排序的時候,若是將sum = num1 + num2 先於前兩行代碼執行,此時計算結果就會出錯;
複製代碼
固然,重排序的內容不是本文重點,有興趣的讀者自行百度。
synchronized能夠保證在同一時刻只有一個線程執行被synchronized修飾的方法/代碼,即保證操做的原子性和可見性。
synchronized能夠被用在三個地方:
下面咱們經過代碼來進行實踐一下。
當沒有明確給synchronized指明鎖時,默認獲取到的是對象鎖。
public synchronized void Method1(){
System.out.println("我是對象鎖也是方法鎖");
try{
Thread.sleep(500);
} catch (InterruptedException e){
e.printStackTrace();
}
}
複製代碼
// 類鎖:鎖靜態方法
public static synchronized void Method1(){
System.out.println("我是類鎖");
try{
Thread.sleep(500);
} catch (InterruptedException e){
e.printStackTrace();
}
}
複製代碼
// 類鎖:鎖靜態代碼塊
public void Method2(){
synchronized (Test.class){
System.out.println("我是類鎖");
try{
Thread.sleep(500);
} catch (InterruptedException e){
e.printStackTrace();
}
}
}
// 對象鎖
public void Method(){
synchronized (this){
System.out.println("我是對象鎖");
try{
Thread.sleep(500);
} catch (InterruptedException e){
e.printStackTrace();
}
}
}
}
複製代碼
synchronized的工做流程是:
synchronized可以實現原子性和可見性,本質上依賴的是 底層操做系統的 互斥鎖機制。
你們平時在寫單例模式的時候,確定知道用雙重鎖的方式,那麼,爲何不用下面這種方式,這種方式存在什麼缺點?
static synchroinzed SingleMan newInstance(){
if(sInstance = null){
sInstance= new SingleMan();
}
}
複製代碼
這個寫法有什麼壞處呢? 壞處是,把synchronized加上方法上時,做用的是整個對象的資源,當其餘訪問這個對象中的其餘資源時,也須要等待。代價很是大。 舉個例子:
public synchronized void setX(int x){
this.x = x;
}
public synchronized int getY(){
return this.y;
}
//當調用setX方法時,若是此時有其餘線程想要調用getY方法,那麼須要進行等待,由於此時鎖已經被當前線程拿了。因此若是把synchroinzed加在方法上時,就算操做的不是相同的資源,也須要等待。代價比較大。
複製代碼
那麼,好的單例模式的寫法是什麼呢?答案是 不要使用對象鎖,使用局部鎖:
private static volatile SingleMan sInstance; //這裏爲何要用volatile呢?由於有些對象在還沒初始化完成的時候,對外就已經暴露不爲空,可是此時還不能用,若是此時有線程使用了這個對象,就會有問題。加入volatile就能夠同步狀態
static SingleMan newInstance(){
if(sInstance = null){ //可能有兩個線程同時到了這個地方,都以爲是空,而後可能會同時去嘗試拿monitor,而後另一個進入等待,當對象初始化後,等待的線程往下走,此時就已經不爲空。因此,須要雙重檢查
synchroinzed(SingleMan.class){
if(sInstance = null){
sInstance= new SingleMan();
}
}
}
}
複製代碼
volatile關鍵字只能用於修飾變量,沒法用於修飾方法。而且volatile只能保證可見性,但不能保證操做的原子性。在具體編程中體現爲:volatile只能保證基本類型以及通常對象的引用賦值是線程安全的。舉個例子:
volatile User user;
private void setUserName(String userName){
user.name = userName;//不安全的
}
private void setUser(User user){
this.user = user;//安全的,只能保證引用
}
複製代碼
爲何volatile只能保證可見性,不能保證原子性呢? 這跟它的工做原理有關。
線程寫volaitle變量的步驟爲:
線程讀volatile變量的步驟爲:
因爲在整個過程沒有涉及到鎖相關的操做,因此沒法保證原子性,可是因爲實時刷新了主內存中的變量值,所以任什麼時候刻,不一樣線程總能看到該變量的最新值,保證了可見性。
下面出個練習來練練手: 有下面👇這麼一句代碼:
private volatile int number =0;
複製代碼
問:當建立500個線程同時操做number ++ 時,是否能保證最終打印的值是500?
答案:不能;由於number++不是原子操做,而volatile沒法保證原子性。
那要如何改呢?
解法1:synchronized關鍵字
synchronized(this){
number++;
}
解法2:使用ReentrankLock
private ReentrankLock lock = new ReentrankLock();
lock.lock();
try{
number++;
}finally{
lock.unlock();
}
解法3: 將int改爲AtomicIntege
複製代碼
要在多線程中安全的使用volatile變量,必須同時知足:
在實際項目中,因爲不少狀況下都不滿意volatile的使用條件,因此volatile使用的場景並無synchronized廣。
在Java中,對64位(long、double)變量的讀寫可能不是原子操做,由於Java內存模型容許JVM將沒有被volatile修飾的64位數據類型的讀寫操做劃分爲兩次32位的讀寫操做來進行。 所以致使:有可能會出現讀取到」半個變量「的狀況; 解決方案是:加volatile關鍵字。
這裏有同窗可能會問啦,不是說volatile不保證原子性嗎?爲何對於64位類型的變量用volatile修飾? 緣由是:volatile自己不保證獲取和設置操做的原子性,僅僅保持修改的可見性。可是java的內存模型保證聲明爲volatile的long和double變量的get和set操做是原子的。
請你們先思考如下問題: 對於一個公共變量,若是:
答案是:一、二、3會出問題,4不會出問題。一、二、3出問題的緣由在於有一個線程對變量進行了修改,此時會致使數據發生改變,若是有另一個線程要進行讀取,會出現讀取的數據可能出錯。可是,當兩個線程同時進行讀操做的時候,是OK的,不會出現你讀出來是個1,我讀出來是個2的問題。
由於,當多個線程同時進行讀操做的時候,咱們就沒有必要進行同步,浪費資源。爲了減小這種資源浪費,讀寫鎖就出現了~
讀寫鎖維護了一對鎖,一個讀鎖和一個寫鎖,同一時刻,能夠有多個線程拿到讀鎖,可是隻有一個線程拿到寫鎖。 總結起來爲:讀讀不互斥,讀寫互斥,寫寫互斥。
讀寫鎖在Java中是ReentrantReadWriteLock
,使用方式是:
import java.util.concurrent.locks.ReentrantReadWriteLock;
public class ReadWriteLockDemo implements TestDemo {
ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
ReentrantReadWriteLock.ReadLock readLock = lock.readLock();
ReentrantReadWriteLock.WriteLock writeLock = lock.writeLock();
private int x = 0;
private void count() {
writeLock.lock();
try {
x++;
} finally {
writeLock.unlock();// 保證當讀的時候若是出現異常,會釋放鎖,synchronized爲何不用呢?由於synchronized內部已經幫咱們作了~
}
}
private void print(int time) {
readLock.lock();
try {
for (int i = 0; i < time; i++) {
System.out.print(x + " ");
}
System.out.println();
} finally {
readLock.unlock();// 保證當讀的時候若是出現異常,會釋放鎖,synchronized爲何不用呢?由於synchronized內部已經幫咱們作了~
}
}
@Override
public void runTest() {
}
}
複製代碼
這個包裏的類自己就被設計成原子的,能夠方便咱們實現線程安全。 好比:
int count ;
//若是你想保證count++是安全的,可是不想用synchronized,那麼使用AtomicInteger;
複製代碼
好了,到這裏本篇文章就已經結束了。在此次的文章中,咱們主要簡單介紹了線程和進程,詳細瞭解了synchronized和volatile的工做原理,並對他們二者的使用場景進行了比較。相信你對多線程應該已經稍微熟悉一點了,如今來幾道面試練練手,加深印象吧~ Java線程面試題Top50
對於面試題裏面的notify,wait,sleep等線程通訊知識不瞭解的童鞋,歡迎期待下一場chat~