重走JAVA之路(六):你应该要知道的线程调度

作为Android开发者,老实说,平常关于一些线程调度的方法,用的确实不多,可能用的最多的也就是sleep作为一个休眠延时的操作,但是既然是Java之路,那就必须把那些东西拎出来说一说了,也是加强大家对线程的理解程度以及在处理线程中应该注意的问题。

1.join() 等待线程终止

这个方法大家可能用的不多,我们想象一个场景: 主线程生成并起动了子线程,如果子线程里要进行大量的耗时的运算,主线程往往将于子线程之前结束,但是如果主线程处理完其他的事务后,需要用到子线程的处理结果,也就是主线程需要等待子线程执行完成之后再结束
,这个时候我们第一想法可能是要不然在子线程中处理完之后,用Handler把消息传到主线程再处理?这样往往比较麻烦,这个时候就可以用join方法来实现

public class TestClass {
    public static void main(String []agrs){
       Thread thread = new Thread(new Runnable() {
           @Override
           public void run() {
               System.out.println("child thread start");
               try {
                   //模拟耗时操作
                   Thread.sleep(3000);
               } catch (InterruptedException mE) {
                   mE.printStackTrace();
               }
               System.out.println("child thread over");
           }
       });
       thread.start();
        try {
            thread.join();
        } catch (InterruptedException mE) {
            mE.printStackTrace();
        }
        System.out.println("main thread call");
    }
}
复制代码

运行,打印一波,可以看到调用join方法之后,主线程就可以在主线程执行完成之后,处理逻辑了

child thread start
child thread over
main thread call
Process finished with exit code 0
复制代码

2.wait()/notifyAll—–经典的生产者消费者问题

话不多说,直接上代码

public class Model {
    //为了触发阻塞状态,这里把最大容量设置为1
    public static final int MAX_SIZE = 1;
    //存储数据的集合
    public static LinkedList<Integer> list = new LinkedList<>();

