一、join线程
Thread提供了让一个线程等待另一个线程完成的方法:join() 方法。当在某个程序执行流中调用其他线程的join()方法时,调用线程将被阻塞,直到被join方法加入的join线程完成为止。
join()方法通常由使用线程的程序调用,以将大问题划分成许多小问题,每个小问题分配一个线程。当所有的小问题都得到处理后,再调用主线程来进一步操作。
package section3;
public class JoinThread extends Thread
{
//提供一个有参数的构造器,用于设置该线程的名字
public JoinThread(String name)
{
super(name);
}
public void run()
{
for(var i=0;i<100;i++)
{
System.out.println(getName()+" "+i);
}
}
public static void main(String[] args) throws Exception
{
//启动子线程
new JoinThread("新线程").start();
for(var i=0;i<100;i++)
{
if(i==20)
{
var jt=new JoinThread("被Join的线程");
jt.start();
//main线程调用jt线程的join()方法,main线程必须等jt执行结束才会向下执行
jt.join();
}
System.out.println(Thread.currentThread().getName()+" "+i);
}
}
}
上面程序一共由三个线程,主方法开始时就启动了名为“新线程”的子线程,该子线程将会在main线程并发执行。当主线程的循环变量等于20时,启动了名为“Join的线程”的线程,该线程不会和mian线程并发执行,main线程必须等到该线程执行结束后才可以向下执行。在名为“被Join的线程”的线程执行时,实际只有两个线程并发执行,而主线程处于等到状态。
运行上面的程序将可以看到如下运行效果:
从上图可以看出,主线程执行到i==20时,程序启动并join了名为“被Join的线程”的线程,所以程序将一直处于阻塞状态,直到名为“被Join的线程”的线程执行完成。
join()方法有如下三种重载形式:
1、join():等待被join的线程执行完成;
2、join(long millis):等待被join的线程的时间最长为millis毫秒。如果在millis毫秒内被join的线程还没有执行结束,则并不再等待。
3、join(long milis,int nanos):等待被join的线程的时间最长为millis毫秒加nanos毫微秒。
二、后台线程(守护线程” 或“精灵线程)
有一种线程,它是在后台运行的,它的任务是为其他的线程提供服务,这种线程被称为“后台线程(Daemon Thread)”,又称为“守护线程” 或“精灵线程”。JVM的垃圾回收线程就是典型的后台线程。
后台线程有个特征:如果所有的前台线程都死亡,后台线程会自动死亡。
调用Thread对象setDaemon(true)方法可将指定线程设置成后台线程。
下面程序将执行线程设置为后台线程,可以看到当所有的前台线程死亡时,后台线程随之死亡。当整个虚拟机中只剩下后台线程时,程序就没有继续运行的必要了,所以虚拟机也就退出了。
package section4;
public class DaemonThread extends Thread
{
//定义后台线程的程序与普通线程没有任何区别
public void run()
{
for(var i=0;i<1000;i++)
{
System.out.println(getName()+" "+i);
}
}
public static void main(String[] args)
{
var t=new DaemonThread();
//将此线程设置为后台线程
t.setDaemon(true);
//启动后台线程
t.start();
for(var i=0;i<10;i++)
{
System.out.println(Thread.currentThread().getName()+" "+i);
}
//-----程序执行到此处,前台线程(main线程)结束-----
//后台线程耶随之结束
}
}
main 0
main 1
main 2
main 3
main 4
main 5
main 6
main 7
main 8
main 9
Thread-0 0
Thread-0 1
Thread-0 2
Thread-0 3
Thread-0 4
Thread-0 5
Thread-0 6
Thread-0 7
上面程序t.setDaemon(true);先将t线程设置为后台线程,然后启动该线程,本来该线程应该执行到i等于999时才会结束,当运行程序不难发现后台线程无法运行到999,因为主线程也就是程序中唯一的前台线程运行结束时,JVM会主动退出,因而后台线程也就被结束。
Thread类还有一个isDaemon()方法,用于判断指定线程是否为后台线程。
从上面的程序可以看出,主线程默认时前台线程,t线程耶默认是前台线程,并不是所有的线程默认都是前台线程,有些线程默认就是后台线程——前台线程创建的子线程默认是前台线程,后台线程创建的子线程默认是后台线程。
前台线程死亡后,JVM会通知后台线程死亡,但从它接受指令到做出响应,需要一定时间。而且要将某个线程设置为后台线程,必须在该线程启动之前设置,也就是说,setDaemon(true)必须在start()方法之前调用,否则会引发IllegalThreadException异常。
三、线程休眠
3.1 sleep()方法
如果我们需要让当前正在执行的线程暂停一段时间,并进入阻塞状态,则可以通过调用Thread类的静态sleep方法,sleep方法有两种重载的形式:
static void sleep(long millis):让当前正在执行的线程暂停millis毫秒,并进入阻塞状态,该方法受到系统计时器和线程调度器的精度和准确度的影响。
static void sleep(long millis, int nanos):让当前正在执行的线程暂停millis毫秒加nanos毫微妙,并进入阻塞状态,该方法受到系统计时器和线程调度器的精度和准确度的影响。
当前线程调用sleep()方法进入阻塞状态后,在其睡眠的时间内,该线程不会获得执行机会。即使系统中没有其他可执行的线程,处于sleep()中的线程也不会执行,因此sleep()方法常用来暂停程序执行。
下面程序调用sleep()方法来暂停主线程的执行,因为该程序只有一个主线程,当主线程进入睡眠后,系统没有可执行的线程,所以可以看到程序在sleep()方法处暂停。
package section4;
import java.util.Date;
public class SleepTest
{
public static void main(String[] args)
throws Exception
{
for(var i=0;i<10;i++)
{
System.out.println("当前时间:"+new Date());
//调用sleep方法让当前线程暂停1s
Thread.sleep(1000);
}
}
}
当前时间:Sun May 10 21:32:06 CST 2020
当前时间:Sun May 10 21:32:08 CST 2020
当前时间:Sun May 10 21:32:09 CST 2020
当前时间:Sun May 10 21:32:10 CST 2020
当前时间:Sun May 10 21:32:11 CST 2020
当前时间:Sun May 10 21:32:12 CST 2020
当前时间:Sun May 10 21:32:13 CST 2020
当前时间:Sun May 10 21:32:14 CST 2020
当前时间:Sun May 10 21:32:15 CST 2020
当前时间:Sun May 10 21:32:16 CST 2020
3.2 yield()方法
yield()方法是一个和sleep方法有点相似的方法,它也是一个Thread类提供的一个静态方法,它也可以让当前正在执行的线程暂停,但它不会阻塞该线程。yield只是让当前线程暂停一下,让系统的线程调度器重新调度一次,完全可能的情况是:当某个线程调用了yield方法暂停之后,线程调度器又将其调度出来重新执行。
实际上,当某个线程调用了yield方法暂停之后,只有优先级与当前线程相同,或者优先级比当前线程更高的、就绪状态的线程才会获得执行的机会。
3.3 关于sleep()方法和yield()方法的区别
1、sleep方法暂停当前线程后,会给其他线程执行机会,不会理会其他线程的优先级。但yield方法只会给优先级相同,或优先级更高的线程执行机会。
2、sleep方法会将线程转入阻塞状态,直到经过阻塞时间才会转入就绪状态。而yield不会将线程转入阻塞状态,它只是强制当前线程进入就绪状态。因此完全有可能某个线程调用3、yield方法暂停之后,立即再次获得处理器资源被执行。
4、sleep方法声明抛出了InterruptedException异常,所以调用sleep方法时要么捕捉该异常,要么显式声明抛出该异常。而yield方法则没有声明抛出任何异常。
sleep方法比yield方法有更好的可移植性,通常不要依靠yield来控制并发线程的执行
四、改变线程的优先级
每个线程执行时都有具有一定的优先级,优先级高的线程获得较多的执行机会,而优先级低的线程则获得较少的执行机会。
每个线程默认的优先级都与创建它的父线程具有相同的优先级,默认情况下,main线程的具有普通优先级,由main线程创建的子线程也有普通优先级。
Thread提供了setPriority(int newPriority)和getPriority()方法来设置和返回指定线程的优先级,其中setPriority方法的参数可以是一个整数,范围是1~10之间,也可以使Thread类的三个静态常量:
MAX_PRIORITY:其值是10。
MIN_PRIORITY:其值是1。
NORM_PRIORITY:其值是5。
下面程序使用了setPriority()方法来改变主线程的优先级,并使用该方法改变两个线程的优先级,从而可以看出优先级高的线程将会获得更多的执行机会。
package section4;
public class PriorityTest extends Thread
{
//定义一个有参数的构造器,由于创建线程时指定name
public PriorityTest(String name)
{
super(name);
}
public void run()
{
for(var i=0;i<10;i++)
{
System.out.println(getName()+" "+i+",其优先级为:"+getPriority());
}
}
public static void main(String[] args)
{
//改变主线程的优先级
Thread.currentThread().setPriority(6);
for(var i=0;i<30;i++)
{
if(i==10)
{
var low=new PriorityTest("低级");
low.start();
System.out.println("创建之初的优先级:"+low.getPriority());
//设置该线程为最低优先级
low.setPriority(MIN_PRIORITY);
}
if(i==20)
{
var high=new PriorityTest("高级");
high.start();
System.out.println("创建之初的优先级:"+high.getPriority());
//设置该线程为最低优先级
high.setPriority(MAX_PRIORITY);
}
}
}
}
上面程序Thread.currentThread().setPriority(6);改变了主线程的优先级,这样由main线程所创建的子线程的优先级默认都是6,所以程序直接输出low、high两个线程的优先级时都看到6.接着程序将low线程的优先级设为Thread.MIN_PRIORITY,将high线程的优先级设为Thread.MAX_PRIORITY。
运行上面的程序将看到如下运行效果:
优先级级别需要操作系统的支持,遗憾的是不同的操作系统上的优先级并不相同,而且不能很好地和Java提供的10个优先级相对应,例如windows 2000提供了7个优先级。而应该使用MAX_PRIORITY、MIN_PRIORITY和NORM_PRIORITY三个静态常量来设置优先级,这样才可以包装程序具有很好的可移植性。