Java并发学习笔记(一):线程和进程、线程的基本使用、线程相关方法、线程运行原理、线程的状态

简介

一、进程与线程

参考:进程和线程

1、进程

  • 进程是由指令和数据组成的,但这些指令要运行,数据要读写,就必须将指令加载到CPU,数据加载至内存。在指令运行过程中还需要用到磁盘、网络设备。进程就是用来加载指令、管理I/O、管理内存
  • 当一个程序被运行,从磁盘加载这个程序的代码至内存,这时就启动了一个进程
  • 进程可以视为程序的一个运行实例(程序是静态的,进程是动态的),大部分程序可以运行多个进程(浏览器、记事本等),也有的程序只运行一个进程(360等)

2、线程

  • 一个进程中可以有一到多个进程
  • 一个进程就是一段指令流,将指令流中的一条条指令以一定的顺序交给CPU执行
  • 在Java中,线程作为最小的调度单位,进程作为资源分配的最小单位。(Windows中进程是不活动的,只是作为线程的容器)
  • 线程会共享进程范围内的资源,例如内存句柄和文件句柄,但每个线程都有各自的程序计数器(Program Counter)、栈以及局部变量等

3、二者对比

  • 进程基本上相互独立,而线程存在于进程中,是进程的一部分
  • 进程拥有共有资源,如内存空间等,供其内部分的线程共享
  • 进程间通信较为复杂
    • 同一台计算器中的进程通信称为IPC(Inter Process Communication)
    • 不同计算机之间的进程通信,需要通过网络,并遵守共同的协议例如HTTP
  • 线程通信相对的简单,因为他们共享进程中的内存,一个例子是多个线程可以访问同一个共同变量
  • 线程更加轻量,线程上下文切换成本一般低于进程的上下文切换。

4、并发与并行

单核CPU下,线程实际上还是串行执行的。操作系统中有一个组件叫做任务调度器,将CPU的时间片(Windows下默认是最小15毫秒)分给不同的线程使用,只是由于CPU在线程之间的切换非常快,人们感觉是同时运行的。总结一句话是:微观串行,宏观并行

一般将这种线程轮流使用CPU的做法叫做并发(concurrent)

在这里插入图片描述

多核CPU下,每个核(core)都可以调度运行线程,这时候线程可以是**并行(Parallel)**的

在这里插入图片描述

引用Rob Pike的一段描述:

  • 并发(Concurrent)是同一时间应对(deal with)多件事情的能力
  • 并行(Parallel)是同一时间动手做(doing)多件事情的能力

二、线程的应用场景

1、应用之异步调用

从方法调用的角度来讲,如果

  • 需要等待方法调用结束,才能继续运行称为同步
  • 不需要等待方法调用结束,就能继续运行称为异步

注意:同步在多线程中还有一层意思是让多个线程步调一致

多线程可以让方法执行变成异步的(即不需要等待),比如说读取磁盘文件时,假设读取操作花费5秒,如果没有多线程调度机制,这5秒什么都干不了,其他代码都需要暂停

例子

  • UI程序中,开启线程运行费时操作,避免阻塞UI线程
  • Tomcat的异步Servlet也有类似的目的,让用户线程处理耗时较长的操作,避免阻塞Tomcat的工作线程
2、应用之提升效率

多线程能够充分利用多核CPU的优势,提高运行效率。对下面的场景:执行三个计算,最后将计算结果汇总

计算1花费10ms
计算2花费12ms
计算3花费9ms
汇总花费1ms
  • 如果是串行执行,那么总共花费 10+12+9+1=32ms
  • 如果是四核CPU,各个核心分别使用线程123运行计算123,那么三个线程是并行的,花费的时间只取决于最长那个线程运行的时间即12ms,最后加上汇总的时间总共13ms

注意:需要在多核CPU下才能提高执行效率,单核仍然是轮流执行,效率可能更低

