小例子带你入门多线程

当时我刚开始学习并发编程其实是挺茫然的,讲的好的视频资料很少,只能靠自己看书看文档。但是大部分书是写的是比较教科书似的,让人看一眼就想关上。所以自己在入门之后,就想做一个简单的并发编程的教程,方便想要入门学习的人有一个低门槛。

在本场 Chat 中,会讲到如下内容:

  1. Thread 的基本使用
  2. 基本的等待通知模型

适合人群: 准备学习并发编程的人群。

构建简单的 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 (&lt;condition does not hold&gt;)     *             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. 对象 1 在获得锁的基础上,当条件不达到,就循环等待;
  2. 对象 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 专享技术内容哦。

FtooAtPSkEJwnW-9xkCLqSTRpBKX

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值