多執行緒第一章
多執行緒
1、簡單介紹:
多執行緒對應的就是單執行緒,在java中最開始用到執行緒的時候是我們的main,main是主執行緒,所以在最開始的寫的時候,不知道,後知後覺吧。
對於我們的一個程式來說,我們執行一個main函式,那麼就相當於使用了JVM啟動了一個程序,在java一個程序中至少包含了兩個執行緒,一個是main執行緒,一個是GC執行緒;
我個人的理解是學習多執行緒之前,先參考之前寫的單執行緒中的思想,然後再思考多執行緒,然後再引入到多執行緒中帶來的各種問題。
2、多執行緒
java提供了Thread類的API來操作執行緒,先寫程式碼,再一個一個慢慢解釋:
2.1、建立執行緒
建立執行緒通常來說是有兩種方式:1、實現Thread類;2、繼承Runnale介面,實際上Thread類也是實現了Runnable介面
@Slf4j public class CreateThreadDemoOne { public static void main(String[] args) { // 這裡是主執行緒的位置 log.info("執行緒種執行的程式碼------------->我的執行緒名字是---->{}",Thread.currentThread().getName()); // 建立執行緒並執行 MyThread thread = new MyThread(); new Thread(thread).start(); // 主執行緒睡一會兒 try { Thread.sleep(1000); log.info("我是{},睡醒了",Thread.currentThread().getName()); } catch (InterruptedException e) { e.printStackTrace(); } } } /** * 通過繼承的方式來實現我們自己的執行緒類 */ @Slf4j class MyThread extends Thread { /** * 一定要重寫這個方法 */ @Override public void run() { log.info("執行緒種執行的程式碼------------->我的執行緒名字是---->{}",Thread.currentThread().getName()); } }
通過實現Runnable介面來操作:
@Slf4j public class ThreadDemoOne { public static void main(String[] args) { // 這裡是主執行緒的位置 log.info("執行緒種執行的程式碼------------->我的執行緒名字是---->{}",Thread.currentThread().getName()); // 建立執行緒並執行 MyThread thread = new MyThread(); new Thread(thread).start(); // 主執行緒睡一會兒 try { Thread.sleep(1000); log.info("我是{},睡醒了",Thread.currentThread().getName()); } catch (InterruptedException e) { e.printStackTrace(); } } } @Slf4j class MyThread implements Runnable{ @Override public void run() { log.info("I can run"); } }
通過觀察,發現上面的程式碼是及其相似的,那麼我們兩種通過都會來使用。
但是在jdk8中,我們更喜歡使用介面來實現,使用lambda的方式來快速實現一個執行緒:
@Slf4j
public class ThreadDemoOne {
public static void main(String[] args) {
log.info("當前執行緒是:{}",Thread.currentThread().getName());
new Thread(()->{
log.info("I can run");
}).start();
// 主執行緒休息一會兒
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
在上面中建立的兩個執行緒物件,最後都會傳給Thread的構造中去,然後呼叫start方法;但是從上面可以看到,自定義的執行緒類實現或者是繼承了介面,那麼都重寫了run方法,為什麼不呼叫物件直接呼叫run方法而是呼叫start方法呢?看下程式碼,然後走下原始碼:
// 建立執行緒並執行
MyThread thread = new MyThread();
new Thread(thread).start();
從構造方法中開始進入,點選init方法,最終會走到下面的原始碼中來:
// 一定要看註釋!不看註釋就不知道幹嘛的
/**
* Initializes a Thread.功能介紹:建立一個執行緒
* 引數介紹
* @param g the Thread group
* @param target the object whose run() method gets called 這個才是重點引數目標物件呼叫run方法,找到這個引數呼叫的地方即可
* @param name the name of the new Thread
* @param stackSize the desired stack size for the new thread, or
* zero to indicate that this parameter is to be ignored.
* @param acc the AccessControlContext to inherit, or
* AccessController.getContext() if null
* @param inheritThreadLocals if {@code true}, inherit initial values for
* inheritable thread-locals from the constructing thread
*/
// 現在其他的引數不跟!直接跟蹤target
private void init(ThreadGroup g, Runnable target, String name,
long stackSize, AccessControlContext acc,
boolean inheritThreadLocals) {
if (name == null) {
throw new NullPointerException("name cannot be null");
}
this.name = name;
Thread parent = currentThread();
SecurityManager security = System.getSecurityManager();
if (g == null) {
/* Determine if it's an applet or not */
/* If there is a security manager, ask the security manager
what to do. */
if (security != null) {
g = security.getThreadGroup();
}
/* If the security doesn't have a strong opinion of the matter
use the parent thread group. */
if (g == null) {
g = parent.getThreadGroup();
}
}
/* checkAccess regardless of whether or not threadgroup is
explicitly passed in. */
g.checkAccess();
/*
* Do we have the required permissions?
*/
if (security != null) {
if (isCCLOverridden(getClass())) {
security.checkPermission(SUBCLASS_IMPLEMENTATION_PERMISSION);
}
}
g.addUnstarted();
this.group = g;
this.daemon = parent.isDaemon();
this.priority = parent.getPriority();
if (security == null || isCCLOverridden(parent.getClass()))
this.contextClassLoader = parent.getContextClassLoader();
else
this.contextClassLoader = parent.contextClassLoader;
this.inheritedAccessControlContext =
acc != null ? acc : AccessController.getContext();
// 跟蹤到了這裡,發現只是給成員變數去進行賦值而已!但是上面說了,target變數會呼叫run方法!
// 剛剛在進行繼承或者是實現的時候,又重寫了run方法,根據多型的性質,最終會呼叫自己實現的run方法
this.target = target;
setPriority(priority);
if (inheritThreadLocals && parent.inheritableThreadLocals != null)
this.inheritableThreadLocals =
ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);
/* Stash the specified stack size in case the VM cares */
this.stackSize = stackSize;
/* Set thread ID */
tid = nextThreadID();
}
// 上面的重點引數的註釋!可以看到我們為什麼要實現Runnable介面了,這裡利用了多型
// 這裡也是為何說要麼是通過繼承Thread(實現了Runnable),或者是實現Runnable介面
/* What will be run. */
private Runnable target;
// 導致這個執行緒開始執行,java虛擬機器將會呼叫這個執行緒的run方法
// 也就是說我們建立的執行緒在呼叫start方法之後,會呼叫重寫的run方法
/**
* Causes this thread to begin execution; the Java Virtual Machine
* calls the <code>run</code> method of this thread.
* <p>
* The result is that two threads are running concurrently: the
* current thread (which returns from the call to the
* <code>start</code> method) and the other thread (which executes its
* <code>run</code> method).
// 看看上面的介紹:結果就是將會呼叫兩個執行緒來執行,一個呼叫start方法,另外一個執行緒呼叫run方法
* <p>
* It is never legal to start a thread more than once. 通常來說,一個執行緒啟動多次是合法的,在執行完成之後不會再次執行啦
* In particular, a thread may not be restarted once it has completed
* execution.
*
* @exception IllegalThreadStateException if the thread was already
* started.
* @see #run()
* @see #stop()
*/
public synchronized void start() {
/**
* This method is not invoked for the main method thread or "system"
* group threads created/set up by the VM. Any new functionality added
* to this method in the future may have to also be added to the VM.
*
* A zero status value corresponds to state "NEW".
*/
if (threadStatus != 0)
throw new IllegalThreadStateException();
/* Notify the group that this thread is about to be started
* so that it can be added to the group's list of threads
* and the group's unstarted count can be decremented. */
group.add(this);
boolean started = false;
try {
// 呼叫了本地方法
start0();
started = true;
} finally {
try {
if (!started) {
group.threadStartFailed(this);
}
} catch (Throwable ignore) {
/* do nothing. If start0 threw a Throwable then
it will be passed up the call stack */
}
}
}
private native void start0();
本地方法,看不到原始碼了。
總結一下
// 建立執行緒並執行
MyThread thread = new MyThread();
new Thread(thread).start();
這段程式碼背後的原理:在自定義了執行緒類物件之後,重寫了run方法,傳遞給Thread構造之後,呼叫了start方法後。首先儲存當前的執行緒的引用,然後在呼叫start方法的之後,JVM將會呼叫當前執行緒(我們自定義)引用的run方法。
那麼這裡就是為何我們自定義的執行緒不去呼叫run方法來進行執行了。物件執行自己重寫的run方法,最終的效果就是一個普通物件呼叫普通方法執行,而看不到執行緒的效果。所以執行緒的建立是由JVM通過系統呼叫向作業系統來實現的。
在作業系統章節裡介紹了java使用的執行緒模型是KLT模型。
3、對於run方法的理解
我們重寫的run方法,可以理解成是我們程式要進行執行的task任務。那麼參考著之前只是在單執行緒中寫的程式碼,也就是在主執行緒中寫的程式碼。
在main執行緒(沒有寫多執行緒的程式碼)的執行,在main執行緒種執行的程式碼就是一個任務。
也就是說,可以理解成main執行緒執行的run方法就是一個task,在我們自定義的執行緒的run方法中的又是一個task。
多執行緒的最終效果就是多個執行緒執行多個task任務。
4、執行緒之間的執行關係圖
所以我們可以看到其他執行緒一定是在主執行緒中創建出來的,隨著主執行緒的消失而消失,因為主執行緒消失了,那麼子執行緒失去了引用,將會被GC掉;
所以在主執行緒中,一定要保證的是其他的子執行緒都執行完成了之後,然後主執行緒再執行完畢。
所以又牽扯出來另外一個概念,執行緒阻塞。
可能說主執行緒中的業務邏輯程式碼很快就執行完了,但是子執行緒中沒有執行結束,然後主執行緒執行完了就退出了,那麼子執行緒邏輯沒有執行完成就已經結束了,可能會出現問題。所以我們要做的事情就是在主執行緒中讓其他的執行緒執行完成之後,主執行緒再執行。
5、執行緒阻塞
阻塞的情況又分為了很多種。比如說IO阻塞是最常見的,在檔案上傳、檔案下載的時候,這個是比較常見的
還有一種就是上面介紹的,另外一種情況就是需要一個執行緒需要使用到另外一個執行緒的資料,那麼這個執行緒就應該等待需要拿到資料的另外一個執行緒執行結束。
分別模擬一下場景實現:
@Slf4j
public class ThreadDemo {
public static void main(String[] args) {
// 開啟子執行緒
// lambda表示式中寫的就是task程式碼
Thread t = new Thread(() -> {
log.info("執行的就是檔案上傳的程式碼-----------------------------------");
});
// 主執行緒什麼業務程式碼都沒有,那麼利用睡一會兒來模擬對應的業務邏輯程式碼
try {
t.start();
// Thread.sleep(5000);
t.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
從上面的程式碼中可以看到有兩種實現方式,第一種是讓主執行緒睡眠,第二種方式join方法,join方法就是讓當前執行緒執行完成之後,當前執行緒再繼續執行。
也就是說,t執行緒執行完成之後,main執行緒再去執行。
看下join底層實現:
// 等待執行緒死亡
/**
* Waits for this thread to die.
*
* <p> An invocation of this method behaves in exactly the same
* way as the invocation
*
* <blockquote>
* {@linkplain #join(long) join}{@code (0)}
* </blockquote>
*
* @throws InterruptedException
* if any thread has interrupted the current thread. The
* <i>interrupted status</i> of the current thread is
* cleared when this exception is thrown.
*/
public final void join() throws InterruptedException {
// 呼叫了另外一個方法
join(0);
}
// 最多等待ms後讓該執行緒死亡,超時為0意味著永遠等待
/**
* Waits at most {@code millis} milliseconds for this thread to
* die. A timeout of {@code 0} means to wait forever.
*
* <p> This implementation uses a loop of {@code this.wait} calls
* conditioned on {@code this.isAlive}. As a thread terminates the
* {@code this.notifyAll} method is invoked. It is recommended that
* applications not use {@code wait}, {@code notify}, or
* {@code notifyAll} on {@code Thread} instances.
*
* @param millis
* the time to wait in milliseconds
*
* @throws IllegalArgumentException
* if the value of {@code millis} is negative
*
* @throws InterruptedException
* if any thread has interrupted the current thread. The
* <i>interrupted status</i> of the current thread is
* cleared when this exception is thrown.
*/
public final synchronized void join(long millis) throws InterruptedException {
long base = System.currentTimeMillis();
long now = 0;
if (millis < 0) {
throw new IllegalArgumentException("timeout value is negative");
}
if (millis == 0) {
// 一直判斷實現是否存活!結果呼叫到了wait方法!那麼看看wait方法
// 很明顯,這個陷入了while迴圈,那麼什麼時候出來呢?
// 這個執行緒會一直判斷是否處於是否存活狀態!如果存活的,那麼就一直排隊等待0秒
// 所以說,wait(0)是無法出去了,要想出去,只可能是isAlive=true,也就是說任務執行完成結束後,這裡將會返回false,退出,然後執行完成!
while (isAlive()) {
wait(0); // 看方法上!有了synchronized關鍵字,預設的是位元組碼物件監視器
}
} else {
// 如果傳入進來的不是0,那麼下面將會來進行判斷
while (isAlive()) {
long delay = millis - now;
// 一直到指定時間到了,然後中斷迴圈出去!所以join方法還可以指定時間!在執行的時間到了之後將會讓出CPU的執行權;
if (delay <= 0) {
break;
}
// 如果不是小於0的,那麼再等一會兒!然後再來判斷下時間
wait(delay);
now = System.currentTimeMillis() - base;
}
}
}
上面程式碼的意思就是說:如果加上了時間,比如說是10ms,那麼先獲取得到now的時間是0,然後delay=10,然後就睡眠十秒鐘,也就是說陷入wait狀態;然後時間過了,就醒過來了,進入到了就緒狀態,然後準備開始搶了,緊接著,先拿到當前時間-最開始的時間,也就是說相差時間>=10秒,然後10-(一個大於等於10)<=0,那麼就直接break掉了,也就是說不管t.join的執行緒t有沒有執行完成,開始執行主執行緒的業務邏輯了,主執行緒執行完成就結束了!
所以我們並不會在join後面加上時間,因為我們不知道什麼時候執行結束!最完整的就是不寫時間,讓其執行完成即可。
可以看到這個本地方法寫了大量的註釋說明,那麼看下寫了什麼東西:
/**
* Causes the current thread to wait until either another thread invokes the
* {@link java.lang.Object#notify()} method or the
* {@link java.lang.Object#notifyAll()} method for this object, or a
* specified amount of time has elapsed.
// 上面說的是:這個執行緒會一直等待,直到要麼是其他的執行緒呼叫notify或者是notifyAll對於這個物件,或者是指定的時間過去了;
* <p>
* The current thread must own this object's monitor.
這個執行緒必須有自己的物件監視器。我的理解就是一個鎖物件,這個鎖物件中會記錄多個執行緒。讓多個執行緒來搶這個鎖,搶成功了,那麼就可以接著執行程式碼了。
這裡說明了這個wait方法要有物件監視器的條件下執行。那麼也就說明了一定要有synchronized這個關鍵字的包裹。
* <p>
* This method causes the current thread (call it <var>T</var>) to
* place itself in the wait set for this object and then to relinquish
* any and all synchronization claims on this object. Thread <var>T</var>
* becomes disabled for thread scheduling purposes and lies dormant
* until one of four things happens:
* <ul>
* <li>Some other thread invokes the {@code notify} method for this
* object and thread <var>T</var> happens to be arbitrarily chosen as
* the thread to be awakened.
* <li>Some other thread invokes the {@code notifyAll} method for this
* object.
* <li>Some other thread {@linkplain Thread#interrupt() interrupts}
* thread <var>T</var>.
/**
此方法使當前執行緒(稱為 T)將自己置於此物件的等待集中,然後放棄對該物件的任何和所有同步宣告。 執行緒 T 出於執行緒排程目的而被禁用並處於休眠狀態,直到發生 以下四種情況之一:
1、某個其他執行緒為此物件呼叫了notify 方法,而執行緒T 恰好被任意選擇為要喚醒的執行緒。
2、其他一些執行緒為此物件呼叫notifyAll 方法。
3、其他一些執行緒中斷了執行緒 T。
4、或多或少已經過了指定的實時時間。 但是,如果超時為零,則不考慮實時時間,執行緒只是等待直到收到通知。
*/
* <li>The specified amount of real time has elapsed, more or less. If
* {@code timeout} is zero, however, then real time is not taken into
* consideration and the thread simply waits until notified.
* </ul>
* The thread <var>T</var> is then removed from the wait set for this
* object and re-enabled for thread scheduling. It then competes in the
* usual manner with other threads for the right to synchronize on the
* object; once it has gained control of the object, all its
* synchronization claims on the object are restored to the status quo
* ante - that is, to the situation as of the time that the {@code wait}
* method was invoked. Thread <var>T</var> then returns from the
* invocation of the {@code wait} method. Thus, on return from the
* {@code wait} method, the synchronization state of the object and of
* thread {@code T} is exactly as it was when the {@code wait} method
* was invoked.
/**
然後,執行緒 T 從該物件的等待集中移除,並重新啟用執行緒排程。 然後它以通常的方式與其他執行緒競爭在物件上同步的權利; 一旦它獲得了物件的控制權,它對物件的所有同步宣告都將恢復到之前的狀態——也就是說,恢復到呼叫 wait 方法時的情況。 然後執行緒 T 從 wait 方法的呼叫中返回。 因此,從 wait 方法返回時,物件和執行緒 T 的同步狀態與呼叫 wait 方法時完全相同。
執行緒也可以在沒有被通知、中斷或超時的情況下喚醒,即所謂的虛假喚醒。 雖然這在實踐中很少發生,但應用程式必須通過測試應該導致執行緒被喚醒的條件來防止它,如果條件不滿足則繼續等待。 換句話說,等待應該總是發生在迴圈中
也就是說:從wait狀態出來後,和在執行wait之前狀態是一樣的,都可以來進行搶鎖
比如說這種情況:
like this one:
synchronized (obj) {
while (<condition does not hold>)
obj.wait(timeout);
... // Perform action appropriate to condition
}
*/
* <p>
* A thread can also wake up without being notified, interrupted, or
* timing out, a so-called <i>spurious wakeup</i>. While this will rarely
* occur in practice, applications must guard against it by testing for
* the condition that should have caused the thread to be awakened, and
* continuing to wait if the condition is not satisfied. In other words,
* waits should always occur in loops, like this one:
* <pre>
* synchronized (obj) {
* while (<condition does not hold>)
* obj.wait(timeout);
* ... // Perform action appropriate to condition
* }
* </pre>
* (For more information on this topic, see Section 3.2.3 in Doug Lea's
* "Concurrent Programming in Java (Second Edition)" (Addison-Wesley,
* 2000), or Item 50 in Joshua Bloch's "Effective Java Programming
* Language Guide" (Addison-Wesley, 2001).
*
* <p>If the current thread is {@linkplain java.lang.Thread#interrupt()
* interrupted} by any thread before or while it is waiting, then an
* {@code InterruptedException} is thrown. This exception is not
* thrown until the lock status of this object has been restored as
* described above.
*
* <p>
* Note that the {@code wait} method, as it places the current thread
* into the wait set for this object, unlocks only this object; any
* other objects on which the current thread may be synchronized remain
* locked while the thread waits.
* <p>
* This method should only be called by a thread that is the owner
* of this object's monitor. See the {@code notify} method for a
* description of the ways in which a thread can become the owner of
* a monitor.
/**
如果當前執行緒在等待之前或期間被任何執行緒中斷,則丟擲 InterruptedException。 直到如上所述恢復此物件的鎖定狀態後,才會丟擲此異常。
請注意,wait 方法將當前執行緒放入此物件的等待集中,因此僅解鎖此物件; 當前執行緒可能同步的任何其他物件線上程等待時保持鎖定。
此方法只能由作為此物件監視器的所有者的執行緒呼叫。 有關執行緒可以成為監視器所有者的方式的描述,請參閱通知方法。
*/
* @param timeout the maximum time to wait in milliseconds.
* @throws IllegalArgumentException if the value of timeout is
* negative.
* @throws IllegalMonitorStateException if the current thread is not
* the owner of the object's monitor.
* @throws InterruptedException if any thread interrupted the
* current thread before or while the current thread
* was waiting for a notification. The <i>interrupted
* status</i> of the current thread is cleared when
* this exception is thrown.
* @see java.lang.Object#notify()
* @see java.lang.Object#notifyAll()
*/
public final native void wait(long timeout) throws InterruptedException;
總結一下wait方法:在使用的時候要有物件監視器,也就是synchronized(物件),多個執行緒都會置於此物件的等待集中,處於休眠狀態。
直到遇到了下面四種情況:
1、某個其他執行緒為此物件呼叫了notify 方法,而執行緒T 恰好被任意選擇為要喚醒的執行緒。
2、其他一些執行緒為此物件呼叫notifyAll 方法。
3、其他一些執行緒中斷了執行緒 T。
4、或多或少已經過了指定的實時時間。 但是,如果超時為零,則不考慮實時時間,執行緒只是等待直到收到通知。
緊接著進行的操作就是開始搶鎖,但是前一次因為搶鎖因為進入了阻塞狀態,第二次來搶鎖的時候和第一次來搶鎖的狀態是一樣的。如果搶到鎖了,那麼其他執行緒就會進入到wait狀態,知道當前執行緒執行結束!如果搶到鎖的執行緒又遇到了wait,那麼將又會進入到原來的狀態中去;
如果有中斷標記,那麼直到恢復到狀態的時候,才會丟擲異常。這個方法只能由同一個物件監視器的所有執行緒才可以呼叫:notify(), notifyAll()
也就是說在同一個物件監視器上的其他執行緒呼叫notify(), notifyAll(),所以也就是為什麼說wait、notify和notifyAll都只能夠在物件監視器,也就是在synchronized中進行執行。
那麼看完了wait方法之後,相信其他的過載方法也都應該知道了。
那麼接下來就應該會一會syncronized這個關鍵字了
6、synchronized關鍵字
上面提到了物件監視器和synchronized,作為一個同步器,使用環境是多環境條件下;
使用方式:在方法上(普通方法、靜態方法)、同步程式碼塊中,從上面的wait方法中,可以看到使用的環境是有多執行緒的,那麼在單執行緒條件下使用也是毫無影響的。那麼看一下使用場景,先寫程式碼模擬一下:
@Slf4j
public class SynchronizedTest {
public static void main(String[] args) {
MyThread thread = new MyThread();
// 啟動三個執行緒執行
new Thread(thread).start();
new Thread(thread).start();
new Thread(thread).start();
while (true) {
log.info("main執行緒中num對應的值是--------------------------------->:{}", thread.num);
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
@Slf4j
class MyThread implements Runnable {
int num = 100;
@Override
public void run() {
while (true) {
if (num > 0) {
log.info("當前獲取得到的變數num的值是:{}", num);
try {
Thread.sleep(1000);
num--;
log.info("操作完成之後,對應的變數num的值是:{}", num);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
控制檯顯示:
11:56:36.905 [main] INFO com.guang.thread.safe.SynchronizedTest - main執行緒中num對應的值是--------------------------------->:100
11:56:36.921 [Thread-1] INFO com.guang.thread.safe.MyThread - 操作完成之後,對應的變數num的值是:99
11:56:36.921 [Thread-1] INFO com.guang.thread.safe.MyThread - 當前獲取得到的變數num的值是:99
11:56:36.921 [Thread-0] INFO com.guang.thread.safe.MyThread - 操作完成之後,對應的變數num的值是:98
11:56:36.921 [Thread-2] INFO com.guang.thread.safe.MyThread - 操作完成之後,對應的變數num的值是:97
11:56:36.921 [Thread-0] INFO com.guang.thread.safe.MyThread - 當前獲取得到的變數num的值是:98
11:56:36.921 [Thread-2] INFO com.guang.thread.safe.MyThread - 當前獲取得到的變數num的值是:97
11:56:37.015 [main] INFO com.guang.thread.safe.SynchronizedTest - main執行緒中num對應的值是--------------------------------->:97
11:56:37.126 [main] INFO com.guang.thread.safe.SynchronizedTest - main執行緒中num對應的值是--------------------------------->:97
對自定義的執行緒定義了一個變數num,然後執行緒任務是對這個變數進行操作,啟動了三個執行緒來進行操作。
所以這裡測試的目標也很顯然:多執行緒操作一個變數(這個變數稱之為共享變數)
我們理想的效果是當num為0的時候,三個執行緒都不會再去執行了,執行緒而是被GC回收掉了。但是控制檯顯示是出現了負值!為什麼?
本質原因是執行緒存在著執行緒排程的問題,程式並不是一直在CPU上執行,CPU是從就緒佇列上隨機挑選一個執行緒來進行隨機,那麼其他的執行緒就需要進行等待了。
那麼這裡就會造成一個問題:多執行緒操作共享變數帶來的執行緒安全問題
通過上面的執行緒之間的關係圖可以看到,每個執行緒在地位上是等價的,每個執行緒都有自己獨立執行的空間。但是我們的執行緒是從main執行緒開始的,資料也都儲存在main執行緒裡面,所以執行緒在啟動後,被JVM呼叫了之後,執行緒的資料也應該會從main執行緒中獲取得到。
那麼main執行緒和子執行緒獲取得到資料應該都是一樣的。那麼這裡又將設計到執行緒記憶體模型。所以這裡先放一下,等介紹完了執行緒記憶體模型再來分析這裡。
從下面簡單的分析後:得出來一個結論,子執行緒需要使用到main執行緒中的變數,那麼是一份拷貝的資料變數。
那麼再思考一個問題:子執行緒操作完成變數之後,對從主執行緒拷貝出來的執行緒如何處理的?再寫回去!
從上面可以看到,對於主執行緒的變數num=100,每個執行緒拷貝了之後進行操作之後,然後主執行緒列印了之後,發現不再是100,那麼可以說明,子執行緒裡面操作後資料,還會將當前本地執行緒中從main執行緒中拷貝過來的變數給寫回去!
對於引用型別來說,寫回去的時候改變的是屬性;對於基本型別來說,寫回去的時候改變的是值;
從上面的結果中驗證了子執行緒都會將最終的結果寫到main執行緒中去;
每個子執行緒中的操作就相當於是一個單執行緒執行程式,然後將操作後的值寫回到主記憶體中去。
那麼這個時候就會帶來一個新的問題!到底是執行緒在操作完main執行緒中的變數之後就寫回?還是當前執行緒執行緒執行完成之後再寫回:
每次操作完成main執行緒中的變數之後,都會寫回。
比如說上面的三個執行緒,每次計算後假如都是-1,那麼三個執行緒中的Num都是99,那麼都寫回去的時候,num=99,num=99,num=99。這樣看起來並沒有什麼問題!這裡最終的效果,是根據先後順序來進行覆蓋掉main執行緒中的值。
那麼考慮下為什麼會出現負值的情況:
12:02:47.660 [main] INFO com.guang.thread.safe.SynchronizedTest - main執行緒中num對應的值是--------------------------------->:1
12:02:47.769 [main] INFO com.guang.thread.safe.SynchronizedTest - main執行緒中num對應的值是--------------------------------->:1
12:02:47.879 [main] INFO com.guang.thread.safe.SynchronizedTest - main執行緒中num對應的值是--------------------------------->:1
12:02:47.879 [Thread-0] INFO com.guang.thread.safe.MyThread - 操作完成之後,對應的變數num的值是:-1
12:02:47.879 [Thread-1] INFO com.guang.thread.safe.MyThread - 操作完成之後,對應的變數num的值是:-2
12:02:47.879 [Thread-2] INFO com.guang.thread.safe.MyThread - 操作完成之後,對應的變數num的值是:0
12:02:47.994 [main] INFO com.guang.thread.safe.SynchronizedTest - main執行緒中num對應的值是--------------------------------->:-2
從上面可以看到main執行緒從1的時候,子執行緒重新從main執行緒中讀取對應的資料,然後來進行操作。
如果說,只是寫回去,然後不從main執行緒中讀資料回去,那麼就不會產生負值的情況。
那麼這個時候就有了一個新的問題:
@Override
public void run() {
while (true) {
if (num > 0) {
log.info("當前獲取得到的變數num的值是:{}", num);
try {
Thread.sleep(1000);
num--;
log.info("操作完成之後,對應的變數num的值是:{}", num);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
共享變數是num,在每個子執行緒中,在操作共享變數的只有兩個地方,一個是判斷,一個是做自減操作;
這個時候問題就在於:是將回去的資料帶回去之後,操作完成之後寫回?還是說只要涉及到操作共享變數的地方,都將讀寫main執行緒中的資料?
答案是:只要涉及到操作共享變數的地方,都將會從main執行緒中獲取得到資料;
因為從main執行緒拷貝回去的時候,如果是一個負值,那麼就不會經過判斷,而直接結束了!
但是如果按照第二種方式理解:只要涉及到操作的地方,都將會去main執行緒中去讀
那麼接下來就開始說:為什麼會有負值的情況產生?
執行緒執行是沒有先後順序的,但是寫回資料一定是有順序的。
按照上面控制檯結果來:
假設是這樣子來執行的:
1、執行緒2讀取到了1,然後操作完之後,準備寫回去;
2、執行緒0讀取到了1之後,經過判斷後,卡住了;
3、執行緒1讀取到了1之後,經過判斷後,也卡住了
4、執行緒2將資料寫回去的時候,main執行緒中已經是0了,那麼再讀回去的時候,發現是0,沒有經過判斷,那麼直接結束掉執行緒;
5、執行緒0恢復執行,發現涉及到共享變數num的操作了,再去從main執行緒中來進行讀取資料,讀回去的時候是0,然後減一操作,返回-1;
6、執行緒1恢復執行,發現涉及到共享變數num的操作了,再去從main執行緒中來進行讀取資料,讀回去的時候是-1,然後減一操作,返回-2;
7、執行緒0和1寫回去之後,再次拷貝回去,執行緒0發現是-1,執行緒1發現是-2,那麼兩個執行緒的判斷通不過,那麼直接結束執行
最終控制檯打印出來的資料就是上面分析出來的資料!
-------實踐分析出來的!如果覺得這裡分析有問題的同學請聯絡我---------
所以在這裡先總結一下:
在多執行緒環境下涉及到共享變數的時候:
1、子執行緒先從共享變數所在的執行緒中拷貝資料到自己的執行緒空間去;
2、在設計到操作共享變數的時候都將會去主執行緒中獲取得到新的資料去執行,直到沒有涉及到共享資料之後,操作完成寫回到共享變數所在的執行緒;
那麼這裡又將帶來一個新的問題!
執行緒模型和計算機硬體是如何進行對應的?專門開一個章節來說明。
7、執行緒記憶體模型
參考:https://www.cnblogs.com/qishuai/p/8724202.html
Java記憶體模型規範了Java虛擬機器與計算機記憶體是如何協同工作的。
JVM中規定了,每個執行緒都有自己的執行空間,也就是都有自己的執行緒棧空間。每個執行緒之間是相互隔離的。在一個執行緒中建立的區域性變數僅由當前執行緒可見,即使兩個執行緒執行同樣的程式碼,這兩個執行緒任然在在自己的執行緒棧中的程式碼來建立本地變數。因此,每個執行緒擁有每個本地變數的獨有版本
所有原始型別的本地變數都存放線上程棧上,因此對其它執行緒不可見。一個執行緒可能向另一個執行緒傳遞一個原始型別變數的拷貝,但是它不能共享這個原始型別變數自身。也就是說執行緒之間的共享變數只是一份拷貝!
對於基本型別來說,每個執行緒都可以對這個拷貝過來的變數進行操作,但是注意:每個執行緒操作後的變數都獨屬於當前執行緒空間,所以最終導致的效果就是每個執行緒空間的變數可能都不會是一樣的。
對於引用型別來說,物件的地址值是無法來進行修改的,但是可以對物件的屬性來進行操作;物件的建立是在堆上的,這個注意下
對於方法來說,要操作的變數肯定是存在於本地執行緒中的。所以多執行緒安全的問題就在於共享變數上,那麼這個是一個重點!
千里之行,始於足下。不積跬步,無以至千里