多线程的优点和必要性是不言而喻的。
三种方法实现多线程
1. 继承Thread
class A extends Thread{ public void run() {...} }
使用时,
new A().start();
2. 实现Runnable
(1)定义Runnable接口的实现类,并重写该接口的run()方法。
(2)创建该实现类的实例,并以此实例作为Thread的target来创建Thraed,这个Thread才是真正的线程对象。
class A implements Runnable{ public void run(){...} } ... Thread(new A()).start(); Thread(new A(),"指定一个名字").start();
3. 利用Callable和Future
前面指出,通过实现Runnable接口创建多线程,Thread类把run()方法包装成线程的执行体,但是只能是这个run方法。C#支持把任意方法包装成线程执行体。
Java5后,Java提供了Callable接口,它的call()方法可以作为线程的执行体,较之run,它的优点在于能有返回值和能抛出异常。但是,Callable并不是Runnable的子接口,所以它不能被Thread类直接做target对象。和Callable接口同时推出的Future接口是用来代表call()方法的返回值的;特别的,有一个FutureTask的实现类,该类同时实现了Future接口和Runnable接口,因而可以作为Thread类的target。(注意体会此处精良的设计模式)
这里给出Future接口定义的方法:
- boolean cancel(boolean mayInterruptIfRunning):试图取消该Future关联的Callable任务。
- V get():返回call方法的返回值。该方法的调用会导致程序阻塞,必须等到子线程结束后才会得到返回值。
- V get(long timeout , TimeUnit unit):和上面方法的区别在于是让程序阻塞最多timeout,unit指定的时间,如果在指定时间后Callable任务依然没有返回值,会抛出异常。
- boolean isCancelled():如果在Callable任务正常完成前被取消,则返回true.
- boolean isDone():如果Callable任务已完成,则返回true。
下面是使用Callable的示例。
public class MyThread implements Callable<Integer> { public Integer call() {...} public static void main(String[] args) { MyThread mt = new MyThread(); FutureTask<Integer> task = new FutureTask<Integer>(mt); new Thread(task,"线程名").start(); } }
我们可以用task.get()得到子线程的返回值。
线程的生命周期
熟悉操作系统的读者对线程、进程的生命周期一定很理解。一个线程要经过新建(new)、就绪(Runnable)、运行(Running)、阻塞(Block)和死亡(Dead)。并且一个线程也不可能一直占有CPU,所以它会多次在运行、阻塞之间切换。
1. start() vs run()
值得强调的是,启动线程使用start方法,而不是run方法。因为你调用了start启动线程,系统会把run方法当成线程的执行体,但是如果你自己调用run,在这个run执行结束前其他线程无法并发执行,也就是说,你多次调run,只是相当与在调用一个普通方法,和多线程无法。调用start就会进行就绪状态。
2. 运行、阻塞
现在的操作系统都是采用抢占式的调度策略,也就是系统可每个可执行的线程一个小时间段,结束后就剥夺资源。但一些嵌入式设备可能采用协作调度,即只有调用sleep()或yield()才会放弃作占的资源。
3. 死亡
一般死亡的方式有三种:正常结束;抛出未捕获的异常;调用stop()。我们一般不建议使用线程的stop,因为这样很容易导致死锁。
要注意的是,子线程在启动后就和主线程有相同地位,不会因为主线程结束而死亡。还有,不要试图对已经死亡的线程使用start方法,这会导致IllegalThreadState异常。实际上,start只能使用一次,也就是在线程新建状态下使用。
线程的控制
1. join 方法
Thread的join方法,可以让一个线程等待另一个线程完成。简单的说,就是线程A调用了b.join(),等b这个线程死亡后,A才继续向下执行。
public class JoinThread extends Thread { public JoinThread(String name) { super(name); } public void run() { for(int i = 0;i<10;i++) { System.out.println(getName()+" "+i); } } public static void main(String[] args) throws Exception { // new JoinThread("线程1").start(); for(int i = 0;i<10;i++) { if(i==5) { JoinThread jt = new JoinThread("被join"); jt.start(); jt.join(); } System.out.println(Thread.currentThread().getName()+" "+i); } } }
结果的截图:
2.后台进程
所谓后台进程就是在后台运行、为其他进程服务的。它的特征是前台进程死亡时后台进程也会死亡。指定后台进程只需调用Thread对象的setDeamon(true)。其中,deamon是守护的意思,因此后台进程有时也称守护进程。
public class DaemonThread extends Thread { public void run() { for(int i = 0;i<1000;i++) { System.out.println(getName()+" "+i); } } public static void main(String[] args) { DaemonThread t = new DaemonThread(); t.setDaemon(true); t.start(); for(int i=0;i<5;i++) System.out.println("Current "+Thread.currentThread().getName()+" "+i); } }
需要注意的是,setDaemon(true)要在start之前调用。
3 休眠 sleep、让步yield
执行Thread的静态方法sleep()可以正在运行的线程暂停一会儿。有两种重载:
-
static void sleep(long millis):单位毫秒
-
static void sleep(long millis,int nanos):相当于暂停millis后再暂停nanos。考虑到计算机本身时钟问题,这个方法并不常用。
yield方法和sleep很相似,都是Thread类的静态方法。但它不会阻塞该线程,而是让它回到就绪状态。我们使用yield的目的一般是为了让和当前线程优先级相同或更高的线程得到执行机会,但并不能得到完全的保证。虽然有的书上说,yiled()方法只会给优先级相同、更高的线程执行机会,但笔者利用下面代码测试,发现结果并不尽然:
public class YieldTest extends Thread{ public YieldTest(String name) { super(name); } public void run() { for(int i=0;i<30;i++) { System.out.println(getName()+" "+i); if(i==15) { Thread.yield(); } } } public static void main(String[] args) throws Exception { YieldTest y = new YieldTest("High"); y.setPriority(Thread.MAX_PRIORITY); y.start(); YieldTest y2 = new YieldTest("Low"); y2.setPriority(Thread.MIN_PRIORITY); y2.start(); } }
查阅API文档发现这么一句话,"a hint to the scheduler that the current thread is willing to yield its current use of a processor. The scheduler is free to ignore this hint‘。不难发现,调用yield()只是个暗示放弃,但系统是否这样做是不确定。所以我们一般倾向于使用sleep而不是yield。
线程同步
这部分知识是线程学习的核心部分,尤其在网络编程,保证线程同步的安全问题是屡见不鲜的。
1. 同步代码块
线程安全的经典问题是银行取钱。这部分的代码写在这里显得很臃肿,有兴趣的朋友可以参考https://github.com/ChenZhongPu/GitJava。
为了解决这个问题,java的多线程支持同步监视器。使用同步监视器的通用语法是同步代码块,格式如下:
synchronize(obj) { ... }
上面代码的含义是,线程开始执行同步代码之前,必须先获得对同步监视器的锁定。这样,任何一个时刻只有一个线程可以获得对同步监视器的锁定,完成后就释放锁定。
2.同步方法
同步方法就是使用synchronized修饰方法,它无需显式指定同步监视器,同步方法的同步监视器就是this。通过使用同步方法可以很方便的实现线程安全的类,
比如,我们在Account方法里添加一个同步的draw方法,而不是在run方法实现取钱逻辑。
需要说明的是,可变类的线程安全是以减低效率为代价的。因此我们不要对所有方法都进行同步,只对那些改变共享资源的方法进行同步。
我们注意到,程序是无法显式释放对同步监视器的锁定,一般线程会在下面情况释放对同步监视器的锁定:
- 同步方法、同步代码块执行结束。
- 在同步方法、代码块遇到break,return。
- 同步方法、代码块出现未处理的error,Exception。
- 执行同步监视器对象的wait()方法。
而下面的情况不会释放同步监视器:
- 在执行同步方法、代码块时调用Thread,sleep(),Thread.yield()。
- 其他线程调用该线程的suspendI()。
3. 同步锁
java5之后提供了更强大的线程同步机制,即显式定义同步锁对象。
Lock是控制多个线程对共享资源进行访问的工具。通常,锁提供了对共享资源的独占访问,每次只有一个线程对Lock对象进行加锁。某些锁可能允许对共享资源并发访问,如ReadWriteLock。
找线程安全的控制中,常用的是ReentrantLock(可重入锁)。使用时代码格式如下:
class A{ private final ReentrantLock lock = new ReentrantLock(); //... // 定义需要保证线程安全的方法 public void m () { lock.lock();//加锁 try{ //... } finally { lock.unlock();//释放 } } }
现在简要解释可重入的含义,一个线程可以对已被加锁的ReentrantLock锁进行再次加锁,有一个计数器负责维护嵌套次数,线程在每次调用lock加锁后,必须显式调用unlock解锁,所以一段被锁保护的代码可以调用另一个被锁保护的方法。
4. 死锁
死锁现象发生在两个进程相互等待对方释放同步监视器,而JVM没有监测。一旦发生死锁,程序既无异常,也不提示。
死锁发生是很常见的,尤其是系统出现多个同步监视器。有兴趣朋友可以参考,https://github.com/ChenZhongPu/GitJava下面的DeadLock.java。