    class Producer implements Runnable {
        @Override
        public void run() {
            synchronized (list) {
                //仓库容量已经达到最大值
                while (list.size() == MAX_SIZE) {
                    System.out.println(Thread.currentThread().getName() + " no need to produce! repertory is full");
                    try {
                        list.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                list.add(1);
                System.out.println( Thread.currentThread().getName() + " produce,current repertory is " + list.size());
                list.notifyAll();
            }
        }
    }

    class Consumer implements Runnable {

        @Override
        public void run() {
            synchronized (list) {
                while (list.size() == 0) {
                    System.out.println(Thread.currentThread().getName() + " no product to consume! repertory is empty ");
                    try {
                        list.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                list.removeFirst();
                System.out.println(Thread.currentThread().getName() + " consume,current repertory is " + list.size());
                list.notifyAll();
            }
        }
    }

}
复制代码

主要代码就是调用wait/notifyAll方法,分别在极限情况时,对线程进行挂起以及唤醒,消费者和生产者开启10个线程来测试一波

public class TestClass {
    public static void main(String []agrs){
       Model model = new Model();
       Model.Producer producer = model.new Producer();
       Model.Consumer consumer = model.new Consumer();
        for (int i = 0; i < 10; i++) {
            Thread proThread  = new Thread(producer);
            proThread.start();
            Thread conThread = new Thread(consumer);
            conThread.start();
        }
    }
}
复制代码
Thread-0 produce,current repertory is 1
Thread-2 no need to produce! repertory is full
Thread-1 consume,current repertory is 0
Thread-2 produce,current repertory is 1
Thread-3 consume,current repertory is 0
Thread-5 no product to consume! repertory is empty 
Thread-4 produce,current repertory is 1
Thread-5 consume,current repertory is 0
Thread-7 no product to consume! repertory is empty 
Thread-6 produce,current repertory is 1
Thread-7 consume,current repertory is 0
Thread-8 produce,current repertory is 1
Thread-9 consume,current repertory is 0
Thread-10 produce,current repertory is 1
Thread-11 consume,current repertory is 0
Thread-12 produce,current repertory is 1
Thread-13 consume,current repertory is 0
Thread-14 produce,current repertory is 1
Thread-15 consume,current repertory is 0
Thread-16 produce,current repertory is 1
Thread-17 consume,current repertory is 0
Thread-18 produce,current repertory is 1
Thread-19 consume,current repertory is 0

Process finished with exit code 0
复制代码

可以看到,当Thread-2要去生产时,发现此时仓库以及满了,此时调用wait方法,释放,同时线程阻塞,注意,这里线程并不会结束掉,只是出于挂起状态,当下次被唤醒时,会沿着wait方法后面继续执行,在第四行也可以看到,当Thread-1消费了的时候,会调用notifyAll,此时唤醒所有在锁池的对象,重新竞争获取锁,此时Thread-2又开始生产了

这种方法现象在Java中很常见,比如上篇线程池文章也有提到过,里面使用的阻塞队列底层采用也是类似的机制,核心线程不会被回收被挂起,当有任务来时,唤醒线程去执行,有兴趣的可以去看看 重走JAVA之路(五):面试又被问线程池原理?教你如何反击

再次总结一下:

  • 如果一个线程调用了wait方法,那么该线程首先需要获取到这个对象的锁(换句话说,一个线程如果调用了某个方法的wait方法,那么该wait方法必须是在synchronized方法中的)
  • 如果一个线程调用了wait方法,那么当前线程就会释放掉线程的锁。(这个是wait和sleep方法不同的地方)
  • java中一个对象,会有两个池(锁池,等待池),如果一个线程调用了wait方法,那么该线程进入该对象的等待池中(释放锁),如果未来的某一刻,另外一个线程调用了这个对象的notify方法,或者notifyAll,那么在等待池中的线程就会起来进入该对象的锁池中,参与到获取锁的竞争当中,如果获取锁成功,将沿着wait方法之后的代码执行(这就是代码中,使用while来判断状态,而不是if)。
  • notify和notifyAll方法并不会去释放当前锁对象,而是通过moniterexist来释放,也就是说,当前所述的代码块在执行结束之后,回去释放掉锁,只有在锁被释放掉之后,等待池中的线程进入到锁池,去竞争锁资源,所以一般notifyAll会防止同步代码的最后边

我们知道android是基于消息机制的,像之前的一个问题 为什么Looper.loop()死循环不会导致ANR一样
,主线程从队列中读取消息,当没有消息时,主线程阻塞,让出CPU,当消息队列中有消息时,唤醒主线程,接着处理数据,所以 Looer.loop()方法可能会引起主线程的阻塞,但只要它的消息循环没有被阻塞,能一直处理事件就不会产生ANR异常。

3.interrupt() 停止线程

当需要终止一个线程时,Java给我们提供了2中方法,stop/interrupt,前者已经被废弃了,也是不提倡调用的, 一调用该方法,被stop的线程会马上会释放所有获取的锁并在线程的run()方法内,任何一点都有可能抛出ThreadDeath Error,包括在catch或finally语句中,那么很容易照成被同步的数据没有被正确的处理完,那么其它线程在读取时就会得到脏数据

这里主要讲解interrupt方法, 首先我们要明白一点:

调用interrupt()方法,立刻改变的是中断状态,但如果不是在阻塞态,就不会抛出异常;如果在进入阻塞态后,中断状态为已中断,就会立刻抛出异常,什么叫阻塞态呢,大概就是调用了sleep,join,wait这几个方法,其实在源码方法注释上面也可以看到这些解释,如果是非阻塞态的话,那其实这个方法是不起作用的,什么,不信?那来测试下

public class TestClass {
    public static void main(String []agrs){
       Thread thread = new Thread(new Runnable() {
           @Override
           public void run() {
                while (true){
                    System.out.println("run");
                }
           }
       });
       thread.start();
       thread.interrupt();
    }
}
复制代码
run
run
run
run
run
run
run
复制代码

果然,是不起作用的,那我们再加上阻塞状态sleep,试一下

public class TestClass {
    public static void main(String []agrs){
       Thread thread = new Thread(new Runnable() {
           @Override
           public void run() {
                while (true){
                    System.out.println("run");
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException mE) {
                        mE.printStackTrace();
                        return;
                    }
                }
           }
       });
       thread.start();
       thread.interrupt();
    }
}
复制代码
run
java.lang.InterruptedException: sleep interrupted
	at java.lang.Thread.sleep(Native Method)
	at com.example.hik.lib.MyClass$1.run(MyClass.java:12)
	at java.lang.Thread.run(Thread.java:748)
复制代码

可以看到,在Thread.sleep的方法,抛出了异常,同时return掉,此时才是停止了线程,我们根据捕获异常实现逻辑,如果无法确定逻辑,那就直接抛出,由上层去处理。

4.总结

需要注意的一点,wait/notify是Object的方法,其他是Thread的方法,因为每个对象都有内置锁, 主要目的还是理解下线程中的一些状态以及阻塞状态的本质希望能够帮助到大家,如有疑问或者错误,欢迎一起讨论

原文 

https://juejin.im/post/5ca30c1ae51d45699466a58a

本站部分文章源于互联网,本着传播知识、有益学习和研究的目的进行的转载,为网友免费提供。如有著作权人或出版方提出异议,本站将立即删除。如果您对文章转载有任何疑问请告之我们,以便我们及时纠正。

PS:推荐一个微信公众号: askHarries 或者qq群:474807195,里面会分享一些资深架构师录制的视频录像:有Spring,MyBatis,Netty源码分析,高并发、高性能、分布式、微服务架构的原理,JVM性能优化这些成为架构师必备的知识体系。还能领取免费的学习资源,目前受益良多

转载请注明原文出处:Harries Blog™ » 重走JAVA之路(六):你应该要知道的线程调度

赞 (0)
分享到:更多 ()

评论 0

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址