并发编程笔记

并发编程

基础概念

进程和线程

进程

​ 进程是程序运行资源分配的最小单位。

线程

​ 一个机器中会运行很多程序,程序之间的切换依赖CPU调度,线程就是CPU调度的最小单位。线程依赖进程存在,是进程中的一个实体,是CPU调度分派的基本单位。

进程间的通信方式
  • 管道(channel)
    • 匿名管道(pipe)

      父子进程间的通信,即父进程可以fork出一个子进程

    • 命名管道(named pipe)

      除了具备管道所有功能外,还允许无亲缘关系间进程的通信

  • 信号(single)

    信号是软件层次上对中断机制的一种模拟,是比较复杂的一种通信方式,用于通知进程有某事件发,生一个进程收到一个信号与处理器 收到一个中断请求效果上可以说是一致的。

  • 消息队列(message queue)

​ 消息队列是消息的链接表,它克服了上两 种通信方式中信号量有限的缺点,具有写权限得进程可以按照一定得规则向消息队列中添加新信息;对消息队列有读权限得进程则可以从消息队列中读取信息。

  • 共享内存(shared memory)

​ 可以说这是最有用的进程间通信方式。它使得多个进程可以访问同一块内存空间,不同进程可以及时看到对方进程中对共享内存中数据得更新。这种方式需要依靠某种同步操作,如互斥锁和信号量等。

  • 信号量(semaphore)

    主要作为进程之间及同一种进程的不同线程之间的同步和互斥手段。

  • 套接字(socket)

    这是一种更为一般得进程间通信机制,它可用于网络 中不同机器之间的进程间通信,应用非常广泛。同一机器中的进程还可以使用 Unix domain socket(比如同一机器中 MySQL 中的控制台 mysql shell 和 MySQL 服务程序的连接),这种方式不需要经过网络协议栈,不需要打包拆包、计算校验、维护序号和应答等,比纯粹基于网络的进程间通信肯定效率更高。

上下文切换

在这里插入图片描述

​ 既然操作系统要在多个进程(线程)之间进行调度,而每个线程在使用 CPU时总是要使用 CPU中的资源,比如 CPU寄存器和程序计数器。这就意味着,操作系统要保证线程在调度前后的正常执行,所以,操作系统中就有上下文切换的概念,它是指 CPU(中央处理单元)从一个进程或线程到另一个进程或线程的切换。

​ 上下文是 CPU 寄存器和程序计数器在任何时间点的内容。

​ 寄存器是 CPU 内部的一小部分非常快的内存(相对于 CPU 内部的缓存和 CPU 外部较慢的 RAM 主内存),它通过提供对常用值的快速访问来加快计算机程序的执行。

​ 程序计数器是一种专门的寄存器,它指示 CPU 在其指令序列中的位置,并保存着正在执行的指令的地址或下一条要执行的指令的地址,这取决于具体的系统。

​ 上下文切换可以更详细地描述为内核(即操作系统的核心)对 CPU 上的进程 (包括线程)执行以下活动:

  1. 暂停一个进程的处理,并将该进程的 CPU 状态(即上下文)存储在内存中的 某个地方
  2. 从内存中获取下一个进程的上下文,并在 CPU 的寄存器中恢复它
  3. 返回到程序计数器指示的位置(即返回到进程被中断的代码行)以恢复进程。

​ 从数据来说,以程序员的角度来看,是方法调用过程中的各种局部的变量与资源; 以线程的角度来看,是方法的调用栈中存储的各类信息。

​ 引发上下文切换的原因一般包括:线程、进程切换、系统调用等等。上下文切换通常是计算密集型的,因为涉及一系列数据在各种寄存器、缓存中的来回拷贝。

就 CPU 时间而言,一次上下文切换大概需要 5000~20000 个时钟周期,相 对一个简单指令几个乃至十几个左右的执行时钟周期,可以看出这个成本的巨大。

并发和并行
并发

​ 指应用能够交替执行不同的任务,比如单 CPU 核心下执行多线程并非是同时执行多个任务,如果你开两个线程执行,就是在你几乎不可能察觉到的速度不断去切换这两任务,已达到"同时执行效果",其实并不是的,只是计算机的速度太快,我们无法察觉到而已。

并行

​ 指应用能够同时执行不同的任务,例:吃饭的时候可以边吃饭边 打电话,这两件事情可以同时执行。

两者区别

​ 一个是交替执行,一个是同时执行。

JAVA中的线程

