当时我刚开始学习并发编程其实是挺茫然的,讲的好的视频资料很少,只能靠自己看书看文档。但是大部分书是写的是比较教科书似的,让人看一眼就想关上。所以自己在入门之后,就想做一个简单的并发编程的教程,方便想要入门学习的人有一个低门槛。
在本场 Chat 中,会讲到如下内容:
- Thread 的基本使用
- 基本的等待通知模型
适合人群: 准备学习并发编程的人群。
构建简单的 GUI 线程和数据线程工作模式
GUI 初始化和数据初始化
为了为我们所有的 test 构建一个上下文的 context,首先我们来写几个类,来简单模拟下我们安卓中,GUI 线程和数据线程是如何来显示按钮和数据的:
- ModelAndView我们简单的编写一个可见的按钮,并给出了几个主要的属性,其中包含外观的数据和按钮本身要绑定的数据:
/** * 一个 GUI 控件,缩略版本 */ @Data class ModelAndView { /** * 长度 */ private Integer length; /** * 宽度 */ private Integer width; /** * X 位置 */ private Integer xPos; /** * Y 位置 */ private Integer yPos; /** * 控件上绑定数据 */ private Map<String, Object> data; }
- 初始化 GUI 线程在这个线程中,我们主要模拟按钮的外观初始化过程:
/** * 初始化 GUI 线程 */ class GuiInitThread implements Runnable { ModelAndView modelAndView; public GuiInitThread(ModelAndView modelAndView) { this.modelAndView = modelAndView; } @Override public void run() { modelAndView.setLength(1); modelAndView.setWidth(1); modelAndView.setXPos(0); modelAndView.setYPos(0); } }
- 数据初始化线程在这个线程中,我们主要是进行绑定数据的初始化。因为一般来说,在我们实际使用中,这个数据的初始化时间是比较长的,为了跟展示的初始化相互影响,一般绑定数据的初始化都会放在额外的线程来做。在类里面,我们主要是对绑定数据进行赋值。一般来说,这个线程所承担的工作大部分是对远端接口进行请求,获取数据,然后处理数据,绑定回控件,整个过程受网络影响,数据大小影响等等。
/** * 数据初始化线程 */ class DataThread implements Runnable { ModelAndView modelAndView; public DataThread(ModelAndView modelAndView) { this.modelAndView = modelAndView; } @Override public void run() { Map<String, Object> data = new HashMap<>(); try { Thread.sleep(1000); //徒增耗时 } catch (InterruptedException e) { //先这样 e.printStackTrace(); } for (int i = 0; i < 100; i++) { data.put(String.valueOf(i), i); } modelAndView.setData(data); } }
主要的类我们编写完了,接下来来简单写个 test case:
/** * init model and view test * * @throws Exception */ @Test public void initModelAndView() throws Exception { ModelAndView modelAndView = new ModelAndView(); Thread guiInitThread = new Thread(new GuiInitThread(modelAndView)); Thread dataThread = new Thread(new DataThread(modelAndView)); guiInitThread.start(); dataThread.start(); guiInitThread.join(); dataThread.join(); System.out.println(JSONObject.toJSONString(modelAndView)); //完成后打印下啦,看看初始完成之后的情况 }
以上就是第一个 demo,做这个 demo 的目的主要是自己当时初学多线程时候,由于当时还没接触过客户端的开发,对多线程的学习完全是从方法学起的,而不是在这样一个环境下,这就造成了不知道什么时候该多线程,这样的不在 context 环境下的对多线程的学习,其实是无用的。所以之后,我们尽量都会先构建一个环境,然后再环境下我们面对什么样子的问题,对于这个问题我们去学习。
PS
:少理论,多硬核代码,主要还是对照例子体会。
涉及方法解析
- start
/** * 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). * <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() */
调用 start 马上会执行我们在 run 里面写的代码。
- sleep
/** * Causes the currently executing thread to sleep (temporarily cease * execution) for the specified number of milliseconds, subject to * the precision and accuracy of system timers and schedulers. The thread * does not lose ownership of any monitors. * * @param millis * the length of time to sleep 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 static native void sleep(long millis) throws InterruptedException;
使得当前线程睡眠一个毫秒数,但是当前线程不会
放弃它的监视器,即不会释放锁(monitor 的事情后面再说);
- 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); }
等待当前线程死掉,就是 run 方法跑完了。这里实际调用的是 join(0),再让我们看看 join(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) { while (isAlive()) { wait(0); } } else { while (isAlive()) { long delay = millis - now; if (delay <= 0) { break; } wait(delay); now = System.currentTimeMillis() - base; } } }
当传入参数是 0 的时候,不计较时间,就一直等着就完事儿了。但是当大于 0 的时候,会有个循环,循环里面调用的是我们的 Object 的 wait 方法,所以实际上,调用 join 方法是会释放锁的。
GUI 不断刷新,当重新绑定数据时候,停止刷新过程
构造移动过程
我们通过调整 xPos 和 yPos 来改变这个小按钮的位置,来改变按钮的位置,从视觉上产生一个按钮在移动的感觉。之后我们重新绑定数据,同时希望重新绑定数据开始时候,小按钮位置不再改变。
下面我们来添加一个对象移动的方法:
/** * 对象移动 */ class MoveThread extends Thread { ModelAndView modelAndView; public Boolean isStop; //停止标记位置 public MoveThread(ModelAndView modelAndView) { this.modelAndView = modelAndView; this.isStop = false; } @Override public void run() { //注意:此处为正确停止线程方式 while (!this.isInterrupted() && !isStop) { modelAndView.setXPos(modelAndView.getXPos() + 1); modelAndView.setYPos(modelAndView.getYPos() + 1); } } }
通过这个新线程,就能让我们的小按钮一直沿直线移动。
接着写我们的 test case:
/** * 一直移动,但当重新绑定数据时候,停止移动 */ @Test public void moveInterruptedByBindingData() throws Exception { ModelAndView modelAndView = new ModelAndView(); //初始化 gui Thread guiInitThread = new Thread(new GuiInitThread(modelAndView)); guiInitThread.start(); //开始移动 MoveThread moveThread = new MoveThread(modelAndView); moveThread.start(); //开始数据绑定 Thread dataThread = new Thread(new DataThread(modelAndView)); dataThread.setDaemon(true); dataThread.start(); //把移动过程停止 moveThread.interrupt(); //moveThread.isStop = true; Thread.sleep(100); System.out.println(JSONObject.toJSONString(modelAndView)); //完成后打印下啦,看看完成之后的情况 }
观察打印结果,会发现由于数据绑定开始,所需要的时间较长,所以将移动线程中断之后,又过了一段时间,绑定数据的线程还没开始准备数据。
涉及方法解析
- 正确的停止线程让我们再次重新看类
MoveThread
,它内部定义了 isStop 方法:
public Boolean isStop; //停止标记位置
通过对 run 方法执行条件的观察,可以发现,当遇到外部中断或者手动标记 stop 都会使 run 方法停止,这种方法不会抛出异常,或者像之前的 stop 方法一样,出现不会立即停止的情况。
- isInterrupted
/** * Tests whether this thread has been interrupted. The <i>interrupted * status</i> of the thread is unaffected by this method. * * <p>A thread interruption ignored because a thread was not alive * at the time of the interrupt will be reflected by this method * returning false. * * @return <code>true</code> if this thread has been interrupted; * <code>false</code> otherwise. * @see #interrupted() * @revised 6.0 */ public boolean isInterrupted() { return isInterrupted(false); } /** * Tests if some Thread has been interrupted. The interrupted state * is reset or not based on the value of ClearInterrupted that is * passed. */ private native boolean isInterrupted(boolean ClearInterrupted);
该方法只会测试下线程是被中断,而不会影响中断标记位置,可以用作判断使用。
- interrupt
/** * Interrupts this thread. * * <p> Unless the current thread is interrupting itself, which is * always permitted, the {@link #checkAccess() checkAccess} method * of this thread is invoked, which may cause a {@link * SecurityException} to be thrown. * * <p> If this thread is blocked in an invocation of the {@link * Object#wait() wait()}, {@link Object#wait(long) wait(long)}, or {@link * Object#wait(long, int) wait(long, int)} methods of the {@link Object} * class, or of the {@link #join()}, {@link #join(long)}, {@link * #join(long, int)}, {@link #sleep(long)}, or {@link #sleep(long, int)}, * methods of this class, then its interrupt status will be cleared and it * will receive an {@link InterruptedException}. * * <p> If this thread is blocked in an I/O operation upon an {@link * java.nio.channels.InterruptibleChannel InterruptibleChannel} * then the channel will be closed, the thread's interrupt * status will be set, and the thread will receive a {@link * java.nio.channels.ClosedByInterruptException}. * * <p> If this thread is blocked in a {@link java.nio.channels.Selector} * then the thread's interrupt status will be set and it will return * immediately from the selection operation, possibly with a non-zero * value, just as if the selector's {@link * java.nio.channels.Selector#wakeup wakeup} method were invoked. * * <p> If none of the previous conditions hold then this thread's interrupt * status will be set. </p> * * <p> Interrupting a thread that is not alive need not have any effect. * * @throws SecurityException * if the current thread cannot modify this thread * * @revised 6.0 * @spec JSR-51 */ 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(); }
中断线程,清除标记为,简单粗暴,没了;
- setDaemon
/** * Marks this thread as either a {@linkplain #isDaemon daemon} thread * or a user thread. The Java Virtual Machine exits when the only * threads running are all daemon threads. * * <p> This method must be invoked before the thread is started. * * @param on * if {@code true}, marks this thread as a daemon thread * * @throws IllegalThreadStateException * if this thread is {@linkplain #isAlive alive} * * @throws SecurityException * if {@link #checkAccess} determines that the current * thread cannot modify this thread */ public final void setDaemon(boolean on) { checkAccess(); if (isAlive()) { throw new IllegalThreadStateException(); } daemon = on; }
标记线程为后台线程,注意 start 前面设置,之后再设置就没用了。
二人对话
交替对话
>
- hihello
- how are ui'm fine,thank u
- and u?i'm ok!
下面我们来 imagine 一个初中背诵并默写全文的一个英语场景,这也能是你学习这么多年英语别的都忘了,就记得这段对话的一个场景。
(PS:我的建议是先自己写一个交替对话这样的两个线程,完成之后再往下看
)
先上代码,然后我们来分析下这个:
String[] dialogs = {"hi", "hello", "how are u", "i'm fine,thank u", "and u?", "i'm ok!"}; Boolean isChineseSpeak = true; final Object monitor = new Object(); Integer index = 0; class ChinesePersonThread implements Runnable { @Override public void run() { while (index < 5) { synchronized (monitor) { while (!isChineseSpeak) { try { //当条件不满足时候,在这里等待条件对方完成的通知 monitor.wait(); } catch (InterruptedException e) { e.printStackTrace(); } } isChineseSpeak = false; System.out.println("thread id : " + Thread.currentThread().getId() + ", say : " + dialogs[index]); index++; } } } } class ForeignPersonThread implements Runnable { @Override public void run() { while (index < 5) { synchronized (monitor) { if (!isChineseSpeak) { System.out.println("thread id : " + Thread.currentThread().getId() + ", say : " + dialogs[index]); index++; isChineseSpeak = true; //执行完成之后通知等待线程 monitor.notifyAll(); } } } } } @Test public void test() throws Exception { Thread chineseThread = new Thread(new ChinesePersonThread()); Thread foreignThread = new Thread(new ForeignPersonThread()); chineseThread.start(); foreignThread.start(); Thread.sleep(1000); }
涉及方法解析
- wait
/** * 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> * 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> * 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> * 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); }
调用此方法时候,必须获得对象的锁,然后直到其他线程通过 notify/notifyAll 或中断,它才能继续执行。ps,wait 方法会释放锁(emm,大家都这么写,其实翻译过来是监视器,一个意思)。同样,notify 这种通知方法,使用前也需要获取对象锁,然后通知一个在该对象上等待的线程。
- 等待通知模式
- 对象 1 在获得锁的基础上,当条件不达到,就循环等待;
- 对象 2 在获得锁的基础上,执行完成之后,通知等待对象。
这个例子主要是为了写线程交互中的等待通知模式,其实你可以看完之后,自己再写写其他实现方式。
测试锁的释放情况
测试 wait / notify 释放锁情况
final Object lock = new Object(); boolean waiting = true; class WaitThread extends Thread { @Override public void run() { synchronized (lock) { System.out.println("current time : " + System.currentTimeMillis() + " ; wait thread hold lock : " + Thread.holdsLock(lock)); while (waiting) { try { System.out.println("begin wait ...." + "current time : " + System.currentTimeMillis()); lock.wait(); System.out.println("current time : " + System.currentTimeMillis() + " ; wait thread hold lock : " + Thread.holdsLock(lock)); } catch (InterruptedException e) { e.printStackTrace(); } } } } } class NotifyThread implements Runnable { @Override public void run() { try { Thread.sleep(10); } catch (InterruptedException e) { e.printStackTrace(); } synchronized (lock) { System.out.println("current time : " + System.currentTimeMillis() + " ; notify thread hold lock : " + Thread.holdsLock(lock)); if (waiting) { waiting = false; lock.notify(); } try { Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("current time : " + System.currentTimeMillis() + " ; notify thread hold lock : " + Thread.holdsLock(lock)); } } } @Test public void testWaitNotifyLock() throws Exception { new WaitThread().start(); new Thread(new NotifyThread()).start(); Thread.sleep(3000); }
输出:
current time : 1574852090614 ; wait thread hold lock : truebegin wait ....current time : 1574852090614current time : 1574852090627 ; notify thread hold lock : truecurrent time : 1574852090729 ; notify thread hold lock : truecurrent time : 1574852090729 ; wait thread hold lock : true
从时间上来看, wait 线程先获得锁,之后进入等待过程,调用 wait 方法;此时 wait 线程还没执行完,这时 notify 线程获取了锁,并执行完成,说明在 wait 之后,notify 线程获取到了 lock ,说明 wait 方法调用之后,锁被释放掉了, notify 线程才能获取到锁。当 notify 线程执行完成之后, wait 线程又重新获得了锁,继续执行。
测试 sleep 方法获取释放锁情况
@Test public void testSleepLock() throws Exception { Runnable r1 = () -> { synchronized (sleepLock) { System.out.println("r1 begin current time : " + System.currentTimeMillis()); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("r1 end current time : " + System.currentTimeMillis()); } }; Runnable r2 = () -> { //让r1先获取到锁 try { Thread.sleep(20); } catch (InterruptedException e) { e.printStackTrace(); } synchronized (sleepLock) { System.out.println("r2 current time : " + System.currentTimeMillis()); } }; new Thread(r1).start(); new Thread(r2).start(); Thread.sleep(3000); }
输出:
r1 begin current time : 1574855304815r1 end current time : 1574855305819r2 current time : 1574855305819
我们让存在 sleep 的线程 r1 先获取到锁,然后r1进入一个长时间的 sleep ,可以看到在这个时间内,r2 并没有获取到锁,而是 r1 执行完之后,r2 才获取到锁。
测试 yield 方法获取释放锁情况
在上面的基础上,我们已经证明了 sleep 不会释放线程拥有的锁,然后我们改改上面例子,测试下 yield 方法会不会释放锁:
@Test public void testYieldLock() throws Exception { Runnable r1 = () -> { synchronized (sleepLock) { System.out.println("r1 begin current time : " + System.currentTimeMillis()); Thread.yield(); try { Thread.sleep(800); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("r1 end current time : " + System.currentTimeMillis()); } }; Runnable r2 = () -> { //让r1先获取到锁 try { Thread.sleep(20); } catch (InterruptedException e) { e.printStackTrace(); } synchronized (sleepLock) { System.out.println("r2 current time : " + System.currentTimeMillis()); } }; new Thread(r1).start(); new Thread(r2).start(); Thread.sleep(2000); }
输出:
r1 begin current time : 1574855591635r1 end current time : 1574855592437r2 current time : 1574855592437
可以看到 r1 获取锁之后,就一直占用,直到同步块结束。
本文首发于 GitChat,未经授权不得转载,转载需与 GitChat 联系。
阅读全文: http://gitbook.cn/gitchat/activity/5ddde8a981c08a49d99654bf
您还可以下载 CSDN 旗下精品原创内容社区 GitChat App ,阅读更多 GitChat 专享技术内容哦。