首页 > 代码库 > 多线程总结
多线程总结
1、继承Thread和实现Runnable的不同
先看下Thread类 start方法的源码
public synchronized void start() { if (started) throw new IllegalThreadStateException(); started = true; group.add(this); start0(); }
该源码告诉我们 通过继承Thread来产生的对象 只能产生一个线程 即只能调用一次start方法
第二次调用时 由于started=true 会直接抛出 IllegalThreadStateException异常
那么如果要在一个实例上产生多个线程(多个线程共同访问同一实例的一些共同资源),我们应该如何做呢?这就是Runnable接口给我们带来的伟大的功能.
class R implements Runnable{ private int x = 0; public void run(){ for(int i=0;i<100;i++){ try{ Thread.sleep(10); }catch(Exception e){} System.out.println(x++); } }}
正如它的名字一样,Runnable的实例是可运行的,但它自己并不能直接运行,它需要被Thread对象来
包装才行运行:
public class Test { public static void main(String[] args) throws Exception{ new Thread(new R()).start(); }}
当然这个结果和mt.start()没有什么区别.但如果我们把一个Runnable实例给Thread对象多次包装,我们就可以看到它们实际是在同一实例上启动线程:
public class Test { public static void main(String[] args) throws Exception{ R r = new R(); for(int i=0;i<10;i++) new Thread(r).start(); }}
x是实例对象,但结果是x被加到了999,说明这10个线程是在同一个r对象上运行的.请大家注意,因为这个例子是在单CPU上运行的,所以没有对多个线程同时操作共同的对象进行 同步.这里是为了说明的方便而简化了同步,而真正的环境中你无法预知程序会在什么环境下运行,所以一定要考虑同步.
到这里我们做一个完整的例子来说明线程产生的方式不同而生成的线程的区别:
class MyThread extends Thread{ public int x = 0; public void run(){ System.out.println(++x); }}class R implements Runnable{ private int x = 0; public void run(){ System.out.println(++x); }}public class Test { public static void main(String[] args) throws Exception{ for(int i=0;i<10;i++){ Thread t = new MyThread(); t.start(); } Thread.sleep(10000);//让上面的线程运行完成 R r = new R(); for(int i=0;i<10;i++){ Thread t = new Thread(r); t.start(); } }}
上面10个线程对象产生的10个线程运行时打印了10次1.
下面10个线程对象产生的10个线程运行时打印了1到10.
我们把下面的10个线程称为同一实例(Runnable实例)的多个线程.
2、线程的interrupt()方法,interrupted()和isInterrupted()]
interrupt()方法:只是改变了它的中断状态.使当前线程的状态变以中断状态,如果没有其它影响,线程还会自己继续执行.只有当线程执行到sleep,wait,join等方法时,或者自己检 查中断状态而抛出异常的情况下,线程才会抛出异常.
interrupted()方法:会检查当前线程的中断状态,如果为"被中断状态"则改变当前线程为"非中断状态"并返回true,如果为"非中断状态"则返回false,它不仅检查当前线程是否为 中断状态,而且在保证当前线程回来非中断状态,所以它叫"interrupted",是说中断的状态已经结束(到非中断状态了).
isInterrupted()方法:则仅仅检查线程对象对应的线程是否是中断状态,并不改变它的状态.
3、sleep(),join(),yield()方法
sleep()方法中是类方法,也就是对当前线程而言的,程序员不能指定某个线程去sleep,只能是当前线程执行到sleep()方法时.原则:[在同步方法中尽量不要调用线程的sleep()法], 或者简单说,对于一般水平的程序员你基本不应该调用sleep()方法.
join()方法,在一个线程对象上调用join方法,是当前线程等待这个线程对象对应的线程结束,比如有两个工作,工作A要耗时10秒钟,工作B要耗时10秒或更多.我们 在程序中先 生成一个线程去做工作B,然后做工作A.
new B().start();//做工作B A();//做工作A 工作A完成后,下面要等待工作B的结果来进行处理.如果工作B还没有完成我就不能进行下面的工作C,所以 B b = new B(); b.start();//做工作B A();//做工作A b.join();//等工作B完成. C();//继续工作C.
原则:[join是测试其它工作状态的唯一正确方法],我见过很多人,甚至有的是博士生,在处理一项工作时如果另一项工作没有完成,说让当前工作线程sleep(x),我问他,你这个x是如 何指定的,你怎么知道是100毫秒而不是99毫秒或是101毫秒?其实这就是OnXXX事件的实质,我们不是要等多长时间才去做什么事,而是当等待的工作正好完成的时候去做.
yield()方法也是类方法,只在当前线程上调用,理由同上,它主是让当前线程放弃本次分配到的时间片原则:[不是非常必要的情况下,没有理由调用它].调用这个方法不会提高任何效 率,只是降低了CPU的总周期.
4、[wait(),notify()/notifyAll()]
关于这两个方法,有很多的内容需要说明.在下面的说明中可能会有很多地方不能一下子明白,但在看完本节后,即使不能完全明白,你也一定要回过头来记住下面的两句话:
[wait(),notify()/notityAll()方法是普通对象的方法(Object超类中实现),而不是线程对象的方法]
[wait(),notify()/notityAll()方法只能在同步方法中调用]
5、 [synchornized关键字]
把一个单元声明为synchronized,就可以让在同一时间只有一个线程操作该方法.
有人说synchornized就是一把锁,事实上它确实存在锁,但是是谁的锁,锁谁,这是一个非常复杂的问题.
每个对象只有一把监视锁(monitor lock),一次只能被一个线程获取.当一个线程获取了这一个锁后,其它线程就只能等待这个线程释放锁才能再获取.
那么synchronized关键字到底锁什么?得到了谁的锁?
对于同步块,synchronized获取的是参数中的对象锁:
synchronized(obj){ //............... }
线程执行到这里时,首先要获取obj这个实例的锁,如果没有获取到线程只能等待.如果多个线程执行到这里,只能有一个线程获取obj的锁,然后执行{}中的语句,所以,obj对象的作
用范围不同,控制程序不同.
假如:
public void test(){ Object o = new Object(); synchronized(obj){ //............... } }
这段程序控制不了任何,多个线程之间执行到Object o = new Object();时会各自产生一个对象然后获取这个对象有监视锁,各自皆大欢喜地执行.
而如果是类的属性:
class Test{ Object o = new Object(); public void test(){ synchronized(o){ //............... } } }
所有执行到Test实例的synchronized(o)的线程,只有一个线程可以获取到监视锁.
有时我们会这样:
public void test(){ synchronized(this){ //............... } }
那么所有执行Test实例的线程只能有一个线程执行.而synchornized(o)和synchronized(this)的范围是不同的,因为执行到Test实例的synchornized(o)的线程等待时,其它线
程可以执行Test实例的synchronized(o1)部分,但多个线程同时只有一个可以执行Test实例的synchornized(this).
而对于
synchronized(Test.class){ //............... }
这样的同步块而言,所有调用Test多个实例的线程赐教只能有一个线程可以执行.
[synchronized方法]
如果一个方法声明为synchronized的,则等同于把在为个方法上调用synchronized(this).
如果一个静态方法被声明为synchronized,则等同于把在为个方法上调用synchronized(类.class).
现在进入wait方法和notify/notifyAll方法.这两个(或叫三个)方法都是Object对象的方法,而不是线程对象的方法.如同锁一样,它们是在线程中调用某一对象上执行的.
class Test{ public synchronized void test(){ //获取条件,int x 要求大于100; if(x < 100) wait(); } }
这里为了说明方法没有加在try{}catch(){}中,如果没有明确在哪个对象上调用wait()方法,则为this.wait();
假如:Test t = new Test();现在有两个线程都执行到t.test();方法.其中线程A获取了t的对象锁,进入test()方法内.这时x小于100,所以线程A进入等待.
当一个线程调用了wait方法后,这个线程就进入了这个对象的休息室(waitset),这是一个虚拟的对象,但JVM中一定存在这样的一个数据结构用来记录当前对象中有哪些程线程在
等待.
当一个线程进入等待时,它就会释放锁,让其它线程来获取这个锁.
所以线程B有机会获得了线程A释放的锁,进入test()方法,如果这时x还是小于100,线程B也进入了t的休息室.
这两个线程只能等待其它线程调用notity[All]来唤醒.
但是如果调用的是有参数的wait(time)方法,则线程A,B都会在休息室中等待这个时间后自动唤醒.
[为什么真正的应用都是用while(条件)而不用if(条件)]
在实际的编程中我们看到大量的例子都是用while(x < 100) wait();go();而不是用if,为什么呢?
在多个线程同时执行时,if(x <100)是不安全的.因为如果线程A和线程B都在t的休息室中等待,这时另一个线程使x==100了,并调用notifyAll方法,线程A继续执行下面的go().而
它执行完成后,x有可能又小于100,比如下面的程序中调用了--x,这时切换到线程B,线程B没有继续判断,直接执行go();就产生一个错误的条件,只有while才能保证线程B又继续
检查一次.
[notify/notifyAll方法]
这两个方法都是把某个对象上休息区内的线程唤醒,notify只能唤醒一个,但究竟是哪一个不能确定,而notifyAll则唤醒这个对象上的休息室中所有的线程.
一般有为了安全性,我们在绝对多数时候应该使用notifiAll(),除非你明确知道只唤醒其中的一个线程.
那么是否是只要调用一个对象的wait()方法,当前线程就进入了这个对象的休息室呢?事实中,要调用一个对象的wait()方法,只有当前线程获取了这个对象的锁,换句话说一定要在
这个对象的同步方法或以这个对象为参数的同步块中.
class MyThread extends Thread{ Test t = new Test(); public void run(){ t.test(); System.out.println("Thread say:Hello,World!"); }}public class Test { int x = 0; public void test(){ if(x==0) try{ wait(); }catch(Exception e){} } public static void main(String[] args) throws Exception{ new MyThread().start(); }}
这个线程就不会进入t的wait方法而直接打印出Thread say:Hello,World!.而如果改成:
public class Test { int x = 0; public synchronized void test(){ if(x==0) try{ wait(); }catch(Exception e){} } public static void main(String[] args) throws Exception{ new MyThread().start(); }}
我们就可以看到线程一直等待,注意这个线程进入等待后没有其它线程唤醒,除非强行退出JVM环境,否则它一直等待.
所以请记住:[线程要想调用一个对象的wait()方法就要先获得该对象的监视锁,而一旦调用wait()后又立即释放该锁]