​ Java程序天生就是多线程的,即使自身没有开启线程,实际上也有很多JVM自行启动的线程:

​ [6] Monitor Ctrl-Break //监控 Ctrl-Break 中断信号的

​ [5] Attach Listener //内存 dump,线程 dump,类信息统计,获取系统属性等

​ [4] Signal Dispatcher // 分发处理发送给 JVM信号的线程

​ [3] Finalizer // 调用对象 finalize 方法的线程

​ [2] Reference Handler//清除 Reference 的线程

​ [1] main //main 线程,用户程序入口

​ 即Java天生就是多线程的。

线程的启动

启动线程的方式
继承Thread类

​ X.extends Thread -----> x.start();

public class UseThread extends Thread{
	@Override
	public void run() {
		super.run();
		// 业务逻辑;
	}
}

//启动
UseThread useThread = new UseThread();
useThread.start();
实现Runnable接口

​ +…X implements Runnable;然后交给 Thread 运行

public class UseRunnable implements Runnable{
	@Override
	public void run() {
		// 业务逻辑;
	}
}

//启动
UseRunnable useRunnable = new UseRunnable();
new Thread(useRunnable).start();
Thread 和 Runnable 的区别
Thread 才是 Java 里对线程的唯一抽象,Runnable 只是对任务(业务逻辑) 的抽象。Thread 可以接受任意一个 Runnable 的实例并执行。
Callable、Future 和 FutureTask

​ Runnable 是一个接口,在它里面只声明了一个 run()方法,由于 run()方法返回值为 void 类型,所以在执行完任务之后无法返回任何结果。 Callable 位于java.util.concurrent 包下,它也是一个接口,在它里面也只声明 了一个方法,只不过这个方法叫做 call(),这是一个泛型接口,call()函数返回的 类型就是传递进来的 V 类型。

Future 就是对于具体的 Runnable 或者 Callable 任务的执行结果进行取消、查询是否完成、获取结果。必要时可以通过 get 方法获取执行结果,该方法会阻塞直到任务返回结果。

​ 因为 Future 只是一个接口,所以是无法直接用来创建对象使用的,因此就有了下面的 FutureTask。

​ FutureTask 类实现了 RunnableFuture 接口,RunnableFuture 继承了 Runnable 接口和 Future 接口,而 FutureTask 实现了 RunnableFuture 接口。所以它既可以作为 Runnable 被线程执行,又可以作为 Future 得到 Callable 的返回值。

​ 因此我们通过一个线程运行 Callable,但是 Thread 不支持构造方法中传递 Callable 的实例,所以我们需要通过FutureTask 把一个Callable 包装成 Runnable,

然后再通过这个 FutureTask 拿到 Callable 运行后的返回值。

public class UseCallable implements Callable {

	@Override
	public Object call() throws Exception {
		// 业务逻辑
		return null;
	}
}

//启动
UseCallable useCallable = new UseCallable();
FutureTask<UseCallable> futureTask = new FutureTask<UseCallable>(useCallable);
new Thread(futureTask).start();
run和start的区别

​ start()方法让一个线程进行就绪队列等待分配CPU资源,分到CPU之后才会调用实现的run()方法,start()方法不可以重复调用,会抛出异常。

​ run()方法是实现业务逻辑的方法,可以重复执行。

线程的终止

自然终止

​ 即run方法执行完成或抛出一个未处理的异常导致线程提前结束。

stop

​ 线程的暂停、恢复、停止之类的操作对应的Thread的API就是 suspend()、resume() 和 stop()。但是这些API都是过期的,不推荐使用。

以 suspend()方法为例,在调用后,线程不会释放已经占有的资源(比如 锁),而是占有着资源进入睡眠状态,这样容易引发死锁问题。同样,stop()方法在终结一个线程时不会保证线程的资源正常释放,通常是没有给予线程完成资 源释放工作的机会,因此会导致程序可能工作在不确定状态下。正因为 suspend()、esume()和 stop()方法带来的副作用,这些方法才被标注为不建议使用的过期方法。

中断

​ 既然上述的方法都不推荐使用,那么必然有推荐使用的中断方法,即 interrupt()方法。

​ 其他线程通过调用某个线程的 interrupt()方法对其进行中断操作,但是该线程并不会立即停止工作,线程会通过检查自身的中断标志位是否被置为true来响应。

​ 线程通过isInterrupted()来进行判断是否被中断,也可以调用静态方法 Thread.interrupted()来进行判断当前线程是否被中断,不过 Thread.interrupted()会同时将中断标识位改写为 false。

​ 如果一个线程处于了阻塞状态(如线程调用了 thread.sleep、thread.join、 thread.wait 等),则在线程在检查中断标示时如果发现中断标示为 true,则会在这些阻塞方法调用处抛出 InterruptedException 异常,并且在抛出异常后会立即将线程的中断标示位清除,即重新设置为 false。

不建议自定义一个取消标志位来中止线程的运行。因为 run 方法里有阻塞调用时会无法很快检测到取消标志,线程必须从阻塞调用返回后,才会检查这个取消标志。

这种情况下,使用中断会更好,因为,

​ 一、一般的阻塞方法,如 sleep 等本身就支持中断的检查,

​ 二、检查中断位的状态和检查取消标志位没什么区别,用中断位的状态还可 以避免声明取消标志位,减少资源的消耗。

注意:处于死锁状态的线程无法被中断

线程的生命周期

​ Java中的线程状态分为6种:

  1. 初始(NEW):新创建了一个线程对象,但还没有调用 start()方法。

  2. 运行(RUNNABLE):Java 线程中将就绪(ready)和运行中(running)两种 状态笼统的称为“运行”。 线程对象创建后,其他线程(比如 main 线程)调用了该对象的 start()方法。 该状态的线程位于可运行线程池中,等待被线程调度选中,获取 CPU 的使用权, 此时处于就绪状态(ready)。就绪状态的线程在获得 CPU 时间片后变为运行中 状态(running)。

  3. 阻塞(BLOCKED):表示线程阻塞于锁。

  4. 等待(WAITING):进入该状态的线程需要等待其他线程做出一些特定动作 (通知或中断)。

  5. 超时等待(TIMED_WAITING):该状态不同于 WAITING,它可以在指定的时 间后自行返回。

  6. 终止(TERMINATED):表示该线程已经执行完毕。

在这里插入图片描述

相关方法

​ yield()方法:使当前线程让出 CPU 占有权,但让出的时间是不可设定的。也不会释放锁资源。同时执行 yield()的线程有可能在进入到就绪状态后会被操作系

统再次选中马上又被执行。

​ join():把指定的线程加入到当前线程,可以将两个交替执行的线程合并为顺序执行。比如在线程 B 中调用了线程 A 的 Join()方法,直到线程 A 执行完毕后,才会继续执行线程 B 剩下的代码。

​ notify(): 通知一个在对象上等待的线程,使其从wait方法返回,而返回的前提是该线程 获取到了对象的锁,没有获得锁的线程重新进入 WAITING 状态。

​ notifyAll():通知所有等待在该对象上的线程。

wait():调用该方法的线程进入 WAITING 状态,只有等待另外线程的通知或被中断才会返回.需要注意,调用wait()方法后,会释放对象的锁。

​ wait(long):超时等待一段时间,这里的参数时间是毫秒,也就是等待长达n毫秒,如果没有 通知就超时返回 wait (long,int)对于超时时间更细粒度的控制,可以达到纳秒。

线程的优先级

​ 在 Java 线程中,通过一个整型成员变量 priority 来控制优先级,优先级的范 围从 1~10,在线程构建的时候可以通过 setPriority(int)方法来修改优先级,默认 优先级是 5,优先级高的线程分配时间片的数量要多于优先级低的线程。

​ 设置线程优先级时,针对频繁阻塞(休眠或者 I/O 操作)的线程需要设置较高优先级,而偏重计算(需要较多 CPU 时间或者偏运算)的线程则设置较低的优先级,确保处理器不会被独占。在不同的 JVM以及操作系统上,线程规划会存在差异,有些操作系统甚至会忽略对线程优先级的设定。

线程的调度

​ 线程调度是指系统为线程分配 CPU 使用权的过程,主要调度方式有两种:

​ 协同式线程调度(Cooperative Threads-Scheduling)

​ 线程的执行时间由线程本身控制,执行完成会通知操作系统切换到另一线程,没有同步问题,但是当一个线程除了问题,程序也会因此一直阻塞。

​ 抢占式线程调度(Preemptive Threads-Scheduling)

​ 线程的执行时间和切换都由系统控制。

线程和协程

​ 线程的实现方式主要有以下三种:

  • ​ 内核线程实现(1:1)

    ​ 直接由操作系统内核支持的线程,由内核完成线程切换,每个线程都是一个独立的调度单元,即使某一个线程阻塞了,不会影响整个进程的工作。但是由于使用内核线程调度,各种线程操作,如创建、 析构及同步,都需要进行系统调用。而系统调用的代价相对较高, 需要在用户态(User Mode)和内核态(Kernel Mode)中来回切换。其次,每个语言层面的 线程都需要有一个内核线程的支持,因此要消耗一定的内核资源(如内核线程的栈空间),因此一个系统支持的线程数量是有限的。

  • ​ 用户线程实现(1:N)

    ​ 完全建立在用户空间的线程库上,系统内核感知不到线程的存在和实现,所有线程操作都在用户态完成,操作快速且消耗低,能支持规模更大的线程数目。但由于没有内核的帮助,大量的操作都需要用户程序自行处理,很大问题解决起来也比较困难甚至无法实现,导致程序非常复杂,一般不倾向于使用。

  • ​ 用户线程加轻量级进程混合实现(N:M)

    ​ 即存在用户线程也存在内核线程,用户线程的创建、切换等操作仍在用户线程,操作快速消耗低,又可以使用内核线程的调度功能。

Java线程实现

​ 目前来说Java采用的是基于操作系统原生线程模式,即1:1,因此Java线程的优先级都由操作系统控制,显得不是特别靠谱。

守护线程

​ Daemon(守护)线程是一种支持型线程,因为它主要被用作程序中后台调 度以及支持性工作。这意味着,当一个 Java 虚拟机中不存在非 Daemon 线程的 时候,Java 虚拟机将会退出。可以通过调用 Thread.setDaemon(true)将线程设置 为 Daemon 线程。我们一般用不上,比如垃圾回收线程就是 Daemon 线程。 +Daemon 线程被用作完成支持性工作,但是在 Java 虚拟机退出时 Daemon 线程中的 finally 块并不一定会执行。在构建 Daemon 线程时,不能依靠 finally 块中的内容来确保执行关闭或清理资源的逻辑。

synchronized 内置锁

在这里插入图片描述

​ 多个线程同时访问一块共享资源,会导致不可预料的结果。关键字 synchronized 可以修饰方法或者以同步块的形式来进行使用,它主要确保多个线程在同一个时刻,只能有一个线程处于方法或者同步块中,它保证了线程对变量访问的可见性和排他性,使多个线程访问同一个变量的结果正确,它又称为内置锁机制。

对象锁和类锁

​ 对象锁是用于对象实例方法,或者一个对象实例上的,类锁是用于类的静态 方法或者一个类的 class 对象上的。

/*用在同步块上*/
public void incCountBlock(){
    synchronized (this){
        count++;
    }
}

/*用在同步块上,但是锁的是单独的对象实例*/
public void incCountObj(){
    synchronized (obj){
        count++;
    }
}

volatile关键字

​ volatile 保证了不同线程对这个变量进行操作时的可见性,即一个线程修改了某个变量的值,这新值对其他线程来说是立即可见的。但是 volatile 不能保证数据在多个线程下同时写时的线程安全,volatile 最适用的 场景:一个线程写,多个线程读。

等待和通知

等待方遵循如下原则。

1)获取对象的锁。

2)如果条件不满足,那么调用对象的wait()方法,被通知后仍要检查条件。

3)条件满足则执行对应的逻辑。

sychroizied(obj) {
	while(!条件){
		obj.wait();
	}
	//业务逻辑
}

通知方遵循如下原则。

1)获得对象的锁。

2)改变条件。

3)通知所有等待在对象上的线程。

sychroizied(obj) {
	//改变条件
    obj.notifyAll();
}
notify和notifyAll

​ 尽可能用 notifyall(),谨慎使用 notify(),因为 notify()只会唤醒一个线程,我 们无法确保被唤醒的这个线程一定就是我们需要唤醒的线程。

方法和锁
调用 yield() 、sleep()、wait()、notify()等方法对锁有何影响? 

​ yield() 、sleep()被调用后,都不会释放当前线程所持有的锁。 调用wait()方法后,会释放当前线程持有的锁,而且当前被唤醒后,会重新 去竞争锁,锁竞争到后才会执行wait 方法后面的代码。 调用 notify()系列方法后,对锁无影响,线程只有在 syn 同步代码执行完后才会自然而然的释放锁,所以 notify()系列方法一般都是 sync 同步代码的最后一行。

wait和notify

为什么wait 和 notify 方法要在同步块中调用?

原因
主要是因为 Java API 强制要求这样做,如果你不这么做,代码会抛出IllegalMonitorStateException 异常。其实真实原因是: 这个问题并不是说只在 Java 语言中会出现,而是会在所有的多线程环境下出现。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值