1 线程的创建
创建线程的方式分为:创建线程类和匿名内部类
- start()方法实际上是给CPU注册当前线程,并且触发run()方法;
- 线程的执行必须是调用start()方法,如果调用run()方法的话就变成了普通类的执行;
- 应该先创建子线程,再执行主线程的任务,不然子线程会是最先执行完的,而结束。
1.1 直接使用Thread创建
构造器方法:public Thread(), public Thread(String name)
public class ThreadTesting {
public static void main(String[] args) {
Thread t1 = new Thread("t1"){
@Override
public void run(){
//执行任务代码
}
};
t1.start();
}
}
也可以简化创建代码
public class ThreadTesting {
public static void main(String[] args) {
Thread t1 = new Thread(()->{
//需要执行的代码
},"t1");
t1.start();
}
}
优点:编码简单
1.2 使用Runnable配合Thread创建
该方式可以实现线程和要执行的代码分开,将要执行的代码写在Runnable中而创建线程将Runnable对象传入就可以。
构造器方法:public Thread(Runnable target), public Thread(Runnable target, String name)
public class ThreadRunnable {
public static void main(String[] args) {
Runnable runnable = new Runnable() {
@Override
public void run() {
System.out.println("runnableTesting");
}
};
Thread t1 = new Thread(runnable);
//给线程命名
Thread t2 = new Thread(runnable,"t2");
t1.start();
t2.start();
}
}
缺点:代码稍微复杂
优点:
线程任务类只是实现了 Runnable 接口,可以继续继承其他类,避免了单继承的局限性
同一个线程任务对象可以被包装成多个线程对象
适合多个多个线程去共享同一个资源
实现解耦操作,线程任务代码可以被多个线程共享,线程任务代码和线程独立
线程池可以放入实现 Runnable 或 Callable 线程任务对象
1.3 使用FutureTask配合Thread
处理有返回参数的结果,使用线程.get()方法来阻塞获取结果。
步骤:
定义一个线程任务类实现 Callable 接口,申明线程执行的结果类型
重写线程任务类的 call 方法,这个方法可以直接返回执行的结果
创建一个 Callable 的线程任务对象
把 Callable 的线程任务对象包装成一个未来任务对象
把未来任务对象包装成线程对象
调用线程的 start() 方法启动线程
因为Thread只能执行Runnable对象,而FutureTask就是Runnable对象,所以使用未来任务类将Callbale进行包装。
public V get() : 同步等待 task 执行完毕的结果,如果在线程中获取另一个线程执行结果,会阻塞等待,用于线程同步
public class ThreadDemo {
public static void main(String[] args) {
//创建Callable线程任务对象
Callable call = new MyCallable();
//把 Callable 的线程任务对象包装成一个未来任务对象
FutureTask<String> task = new FutureTask<>(call);
//创建线程
Thread t = new Thread(task);
//执行线程
t.start();
try {
String s = task.get(); // 等待call的结果,获取call方法返回的结果(正常/异常结果)
System.out.println(s);
} catch (Exception e) {
e.printStackTrace();
}
}
public class MyCallable implements Callable<String> {
@Override//重写线程任务类方法,该方法的结果能够通过get()来获得
public String call() throws Exception {
return Thread.currentThread().getName() + "->" + "Hello World";
}
}
简化代码:
public class FutureTaskThread {
public static void main(String[] args) throws ExecutionException, InterruptedException {
FutureTask<Integer> futureTask = new FutureTask<>(()->{
System.out.println("futureTask");
return 100;
});
new Thread(futureTask,"futureTaskThread").start();
//获得线程返回的结果
Integer res = futureTask.get();
System.out.println("返回的结果是"+res);
}
}
优缺点同Runnable,只是能够得到线程运行的结果
2 运行原理
Java虚拟机栈:每个线程启动后,虚拟机就会为其分配一块栈内存
-
每个栈由多个栈帧(Frame)组成,对应着每次方法调用时所占用的内存
-
每个线程只能有一个活动栈帧,对应着当前正在执行的那个方法
线程上下文切换(Thread Context Switch):一些原因导致 CPU 不再执行当前线程,转而执行另一个线程
-
线程的 CPU 时间片用完(被动)
-
垃圾回收(被动)
-
有更高优先级的线程需要运行(被动)
-
线程自己调用了 sleep、yield、wait、join、park、synchronized、lock 等方法(主动)
程序计数器(Program Counter Register):记住下一条 JVM 指令的执行地址,是线程私有的
当 Context Switch 发生时,需要由操作系统保存当前线程的状态,并恢复另一个线程的状态,包括程序计数器、虚拟机栈中每个栈帧的信息,如局部变量、操作数栈、返回地址等
3 线程相关方法
方法 | 说明 |
---|---|
public void start() | 启动一个新线程;Java虚拟机调用此线程的run方法。(让线程进入就绪状态,运行与否看调度器)每个线程的start方法只能调用一次 |
public void run() | 线程启动后调用该方法。(如果传递了runnable参数,则启动后会调用runnable中的run方法否则默认不进行任何操作) |
public void setName(String name) | 给当前线程取名字 |
public void getName() | 获取当前线程的名字 线程存在默认名称:子线程是Thread-索引,主线程是main |
public static Thread currentThread() | 获取当前线程对象,代码在哪个线程中执行 |
public static void sleep(long time) | 让当前线程休眠多少毫秒再继续执行 Thread.sleep(0) : 让操作系统立刻重新进行一次cpu竞争 |
public static native void yield() | 提示线程调度器让出当前线程对CPU的使用 |
public final int getPriority() | 返回此线程的优先级 |
public final void setPriority(int priority) | 更改此线程的优先级,常用1 5 10 |
public void interrupt() | 中断这个线程,异常处理机制 |
public static boolean interrupted() | 判断当前线程是否被打断,清除打断标记 |
public boolean isInterrupted() | 判断当前线程是否被打断,不清除打断标记 |
public final void join() | 等待这个线程结束 |
public final void join(long millis) | 等待这个线程死亡millis毫秒,0意味着永远等待 |
public final native boolean isAlive() | 线程是否存活(还没有运行完毕) |
public final void setDaemon(boolean on) | 将此线程标记为守护线程或用户线程 |
3.1 run和start
run():包含了要执行这个线程的内容,方法执行结束,线程也随之结束。但是如果直接调用run方法的话不是开启新的线程,而是在主线程中顺序执行run方法。
start():方法启动新的线程,让该线程处于就绪状态,并且通过新线程间接调用run方法。start线程只能调用一次。
面试题:为什么run方法不能抛出异常,只能捕获异常?
1. 因为run()方法是Runnable接口里面的方法,而Runnable接口在定义run()方法的时候没有抛出任何异常,所以子类在重写run()方法的时候要小于或等于父类(Runnable)的run()方法的异常,所以父类没有抛出异常,子类不能抛出异常
2. 异常不能跨线程传播回 main() 中,因此必须在本地进行处理
3.2 sleep和yield
sleep:
调用 sleep 会让当前线程从 Running 进入
Timed Waiting
状态(阻塞)sleep() 方法的过程中,线程不会释放对象锁
其它线程可以使用 interrupt 方法打断正在睡眠的线程,这时 sleep 方法会抛出 InterruptedException
睡眠结束后的线程未必会立刻得到执行,需要等待CPU时间片
建议用 TimeUnit 的 sleep 代替 Thread 的 sleep 来获得更好的可读性,其内部也是Thread的sleep方法,只不过是换算了单位,有着更好的可读性
sleep的一个应用是在while(true)循环中加上一个较小的sleep,防止循环空转浪费cpu
yield:
调用 yield 会让提示线程调度器让出当前线程对 CPU 的使用
具体的实现依赖于操作系统的任务调度器
会放弃 CPU 资源,锁资源不会释放
3.3 优先级priority
线程优先级会提示(hint)调度器优先调度该线程,但这仅仅是一个提示,调度器可以忽略它
如果 cpu 比较忙,那么优先级高的线程会获得更多的时间片,但 cpu 闲时,优先级几乎没作用
3.4 join()
public final void join():等待该线程结束 可以添加参数,设置最长等待时间
原理:轮询检查alive状态
synchronized (t1) {
// 调用者线程进入 t1 的 waitSet 等待, 直到 t1 运行结束
while (t1.isAlive()) {
t1.wait(0);
}
}
t1 会强占 CPU 资源,当调用某个线程的 join 方法后,该线程抢占到 CPU 资源,就不再释放,直到线程执行完毕
join方法实现线程同步:
join 实现线程同步,因为会阻塞等待另一个线程的结束,才能继续向下运行
需要外部共享变量,不符合面向对象封装的思想
必须等待线程结束,不能配合线程池使用
Future 实现(同步):get() 方法阻塞等待执行结果
main 线程接收结果
get 方法是让调用线程同步等待
public class JoinTest {
static int r = 0;
public static void main(String[] args) throws InterruptedException {
test1();
}
private static void test1() throws InterruptedException {
Thread t1 = new Thread(() -> {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
r = 10;
});
t1.start();
t1.join();//不等待线程执行结束,输出的10,注释掉这行输出0
System.out.println(r);
}
}
3.5 interrupt 打断 !!!
interrupt() 它基于「一个线程不应该由其他线程来强制中断或停止,而是应该由线程自己自行停止。」思想,是一个比较温柔的做法,它更类似一个标志位。其实作用不是中断线程,而是「通知线程应该中断了」,具体到底中断还是继续运行,应该由被通知的线程自己处理。
interrupt() 并不能真正的中断线程,这点要谨记。需要被调用的线程自己进行配合才行。
一个线程如果有被中断的需求,那么就需要这样做:
- 在正常运行任务时,经常检查本线程的中断标志位,如果被设置了中断标志就自行停止线程。
- 在调用阻塞方法时正确处理InterruptedException异常。(例如:catch异常后就结束线程。)
相关方法:
方法 | 说明 |
public void interrupt() | 打断这个线程,异常处理机制。注意区分打断正常运行的线程还是正在sleep,wait,join阻塞的线程的区别 |
public static boolean interrupted() | 判断当前线程是否被打断,打断返回 true,清除打断标记,连续调用两次一定返回 false |
public boolean isInterrupted() | 判断当前线程是否被打断,不清除打断标记 |
其他线程通过 该线程的interrupt()方法对其进行一个打断的提醒。线程通过检查自身是否被中断过来进行相应,线程通过调用方法isInterrupted()来查看是否被打断,也可以调用interrupted()对线程的中断表示进行复位。
首先讲interrupt()方法:
打断正常运行的线程的话,并不会清除中断标识,这时调用isInterrupted()返回true。
对正处于sleep,wait,join阻塞状态的线程进行打断的话,这些方法在抛出InterruptedException方法之前,Java虚拟机会先将线程的中断标识清除,这时调用isInterrupted()返回false,然后再返回中断异常。
interrupt()t打断park并不会清除打断标记,并且还会顺便调用unpark(),如下面代码的执行;
public static void test03() throws InterruptedException {
//打断park线程
Thread t1 = new Thread(()->{
System.out.println("park......");
LockSupport.park();
System.out.println("unpark......");
boolean flag = Thread.currentThread().isInterrupted();
System.out.println("打断状态"+ flag);
});
t1.start();
Thread.sleep(500);
//如果注释掉下面这行,则会只输出”parking......“ 说明打断不会清除park不会清除打断标记,并且还会调用unpark()
t1.interrupt();
}
park......
unpark......
打断状态true
再看interrupted()方法:
如果线程处于打断状态,那么调用interrupted()方法以后会返回true,并且清除打断标记,这时如果再调用interrupted()的话会返回false;
最后IsiIterrupted()方法,用来判断打断标记是否为true。
3.6 不推荐的方法
像一些暴力中断线程的方法现在已经不推荐使用,比方说stop(),suspend(),resume()。
4 主线程和守护线程
通过调用Thread.setDaemon(true)来将Thread设置为守护线程,通过Thread.setDaemon(false)来设置用户线程。如果不设置线程属性,那么默认为用户线程。
守护线程:只要非守护线程运行结束,守护线程也会强制结束,不管守护线程代码是否执行完毕。如果没有用户线程,都是守护线程,那么JVM结束。垃圾回收器就是一种守护线程。
用户线程: 主线程结束后用户线程还会继续运行,JVM存活