结论

  • 单核CPU下,多线程并不能实际提高程序运行效率,只是为了能够在不同任务之间切换,不同线程轮流使用CPU。同时线程切换需要一定的成本。
  • 多核CPU可以并行运行多个线程,但是能否提高运行效率还是要分情况
    • 有些任务可以进行拆分,并行执行进而提高执行效率,但不是所有任务都可以拆分(参考后文【阿姆达尔法则】)
    • 也不是所有任务都需要拆分
  • IO操作不占用CPU,只是一般情况下使用的是阻塞IO,此时需要等待IO结束才能继续运行线程。所以才有了后文介绍的非阻塞IO异步IO优化

Java线程

一、创建运行线程

1、方法一:直接使用Thread匿名内部类
//创建线程对象,创建时最好传入线程的名称
Thread t1 = new Thread("t1"){
    //run方法内实现要执行的方法
    @Override
    public void run(){
        //要执行的任务
        
    }
};
//启动线程
t1.start();
2、使用Runnable配合Thread

把【线程】和【任务】(要执行的代码)分开

  • Thread代表线程
  • Runnable代表可执行任务(线程要执行的代码)
//创建可执行任务对象
Runnable runnable = new Runnable() {
    @Override
    public void run() {
    	//要执行的任务
    }
};
//创建线程对象,参数1是可执行任务,参数2是线程的名称
Thread t1 = new Thread(runnable, "t1");
//运行线程
t1.start();

Java8之后可以使用lambda简化代码

Runnable runnable = () -> {
    //要执行的任务
};
Thread t1 = new Thread(runnable, "t1");
t1.start();

Thread和Runnable之间的关系:

第二种方法中,传入Thread类构造方法的runnable对象会赋值给,Thread类中的成员变量target

private void init(ThreadGroup g, Runnable target, String name,
					long stackSize, AccessControlContext acc) {
	...
	this.target = target;
	...
}

而Thread类继承了只有一个抽象方法的Runnable类:

@FunctionalInterface
public interface Runnable {
    public abstract void run();
}
public
class Thread implements Runnable {
	...
}

并在run()方法中进行了如下重写,如果发现target不为空(也就是传入了可执行任务runnable对象),则执行该runnable对象的run方法,也就是第二种方法。而第一种方式就是直接重写了Thread类的run()方法。

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

总结

  • 方法1把线程和执行任务合并到一起,方法2则把线程和任务分开了
  • 用Runnable更容易与线程池等高级API配合
  • 用Runnable让任务脱离了Thread继承体系,更加灵活
2、FutureTask配合Thread

FutureTask能够接收Callable类型的参数,用来处理有返回结果的情况

//创建任务对象
FutureTask<Integer> task = new FutureTask<>(new Callable<Integer>() {
    @Override
    public Integer call() throws Exception {
        //执行任务,并返回结果
        return 1;
    }
});
//传入任务对象和线程名,并运行线程
Thread t1 = new Thread(task, "t1");
t1.start();
//主线程阻塞,同步等待task执行完毕的结果
Integer result = task.get();

二、查看进程线程的方法

1、Windows
  • 任务管理器可以查看进程和线程数,也可以用来杀死线程
  • tasklist 查看进程
  • taskkill 杀死进程
2、Linux
  • ps -ef:查看所有进程
  • ps -fT -p <PID>:查看某个进程(PID)所有的线程
  • kill PID:杀死进程
  • top:动态的查看进程运行情况
  • top -H -p <PID>:查看某个进程(PID)的所有线程
3、Java
  • jps查看所有的Java进程
  • jstack <PID>:查看某个进程(PID)的所有线程的状态
  • jconsole <PID>:使用图形界面查看当前进程允许的状况

三、线程运行的原理

1、栈和栈桢

Java Virtual Machine Stack(Java虚拟机栈)

JVM中由堆、栈、方法区等组成。每个线程启动之后,虚拟机就会为其分配一块栈内存。

  • 每个栈由多个栈桢(Frame)组成,对应着每次方法调用时所占用的内存
  • 每个线程每个时刻只能有一个活动栈桢,对应着当前正在执行的那个方法
  • 栈桢在栈中的进进出出对应着方法的调用

