概念:
1、线程是什么
线程是一个“执行流”。每个线程之间可以按照自己的顺序执行自己的代码,多个线程之间还可以同时执行多份代码。
用银行来举例子:一个人去银行办理业务,多个业务一个人跑肯定是效率不高,这时候就需要多个人来跑业务,而每一个跑业务的人就是一个“执行流”。
2、为什么需要线程
①并发线程已经成为刚需
单核CPU的发展有瓶颈,想要提高算力就必须要有多核CPU,并发变成能更充分利用多核CPU资源,而且在遇到有IO的任务场景,在IO时间内能去做其他的事更能提高效率,这也需要并发编程
②虽然多进程能实现,但是线程比进程更加轻量
线程在创建、销毁和调度上都要比进程更快。为满足开发需求,后续还有“线程池”以及“协程”
3、线程与进程的区别
①进程是包含线程的。一个进程必定有一个线程(即主线程)
②进程与进程之间不共享内存空间,同一个进程的线程之间共享同一个内存空间
③进程是系统分配资源的最小单位,线程是系统调度的最小单位
一个进程挂了一般不会影响其他进程,但是一个进程中的一个线程挂了的话是有可能影响到同一个进程中的其他线程(让整个进程也跟着崩掉)。
线程是操作系统中的概念,操作系统内核实现了线程这种机制,也提供了这样的API给用户层的用户使用。Java中的Thread类可以视为是对操作系统提供的API进行了进一步的抽象和封装。
创建线程
通过观察Thread的源文件代码我们可以知道,大佬们在实现线程的过程中也是调用了Runnable接口。
因此,创建线程有两种方式:继承Thread类和实现Runnable接口,这两种方法都需要重写其中的run()方法。
继承Thread类:
java">class MyThread extends Thread{@Overridepublic void run() {System.out.println("这是继承Thread类重写过后的线程run方法");}
}
实现Runnable接口:
java">public class MyRunnable implements Runnable{@Overridepublic void run() {System.out.println("这是实现Runnable接口重写过后的线程run方法");}}
在创建一个线程之后用start()方法启动线程
java">//实现Runnable接口public static void main1(String[] args) {Thread t1 = new Thread(new MyRunnable());t1.start();
}
//继承Thread类public static void main (String[] args)
{MyThread thread = new MyThread();thread.start();
}
创建线程的其他变形:
java">//内部类创建Runnable的子类public static void main(String[] args){Thread thread = new Thread(new Runnable(){@Overridepublic void run(){System.out.print("通过内部类重写Runnabler子类的run方法创建的线程");}});thread.start();}
java">Thread thread = new Thread(){@Overridepublic void run(){System.out.print("通过内部类重写Thread子类的run方法创建的线程");}};
多线程的优势——加快运行速度
还是拿银行的例子来解释,一个人跑事务和几个人跑事务的速度是不同的。多线程的并发执行决定了这种运行方法在一些场景下是能提高整体代码的运行效率的。
例:concurrency()中用一个子线程计算a,主线程计算b,而serial()中则是计算a和b
java">public class ThreadSAdvantage {public static final long count = 10_0000_0000;public static void main(String[] args) throws InterruptedException{concurrency();serial();}private static void concurrency ()throws InterruptedException {{long begin = System.nanoTime();Thread thread = new Thread(new Runnable() {@Overridepublic void run() {int a = 0;for (long i= 0; i < count; i++) {a--;}}});thread.start();//到此处是用一个线程计算a的值int b = 0;for (long i= 0; i < count; i++) {b--;}//统计计时long end = System.nanoTime();double time = (end-begin)*1.0 /1000/1000;System.out.printf("并发:%f 毫秒%n",time);}}private static void serial (){long begin = System.nanoTime();int a =0;for (long i= 0; i < count; i++) {a--;}int b = 0;for (long i= 0; i < count; i++) {b--;}//统计时长long end = System.nanoTime();double time = (end - begin)*1.0/1000/1000;System.out.printf("串行:%f 毫秒%n",time);
}}
通过结果我们可以发现:并行的运行时间是要比串行执行快很多的
线程的常见构造
线程的构造方法参数有:Runnable target,String name
常见属性:
线程id是线程的唯一标识,不同线程不会出现id重复的情况(例如你们班有两个人叫张三,身份证号码是辨认这两个张三身份的唯一标识)
名称应用于各种调试工具
优先级高的线程会比较先被调度到
是否存活简单理解为run方法是否运行结束
自定义线程优先级=建议,系统如果不按优先级调度那这个优先级意义就不大(系统的意思就是:你的建议很好,但我不接受!!!)
再单独说一说是否后台线程:
与后台线程对应的还有一个前台线程。前台线程与后台线程的差别就在于线程的结束会不会导致java进程的结束。
后台进程就是即使还在执行也阻止不了进程结束的线程,相反,前台线程就是如果不结束执行就不会人java进程结束的线程。(前台线程:干完活了之后只要还在工作,就不下班。后台线程:就算还在干活,我依旧要下班)
tips:前台线程可以有多个,有多个前台线程的话必须最后一个前台线程结束执行才会让java进程结束。
java程序中,main线程就是前台线程,另外,程序员创建的线程默认情况下是前台线程,通过setDaemon方法将线程设置为后台线程。
注意:设置线程为后台线程的操作必须要在启动线程之前,也就是setDaemon()要放在start()之前,不然线程依旧默认是前台线程
启动一个线程:
实例化一个线程之后,用.调用start()方法就是启动一个线程(实例化一个线程对象并不意味着线程已经开始执行)
区分run()和start():
run()是线程的入口,交代线程的执行内容(run相当于事物清单)
start()是在内核中创建线程,让线程跑起来(start相当于告诉线程——按run()里面的内容去给我跑腿干活吧!)
一个线程只能调用start()一次,否则会抛异常
中断一个线程:
Thread类中有一个内制的标志位isinterrupted,通过调用interrupt()改变位置量的值来决定是否中断线程。
将一个处于工作状态的线程停止工作,常见的方式有两种:1.模仿Thread类的内置标志位,自定义布尔变量来决定线程是否中断;2.直接调用interrupt()方法
interrupt()的默认值是true,这个true的意义就是中断线程
看例子:
java">public class Threadtest{
private static class Thread1 {private static class MyRunnable implements Runnable{public volatile boolean quit = false;@Overridepublic void run() {while(!quit){System.out.println(Thread.currentThread().getName()+"我忙着干活呢!");try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}}System.out.println("什么?!没钱发工资?那我卖啥命呢!");}}public static void main(String[] args)throws InterruptedException {MyRunnable target = new MyRunnable();Thread thread = new Thread(target,"李四");System.out.println(Thread.currentThread().getName()+"让李四开始干活!");thread.start();thread.sleep(10000);System.out.println(Thread.currentThread().getName()+"告诉李四别干了,老板跑路了发不出工资!");thread.sleep(1000);target.quit = true;}}
}
在线程休眠了十秒之后,修改自定义的标志位让线程结束
操作系统中针对多个线程的特性:随机调度,抢占执行
看例子:
java">public class Threadtest2 {public static void main(String[] args)throws InterruptedException {Thread thread = new Thread(()->{System.out.println(Thread.currentThread().getName()+" is working");},"Lisi");Thread thread2 = new Thread(()->{System.out.println(Thread.currentThread().getName()+" is coming!");},"Wangwu");thread.start();thread2.start();thread2.join();thread.join();}
}
通过例子我们可以验证这一点:操作系统对于多个线程的调度是随机的,而且线程之间是抢占式执行,不是说先谁调用start()方法就是先执行
阻塞一个线程:
在有多个线程执行时,调用.join()方法阻塞一个线程就是让另一个线程稍后再工作(这句话有点含糊)
例如:有a和b两个线程,在a线程中将b阻塞,就是让a线程等b线程完成工作之后再工作,也就是说——b.join()其实是b线程在执行,a线程是在等待的那个
看实例:
java">public class Threadtest3 {public static void main(String[] args) {Thread t = new Thread(()->{System.out.println("this is Thread t");for (int i = 0; i < 5; i++) {System.out.println("t is working...");try {Thread.sleep(1000);} catch (InterruptedException e) {throw new RuntimeException(e);}}System.out.println("Thread t is finished!!!");});System.out.println("The main thread is waitiing!!!");t.start();try {t.join();} catch (InterruptedException e) {throw new RuntimeException(e);}System.out.println("The main thread is after waiting!!!");}
}
这是线程t,我们在主线程中阻塞线程t
从结果来看,我们就能体会到上面那句看起来很含糊的话的意思——main线程中阻塞了t线程,那就要等t忙完才轮到main线程继续执行
小结:
1.阻塞为的是确保目标线程先执行完毕,当线程已经执行完再进行阻塞并没有用
2.线程之间是可以相互等待的,多个线程可以同时阻塞相互等待
3.无参数的join()就是死等模式,当前阻塞线程没有执行完就会让其他的线程一直等待下去
休眠当前线程:
调用sleep()就是休眠一个线程
重点:
中断线程会唤醒sleep(),当sleep()被唤醒抛出异常,此时内置的标志位就给清空了。
调用sleep()的线程不会参与cpu调度,会把cpu资源让出来给其他线程去干事情。这种也称之为“放权”,即放弃使用cpu的权利。因为在开发中会有很多的线程,线程之间又存在着轻重缓急,所以就有了“放权”。
线程的状态:
New:Thread对象有了,但是还没有在cpu内核上创建线程——还没调用start()(安排了工作,还没执行)
Runnable:处于就绪状态,已经在cpu上工作 / 准备好在cpu上工作
Blocked:阻塞(因为锁竞争导致的阻塞)
Waiting:阻塞(没有超过时间限制的等待)
Timed_waiting:阻塞(超过时间限制的等待)
Terminated:Thread类对象虽然还在,但是线程已经从内核销毁了(工作完了)
顺带一提:类的对象在程序结束时销毁
(参考前面博客,JVM内存分布一栏:JavaSE—数组的定义以及应用_javase数组的定义与使用-CSDN博客)