目 录CONTENT

文章目录

线程

FatFish1
2024-10-22 / 0 评论 / 0 点赞 / 74 阅读 / 0 字 / 正在检测是否收录...

线程与线程池的关系

线程是执行任务的最小单元,单个线程可以启停,从而执行任务。

  • 线程是工厂中的工人, 工人可能自己有设计方法(自己写run方法),也可能使用图纸中的设计方法(任务中的run方法)

  • 任务是工厂承接的设计任务,任务中包括图纸(run方法或call方法)

  • 线程池是工厂,每过来一个任务,工厂就要启动工人生产产品。

  • 工厂为了按图纸生产产品,可以配置启动多少个工人(核心线程数、最大线程数)

  • 工厂接受任务的运营模式是什么(线程池队列)。

  • 可以配置如果任务太多如何决策(抛弃策略)。

线程的基础知识

(1)Thread线程的状态

见源码部分

(2)Thread线程的中断

  • thread.interrupt()方法只会改变线程的中止状态位,但线程是否立即中止,由线程本身判断。new和terminated对于中断操作几乎是屏蔽的,runnable和blocked类似,对于中断操作只是设置中断标志位并没有强制终止线程,对于线程的终止权利依然在程序手中。waiting/timed_waiting状态下的线程对于中断操作是敏感的,他们会抛出异常并清空中断标志位。

  • thread.isInterrupted可以判断线程当前标志位是否是中止态

  • 利用isInterrupted和return可以实现判断并彻底中止线程

Thread源码

线程状态综述

线程的内部类Status代表线程状态:

// 刚创建还没开始的状态
NEW
// 就绪状态,可执行,但还没有分配时间片,跟RUNNING区分不明显,都可以叫运行状态
RUNNABLE
// 阻塞状态,进入synchronized关键字修饰的方法或代码块(获取锁)时的状态,一般是由于获取不到锁阻塞
BLOCKED
// 等待状态,执行了wait方法、join方法、AQS的park方法会进入这种状态,无限期等待
WAITING
// 限时等待状态,执行wait(long)、join(long)、AQS的parkNanos、parkUntil会进入这种状态,有限期等待
TIMED_WAITING
// 终止状态,完成任务的状态,不能重新start
TERMINATED

获取线程状态

使用jstack可以获取线程状态

./jstack <pid>

WAITING和BLOCKED状态区别

synchronized会导致线程进入Blocked状态,Object.wait()导致线程进入Waiting状态,Waiting线程调用了notify()方法之后,可能会直接获取synchonized锁到达可运行Runnable,但是如果没有获取到synchonized锁的时候应该进入Blocked状态

状态转换常用方法包括:

// sleep:当前线程调用,进入TIMED_WAITING状态,但不释放对象锁,超时后回归RUNNABLE,
// 它是为了给其他线程执行的时机
Thread.sleep(long millies)
// yield:当前线程调用,放弃CPU时间片资源,但不释放对象锁,由RUNNING变为RUNNABLE
// 它是为了给优先级相同的线程被cpu重新选择的机会,但不保证一定轮流执行
Thread.yield()
// join:当前线程调用其他线程t的join方法,当前线程进入WAITING/TIMED_WAITING状态,当前线程不释放对象锁,其他线程t执行完,或到时间回归RUNNABLE状态,也可能因没有synchronized锁进入BLOCKED状态
Thread.join()
// wait:当前线程调用对象的wait方法,当前线程释放对象锁,进入等待队列,依靠notify或wait到时唤醒
Object.wait()
// notify:唤醒此对象监视器上等待的单个线程,选择是任意的,notifyAll唤醒所有线程
Object.notify()
// park:当前线程进入WAITING/TIMED_WAITING状态,对比wait方法不需要获取锁就可以让线程等待,unpark执行唤醒
LockSupport.park()、LockSupport.parkNanos、LockSupport.parkUntil
LockSupport.unpark(Thread thread)

RUNNBALE状态

表示线程是可以执行的,就等cpu进行调度了。抢占到cpu时间片资源就可以执行了。有两个子状态:READY就绪和RUNNING运行中就绪到运行是通过cpu调度,而RUNNING到READY是通过Thread.yield()主动释放

核心属性

private Runnable target;
private ThreadGroup group;

Thread里面封装了一个Runnable对象target,Thread本身就是一个Runnable对象,为什么还要封装一个Runnable进去,而且在下面构造的时候还要传一个runnable对象进去呢?

这是为了实现执行器和任务的分离。Thread是任务执行的执行器,target是真正的可执行对象,如果二者分离,才能真正实现在线程池流程中thread的复用,如果二者绑定了,那一个任务就绑死在一个线程上了,这个任务执行完成,这个线程就要跟着销毁,不符合线程池和复用的设计理念

构造方法

Thread的核心成员变量包括:

// 线程名称
private volatile String name;
// 线程上下文
ThreadLocal.ThreadLocalMap threadLocals = null;
// 线程组,存储执行线程
private ThreadGroup group;
// 线程执行的可执行单元(线程执行谁)
private Runnable target;

