- 线程启动
- 线程暂停
- 线程停止
- 线程优先级
- 线程安全
进程和多线程概述
进程
- 进程是受操作系统管理的基本单元
又如WIN的任务管理器的exe文件都可以理解为一个进程。
*.java程序经编译之后形成*.class文件,在WIN中启动一个JVM虚拟机相当于创建了一个进程,在虚拟机中加载class文件并运行,在class文件中通过执行创建新线程的代码来执行具体的任务
举例:
1.打开一个IntelliJ程序后显示一个进程。但其下面有2个线程。一个是Java一个是tsnotifier(用于和系统文件交互)
2.在Idea中执行
public class Test1 {
public static void main(String[] args) {
try {
Thread.sleep(Integer.MAX_VALUE);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
最后多出来2个Java相关线程(因为其在Idea中执行,所以其父进程属于Idea)
线程
在进程中独立运行的子任务。
首先线程对应的是lang包下的Thread类:
一个程序进程的子任务可能在同时运行不同的任务。也如上图的Idea所示
进程负责向操作系统申请资源,在一个进程中,多个线程可以共享进程中相同的内存或文件资源
多线程
多线程技术可以在同一时间内执行更多不同的任务,异步。和消息队列中间件类似异步解耦。
使用多线程的场景
- 阻塞
出现阻塞现象可以根据实际情况来使用多线程技术提高运行效率 - 依赖
A、B2个逻辑处理不相互依赖,可以使用多线程提高效率
不要为了使用多线程还使用多线程,要根据实际场景决定
使用多线程
一个进程正在运行时至少会有一个线程在运行。
public class Test {
public static void main(String[] args) {
System.out.println(Thread.currentThread().getName());
}
}
currentThread()回对当前正在执行的线程对象的引用
得到的是main,则Java首先创建的线程是main线程。这个main线程和main方法没有任何关系,只是名字相同
http://jtao.work/archives/di-er-shi-liu-zhang--xian-cheng-ji-chu
实现多线程原始的主要方式有2类:
1.继承Thread类
2.实现Runnable接口
继承Thread类
Thread类实现了Runnable接口,其之间有多态的关系即Runnable a= new Thread()
- 缺点
Java不支持多继承,不利于面向对面的变成拓展 - 实现方式
1.继承Thread
2.重现run方法
3.调用start方法
通过上图可以看出,虽然start在print前面被调用,但run方法反而在后面执行这是因为创建线程比较耗时:
1.通过JVM与操作系统沟通创建Thread
2.操作系统开辟内存并且使用操作系统原生SDK函数进行创建Thread对象
3.操作系统对Thread进行调度,以确定执行时机
4.Thread在操作系统中被成功执行
如果将当前线程休眠200ms,则因为thread已经被start,且不受影响,所以其会先执行
在使用多线程技术时,**代码的运行结果与代码的执行顺序或调用顺序是无关的。**线程是一个子任务,CPU以不确定的方式,随机的时间来调用线程中的run()方法,具有不确定性
使用常见命令分析线程的信息
常用工具在WIN环境下使用见我的JVM博客,这里不细说。只说MacOS
jps、jstack,注意一些软件是无法用于商业的,需要授权
http://jtao.work/archives/xu-ni-ji-xing-neng-jian-kong--gu-zhang-chu-li-gong-ju
使用方法不具体细说了,参看我另一篇博客
public class Run3 {
public static void main(String[] args) {
for (int i = 0; i < 5; i++) {
new Thread(){
@Override
public void run() {
try {
Thread.sleep(500000);
//因为是匿名内部类,异常无法抛出只能try-catch
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}.start();
}
}
}
运行上述代码后
线程随机性的展现
线程的调用是随机的
/**
* @author jiangtao
* @date 2021/9/26 22:52
*/
public class MyThread extends Thread{
@Override
public void run() {
for (int i = 0; i < 10000; i++) {
System.out.println("run="+Thread.currentThread().getName()+i);
}
}
public static void main(String[] args) {
MyThread myThread = new MyThread();
myThread.setName("MyThread");
myThread.start();
for (int i = 0; i < 10000; i++) {
System.out.println("main="+Thread.currentThread().getName()+i);
}
}
}
由结果可以看出其输出是异步,同时进行的。
- start方法通知“线程规划起”,此线程已经准备就绪,系统自己安排时间调用线程对象的run方法。
start方法会调用start0方法,而start0方法是一个native方法
- 如果直接调用run方法,则不会创建线程,而是继续由main主线程执行,只能串行执行
执行start()的顺序不代表执行run()的顺序
/**
* @author jiangtao
* @date 2021/9/26 23:06
*/
public class MyThread extends Thread {
private int i;
public MyThread(int i) {
this.i = i;
}
@Override
public void run() {
System.out.println(i);
}
public static void main(String[] args) {
IntStream.iterate(1,vo->vo+=1).limit(10).forEach(vo -> new MyThread(vo).start());
}
}
其执行run方法的顺序和start方法顺序无关
实现Rnnable接口
因为是Functional接口,可以解决Java不能多重继承的问题。
Thread的构造函数有5种方法可以接受Runnable对象,同时又因为多态的缘故,Thread也可以接受Thread对象作为构造函数的入参
/**
* @author jiangtao
* @date 2021/9/26 23:20
*/
public class Run implements Runnable {
@Override
public void run() {
}
public static void main(String[] args) {
Thread thread = new Thread(new Run());
new Thread(new Runnable() {
@Override
public void run() {
}
});
new Thread(() -> {
});
new Thread(new Thread(){
@Override
public void run() {
super.run();
}
});
}
}
在大多数情况下,使用Runnable和Thread没有区别,但是涉及到多继承情况,则应该实现Runnable接口
实现Runnable接口与继承Thread类的内部流程
实现Runnable接口方法在JDK代码执行上相对较复杂
- Thread
可以使用无参构造target为null - Runnable
有参构造,target不为null
实例变量共享造成的非线程安全问题与解决方案
多个线程交互时变量对其他线程有共享与不共享之分
不共享数据的情况
/**
* @author jiangtao
* @date 2021/9/26 23:37
*/
public class MyThread extends Thread{
private int count=5;
public MyThread(String name){
setName(name);
}
@Override
public void run() {
while (count-->0){
System.out.println("由"+ currentThread().getName()+"计算,count"+count);
}
}
public static void main(String[] args) {
new MyThread("A").start();
new MyThread("B").start();
new MyThread("C").start();
}
}
因为count是实例变量,每个变量独享的,所以不会共享
共享数据的情况
将上述的count属性改为static,变成类共享属性,则
同时又因为多个线程访问,使得变量的值不正确,出现线程安全问题
-
原因
JVM中对count--的操作分解为3步骤
1.取得原有的count值
2.计算count-1
3.对count重新赋值 -
解决
1.将属性count变为原子类AtomicInteger
2.将被使用的方法加锁
Thread类常用API
- currentThread()返回对当前正在执行的线程对象的引用
即返回该代码块正在被哪个线程所调用 - run()和srart()
1.run立即执行,用当前线程执行,不启用新线程
2.start执行时间不确定,启用新的线程执行 - isAlive() 当前线程是否存活
- sleep(long millis) 让线程休眠
- sleep(long millis)
- StackTraceElement[] getStackTrace()
返回表示此线程的堆栈转储的堆栈跟踪元素数组。 如果此线程尚未启动、已启动但尚未被系统安排运行或已终止,则此方法将返回一个零长度数组。 如果返回的数组长度不为零,则数组的第一个元素表示堆栈的顶部,这是序列中最近的方法调用。 数组的最后一个元素代表堆栈的底部,它是序列中最近的方法调用。
- dumpStack()
将当前线程的堆栈跟踪打印到标准错误流。 此方法仅用于调试
- Map<Thread, StackTraceElement[]> getAllStackTraces()
- getId()
中断线程
- stop
这种方法本质上是不安全的。 使用 Thread.stop 停止线程会使其解锁所有已锁定的监视器(这是未经检查的ThreadDeath异常向上传播堆栈的自然结果) - 使用推出标志使线程正常退出
- 使用interrupt()中断线程
检测线程是否被中断
- static interrupted() 用于检测当前代码所在的线程是否被中断
- isInterrupted() 用于检测被调用的Thread对象线程是否被中断
采用异常停止
- 方法一:调用interrupt()
使得interrupted()处于true状态。如果出true则抛出异常后再catch住InterruptedException后做其他相应处理
- 方法二:调用sleep()
调用sleep和interrupt有类似的效果,sleep和interrupt状态同时出现就会有InterruptedException异常被抛出
- 方法三:直接return
如果判断线程被终止了则直接return退出方法即可
stop暴力停止(不推荐)
直接调用线程对象stop()方法,会抛出ERROR,不需要显式捕捉处理
- 暴力停止,容易造成多线程数据处理的不确定性
- ThreadDeath会被抛出
- 对锁定的对象进行解锁会导致数据得不到同步的处理,出现数据不一致问题
暂停线程
suspend()暂停线程、resume()重启线程。
- 注意这2个方法都是废弃的,因为容易造成死锁
suspend()方法被调用后在resume之前并不会释放对象锁,造成其他线程无法访问该对象(独占锁),因此容易死锁,被废弃
- 因为线程被暂停,可能会数据不完整
- 可使用wait()、notify()、notifyAll()方法
yield()放弃CPU使用权
放弃当前CPU资源,让其他任务去占用CPU执行时间,放弃的时间大小不确定,有可能放弃之后立马又被CPU调用
线程的优先级
线程可以划分优先级,优先级高的线程得到CPU资源较多,会获得更多的CPU时间片。
使用setPriority(int)设置线程优先级
- 优先级分为1~10个等级,如果在其他范围则会报错
- 优先级等级高约容易获得更多的CPU时间片
- 线程的优先级具有继承性
A线程启动B线程,则B线程与A线程的优先级是一样的 - 高优先级的线程总是大部分先执行完
- 当线程优先级等级差距很大时,谁先执行完和代码的先后顺序无关
- 优先级高的线程不总是先执行完
守护线程
Java有2种线程分类:用户线程(非守护线程)、守护线程
- 定义
特殊的线程,当进程中不存在非守护线程了,则守护线程自动销毁。例如垃圾回收GC。其存在是为其他线程提供服务的 - 设置守护线程
main线程为非守护线程是胡永线程。调用setDaemon(true)代码并且传入值的线程为守护线程
需要在isAlive为false的时候设置,即调用start之前 - 设置守护线程时效果
如上图所示,设置了守护线程后main结束,守护线程也结束了,而不是一直打印数字 - 未设置守护线程效果
线程一直在运行,并未结束
Comments | 0 条评论