侧边栏壁纸
  • 累计撰写 98 篇文章
  • 累计创建 20 个标签
  • 累计收到 3 条评论

并发编程基础

林贤钦
2020-05-23 / 0 评论 / 13 点赞 / 638 阅读 / 0 字
温馨提示:
本文最后更新于 2020-05-25,若内容或图片失效,请留言反馈。部分素材来自网络,若不小心影响到您的利益,请联系我们删除。

并发编程基础

1、基础概念

1.1、进程与线程

进程

程序由指令和数据组成,但这些指令要运行,数据要读写,就必须将指令加载至 CPU,数据加载至内存。在指令运行过程中还需要用到磁盘、网络等设备。进程就是用来加载指令、管理内存、管理 IO 的。当一个程序被运行,从磁盘加载这个程序的代码至内存,这时就开启了一个进程。进程就可以视为程序的一个实例。

大部分程序可以同时运行多个实例进程(例如记事本、画图、浏览器等),也有的程序只能启动一个实例进程(例如网易云音乐、360 安全卫士等)

线程

线程是进程中的一个执行单元,负责当前进程中程序的执行,一个进程中至少有一个线程。一个进程中是可以有多个线程的,这个应用程序也可以称之为多线程程序。 一个线程就是一个指令流,将指令流中的一条条指令以一定的顺序交给 CPU 执行

Java 中,线程作为最小调度单位,进程作为资源分配的最小单位。 在 windows 中进程是不活动的,只是作为线程的容器

1.2、线程和进程的区别

  • 进程是程序运行和资源分配的基本单位,一个程序至少有一个进程,一个进程至少有一个线程。

  • 进程在执行过程中拥有独立的内存单元,而多个线程共享内存资源,减少切换次数,从而效率更高。

  • 线程是进程的一个实体,是cpu调度和分派的基本单位,是比程序更小的能独立运行的基本单位。同一进程中的多个线程之间可以并发执行。

简而言之:一个程序运行后至少有一个进程,一个进程中可以包含多个线程

1.3、并发与并行

  • 并发:多条线程在同一时间段内交替执行。通过cpu调度算法,让用户看上去同时执行,实际上从cpu操作层面不是真正的同时。微观上串行,宏观上并行

    一个人同时要做很多件事,但只能一件一件的做

  • 并行:多个cpu实例或者多台机器同时执行一段处理逻辑,多条线程同时执行。

    多个人同时做多件事,是真正的同时。

1.4、线程的优点

​ 恰当的使用线程时,可以降低开发和维护的开销,并且能提高复杂应用的性能

2、java 线程

2.1、创建和运行线程

一种是继承Thread类方式,一种是实现Runnable接口方式

2.1.1、直接使用 Thread

在线程的Thread对象上调用start()方法,而不是run()或者别的方法。

在调用start()方法之前:线程处于新状态中,新状态指有一个Thread对象,但还没有一个真正的线程。

在调用start()方法之后:发生了一系列复杂的事情——

启动新的执行线程(具有新的调用栈);

该线程从新状态转移到可运行状态;

当该线程获得机会执行时,其目标run()方法将运行。

// 创建线程对象
Thread t = new Thread() {
 public void run() {
 // 要执行的任务
 }
};
// 启动线程
t.start();
构造方法
public Thread()分配一个新的线程对象。
public Thread(String name)分配一个指定名字的新的线程对象
public Thread(Runnable target)分配一个带有指定目标新的线程对象。
public Thread(Runnable target,String name)分配一个带有指定目标新的线程对象并指定名字。

常用方法
public String getName()获取当前线程名称
public void start()导致此线程开始执行; Java虚拟机调用此线程的run方法
public void run()此线程要执行的任务在此处定义代码。
public static void sleep(long millis)使当前正在执行的线程以指定的毫秒数暂停(暂时停止执行)。
public static Thread currentThread()返回对当前正在执行的线程对象的引用

2.1.2、使用 Runnable 配合 Thread

步骤如下:

  1. 定义Runnable接口的实现类,并重写该接口的run()方法,该run()方法的方法体同样是该线程的线程执行体。
  2. 创建Runnable实现类的实例,并以此实例作为Thread的target来创建Thread对象,该Thread对象才是真正 的线程对象。
  3. 调用线程对象的start()方法来启动线程。
Runnable runnable = new Runnable() {
 public void run(){
 // 要执行的任务
 }
};
// 创建线程对象
Thread t = new Thread( runnable );
// 启动线程
t.start();

注意:对Java来说,run()方法没有任何特别之处。像main()方法一样,它只是新线程知道调用的方法名称(和签名)。因此,在Runnable上或者Thread上调用run方法是合法的。但并不启动新的线程。

2.1.3、FutureTask 配合 Thread

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

// 创建任务对象
FutureTask<Integer> task = new FutureTask<>(() -> {
 log.debug("hello");
 return 100;
});
// 参数1 是任务对象; 参数2 是线程名字,推荐
new Thread(task, "t").start();
// 主线程阻塞,同步等待 task 执行完毕的结果
Integer result = task.get();
log.debug("结果是:{}", result);

2.2、Thread和Runnable的区别

如果一个类继承Thread,则不适合资源共享。但是如果实现了Runable接口的话,则很容易的实现资源共享。

总结:

实现Runnable接口比继承Thread类所具有的优势:

  1. 适合多个相同的程序代码的线程去共享同一个资源。
  2. 可以避免java中的单继承的局限性。
  3. 增加程序的健壮性,实现解耦操作,代码可以被多个线程共享,代码和线程独立。
  4. 线程池只能放入实现Runable或Callable类线程,不能直接放入继承Thread的类。

扩充:在java中,每次程序运行至少启动2个线程。一个是main线程,一个是垃圾收集线程。因为每当使用 java命令执行一个类的时候,实际上都会启动一个JVM,每一个JVM其实在就是在操作系统中启动了一个进 程。

2.3 查看进程线程的方法

  • windows

    tasklist 查看进程

    taskkill 杀死进程

    任务管理器可以查看进程和线程数,也可以用来杀死进程

  • linux

    ps -fe 查看所有进程

    ps -fT -p 查看某个进程(PID)的所有线程

    kill 杀死进程

    top 按大写 H 切换是否显示线程

    top -H -p 查看某个进程(PID)的所有线程

  • Java

    jps 命令查看所有 Java 进程

    jstack 查看某个 Java 进程(PID)的所有线程状态

    jconsole 来查看某个 Java 进程中线程的运行情况(图形界面)

2.4、原理之线程运行

栈与栈帧

Java Virtual Machine Stacks (Java 虚拟机栈)

我们都知道 JVM 中由堆、栈、方法区所组成,其中栈内存是给谁用的呢?其实就是线程,每个线程启动后,虚拟

机就会为其分配一块栈内存。

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

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

因为以下一些原因导致 cpu 不再执行当前的线程,转而执行另一个线程的代码

  • 线程的 cpu 时间片用完

  • 垃圾回收

  • 有更高优先级的线程需要运行

  • 线程自己调用了 sleep、yield、wait、join、park、synchronized、lock 等方法

当 Context Switch 发生时,需要由操作系统保存当前线程的状态,并恢复另一个线程的状态,Java 中对应的概念

就是程序计数器(Program Counter Register),它的作用是记住下一条 jvm 指令的执行地址,是线程私有的

状态包括程序计数器、虚拟机栈中每个栈帧的信息,如局部变量、操作数栈、返回地址等

Context Switch 频繁发生会影响性能

2.5、 常见方法

