目录
一、创建线程
1.方法一:继承 Thread 类
class MyThread extends Thread {
@Override
public void run() {
System.out.println("这里是线程运行的代码");
}
}
public class Thread{
public void main(String[] args){
//创建线程对象
MyThread thread = new MyThread();
//通过.strat来启动线程
thread.start();
}
}
2.方法二 :实现 Runnable 接口
class MyRunnable implements Runnable {
@Override
public void run() {
System.out.println("这里是线程运行的代码");
}
}
public class Thread{
public void main(String[] args){
//创建线程对象
Thread thread = new Thread(MyRunnable());
//通过.strat来启动线程
thread.start();
}
}
对比上面两种方法 :
- 继承 Thread 类, 直接使用 this 就表示当前线程对象的引用.
- 实现 Runnable 接口, this 表示的是 MyRunnable 的引用. 需要使Thread.currentThread()
3.变形
1)匿名内部类
创建Thread匿名对象
// 使用匿名类创建 Thread 子类对象
Thread t1 = new Thread() {
@Override
public void run() {
System.out.println("使用匿名类创建 Thread 子类对象");
}
};
创建Runnable匿名内部类
// 使用匿名类创建 Runnable 子类对象
Thread t2 = new Thread(new Runnable() {
@Override
public void run() {
System.out.println("使用匿名类创建 Runnable 子类对象");
}
});
2)使用Lambda表达式
// 使用 lambda 表达式创建 Runnable 子类对象
Thread t3 = new Thread(() -> System.out.println("使用匿名类创建 Thread 子类对象"));
二、多线程增加运行效率
serial()方法实现串行,parallel() 方法实现并行,System.nanoTime()记录开始或者结束的时间戳
public class Demo1 {
public static final int counter = 1_000_000_000;
public static void main(String[] args) {
//运行串行
serial();
//运行并行
parallel();
}
public static void serial(){
//记录开始时间
long begin = System.nanoTime();
int a = 0;
int b = 0;
for(long i = 0 ; i < counter ; i++){
a++;
}
for(long i = 0 ; i < counter ; i++){
b++;
}
//记录结束时间
long end = System.nanoTime();
System.out.println("串行耗时:" + (end - begin)*1.0/1000/1000 + "ms");
}
public static void parallel(){
//记录开始时间
long begin = System.nanoTime();
//创建线程t1
Thread t1 = new Thread(() -> {
int a = 0;
for(long i = 0 ; i < counter ; i++){
a++;
}
});
//运行线程
t1.start();
//方法自己运行一个循环
int b = 0;
for(long i = 0 ; i < counter ; i++){
b++;
}
//此处需要等待线程t1结束才能记录结束时间,因为方法本身可能结束了,但是线程没有结束,需要等待其结束才是真正的时间,使用join()方法(后续介绍)
try {
t1.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
long end = System.nanoTime();
System.out.println("并行耗时:" + (end - begin)*1.0/1000/1000 + "ms");
}
}
运行结果:
串行耗时:789.0998000000001ms
并行耗时:437.6658ms
可想而知,后者执行更快,其原因在于,串行是两个循环一个一个来执行,而并行是两个几乎同时运行,充分利用CPU资源,从而提高速度。所以同样是20亿次的循环,并行会快。但是同样在计算量比较大的时候,并行才会快与串行;而小量计算的时候,创建线程本身也需要时间开销,这样就不见的一定比串行快
//当循环次数为1000的时候
串行耗时:0.0362ms
并行耗时:0.8917ms
这里补充说明一下,main方法也是一个线程
三、Thread常用方法
1.Thread常用构造方法
方法 | 说明 |
Thread() | 创建线程 |
Thread(Runnable target) | 使用Runnable 对象创建线程对象 |
Thread(String name) | 创建线程对象,并命名 |
Thread(Runnable target,String name) | 使用Runnable对象创建线程对象,并命名 |
【了解】Thread(ThreadGroup group ,Runnable target) | 线程可以被用来分组管理,分好的组即为线程组,【目前了解即可】 |
Thread t1 = new Thread();
Thread t2 = new Thread(new MyRunnable());
Thread t3 = new Thread("这是我的名字");
Thread t4 = new Thread(new MyRunnable(), "这是我的名字");
2.Thread类的几个常见属性
属性 | 获取方法 |
ID | getId() |
名称 | getName() |
状态 | getState() |
优先级 | getPriority() |
是否后台线程 | isDaemon() |
是否存活 | isAlive() |
是否被中断 | isInterrupted() |
- ID 是线程的唯一标识,不同线程不会重复
- 名称是各种调试工具用到
- 状态表示线程当前所处的一个情况(下面我们会进一步说明)
- 优先级高的线程理论上来说更容易被调度到
- 关于后台线程,需要记住一点:JVM会在一个进程的所有非后台线程结束后,才会结束行。
- 是否存活,即简单的理解,为 run 方法是否运行结束了
- 线程的中断问题(下面我们进一步说明)
package leaner.Thread;
public class Demo2 {
public static void main(String[] args) {
Thread thread = new Thread(() -> {
for (int i = 0; i < 10; i++) {
try {
System.out.println(Thread.currentThread().getName() + ": 我还活着");
Thread.sleep(1 * 1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println(Thread.currentThread().getName() + ": 我即将死去");
});
System.out.println(Thread.currentThread().getName()
+ ": ID: " + thread.getId());
System.out.println(Thread.currentThread().getName()
+ ": 名称: " + thread.getName());
System.out.println(Thread.currentThread().getName()
+ ": 状态: " + thread.getState());
System.out.println(Thread.currentThread().getName()
+ ": 优先级: " + thread.getPriority());
System.out.println(Thread.currentThread().getName()
+ ": 后台线程: " + thread.isDaemon());
System.out.println(Thread.currentThread().getName()
+ ": 活着: " + thread.isAlive());
System.out.println(Thread.currentThread().getName()
+ ": 被中断: " + thread.isInterrupted());
thread.start();
while (thread.isAlive()) {
}
System.out.println(Thread.currentThread().getName()
+ ": 状态: " + thread.getState());
}
}
1) 状态(state)
在操作系统中,一个线程状态只有就绪状态和阻塞/睡眠状态,在Java中,一个线程的状态被划分的更细致,这些状态使用枚举类在保存在Thread中
- NEW(新建): 安排了工作, 还未开始行动
- RUNNABLE(可运行): 可工作的. 又可以分成正在工作中和即将开始工作.
- BLOCKED:(阻塞):获得锁后结束等待
- WAITING(等待): 等待通知才能结束等待
- TIMED_WAITING(计时等待): 等待计时结束或收到通知才能结束等待
- TERMINATED(终止): 工作完成了
public class ThreadState {
public static void main(String[] args) {
for (Thread.State state : Thread.State.values()) {
System.out.println(state);
}
}
}
状态间转化示意图:
2)优先级(priority)
每当线程调度有机会选择新线程时,它首先选择具有较高优先级的线程。但是,线程优先高度依赖于系统。当虚拟机依赖于宿主机平台的线程实现时,java线程的优先级会映射到宿主机平台的优先级。平台的线程优先级可能比java内部的10个级别多,也可能少。所以优先级不一定好使。
3)后台线程(isDaemon)
创建两个线程 t1 和 t2 默认都是前台的线程,即使main方法执行完毕,也得等t1和t2执行完,整个进程才能退出。假如t1 和 t2是后台程序,此时如果main执行完毕,整个进程就直接退出 t1 和 t2就强行终止了
4)存活(isAlive)
Thread t对象的生命周期和内核中对应的线程,并不完全一致。创建一个t对象后,在调用start之前,系统中是没有对应线程的。在run()方法执行完了之后,系统中的线程就销毁了,但是t这个对象可能还存在。
通过调用isAlive就能判断当前系统的线程的运行情况,
- 如果调用start之后,run执行完之前,isAlive就返回true;
- 如果调用start之前,run执行完之后,isAlive就返回false;
5)中断线程(interrupt)
对中断处理有两种方法:
- 使用标记位
- 使用interrupt()
使用标记位:(volatile为关键字,将在后续锁的文章中介绍)
public class Demo3 {
public volatile static boolean flag = false;
public static void main(String[] args) {
//创建线程
Thread t1 = new Thread(() -> {
//标志位没有修改将循环
while (!flag) {
//每次打印线程的名字,currenrThread方法为静态方法,获得当前线程的引用,由于使用的是Runnable接口,
//所以无法使用this来进行引用,如果是创建Thread的匿名对象,则可以使用this来说进行指引
System.out.println(Thread.currentThread().getName());
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
//启动线程
t1.start();
try {
//main线程休眠5s
Thread.sleep(5000);
//main线程醒来就将标记修改
flag = true;
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
只不过这种标记位中断存在缺陷, 一个循环体内部如果没有一直去判断是否应该中断程序,那么即使循环体内部运行的时候,已经被标记为中断也只能等到下一个轮回才能检查出来
使用interrupt方法发出中断信号中断程序
Thread 内部包含了一个 boolean 类型的变量作为线程是否被中断的标记
中断线程三个方法
方法 作用 public void interrupt() 中断对象关联的线程,如果线程正在阻塞,则以异常方式通知, 否则设置标志位 public static boolean interrupted() 判断当前线程的中断标志位是否设置,调用后清除标志位 public boolean isInterrupted() 判断对象关联的线程的标志位是否设置,调用后不清除标志位
public static void main(String[] args) {
Thread t1 = new Thread(() -> {
while (true) {
System.out.println(Thread.currentThread().getName());
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
t1.start();
try {
Thread.sleep(5000);
t1.interrupt();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
运行结果:
这里报了一个异常之后,程序接着循环了,很明显不符合我们的预期....下面通过两个方面解释这一问题
第一个原因:这是因为sleep(wait、join,这类方法签名上有抛出异常)会让程序进入轻量级阻塞状态(WAITING、TIME_WAITING)会抛出异常 ,处在这个状态的线程是没有上CPU运行的,本身没有在运行,谈不上中断,对于运行线程来说才有中断这一说法,所以sleep既然没有在运行,然后有被发出中断信号,自然就报了一个异常出来
第二个原因:在TIME_WAITING时收到中断信号,捕获到异常,然后线程被强制唤醒,执行catch语句块内的代码,但是这个代码只是做了简单的打印异常之后就没处理了,所以循环又接着进行了........解决办法是在catch里面添加break跳出循环即可。
如果线程处在BLOCKED状态(即是否等待获得锁状态)则不会抛出InterruptedException,如果线程处在运行状态,也不会抛出中断异常
所以interrupt是无法中断synchronized代码块/方法的。
public static void main(String[] args) {
Thread t1 = new Thread(()->{
Object object = new Object();
synchronized(object){
while(true){
System.out.println("线程正在运行......");
}
}
});
t1.start();
t1.interrupt();
}
运行结果
线程正在运行......
线程正在运行......
线程正在运行......
线程正在运行......
线程正在运行......
线程正在运行......
线程正在运行......
线程正在运行......
线程正在运行......
线程正在运行......
.........
但是可以通过检查线程的中断状态来判断它是否收到过中断信号。
public static void main(String[] args) {
Thread t1 = new Thread(()->{
Object object = new Object();
synchronized(object){
while(true){
System.out.println("线程正在运行......");
if(Thread.currentThread().isInterrupted()){
break;
}
}
}
});
t1.start();
try {
Thread.sleep(10);
t1.interrupt();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
使用interrupted方法和isInterrupted方法来判断线程的中断标记位,二者区别如下
- 前者为静态方法,直接对类使用
- Thread.interrupted() 判断当前线程的中断标志被设置,清除中断标志
- 后者为实例方法,需要获取到当前线程的引用
- Thread.currentThread().isInterrupted() 判断指定线程的中断标志被设置,不清除中断标志
细节补充:
interrupted()是一个静态方法,而Thread内部的标记位是一个实例变量,正常手段不应该能被一个静态变量修改到,其实这个方法内部手段其实和isInterrupted差不多,都使用到了currentThread()静态方法,来获取对象引用,然后对这个这个对象内部的中断标记当前的标记位进行保存,然后判断是否为true来进行修改,然后修改完后有调用了clearInterruptEvent()方法来清除标记位。
public static boolean interrupted() {
Thread t = currentThread();
boolean interrupted = t.interrupted;
// We may have been interrupted the moment after we read the field,
// so only clear the field if we saw that it was set and will return
// true; otherwise we could lose an interrupt.
if (interrupted) {
t.interrupted = false;
clearInterruptEvent();
}
return interrupted;
}
相比之下,isInterrupted方法朴实无华,直接将对象的中断标记进行返回
public boolean isInterrupted() {
return interrupted;
}
3.启动线程【start方法】
之前我们已经看到了如何通过覆写 run 方法创建一个线程对象,但线程对象被创建出来并不意味线程就开始运行了。
- 覆写 run 方法是提供给线程要做的事情的指令清单
- 而调用 start() 方法,才真的在操作系统的底层创建出一个新线程,并且运行run方法,新旧线程为并发关系
4.等待线程【join方法】
有时候需要等待一个线程结束之后才能进行后续工作,这就需要join来进行帮助,这是一个实例方法,哪个线程对象调用它后续代码只有这个线程执行完之后才能运作。
注意这个方法也是抛出一个InterruptedException
public static void main(String[] args) {
Thread t1 = new Thread(()->{
for(int i = 0 ; i < 1_000_000_000 ; i++){
int a = 0;
}
});
t1.start();
try {
System.out.println("t1线程状态"+ t1.getState());
t1.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("t1线程状态"+ t1.getState());
}
【join】的“同形异构”方法
方法 说明 public void join() 等待线程结束 public void join(long millis) 等待线程结束,最多等 millis 毫秒 public void join(long millis, int nanos) 同理,但可以更高精度
5.线程休眠【sleep】
事实上,睡眠的时间不是说睡眠时间一过就立即上CPU,而是说这段睡眠时间是肯定不上CPU,时间过了能不能上CPU也知道
方法 说明 public static void sleep(long millis) throws InterruptedException
休眠当前线程 millis 毫秒 public static void sleep(long millis, int nanos) throwsInterruptedException可以更高精度的休眠
public class ThreadDemo {
public static void main(String[] args) throws InterruptedException {
System.out.println(System.currentTimeMillis());
Thread.sleep(3 * 1000);
System.out.println(System.currentTimeMillis());
}
}
6.让出线程【yield】
yield大公无私,让出CPU
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
while (true) {
System.out.println("张三");
// 先注释掉, 再放开
// Thread.yield();
}
}
}, "t1");
t1.start();
Thread t2 = new Thread(new Runnable() {
@Override
public void run() {
while (true) {
System.out.println("李四");
}
}
}, "t2");
t2.start();
可以看到 :1. 不使用 yield 的时候 , 张三李四大概五五开2. 使用 yield 时 , 张三的数量远远少于李四结论 :yield 不改变线程的状态 , 但是会重新去排队 .
四、使用jconsole查看线程运行
这个应用是Java jdk自己带的,在jdk的并目录下,它只能识别出Java程序
声明:个人笔记使用