因为Thread的构造函数中有关于ThradGroup的,所以了解它们之间的关系是有必要的。ThradGroup之间的关系是树的关系,而Thread与ThradGroup的关系就像元素与集合的关系。关系如下:
├─ ThreadGroup[name=system,maxpri=10]
│ ├─Thread[Reference Handler,10,system]
│ ├─Thread[Finalizer,8,system]
│ ├─Thread[Signal Dispatcher,9,system]
│ ├─Thread[Attach Listener,5,system]
│ ├─ThreadGroup[name=main,maxpri=10]
│ │ ├─Thread[main,5,main]
│ │ ├─Thread[Monitor Ctrl-Break,5,main]
│ │ ├─Thread[Thread1,5,main] // 这个线程是自定义的线程
│ │ ├─ThreadGroup[name=MyThreadGroup,maxpri=10] // 自定义线程组
│ │ └─Thread[MyThread2,5,MyThreadGroup] // 自定义线程,添加到指定组
贴上代码:
@Test
public void test02(){
// 创建一个线程,不指定线程组,由系统自动分配
Thread thread1 = new Thread(new Runnable() {
@Override
public void run() {
Object local = new Object();
synchronized (local){
try {
local.wait();
} catch (InterruptedException e) {
//e.printStackTrace();
}
}
}
});
// 设置线程名称
thread1.setName("Thread1");
// 创建一个线程组
ThreadGroup threadGroup = new ThreadGroup("MyThreadGroup");
// 指定创建一个线程,并且指定线程组
Thread thread2 = new Thread(threadGroup, new Runnable() {
@Override
public void run() {
Object local = new Object();
try {
local.wait();
} catch (InterruptedException e) {
// 不处理异常
}
}
}, "MyThread2");
// 启动两个线程
thread1.start();
thread2.start();
// 获取主线程的父线程组
ThreadGroup parentThreadGroup = mainThreadGroup.getParent();
// 将有关此线程组的信息打印到标准输出。此方法仅对调试有用。
parentThreadGroup.list();
}
/*
以下是打印的结果
java.lang.ThreadGroup[name=system,maxpri=10]
Thread[Reference Handler,10,system]
Thread[Finalizer,8,system]
Thread[Signal Dispatcher,9,system]
Thread[Attach Listener,5,system]
java.lang.ThreadGroup[name=main,maxpri=10]
Thread[main,5,main]
Thread[Monitor Ctrl-Break,5,main]
Thread[Thread1,5,main]
java.lang.ThreadGroup[name=MyThreadGroup,maxpri=10]
Thread[MyThread2,5,MyThreadGroup]
*/
其中要明确一下: main方法执行后,将自动创建system线程组合main线程组,main方法所在线程存放在main线程组中
属性 | 基本说明 |
---|---|
name | 线程名称,可以重复,若没有指定会自动生成 |
id | 线程ID,一个正long值,创建线程时指定,终生不变,线程终结时ID可以复用。 |
priority | 线程优先级,取值为1到10,线程优先级越高,执行的可能越大,若运行环境不支持优先级分10级,如只支持5级,那么设置5和设置6有可能是一样的。 |
state | 线程状态,Thread.State枚举类型,有NEW、RUNNABLE、BLOCKED、WAITING、TIMED_WAITING、TERMINATED 5种。 |
ThreadGroup | 所属线程组,一个线程必然有所属线程组。 |
UncaughtExceptionHandler | 未捕获异常时的处理器,默认没有,线程出现错误后会立即终止当前线程运行,并打印错误。 |
Thread类有三个字段,设置线程优先级时可使用:
public Thread(ThreadGroup group,
Runnable target,
String name,
long stackSize)
创建一个新的Thread对象,以便它具有target作为其运行对象,将指定的name,以及属于该线程组group ,并具有指定的堆栈大小 。
这个构造函数与Thread(ThreadGroup,Runnable,String)相同,除了它允许指定线程栈大小之外。 堆栈大小是虚拟机为该线程的堆栈分配的大致的地址空间字节数。 stackSize参数的影响(如果有的话)与平台有关。
在某些平台上,指定了一个较高的值stackSize参数可以允许抛出一个前一个线程来实现更大的递归深度StackOverflowError 。 类似地,指定较低的值可能允许更多数量的线程同时存在,而不会抛出OutOfMemoryError (或其他内部错误)。 所述stackSize参数的值和最大递归深度和并发水平之间的关系的细节是依赖于平台的。 在某些平台上,该值stackSize参数可能没有任何效果。
虚拟机可以自由地对待stackSize参数作为建议。 如果平台的指定值不合理地低,虚拟机可能会改为使用一些平台特定的最小值; 如果指定的值不合理地高,虚拟机可能会使用一些平台特定的最大值。 同样,虚拟机可以自由地按照合适的方式向上或向下舍入指定的值(或完全忽略它)。
对于指定的值为零stackSize参数将使这种构造的行为酷似Thread(ThreadGroup, Runnable, String)构造。
由于此构造函数的行为依赖于平台依赖性质,因此在使用时应特别小心。 执行给定计算所需的线程栈大小可能会因JRE实现而异。 鉴于这种变化,可能需要仔细调整堆栈大小参数,并且可能需要对要运行应用程序的每个JRE实现重复调整。
实现注意事项:鼓励Java平台实现者的记录其实施的行为stackSize参数。
参数
group - 线程组。 如果null并且有一个安全管理器,该组由SecurityManager.getThreadGroup()确定 。 如果没有安全管理员或SecurityManager.getThreadGroup()返回null ,该组将设置为当前线程的线程组。
target - 启动此线程时调用其run方法的对象。 如果null ,这个线程的run方法被调用。
name - 新线程的名称
stackSize - 新线程所需的堆栈大小,或为零表示此参数将被忽略。
异常
SecurityException - 如果当前线程无法在指定线程组中创建线程
从以下版本开始:
1.4
首先要了解什么是Thread.UncaughtExceptionHandler,默认来说当线程出现未捕获的异常时,会中断并抛出异常,抛出后的动作只有简单的堆栈输出。如:
@Test
public void thread01(){
/**
* 1. 了解Thread.UncaughtExceptionHandler
*/
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
int a = 1 / 0;
}
});
// 启动t1线程
t1.start();
/*
那么代码运行到int a=1/0;就会报错:
Exception in thread "Thread-0" java.lang.ArithmeticException: / by zero
at dreamhai.thread.MyThread01$1.run(MyThread01.java:25)
at java.lang.Thread.run(Thread.java:745)
*/
}
这时候如果设置了Thread.UncaughtExceptionHandler,那么处理器会将异常进行捕获,捕获后就可以对其进行处理:
@Test
public void thread02() {
/**
* 自定义UncaughtExceptionHandler
* 测试,当线程在运行时,出现了异常,会根据我们自己的逻辑来处理异常
*/
Thread t2 = new Thread(new Runnable() {
@Override
public void run() {
int a = 1 / 0;
}
});
// 设置线程的名称
t2.setName("t2线程");
// 设置UncaughtExceptionHandler
t2.setUncaughtExceptionHandler(new Thread.UncaughtExceptionHandler() {
@Override
public void uncaughtException(Thread t, Throwable e) {
System.out.println("线程 : [" + t2.getName() + "] 出现了异常, 异常信息:" + e);
}
});
// 启动t2线程
t2.start();
/*
线程 : Thread-0出现了异常, 异常信息:java.lang.ArithmeticException: / by zero
*/
}
如果自己写线程,那么完全可以在run方法内,将所有代码进行try catch,在catch里做相同的操作。UncaughtExceptionHandler的意义在于不对(或者不能对)原有线程进行修改的情况下,为其增加一个错误处理器。
因为stop()方法已经不建议使用了,下面的3.5.4进行详解,所以如何中断一个线程就成了一个问题,一种简单的办法是设置一个全局变量needStop,如下:
@Override
public void run(){
while(!needStop){
//执行某些任务
}
}
或者需要操作耗时较长的方法内,每一步执行之前进行判断:
@Override
public void run(){
//耗时较长步骤1
if(needStop) return;
//耗时较长步骤2
if(needStop) return;
//耗时较长步骤3
}
这样在其他的地方将此线程停止掉,因为停止是在自己的预料下,所以不会有死锁或者数据异常问题(当然你的程序编写的时候要注意)。
其实Thread类早就有类似的功能,那就是Thread具有中断属性。可以通过调用interrupt()方法对线程中断属性设置为true,这将导致如下两种情况:
这样就由程序来决定当检测到中断属性为true时,怎么对线程中断进行处理。因此,上面的代码可以改成:
@Override
public void run(){
while(!Thread.currentThread().isInterrupted()){
//执行某些任务
}
}
---------------------------------------------------------
@Override
public void run(){
//耗时较长步骤1
if(Thread.currentThread().isInterrupted()) return;
//耗时较长步骤2
if(Thread.currentThread().isInterrupted()) return;
//耗时较长步骤3
}
interrupted()的方法名容易给人一种误解,看似和interrupt()方法一样,但是其实际含义是,返回当前中断状态,并将其设置为false。
yield()方法的API容易给人一种误解,它的实际含义是停止执行当前线程(立即),让CPU重新选择需要执行的线程,因为具有随机性,所以也有可能重新执行该线程,通过下面例子了解:
/**
* yield()
* 含义: 指的是,停止当前的线程(立即),让CPU重新选择需要执行的线程,
* 因为具有随机性,所有也有可能重新执行该线程
*/
@Test
public void thread03(){
Thread t1 = new Thread(() -> {
while (true){
System.out.println("当前线程id:["+Thread.currentThread().getId()+
"] 当前线程名称: "+Thread.currentThread().getName());
Thread.yield();
}
});
Thread t2 = new Thread(() -> {
while (true) {
System.out.println("当前线程id:["+Thread.currentThread().getId()+
"] 当前线程名称: "+Thread.currentThread().getName());
Thread.yield();
}
});
// 分别给t1, t2 线程设置名字,
t1.setName("t1--青海");
t2.setName("t2--Dreamhai");
// 启动两个线程
t1.start();
t2.start();
/*
打印结果:
当前线程id:[11] 当前线程名称: t1--青海
当前线程id:[12] 当前线程名称: t2--Dreamhai
当前线程id:[11] 当前线程名称: t1--青海
当前线程id:[12] 当前线程名称: t2--Dreamhai
当前线程id:[11] 当前线程名称: t1--青海
当前线程id:[12] 当前线程名称: t2--Dreamhai
当前线程id:[12] 当前线程名称: t2--Dreamhai
当前线程id:[11] 当前线程名称: t1--青海
当前线程id:[12] 当前线程名称: t2--Dreamhai
当前线程id:[12] 当前线程名称: t2--Dreamhai
当前线程id:[11] 当前线程名称: t1--青海
当前线程id:[12] 当前线程名称: t2--Dreamhai
当前线程id:[11] 当前线程名称: t1--青海
当前线程id:[12] 当前线程名称: t2--Dreamhai
当前线程id:[12] 当前线程名称: t2--Dreamhai
当前线程id:[11] 当前线程名称: t1--青海
由此可见,并不是交替打印的,出现了连续的Dreamhai
(多执行几次更容易看到效果)
*/
}
经过测试yield()和sleep(0)的效果是一样的,sleep(0)底层要么是和yield()一样,要么被过滤掉了(纯靠猜测),不过sleep(0)没有任何意义。要是真打算让当前线程暂停还是应该使用sleep(long millis,int nanos)这个方法,设置几纳秒表示下诚意,或者找到想要让步的线程,调用它的join方法更实际一些。
stop方法会立即中断线程,虽然会释放持有的锁,但是线程的运行到哪是未知的,假如在具有上下文语义的位置中断了,那么将会导致信息出现错误,比如:
@Override
public void run(){
try{
//处理资源并插入数据库
}catch(Exception e){
//出现异常回滚
}
}
如果在调用stop时,代码运行到捕获异常需要回滚的地方,那么将会因为没有回滚,保存了错误的信息。
而suspend会将当前线程挂起,但是并不会释放所持有的资源,如果恢复线程在调用resume也需要那个资源,那么就会形成死锁。当然可以通过你精湛的编程来避免死锁,但是这个方法具有固有的死锁倾向。所以不建议使用。其他暂停方法为什么可用:
jdk的文章详细介绍了方法禁用的原因:文章地址,有空可以看一看,如果你足够大胆,也是可以使用的。
name:当前线程的名称。
parent:当前线程组的父线程组。
MaxPriority:当前线程组的最高优先级,其中的线程优先级不能高于此。
只介绍一个构造方法:
ThreadGroup(ThreadGroup parent, String name) :
API详解(中文,英文)。
这个线程组可以用来管理一组线程,通过activeCount() 来查看活动线程的数量。其他没有什么大的用处。