方法名static功能说明注意
start() 启动一个新线程,
在新的线程运行 run
方法中的代码
start 方法只是让线程进入就绪,
里面代码不一定立刻运行
(CPU 的时间片还没分给它)。
每个线程对象的start方法只能调用一次,
如果调用了多次会出现
IllegalThreadStateException
run() 新线程启动后会
调用的方法
如果在构造 Thread 对象时传递了
Runnable 参数,则线程启动后会
调用 Runnable 中的 run 方法,否
则默认不执行任何操作。但可以创
建 Thread 的子类对象,来覆盖默认行为
join() 等待线程运行结束
join(long n) 等待线程运行结束,
最多等待 n毫秒
getId() 获取线程长整型的 idid 唯一
getName() 获取线程名
setName(String) 修改线程名
getPriority() 获取线程优先级
setPriority(int) 修改线程优先级ava中规定线程优先级是1~10
的整数,较大的优先级能提高
该线程被 CPU 调度的机率
getState() 获取线程状态Java 中线程状态是用 6 个
enum 表示,分别为:NEW,
RUNNABLE, BLOCKED, WAITING,
TIMED_WAITING, TERMINATED
isInterrupted() 判断是否被打断不会清除 打断标记
isAlive() 线程是否存活
(还没有运行完毕)
interrupt() 打断线程如果被打断线程正在 sleep,wait,
join 会导致被打断的线程抛出
InterruptedException,并清除
打断标记 ;如果打断的正在运行
的线程,则会设置 打断标记 ;
park 的线程被打断,也会设置 打断标记
interrupted()static判断当前线程是否被打断会清除 打断标记
currentThread()static获取当前正在执行的线程
sleep(long n)static让当前执行的线程休眠
n毫秒,休眠时让出 cpu
的时间片给其它线程
yield()static提示线程调度器让出当
前线程对CPU的使用
主要是为了测试和调试

2.6、 start run

  • 调用 run

    public static void main(String[] args) {
         Thread t1 = new Thread("t1") {
             @Override
             public void run() {
                log.debug(Thread.currentThread().getName());
                FileReader.read(Constants.MP4_FULL_PATH);
             }
         };
         t1.run();
         log.debug("do other things ...");
    }
    

    输出

    19:39:14 [main] c.TestStart - main

    19:39:14 [main] c.FileReader - read [1.mp4] start ...

    19:39:18 [main] c.FileReader - read [1.mp4] end ... cost: 4227 ms

    19:39:18 [main] c.TestStart - do other things ...

    程序仍在 main 线程运行, FileReader.read() 方法调用还是同步的

  • 调用 start

    将上述代码的 t1.run() 改为

    t1.start();

    输出

    19:41:30 [main] c.TestStart - do other things ...

    19:41:30 [t1] c.TestStart - t1

    19:41:30 [t1] c.FileReader - read [1.mp4] start ...

    19:41:35 [t1] c.FileReader - read [1.mp4] end ... cost: 4542 ms

    程序在 t1 线程运行, FileReader.read() 方法调用是异步的

小结

  • 直接调用 run 是在主线程中执行了 run,没有启动新的线程
  • 使用 start 是启动新的线程,通过新的线程间接执行 run 中的代码

2.7、 sleep yield

sleep

  • 调用 sleep 会让当前线程从 Running 进入 Timed Waiting 状态(阻塞)

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

  • 睡眠结束后的线程未必会立刻得到执行

  • 建议用 TimeUnit 的 sleep 代替 Thread 的 sleep 来获得更好的可读性

yield

  • 调用 yield 会让当前线程从 Running 进入 Runnable 就绪状态,然后调度执行其它线程

  • 具体的实现依赖于操作系统的任务调度器

线程优先级

线程优先级会提示(hint)调度器优先调度该线程,但它仅仅是一个提示,调度器可以忽略它

如果 cpu 比较忙,那么优先级高的线程会获得更多的时间片,但 cpu 闲时,优先级几乎没作用

2.8、 interrupt 方法详解

打断 sleep,wait,join 的线程,这几个方法都会让线程进入阻塞状态

打断 sleep 的线程, 会清空打断状态,以 sleep 为例

private static void test1() throws InterruptedException {
     Thread t1 = new Thread(()->{
         sleep(1);
     }, "t1");
     t1.start();
     sleep(0.5);
     t1.interrupt();
     log.debug(" 打断状态: {}", t1.isInterrupted());
}

输出

java.lang.InterruptedException: sleep interrupted

at java.lang.Thread.sleep(Native Method)

at java.lang.Thread.sleep(Thread.java:340)

at java.util.concurrent.TimeUnit.sleep(TimeUnit.java:386)

at cn.itcast.n2.util.Sleeper.sleep(Sleeper.java:8)

at cn.itcast.n4.TestInterrupt.lambda$test1$3(TestInterrupt.java:59)

