Java探索之旅(17)——多线程(1)
1.多线程
1.1线程
线程是程序运行的基本执行单元。指的是一段相对独立的代码,执行指定的计算或操作。多操作系统执行一个程序时会在系统中建立一个进程,而在这个进程中,必须至少建立一个线程(这个线程被称为主线程)来作为这个程序运行的入口点。每个线程都有独立的堆栈、计数器、本地变量。但是能够共享内存,文件处理器,预存储状态等资源。JVM至少有主线程和垃圾清理进程。
1.2使用多线程的好处
如果能合理地使用线程,将会减少开发和维护成本,甚至可以改善复杂应用程序的性能。如在GUI应用程序中,还以通过线程的异步特性来更好地处理事件;在应用服务器程序中可以通过建立多个线程来处理客户端的请求。线程甚至还可以简化虚拟机的实现,如Java虚拟机(JVM)的垃圾回收器(garbage collector)通常运行在一个或多个线程中。
好处包括:
❶充分利用CPU资源。当执行单线程程序时,由于在程序发生阻塞时CPU可能会处于空闲状态。这将造成大量的计算资源的浪费。多线程可以在某一个线程处于休眠或阻塞时,而CPU又恰好处于空闲状态时来运行其他的线程。 ❷简化编程模型。如果程序只完成一项任务,那只要写一个单线程的程序,并且按着执行这个任务的步骤编写代码即可。但要完成多项任务,如果还使用单线程的话,那就得在在程序中判断每项任务是否应该执行以及什么时候执行。如显示一个时钟的时、分、秒三个指针。使用单线程就得在循环中逐一判断这三个指针的转动时间和角度。如果使用三个线程分另来处理这三个指针的显示,那么对于每个线程来说就是指行一个单独的任务。这样有助于开发人员对程序的理解和维护。 ❸简化异步事件的处理。当一个服务器应用程序在接收不同的客户端连接,监听线程负责监听来自客户端的请求。单线程来处理,当监听线程接收某个客户端请求后,读取客户端发来的数据,在读完数据后,read方法处于阻塞状态,也就是说,这个线程将无法再监听客户端请求了。而要想在单线程中处理多个客户端请求,就必须使用非阻塞的Socket连接和异步I/O。但使用异步I/O方式比使用同步I/O更难以控制,也更容易出错。因此,使用多线程和同步I/O可以更容易地处理类似于多请求的异步事件。 ❹使GUI更有效率。使用单线程来处理GUI事件时,必须使用循环来对随时可能发生的GUI事件进行扫描,在循环内部除了扫描GUI事件外,还得来执行其他的程序代码。如果这些代码太长,那么GUI事件就会被“冻结”,直到这些代码被执行完为止。 ❺节约成本。提高程序的执行效率一般有三种方法: ⑴增加计算机的CPU个数。昂贵。 ⑵为一个程序启动多个进程。不容易共享数据,系统资源占用大。 ⑶在程序中使用多进程。多线程可以模拟多块CPU的运行方式,廉价。
2.多线程的创建和实例化
❶扩展(extends)java.lang.Thread类
特点:需要重写public void run()方法。
实例化:直接new调用构造函数即可。
❷实现(implements)java.lang.Runnable接口。
特点:使用Thread的构造方法,如下:
实例化:name是自定的进程名,缺省时系统会默认制定。
Thread(Runnable target) Thread(Runnable target, String name) Thread(ThreadGroup group, Runnable target) Thread(ThreadGroup group, Runnable target, String name) Thread(ThreadGroup group, Runnable target, String name, long stackSize)
❸两者的优劣
实际上,Thread也是实现了Runnable接口:
class Thread implements Runnable { //… public void run() { if (target != null) target.run(); } }
相较于Thread,接口Runnable有利于实现资源共享。且有如下优势: ⑴适合多个相同的程序代码的线程去处理同一个资源 ⑵可以避免java中的单继承的限制 ⑶增加程序的健壮性,代码可以被多个线程共享,代码和数据独立。
❹实例。接口Runable实现资源共享的例子:
class MyThread implements Runnable{ private int ticket = 5; //5张票 public void run() { for (int i=0; i<=20; i++) { if (this.ticket > 0) { System.out.println(Thread.currentThread().getName()+ "正在卖票"+this.ticket--); } } } } public class lzwCode { public static void main(String [] args) { MyThread my = new MyThread(); new Thread(my, "1号窗口").start(); new Thread(my, "2号窗口").start(); new Thread(my, "3号窗口").start(); } }
3.线程的状态
❶新状态:线程对象已经创建,还没有在其上调用start()方法。
❷可运行状态:当线程有资格运行,但调度程序还没有把它选定为运行线程时线程所处的状态。当start()方法调用时,线程首先进入可运行状态。在线程运行之后或者从阻塞、等待或睡眠状态回来后,也返回到可运行状态。
❸运行状态:线程调度程序从可运行池中选择一个线程作为当前线程时线程所处的状态。这也是线程进入运行状态的唯一一种方式。
❹等待/阻塞/睡眠态:这是线程有资格运行时它所处的状态。实际上这个三状态组合为一种,其共同点是:线程仍旧是活的,但是当前没有条件运行。换句话说,它是可运行的,但是如果某件事件出现,他可能返回到可运行状态。
❺死亡态:当线程的run()方法完成时就认为它死去。这个线程对象也许是活的,但是,它已经不是一个单独执行的线程。线程一旦死亡,就不能复生。如果在一个死去的线程上调用start()方法,会抛出java.lang.IllegalThreadStateException异常.
4.线程的控制
4.1 优先 setPriority()
Thread类中有三个常量,定义线程优先级范围: static int MAX_PRIORITY 线程可以具有的最高优先级。 static int MIN_PRIORITY 线程可以具有的最低优先级。 static int NORM_PRIORITY 分配给线程的默认优先级。
线程的优先级用1-10之间的整数表示,数值越大优先级越高,默认的优先级为5。主线程的优先级是5。与线程休眠类似,线程的优先级仍然无法保障线程的执行次序。只不过,优先级高的线程获取CPU资源的概率较大,优先级低的并非没机会执行。
JVM从不会改变一个线程的优先级。然而,1~10之间的值是没有保证的。一些JVM可能不能识别10个不同的值,而将这些优先级进行每两个或多个合并,变成少于10个的优先级,则两个或多个优先级的线程可能被映射为一个优先级。
窗口操作系统中,使用优先权加时间段的处理机制,优先级高的线程执行时间超时,也必须让步给低优先级的进程在一个线程中开启另外一个新线程,则新开线程称为该线程的子线程,子线程初始优先级与父线程相同。
class hello implements Runnable { public void run() { for(int i=0;i<5;++i){ System.out.println(Thread.currentThread().getName()+"运行"+i); } } public static void main(String[] args) { Thread h1=new Thread(new hello(),"A"); Thread h2=new Thread(new hello(),"B"); Thread h3=new Thread(new hello(),"C"); h1.setPriority(8); h2.setPriority(2); h3.setPriority(6); h1.start(); h2.start(); h3.start(); } }输出:
A运行0 A运行1 A运行2 A运行3 A运行4 B运行0 C运行0 C运行1 C运行2 C运行3 C运行4 B运行1 B运行2 B运行3 B运行4
并非优先级越高就先执行。谁先执行还是取决于谁先去的CPU的资源。
4.2让步 yield()
线程的让步含义就是使当前运行的线程让出CPU资源,即当前线程暂停,当前线程的状态回到可运行状态。这样允许具有相同优先级的其他线程获得运行机会。使用yield()的目的是让相同优先级的线程之间能适当的轮转执行。yield()从未导致线程转到等待/睡眠/阻塞状态。在大多数情况下,yield()将导致线程从运行状态转到可运行状态,但有可能没有效果。因为让步的线程还有可能被线程调度程序再次选中。
如:
class hello implements Runnable { public void run() { for(int i=0;i<5;++i){ System.out.println(Thread.currentThread().getName()+"运行"+i); if(i==3){ System.out.println("线程的礼让"); Thread.currentThread().yield(); } } } public static void main(String[] args) { Thread h1=new Thread(new hello(),"A"); Thread h2=new Thread(new hello(),"B"); h1.start(); h2.start(); } }输出:
A运行0A运行1A运行2A运行3线程的礼让A运行4B运行0B运行1B运行2B运行3线程的礼让B运行4
4.3加入 joint()
使调用他的进程插入当前进程,直到该进程执行完毕才恢复阻塞进程的运行。倘若该调用进程为无限循环进程,且发生了其它线程中断它,则或抛出InterruptedException异常,而停止运行。joint()非静态方法:
void join();//等待该线程终止。 void join(long millis);//等待该线程终止的时间最长为 millis毫秒。 void join(long millis,int nanos);//等待该线程终止的时间最长为 millis毫秒 + nanos 纳秒。如下计算圆周率的例子:
public class Estimate { public static void main(String[] args){ Thread demo=new estimatePI(); demo.start(); try{ demo.join();//阻塞主线程直到demo运行完毕 } catch(InterruptedException e){} System.out.println("PI= "+demo); } } class estimatePI extends Thread{ public static double PI=0.0; private double flag=1.0; public void run(){ for(long i=1;i<999;i+=2){ PI+=4.0*flag/i; flag=-flag; } } public String toString(){ return ""+PI; } }
4.4中断 interrupt()
中断正在进行的线程。一旦中断,则抛出Interrupted异常,此时需要在run()中处理该异常。该进程的isInterrupted也将设置为真。
例如,使用中断监视用户从键盘的输入。
public class Stop { public static void main(String[] args){ Thread demo=new Service(); Scanner sc=new Scanner(System.in); demo.start(); String key=""; while(!key.equals("stop")) key=sc.nextLine(); demo.interrupt(); } } class Service extends Thread{ private int count=1; public void run(){ while(!interrupted()) System.out.println(this.getName()+" "+count++); System.out.println("终止"); } }
4.5睡眠sleep()
Thread.sleep(long millis) Thread.sleep(long millis, int nanos)
是帮助所有线程获得运行机会的最好方法。睡眠了,其他的线程就有机会执行了。参数设定线程不会运行的最短时间。sleep()是静态方法,强制当前正在执行的线程休眠(暂停执行)。当睡眠时间到期,则返回到可运行状态而不是运行状态。会抛出Interrupted必检异常。
如下使用睡眠,保证先输出中文再输出English:
public class Test{ public static void main(String[] args){ Thread t1,t2; t1=new English(); t2=new Chinese(); t2.start(); t1.start(); } } class English extends Thread{ public void run(){ System.out.println("English"); } } class Chinese extends Thread{ public void run(){ try {Thread.sleep(500); System.out.println("中文");} catch(InterruptedException e){} } }
5.小结
5.1线程离开运行状态的3种方法
❶调用Thread.sleep():使当前线程睡眠至少多少毫秒(尽管它可能在指定的时间之前被中断)。
❷调用Thread.yield():不能保障太多事情,尽管通常它会让当前运行线程回到可运行性状态,使得有相同优先级的线程有机会执行。
❸调用join()方法:保证当前线程停止执行,直到该线程所加入的线程完成为止。然而,如果它加入的线程没有存活,则当前线程不需要停止。
5.2其它
❶线程的调度是JVM的一部分,在一个CPU的机器上上,实际上一次只能运行一个线程。一次只有一个线程栈执行。JVM线程调度程序决定实际运行哪个处于可运行状态的线程
❷抛出InterruptedException异常的程序包括静态函数Thread.sleep和非静态函数joint(),interrupt().
参考:
1.Java线程详解