一 多线程的优势
1.线程在程序中是独立的、并发的执行流,与分隔的进程相比,进程中线程的隔离程度要小。它们共享内存、文件 句柄和其他每个进程应有的状态。由于线程的划分尺度小于进程,是的多线程程序的并发性高。进程在执行过程中拥有独立的内存单元,而多个线程共享内存,从而 极大地提高了程序的运行效率。
2.线程比进程具有更高的性能,这是由于同一个进程中的线程都有共性——多个线程共享同一个进程虚拟空间。线程共享的环境包括:进程代码块、进程的共有数据等。通过这些共享的数据,线程很容易实现相互之间的通信。
3.在实际应用中,多线程是非常有用的,一个浏览器能同时下载多个图片,一个web服务器必须能同时响应多个用户请求,Java虚拟机本身就在后台提供了一个超级线程来进行垃圾回收,图形用户界面应用也需要启动单独的线程从主机环境收集用户界面事件......
二 线程的创建和启动
Java使用Thread类来代表线程,所有的线程对象都必须是Thread类或其子类的实例,
1.方式一:继承Thread类
(1)定义Thread类的子类,并重写该类的run()方法,该run()方法的方法体就代表了线程需要完成的任务,run()方法即线程执行体。
(2)创建Thread子类的实例,即创建线程对象。
(3)调用线程对象的start()方法来启动该线程。
public class FirstThread extends Thread{
private int i;
public void run(){
for(;i<100;i++){
//当线程类继承Thread类时,直接使用this即可获取当前线程
//Thread对象的getName()返回当前线程的名字
System.out.println(getName()+""+i);
}
}
public static void main(String[] args){
for(int i=0;i<100;i++){
System.out.println(Thread.currentThread().getName()+""+i);
if(i==20){
//创建并启动第一个线程
new FirstThread().start();
//创建并启动第二个线程
new FirstThread().start();
}
}
}
}
程序中显式创建了两个线程,但是实际上程序有三个线程,即显式创建的两个线程和主线程。
Thread.currentThread():这是Thread类的静态方法,该方法总是返回当前正在执行的线程对象。
getName():是Thread类的实例方法,该方法返回调用该方法的线程的名字。在默认情况下,主线程的名字为main,用户启动的多个线程的名字依次为Thread-0,Thread-1....
2.方式二:实现Runnable接口
(1)定义Runnable接口的实现类,并重写该接口的run()方法
(2)创建Runnable实现类的实例,并以此实例作为Thread的target来创建Thread对象,该Thread对象才是真正的线程对象。
public class SecondThread implements Runnable{
private int i;
public void run(){
for(;i<100;i++){
//当线程类实现Runnable接口时,如果想获取当前线程,只能用Thread.currentThread()方法
System.out.println(Thread.currentThread().getName()+""+i);
}
}
public static void main(String[] args){
for(int i=0;i<100;i++){
System.out.println(Thread.currentThread().getName()+""+i);
if(i==20){
SecondThread st=new SecondThread();
//通过new Thread(target,name)方法来创建新线程
new Thread(st,"线程1").start();
new Thread(st,"线程2").start();
}
}
}
}
对比方式一和方式二,前者直接创建的Thread子类即可代表线程对象,后者创建的Runnable对象只能作为线程对象的target。一定要注意的是, 采用Runnable接口的方式创建的多个线程可以共享线程类的实例变量。这是因为,程序所创建的Runnable对象只是线程的target,而多个线 程可以共享同一个target,所以多个线程可以共享同一个线程类。
3.方式三:使用Callable和Future创建线程类
通 过实现Runnable接口创建多线程时,Thread类的作用就是把run()方法包装成线程执行体。那么是否可以直接把任意方法都包装成线程执行体 呢?从Java5开始,Java提供了Callable接口,该接口提供了一个call()方法可以作为线程执行体,但call()方法比run()方法 功能更强大,主要在于:
call()方法可以有返回值;call()方法可以声明抛出异常。
因此完全可以提 供一个Callable对象作为Thread的target,而该线程的线程执行体就是该Callable对象的call()方法。但 是,Callable接口是Java5新增的接口,而且它也不是Runnable接口的子接口,所以Callable对象不能直接作为Thread的 target。而且call()方法还有一个返回值,call()方法并不是直接调用,而是作为线程执行体被调用的,那么如何获取call()方法的返回 值呢?
Java5提供了Future接口来代表Callable接口里call()方法的返回值,并为Fucture接口提供了一 个FutureTask实现类,该实现类实现了Future接口,并且实现了Runnable接口,可以作为Thread类的target。在 Future接口里定义了几个公共方法来控制它关联的Callable任务。
boolean cancel(boolean mayInterruptIfRunning):试图取消该Future里关联的Callable任务。
V get():返回Callable任务里call()方法的返回值。调用该方法将导致程序阻塞,必须等到子线程结束后才会得到返回值。
V get(long timeout,TimeUnit unit):返回Callable任务里call()方法的返回值。该方法让程序最多阻塞timeout和unit指定的时间,如果经过指定时间后 Callable任务依然没有返回值,将会抛出TimeoutException异常。
boolean isCancelled():如果在Callable任务正常完成前被取消,则返回true。
boolean isDone():如果Callable任务已完成,则返回true。
(1)创建Callable接口的实现类,并实现call()方法,该call()将作为线程执行体,且该call()方法有返回值,在创建Callable实现类的实例。从Java8开始,可以直接使用Lambda表达式创建Callable对象。
(2)使用FutureTask类来包装Callable对象,该FutureTask对象封装了该Callable对象的call()方法的返回值。
(3)使用FutureTask对象作为Thread对象的target创建并启动线程。
(4)调用FutureTask对象的get()方法来获得子线程执行结束之后的返回值。
public class ThirdThread{
public static void main(String[] args){
//创建Callable对象
ThirdThread rt=new ThirdThread();
//先使用Lambda表达式创建Callable<Integer>对象
//使用FutureTask来包装Callable对象
FutureTask<Integer> task=new FutureTask<Integer>((Callable<Integer>()->{
int i=0;
for(;i<100;i++){
System.out.println(Thread.currentThread().getName()+"的循环变量i的值为"+i);
}
//call()方法的返回值
return i;
});
for(int i=0;i<100;i++){
System.out.println(Thread.currentThread().getName()+"循环变量i的值为"+i);
if(i==20){
new Thread(task,"有返回值的线程").start();
}
}
try{
System.out.println("子线程的返回值为:"+task.get());
}
catch(Exception ex){
ex.printStackTrace();
}
}
}
程 序先使用Lambda表达式创建一个Callable对象,然后将该实例包装成一个FutureTask对象。主线程中当循环变量i等于20时,程序启动 以FutureTask对象为target的线程。程序最后调用FutureTask对象的get()方法来返回call()方法的返回值——该方法将导 致主线程阻塞,直到call()方法结束并返回为止。
4.三种方式对比
(1)采用Runnable,Callable接口的方式创建多线程的优缺点:
线程类只是实现了Runnable接口或Callable接口,还可以继承其他类。
多个线程共享同一个target对象,所以非常适合多个相同线程来处理同一份资源的情况。
编程稍稍复杂,如果要访问当前线程,必须使用Thread.currentThread()方法
(2)采用继承Thread类的方式创建多线程的优缺点:
线程只能继承自Thread类,不能再继承自其他类了。
编写简单。
三 线程的生命周期
1.线程的生命周期要经过新建(new),就绪(Runnable),运行(Running),阻塞(Blocked)和死亡(Dead)五种状态。
当程序使用new创建了一个线程之后,该线程就处于新建状态,由Java虚拟机为其分配内存,并初始化其成员变量的值。当线程对象调用了start()方法 之后,该线程处于就绪状态,Java虚拟机会为其创建方法调用栈和程序计数器。至于线程如何开始运行,取决于JVM里线程调度器的调度。
2.新建和就绪
这里要注意的是:调用start()方法来启动线程,系统会把该run()方法当成线程执行体来处理;但是如果直接调用线程对象的run()方法,则 run()方法立即就会执行,而且在run()方法返回之前其他线程无法并发执行——也就是说,如果直接调用线程对象的run()方法,系统会把线程当成 一个普通对象,而run()也是一个普通方法。
public class InvokeThread implements Runnable{
private int i;
public void run(){
for(;i<100;i++){
//直接调用run()方法时,Thread的this.getName()返回的是该对象的名字
System.out.println(Thread.currentThread().getName()+""+i);
}
}
public static void main(String[] args){
for(int i=0;i<100;i++){
//返回当前线程的名字
System.out.println(Thread.currentThread().getName()+""+i);
if(i==20){
//不会启动两个线程,而是依次执行两个run()方法
new InvokeThread().run();
new InvokeThread().run();
}
}
}
}
结果:
main 19
main 20
main 0 main 1 ....main 99
main 0 main 1 ....main 99
main 21 main 22 ....
注意:run()方法只是线程里的一个函数,本身并不是多线程,直接调用run()方法相当于调用了一个普通函数而已,程序必须执行完run()方法才会往下走。程序中只有一个线程,即主线程。
3.运行和阻塞
在 一个多处理器的及其上,会有多个线程并行执行,当线程数大于处理器数时,依然会存在多个线程在同一个CPU上轮换的现象。线程需要在运行过程中被中断,目的就是使其他线程获得执行的机会,线程调度的细节取决于底层平台所采用的策略。对于采用抢占式策略的系统而言,系统会给每个可执行的线程一个小时间段来处理任务,当该时间段用完后,系统就会剥夺该线程所占用的资源,让其他线程获得执行的机会,在选择下一个线程时,系统会考虑线程的优先级。
当发生如下情况时,线程将会进入阻塞状态:
(1)线程调用sleep方法主动放弃所占用的处理器资源;
(2)线程调用了一个阻塞式IO方法,在该方法返回之前,该线程被阻塞;
(3)线程试图获得一个同步监视器,但该同步监视器正被其他线程所持有;
(4)线程正在等待某个通知(notify);
(5)程序调用了线程的suspend()方法将该线程挂起,注意,这个方法容易导致死锁。
被阻塞的线程的阻塞解除后,就会重新进入就绪状态,等待线程调度器再次调度它。当发生如下特定的情况时可以接触上面的阻塞:
(1)调用sleep()方法的线程经过了指定的时间;
(2)线程调用的阻塞式IO方法已经返回;
(3)线程成功的获得了试图取得的同步监视器;
(4)线程正在等待某个通知时,其他线程发出了一个通知;
(5)处于挂起状态的线程被调用了resume()恢复方法。
4.线程死亡
线程会以三种方式结束,结束后就处于死亡状态:
(1)run()或call()方法执行完成,线程正常结束;
(2)线程抛出一个未捕获的Exception或error;
(3)直接调用该线程的stop方法来结束该线程,但该方法容易导致死锁,不推荐使用
判断某个线程是否死亡,可以调用线程对象的IsAlive()方法,当线程处于就绪、运行、阻塞状态时,返回true,当线程处于新建、死亡状态时,返回false。
控制线程
5.join线程
Thread 提供了一个线程等待另一个线程的方法——join()方法。当在某个程序执行流中调用其他线程的join()方法时,调用线程将被阻塞,直到被 join()方法加入的join线程执行完为止。join()方法通常由使用线程的程序调用,以将大问题划分成许多小问题,每个小问题分配一个线程。当所 有的小问题都得到处理后,再调用主程序来进一步操作。
public class JoinThread extends Thread { //提供一个有参数的构造器,用于设置该线程的名字 public JoinThread(String name){ super(name); } public void run(){ for(int i=0;i<100;i++){ System.out.println(getName()+" "+i); } } public static void main(String[] args) throws Exception{ new JoinThread("新线程").start(); for(int i=0;i<100;i++) { if (i == 20) { JoinThread jt = new JoinThread("被Join的线程"); jt.start(); //main线程调用了jt线程的join()方法,main线程必须等jt执行结束才会往下执行 jt.join(); } System.out.println(Thread.currentThread().getName() + " " + i); } } }
程序中共有三个线程,main线程和名为“新线程”的线程并发执行,当i等于20时,启动了名为“被join的线程”,main线程必须等这条线程执行结束才可以继续执行。
5.后台线程
有 一种线程,它是在后台运行的,它的任务是为其他的线程提供服务,这种线程称为“后台线程”,又称为“守护线程”。JVM的垃圾回收器就是典型的后台线程。 后台线程的特征就是:当所有的前台线程都死亡,后台线程就会自动死亡。可以调用Thread的setDaemon(true)方法将指定线程设置为后台线程。
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); s.start(); for(int i=0;i<100;i++){ System.out.println(Thread.currentThread().getName()+" "+i); } //程序执行到此处,前台线程main线程执行结束,后台线程也随之结束 } }
6.线程睡眠:sleep
如果需要让当前正在执行的线程暂停一段时间,并进入阻塞状态,则可以通过调用Thread类的静态sleep()方法来实现。
public class SleepThread{ public static void main(String[] args) throws Exception{ for(int i=0;i<10;i++){ System.out.println("当前时间是:"+new Date()); Thread.sleep(1000); } } }
程序每隔一秒钟输出一条结果
7.程序让步:yield
yield() 方法也可以让当前正在执行的线程暂停,但它不会阻塞该线程,它只是将该线程转入就绪状态。yield()方法只是让当前线程暂停一下。让系统的线程调度器 重新调度一次。当某个线程调用了yield()方法之后,只有优先级与当前线程相同或优先级比当前线程更高的处于就绪状态的线程才会获得执行的机会。
public class YieldThread extends Thread { public YieldThread(String name){ super(name); } public void run(){ for(int i=0;i<100;i++){ System.out.println(getName()+" "+i); if(i==20){ Thread.yield(); } } } public static void main(String[] args) throws Exception{ YieldThread yt1=new YieldThread("高级"); yt1.setPriority(Thread.MAX_PRIORITY); yt1.start(); YieldThread yt2=new YieldThread("低级"); yt2.setPriority(Thread.MIN_PRIORITY); yt2.start(); }
名为高级的线程调用yield()方法暂停之后,系统中没有与之优先级相同或优先级更高的线程,所以线程继续执行。
线程的优先级MAX_PRIORITY的值为10,MIN_PRIORITY值为1,NORM_PRIORITY值为5
sleep()和yield()方法的区别如下:
sleep()暂停当前线程后,会给其他线程机会;yield()方法只会给优先级相同或者优先级更高的线程执行机会;
sleep()会将线程转入阻塞状态,经过阻塞时间才会转入就绪状态;yield()只是强制当前线程进入就绪状态;
sleep()抛出了InterruptedException异常;yield()没有声明抛出异常;
sleep()比yield()有更好的移植性,通常不建议使用yield()控制并发线程的执行。