对于下面这段这段代码,其单线程执行情况可以用下图表示:

public class FrameTest {

    public static void main(String[] args) {
		method1(10);
    }

    private static void method1(int x){
        int y = x+1;
        Object obj = method2();
        System.out.println(obj);
    }

    private static Object method2() {
        Object obj = new Object();
        return obj;
    }

}

在这里插入图片描述

2、线程的上下文切换(Thread Context Switch)

下面的一些情况会导致CPU不再执行当前的线程,转而执行另外的线程,即现场的上下文切换

  • 线程的CPU时间片用完
  • 垃圾回收的STW
  • 有更高优先级的线程需要执行
  • 线程自己调用sleep、yield、wait、join、park、synchronized、lock等方法

当上下文切换发生时,需要保存当前线程的状态,并恢复另一个线程的状态

  • 其中状态包括:程序计数器、虚拟机栈中每个栈桢的信息,如局部变量表、操作数栈、返回地址等
  • 上下文切换如果频繁发生会影响性能

下图展示了线程的上下文切换,CPU核心转向了t1线程,并对main线程的内存进行了保存(灰色)

在这里插入图片描述

四、线程相关常见方法

1、Start与Run

start方法:启动一个新的线程,并在新的线程中运行run方法

  • start方法只是让线程进入就绪,里面代码不一定立刻执行(CPU时间片还没有分给他)
  • 每个线程的Start方法只能调用一次,如果多次调用会出现IllegalThreadStateException异常

run方法:线程启动后会调用的方法

  • Thread对象直接调用run方法不会开启新线程,run方法中内容虽然可以运行但是还是在主线程中执行
  • 如果构造Thread对象时参入了Runnable对象,则调用的时Runnable的run方法。否则默认不执行任何操作,但是可以创建Thread的子类对象来重写默认行为
2、sleep与yield

sleep方法:让当前执行的线 程休眠n毫秒, 休眠时让出 cpu 的时间片给其它线程

  • 调用sleep方法会让当前线程从Running状态进入Time Waiting状态

在这里插入图片描述

  • 其他线程可以使用interrupt方法打断正在睡眠的线程,这时被打断的线程会抛出InterruptedException

在这里插入图片描述

  • 睡眠结束后的线程未必立刻得到执行,需要等到CPU分给他时间片
  • 建议使用TimeUnit的sleep方法代替Thread的Sleep方法,可以获得更好的可读性
TimeUnit.SECONDS.sleep(1); //还有DAYS等其他时间

yield方法:提示线程调度器让出当前线程对CPU的使用

  • 调用yield方法会让当前线程从Running运行状态转变为Runnable就绪状态,然后调用其他线程
  • 具体情况依赖于操作系统的任务调度器。如果当前没有其他线程,那仍然会给此线程分配CPU的时间片,让其继续执行。而相对于Sleep在计时完成之前,调度器不会为其分配CPU的时间片。

线程优先级

  • 线程优先级会提示(hint)调度器优先调度该线程,但它仅仅是一个提示,调度器可以忽略
  • 如果CPU比较忙,那么优先级高的线程可以获得更多的时间片,但是CPU闲的时候,优先级影响效果不大
t1.setPriority(Thread.MIN_PRIORITY); 
t2.setPriority(Thread.MAX_PRIORITY);

案例-防止CPU占用100%

在没有利用CPU来计算时,不要用while(true)空转浪费CPU。否则单核CPU下,CPU占用率可能达到近100%。这时可以使用sleep或者yield方法来让出CPU给其他程序

while(true){
    try{
        Thread.sleep(50);
    } catch(InterruptedException){
        e.printStackTrace();
    }
}
  • 可以使用wait或条件变量达到类似的效果,不同是,后两种需要加锁,并且需要相应的唤醒操作,一般适用于需要同步的场景
  • sleep适用于无需锁同步的场景
