1.创建线程
1.1 继承Thread类,重写run方法
class MyThread extends Thread{
@Override
public void run() {
while (true) {
System.out.println("hello world");
try {
//不能throws
//此处是方法重写,对于父类的run方法来说,没有throws xxx异常这样的说法
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
public class demo1 {
public static void main(String[] args) {
MyThread myThread = new MyThread();
myThread.start();
while(true){
System.out.println("hello main");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
- 创建一个类,去继承Thread类,并且重写run方法
run方法:相当于线程的入口方法,线程具体跑起来之后,要做什么,都是通过run来描述的
start方法:创建线程,这个新创建出来的线程会参与到CPU的调度中.
这个线程接下来要执行的工作,就是上面重写的run方法
(这个操作会在底层调用操作系统提供的"创建线程"的API,同时会在操作系统内核里创建出对应的pcb结构,并且加入到对应的链表中).
sleep方法: Thread的静态方法.这两个线程在执行sleep之后,就会进入阻塞状态,当时间到了之后,系统就会唤醒它们,恢复对这两个线程的调度.
当然,系统在进行多个线程调度时,并没有一个明确的顺序,而是按照"随机"的方式进行调度,即"抢占式执行"
问题:
可以直接调用run方法嘛? 不能!!!
myThread.run()
代码运行一下:
根据运行结果可知:
第一种代码,两个线程分别执行自己的循环,都能参与到CPU的调度中,这两个线程在并发的执行.
而第二种代码不会创建新的线程,仍然是在原来的主线程内部.
run只是上面的入口方法(普通的方法),并没有调用系统API,也没有创建出真正的线程
而start会调用系统API,在系统内核中把线程对应的pcb等等给创建出来并管理好,新的线程就能参与调度了
1.2 实现Runnable接口,重写run方法
class MyRunnable implements Runnable{
@Override
public void run() {
while (true){
System.out.println("hello xuexue");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
public class demo2 {
public static void main(String[] args) {
MyRunnable myRunnable = new MyRunnable();
Thread t = new Thread(myRunnable);
t.start();
while (true){
System.out.println("ppsb");
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
- 创建一个类,实现Runnable 接口,并重写run方法
- 创建 Thread 类实例, 调用 Thread 的构造方法时将 Runnable 对象作为 target 参数.
Thread这里是直接要把完成的工作,放到了Thread的run方法里
Runnable这里则是分开了,把要完成的工作放到Runnable中,再让Runnable和Thread配合
1.3 基于匿名内部类,继承Thread,重写run方法
public class demo3 {
public static void main(String[] args) {
Thread t = new Thread(){
@Override
public void run() {
while (true){
System.out.println("hello world");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
};
t.start();
while (true){
System.out.println("hello hi");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
- 在demo3这个类里创建了一个子类,这个子类继承自Thread
- 在子类中,重写了run方法
- 创建了该子类的实例,并且使用t这个引用来指向
1.4 基于匿名内部类,实现Runnable,重写run方法
public class demo4 {
public static void main(String[] args) {
Thread t = new Thread(new Runnable() {
@Override
public void run() {
while (true){
System.out.println("hello");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
});
t.start();
while (true){
System.out.println("hihi");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
- 创建了一个Runnable的子类(实现Runnable)
- 重写run方法
- 把子类创建出实例,把这个实例传给Thread的构造方法
1.5 使用lambda表达式,实现run方法的内容
public class demo5 {
public static void main(String[] args) {
Thread t = new Thread(() ->{
while (true){
System.out.println("hello");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
t.start();
}
}
- lambad表达式,本质上就是一个"匿名函数",主要用来作为回调函数来使用
当然还可以基于Callable,基于线程池来创建线程
2.Thread的常见构造方法
方法 | 说明 |
Thread() | 创建线程对象 |
Thread(Runnable target) | 使用Runnable对象创建线程对象 |
Thread(String name) | 创建线程对象,并命名 |
Thread(Runnble target,String name) | 使用Runnable对象创建线程对象,并命名 |
Thread(ThreadGroup group, Runnable target) | 线程可以被用来分组管理,分好的组即为线程组(了解即可) |
下面演示一下Thread(String name):
public class demo6 {
public static void main(String[] args) {
Thread t = new Thread(() ->{
while (true){
System.out.println("hahha");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}, "myThread");
t.start();
}
}
3.Thread的几个常见属性
属性 | 获取方法 |
ID | getId() |
名称 | getName() |
状态 | getState() |
优先级 | getPriority() |
是否后台线程 | isDaemon() |
是否存活 | isAlive() |
是否被终止 | isInterrupted() |
- ID:
- 线程的身份标识(在JVM这里给线程设定的标识).
- 一个线程,可以有好几个身份标识,都是相互独立的.
- 状态:
- Java中的线程状态,和操作系统中有一定差异
- 优先级:
- 设置/获取优先级,作用并不是很大. (线程的调度主要还是系统内核来负责的,系统调度的速度太快了)
- 是否后台线程:
- 前台线程会影响到进程结束,如果前台线程没执行完,进程是不会结束的
- 后台线程不影响进程结束.
- 创建的线程,默认是前台线程.
- 一个进程中所有的前台线程都执行完,退出了,及时此时存在后台线程仍未执行完,也会随着进程一起退出
- 是否存活:
- Thread对象的生命周期,并不是和系统中的线程完全一致的!!
- 一般是Thread对象先创建好,手动调用start,内核才能真正创建出线程.
- 消亡的时候,可能是Thread对象先结束了生命周期(没有引用指向这个对象),也可能是Thread对象还在,内核中的线程把run执行完了,就结束了
- 是否被终止:
- 一个线程的run方法执行完毕,就算终止了,此处的终止线程,就是想办法让run尽快的执行完毕
4.线程方法的使用
4.1 启动一个线程-start()
- start方法调用系统的API完成线程创建工作
- start方法本身的执行是一瞬间就完成的
- 调用start方法后,代码会立即继续执行start后续的逻辑
4.2 终止一个线程
4.2.1 手动设定标志位
(通过这个手动设置的标志位,让run尽快结束)
第一种方式:
(通过内部类访问外部类的成员)
public class demo8 {
//写作成员变量就不是触发变量捕获的逻辑了,而是"内部类访问外部类的成员"
public static boolean isQuit = false;
public static void main(String[] args) throws InterruptedException {
//boolean isQuit = false;
Thread t = new Thread(() ->{
while (!isQuit){
System.out.println("hello");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
t.start();
//主线程这里执行一些其它逻辑之后,让t线程结束
Thread.sleep(3000);
//修改标志位
isQuit = true;
System.out.println("t线程结束");
}
}
第二种方式:
(lambda变量捕获)
问:
既然lambda表达式的执行时机是靠后的,这是否就导致,当后续真正执行lambda时,局部变量isQuit已经销毁了呢?
答:
这种情况是客观存在的,让lambda去访问一个已经被销毁的变量是明显不合理的.
所以lambda引入了"变量捕获"这样的机制.
lambda看起来是在内部访问外部的变量,其实本质上是把外部的变量给复制了一份到lambda里面(这样就可以解决刚才生命周期的问题了)
不过,变量捕获这里有个限制:
(虽然没有使用final修饰,但是并没在代码中修改变量,此时这个变量就可以视为是final)
如果这个变量想要进行修改,就不能进行变量捕获了
public static void main(String[] args) throws InterruptedException {
boolean isQuit = false;
Thread t = new Thread(() ->{
while (!isQuit){
System.out.println("hello");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
t.start();
//主线程这里执行一些其它逻辑之后,让t线程结束
Thread.sleep(3000);
//修改标志位
//isQuit = true;
System.out.println("t线程结束");
}
4.2.2 直接Thread类,提供好了现成的标志位,不用手动去设置了
Thread对象内部,提供了一个标志位
true:线程应该要结束
false:线程先不必结束
//线程终止,使用Thread自带的标志位
public class demo9 {
public static void main(String[] args) throws InterruptedException {
Thread t = new Thread(() ->{
//!Thread.currentThread()其实就是t
//但是lambda表达式就是在构造t之前就定义好的,编译器看到的lambda里的t就会认为这是一个还没初始化的对象
while (!Thread.currentThread().isInterrupted()){
System.out.println("jjj");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
//e.printStackTrace();
break;
}
}
});
t.start();
Thread.sleep(3000);
//把标志位改为true
t.interrupt();
}
}
下面是一些注意事项:
解释一下:t线程正在sleep,然后被interrupt给唤醒了
- 线程在sleep过程中,其它线程调用interrupt方法,就会强制使sleep抛出一个异常,sleep就被立即唤醒了,sleep在被唤醒的同时,会自动清除前面设置的标志位,此时如果想继续让线程结束,直接在catch中,加个break即可
- 当然,手动设置标志位,是没法唤醒sleep的
当sleep被唤醒后,接下来有以下几种操作方式:
- 立即停止循环,立即结束线程(break)
- 继续做点别的事,过一会再结束线程(catch中执行别的逻辑,执行完了再break)
- 忽略终止的请求,继续循环(不写break)
4.3 等待一个线程-join()
多个线程是并发执行的,具体的执行过程,都是由操作系统负责调度的!!!
而这个调度线程的过程是"随机的",无法确定,线程执行的先后顺序
等待线程,是一种规划线程结束顺序的手段
A,B两个线程,如果希望B先结束,让A线程中调用B.join()的方法,此时,B还没执行完,A线程就会进入"阻塞"状态(让代码暂时不执行了,该线程暂时不去CPU上参与调度),相当于给B留下了执行的时间,B执行完毕之后,A再从阻塞状态中恢复回来,并且继续往后执行,当然,如果A执行到B.join()时,B已经执行完了,A就不必阻塞了,直接往下执行即可
public class demo10 {
public static void main(String[] args) {
Thread b = new Thread(() ->{
for (int i = 0;i < 5; i++){
System.out.println("bbb");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("b结束了");
});
Thread a = new Thread(() ->{
for (int i = 0; i < 3; i++){
System.out.println("aaa");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
try {
//如果b还没结束,a会产生堵塞
b.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("a结束了");
});
b.start();
a.start();
}
}
一般来说更推荐此方法,有最大等待时间:
当然,join也能被interrupt唤醒,唤醒之后自动清除标志位
4.4 获取当前线程引用
返回当前线程对象的引用
上述代码中,Thread提供了静态方法currentThread,在哪个线程调用这个方法,就能够获取到哪个线程的引用
注意:
判定标志位:
Thread.interrupted() 静态方法,判定标志的同时会清除标志位,不可取
Thread.currentThread,isInterrupted() 成员方法,可取
4.5 休眠当前线程
在上述代码的实现过程中,对休眠当前线程的方法已经有了具体的认识,这里不过多解释
方法 | 说明 |
public static void sleep(long millis) throws InterruptedException | 休眠当前线程 millis 毫秒 |
public static void sleep(long millis, int nanos) throws InterruptedException | 可以更高精度的休眠 |