一、线程的状态
前面的文章中已经研究过线程从创建到销毁这之间的各种状态以及各种状态之间切换的条件,但是都比较零碎。这里将这部分知识用一张系统的关系图来表示。
从图中可以看出,调用相关的一些方法后线程会在几个状态之间切换,这些状态之间有些可以双向切换,比如waiting和runnable状态之间可以循环的互相切换(通过等待/通知机制),而有些状态之间只能单向切换。下面对各种状态进行一个简单的总结。
NEW状态是线程实例化之后还没有执行start()方法时所处的状态;
RUNNABLE状态是线程进入运行时的状态;
TERMINATED状态是线程销毁时的状态;
TIMED_WAITING状态代表线程执行了sleep()方法,线程在持有锁的情况下呈等待状态,等待时间到达继续执行;
BLOCKED状态出现在线程等待某个锁的时候,当线程需要获取某个锁才能继续执行,而发现此锁正在被占用时进入BLOCKED状态;
WAITING状态是执行了wait()方法后线程所处的状态,此时线程主动释放锁并等待被唤醒;
ps:用来查看线程状态的方法是Thread.getState()
二、线程组
在处理线程的时候,我们可以把线程归属到某一个组中,并且在组中不仅可以有线程对象,依然可以有线程组。这样的结构类似于文件目录结构,目录下可以存放若干文件以及子目录。
线程组的作用是可以批量的管理线程组或者线程对象,有效地对线程组合线程对象进行组织。
1)线程对象关联线程组:1级关联
1级关联的意思就是父对象中有子对象,但并不创建子孙对象。这种情况很常见,比如为了对一些特定的线程进行有效的组织管理,我们可以创建线程组,见这些线程对象归入这个线程组中,这样就可以堆零散的线程进行有效的组织与规划。
public class ThA extends Thread{
@Override
public void run() {
try {
for(int i=0;i<5;i++) {
System.out.println(Thread.currentThread().getName()+"运行,打印了"+(i+1));
Thread.sleep(1000);
}
}catch(InterruptedException e) {
e.printStackTrace();
}
}
}
public class ThB extends Thread{
@Override
public void run() {
try {
for(int i=0;i<5;i++) {
System.out.println(Thread.currentThread().getName()+"运行,打印了"+(i+1));
Thread.sleep(1000);
}
}catch(InterruptedException e) {
e.printStackTrace();
}
}
}
public class Run {
public static void main(String args[]) {
ThreadGroup group = new ThreadGroup("OceaNier-Group");
Thread tha = new Thread(group, new ThA());
Thread thb = new Thread(group, new ThB());
tha.start();
thb.start();
System.out.println("创建的线程组为"+group.getName());
System.out.println("线程组中活动的线程有"+group.activeCount()+"个");
System.out.println("当前线程所属线程组是"+Thread.currentThread().getThreadGroup().getName());
}
}
运行结果如下:
/*
创建的线程组为OceaNier-Group
Thread-3运行,打印了1
Thread-1运行,打印了1
线程组中活动的线程有2个
当前线程所属线程组是main
Thread-3运行,打印了2
Thread-1运行,打印了2
Thread-1运行,打印了3
Thread-3运行,打印了3
Thread-1运行,打印了4
Thread-3运行,打印了4
Thread-1运行,打印了5
Thread-3运行,打印了5
*/
控制台中打印出了我们创建的线程组的名称以及主线程所在的线程组的名称,还有在指定线程组中处于活跃状态的线程数。
2)多级关联
多级关联的意思就是父对象中创建子对象,自对象中再次创建子孙对象。这种写法在开发中不算常见,因为如果设计成复杂的树形结构的话不便于线程对象的管理,但是jdk仍旧提供了支持多级关联的树形结构。
可以使用
ThreadGroup(ThreadGroup group, String name)
方法来在线程组内继续添加新的线程组。
3)线程组的自动归属特性
自动归属特性的意思就是当创建一个新的线程组的时候,如果不指定它所处的线程组,则新线程组自动归属到当前线程所属的线程组当中;创建线程对象也是一样的,线程对象自动归属到当前线程所属的线程组当中。
4)获取根线程组
获取线程组的父线程组使用的方法是getParent()方法,此方法返回一个线程组对象。当对main线程调用getParent()方法时会返回system线程,继续调用方法则会出现异常。这就说明JVM的跟线程组就是system。
5)批量停止线程组内的方法
当对一个线程组对象调用group.interrupt()方法时,此线程组内所有正在运行的线程会被批量停止。
6)ThreadGroup的enumerate()方法
//此线程组及其子组中的所有活动线程复制到指定数组中。
public int enumerate(ThreadGroup list[]) {
checkAccess();
return enumerate(list, 0, true);
}
//此线程组及其子组中的所有活动线程复制到指定数组中。
public int enumerate(ThreadGroup list[], boolean recurse) {
checkAccess();
return enumerate(list, 0, recurse);
}
//此线程组中的所有活动线程复制到指定数组中。如果 recurse 标志为 true,则还包括对此线程的子组中的所有活动线程的引用。如果数组太小而无法保持所有线程,则 //忽略额外的线程。
private int enumerate(ThreadGroup list[], int n, boolean recurse) {
int ngroupsSnapshot = 0;
ThreadGroup[] groupsSnapshot = null;
synchronized (this) {
if (destroyed) {
return 0;
}
int ng = ngroups;
if (ng > list.length - n) {//防止list放不下线程数目
ng = list.length - n;
}
if (ng > 0) {
System.arraycopy(groups, 0, list, n, ng);//复制线程组
n += ng;
}
if (recurse) { //取得其子组
ngroupsSnapshot = ngroups;
if (groups != null) {
groupsSnapshot = Arrays.copyOf(groups, ngroupsSnapshot);
} else {
groupsSnapshot = null;
}
}
}
if (recurse) {//复制子组
for (int i = 0 ; i < ngroupsSnapshot ; i++) {
n = groupsSnapshot[i].enumerate(list, n, true);
}
}
return n;
}
三、SimpleDateFormat非线程安全
类SimpleDateFormat主要负责日期格式的转换与格式化,但是在多线程环境中,使用SimpleDateFormat类的时候容易出现错误的结果,因为这个类不是线程安全的。
如下示例,创建六个线程分别负责将六个字符串时间转换成Date对象,然后进行校验:
public class MyThread extends Thread{
private SimpleDateFormat sdf;
private String dateString;
public MyThread(SimpleDateFormat sdf, String dateString) {
super();
this.sdf = sdf;
this.dateString = dateString;
}
@Override
public void run() {
try {
//错误情况
Date dateRef = sdf.parse(dateString);
String newDateString = sdf.format(dateRef).toString();
//解决办法一
// Date dateRef = DateTools.parse("yyyy-MM-dd", dateString);
// String newDateString = DateTools.format("yyyy-MM-dd", dateRef);
//解决办法二
// Date dateRef = DateTools2.getSimpleDateFormat("yyyy-MM-dd").parse(dateString);
// String newDateString = DateTools2.getSimpleDateFormat("yyyy-MM-dd").format(dateRef);
if(!newDateString.equals(dateString)) {
System.out.println(this.getName()+"转换出现错误,将"+dateString+"转换成了"+newDateString);
}else {
System.out.println("将"+dateString+"转换成了"+newDateString);
}
} catch (ParseException e) {
e.printStackTrace();
}
}
}
public class Run {
public static void main(String args[]) {
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
String[] dateList = new String[] {"2015-08-30","2015-10-01","2015-10-04","2015-10-06","2015-12-29","2015-12-30"};
MyThread[] mthList = new MyThread[6];
for(int i=0;i<6;i++) {
mthList[i] = new MyThread(sdf, dateList[i]);
}
for(int i=0;i<6;i++) {
mthList[i].start();
}
}
}
运行后部分结果如下:
可见,当多个线程共同使用一个SimpleDateFormat对象的时候容易出现线程安全问题,造成结果错误。那么既然出错的原因在于多个线程共用了一个非线程安全的SimpleDateFormat对象,那么解决的办法就是为每一个线程都创建一个独自拥有的SimpleDateFormat对象,这样线程之间就不会互相干扰了。
解决办法一:
创建一个DateTools类:
public class DateTools {
public static Date parse(String formatPattern, String dateString) throws ParseException{
return new SimpleDateFormat(formatPattern).parse(dateString);
}
public static String format(String formatPattern, Date date) {
return new SimpleDateFormat(formatPattern).format(date);
}
}
将main中的解决办法一部分的注释取消,将错误部分注释。每次调用parse方法和format方法时都重新实例化一个SimpleDateFormat对象,并且使用这个刚刚创建的对象。
运行结果如下:
/*
将2015-12-29转换成了2015-12-29
将2015-10-04转换成了2015-10-04
将2015-12-30转换成了2015-12-30
将2015-10-01转换成了2015-10-01
将2015-10-06转换成了2015-10-06
将2015-08-30转换成了2015-08-30
*/
解决办法二:使用ThreadLocal类
创建DateTools2类
public class DateTools2 {
private static ThreadLocal<SimpleDateFormat> t1 = new ThreadLocal<SimpleDateFormat>();
public static SimpleDateFormat getSimpleDateFormat(String datePattern) {
SimpleDateFormat sdf = null;
sdf = t1.get();
if(sdf == null) {
sdf = new SimpleDateFormat(datePattern);
t1.set(sdf);
}
return sdf;
}
}
将属于每个线程自己的SimpleDateFormat对象放入相应线程自己的ThreadLocal内,使线程间互不影响。
运行结果如下:
/*
将2015-12-29转换成了2015-12-29
将2015-10-04转换成了2015-10-04
将2015-12-30转换成了2015-12-30
将2015-10-01转换成了2015-10-01
将2015-10-06转换成了2015-10-06
将2015-08-30转换成了2015-08-30
*/
四、线程异常处理
1)线程运行时异常
如果线程在运行的过程中出现异常则可以通过相关的配置对线程抛出的异常进行捕获并处理,这个时候就要用到UncaughtExceptionHandler类。其中的方法setUncaughtExceptionHandler()可以给指定的线程设置异常捕捉器,进行异常地捕捉和处理;另外还可以使用Thread中的setDefaultUncaughtExceptionHandler()类方法对此线程类的所有对象统一设置异常捕捉器。
public class MyThread extends Thread{
@Override
public void run() {
String num = "OceaNier";
int i = Integer.parseInt(num);
}
}
public class Run {
public static void main(String qrgs[]) {
MyThread mt = new MyThread();
mt.setUncaughtExceptionHandler(new UncaughtExceptionHandler() {
@Override
public void uncaughtException(Thread t, Throwable e) {
System.out.println("线程"+Thread.currentThread().getName()+"出现异常!");
e.printStackTrace();
System.out.println("对于异常的处理...");
}
});
mt.start();
}
}
运行结果如下:
2)线程组内某个线程出现异常
当一个线程组内所有线程都在运行时,如果一个线程出现异常,则其他线程会继续运行下去不受出现异常线程的影响。
public class MyThread1 extends Thread{
private String num;
public MyThread1(ThreadGroup group, String name, String num) {
super(group, name);
this.num = num;
}
@Override
public void run() {
try {
int i = Integer.parseInt(num);
while(true) {
System.out.println("线程"+Thread.currentThread().getName()+"正在循环");
Thread.sleep(1000);
}
}catch(InterruptedException e) {
e.printStackTrace();
}
}
}
public class Run1 {
public static void main(String args[]) {
ThreadGroup group = new ThreadGroup("OceaNier的线程组");
MyThread1[] mt = new MyThread1[5];
for(int i=0;i<4;i++) {
mt[i] = new MyThread1(group, "线程"+(i+1), ""+(i+1));
}
//最后一个线程初始化时将num设置为字母
mt[4] = new MyThread1(group, "线程5", "a");
for(int i=0;i<5;i++) {
mt[i].start();
}
}
}
运行结果如下:
可以看到线程1、2、3、4运行,线程5出现异常并打印异常信息,前四个线程不受影响继续无限循环输出信息。
接下来要解决的是,如果一个线程出现异常,线程组内的所有线程都停止。首先创建一个MyThreadGroup继承ThreadGroup类,并且重写其中的uncaughtException()方法,使之在被调用时执行this.interrupt(),将线程组内所有的线程中断。需要注意的是如果重写了uncaughtException()方法那么线程的run()中不能有catch语句块,如果有则uncaughtException()不执行。
修改后的代码如下:
public class MyThreadGroup extends ThreadGroup{
public MyThreadGroup(String name) {
super(name);
}
@Override
public void uncaughtException(Thread t, Throwable e) {
super.uncaughtException(t, e);
this.interrupt();
}
}
public class MyThread1 extends Thread{
private String num;
public MyThread1(ThreadGroup group, String name, String num) {
super(group, name);
this.num = num;
}
@Override
public void run() {
int i = Integer.parseInt(num);
while(this.isInterrupted()==false) {
System.out.println("线程"+Thread.currentThread().getName()+"正在循环");
}
}
}
运行结果如下:
异常出现后其余的线程不再循环输出信息。