3、join方法

join方法:等待线程运行结束,之后才会运行后面的代码。可以用于同步。

对于下面的代码,r的值会输出为多少呢?

    static int r = 0;
    public static void main(String[] args) throws InterruptedException {
        test1();
    }
    private static void test1() throws InterruptedException {
        System.out.println("开始");
        Thread t1 = new Thread(() -> {
            try {
                System.out.println("开始");
                sleep(1);
                System.out.println("结束");
                r = 10;
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        },"t1");
        t1.start();
//        t1.join();
        System.out.println("结果为:"+ r);
        System.out.println("结束");
    }

结果是0,原因很简单,因为线程t1中的run方法还没执行结束,r的值就已经被输出了。那么要怎么做到等到r赋值完成之后在进行输出呢?
在这里插入图片描述
只要把第18行的注释去掉,调用线程t1的join方法。之后的代码就会等到t1线程执行完成之后才会执行。则此r的值已经被赋值为了10:

在这里插入图片描述

join(long n)方法:等待线程运行结束,最多等待 n 毫秒。

  • 有时限的等待
  • 如果 n 毫秒之前,线程就已经结束了,那么不会继续等待
4、interrupt方法

interrupt()方法:打断线程

  • 如果被打断线程正在 sleep,wait,join,会导致被打断的线程抛出InterruptedException,并清除打断标记
  • 如果打断的正在运行的线程,则会设置打断标记
  • park 的线程被打断,也会设置打断标记

isInterrupted()方法:判断当前线程是否被打断,之后不会清除打断标记

interrupted() 方法:判断当前线程是否被打断,并清除打断标记

1)、打断线程正在 sleep,wait,join的例子:

    private static void test1() throws InterruptedException {
        Thread t1 = new Thread(()->{
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
                System.out.println(" 打断状态: " + Thread.currentThread().isInterrupted());
            }
        }, "t1");

        t1.start();

        Thread.sleep(500);
        t1.interrupt();
    }

结果是:触发了InterruptedException异常,同时也清除了打断标记

在这里插入图片描述

2)、打断的正在运行的线程:

    private static void test2() throws InterruptedException {
        Thread t1 = new Thread(()->{
            while (true) {
                boolean interrupted = Thread.currentThread().isInterrupted();
                if (interrupted) {
                    System.out.println(" 打断状态: " + Thread.currentThread().isInterrupted());
                    System.out.println("被打断之后,可以再做一些其他事情之后再停止");
                    break;
                }
            }
        }, "t1");

        t1.start();

        Thread.sleep(500);
        t1.interrupt();
    }

结果如下图所示,调用interrupt()方法之后interrupted被标记为true,但是线程没有像之前那样抛出异常结束,而是继续运行。如果想要结束线程,可以在线程中判断interrupted的状态进行结束,这种方式更加优雅,可以在结束之前进行一些善后工作。

在这里插入图片描述

5、两阶段终止模式

引入:在一个线程T1中如何“优雅”终止线程T2?这里的“优雅”是指给T2一个料理后事的机会。

错误做法

  • 使用线程对象的stop方法停止线程
    • stop方法会真正的杀死进程,如果这时线程锁住了共享资源,那么它被杀死后就再也没有机会释放资源锁,那么其他线程也永远无法获得该资源的锁
  • 使用System.exit(init)方法停止线程
    • 这种做法会使整个程序终止。

正确做法:使用两阶段终止模式

假设当前需要不断每2秒钟进行一次监控信息获取,并且在不需要的时候可以进行打断。使用两阶段终止模式实现的整体流程如下图所示:

在这里插入图片描述

具体实现框架可以参考下面的代码:

/**
 * 两阶段终止框架
 */
public class TwoPhaseTermination {

    //监控线程
    private static Thread monitor = null;

    public static void main(String[] args) throws InterruptedException {
        start();
        Thread.sleep(7000);
        stop();
    }

