Java进阶(四十一)多线程讲解

Java多线程讲解

前言

    接到菜鸟网络的电话面试,面试官让自己谈一下自己对多线程的理解,现将其内容整理如下。

线程生命周期

    Java线程具有五种基本状态

    新建状态(New):当线程对象创建后,即进入了新建状态,如:Thread t = new MyThread();

    就绪状态(Runnable):当调用线程对象的start()方法(t.start();),线程即进入就绪状态。处于就绪状态的线程,只是说明此线程已经做好了准备,随时等待CPU调度执行,并不是说执行了t.start()此线程立即就会执行;

    运行状态(Running):当CPU开始调度处于就绪状态的线程时,此时线程才得以真正执行,即进入到运行状态。注:就绪状态是进入到运行状态的唯一入口,也就是说,线程要想进入运行状态执行,首先必须处于就绪状态中;

    阻塞状态(Blocked):处于运行状态中的线程由于某种原因,暂时放弃对CPU的使用权,停止执行,此时进入阻塞状态,直到其进入到就绪状态,才有机会再次被CPU调用以进入到运行状态。根据阻塞产生的原因不同,阻塞状态又可以分为三种:

    1.等待阻塞:运行状态中的线程执行wait()方法,使本线程进入到等待阻塞状态;

    2.同步阻塞 -- 线程在获取synchronized同步锁失败(因为锁被其它线程所占用),它会进入同步阻塞状态;

    3.其他阻塞 -- 通过调用线程的sleep()或join()或发出了I/O请求时,线程会进入到阻塞状态。当sleep()状态超时、join()等待线程终止或者超时、或者I/O处理完毕时,线程重新转入就绪状态。

    死亡状态(Dead):线程执行完了或者因异常退出了run()方法,该线程结束生命周期。

注 同步阻塞的正解

JAVA多线程实现的三种方式

    JAVA多线程实现方式主要有三种:继承Thread类、实现Runnable接口、使用Callable、FutureTask实现有返回结果的多线程。其中前两种方式线程执行完后都没有返回值,只有最后一种是带返回值的。

1.继承Thread类实现多线程

    继承Thread类的方法尽管列为一种多线程实现方式,但Thread本质上也是实现了Runnable接口的一个实例,它代表一个线程的实例,并且,启动线程的唯一方法就是通过Thread类的start()实例方法。start()方法是一个native方法,它将启动一个新线程,并执行run()方法。这种方式实现多线程很简单,通过自己的类直接extends Thread,并复写run()方法,就可以启动新线程并执行自己定义的run()方法。例如:

 


  
  1. public class MyThread extends Thread {
  2. public void run() {
  3. System.out.println("MyThread.run()");
  4. }
  5. }

 

    在合适的地方启动线程如下:

 


  
  1. MyThread myThread1 = new MyThread();
  2. MyThread myThread2 = new MyThread();
  3. myThread1.start();
  4. myThread2.start();

 

2.实现Runnable接口方式实现多线程

    如果自己的类已经extends另一个类,就无法直接extends Thread,此时,必须实现一个Runnable接口,如下:

 


  
  1. public class MyThread extends OtherClass implements Runnable {
  2. public void run() {
  3. System.out.println("MyThread.run()");
  4. }
  5. }

 

    为了启动MyThread,需要首先实例化一个Thread,并传入自己的MyThread实例:

 


  
  1. MyThread myThread = new MyThread();
  2. Thread thread = new Thread(myThread);
  3. thread.start();

 

    事实上,当传入一个Runnable target参数给Thread后,Thread的run()方法就会调用target.run(),参考JDK源代码:

 


  
  1. public void run() {
  2. if (target != null) {
  3. target.run();
  4. }
  5. }

 

