一、进程与线程
1、进程
1、程序由指令和数据组成,但这些指令要运行,数据要读写,就必须将指令加载到
CPU
,数据加载到内存。在指令运行过程中还需要用到磁盘、网络等设备。进程是用来加载指令、管理内存、管理IO
的。2、当一个程序被运行,从磁盘加载这个程序的代码至内存,这时就开启了一个进程。
3、进程就可以视为程序的一个实例。大部分程序可以同时运行多个实例进程(例如:记事本、画图、浏览器等),也有程序只能启动一个实例进程(例如:网易云音乐、360安全卫士等)。
2、线程
1、一个进程之内可以分为一到多个线程。
2、一个线程就是指令流,将指令流中的一条条指令以一定的顺序交给
CPU
执行。3、Java中,线程作为最小调度单位,进程作为资源分配的最小单位。在Windows中进程是不活动的,只是作为线程的容器。
3、区别
区别 | 进程 | 线程 |
---|---|---|
根本区别 | 作为资源分配的单位,拥有共享的资源(如:内存空间等,供其内部的线程共享) | 调度和执行的单位 |
开销 | 每个进程都有独立运行的代码和数据空间,进程间的切换会有较大的开销。 | 线程可以看成轻量的进程,同一线程共享代码和数据空间,每个线程有独立运行的代码和程序计数器(PC),线程切换开销小。 |
所处环境 | 在操作系统中能同时运行多个任务(程序)。 | 在同一应用程序中有多个顺序流同时执行。 |
分配内存 | 系统在运行的时候会为每个进程分配不同的内存区域。 | 除了CPU外,不会为线程分配内存(线程所使用的资源都是它所属的进程资源),线程组只能共享资源。 |
包含关系 | 没有线程的进程是可以被看做单线程的,如果一个进程内拥有多个线程,则执行过程不是一条线的,而是多条线程共同完成的。 | 线程是进程的一部分,所以线程有的时候被称为轻权进程或者轻量级进程。 |
通信 | 通信较为复杂,同一台计算机的进程通信称为IPC,不同计算机之间的进程通信,需要通过网络,并遵守共同的协议(如 HTTP) | 通信简单,因为线程共享进程内的内存,一个例子是多个线程可以访问同一个共享变量 |
4、核心概念
1、线程就是独立的执行路径
2、在程序运行时,即使没有自己创建线程,后台也会存在多个线程, 如GC线程、主线程。
3、
main()
称之为主线程,为系统的入口点,用于执行整个程序。4、在一个进程中,如果开辟了多个线程,线程的运行由调度器安排调度,调度器是与操作系统紧密相关的,先后顺序是不能人为的干预的。
5、对同一份资源操作时,会存在资源抢夺的问题,需要加入并发控制。
6、线程会带来额外的开销,如CPU调度时间,并发控制开销 。
7、每个线程在自己的工作内存交互,加载和存储主内存控制不当会造成数据不一致。
二、并行与并发
1、并发
单核CPU下,线程实际还是串行执行的。操作系统中有一个组件叫任务调度器,将CPU的时间片(windows下时间片最小约为15毫秒)分给不同的程序使用,只是由于CPU在线程间(时间片很短)的切换非常快,我们觉得是同时运行的。总结一句话就是:
微观串行,宏观并行
。
CPU 时间片1 时间片2 时间片3 时间片4 core 线程1 线程2 线程3 线程4
2、并行
多核CPU下,每个核(core)都可以调度运行线程,这时候线程是可以并行的。
CPU 时间片1 时间片2 时间片3 时间片4 core1 线程1 线程2 线程3 线程4 core2 线程1 线程2 线程3 线程4
3、总结
1、并发(concurrent):是同一时间应对(dealing with)多件事情的能力。
2、并行(parallel):是同一时间动手做(doing)多件事情的能力。
3、举例说明:
三、同步与异步
1、同步
同步方法调用一开始,调用者必须等待被调用的方法结束后,调用者后面的代码才能继续执行。
2、异步
调用者不用等待被调用方法是否完成,都会继续执行后面的代码,当被调用的方法执行完成之后会通知调用者(如:网上购物后只需要等待快递的通知就行)。
四、创建和运行线程
1、继承Thread类,重写run方法
run
方法的方法体代表了线程要完成的任务,因此把run()
方法称为执行体。
创建线程:
class Test2 extends Thread{
@Override
public void run() {
for(int i = 0; i < 20; i++) {
System.out.println("一边听歌");
}
}
}
调用线程:
public class Test{
public static void main(String[] args) {
//1、创建Thread的子类对象
Test2 test = new Test2();
//2、开启线程,调用start()方法
test.start();
for(int i = 0; i < 20; i++) {
System.out.println("一边敲代码");
}
}
}
总结:
2、实现Runnable接口,重写run方法(推荐)
定义
Runnable
接口的实现类,并重写该接口的run()
方法,该run()
方法是该线程的线程执行体。
创建线程:
class Test04 implements Runnable{
@Override
public void run() {
for(int i = 0; i <= 10; i++) {
System.out.println("一边听歌");
}
}
}
调用线程:
public class Test03 {
public static void main(String[] args) {
//1、创建Runnable的子类对象
Test04 test = new Test04();
//2、将子类对象当做参数传递给Thread的构造函数
Thread t = new Thread(test);
//3、启动线程
t.start();
for(int i = 0; i <= 10; i++) {
System.out.println("一边敲代码");
}
}
}
3、通过Callable和Future创建线程
1、创建
Callable
接口的实现类,并实现call()
方法,该call()
方法将作为线程执行体,并且有返回值。2、创建
Callable
实现类的实例,使用FutureTask
类来包装Callable
对象,该FutureTask
对象封装了该Callable
对象的call()
方法的返回值。3、使用
FutureTask
对象作为Thread
对象的target
创建并启动新线程。4、调用
FutureTask
对象的get()
方法来获得子线程执行结束后的返回值。
创建线程:
public class CallTest implements Callable {
@Override
public Object call() throws Exception {
for(int i = 0; i <= 5; i++) {
System.out.println("一边唱歌");
}
return 100;
}
}
调用线程:
public class Test01 {
public static void main(String[] args) throws Exception {
CallTest call = new CallTest();
FutureTask<Integer> futureTask = new FutureTask<>(call);
Thread thread = new Thread(futureTask);
thread.start();
System.out.println(futureTask.get());
}
}
4、使用lambda表达式创建线程
public class LambdaTest2 {
public static void main(String[] args) {
/**
* Lambda的思想,只关注做什么,而不关注怎么实现
*/
Runnable runnable = ()->{
for (int i = 0; i < 10; i++) {
System.out.println("线程1开启了,"+i);
}
};
new Thread(runnable).start();
/**
* 简化
*/
new Thread(()->{
for (int i = 0; i < 10; i++) {
System.out.println("线程2开启了," + i);
}
}).start();
}
}
5、使用匿名内部类创建线程
继承Thread类:
public class Test07 {
public static void main(String[] args) {
new Thread() {
@Override
public void run() {
for(int i = 0; i <= 5; i++) {
System.out.println("一边唱歌");
}
}
}.start();
}
}
实现Runnable接口:
public class Test07 {
public static void main(String[] args) {
new Thread(new Runnable() {
@Override
public void run() {
for(int i = 0; i <= 5; i++) {
System.out.println("一边代码");
}
}
}).start();
}
}
五、线程运行原理
1、栈和栈帧
1、JVM由堆、栈、方法区所组成,其中栈内存是给线程使用的,每个线程启动后,虚拟机就会为其分配一块栈内存。
2、每个栈由多个栈帧(Frame)组成,对应着每次方法调用时所占用的内存。
3、每个线程只能有一个活动栈帧,对应着当前正在执行的那个方法。
2、线程上下文切换(Thread Context Switch)
1、因为以下一些原因导致CPU不再执行当前线程,转而执行另一个线程的代码:
2、当上下文发生时,需要由操作系统保存当前线程的状态,并恢复另一个线程的状态,Java中对应的概念就是程序计数器(Program Counter Register),它的作用是记住下一条JVM指令的执行地址,是线程私有的。
3、常见方法
方法名 | 功能说明 | 注意 |
---|---|---|
start() | 启动一个新线程,在新的线程运行run方法中的代码 | start方法只是让线程进入就绪,里面的代码不一定立刻运行(CPU的时间片还没有分配给它)。每个线程对象的start方法只能调用一次,如果调用了多次会出现IllegalThreadStateException异常。 |
run() | 新线程启动后会调用的方法 | 如果在构造Thread对象时传递了Runnable参数,则线程启动后会调用Runnable中的run方法,否则默认不执行任何操作。但可以创建Thread的子类对象,来覆盖默认行为。 |
join() | 等待线程运行结束,再执行其他线程 | |
join(long n) | 等待线程运行结束,最多等待n毫秒 | |
getId() | 获取线程长整型的id | id唯一 |
getName() | 获取线程名 | |
setName(String) | 设置线程名 | |
getPriority() | 获取线程优先级 | |
setPriority(int) | 设置线程优先级 | Java中规定线程优先级是1-10的整数,较大的优先级能提高该线程被CPU调用的机率。 |
getState() | 获取线程状态 | Java中线程状态是用6个Enum ,分别表示:NEW、RUNNABLE、BLOCKED、WAITING、TIMED_WAITING、TERMINATED |
isInterrupted() | 判断是否被打断 | 不会清除打断标记 |
isAlive() | 线程是否存活(还没有运行完毕) | |
interrupt() | 打断线程 | 如果被打断线程正在sleep、wait、join 会导致被打断的线程抛出InterruptedException,并清除打断标记 ;如果打断的正在运行的线程,则会设置打断标记 ;park的线程被打断,也会设置打断标记 |
interrupted() | 判断当前线程是否被打断 | 会删除打断标记 |
currentThread() | 获取当前正在执行的线程 | |
sleep(long n) | 让当前执行的线程休眠n毫秒,休眠时让出CPU时间片给其它线程 | |
yield() | 提示线程调度器让出当前线程对CPU的使用 | 主要是为了测试和调试 |
六、线程状态操作
1、五种状态(从操作系统层面)
1、
新生状态:
仅是在语言层面创建了线程对象,还未与操作系统线程关联。2、
就绪状态:
指该线程已经被创建(与操作系统线程关联),可以由CPU调度执行。3、
运行状态:
指获取了CPU时间片运行中的状态,当CPU时间片用完,会从运行状态
转换至就绪状态
,会导致线程上下文切换。4、
阻塞状态:
如果调用了阻塞API,如BIO读写文件,这时该线程实际不会用到CPU,会导致线程上下文切换,进入
阻塞状态
。等BIO操作完毕,会由操作系统唤醒阻塞的线程,转换至
就绪状态
。与
就绪状态
的区别是,对阻塞状态
的线程来说只要它们一直不唤醒,调度器就一直不会考虑调度它们。5、
死亡状态:
表示线程已经执行完毕,生命周期已经结束,不会再转换为其他状态。
2、六种状态(从Java API 层面)
1、根据
Thread.State
枚举,分为六种状态
状态名称 说明 NEW 初始状态,线程被创建,但是还没有调用start()方法 RUNNABLE 运行状态,Java线程将操作系统中的就绪和运行两种状态笼统称为“运行中” BLOCKED 阻塞状态,表示线程阻塞于锁 WAITING 等待状态,表示线程进入等待状态,进入该状态表示当前线程需要等待其他线程做出一些特定的动作(通知或者中断) TIMED_WAITING 超时等待状态,该状态不同于WAITING,它是可以在指定的时间自行返回的 TERMINATED 终止状态,表示当前线程已经执行完毕 2、说明:
线程创建之后调用
start()
方法开始执行,当调用wait()、join()、LockSupport.park()
方法线程会进入到WAITING
状态,而同样的wait(long timeout)、sleep(long)、join(long)、LockSupport.parkNanos()、LockSupport.parkUtil()
增加了超时等待的功能,也就是调用这些方法后线程会进入TIMED_WAITING
状态,当超时等待时间到达后,线程会切换到RUNNABLE
的状态,另外当WAITING
和TIMED_WAITING
状态时可以通过Object.notify()、Object.notifyAll()
方法使线程转换到RUNNABLE
状态。当线程出现资源竞争时,即等待获取锁的时候,线程会进入到BLOCKED
阻塞状态,当线程获取到锁时,线程进入到RUNNABLE
状态。3、注意:
3、休眠线程sleep
1、
sleep(long millis)
方法是Thread
的静态方法,很显然它是让当前线程按照指定的时间休眠,其休眠时间的精度取决于处理器的计时器和调度器。2、调用
sleep
方法会让当前线程从Running
进入Timed_Waiting
状态。3、其他线程可以使用
interrupt
方法打断正在睡眠的线程,这时sleep
方法会抛出InterruptedException
。4、睡眠结束后的线程未必会立刻执行,因为线程进入就绪状态。
5、每一个对象都有一个锁,
sleep
不会释放锁,抱着锁睡觉。6、建议使用
TimeUnit
的sleep
代替Thread
的sleep
来获得更好的可读性。
public class Test01 {
public static void main(String[] args) {
// 一份资源
Web12306 w = new Web12306();
// 多个代理
new Thread(w,"黄牛1").start();
new Thread(w,"黄牛2").start();
new Thread(w,"黄牛3").start();
}
}
class Web12306 implements Runnable{
private int num = 20;
@Override
public void run() {
while (true){
if (num <= 0){
break;
}
// 模拟网络延迟,方法问题发生的可能性
try {
Thread.sleep(200);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "抢到了,还有" + (num--) +"张");
}
}
}
4、礼让线程yield
1、礼让线程,让当前正在执行线程暂停。
2、会让当前线程从
Running
进入Runnable
就绪状态,然后调度执行其他线程。3、依赖于操作系统的任务调度器。
public class Test02 {
public static void main(String[] args) {
MyYield m = new MyYield();
new Thread(m,"线程a").start();
new Thread(m,"线程b").start();
}
}
class MyYield implements Runnable{
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + "-->start");
//礼让线程
Thread.yield();
System.out.println(Thread.currentThread().getName() + "-->end");
}
}
5、线程优先级priority
1、Java提供一个线程调度器来监控程序中启动后进入就绪状态的所有线程。线程调度器按照线程的优先级决定调用哪个线程来执行。
2、线程的优先级用数字表示,范围从1到10,API中提供三个常量
3、设置、获取线程优先级的方法
4、优先级的设定建议在
start()
调用。5、
优先级低只是意味着获得调度的概率低,并不是绝对先调用优先级高后调用优先级低的线程
。6、如果CPU比较忙,那么优先级高的线程会获得更多的时间片,但CPU闲时,优先级几乎没有作用。
public class Test03 {
public static void main(String[] args) {
System.out.println(Thread.currentThread().getPriority());
//启动线程
MyPriority m = new MyPriority();
Thread t1 = new Thread(m,"线程1");
Thread t2 = new Thread(m,"线程2");
Thread t3 = new Thread(m,"线程3");
Thread t4 = new Thread(m,"线程4");
//设置线程优先级在启动前
t1.setPriority(Thread.MIN_PRIORITY);
t2.setPriority(Thread.NORM_PRIORITY);
t3.setPriority(Thread.MIN_PRIORITY);
t4.setPriority(Thread.MAX_PRIORITY);
t1.start();
t2.start();
t3.start();
t4.start();
}
}
class MyPriority implements Runnable{
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + "--->" + Thread.currentThread().getPriority());
Thread.yield();
}
}
6、加入线程join
join合并线程,待调用此方法的线程执行完成后,再执行其他线程,
其他线程阻塞
。
public class Test04 {
public static void main(String[] args) throws InterruptedException {
Thread t = new Thread(()->{
for (int i = 0; i <= 30; i++)
System.out.println("lambda-->" + i);
});
//启动线程
t.start();
for (int i = 0; i <= 30; i++){
if (i == 20){
//插队,main被阻塞,等待lamdba线程执行完成后,再执行main线程
t.join();
}
System.out.println("main-->" + i);
}
}
}
运行结果:
7、中断interrupted
1、中断可以理解为线程的一个标志位,它表示了一个运行中的线程是否被其他线程进行了中断操作。中断好比其他线程对该线程打了一个招呼。
2、其他线程可以调用该线程的
interrupt()
方法对其进行中断操作,同时该线程可以调用isInterrupted()
来感知其他线程对其自身的中断操作,从而做出响应。3、另外,同样可以调用
Thread
的静态方法interrupted()
对当前线程进行中断操作,该方法会清除中断标志位。4、需要注意的是,当抛出
InterruptedException
的时候,会清除中断标志位,也就是说在调用isInterrupted()
会返回fasle
1、打断sleep的线程,会清空打断状态
public class Test05 {
public static void main(String[] args) throws InterruptedException{
Thread t1 = new Thread(()->{
try {
Thread.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
},"t1");
t1.start();
Thread.sleep(1);
t1.interrupt();
System.out.println("打断状态:" + t1.isInterrupted());
}
}
运行结果:
2、打断正常运行的线程,不会清空打断状态
public class Test07 {
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(()->{
while (true){
//获取当前线程
Thread current = Thread.currentThread();
//获取打断标记
boolean interrupted = current.isInterrupted();
if (interrupted){
System.out.println("打断状态:" + interrupted);
break;
}
}
},"t1");
t1.start();
sleep(1);
t1.interrupt();
}
}
运行结果:
打断状态:true
3、打断park线程,不会清空打断状态
public class Test08 {
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(()->{
System.out.println("park...");
LockSupport.park();
System.out.println("unpark...");
System.out.println("打断状态:" + Thread.currentThread().isInterrupted());
},"t1");
t1.start();
sleep(1);
t1.interrupt();
}
}
运行结果:
park...
unpark...
打断状态:true
4、如果打断标记已经是true,则park会失效
public class Test09 {
public static void main(String[] args) throws InterruptedException{
Thread t1 = new Thread(()->{
for (int i = 0; i < 5; i++){
System.out.println("park...");
LockSupport.park();
System.out.println("打断状态:" + Thread.currentThread().isInterrupted());
}
},"t1");
t1.start();
sleep(1);
t1.interrupt();
}
}
运行结果:
park...
打断状态:true
park...
打断状态:true
park...
打断状态:true
park...
打断状态:true
park...
打断状态:true
8、守护线程daemon
默认情况下,Java进程需要等待所有线程都运行结束,才会结束。但是,有一种特殊的线程叫做守护线程,只要其他非守护线程运行结束了,即使守护线程的代码没有执行完,也会强制结束。守护线程的设定建议在
start()
调用之前。
public class Test10 {
public static void main(String[] args) throws InterruptedException {
System.out.println("主线程开始运行...");
Thread t1 = new Thread(()->{
System.out.println("守护线程开始运行...");
try {
sleep(2);//守护线程休眠2毫秒
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("守护线程运行结束...");
},"t1");
//设置该线程为守护线程
t1.setDaemon(true);
t1.start();
sleep(1);//主线程休眠1毫秒,此时主线程已经执行完成,守护线程也就停止运行
System.out.println("主线程运行结束...");
}
}
运行结果:
主线程开始运行...
守护线程开始运行...
主线程运行结束...
注意:
9、模式之两阶段终止模式
1、在一个线程T1中如何优雅终止线程T2,这里的优雅是指给T2一个处理后事的机会。
2、错误思路
使用线程对象的
stop()
方法停止线程:stop方法会真正杀死线程,如果这时线程锁住了共享资源,那么当它被杀死的时后就再也没有机会释放锁,其它线程将永远无法获取锁。使用
System.exit(int)
方法停止线程:目的仅是停止一个线程,但这种做法会让整个程序都停止。3、正确做法
利用isInterrupted
interrupt()
方法可以打断正在执行的线程,无论这个线程是在sleep、wait
状态,还是正常运行。
public class Test08 {
public static void main(String[] args) throws InterruptedException {
TwoPhaseTermination tpt = new TwoPhaseTermination();
tpt.startMonitor();
Thread.sleep(3500);
tpt.stopMonitor();
}
}
//监控类
class TwoPhaseTermination{
private Thread monitor;
//启动监控线程
public void startMonitor(){
monitor = new Thread(()->{
//循环监控
while (true) {
//获取当前线程
Thread current = Thread.currentThread();
//为true终止循环
if (current.isInterrupted()) {
System.out.println("料理后事...");
break;
}
try {
Thread.sleep(1000); //情况1
System.out.println("执行监控记录"); //情况2
} catch (InterruptedException e) {
e.printStackTrace();
//重新设置打断标记
current.interrupt();
}
}
});
//启动线程
monitor.start();
}
//停止监控线程
public void stopMonitor(){
monitor.interrupt();
}
}