    //开始监控线程
    private static void start(){
        monitor = new Thread(new Runnable() {
            @Override
            public void run() {
                while (true){
                    if (monitor.isInterrupted()){
                        //运行中检测到Interrupte,进行善后工作,并结束
                        System.out.println("善后工作");
                        break;
                    }
                    try {
                        //每隔2秒执行一次检测程序
                        Thread.sleep(2000);
                        System.out.println("执行检测程序");
                    } catch (InterruptedException e) {
                        //睡眠中被打断会抛出该异常,并继续执行
                        e.printStackTrace();
                        //手动打断线程
                        monitor.interrupt();
                    }
                }
            }
        }, "monitor");

        monitor.start();
    }

    //停止监控线程
    private static void stop(){
        monitor.interrupt();
    }

}

执行结果如下图所示:

在这里插入图片描述

6、不推荐使用多个的方法

这些方法已经过时,容易破坏同步代码块,造成线程死锁

  • stop方法:停止线程运行
  • suspend方法:挂起(暂停)线程
  • resume方法:恢复线程运行

五、线程其他相关内容

1、主线程和守护线程

默认情况下,Java进程需要等到所有线程都结束才会停止。但是有一种特殊的线程称为守护线程,只要当其他非守护线程都结束,即使守护线程的代码没有执行完,也会强制停止。

    public static void main(String[] args) {
        log.debug("开始运行..."); 
        Thread t1 = new Thread(() -> {   
                log.debug("开始运行...");    
                sleep(2);    
                log.debug("运行结束..."); 
            }, "daemon"); 
        // 设置该线程为守护线程
        t1.setDaemon(true);
        t1.start();
        sleep(1); 
        log.debug("运行结束...");
    }

结果:

08:26:38.123 [main] c.TestDaemon - 开始运行... 
08:26:38.213 [daemon] c.TestDaemon - 开始运行... 
08:26:39.215 [main] c.TestDaemon - 运行结束... 

应用:

  • 垃圾回收器线程就是一种守护线程
  • Tomcat 中的 Acceptor 和 Poller 线程都是守护线程,所以 Tomcat 接收到 shutdown 命令后,不会等 待它们处理完当前请求
2、线程的五种状态(操作系统层面)

线程的五种状态是从操作系统层面上描述的:

在这里插入图片描述

  • 初始状态:仅是在语言层面上的对象创建,还未于操作系统线程关联
  • 可运行状态(就绪状态):指该线程已经被创建(与操作系统线程关联),可以由CPU执行调度
  • 运行状态:指获取到了CPU时间片,正在运行中的状态。
    • 当CPU时间片用完,会从【运行状态】转变到【可运行状态】,对应着线程上下文的切换
  • 阻塞状态:
    • 如果调用了阻塞API,如BIO读取文件,这时线程实际上不会用到CPU,会导致线程上下文切换,进入【阻塞状态】
    • 等待BIO操作完成,会由操作系统唤醒阻塞的线程,转换为【可运行状态】
    • 【阻塞状态】和【可运行状态】的区别是,对于【阻塞状态】的线程只要他们一直不唤醒,调度器就一直不会考虑调度他们
  • 终止状态:表示线程已经执行完毕,生命周期已经结束,不会在转为其他状态
3、线程的六种状态(Java语言层面)

这是从 Java API 层面来描述的 Thread.State 枚举,线程分为六种状态:

在这里插入图片描述

  • NEW:线程对象刚创建,还没有调用start方法。与操作系统层面上的初始状态对应
  • RUNNABLE:当调用了start方法之后。,注意,Java API 层面的 RUNNABLE 状态涵盖了 操作系统 层面的 【可运行状态】、【运行状态】和【阻塞状态】(由于 BIO 导致的线程阻塞,在 Java 里无法区分,仍然认为 是可运行)
  • BLOCKED , WAITING , TIMED_WAITING 都是 Java API 层面对【阻塞状态】的细分,后面会在状态转换一节 详述
  • TERMINATED 当线程代码运行结束
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值