前言
一、多线程概念
进程与线程
1.多进程有一个明显的缺点:进程太重了.
1)消耗资源更多
2)速度更慢
一旦需要大规模频繁的创建和销毁进程,开销就比较大了,需要给进程分配资源的。
2.若在创建进程的时候,只分配一个简单的pcb, 而不去分配后续的这些内存硬盘资源
这样就既能够并发的执行任务,又能够提升创建/销段的速度
轻量级进程=>线程(Thread)
3.一个进程可以包含多个线程。这个进程中的多个线程,共同复用了进程中的各种资源(内存,硬盘),但是这些线程各自独立的在cpu上进行调度。因此,线程就可以既能够完成"并发编程”的效果,又可以以比较轻量的方式来运行线程,同样也是通过PCB来描述的。
4.同一个进程中的这些线程共用同一份资源(内存指针+文件描述符表相同)
但是每个线程独立去cpu上调度(状态,上下文,优先级,记账信息等…各自有各自的一份)
5.进程,是操作系统进行资源分配的基本单位。线程,是操作系统进行调度执行的基本单位.
6.多线程的问题:不稳定,多个线程没有彼此独立互不干扰(如多线程冲突;如某一线程异常未处理可能导致整个进程终止)
题:谈谈,进程和线程的区别和联系.
1.进程包含线程.都是为了实现并发编程的方式.线程比进程更轻量~~
⒉.进程是系统分配资源的基本单位.线程是系统调度执行的基本单位.
创建进程的时候把分配资源(虚拟地址空间,文件描述符表)的工作给做了,后续创建线程,直接共用之前的资源即可.
3.进程有独立的地址空间,彼此之间不会相互影响到,进程的独立性=>系统稳定性
多个线程共用这一份地址空间.一个线程一旦抛出异常,就可能会导致整个进程异常结束.=>多个线程之间容易相互影响
二、线程的创建
1.第一个简单线程详解
package Tread;
class MyThread extends Thread{
@Override
public void run(){
while (true) {
System.out.println("hello01");
}
}
}
public class Tread01 {
public static void main(String[] args) {
MyThread myThread=new MyThread();
myThread.start();//创建出新的进程
//myTread.run();run只是上面的入口方法(普通的方法)并没有调用系统api,也没有创建出真正的线程来.在主线程内部。
while (true) {//原来的进程继续执行
System.out.println("hello02");
}
}
}
1.当点击运行程序的时候就会先创建出一个java进程.,这个进程中就包含了至少一个线程.这个线程也叫做主线程,也就是负责执行main方法的线程.
2.start会调动系统api,在系统内核中把线程对应的pcb 啥的给创建出来并管理好,新的线程就参与调度了。
3.使用start的方式执行,此时,这俩循环都在执行。两个线程,分别执行自己的循环.
这两个线程都能参与到cpu的调度中,这俩线程(这俩while循环)并发式的执行
2.观察线程的方式
当创建出线程之后。也是可以通过一些方式直观的观察到的一~
1) idea的调试器
2) jconsole
官方在jdk 中给咱们提供的一个调试工具
jdk所在:
双击后,就可以显示所有java线程:
点连接,然后点确定不安全连接,然后点线程,就能够看见。
3.创建线程的多种方法
1.创建一个类,继承Thread,重写run方法.
package Tread;
class MyThread extends Thread{
@Override
public void run(){
while (true) {
System.out.println("hello01");
//这里只能try-catch,不能throws,因为这里是方法重写,父类没有抛异常。
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
public class Tread01 {
public static void main(String[] args) {
MyThread myThread=new MyThread();
myThread.start();
//这里能throws
while (true) {
System.out.println("hello02");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
这俩线程在进行sleep 之后,就会进入阻塞状态.
当时间到,系统就会唤醒这俩线程,并且恢复对这俩线程的调度.
当这俩线程都唤醒了之后,谁先调度,谁后调度,可以视为是"随机"的(抢占式执行)
2.创建一个类,实现Runnable,重写run方法.
package Tread;
class MyRunnable implements Runnable{
@Override
public void run(){
while(true) {
System.out.println("hello1");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
public class Tread02 {
public static void main(String[] args) {
MyRunnable runnable=new MyRunnable();
Thread t=new Thread(runnable);
t.start();
while(true) {
System.out.println("hello2");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
Thread这里是直接把要完成的工作,放到了Thread 的run方法中.
Runnable这里,则是分开了,把要完成的工作放到Runnable 中,再让Runnable和Thread配合~
把线程要执行的任务和线程本身,进一步的解耦合了
3.继承 Thread,重写run,基于匿名内部类.
package Tread;
public class Tread03 {
public static void main(String[] args) {
Thread t = new Thread() {
@Override
public void run() {
while (true) {
System.out.println("hello1");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
};
t.start();
while (true) {
System.out.println("hello2");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
1)创建了一个子类,这个子类继承自Thread,但是,这个子类,是没有名字的!!(匿名),另一方面,这个类的创建,是在Tread3这个类里面.
2)在子类中,重写了run方法.
3)创建了该子类的实例.并且使用t这个引用来指向.
4.实现 Runnable,重写run,基于匿名内部类
package Tread;
public class Tread04 {
public static void main(String[] args) {
Thread t=new Thread(new Runnable() {
@Override
public void run() {
while (true) {
System.out.println("hello1");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
});
t.start();
while (true) {
System.out.println("hello2");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
1)创建了一个 Runnable的子类(类,实现 Runnable)
2)重写了run方法
3)把子类,创建出实例,把这个实例传给Thread的构造方法.
5.使用lambda表达式,表示 run方法的内容
package Tread;
public class Tread05 {
public static void main(String[] args) {
Thread t=new Thread(()->{
while (true) {
System.out.println("hello1");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
t.start();
while (true) {
System.out.println("hello2");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
lambda表达式,本质上就是一个"匿名函数”。
这样的匿名函数,主要就可以用来作为回调函数来使用.
回调函数,不需要咱们程序猿自己主动调用(要知道文件名)。而是在合适的时机,自动的被调用
此处的这个"回调函数”就是在线程创建成功之后,才真正执行.
给线程重命名
6.基于Callable interface
也是一种创建线程的方式.
使用Callable 不能直接作为Thread的构造方法参数.
FutureTask
线程的创建其他写法还有:基于线程池
4.Thread的几个常见属性
1)ID: getld() 线程的身份标识 (在JVM这里给线程设定的标识)
2)状态:getState()
3)优先级:getPriority() 设置/获取优先级,作用不是很大(线程的调度,主要还是系统内核来负责的)
4)是否后台线程:isDaemon()
后台线程/守护线程后台线程不影响进程结束;
前台线程前台线程,会影响到进程结束,如果前台线程没执行完,进程是不会结束的。
创建的线程,默认是前台线程。通过setDaemon显式的设置成后台。
(一个进程中所有的前台线程都执行完,退出了,此时即使存在后台线程仍然没执行完,也会随着进程一起退出)
public static void main(String[] args) {
Thread t=new Thread(()->{
while (true) {
System.out.println("hello1");
}
});
t.setDaemon(true);//设置为后台线程了
t.start();
}
立即退出,因为将t设置为后台线程了,只有main线程在,main运行完就结束了。
5)是否存活:isAlive()
Thread 对象,对应的线程(系统内核中)是否存活
Thread 对象的生命周期,并不是和系统中的线程完全一致的!!
一般,都是Thread对象,先创建好,手动调用start,内核才真正创建出线程。消亡的时候,可能是thread对象,先结束了生命周期(没有引用指向这个对象)(如突然掉电),也可能是 thread对象还在,内核中的线程把 run 执行完了,就结束了
5.线程终止
1)程序猿手动设定标志位
通过这个手动设置的标志位,来让run尽快结束~
package Tread;
public class Tread05 {
public static boolean isQuit=false;
public static void main(String[] args) throws InterruptedException {
Thread t=new Thread(()->{
while (!isQuit) {
System.out.println("hello1");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
t.start();
Thread.sleep(3000);
isQuit=true;//修改前面的标志位
System.out.println("线程终止");
}
}
lambda引入了"变量捕获"这样的机制
lambda内部看起来是在直接访问外部的变量,其实本质上是把外部的变量给复制了一份,到lambda里面.(这样就可以解决:后续真正执行lambda的时候,局部变量可能已经被销毁了)
但是,变量捕获这里有个限制,要求捕获的变量得是final (至少是看起来是final)(这个变量不能被修改)
注意:此处是静态成员变量的使用,不涉及变量捕获。
2)直接Thread类现成的标志位
public class Tread05 {
public static void main(String[] args) throws InterruptedException {
Thread t=new Thread(()->{
//Thread.currentThread()就是t
while (!Thread.currentThread().isInterrupted()) {
System.out.println("hello1");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
t.start();
Thread.sleep(3000);
t.interrupt();//修改前面的标志位为true
System.out.println("线程终止");
}
}
手动设置标志位是没办法唤醒sleep的。
当sleep被唤醒之后,程序猿接下来可以有以下几种操作方式:
1.立即停止循环,立即结束线程
2.继续做点别的事情,过一会再结束线程~catch 中执行别的逻辑,执行完了再break
3.忽略终止的请求,继续循环。不写break
6.等待一个线程join()
操作系统的调度线程的过程,是"随机"的,无法确定,线程执行的先后顺序
等待线程,就是一种规划线程结束顺序的手段。
AB两个线程,希望B先结束,A后结束,此时就可以让A线程中调用B.join()的方法.
此时,B线程还没执行完,A线程就会进入“阻塞”状态.就相当于给B留下了执行的时间.B执行完毕之后,A再从阻塞状态中恢复回来,并且继续往后执行
如果A执行到B.join 的时候,B已经执行完了,A就不必阻塞了,直接往下执行就可以了
public class Tread06 {
public static void main(String[] args) {
Thread b=new Thread(()->{
for (int i = 0; i <5 ; i++) {
System.out.println("bbbbbb");
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("aaaaa");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
try {
b.join();//b如果还没结束,阻塞等待,直到b结束
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("a结束");
});
a.start();
b.start();
}
}
阻塞 =>让代码暂时不继续执行了.(该线程暂时不去cpu上参与调度)
sleep 也能让线程阻塞.阻塞是有时间限制的.
join的阻塞,则是"死等"“不见不散”
更推荐第二种带有更大等待时间的,超过这个时间就不等了。
sleep, join,wait…产生阻塞之后,都是可能被interrupt方法唤醒的~~这几个方法都会在被唤醒之后自动清除标志位(和sleep类似的)
···