有一个空参构造,一个带参构造,都是最终调用到init方法中。

    public Thread() {
        init(null, null, "Thread-" + nextThreadNum(), 0);
    }

    public Thread(Runnable target) {
        init(null, target, "Thread-" + nextThreadNum(), 0);
    }

可以看到二者的区别在于带参构造是传入了一个Runnable可执行单元做target的,这就使得线程和作业分离。

在不指定名称的情况下,线程Thread会自动默认为“Thread-”的名字,如果使用线程工厂,就可以自己指定名称。

init方法

// 设置当前线程是该线程的父线程
Thread parent = currentThread();
……
// 向threadGroup中添加未执行线程
g.addUnstarted();
this.group = g;
// 将daemon、priority属性设置为父线程的对应属性,这里daemon就是是否是守护线程
this.daemon = parent.isDaemon();
this.priority = parent.getPriority();
……
// 传入核心变量target,后面run的时候使用,target代表的就是线程要执行的runnable对象
this.target = target;
……
// 将父类的可继承inheritThreadLocal继承过来
if (inheritThreadLocals && parent.inheritableThreadLocals != null)
    this.inheritableThreadLocals =
        ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);
// 分配线程id
tid = nextThreadID();

start - java多线程启动入口

Thread提供了start方法,是java多线程启动的入口。

if (threadStatus != 0)
        throw new IllegalThreadStateException();

首先判断status,0代表NEW状态,如果不是new状态,不允许thread执行start方法。

start0();
    started = true;

最核心的在这里,执行start0方法。start0是一个native方法,它的实现包括:

  • 首先,它会检查当前线程对象的状态是否允许启动。如果线程对象的状态不满足启动条件,start0()方法将抛出一个IllegalThreadStateException异常。

  • 接下来,start0()方法会为新线程分配所需的系统资源和内存空间。这包括为线程创建一个独立的执行环境和堆栈空间。

  • 一旦系统资源和内存分配完成,start0()方法将调用操作系统提供的原生函数,以便创建一个真正的操作系统级线程。这个原生函数可能是基于平台的API,比如Windows的CreateThread()或Linux的pthread_create()。

  • 最后,start0()方法会更新线程对象的状态,并且将线程添加到线程调度器中,使其可以被调度执行。

同时,在start0的执行过程中,JVM会自动调用线程thread的run方法。因此在jdk源码中,实际看不到run方法的调用,但其实在start方法触发时就执行了run方法

run - 线程执行单元

集成Runnable接口的Thread类实现了Runnable的run方法。这里思考,为什么不是Thread直接自带run方法?

这样可以实现线程和可执行单元的分离。如果线程自带run,那么创建多个可执行任务就是多个thread,只能一个执行一个,而通过创建一个Runnable,可以给多个线程执行,对一个Runnable进行并发处理。例如银行取号机,四台取号机共用一天的1-50号码序列,要对一个可执行单元做并发

Thread实现的run方法实际就比较简单了:

    public void run() {
        if (target != null) {
            target.run();
        }
    }

相当于它将执行内容直接交给了可执行单元去实现。这就是典型的策略模式,执行器和可执行单元分离。

线程死锁

死锁的概念

死锁是指两个线程同时拥有对方请求的资源,导致均永远无法执行下去。

Object lockA = new Object();
synchronized (lockA) {
                    System.out.println("线程 1:获取到锁 A!");
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println("线程 1:等待获取 B...");
                    synchronized (lockB) {
                        System.out.println("线程 1:获取到锁 B!");
                    }
                }

synchronized (资源) {}中的代码段在执行时保有对资源的占用,其他线程想要使用资源A必须先请求锁。

解决死锁的方法

顺序锁

将线程对资源的请求由互斥改为顺序,如下图:

加图

锁超时

Lock lockA = new ReentrantLock();
Lock lockB = new ReentrantLock();
public static void pollingLock(Lock lockA, Lock lockB) {
        while (true) {
            if (lockA.tryLock()) { // 尝试获取锁
                System.out.println("线程 1:获取到锁 A!");
                try {
                    Thread.sleep(1000);
                    System.out.println("线程 1:等待获取 B...");
                    if (lockB.tryLock()) { // 尝试获取锁
                        try {
                            System.out.println("线程 1:获取到锁 B!");
                        } finally {
                            lockB.unlock(); // 释放锁
                            System.out.println("线程 1:释放锁 B.");
                            break;
                        }
                    }
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } finally {
                    lockA.unlock(); // 释放锁
                    System.out.println("线程 1:释放锁 A.");
                }
            }
            // 等待一秒再继续执行
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

上示例lockB.tryLock()获取不到会报中断异常,释放锁A,然后执行try语句等待1秒,再重新while true循环

Runnable和Callable

实现Runnable接口/Callable接口表明此对象是一个可执行任务

二者的区别在于:

  • run()实现自runnable接口,call()实现自callable接口

  • callable 和runnable 很像, callable 的call() 方法可以等同于runnable 的run() 方法,

  • call() 能返回结果,run() 不能。

  • call() 可以抛出受检查的异常,比如ClassNotFoundException, 而run()不能抛出受检查的异常。

0

评论区