线程用来解决多任务并发,使用同一块内存区域的资源,相对于多进程的是一种很轻量级的处理方案。还可以结合线程池来重用线程资源,来提升更高的性能和资源利用率。使用线程提升性能是有一些代价的:它会使程序变的复杂,如安全性和活动性问题。因为不同的线程共享相同的内存,一个线程完全有可能破坏另一个线程正在使用的变量和数据结构。还有可能两个线程太过于小心的等待资源的访问权限,却永远都等不到。这就会导致死锁。
运行线程
在java中想让线程运行有两种方式:
1.Thread类,你可以通过继承Thread类并覆盖run()方法。
Thread t = new Thread();
t.start();
然后执行start方法就可以启动一个线程来工作了。
2.Runnable接口,或者实现Runnable,然后也是在run()方法中执行你想要的操作。
Runnable r = new Runnable();
Thread t = new Thread(r);
t.start();
这里不太一样的是,Runnable需要通过Thread来启动。
至于到底用哪种比较好,这里没有明确规定。如果你有一个类需要执行线程,但是它已经有了父类,这时候就可以选择使用Runnable来执行线程操作,反之你两种方式都可以使用。
从线程返回信息
很多时候我们需要从线程结束之后拿到结果,但是往往线程之间的运行时间是不同步的,这就导致我们很大可能拿不到数据。我们可以使用循环检测线程时候返回数据,但是这样做明显是效率很低的,在这里我们可以使用两种方式来得到线程返回的数据:
1.回调来完成线程数据的返回,在线程内部注册一个监听,等线程结束之前再调用这个监听方法达到数据返回的目的。
比如:public void runThreadBack(Callback callback);
2.使用Future、Callable和Executor,将需要耗时的计算放进Callable实现类的call方法中,然后使用Executor来调用这个Callable,最后Executor会返回Future,通过Future的get就可以拿到数据。
比如:MyRunnable runnable = new MyRunnable();
ExecutorService service = Executors.newFixedThreadPool(2);
Future<Integer> future = service.submit(runable);
int result = future.get();
同步
如果两个线程同时访问一个资源,其中一个就必须等待另一个结束。如果其中一个没有等待,资源就可能遭到破坏。
要想使程序有同步的功能,我们可以使用以下几种方式来做到:
同步块:synchronized(object) 一旦有A线程使用了object的资源,在访问到这里的时候,如果有其它线程正在使用该资源,那么A线程将会等待。
同步方法:我们也可以在方法体加入同步关键字做到同样的效果:public synchronized void write(String msg);同步方法响应的也会带来一些问题:1.导致JVM的性能下降;2.大大增加了死锁的可能性;3.并不总是对象本身需要同步,如果只是该方法所属类的实例进行同步,这样就没法解决同步问题。
其它的同步代替法:
1.尽量使用局部变量来避免同步。
2.将其声明的字段标记为private和final,而且不提供任何改变他们的方法。
3.将非线程安全的类作为线程安全类的一个私有字段。
4.使用java.util.concurrent.atomic包中类,对于集合可以使用java.util.Collections里的方法来包装成线程安全的集合。
死锁
同步会导致另一个可能的问题就是死锁(deadlock)。如果两个线程需要独占访问统一的一个资源集,而每个线程分别有这些资源的不同子集锁,就会发生死锁。如果两个线程都不愿意放弃已经拥有的资源,就会进入无限停止状态。
要防止死锁,最重要的技术是避免不必要的同步。如果有其它方法可以确保线程安全,比如让对象不可变或保存对象的一个局部副本。如果确实要使用同步,要保持同步块尽可能小,而且尽量不要一次同步多个对象。
线程调度
当多个线程同时运行时,必须考虑线程调度问题。线程调度需要知道以下关键字:
优先级:不是所有线程创建时都是均等的。每个线程都有一个优先级,指定为一个0到10的整数。当有多个线程可以运行时,虚拟机通常只运行最高优先级的线程。在Java中最高级为10,最低级为0,默认为5。设置线程优先级的方式是:setPriority(int new Proiority)。
抢占:虚拟机有两种线程调度 -- 抢占式(preemptive)和协作式(cooperative)。抢占式线程调度器确定一个线程正常地轮到其CPU时间时,会暂停这个线程,将CPU控制权交给另外的线程。协作式线程调度器在将CPU交给其它线程之前,会等待正在运行的线程自己暂停。与抢占式线程调度器相比,协作式线程调度器的虚拟机更容易使线程陷入“饥饿”,因为一个高优先级的线程会独占CPU资源直到完成任务。
为了保证其它线程有机会运行,一个线程有10种方式可以暂停或指示它准备暂停:
1.可以对I/O阻塞
2.可以对同步对象阻塞
3.可以放弃
4.可以休眠
5.可以连接另一个线程
6.可以等待一个对象
7.可以结束
8.可以被更高优先级线程抢占
9.可以被挂起(废弃)
10.可以停止(废弃)
介于最后两种方式已经被废弃,因为它们可能会让对象处于不一致的状态,我们将介绍其它8种方法。
阻塞:有些时候线程必须停下来等待它没有的资源,就会发生阻塞。这经常出现在网络操作、文件读写上。线程进入一个同步方法或代码块时也会阻塞。这种阻塞的方式暂停线程并不会释放该线程已经拥有的资源锁。对于I/O操作它可以等到不阻塞或者出现IO异常就会释放该锁。但是第二种情况它将会永远等待下去,如果它持有另一个线程需要的资源,那么将会出现死锁。
放弃:线程可以通过调用Thread.yield()静态方法来让线程放弃控制权,这个方法会通知虚拟机,如果有另一个线程准备运行,可以运行该线程。放弃也不会释放资源锁,所以在放弃之前避免做任何同步操作,否则将影响后面的线程。
休眠:休眠是更有力的放弃方式。放弃只是表示线程愿意暂停,让其它有相同优先级的线程有机会运行,而进入休眠的线程有所不同,不管有没有其它线程准备运行,休眠线程都会暂停,这样一来,不止是相同优先级的线程,还会给较低优先级的线程运行的机会。休眠也不会释放已经拿到的锁。使用Thread.sleep(long milliseconds)可以休眠。如果其它线程在该线程休眠时间结束之前完成了任务,其它线程可以使用interrupt()来唤醒该线程。会让休眠中的线程得到一个InterruptedException异常。
连接:一个线程可能需求另一个的结果,比如线程A需要线程B的计算结果做展示,那么就可以使用join()方法来连接另一个线程,等待它的执行结果。
一个排序的例子:
int []array = new int[10000];
for(int i=0;i<array.length;i++){
array[i] = Math.random();
}
AddThread t = new AddThread(array);
t.start();
try{
t.join();
System.out.println("Maximum: "+array[array.length-1]);
}catch(InterruptedException ex){
}
如果有其它线程调用了interrupt()方法,它就被中断,那么它将会执行catch里的异常,就不会得到排序结果。
等待一个对象:线程可以等待(wait)一个它锁定的对象。在等待时,他会释放这个对象的锁并暂停,直到它得到其它线程的通知。等待一个对象可以使用wait()方法,该方法是Object的方法,因此可以在任何类的任何对象上调用这些方法。如果有其它线程在这个线程所等待的对象上调用notify()或notifyAll()方法时,就会发生通知。一旦等待线程得到通知,它就试图重新获得所等待对象的锁。要等待特定的对象,必须使用synchronized获得这个对象的锁。
线程池和Executor
向程序添加多个线程会极大的提升性能,尤其是I/O受限的程序,不过线程自身也存在开销。启动一个线程时,以及线程撤销后进行清理时,都需要虚拟机进行大量工作。虽然使用线程有助于高效利用计算机有限CPU资源,但是所能提供的资源毕竟有限。一旦已经生成足够多的线程来使用计算机所有可用的空闲时间,那么再生成更多的线程只会将MIPS和内存浪费在线程管理上。
为了解决线程开销问题,我们可以使用java.util.concurrent中的Exctutors类,使用该类可以非常容易地建立线程池。
比如:ExecutorService pool = Executors.newFixedThreadPool(10);
for(int i=0;i<10;i++){
Runnable runnable = new MyRunnable();
//执行这个线程
pool.submit(runnable);
}
// 当线程池没有任务时,就关闭
pool.shutdown();