手把手带你入门多线程

394 阅读13分钟

前言:

提莫在家办公的第二天!

构建简单的 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 前面设置,之后再设置就没用了。

二人对话

交替对话

  • hi hello
  • how are u i'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 : true
begin wait ....current time : 1574852090614
current time : 1574852090627 ;  notify thread hold lock : true
current time : 1574852090729 ;  notify thread hold lock : true
current 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 : 1574855304815
r1 end  current time : 1574855305819
r2 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 : 1574855591635
r1 end  current time : 1574855592437
r2 current time : 1574855592437

可以看到 r1 获取锁之后,就一直占用,直到同步块结束。

完结福利

在即将到来的金三银四跳槽面试季,希望大家好好加油。

现在整理好了 1000 道多家公司 java 面试题 400 多页 pdf 文档,都已经分专题整理好了。

还有几百页的Java核心知识点PDF。

欢迎大家关注公众号领取,回复:PDF即可,顺便向大家讨个赞,点个关注!嘿嘿


我是提莫

一个节操泛滥,一身凛然正气,刚正不阿的Java程序员