一,认识线程(Thread)
1,什么是线程?
首先,大家都知道进程,进程就是跑起来的程序,也就是任务,这个任务需要完成某个具体的工作,而线程就是去完成这些工作的流水线。说的更加形象一点,进程就是工厂,而线程就是工厂里面的各个生产流水线。线程包含于进程,一个进程至少含有一个线程。
2,为什么要引入线程?
在现在的环境下,并发编程是刚需,我们可以使用进程达到并发编程的目的。但是,进程的创建与销毁都是很耗时的(资源分配回收频繁导致),所以我们就需要更加轻量级的—线程来达到并发编程的目的。线程的创建,销毁,调度都是比进程要快的。
比如:现在工厂要生产一批某产品(工厂就是进程),工厂内会分成很多条流水线进行生产(流水线就是线程)。现在厂家要加快速度生产,有两个办法:
1,再买一块地皮,开一家相同的工厂来进行制造。(多进程)
2,在原工厂内再多加几条流水线。(多线程)
试问那种方法的成本,效率最高?毫无疑问就肯定是方案二,也就是我们多线程,对于同一个进程中的线程而言,他们是共用同一份资源的,所以不会去频繁的分配资源,回收资源,只会等到最后一个线程销毁的时候才会销毁,那效率自然会高很多,同时也能够充分利用到我们的多核CPU。
我们说在操作系统内核里面,我们使用进程控制块PCB来描述进程的,其实现在看来这个说法也不是很准确,应该说是一组PCB来描述一个进程,一个线程对应一个PCB。
这些PCB上面的内存指针,文件信息都是相同的(资源信息)。而状态,上下文,优先级,记账信息这些都是每个PCB自己独有一份(调度信息)。也正因为如此,我们把进程作为资源分配的基本单位,把线程作为调度执行的基本单位。
【面试题:谈谈进程与线程的区别?----重要】
1,进程包含线程。一个进程至少含有一个线程,叫做主线程。 |
2,线程更加轻量化,因为线程的创建,销毁效率会高很多。 |
3,同一个进程下的线程都是共用同一份资源的。但是进程与进程之间则是相互独立的资源。 |
4,进程是分配资源的基本单位,线程是调度执行的基本单位。 |
【注意:】
对于一个进程而言,它的线程数目增加,效率也会随之增加。但是物极必反,线程数也是有上限的,不是说越多越好,因为当线程数增加到一定程度之后,CPU核心数会被吃满,CPU资源不够用了,这个时候反而会降低效率。
另外,不像多进程,进程与进程之间是相互独立的,多线程下的线程与线程之间共用资源,那么可能就会存在冲突的问题,并且如果说多个线程同时要去修改一个变量,也会产生线程不安全的问题。
最后,因为线程之间可能会相互影响,所以一旦某个线程出现问题没有处理好,是有可能直接把进程给带走的。
3,代码实现多线程
线程是操作系统中的概念,操作系统内核实现了这样的机制,并且给用户也提供了一套供以使用的API。但是这个原生API是C实现的,所以Java也就对这些API进行了封装,改成了Java风格的API来进行使用。
//1,写一个类继承于Thread
class MyThread extends Thread{
@Override
public void run() {
while(true){
System.out.println("hello thread");
try {
Thread.sleep(1000);//睡眠1s
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
public class Threading {
//运行一个java程序就是开启了一个进程,每个进程至少有一个线程,默认这个线程就是main方法所在的线程(主线程)【jvm创建】
//main所在的线程与我们自己创建的线程它是一个并发执行的关系
public static void main(String[] args) {
MyThread myThread = new MyThread();//这里并没有创建一个线程
myThread.start();//调用start方法的时候创建线程,这个线程的工作就是我们run方法里面所写的
//start另外启动一个新的线程,这个新线程是一个新的执行流,与现有线程的执行流不相关。他们是并发(并发+并行)的关系
while(true){
System.out.println("hello main");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
【执行结果:】
可以看到我们的结果是不规律的,那是因为在调度线程的时候,它就是一个不确定的过程,所以是输出hello main还是输出hello
thread自然就是不确定的。上面这种情况我们也称之为抢占式执行。这个是我们在编写多线程代码时候的一个大问题。因为不同的线程之间的调度顺序就是不确定的,可能在这种调度下代码一切正常,但是换了一种调度顺序代码就出错了。这就需要我们程序员去综合考虑所有可能出现的调度情况,保证我们的代码在每种情况下都不会出现bug。
4,创建线程的几种方式
4.1,写一个类继承Thread
class MyThread1 extends Thread{
@Override
public void run() {
while(true){
System.out.println("hello thread");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
public class Demo1{
public static void main(String[] args) {
MyThread1 myThread = new MyThread1();
myThread.start();
while(true){
System.out.println("hello main");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
4.2,使用匿名内部类创建Thread子类
public class Demo3 {
public static void main(String[] args) {
Thread t3 = new Thread(){
@Override
public void run() {
System.out.println("hello thread");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
};
t3.start();
while(true){
System.out.println("hello main");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
4.3,实现Runnable接口
class MyThread2 implements Runnable{
@Override
public void run() {
while(true){
System.out.println("hello thread");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
public class Demo2 {
public static void main(String[] args) {
MyThread2 myThread2 = new MyThread2();
Thread t2 = new Thread(myThread2);//传入实现类对象。解耦合,也更适用于多线程,多个线程干同一个活
t2.start();
while(true){
System.out.println("hello main");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
对于实现Runnable接口的方法是最好的,因为它可以将我们线程要做的工作与线程本身分开,降低耦合性,同时如果后期如果不想利用多线程了,改动也相对较小。多个线程,只要传入的Runnable接口实现类对象是相同的,那么做的工作就是一样的。
4.4,匿名内部类实现Runnable接口
public class Demo4 {
public static void main(String[] args) {
Thread t4 = new Thread(new Runnable() {
@Override
public void run() {
while(true){
System.out.println("hello thread");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
});
t4.start();
while(true){
System.out.println("hello main");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
4.5,lambda表达式
public class Demo5 {
public static void main(String[] args) {
Thread t5 = new Thread(()->{
while(true){
System.out.println("hello thread");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
t5.start();
while(true){
System.out.println("hello main");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
二,Thread类及其常见方法
2.1,构造方法
//1.无参构造Thread对象
Thread t = new Thread();
//2.传入实现了Runnable接口的对象
Thread t = new Thread(Runnable runnable);
//3,给这个线程取名。注意这个名字可以重复
Thread t = new Thread(String name);//这个注意,如果你是子类继承Thread,你需要在子类里面加一个构造函数去调用Thread的这个构造方法
Thread t = new Thread(Runnable runnablb,String name);
2.2,线程的属性
属性:ID | 获取方法:getID() |
---|
注意:这个ID是java对Thread对象做的一个身份标识,他和操作系统内核里面的PCB的pid,以及操作系统提供的线程API里面的线程id不是一件事情。在不同的环境下,我们对应的身份标识也是不一样的。
属性:名称 | 获取方法:getName() |
---|
获取此线程的名字。这个名字可能是JVM自己取的,也可能是我们自己指定的。
属性:状态 | 获取方法:getState() |
---|
获取当前线程的状态。
属性:优先级 | 获取方法:getPriority() |
---|
属性:是否后台线程 | 获取方法:isDaemon() |
---|
我们默认创建的线程是前台线程,main也是前台线程。前台线程会阻止进程退出。如果main线程运行完了,但是还有其他前台线程还没有走完,那么整个java进程是不会退出的。
相反,后台线程不会阻止进程退出,也就是说,当我们的main线程以及其他前台线程都走完之后,即使还有后台进程没有走完,进程也会直接结束掉。
class MyThread extends Thread{
@Override
public void run() {
while(true){
System.out.println("hello");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public MyThread(String name) {
super(name);
}
}
public class Demo7 {
public static void main(String[] args) {
MyThread myThread = new MyThread("这是一个新创建的线程!");
myThread.setDaemon(true);//使用setDaemon将其设置为后台线程。设置操作要在start之前
myThread.start();
System.out.println("main进程结束!");
}
}
设置为后台进程之后,随着main线程结束,myThread这个线程也跟着结束了。只不过要注意,我们在将其设置为后台线程的时候,必须是在start之前,线程开启之后是无法进行设置的。
属性:是否存活 | 获取方法:isAlive() |
---|
我们的Thread对象虽然是和线程一一对应,但是二者的生命周期是不一样的。例如当我们创建出Thread对象之后,在还没有start该线程之前,线程都是不存在的,但是Thread对象是存在的。同理,销毁也是一样,线程销毁了,但是对象还在。对象的生命周期是要长一些的,所以要利用这个方法才能判断内核线程是否还存活。
属性:是否被中断 | 获取方法:isinterrupted() |
---|
2.3,start()与run()的区别
class MyThread3 extends Thread{
@Override
public void run() {
while(true){
System.out.println("hello");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
public class Demo8 {
public static void main(String[] args) {
MyThread3 myThread3 = new MyThread3();
//myThread3.run();
myThread3.start();
System.out.println("main线程结束!");
}
}
可以发现,如果直接调用run()方法,它是没有开启新线程的,只是在main主线程下串行执行代码。但是start()之后是开启了新的线程的,它是在新的线程里面,与main主线程之间并发执行。不过有一个共通的就是run()里面描述的是我们要做的工作。
2.4,线程的中断
我们知道,当我们的run()方法执行完之后,线程才会结束。但是有时候我们希望线程提前中断,所以可以利用一个标志位来决定是否继续执行。
2.4.1,自己定义一个标志位
public class Demo9 {
public static boolean singal = false;
public static void main(String[] args) {
Thread thread = new Thread(()->{
while(!singal){
System.out.println("hello");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
thread.start();
try {//main线程休眠3秒
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
//设置标志位
singal = true;//设置为true之后,run()里面while循环就不再满足执行条件了,线程结束
System.out.println("标志位更改!");
}
}
这个自己设置标志位就是利用一个变量来作为条件,决定进程是否继续执行。
2.4.2,利用系统提供的方法进行设置
public class Demo9 {
public static boolean singal = false;
public static void main(String[] args) {
Thread thread = new Thread(()->{
while(!Thread.currentThread().isInterrupted()){
System.out.println("hello");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
thread.start();
try {//main线程休眠3秒
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
thread.interrupt();//将进程打断
System.out.println("进程已被打断!");
}
}
Thread.currentThread()可以获取到当前进程的实例对象,isInterrupted()可以判断当前进程是否被打断。我们只需要在main线程里面对这个线程进行打断就可以让它提前结束。
但是呢,我们运行起来之后发现报了异常,并且异常报了之后进程也没有停止。其实是因为原来进程中我们是设置了sleep()的,所以我们在进行打断的时候,如果进程是执行状态还好,如果是阻塞状态,那么这个时候进行打断就会报这么个异常,并且一旦报了这个异常,它会提前结束该进程的阻塞状态,然后恢复执行,因为我们并没有对catch子句做一些异常的处理,只是让它输出异常的信息。
所以,我们既然是想让它打断,又存在这个特殊情况,那么就可以在catch子句里面跳出循环就好。
public class Demo9 {
public static boolean singal = false;
public static void main(String[] args) {
Thread thread = new Thread(()->{
while(!Thread.currentThread().isInterrupted()){
System.out.println("hello");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
break;
}
}
});
thread.start();
try {//main线程休眠3秒
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
thread.interrupt();//将进程打断
System.out.println("进程已被打断!");
}
}
这里我们其实是打断失败了的,按照意愿来说是想结束线程的。打断失败,但是注意,这个过程中标志位其实是设置过的,只是因为现在线程是阻塞状态,而sleep(),wait()这些阻塞方法会清除掉标志位,就导致像是没有设置成功。
这里的catch子句里面的处理方法可以有很多。你可以选择就立即break跳出,结束线程。你也可以不做任何理会让线程继续执行。也可以稍后进行处理。根据代码写得不同,这种情况下得到的结果也是不一样的。
2.5,线程等待
我们知道,多线程下,各个线程之间的调度顺序是不确定的,在某些情况下我们要求某个线程必须要等到另外的线程结束之后才能继续执行。所以就有了join(),在一个线程里面调用,那么该线程就要阻塞等待你指定的线程执行。
public class Demo10 {
public static void main(String[] args) {
Thread thread = new Thread(()->{
for(int i = 0;i < 3;i++){
System.out.println("hello");
}
});
thread.start();
System.out.println("main线程开始阻塞等待thread执行");
try {
thread.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("main线程阻塞结束");
}
}
当然,如果说join()之前这个线程已经就执行完了,那就不需要main线程阻塞等待了。相当于这个时候join()没有起到作用罢了。
【扩展:有时间限制的等待】
public void join(long millis);//等待多少毫秒
public void join(long millis, int nanos);//等待多少毫秒多少纳秒。精度更高
这个应用场景比如你请求一个访问然后等待超时之类的,就是不是无限制的等待。
2.6,获取当前线程的引用
public class Demo11 {
public static void main(String[] args) {
System.out.println(Thread.currentThread().getName());
}
}
在哪个线程里面调用,获取到的就是哪个线程的引用。
三,线程的状态
3.1,线程的状态
这套状态区别于操作系统自带的对状态的描述,这个是属于Java的一套对于线程状态的描述。
状态 | 描述 |
---|---|
NEW | 创建好了Thread对象,但是内核PCB还没创建。此时线程没有创建 |
TERMINATED | 内核PCB销毁,但是Thread对象还在 |
RUNNABLE | 可运行的,要么是在CPU上执行,要么是在就绪队列 |
TIMED_WAITING | 按照一定的时间进行阻塞等待,休眠。调用sleep(),join(time) |
WAITING | 特殊的阻塞状态,调用wait,join() |
BLOCKED | 等待锁的时候进入阻塞状态 |
public class Demo12 {
public static void main(String[] args) throws InterruptedException {
Thread t = new Thread(()->{
try {
Thread.sleep(8000);
} catch (InterruptedException e) {
e.printStackTrace();
}
});
//线程还未创建
System.out.println(t.getState());
t.start();
//线程创建开始工作
System.out.println(t.getState());
Thread.sleep(1000);
System.out.println(t.getState());
t.join();//让main线程等待t执行完
//t线程执行完成
System.out.println(t.getState());
}
}
3.2,状态之间的切换
今天关于多线程的分享就到这了,后续还会持续更新有关内容,如果大家觉得还可以的话,还请帮忙点点赞啰!🥰🥰🥰