多线程(一)、基础概念及notify()和wait()使用

一、基础概念

1.1、CPU核心数和线程数的关系

多核心 :单芯片多处理器( Chip Multiprocessors,简称CMP),其思想是将大规模并行处理器中的SMP(对称多处理器)集成到同一芯片内,各个处理器并行执行不同的进程。这种依靠多个CPU同时并行地运行程序是实现超高速计算的一个重要方向,称为并行处理,

多线程 :让同一个处理器上的多个线程同步执行共享处理器的执行资源,可最大限度地实现宽发射、乱序的超标量处理,提高处理器运算部件的利用率,缓和由于数据相关或 Cache未命中带来的访问内存延时。

二者关系 : 目前CPU基本都是多核,很少看到单核CPU。增加核心数目就是为了增加线程数,因为操作系统是通过线程来执行任务的,一般情况下它们是1:1对应关系,也就是说四核CPU一般拥有四个线程。但Intel引入超线程技术后,使核心数与线程数形成1:2的关系.

1.2、时间片轮转机制 (RR 调度)

定义:系统把所有就绪进程先入先出的原则排成一个队列。新来的进程加到就绪队列末尾。每当执行进程调度时,进程调度程序总是选出就绪队列的队首进程,让它在CPU上运行一个时间片的时间。时间片是一个小的时间单位,通常为10~100ms数量级。当进程用完分给它的时间片后,系统的计时器发出时钟中断,调度程序便停止该进程的运行,把它放入就绪队列的末尾;然后,把CPU分给就绪队列的队首进程,同样也让它运行一个时间片,如此往复。

根据上面CPU核心数和线程数的关系 1:1的关系,如果我们手机是双核手机,那么我们按道理只能起两个线程,但是在实际的开发过程中并不是这样,我们可能开了十几个线程 "同时" 在执行,这是因为操作系统提供了CPU时间片轮转这个机制,它为每个进程分配一个时间段(即时间片),让他们在一段时间内交替执行。

上下文切换时间:由于时间片轮转进制,会使得进程之间不停的进行切换,进程之间切换涉及到保存和装入到寄存器值及内存映像,更新表格及队列,这个过程是需要消耗时间的。

时间片时间设置: 时间片如果设置太短,会导致过多进程不断切换,由于切换过程会产生上小文切换时间,所以降低CPU效率,设置太长,又会导致相对较短的交互请求响应变差,通常时间片设置在100ms左右比较合理。

1.3、进程和线程

1.3.1、什么是进程?

进程是程序运行资源分配的最小单元

进程是操作系统进行资源分配和调度的独立单元,资源包括CPU,内存空间,磁盘IO等等,同一个进程的所有线程共享该进程的全部资源进程与进程之间相互独立

1.3.2、什么是线程?

线程是CPU调度的最小单位,必须依赖进程而存在。

线程是进程的实体,是CPU调度和分派的基本单位,线程基本不拥有系统资源,但是拥有程序计数器、一组寄存器、栈等运行中不可少的资源,同一个进程中的线程共享进程所拥有的全部资源

1.4、 并发与并行

1.4.1、什么是并发

并发是指一个时间段内,有几个程序都在同一个CPU上运行,但任意一个时刻点上只有一个程序在处理机上运行。

多个线程 一个CPU

跟时间挂钩,单位时间内。

1.4.2、什么是并行

并行是指一个时间段内,有几个程序都在几个CPU上运行,任意一个时刻点上,有多个程序在同时运行,并且多道程序之间互不干扰。

多个线程 多个CPU

1.5、同步与异步

1.5.1、什么是同步

同步:在发出一个同步调用时,在没有得到结果之前,该调用就不返回,直到结果的返回。

好比我给朋友打电话,你要不接电话,我就一直打,这个过程啥也不干,就给你打电话,打到你接电话为止。

1.5.2、什么是异步

异步:在发出一个异步调用后,调用者不会立刻得到结果,该调用就返回了。

同样打电话,我先给你发个消息,告诉我有事找你,然后我就去干我自己的事情去了,等你看到消息给我回电话,当然,你也可以不回我电话。

二、多线程使用

2.1、创建多线程

2.1.1、实现Runnable接口
    public static class newRunnable implements Runnable {
        @Override
        public void run() {
            System.out.println("Runnable");
        }
    }

调用:

new Thread(new newRunnable()).start();
2.1.2、继承Thread类
    public static class newThread extends Thread {
        @Override
        public void run() {
            super.run();
            System.out.println("newThread");
        }
    }

调用:

new newThread().start();

1、Thread 是java里面对线程的抽象概念,我们通过new thread的时候,其实只是创建了一个thread实例,操作系统并没有和该线程挂钩,只有执行了start方法后,才是真正意义上启动了线程。

2、start() 会让一个线程进入就绪队列等待分配CPU,分到CPU后才调用 run() 方法,

3、start() 方法不能重复调用,否则会抛出 IllegalThreadStateException 异常。

2.1.3、实现Callable<V>接口

Callable接口 是在Java1.5开始提供,可以在任务执行结束后提供返回值。

public interface Callable<V> {
    /**
     * Computes a result, or throws an exception if unable to do so.
     *
     * @return computed result
     * @throws Exception if unable to compute a result
     */
    V call() throws Exception;
}

从源码可以看到,CallableRunnable 对比来看,不同点就在其call方法提供了返回值和进行异常抛出。

使用:

public static class newCallable implements Callable<String> {
        @Override
        public String call() throws Exception {
            System.out.println("newCallable");
            Thread.sleep(3000);
            return "java1.5后提供,可在任务执行结束返回相应结果";
        }
    }

对Callable的调用需要 FutureTask 这个类,这个类也是 Java1.5 以后提供

        FutureTask<String> futureTask = new FutureTask<>(new newCallable());
        futureTask.run();
        String result = null;
        try {
            result = futureTask.get();
        } catch (InterruptedException | ExecutionException e) {
            e.printStackTrace();
        }

执行结果:

可以看到,我们通过 FutureTaskget 方法 ,get 方法会进行阻塞,直到任务结束后,才将返回值进行返回。

2.2、终止线程

2.2.1、 自然终止

线程任务执行完成,则这个线程进行终止。

2.2.2、手动终止

暂停、恢复和停止操作对应在线程Thread的API就是suspend()、resume()和stop()。但是这些API是过期的,也就是不建议使用的,主要原因是方法的调用不能保证线程资源的正常释放,容易引起其他副作用的产生。

suspend() :在调用后,线程不会释放已经占有的资源(比如锁),而是占有着资源进入睡眠状态,这样容易引发死锁问题

stop() : 终结一个线程时不会保证线程的资源正常释放,通常是没有给予线程完成资源释放工作的机会,因此会导致程序可能工作在不确定状态下

真正安全的终止线程使用 interrupt() 方法

由于线程之间是协作式工作,所以在其他线程使用 interrupt() 终止某个线程时候,这个线程并不会立即终止,只是收到了终止通知,通过检查自身的中断标志位是否为被置为 True 再进行相应的操作,当然,这个线程完全可以不用理会。

中断标志位的判断:

1、isInterrupted()

判断线程是否中断,如果该线程已被中断则返回 true 否则返回 false

    /**
     * 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);
    }

2、interrupted()

判断线程是否中断,如果该线程已被中断返回 true,状态返回后该方法会清除中断标志位,重新置为 false,当第二次再次调用的时候又会返回 false ,(除非重新调用 interrupt()进行中断 )

    /**
     * Tests whether the current thread has been interrupted.  The
     * <i>interrupted status</i> of the thread is cleared by this method.  In
     * other words, if this method were to be called twice in succession, the
     * second call would return false (unless the current thread were
     * interrupted again, after the first call had cleared its interrupted
     * status and before the second call had examined it).
     *
     * <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 the current thread has been interrupted;
     *          <code>false</code> otherwise.
     * @see #isInterrupted()
     * @revised 6.0
     */
    public static boolean interrupted() {
        return currentThread().isInterrupted(true);
    }

下面通过一个Demo演示 isInterrupted()interrupted() 的区别

isInterrupted:

private static class UseThread extends Thread{
        public UseThread(String name) {
            super(name);
        }
        @Override
        public void run() {
            String threadName = Thread.currentThread().getName();
            System.out.println(threadName+" interrupt start flag  ="+isInterrupted());
            while(!isInterrupted()){
                System.out.println(threadName+" is running");
                System.out.println(threadName+" inner interrupt flag ="+isInterrupted());
            }
            System.out.println(threadName+" interrupt end flag ="+isInterrupted());
        }
    }

运行上面的程序:

开启线程,休眠一微秒后调用 interrupt() 进行中断

    public static void main(String[] args) throws InterruptedException {
        Thread endThread = new UseThread("test isInterrupted");
        endThread.start();
        Thread.sleep(1);
        endThread.interrupt();
    }

结果:

可以看到 UseThreadisInterrupted() 一直为 false ,当主线程执行 endThread.interrupt() 中断方法后,其中断标志被置为 true ,跳出循环,结束 run 方法。我们后续再调用 isInterrupted() 方法打印中断标志的值一直为 true,并没有更改。

interrupted():

我们简单改了一下代码:

 private static class UseThread extends Thread{
        public UseThread(String name) {
            super(name);
        }
        @Override
        public void run() {
            String threadName = Thread.currentThread().getName();

            while(!Thread.interrupted()){
                System.out.println(threadName+" is running");
            }
            System.out.println(threadName+" interrupted end flag ="+Thread.interrupted());
        }
    }

可以看到,run 方法里面一直循环执行,直到线程被中断,结束后我们再次调用了打印了 Thread.interrupted()

调用:

    public static void main(String[] args) throws InterruptedException {
        Thread endThread = new UseThread("test interrupted");
        endThread.start();
        Thread.sleep(1);
        endThread.interrupt();
    }

同样休眠一微秒后进行中断操作。

结果:

我们再分析一下,前面线程结束循环的条件是 Thread.interrupted()true , 但是当线程结束循环后,我们再次调用 Thread.interrupted() 方法,发现其值为又被置为 false ,说明 Thread.interrupted() 执行后,会清除 中断标志位,并将其重新置为 false

注意:处于死锁状态的线程无法被中断

如果一个线程处于了阻塞状态(如线程调用了thread.sleep、thread.join、thread.wait),则在线程在检查中断标示时如果发现中断标示为true,则会在这些阻塞方法调用处抛出InterruptedException异常,并且在抛出异常后会立即将线程的中断标示位清除,即重新设置为false。