at java.lang.Thread.run(Thread.java:745)

21:18:10.374 [main] c.TestInterrupt - 打断状态: false

打断正常运行的线程

打断正常运行的线程, 不会清空打断状态

private static void test2() throws InterruptedException {
     Thread t2 = new Thread(()->{
         while(true) {
             Thread current = Thread.currentThread();
             boolean interrupted = current.isInterrupted();
             if(interrupted) {
                 log.debug(" 打断状态: {}", interrupted);
                 break;
            }
         }
     }, "t2");
     t2.start();
     sleep(0.5);
     t2.interrupt();
}

输出

20:57:37.964 [t2] c.TestInterrupt - 打断状态: true

2.9 、 不推荐的方法

还有一些不推荐使用的方法,这些方法已过时,容易破坏同步代码块,造成线程死锁

方法名功能说明
stop()停止线程运行
suspend()挂起(暂停)线程运行
resume()恢复线程运行

2.10、主线程与守护线程

默认情况下,Java 进程需要等待所有线程都运行结束,才会结束。有一种特殊的线程叫做守护线程,只要其它非守护线程运行结束了,即使守护线程的代码没有执行完,也会强制结束。

注意

垃圾回收器线程就是一种守护线程

Tomcat 中的 Acceptor 和 Poller 线程都是守护线程,所以 Tomcat 接收到 shutdown 命令后,不会等

待它们处理完当前请求

3、线程的状态

系统中的线程状态

  • 创建状态: (NEW)

    线程对象被创建后,就进入了新建状态

  • 就绪状态: (Runnable)

    线程对象创建后,其他线程(比如main线程)调用了该对象的start()方法。该状态的线程位于可运行线程池中,等待被线程调度选中,获取cpu 的使用权 。

  • 运行状态: (Runnable)

    可运行状态(runnable)的线程获得了cpu 时间片(timeslice) ,执行程序代码。

  • 阻塞状态:(Blocked、Waiting 、 Timed Waiting)

    阻塞状态是指线程因为某种原因放弃了cpu 使用权,也即让出了cpu timeslice,暂时停止运行。直到线程进入可运行(runnable)状态,才有机会再次获得cpu timeslice 转到运行(running)状态。阻塞的情况分三种:

    等待阻塞 : 运行(running)的线程执行o.wait()方法,JVM会把该线程放入等待队列(waitting queue)中。

    同步阻塞: 运行(running)的线程在获取对象的同步锁时,若该同步锁被别的线程占用,则JVM会把该线程放入锁池(lock pool)中。

    其他阻塞:运行(running)的线程执行Thread.sleep(long ms)或t.join()方法,或者发出了I/O请求时,JVM会把该线程置为阻塞状态。当sleep()状态超时、join()等待线程终止或者超时、或者I/O处理完毕时,线程重新转入可运行(runnable)状态。

  • 死亡状态:(Teminated )

    线程run()、main() 方法执行结束,或者因异常退出了run()方法,则该线程结束生命周期。死亡的线程不可再次复生。

java线程状态

线程状态导致状态发生条件
NEW(新建)线程刚被创建,但是并未启动。还没调用start方法。
Runnable(可运行)线程可以在java虚拟机中运行的状态,可能正在运行
自己代码,也可能没有,这取决于操作系统处理器。
Blocked(锁阻塞)当一个线程试图获取一个对象锁,而该对象锁被其
他的线程持有,则该线程进入Blocked状 态;当该
线程持有锁时,该线程将变成Runnable状态。
Waiting(无限等待)一个线程在等待另一个线程执行一个(唤醒)动作
时,该线程进入Waiting状态。进入这个状态后是
不能自动唤醒的,必须等待另一个线程调用notify
或者notifyAll方法才能够唤醒。
Timed Waiting(计时等待)waiting状态,有几个方法有超时参数,调用他们
将进入Timed Waiting状态。这一状态 将一直保持
到超时期满或者接收到唤醒通知。带有超时参数的
常用方法有Thread.sleep
Teminated(被 终止)因为run方法正常退出而死亡,或者因为没有捕获
的异常终止了run方法而死亡。

13

评论区