2、线程-操作 2022-02-15 19:28 > *以下基于JDK1.8* 。此文需要对Java的线程和锁有最基本的了解 ### 线程状态的转换 #### 抢占锁(资源) 上节中我们的线程只经历了NEW、RUNNABLE、TERMINATED状态,下面就看一下其他状态如何达到与转换。 ```java package com.example.demo.core.Thread.option; /** * @author: HanXu * on 2022/2/8 * Class description: BLOCKED、WAITING 线程状态的切换 */ public class Demo1 { public static void main(String[] args) { Object monitor = new Object(); Thread t1 = new Thread(() -> { System.out.println("t1开始执行"); sleep(); System.out.println("t1开始抢占monitor锁"); synchronized (monitor) { System.out.println("t1抢到锁"); //to do something } System.out.println("t1结束执行"); }); Thread t2 = new Thread(() -> { System.out.println("t2开始执行"); sleep(); System.out.println("t2开始抢占monitor锁"); synchronized (monitor) { System.out.println("t2抢到锁"); //to do something } System.out.println("t2结束执行"); }); t1.start(); t2.start(); } public static void sleep() { try { Thread.sleep(50); } catch (InterruptedException e) { e.printStackTrace(); } } } ``` 某次执行结果如下: ``` t1开始执行 t2开始执行 t2开始抢占monitor锁 t1开始抢占monitor锁 t1抢到锁 t1结束执行 t2抢到锁 t2结束执行 ``` t1、t2都开始执行,然后sleep()后,相继醒来,都开始抢占monitor锁(线程状态:NEW->RUNNABLE),结果先输出“t1抢到锁”,那么证明是t1先抢到monitor锁,所以它先执行了synchronized代码块中的代码,此时t1仍是RUNNABLE状态;但是t2由于没有抢到monitor锁,所以处于BLOCKED状态(Thread state for a thread blocked waiting for a monitor lock),接着输出"t1结束执行",说明t1执行完了内部代码,则进入TERMINATED状态;t1在执行完synchronized代码块后自动释放了monitor锁,t2就能抢占到monitor锁,故而执行其内部代码,进入RUNNABLE状态,输出“t2抢到锁”、“t2结束执行”,紧接着进入TERMINATED状态。 #### 线程交互 使用wait()、notify()、notifyAll()可以让线程间进行交互,达到线程间通信的效果。 例如下面例子中,循环10次,两个线程交替打印奇偶数。可以试着将A线程打印奇数,B线程打印偶数。 ```java package com.example.demo.core.Thread.option; /** * @author: HanXu * on 2022/2/8 * Class description: 循环10次,两个线程交替打印奇偶数 */ public class Demo2 { public static volatile int i = 0; private static int count = 10; public static void main(String[] args) { Object monitor = new Object(); Thread t1 = new Thread(() -> { while (i < count) { synchronized (monitor) { //不是奇数,则wait() while ((i & 1) != 1) { try { monitor.wait(); } catch (InterruptedException e) { e.printStackTrace(); } } System.out.println(Thread.currentThread().getName() + ":" + i); i++; monitor.notifyAll(); } } }, "线程A"); Thread t2 = new Thread(() -> { while (i < count) { synchronized (monitor) { //不是偶数,则wait() while ((i & 1) == 1) { try { monitor.wait(); } catch (InterruptedException e) { e.printStackTrace(); } } System.out.println(Thread.currentThread().getName() + ":" + i); i++; monitor.notifyAll(); } } }, "线程B"); t1.start(); t2.start(); } } ``` 执行结果: ``` 线程B:0 线程A:1 线程B:2 线程A:3 线程B:4 线程A:5 线程B:6 线程A:7 线程B:8 线程A:9 线程B:10 ``` 可以看到正确的打印了出来。 ##### 分析线程状态 上述代码中,仍旧是两个线程执行,但是这次跟以往不同,他们之间增加了一个公共的资源:i(除了monitor锁之外,count虽然也公用,但我们只用到了它的值,相当于当成常量来用了),i作为我们要输出并自增的数的载体,两个线程都要使用,并各自判断,满足自身条件了,就执行,否则就等待。 按照执行结果来分析,线程A,B开始执行后,进入while(i < count)循环,假设A先抢占到monitor锁,那么执行`while ((i & 1) != 1)`,判断到目前i是0,不是奇数,那么不是它的活,就执行`monitor.wait();`,执行这句代码会释放monitor锁,并由RUNNABLE状态变为WAITING状态;而线程B由于没有抢占到锁,所以是由RUNNABLE状态变为BLOCKED状态,此时A执行了`monitor.wait();`,释放调了monitor锁,那么B就能够重新抢占锁,这时A处于WAITING状态,没有能力抢占锁,所以只剩B线程,那他自然就能抢到锁,抢到锁后,执行while ((i & 1) == 1),不通过,说明是偶数,那么执行后面的输出和自加,并执行`monitor.notifyAll();`,执行这句代码,会将处于WAITING状态的线程全部唤醒,但是唤醒之后需要重新争抢monitor锁,由于此时锁还在B手里,那么线程A被唤醒后,就会进入BLOCKED状态;待B线程继续执行,结束synchronized代码块,则B是否monitor锁,接着A可重新抢占锁。 注意,B结束synchronized代码块后,还是会进入`while (i < count)`循环,进入循环仍会执行synchronized,则仍会抢占锁,所以并不是A、B就一个挨着一个顺序执行的,而是看谁执行的快,谁执行的慢,如果两个都执行到了synchronized,那么还是需要抢占锁,最终拿到锁的才能继续执行。所以我们需要设计我们的代码,正确的使用while条件和wait()、notifyAll()来保证无论哪个线程有多么会抢占资源,最终结果仍然是A、B交替执行的。这就是线程间通信的意义之一:确保按照我们想要的结果执行。 #### TIMED_WAITING TIMED_WAITING和WAITING状态差不多,区别就是前者会等待一定时间,后者会无限期等待下去。 ```java public static void main(String[] args) { Object monitor = new Object(); Thread t1 = new Thread(() -> { synchronized (monitor) { System.out.println("子线程已经执行:" + System.currentTimeMillis()); try { monitor.wait(2000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("子线程准备结束:" + System.currentTimeMillis()); //to do something } }); t1.start(); } ``` 执行结果: ``` 子线程已经执行:1644310387807 子线程准备结束:1644310389808 ``` t1线程在执行完`monitor.wait(2000);`之后,就由RUNNABLE状态转换为TIMED_WAITING状态,等待等待时间到了之后,由自动转换为RUNNABLE状态。 ### JNI方法解读 上述在操作过程中使用到了wait()、notifyAll(),这类方法是用来是线程等待、唤醒的。来看下他们的源码: #### wait() ```java /** 使当前线程等待,直到另一个线程执行了notify() or notifyAll() wait()和wait(0)是一样的 * Causes the current thread to wait until another thread invokes the * {@link java.lang.Object#notify()} method or the * {@link java.lang.Object#notifyAll()} method for this object. * In other words, this method behaves exactly as if it simply * performs the call {@code wait(0)}. * <p> 当前线程必须拥有this object's monitor。线程释放此监视器的所有权,并等待另一个线程通过调用{@code notify}方法或{@code notifyAll}方法通知等待此对象监视器的线程唤醒 * The current thread must own this object's monitor. The thread * releases ownership of this monitor and waits until another thread * notifies threads waiting on this object's monitor to wake up * either through a call to the {@code notify} method or the * {@code notifyAll} method. The thread then waits until it can * re-obtain ownership of the monitor and resumes execution. * <p> 就像在单参数版本中一样,中断和虚假唤醒是可能的,这种方法应始终在循环中使用: 这里向我们明说了wait()应该在while中,否则会出现虚假唤醒的可能(如果wait在if中) 正确的写法是: synchronized (obj) { while (线程执行的条件不成立时) { obj.wait(); } //执行适合条件的操作 todoSomething(); } * As in the one argument version, interrupts and spurious wakeups are * possible, and this method should always be used in a loop: * <pre> * synchronized (obj) { * while (<condition does not hold>) * obj.wait(); * ... // Perform action appropriate to condition * } * </pre> 此方法只能由作为所有者的线程调用此对象的监视器的。有关详细信息,请参阅{@code notify}方法描述线程成为的所有者的方式监视器。 也就是说,只有获取到锁,才能调用此方法。 * 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. * * @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 void wait() throws InterruptedException { wait(0); } //上述wait(0)调用此方法 public final native void wait(long timeout) throws InterruptedException; ``` 调用wait()方法,首先要获取到对应的对象监视器(获取到锁)。 此方法的效果是:使当前线程等待,会释放线程监视器的所有权(释放锁),并等待另一个线程调用notify()/notifyAll()唤醒他。 #### notifyAll() ```java /** 唤醒所有正在等待对象监视器的线程。等待对象监视器的线程是调用了wait()。 * Wakes up all threads that are waiting on this object's monitor. A * thread waits on an object's monitor by calling one of the * {@code wait} methods. * <p> 被唤醒的线程不会能够执行,直到当前线程释放了对象的锁。被唤醒的线程将以正常的方式与 任何其他正在竞争对象锁的线程 竞争。 并且被唤醒的线程在这个竞争中没有任何优势也没有任何劣势。 (总结:被唤醒的线程不会立即执行,必须等待当前线程释放了锁,且被唤醒的线程执行的时候还需要和其他竞争锁的线程公平竞争,抢到了锁才能执行响应代码 就是说这个方法的目的是将处于waiting状态的线程转化为blocked状态。即将线程从WaitSet集合中拿出来放到EntryList集合中。 ) * The awakened threads will not be able to proceed until the current * thread relinquishes the lock on this object. The awakened threads * will compete in the usual manner with any other threads that might * be actively competing to synchronize on this object; for example, * the awakened threads enjoy no reliable privilege(特权) or disadvantage(劣势) in * being the next thread to lock this object. * <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. * * @throws IllegalMonitorStateException if the current thread is not * the owner of this object's monitor. * @see java.lang.Object#notify() * @see java.lang.Object#wait() */ public final native void notifyAll(); ``` notify(): ```java /** 唤醒一个正在等待对象监视器的线程,只会有一个被唤醒。该选择是任意的,由实现自行决定。 线程通过调用wait()方法进入等待。 * Wakes up a single thread that is waiting on this object's * monitor. If any threads are waiting on this object, one of them * is chosen to be awakened. The choice is arbitrary and occurs at * the discretion of the implementation. A thread waits on an object's * monitor by calling one of the {@code wait} methods. * <p> 被唤醒的线程不能立即执行,直到当前线程放弃对象的锁。唤醒的线程将以通常的方式与任何其他线程竞争,这些线程可能正在积极竞争以在此对象上锁。被唤醒的线程在成为下一个锁定此对象的线程时没有可靠的特权或劣势。 * The awakened thread will not be able to proceed until the current * thread relinquishes(放弃) the lock on this object. The awakened thread will * compete in the usual manner with any other threads that might be * actively competing to synchronize on this object; for example, the * awakened thread enjoys no reliable privilege or disadvantage in being * the next thread to lock this object. * <p> 此方法只能由作为此对象监视器所有者的线程调用。线程通过以下三种方式之一成为对象监视器的所有者: - 通过执行该对象的同步实例方法。 - 通过在对象上执行同步的{@code synchronized}语句体。 - 对于{@code类,}类型的对象,执行该类的同步静态方法。 * This method should only be called by a thread that is the owner * of this object's monitor. A thread becomes the owner of the * object's monitor in one of three ways: * <ul> * <li>By executing a synchronized instance method of that object. * <li>By executing the body of a {@code synchronized} statement * that synchronizes on the object. * <li>For objects of type {@code Class,} by executing a * synchronized static method of that class. * </ul> * <p> 同一时刻只能有一个线程可以拥有对象的同步器 * Only one thread at a time can own an object's monitor. * * @throws IllegalMonitorStateException if the current thread is not * the owner of this object's monitor. * @see java.lang.Object#notifyAll() * @see java.lang.Object#wait() */ public final native void notify(); ``` 调用notify()方法,首先要获取到对应的对象监视器(获取到锁)。 效果:调用notify()方法,会随机唤醒一个通过执行wait()而进入等待的线程。该唤醒是随机的。 被唤醒的线程:不能立即执行,需要等到当前线程释放了锁,并需要公平正常的与其他正在竞争锁的线程竞争。 #### 总结: 调用wait()、notify()、notifyAll()都需要获取到锁。若不在锁内执行,则会抛出:java.lang.IllegalMonitorStateException。 wait()会让当前线程等待(WAITING),并释放锁,等待另一个线程调用notify()/notifyAll()唤醒他。WAITING状态的线程在WaitSet集合中。 notify()会随机唤醒一个wait()等待的线程,notifyAll()会唤醒全部wait()等待的线程。 被唤醒的线程不会立即执行,而是等到当前线程释放了锁,然后公平正常的和其他正在竞争锁的线程竞争,抢到了锁,才能执行代码。 wait()将处于RUNNABLE状态的线程转换为WAITING状态,notify()将处于WAITING状态的线程转化为BLOCKED状态,BLOCKED状态的线程在EntryList中。即将线程从WaitSet集合中拿出来放到EntryList集合中。 这些方法都是native方法,即都是JNI( Java Native Interface,Java本地接口),属于JDK底层封装的方法。 #### 先notifyAll()再wait()会将自己唤醒吗? 这个问题主要是有些地方说“notifyAll()不是立即唤醒,而是等当前线程释放了锁才会唤醒”。如果照这个说法,notifyAll()不立即唤醒,然后自身wait()也加入到了WaitSet中,并释放了锁,那么这时执行"唤醒全部wait线程"的操作,就会将自己也唤醒了呀。可是事实并非如此: ```java package com.example.demo.core.Object; /** * @author: HanXu * on 2022/1/21 * Class description: * A、B、C、D四个线程全部wait阻塞,然后E线程notifyAll唤醒全部线程,再wait阻塞自己。观察A、B、C、D、E线程的输出情况 */ public class NotifyAllDemo { public static void main(String[] args) { Object monitor = new Object(); Thread threadA = new Thread(() -> { synchronized (monitor) { try { monitor.wait(); System.out.println(Thread.currentThread().getName() + ":已经被唤醒"); } catch (InterruptedException e) { e.printStackTrace(); } } }, "A"); Thread threadB = new Thread(() -> { synchronized (monitor) { try { monitor.wait(); System.out.println(Thread.currentThread().getName() + ":已经被唤醒"); } catch (InterruptedException e) { e.printStackTrace(); } } }, "B"); Thread threadC = new Thread(() -> { synchronized (monitor) { try { monitor.wait(); System.out.println(Thread.currentThread().getName() + ":已经被唤醒"); } catch (InterruptedException e) { e.printStackTrace(); } } }, "C"); Thread threadD = new Thread(() -> { synchronized (monitor) { try { monitor.wait(); System.out.println(Thread.currentThread().getName() + ":已经被唤醒"); } catch (InterruptedException e) { e.printStackTrace(); } } }, "D"); Thread threadE = new Thread(() -> { synchronized (monitor) { try { //先notifyAll再wait,不会唤醒自己 //A B C D四个线程全部在WaitSet中,notifyAll将他们全部唤醒,这些线程进入EntryList中。然后正常的抢占CPU执行权,抢占锁,向下执行代码。 monitor.notifyAll(); //wait()后当前线程会释放锁,所以A B C D四个线程才会开始竞争锁。竞争到的向下执行。自身wait()后将自己加如到WaitSet中。 monitor.wait(); System.out.println(Thread.currentThread().getName() + ":已经被唤醒!!!"); } catch (InterruptedException e) { e.printStackTrace(); } } }, "E"); threadA.start(); threadB.start(); threadC.start(); threadD.start(); //让其他线程全部被阻塞 try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } //开始唤醒全部线程 threadE.start(); /* 执行结果: D:已经被唤醒 C:已经被唤醒 B:已经被唤醒 A:已经被唤醒 */ } } /* EntryList, 抢占不到锁,进入blocked阻塞状态,加入此list WaitSet, 获取锁后,执行wait,加入此set */ ``` 上述代码执行后,可以看到,E线程并没有被唤醒。所以notifyAll()并不是“等当前线程释放了锁才唤醒其他线程”,他在执行的那一刻就执行了,将其他waiting的线程转化为blocked。这些blocked线程抢占到锁才能执行,所以要等执行notifyAll()的线程释放了锁,他们才开始抢占。 ### 合作Join() 两个线程执行时,如果执行时间不确定,那么主线程如何知道什么时候结束呢?这时就用到了Join(),他也是Thread的方法,下面看一个小例子:线程A、B执行,主线程等待他们结束后,再输出一句话,然后结束: ```java package com.example.demo.core.Thread; /** * @author: HanXu * on 2022/1/6 * Class description: join */ public class JoinDemo { public static void main(String[] args) throws InterruptedException { Thread thread1 = new Thread(() -> { try { Thread.sleep(1000); System.out.println(Thread.currentThread().getName() + "执行完了"); } catch (InterruptedException e) { e.printStackTrace(); } }, "线程A"); thread1.start(); Thread thread2 = new Thread(() -> { try { Thread.sleep(1000); System.out.println(Thread.currentThread().getName() + "执行完了"); } catch (InterruptedException e) { e.printStackTrace(); } }, "线程B"); thread2.start(); thread1.join(); thread2.join(); System.out.println("主线程执行"); } } /* 线程A执行完了 线程B执行完了 主线程执行 */ ``` 通过执行结果我们可以得知,主线程确实是在thread1、thread2都执行完毕后才继续执行的。那么join()是如何实现这个效果的呢? 点进join()的源码: ```java `Thread` public final void join() throws InterruptedException { join(0); } 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) { while (isAlive()) { wait(0); } } else { while (isAlive()) { long delay = millis - now; if (delay <= 0) { break; } wait(delay); now = System.currentTimeMillis() - base; } } } public final native boolean isAlive(); ``` 可以看到,join()内部流程并不复杂,走的是`if (millis == 0)`这个分支,主要执行的代码就是` while (isAlive()) {wait(0);}` `join(long millis)`是在方法上加锁的,锁的是当前实例对象,比如执行`thread1.join();`时,锁就是thread1这个实例对象;而执行isAlive()和wait(0)时,操作的是当前执行的线程,是main线程去执行`thread1.join()`的,因此是判断main线程是否还在存活,在的话,就wait(0)。看到这我们可以知道join()是如何实现等待的,那唤醒呢?好像并没有手动的notify()方法调用。我在查阅相关资料后发现,大多数观点认为是 jvm 在每个线程执行完毕后都会检测下有无阻塞在这个线程对象上的线程,如果有,那就执行notifyAll()。因此可以得知,join()方法执行完毕退出方法后,main线程还在阻塞,当thread1和thread2都线程都执行完了后,jvm在要关闭thread1和thread2之前检测了下,发现main线程还阻塞在thread1和thread2对象上,因此就调用了notifyAll()方法,将main()唤醒。 ### 线程中断 线程中断相关的方法共有三个:`thread.interrupt()`,`Thread.interrupted()`,`thread.isInterrupted()`。 `thread.interrupt()`:将thread线程中断,给线程添加了中断状态。 `Thread.interrupted()`:通过前面的方法给线程添加了中断标记后,即可使用此方法判断自己是否被中断,且判断后会立即将状态清空。即,被中断的线程使用两次此方法判断,第一次是true,第二次是false。 `thread.isInterrupted()`:thread线程被中断了吗?返回是否有中断标记,仅仅是返回,不做其他处理。即,可以多次使用它来判断。 通过源码我们可以发现一些区别: ```java `Thread` private final Object blockerLock = new Object(); public void interrupt() { if (this != Thread.currentThread()) checkAccess(); synchronized (blockerLock) { Interruptible b = blocker; if (b != null) { interrupt0(); // Just to set the interrupt flag b.interrupt(this); return; } } interrupt0(); } public static boolean interrupted() { return currentThread().isInterrupted(true); } public boolean isInterrupted() { return isInterrupted(false); } private native boolean isInterrupted(boolean ClearInterrupted); private native void interrupt0(); ``` interrupted()和isInterrupted()都是调用了native的`boolean isInterrupted(boolean ClearInterrupted)`方法,只不过前者传入的`ClearInterrupted`标志是true,后者是false。 --- 线程中断文章:https://riun.xyz/work/4891099 --END--
发表评论