3.使用Callable和Future创建线程

    Callable接口提供了一个call()方法可以作为线程执行体,但call()方法比run()方法功能更为强大:call()方法可以有返回值;call()方法可以声明抛出异常。代码如下:

 


  
  1. //实现Callable接口实现线程类
  2. public class callableThread implements Callable<Integer>{
  3. //实现call()方法作为线程执行体
  4. public Integer call(){
  5. int i = 0;
  6. for(; i < 100; i++){
  7. System.out.println(Thread.currentThread().getName() + “ 的循环变量i的值:” + i);
  8. }
  9. //call()方法可以有返回值
  10. return i;
  11. }
  12. public static void main(String [] args){
  13. //创建Callable对象
  14. callableThread ct= new callableThread();
  15. //使用FutureTask来包装Callable对象
  16. FutureTask<Integer> task = new FutureTask<Integer>(ct);
  17. for(int i = 0; i < 100; i++){
  18. System.out.println(Thread.currentThread().getName() + “ 的循环变量i的值:” + i);
  19. if(i ==20){
  20. //实质还是以Callable对象来创建并启动线程
  21. new Thread(task,有返回值的线程).start;
  22. }
  23. }
  24. try{
  25. //获取线程返回值
  26. System.out.println(“子线程的返回值:” + task.get());
  27. }
  28. catch(Exception ex){
  29. ex.printStackTrace();
  30. }
  31. }
  32. }

 

多线程通信(摘录自课本)

    经典场景:用2个线程,这2个线程分别代表存款和取款。——现在系统要求存款者和取款者不断重复的存款和取款的动作,而且每当存款者将钱存入账户后,取款者立即取出这笔钱。不允许2次连续存款、2次连续取款。

传统的线程通信

    实现上述场景需要用到Object类提供的wait、notify和notifyAll三个方法,这3个方法并不属于Thread类。但这3个方法必须由同步监视器调用,可分为2种情况:

    A、对于使用synchronized修饰的同步方法,因为该类的默认实例this就是同步监视器,所以可以在同步中直接调用这3个方法。

    B、对于使用synchronized修改的同步代码块,同步监视器是synchronized后可括号中的对象,所以必须使用括号中的对象调用这3个方法

    传统方法概述:

    一、wait方法:导致当前线程进入等待,直到其他线程调用该同步监视器的notify方法或notifyAll方法来唤醒该线程。

    wait方法有3中形式:无参数的wait方法,会一直等待,直到其他线程通知;带毫秒参数的wait和微妙参数的wait,这2种形式都是等待时间到达后苏醒。调用wait方法的当前线程会释放对该对象同步监视器的锁定。

    二、notify:唤醒在此同步监视器上等待的单个线程。如果所有线程都在此同步监视器上等待,则会随机选择唤醒其中一个线程。只有当前线程放弃对该同步监视器的锁定后(用wait方法),才可以执行被唤醒的线程。

    三、notifyAll:唤醒在此同步监视器上等待的所有线程。只有当前线程放弃对该同步监视器的锁定后,才能执行唤醒的线程。

使用条件变量Condition控制协调

    如果程序不使用synchronized关键字来保证同步,而是直接使用Lock对象来保证同步,则系统中不存在隐式的同步监视器对象,也不能使用wait、notify、notifyAll方法来协调进程的运行。

    当使用Lock对象同步,Java提供一个Condition类来保持协调,使用Condition可以让那些已经得到Lock对象却无法继续执行的线程释放Lock对象,Condition对象也可以唤醒其它处于等待的线程,Condition将同步监视器(wait()、notify()、notifyAll())分解成截然不同的对象,以便通过将这些对象与Lock对象组合使用,为每个对象提供多个等待集(wait-set)。在这种情况下,Lock替代了同步方法和同步代码块,Condition替代同步监视器的功能。

    Condition实例实质上被绑定在一个Lock对象上,要获得特定的Lock实例的Condition实例,调用Lock对象的newCondition即可。    

使用阻塞队列(BlockingQueue)控制线程通信

    java中阻塞BlockingQueue 接口实现类中用的较多的通常是ArrayBlockingQueue,LinkedBlockingQueue.它们都是线程安全的.ArrayBlockingQueue以数组的形式存储,LinkedBlockingQueue以node节点的方式进行存储.

    开发中如果队列的插入操作比较频繁建议使用LinkedBlockingQueue,因为每个node节点都有一个前后指针,插入新元素仅需要变更前后的指针引用即可, ArrayBlockingQueue插入新元素,则新元素之后的元素数组下标位置都要发生变化,性能较差. 如果队列的读取操作比较频繁建议使用ArrayBlockingQueue, ArrayBlockingQueue通过数组下标直接能找到对应元素,LinkedBlockingQueue则需要遍历node链来找到元素.

    BlockingQueue 队列常用的操作方法:

    1.往队列中添加元素: add(), put(), offer()

    2.从队列中取出或者删除元素: remove() element()  peek()   pool()  take()

    队列添加新元素一般都是往队尾添加元素,

    offer()方法往队列添加元素如果队列已满直接返回false,队列未满则直接插入并返回true;

    add()方法是对offer()方法的简单封装.如果队列已满,抛出异常new IllegalStateException("Queue full");

    put()方法往队列里插入元素,如果队列已经满,则会一直等待直到队列为空插入新元素,或者线程被中断抛出异常.

    队列中取出或者删除元素都是针对队头的元素

    remove()方法直接删除队头的元素:

    peek()方法直接取出队头的元素,并不删除.

    element()方法对peek方法进行简单封装,如果队头元素存在则取出并不删除,如果不存在抛出异常NoSuchElementException()

    pool()方法取出并删除队头的元素,当队列为空,返回null;

    take()方法取出并删除队头的元素,当队列为空,则会一直等待直到队列有新元素可以取出,或者线程被中断抛出异常

    offer()方法一般跟pool()方法相对应, put()方法一般跟take()方法相对应.日常开发过程中offer()与pool()方法用的相对比较频繁.

