今天我们通过代码分析java线程:
一、java 天生支持多线程
1、main 方法模拟一个多线程:
一个
Java
程序从
main()
方法开始执行,然后按照既定的代码逻辑执行,看 似没有其他线程参与,但实际上 Java
程序天生就是多线程程序,因为执行
main() 方法的是一个名称为 main
的线程。
2、代码如下:
/**
*类说明:只有一个main方法的程序
*/
public class OnlyMainTest {
public static void main(String[] args) {
//Java 虚拟机线程系统的管理接口
ThreadMXBean threadMXBean = ManagementFactory.getThreadMXBean();
// 不需要获取同步的monitor和synchronizer信息,仅仅获取线程和线程堆栈信息
ThreadInfo[] threadInfos =
threadMXBean.dumpAllThreads(false, false);
// 遍历线程信息,仅打印线程ID和线程名称信息
for (ThreadInfo threadInfo : threadInfos) {
System.out.println("[" + threadInfo.getThreadId() + "] "
+ threadInfo.getThreadName());
}
}
}
执行后:
[6] Monitor Ctrl-Break //
监控
Ctrl-Break
中断信号的
[5] Attach Listener //
内存
dump
,线程
dump
,类信息统计,获取系统属性等
[4] Signal Dispatcher //
分发处理发送给
JVM
信号的线程
[3] Finalizer //
调用对象
finalize
方法的线程
[2] Reference Handler//
清除
Reference
的线程
[1] main //main
线程,用户程序入口
六个线程其中只有man方法是用户线程,其他的都是守护线程。
二、
线程的启动与中止
启动
启动线程的方式有:
1
、
X extends Thread;
,然后
X.start
2
、
X implements Runnable
;然后交给
Thread
运行
/**
*类说明:新启线程的方式
*/
public class NewThread {
/*扩展自Thread 静态内部类*/
private static class UseThread extends Thread{
@Override
public void run() {
super.run();
System.out.println("I am extendec Thread");
}
}
/*实现Runnable接口的静态内部类*/
private static class UseRunnable implements Runnable{
@Override
public void run() {
System.out.println("I am implements Runnable");
}
}
public static void main(String[] args)
throws InterruptedException, ExecutionException {
UseThread useThread = new UseThread();
useThread.start();
useThread.start();//如果同时调用两次,则抛异常:java.lang.IllegalThreadStateException
UseRunnable useRunnable = new UseRunnable();
new Thread(useRunnable).start();
}
}
Thread 和 Runnable 的区别
Thread
才是
Java
里对线程的唯一抽象,
Runnable
只是对任务(业务逻辑) 的抽象。Thread
可以接受任意一个
Runnable
的实例并执行。
3、中止
线程自然终止
要么是
run
执行完成了,要么是抛出了一个未处理的异常导致线程提前结束。
stop
暂停、恢复和停止操作对应在线程 Thread
的
API
就是
suspend()
、
resume()
和
stop()
。但是这些
API
是过期的,也就是不建议使用的。不建议使用的原因主 要有:以 suspend()
方法为例,在调用后,线程不会释放已经占有的资源(比如 锁),而是占有着资源进入睡眠状态,这样容易引发死锁问题。同样,stop()
方 法在终结一个线程时不会保证线程的资源正常释放,通常是没有给予线程完成资 源释放工作的机会,因此会导致程序可能工作在不确定状态下。正因为 suspend()、 resume()和
stop()
方法带来的副作用,这些方法才被标注为不建议使用的过期方 法。
4、中断:继承 therad 的线程中断方法:
/**
*类说明:如何安全中断线程
*/
public class EndThreadTest {
//静态内部类
private static class UseThread extends Thread{
public UseThread(String name) {
super(name);
}
@Override
public void run() {
String threadName = Thread.currentThread().getName();
System.out.println(threadName+" interrrupt flag ="+isInterrupted());
int i=1;
while(!isInterrupted()){//判断是否已经通知停止的非静态方法,一般常用这个
//while(!Thread.interrupted()){//判断是否已经通知停止的静态方法
i++;
System.out.println("第几次:"+i+";"+threadName+" is running");
System.out.println("第几次:"+i+";"+threadName+"inner interrrupt flag ="
+isInterrupted());
}
System.out.println("第几次:"+i+";"+threadName+" interrrupt flag ="+isInterrupted());
}
}
public static void main(String[] args) throws InterruptedException {
Thread endThread = new UseThread("endThread");
endThread.start();
Thread.sleep(20);
endThread.interrupt();//中断线程,其实设置线程的标识位true,起到通知作用,线程一般不会立刻停止,而是执行完一定的任务后停止。
}
5、两种情况的执行结果类似:静态方法:
非静态方法:
6、 实现runnable接口的线程的中断:
/**
*类说明:实现接口Runnable的线程如何中断
*/
public class EndRunnableTest {
private static class UseRunnable implements Runnable{
@Override
public void run() {
while(!Thread.currentThread().isInterrupted()) {//静态方法判断
System.out.println(Thread.currentThread().getName()
+ " I am implements Runnable.");
}
System.out.println(Thread.currentThread().getName()
+" interrupt flag is "+Thread.currentThread().isInterrupted());
}
}
public static void main(String[] args) throws InterruptedException {
UseRunnable useRunnable = new UseRunnable();
Thread endThread = new Thread(useRunnable,"endThread");
// endThread.start();//执行的线程方法
endThread.run();//执行的仅是业务方法
Thread.sleep(20);
endThread.interrupt();
}
}
执行start方法时,执行的是线程方法:
执行run方法时,执行的是业务方法,而且是死循环执行:
7、由此可知:Thread类是
Java
里对线程概念的抽象,可以这样理解:我们通过
new Thread() 其实只是 new
出一个
Thread
的实例,还没有操作系统中真正的线程挂起钩来。 只有执行了 start()
方法后,才实现了真正意义上的启动线程。 start()方法让一个线程进入就绪队列等待分配
cpu
,分到
cpu
后才调用实现 的 run()
方法,
start()
方法不能重复调用,如果重复调用会抛出异常。 而 run
方法是业务逻辑实现的地方,本质上和任意一个类的任意一个成员方 法并没有任何区别,可以重复执行,也可以被单独调用。阻塞方法中断线程,核心方法:
/**
*说明:阻塞方法中抛出InterruptedException异常后,如果需要继续中断,需要手动再中断一次
*/
@Override
public void run() {
while(!isInterrupted()) {
try {
Thread.sleep(100);//休眠阻塞方法
} catch (InterruptedException e) {
System.out.println(Thread.currentThread().getName()
+" in InterruptedException interrupt flag is "
+isInterrupted());
//手动资源释放
interrupt();
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()
+ " I am extends Thread.");
}
System.out.println(Thread.currentThread().getName()
+" interrupt flag is "+isInterrupted());
}
8、总之,安全的中止则是其他线程通过调用某个线程
A
的
interrupt()
方法对其进行中 断操作,
中断好比其他线程对该线程打了个招呼,“
A
,你要中断了”,不代表
线程
A
会立即停止自己的工作,同样的
A
线程完全可以不理会这种中断请求。 线程通过检查自身的中断标志位是否被置为 true
来进行响应, 线程通过方法
isInterrupted()
来进行判断是否被中断,也可以调用静态方法
Thread.interrupted()
来进行判断当前线程是否被中断,不过
Thread.interrupted() 会同时将中断标识位改写为 false
。 如果一个线程处于了阻塞状态(如线程调用了 thread.sleep
、
thread.join
、 thread.wait 等),则在线程在检查中断标示时如果发现中断标示为
true
,则会在
这些阻塞方法调用处抛出
InterruptedException
异常,并且在抛出异常后会立即 将线程的中断标示位清除,即重新设置为 false
。
不建议自定义一个取消标志位来中止线程的运行
。因为
run
方法里有阻塞调 用时会无法很快检测到取消标志,线程必须从阻塞调用返回后,才会检查这个取 消标志。这种情况下,使用中断会更好,因为:
一、一般的阻塞方法,如
sleep
等本身就支持中断的检查,
二、检查中断位的状态和检查取消标志位没什么区别,用中断位的状态还可 以避免声明取消标志位,减少资源的消耗。
注意:处于死锁状态的线程无法被中断
yield()
方法:使当前线程让出 CPU
占有权,但让出的时间是不可设定的。也 不会释放锁资源。注意:并不是每个线程都需要这个锁的,而且执行 yield( )
的线 程不一定就会持有锁,我们完全可以在释放锁后再调用 yield
方法。 所有执行 yield()
的线程有可能在进入到就绪状态后会被操作系统再次选中 马上又被执行。
9、join
方法:
把指定的线程加入到当前线程,可以将两个交替执行的线程合并为顺序执行。 比如在线程 B
中调用了线程
A
的
Join()
方法,直到线程
A
执行完毕后,才会继续
执行线程
B
。
/**
*类说明:演示Join方法的使用
*/
public class UseJoinTest {
static class Goddess implements Runnable {
private Thread thread;
public Goddess(Thread thread) {
this.thread = thread;
}
public void run() {
System.out.println("Goddess开始排队打饭.....");
try {
if(thread!=null) thread.join();
} catch (InterruptedException e) {
}
SleepTools.second(2);//休眠2秒
System.out.println(Thread.currentThread().getName()
+ " Goddess打饭完成.");
}
}
static class GoddessBoyfriend implements Runnable {
public void run() {
SleepTools.second(2);//休眠2秒
System.out.println("GoddessBoyfriend开始排队打饭.....");
System.out.println(Thread.currentThread().getName()
+ " GoddessBoyfriend打饭完成.");
}
}
public static void main(String[] args) throws Exception {
GoddessBoyfriend goddessBoyfriend = new GoddessBoyfriend();
Thread gbf = new Thread(goddessBoyfriend);
Goddess goddess = new Goddess(gbf);
Thread g = new Thread(goddess);
g.start();
gbf.start();
System.out.println("nan开始排队打饭.....");
g.join();//插队。。。
SleepTools.second(2);//让主线程休眠2秒
System.out.println(Thread.currentThread().getName() + " nan打饭完成.");
}
}
执行结果:
10、线程的优先级
在
Java
线程中,通过一个整型成员变量
priority
来控制优先级,优先级的范 围从 1~10
,在线程构建的时候可以通过
setPriority(int)
方法来修改优先级,默认 优先级是 5
,优先级高的线程分配时间片的数量要多于优先级低的线程。
设置线程优先级时,针对频繁阻塞(休眠或者
I/O
操作)的线程需要设置较 高优先级,而偏重计算(需要较多 CPU
时间或者偏运算)的线程则设置较低的 优先级,确保处理器不会被独占。在不同的 JVM
以及操作系统上,线程规划会 存在差异,有些操作系统甚至会忽略对线程优先级的设定。
11、线程的调度
线程调度是指系统为线程分配
CPU
使用权的过程,主要调度方式有两种: 协同式线程调度(Cooperative Threads-Scheduling) 抢占式线程调度(Preemptive Threads-Scheduling) 使用协同式线程调度的多线程系统,线程执行的时间由线程本身来控制,线 程把自己的工作执行完之后,要主动通知系统切换到另外一个线程上。使用协同 式线程调度的最大好处是实现简单,由于线程要把自己的事情做完后才会通知系 统进行线程切换,所以没有线程同步的问题,但是坏处也很明显,如果一个线程 出了问题,则程序就会一直阻塞。 使用抢占式线程调度的多线程系统,每个线程执行的时间以及是否切换都由 系统决定。在这种情况下,线程的执行时间不可控,所以不会有「一个线程导致 整个进程阻塞」的问题出现。 在 Java
中,
Thread.yield()
可以让出
CPU
执行时间,但是对于获取,线程本身 是没有办法的。对于获取 CPU
执行时间,线程唯一可以使用的手段是设置线程 优先级,Java
设置了
10
个级别的程序优先级,当两个线程同时处于
Ready
状态 时,优先级越高的线程越容易被系统选择执行。 Java 中的线程优先级是通过映射到操作系统的原生线程上实现的,所以线程 的调度最终取决于操作系统,操作系统中线程的优先级有时并不能和 Java 中的 一一对应,所以 Java
优先级并不是特别靠谱。 所以在面试中如果遇到相关的问题,可以这样回答:Java
中的线程是通过映 射到操作系统的原生线程上实现的,所以线程的调度最终取决于操作系统,而操 作系统级别,OS
是以抢占式调度线程,我们可以认为线程是抢占式的。
Java
虚 拟机采用抢占式调度模型,是指优先让可运行池中优先级高的线程占用 CPU
,如 果可运行池中的线程优先级相同,那么就随机选择一个线程,使其占用 CPU
。处 于运行状态的线程会一直运行,直至它不得不放弃 CPU
。而且操作系统中线程的 优先级有时并不能和 Java
中的一一对应,所以
Java
优先级并不是特别靠谱。但 是在 Java
中,因为
Java
没有提供安全的抢占式方法来停止线程,要安全的停止 线程只能以协作式的方式。
12、守护线程
Daemon
(守护)线程是一种支持型线程,因为它主要被用作程序中后台调 度以及支持性工作。这意味着,当一个 Java
虚拟机中不存在
非
Daemon
线程的 时候,Java
虚拟机将会退出。可以通过调用
Thread.setDaemon(true)
将线程设置 为 Daemon
线程。我们一般用不上,比如垃圾回收线程就是
Daemon
线程。
Daemon
线程被用作完成支持性工作,但是在
Java
虚拟机退出时
Daemon
线 程中的 finally
块并不一定会执行。在构建
Daemon
线程时,不能依靠
finally
块中 的内容来确保执行关闭或清理资源的逻辑。
总结如图: