Java线程图文总结
实现方式
简单介绍一下Java多线程实现方式,有以下三种:
1、继承Thread类
2、实现Runnable接口
3、使用ExecutorService、Callable、Future实现有返回结果的多线程
区别是前两种执行完之后不带返回值,最后一种带返回值,其中最常用为前两种。
线程的状态
java线程的整个生命周期有5个状态:新建,就绪,运行中,阻塞,结束。
5个状态之间的关系将结合下图理解:
上图为java线程生命周期期间的各种命运,下面介绍常见的几种命运。
命运一:
新线程创建成功,调用start()进入就绪状态,即进入待运行的线程池中等待,等待获取CPU的使用权。当获得CPU使用权,该线程从就绪状态进入运行状态。运行过程中,运气好的,一次运行就把所要执行的任务执行完毕,线程结束;命运不好的,运行中途被CPU暂停运行,重新回到就绪状态,等待分配,然后再等待进入运行期,直到最后运行完毕,最后结束。
命运二:
新线程创建成功,进入就绪状态,获取了CPU使用权,处于运行状态。这里意外出现,该线程执行了sleep、yield、join三者其中一个命令。sleep、join需要被暂停执行一段时间,线程进入阻塞状态。休息时间到,再重新进入就绪状态;而yield是从运行状态直接跳会就绪状态。当到了就绪状态后再重新等待CPU调度,重新进入运行期。run -> block -> run -> block.... 此种状态会持续,一直到该线程的任务执行完毕。sleep、yield、join三者区别如下:
sleep:CPU暂停当前线程运行,同时让就绪状态(待运行池中)优先级较低的一个线程运行。当前线程被暂停后,会处于阻塞状态n秒(n由开发人员设定),时间到了之后,会自动回到就绪状态,等待CPU重新调度,重新从刚刚暂停的地方运行。注意,若代码块中包含了对象的锁,在睡眠的过程中是不会释放掉对象的锁的,其他线程是不能访问到共享数据的。
join:将指定的线程执行完成后,再运行当前线程剩下的任务。典型的例子是将两个交替执行的线程合并顺序执行。请结合下面代码理解:
public static void main(String[] args) throws IOException { final Thread aThread = new Thread(new Runnable() { @Override public void run() { for (int i = 0; i < 5; i++) { System.out.println("a:" + i); } } }); Thread bThread = new Thread(new Runnable() { @Override public void run() { try { aThread.join(); } catch (InterruptedException e) { } for (int i = 0; i < 5; i++) { System.out.println("b:" + i); } } }); bThread.start(); aThread.start(); try { bThread.join(); } catch (InterruptedException e) { } for (int i = 0; i < 5; i++) { System.out.println("c:" + i); } }
输出结果:
a:0 a:1 a:2 a:3 a:4 b:0 b:1 b:2 b:3 b:4 c:0 c:1 c:2 c:3 c:4
如果把代码中的join部分去掉,就不能保证a、b、c的输出顺序。
yield:实质是让当前正在运行的线程直接回到就绪状态,而不用进入阻塞状态等待,且只会选择优先级相同的线程进入运行状态。若待运行的线程池中,没有跟当前线程优先级相同的线程,或者当前线程又被选中运行,则当前线程还是会继续运行,会误造成yield无法达到目的的效果。
命运三:
线程a成功进入创建、就绪、运行流程,运行过程中,需要访问资源i,而此时资源i正在被另外的线程b访问并且上锁了,此时线程a就会暂停运行,进入资源i的锁池,等待线程b释放资源i的锁。当线程a获得资源i的锁时,会从资源i的锁池中进入就绪状态(待运行池),等待调度。以下为示例代码:
public static void main(String[] args) throws IOException { final Object obj = 1; Thread aThread = new Thread(new Runnable() { @Override public void run() { synchronized(obj){ for (int i = 0; i < 3; i++) { System.out.println("线程a在使用obj..."); try { Thread.sleep(500); } catch (InterruptedException e) { } } } } }); Thread bThread = new Thread(new Runnable() { @Override public void run() { synchronized(obj){ for (int i = 0; i < 3; i++) { System.out.println("线程b在使用obj..."); try { Thread.sleep(500); } catch (InterruptedException e) { } } } } }); bThread.start(); aThread.start(); }
输出结果是:
线程b在使用obj... 线程b在使用obj... 线程b在使用obj... 线程a在使用obj... 线程a在使用obj... 线程a在使用obj...
两个线程都start()之后,两个线程随机先后访问到obj对象,有可能是a先访问obj,有可能是b先访问obj。我的结果就是b先访问obj,拿到obj的锁,之后线程a无法立即访问到obj,a就进入obj的锁池中等待;当b中被锁的代码块跑完且释放锁,a拿到obj的锁,重新进入就绪状态,等待分配运行。
命运四:
线程a成功的创建、就绪、运行,运行过程中调用obj.wait(),a就从run状态进入到obj的等待池,等待其他线程调用obj.notify;当其他线程调用notify之后,线程a从obj的等待池中,进入到obj的锁池,等待获得锁以重新进入就绪状态恢复运行。下面具体解析一下wait、notify、notifyAll的作用:
wait:在已经获得obj的锁的前提下,主动释放obj的锁,释放后当前线程会进入obj的等待池,即开始休眠。另外,当调用了wait之后,并不是立即释放对象obj的锁,而是在相应的synchronized()语句块执行结束后才真正释放。
notify的作用是从对象obj的等待池中随机抽取一个线程放到对象obj的锁池中,让该线程继续在锁池中等待锁,成功拿到锁后,等待分配执行。
notifyAll的作用跟notify类似,只不过是把对象obj的等待池中的所有的线程全部取出,放到对象obj的锁池中。
需要明确的一点:wait、notify、notifyAll均为在当前线程获取对象的锁的前提下执行的,所以这三个操作都必须在synchronized()块中执行。
举个例子:线程a、线程b、线程c需要协同工作,线程b、c的任务需要在a线程的任务完成后才能开始。那就是说a在完成任务后,需要通知b和c恢复工作。这种情况就可通过wait,notifyAll来实现协同工作。请看以下代码:
public static void main(String[] args) { final Object object = new Object(); Thread a = new Thread() { public void run() { System.out.println("a start"); try { Thread.sleep(2000); } catch (InterruptedException e) { } synchronized (object) { System.out.println("a finish, notify b and c"); object.notifyAll(); //wait,notify,notifyAll必须在synchronized块里面使用 } } }; Thread b = new Thread() { public void run() { synchronized (object) { System.out.println("b is starting, waiting for a finish..."); try { object.wait(); } catch (InterruptedException e) { } System.out.println("b end"); } } }; Thread c = new Thread() { public void run() { synchronized (object) { System.out.println("c is starting, waiting for a finish..."); try { object.wait(); } catch (InterruptedException e) { } System.out.println("c end"); } } }; a.start(); b.start(); c.start(); }
其中一次的输出结果:
a start b is starting, waiting for a finish... c is starting, waiting for a finish... a finish, notify b and c c end b end
因为线程在锁池中是被随机抽取的,所以不可能保证b,c哪个先运行。上面的结果就是c先被抽取。
上述全部讨论的前提是线程运行中没有遇到exception的情况,若遇上了exception,就直接end。
以上为本人对Java线程的理解,是基于线程的浅层部分展开讨论,欢迎指正。若想深入了解线程,可以看看java.util.concurrent包下的类。本人之前写过一个基于多线程实现的远程监控程序,有兴趣可参考一下:
http://my.oschina.net/ericquan8/blog/383882