sleep()和yield()的区别

    sleep()和yield()的区别:sleep()使当前线程进入停滞状态(阻塞态),失去锁的占有权,所以执行sleep()的线程在指定的时间内肯定不会被执行;yield()不会阻塞线程,它只是将该线程转入就绪态。yield()只是让当前线程暂停一下,让系统的线程调度器重新调度一次,完全可能的情况是:当某个线程调用了yield()方法暂停之后,线程调度器又将其调度出来重新执行。

    sleep方法使当前运行中的线程睡眠一段时间,进入不可运行状态(阻塞态),这段时间的长短是由程序设定的,yield方法使当前线程让出CPU占有权,但让出的时间是不可设定的。实际上,yield()方法对应了如下操作:先检测当前是否有相同优先级的线程处于同可运行状态,如有,则把CPU 的占有权交给此线程,否则,继续运行原来的线程。所以yield()方法称为“退让”,它把运行机会让给了同等优先级的其他线程。

    另外,sleep方法允许较低优先级的线程获得运行机会,但 yield()方法执行时,当前线程仍处在可运行状态(仍未失去锁),所以,不可能让出较低优先级的线程些时获得CPU占有权。在一个运行系统中,如果较高优先级的线程没有调用sleep方法,又没有受到I\O阻塞,那么,较低优先级线程只能等待所有较高优先级的线程运行结束,才有机会运行。

死锁与线程阻塞的区别

死锁

  所谓死锁: 是指两个或两个以上的进程在执行过程中,因争夺资源而造成的一种互相等待的现象,若无外力作用,它们都将无法推进下去。   

     那么为什么会产生死锁呢?

    1.因为系统资源不足。

    2.进程运行推进的顺序不合适。   

    3.资源分配不当。            

    学过操作系统的朋友都知道:产生死锁的条件有四个:

    1.互斥条件:所谓互斥就是进程在某一时间内独占资源。

    2.请求与保持条件:一个进程因请求资源而阻塞时,对已获得的资源保持不放。

    3.不剥夺条件:进程已获得资源,在末使用完之前,不能强行剥夺。

    4.循环等待条件:若干进程之间形成一种头尾相接的循环等待资源关系。

阻塞

    线程被堵塞可能是由下述五方面的原因造成的:

    (1) 调用sleep(毫秒数),使线程进入“睡眠”状态。在规定的时间内,这个线程是不会运行的。

    (2) 用suspend()暂停了线程的执行。除非线程收到resume()消息,否则不会返回“可运行”状态。

    (3) 用wait()暂停了线程的执行。除非线程收到nofify()或者notifyAll()消息,否则不会变成“可运行”。

    (4) 线程正在等候一些IO(输入输出)操作完成。

    (5) 线程试图调用另一个对象的“同步”方法,但那个对象处于锁定状态,暂时无法使用。

   亦可调用yield()(Thread类的一个方法)自动放弃CPU,以便其他线程能够运行。然而,假如调度机制觉得我们的线程已拥有足够的时间,并跳转到另一个线程,就会发生同样的事情。也就是说,没有什么能防止调度机制重新启动我们的线程。

美文美图

文章来源: shq5785.blog.csdn.net,作者:No Silver Bullet,版权归原作者所有,如需转载,请联系作者。

原文链接:shq5785.blog.csdn.net/article/details/52372937

(完)