并发概念
并发用来提高运行在单处理器上的程序的性能。
这听起来有些违背直觉。如果有多个CPU处理器,那么我们让不同CPU并发处理程序一定会让速度变快。但是我们只有一个处理器,并发看起来只会增加上下文切换的开销时间。真的是这样吗?让这个问题的答案反转的是:阻塞。如果我们在执行一段代码中,有一处发生了阻塞,我们只能将整个程序停下来。如果我们采用并发的方式,即使这一处发生了阻塞,其他的任务还可以继续执行,直到程序结束,最后的情况只是一处阻塞,结果还不算太坏。
事实上,从性能的角度看,如果没有任务会阻塞,那么在单处理器上使用并发就没有任何意义。 ——Bruce Eckel
实现并发最直接的方式就是我们的操作系统做的那样,使用进程。在多任务操作系统中可以通过周期性将CPU从一个进程切换到另一个进程,来实现同时运行多个进程的效果。尽管这样会让进程看起来执行的停停歇歇,但是互不干扰的进程还是非常吸引人。不同的,JAVA中使用线程来实现并发的概念。不同的线程会共享一个进程下的资源和I/O设备,这使得如何控制访问的同步变成了重要的课题。
在单CPU处理器上使用并发程序在任何时刻都只是执行一项工作,因此从理论上讲,肯定可以不用任何任务而编写出相同的程序。但是并发提供了一个重要的组织结构上的好处:类似仿真等程序的设计可以极大地简化。最浅显的例子,比如我们做超级马里奥的小游戏,采用多线程来控制马里奥和怪物的行为就比不使用方便的多。如果在游戏中还有类似菜单的按钮,我们不可能每段代码都去检查这个按钮是否被点击,这个时候使用一个线程就显得方便简洁。
线程状态
在Thread中的内部嵌套类State中规定,线程一共有6种状态。
New 新创建的线程
这里的创建新的线程真的是仅仅new了一个线程。创建新的线程,是指刚new出来的线程,这个线程没有通过start的方法来启动。
Runnable 可运行
一旦我们调用了start方法,这个线程开始工作并处于可运行状态。可运行状态不只包含线程运行,线程中断也被算为可运行状态。一个可运行状态的线程可能在运行也可能没在运行,不要因线程在可运行的状态下没运行而急躁,很有可能这个线程的终止只
是为了让其他的线程获得机会。
Blocked 被阻塞
一个线程试图去获得一个内部锁时,但这个内部锁被其他的线程持有,这个时候,为了等待去使用这个内部锁,这个线程将会暂时处在被阻塞的状态。当其他线程释放锁的时候,这个线程获得了内部锁,并且从阻塞状态转变为非阻塞状态。
Wait 等待
一个线程等待另一个线程通知调度器一个条件(condition),这个线程自己进入等待状态。等待状态和阻塞状态很类似,但是他们是存在本质区别的。如果另一个线程通知调度器结束,那么这个线程进行工作,等待状态也随之结束。
Timed waiting 计时等待
计时等待和等待是比较相似的,计时等待相比较等待多了一个超时参数。调用他们导致线程会进入计时等待。这个状态将一直保持到超时期满或者接收到适当的通知。相比较直接的等待,变得更加的安全。
Terminated 终止
线程终止。线程run方法执行方法体中最后一条语句后,正常退出而自然死亡。或者,出现了在方法中没有捕获的异常,此时终止run方法意外死亡。
创建线程
1.Thread创建线程
第一种创建线程的方式是从Java.lang.Thread类派生一个新的线程类,重载它的run()方法。ExtThread类是实现了Thread的一个子类,它重写了run方法。NewThread类是主方法,创建了一个ExtThread的实例,并且通过调用start方法启动了该线程,自动调用了run方法。我们不需要手动调用run方法,而应该调用start方法来让它自动调用run方法。在JAVA的API中,start是这样定义的:
public void start( )
使该线程开始执行;
Java 虚拟机调用该线程的 run 方法。
结果是两个线程并发地运行;当前线程(从调用返回给 start 方法)和另一个线程(执行其 run 方法)。
多次启动一个线程是非法的。特别是当线程已经结束执行后,不能再重新启动。
如果我们直接调用run方法,得到的将是在main函数的主线程中调用的run方法,这样没有开启新的线程。
run方法通常会以某种形式的循环来进行,使得任务一直运行下去直到不再需要,所以要设定跳出循环的条件(或者直接从run方法返回)。通常run方法被写成无限循环的形式,这样就意味着,除非某个条件使得run终止,否则他将永远运行下去。
NewThread类:
package AllThread;
/**
*
* @author QuinnNorris
*
* 通过Thread创建新线程
*/
public class NewThread {
/**
* @param args
*/
public static void main(String[] args) {
// TODO Auto-generated method stub
Thread t1 = new ExtThread();
// 创建一个ExtThread对象
t1.start();
// 调用start方法,运行新的线程,即运行的是t1中的run方法
}
}
ExtThread类:
package AllThread;
/**
*
* @author QuinnNorris
*
* Thread的一个实现类
*/
public class ExtThread extends Thread {
@Override
public void run() {
// TODO Auto-generated method stub
System.out.println("new thread");
}
}
2.实现Runnable接口创建线程
在JAVA中类仅支持单继承。这样,如果创建自定义线程类的时候是通过扩展 Thread类的方法来实现的,那么这个自定义类就不能再去扩展其他的类,也就无法实现更加复杂的功能。因此,如果自定义类必须扩展其他的类,那么就可以使用实现Runnable接口的方法来定义该类为线程类,这样就可以避免Java单继承所带来的局限性。
NewRunable类:
package AllThread;
/**
*
* @author QuinnNorris
*
* 通过Runnable接口创建新线程
*/
public class NewRunable {
/**
* @param args
*/
public static void main(String[] args) {
// TODO Auto-generated method stub
Runnable r = new ImplRunnable();
// 创建一个Runnable实例类的对象
Thread t1 = new Thread(r);
// 由r作为构造器的参数创建一个Thread对象
// 将这个Runnable子类对象作为参数传入Thread中
t1.start();
// 调用start方法,运行新的线程,即运行的是t1中的run方法
}
}
ImplRunnable类:
package AllThread;
/**
*
* @author QuinnNorris
*
* Runnable的一个实现类
*/
public class ImplRunnable implements Runnable {
@Override
public void run() {
// TODO Auto-generated method stub
System.out.println("new thread");
}
}
多线程资源共享
我们可以利用实现Runnable接口的方式实现多线程的资源共享:把资源保存在Runnable接口中,只创建一份实现了Runnable接口的类的实例,多个Thread对象用同一个Runnable接口实例为参数实例化。但是要注意的是,资源的共享会涉及到同步的问题,如果处理不当,那么数据的谬误、脏数据的出现是必然的事情。每当涉及到资源共享时都要小心谨慎。而且这种资源共享的方法也不是必须的,只要我们能确保最后所有的线程都指向同一个资源,那么他的存放位置不需要被严格规定。
3.使用执行器(Executor)创建线程池(thread pool)
使用线程池是比前两种相对少见的创建线程做法。从JAVA SE5开始,java.util.concurrent包中的执行器(Executor)将为你管理Thread对象,从而简化了并发编程。如果我们的程序需要用到很多生命周期比较短的线程,那么应该使用线程池,线程池中包含了很多空闲线程,而且这些线程的生命周期不需要我们操心。另一个使用线程池的原因是:如果你的代码需要大量的线程,那么最好使用一个线程池来规定总线程数的上线,防止虚拟机崩溃。这样可以限制最大的并发数量。
静态方法创建线程池实例
正如Collection类的静态方法都在Collections中一样,执行器Executor创建线程池的静态方法全部在Executors类中:
public static ExecutorService newCachedThreadPool()
创建一个新的线程池。如果需要线程而线程池中无空闲线程时,创建一个新的线程。空闲线程会被保留60秒。
public static ExecutorService newFixedThreadPool(int nThreads)
根据参数值创建一个固定数量线程的线程池。如果所需线程超过池中线程数则会发生等待。空闲线程会被一直保留。
public static ExecutorService newSingleThreadExecutor()
创建一个仅有一个线程的线程池,顺序执行每一个提交的任务。(和第二种方法参数为1时效果相同)
提交Runnable任务到线程池中
Executor作为一个祖先接口,提供了一个也仅有一个提交线程的方法:
void execute(Runnable command)
在Executor的子类中,有很多子类提供了具有返回值的提交方法,返回提交的结果。
public Future<?> submit(Runnable task)
比如这种submit方法,提交一个 Runnable 任务用于执行,并返回一个表示该任务的 Future。该 Future 的 get 方法在成功 完成时将会返回 null。调用get方法就能得到提交的结果。
关闭线程池
在线程使用结束后,为了保证程序的安全,我们有必要手动调用关闭线程池的方法:
public void shutdown()
ThreadPool类:
package AllThread;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
/**
*
* @author QuinnNorris
*
* 创建线程池
*/
public class ThreadPool {
/**
* @param args
*/
public static void main(String[] args) {
// TODO Auto-generated method stub
ExecutorService es = Executors.newFixedThreadPool(5);
// 我们调用静态方法创建了包含五个线程的线程池
for (int i = 0; i < 5; i++)
es.submit(new ImplRunnable());
es.shutdown();
// 在使用结束之后,一定要关闭线程池
}
}
ImplRunnable类:
package AllThread;
/**
*
* @author QuinnNorris
*
* Runnable的一个实现类
*/
public class ImplRunnable implements Runnable {
@Override
public void run() {
// TODO Auto-generated method stub
System.out.println("new thread");
}
}
4.使用Callable与Future创建线程并获取返回值
我们使用Runnable封装了一个异步运行的任务,我们可以把它想象成一个没有参数和返回值的异步方法,Callable与Runnable相似,但是Callable具有返回值,可以从线程中返回数据。
Callable
我们从jdk中找到了Callable<V>的源代码,去掉一些无用的部分:
package java.util.concurrent;
public interface Callable<V> {
V call() throws Exception;
}
可以看出Callable接口只是将run方法换成了call方法,其他并没有太多的改动。
Future
Future类负责保存异步计算的结果。可以启动一个计算,将Future对象交给某个线程,然后我们去做其他的事情,Future对象的所有者在结果计算好之后就可以调用get方法获得它。我们在上面线程池的submit方法中也提到过。
V get() throws InterruptedException,ExecutionException
如有必要,等待计算完成,然后通过get方法获取其结果。
FutureTask包装器
我们虽然有了Callable和Future类,但是我们仍然需要一种方法将他们结合起来使用。而且还存在的问题是,Callable的出现替代了Runnable。我们需要一种手段让Thread类能够接受Callable做参数。在这里我们使用非常好用的FutureTask包装器。它可以将Callable转换成Futrue和Runnable,因为他同时实现了Runnable和Future<V>两个接口。
CallablePool类:
package AllThread;
import java.util.ArrayList;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.FutureTask;
/**
*
* @author QuinnNorris
*
* 用线程池实现Callable创建线程
*/
public class CallablePool {
/**
* @param args
*/
public static void main(String[] args) {
// TODO Auto-generated method stub
ExecutorService es = Executors.newFixedThreadPool(5);
// 创建一个5个线程大小的线程池
ArrayList<Future<Integer>> results = new ArrayList<Future<Integer>>(5);
// 创建一个Future<Integer>类型的数组
FutureTask<Integer> ft = null;
for (int i = 0; i < 5; i++) {
ft = new FutureTask<Integer>(new ImplCallable(i));
// 将Callable类型转化成FutureTask类型
es.submit(ft);
// 提交线程
results.add(ft);
// 将返回的结果提交,因为FutureTask同时也可变为Future类型,所以这里不需要其他类型转化
}
for (int i = 0; i < 5; i++)
try {
System.out.println(results.get(i).get());
// 打印结果,发现数组中为0到4五个数字,成功。
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (ExecutionException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
ImplCallable类:
package AllThread;
import java.util.concurrent.Callable;
/**
*
* @author QuinnNorris
*
* Callable的实现类
*/
public class ImplCallable implements Callable<Integer> {
private int index;
ImplCallable(int index) {
this.index = index;
}
@Override
public Integer call() throws Exception {
// TODO Auto-generated method stub
return index;
}
}
上面采用了线程池的方法来表现Callable和Future的使用方法,如果是简单实用Thread道理也是相同的,我们需要把:
ExecutorService es = Executors.newFixedThreadPool(5);
es.submit(ft);
这两句去掉,在for循环中替换成下面两句。创建Thread实例,开启新的线程。
Thread th = new Thread(ft);
th.start();