我们在前面 isInterrupted 演示的 Demo中 进行修改

 private static class UseThread extends Thread {
        public UseThread(String name) {
            super(name);
        }

        @Override
        public void run() {
            String threadName = Thread.currentThread().getName();
            System.out.println(threadName + " interrupt start flag  =" + isInterrupted());
            while (!isInterrupted()) {
                try {
                    // 线程进行休眠3秒
                    Thread.sleep(3000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                    System.out.println("sleep  error=" + e.getLocalizedMessage());
                }
                System.out.println(threadName + " is running");
                System.out.println(threadName + " inner interrupt flag =" + isInterrupted());
            }
            System.out.println(threadName + " interrupt end flag =" + isInterrupted());
        }
    }

我们再执行前面的方法的时候,结果:

我们可以看到,即使抛了异常,但是线程依旧在执行,这个时候标志位还有没有置为true,所以我们要注意

抛出InterruptedException异常的时候 对中断标志位的操作,我们改一下代码,在catch中再次执行 interrupt()来中断任务

 try {
       // 线程进行休眠3秒
       Thread.sleep(3000);
    } catch (InterruptedException e) {
      e.printStackTrace();
      // 在catch方法中,执行interrupt() 方法中断任务。
      interrupt();
      System.out.println("sleep  error=" + e.getLocalizedMessage());
   }

结果:

三、线程之间共享和协作

3.1、 线程之间共享

前面说过,同一个进程的所有线程共享该进程的全部资源,共享资源就会导致一个问题,当多个线程同时访问一个对象或者一个对象的成员变量,可能会导致数据不同步问题,比如 线程A 对数据a进行操作,需要从内存中进行读取然后进行相应的操作,操作完成后再写入内存中,但是如果数据还没有写入内存中的时候,线程B 也来对这个数据进行操作,取到的就是还未写入内存的数据,导致前后数据同步问题。

为了处理这个问题,Java 中引入了关键字 synchronized ( 下一篇文章单独讲)。

3.2、线程之间的协作

线程之间可以相互配合,共同完成一项工作,比如线程A修改了某个值,这个时候需要通知另一个线程再执行后续操作,整个过程开始与一个线程,最终又再另一个线程执行,前者是生产者,后者就是消费者。

3.2.1、 nitify()、notifyAll()、wait() 等待/通知机制

是指一个线程A调用了对象Owait() 方法进入等待状态,而另一个线程B调用了对象Onotify()或者notifyAll()方法,线程A收到通知后从对象O的wait()方法返回,进而执行后续操作。上述两个线程通过对象O来完成交互,而对象上的wait()notify、notifyAll()的关系就如同开关信号一样,用来完成等待方和通知方之间的交互工作。

我们知道 Object 类是所有类的父类,而 Object 类中就存在相关方法

notify():

通知一个在对象上等待的线程,使其从wait方法返回,而返回的前提是该线程获取到了对象的锁,没有获得锁的线程重新进入WAITING状态。

notifyAll():

通知所有等待在该对象上的线程

wait()

调用该方法的线程进入 WAITING状态,只有等待另外线程的通知或被中断才会返回.需要注意,调用wait()方法后,会释放对象的锁

wait(long)

超时等待一段时间,这里的参数时间是毫秒,也就是等待长达n毫秒,如果没有通知就超时返回

wait (long,int)

对于超时时间更细粒度的控制,可以达到纳秒

下面通过案例说明,双十一的时候,你购买了三件商品,你在家焦急的等待,没事就刷新一下手机看商品快递信息,我们就来模拟一个快递信息的更新,这里以地点变化进行数据更新:

public class NwTest {
    // 发货地点
    public String location = "重庆";
    // 所有货物在不同一趟车上,货物到了下一站,分别更新对应的快递信息
    public synchronized void changeLocationNotify(String location) {
        this.location = location;
        this.notify();
    }
    // 所有货物在同一趟快递车上,货物到了下一站,全部信息更新。
    public synchronized void changeLocationNotifyAll(String location) {
        this.location = location;
        System.out.println("changeLocationNotifyAll");
        this.notifyAll();
    }

    public static class LocationThread extends Thread {
        public final NwTest mNwTest;
        public LocationThread(NwTest nwTest) {
            this.mNwTest = nwTest;
        }

        @Override
        public void run() {
            super.run();
            try {
                synchronized (mNwTest) {
                    System.out.println("LocationThread  current location : " + mNwTest.location);   
                    // 等待位置更新
                    mNwTest.wait();
                    String name = Thread.currentThread().getName();
                    // 获取当前商品的商家信息
                    System.out.println("LocationThread——>current thread name : " + name);
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            // 获取更新后的位置
            System.out.println("LocationThread  update location : " + mNwTest.location);
        }
    }

注意:

只能在同步方法或者同步块中使用 wait()notifyAll()notify() 方法

否则会抛 IllegalMonitorStateException 异常

调用:

    public static void main(String[] args) throws InterruptedException {
        NwTest nwTest = new NwTest();
        for (int x = 0; x < 3; x++) {
            new LocationThread(nwTest).start();
        }
        // 模拟三天后
        Thread.sleep(3000);
        // 通知单个商品信息进行更新
        nwTest.changeLocationNotify("合肥");
    }

我们启动了三个线程,模拟了你购买的三件货物,如果使用 notify() ,只是使单个商品进行信息更新

结果:

我们看到三个货物同时发货,其中 Thread_0 最先到达合肥,并进行了数据更新。

如果使用 notifyAll() ,所有商品快递信息都会刷新。

    public static void main(String[] args) throws InterruptedException {
        NwTest nwTest = new NwTest();
        for (int x = 0; x < 3; x++) {
            new LocationThread(nwTest).start();
        }
        Thread.sleep(3000);
        // 通知三件商品进行信息更新
        nwTest.changeLocationNotifyAll("合肥");
    }

结果:

这就是 notifyAll()notify()wait() 基本使用,其中 wait(long) 表示线程会等待 n 毫秒,如果这个时间段内没有收到 notifyAll() 或者 notify() 就自动执行后续方法。

根据上面的Demo,我们可以整理一下 等待和通知的标准范式

wait():

1)获取对象的锁。

2)根据判断条件调用 wait() 方法。

3)条件满足则执行对应的逻辑。

notify() 或者 notifyAll()

1)获得对象的锁。

2)改变条件,发送通知。

3)通知所有等待在对象上的线程。

以上主要是整理的多线程的一些基本概念,还有 notify()和wait() 的基本使用,关键字 synchronized 准备下一篇单独整理,后续计划整理线程池相关知识以及Android 中 AsyncTask 的源码分析,喜欢的话点个赞呗!

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章