线程简介
这里先说明一下,进程和线程是不同的
-
进程:程序的执行过程,是一个独立的运行环境,持有资源和线程,相当于一个应用程序,操作系统在分配资源时把资源分配给进程(堆和方法区是属于进程的) -
线程:进程中执行的一个任务,系统中最小的执行单元,同一进程有多个线程,线程共享进程的资源,CPU资源分配给线程(程序计数器和栈是属于线程的)
创建一个线程Thread时,JVM将分配一大块内存到专为线程保留的特殊区域上,用于提供运行任务时所需的一切,包括:
-
程序计数器,指明要执行的下一个JVM字节码指令,由于线程是占有CPU执行的基本单位,CPU是用时间片轮转的方式,当前线程时间片用完之后需要让出CPU,而程序计数器就是记录该线程让出CPU时的执行地址,等到下次分配到时间片时线程就可以从本身私有的计数器中指定的地址继续执行
-
用于支持Java代码执行的栈,包含有关线程已到达当时执行位置所调用方法的信息(调用栈帧)以及每个正在执行的方法的所有局部变量
-
第二个则用于native code执行的栈
-
线程本地变量的存储区域
-
用于控制线程的状态管理变量
每当调用一个方法时,当前程序计数器被推到该线程的栈上,然后栈指针向下移动以足够来创建一个栈帧,其栈帧里存储该方法的所有局部变量,参数和返回值。所有基本类型变量都直接在栈上,虽然方法中创建对象的任何引用都位于栈帧中,但对象本身存于堆中。
创建线程的方式
-
继承Thread类,并且重写run方法
-
实现Runnable接口,使用带参的Thread构造器来创建Thread对象
-
实现Callable接口使用FutureTask的方式,使用带参的Thread构造器来创建Thread对象,该方式可以获取到线程执行的返回结果
FutureTask<Integer> futureTask = new FutureTask<>(call);
new Thread(futureTask).start(); -
使用线程池
调用start()方法才会调用线程,如果直接调用run()方法,和普通的方法没有区别
创建一个Thread类的实例与创建其他类不同,JVM会为一个Thread实例分配两个调用栈所需的内存空间,一个调用栈用于跟踪Java代码间的调用关系,另一个用于跟踪Java代码对本地代码(native代码)的调用关系。且一个Thread实例通常对应两个线程,一个是JVM中的线程,另一个是与JVM中的线程相对应的依赖于JVM宿主机操作系统的本地(Native)线程
实现Runnable接口比继承Thread的优势
-
代码可被多个线程共享,适合多个相同的代码去处理同一个资源
-
可以避免java单继承的限制
-
增加程序的健壮性,代码和任务分离
方法介绍
-
join() 线程加入进来,要等待该线程执行完在继续执行join之后的代码,可以确保两个线程的执行顺序
t1.start();
t1.join(); // 等待t1线程执行完成,再执行t2.start()
t2.start();join是当前线程等待所join的线程先执行完才继续进行,如果是多个线程的话,需要在线程的run方法中调用前一个线程的join
-
yield() 暂停当前正在执行的线程,并执行其他线程,yield()方法会使当前线程让出CPU使用权,从运行状态变成就绪状态,重新争抢cpu
-
sleep() 在指定的时间内让当前正在执行的线程休眠,暂时让出指定时间的执行权,在这期间不参与CPU的调度,但是该线程所拥有的监视器资源不会让出。使用sleep(0)可以暂时释放cpu,从运行状态变成就绪状态,这样可以让其他线程获得cpu
在hotspot中sleep(0)的作用相当于yield()
-
getState() 获取线程状态
-
wait() Object的wait()方法、notify()、notifyAll()方法必须要与synchronized(obj)一起使用,只能针对已经获取到obj锁的情况,否则会抛出”java.lang.IllegalMonitorStateException“异常。wait()方法会释放该对象的锁资源,从而进行阻塞;notify()方法会通知等待该对象资源的线程进行唤醒
-
interrupt()方法,中断线程,将中断状态设置为true,但是不一定会被中断,只有在阻塞时才会被中断而抛出InterruptedException异常
public void interrupt() {
if (this != Thread.currentThread())
checkAccess();
synchronized (blockerLock) {
Interruptible b = blocker;
// 只有被阻塞才会抛出异常
if (b != null) {
interrupt0(); // Just to set the interrupt flag
b.interrupt(this);
return;
}
}
// 该方法只会设置中断标志为true,实际上并没有被中断,还是会继续执行
interrupt0();
} -
isInterrupted()方法,实例方法,检查当前线程是否被中断,如果是返回true,调用该方法不会改变中断状态
-
interrupted()方法,静态方法,检查线程是否被中断,与isInterrupted()不同的是,该方法如果发现当前线程被中断,则会清除中断标志,将中断状态重置为false,且该方法是静态方法,判断的是当前调用线程的中断标志而不是调用该方法的实例对象的中断标志
sleep和wait的区别
-
sleep()是Thread的静态方法,wait()是Object的方法 -
sleep()和wait()虽然都释放CPU,但是如果线程持有对象锁资源的话,sleep()在休眠时不释放锁资源,wait()在等待时会释放锁资源 -
sleep()需要捕获异常,wait()不需要 -
sleep()可以在任意位置使用,wait()必须要在synchronized同步块内使用 -
wait()当前线程必须拥有对象的监视器
线程阻塞
-
当执行Thread.sleep方法时,会一直阻塞指定时间,或者阻塞被另一个线程打断 -
当线程执行wait方法时,会一直阻塞到接到通知(notify方法)或者被中断或者经过指定时间为止
上下文切换
一般情况下使用多线程的时候,使用的线程个数会大于CPU个数,但是每个CPU同一时刻只能被一个线程使用,CPU资源采用了时间片轮转的策略为每个线程分配一个时间片,当前线程的时间片到后,会处于就绪状态让出CPU,这就是上下文切换,而在上下文切换时需要保存当前线程的执行现场,当再次执行时根据保存的执行现场信息恢复执行现场
线程上下文切换的时机:
-
当前线程的CPU时间片用完处于就绪状态时 -
当前线程被其他线程中断时
上下文切换会带来额外的开销,包括保存和恢复线程上下文信息的开销,对线程进行调度的CPU时间开销以及CPU缓存内容失效(即CPU的L1 Cache、L2Cache等)的开销
线程状态
public enum State {
// 新建的线程,且还没有调用start方法启动
// new Thread()
NEW,
//运行状态,等待CPU调度,包含了两个子状态READY和RUNNING
// READY表示处于该状态的线程可以被JVM的线程调度器进行调度而使之处于RUNNING状态
// RUNNING表示线程已经获取了CPU,正在运行
// 调用start方法
// yield方法 RUNNING->READY
// 被synchronized标记的代码,获取同步监视器
// obj.notify()/obj.notifyAll()唤醒线程
// obj.wait(time)、thread.join(time)等待时间耗尽
RUNNABLE,
// 阻塞 等待获取synchronized同步块的对象锁或者调用wait方法之后重新进入synchronized同步块
BLOCKED,
// 等待
// 调用Object.wait、Thread.join、LockSupport.park方法会进入等待状态
// WAITING->RUNNABLE 需要调用notify、notifyAll或者等待调用Thread.join的终止
WAITING,
// 调用Thread.sleep、Object.wait(long)、Thread.join(long)、LockSupport.parkNanos、LockSupport.parkUntil这种等待超时
TIMED_WAITING,
// 线程完成工作
// 1.run方法执行完毕正常退出 2.没有捕获异常导致run方法意外终止
TERMINATED;
}
https://zhhll.icu/2020/多线程/基础/1.线程简介/
本文由 mdnice 多平台发布