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()方法。例如:
public class MyThread extends Thread { public void run() {
System.out.println("MyThread.run()");
}
}
在合适的地方启动线程如下:
MyThread myThread1 = new MyThread(); MyThread myThread2 = new MyThread(); myThread1.start(); myThread2.start();
2.实现Runnable接口方式实现多线程
如果自己的类已经extends另一个类,就无法直接extends Thread,此时,必须实现一个Runnable接口,如下:
public class MyThread extends OtherClass implements Runnable { public void run() { System.out.println("MyThread.run()"); } }
为了启动MyThread,需要首先实例化一个Thread,并传入自己的MyThread实例:
MyThread myThread = new MyThread(); Thread thread = new Thread(myThread); thread.start();
事实上,当传入一个Runnable target参数给Thread后,Thread的run()方法就会调用target.run(),参考JDK源代码:
public void run() { if (target != null) { target.run(); } }
3.使用Callable和Future创建线程
Callable接口提供了一个call()方法可以作为线程执行体,但call()方法比run()方法功能更为强大:call()方法可以有返回值;call()方法可以声明抛出异常。代码如下:
//实现Callable接口实现线程类public class callableThread implements Callable<Integer>{//实现call()方法作为线程执行体public Integer call(){int i = 0;for(; i < 100; i++){System.out.println(Thread.currentThread().getName() + “ 的循环变量i的值:” + i);}//call()方法可以有返回值return i;}public static void main(String [] args){//创建Callable对象callableThread ct= new callableThread();//使用FutureTask来包装Callable对象FutureTask<Integer> task = new FutureTask<Integer>(ct);for(int i = 0; i < 100; i++){System.out.println(Thread.currentThread().getName() + “ 的循环变量i的值:” + i);if(i ==20){//实质还是以Callable对象来创建并启动线程new Thread(task,有返回值的线程).start;}}try{//获取线程返回值System.out.println(“子线程的返回值:” + task.get());}catch(Exception ex){ex.printStackTrace();}}}
多线程通信(摘录自课本)
经典场景:用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,以便其他线程能够运行。然而,假如调度机制觉得我们的线程已拥有足够的时间,并跳转到另一个线程,就会发生同样的事情。也就是说,没有什么能防止调度机制重新启动我们的线程。
美文美图