并发编程之线程基础---01

1.线程状态

  线程一共有 6 种状态(NEW、RUNNABLE、BLOCKED、WAITING、TIME_WAITING、TERMINATED)

  • NEW:初始状态,线程被构建,但是还没有调用 start 方法
  • RUNNABLED:运行状态,JAVA 线程把操作系统中的就绪和运行两种状态统一 称为“运行中”
  • BLOCKED:阻塞状态,表示线程进入等待状态,也就是线程因为某种原因放弃 了 CPU 使用权,阻塞也分为几种情况:

   Ø等待阻塞:运行的线程执行 wait 方法,jvm 会把当前线程放入到等待队列
  Ø同步阻塞:运行的线程在获取对象的同步锁时,若该同步锁被其他线程锁占 用了,那么 jvm 会把当前的线程放入到锁池中
  Ø其他阻塞:运行的线程执行 Thread.sleep 或者 t.join 方法,或者发出了 I/O 请求时,JVM
会把当前线程设置为阻塞状态,当 sleep 结束、join 线程终止、 io 处理完毕则线程恢复

  • WAITING :等待状态
  • TIME_WAITING:超时等待状态,超时以后自动返回
  • TERMINATED:终止状态,表示当前线程执行完毕

如图:

在这里插入图片描述

2.通过命令显示线程状态

  • jps: 打开终端或者命令提示符,键入“jps”,(JDK1.5 提供的一个显示当前所有 java
    进程 pid 的命令),可以获得相应进程的 pid
  • jstack pid : 根据上一步骤获得的 pid,继续输入 jstack pid(jstack 是 java 虚拟机自带的一种堆栈跟踪工具。jstack 用于打印出给定的 java 进程 ID 或 core file 或远程
    调试服务的 Java 堆栈信息)

3.线程启动和停止

3.1 启动

通过start方法实现

3.2 停止

1)使用 interrupt() 方法
  当其他线程通过调用当前线程的 interrupt 方法,表示向当前线程打个招呼,告诉他可以中断线程的执行了,至于什么时候中断,取决于当前线程自己。线程通过检查资深是否被中断来进行相应,可以通过 isInterrupted()来判断是否被中断。另外使用interrupt会使阻塞的线程抛出异常,注意看源码注释

线程状态复位
a: Thread.interrupted()
  线程中还提供了静态方法 Thread.interrupted()对设置中断标识的线程复位。比如一个的线程调用 thread.interrupt 来设置中断标识,而另外一个线程,又通过Thread.interrupted 把线程的标识又进行了复位
b:抛出 InterruptedException 异常
  还有一种被动复位的场景,就是对抛出 InterruptedException 异常的方法,在InterruptedException 抛出之前,JVM 会先把线程的中断标识位清除,然后才会抛出 InterruptedException,这个时候如果调用 isInterrupted 方法,将会返回 false。

2)定义一个 volatile 修饰的boolean成员变量

public class VolatileDemo {
	
	private volatile static boolean stop=false;
	
	public static void main(String[] args) throws InterruptedException {
		Thread thread=new Thread(()->{
			int i=0;
			while(!stop){
				i++;
			}
		});
	
	thread.start();
	System.out.println("begin start thread");
	Thread.sleep(1000);
	stop=true;
	}
}

4.线程安全问题

  线程安全问题可以总结为: 可见性、原子性、有序性

4.1 计算机底层线程原理

  线程是 CPU 调度的最小单元,线程涉及的目的最终仍然是更充分的利用计算机处理的效能。如:
在这里插入图片描述
  但是绝大部分的运算任务不能只依靠处理器“计算”就能完成,处理器还需要与内存交互,比如读取运算数据、存储运算结果,这个 I/O 操作是很难消除的。而由于计算机的存储设备与处理器的运算速度差距非常大,所以现代计算机系统都会增加一层读写速度尽可能接近处理器运算速度的高速缓存来作为内存和处理器之间的缓冲:将运算需要使用的数据复制到缓存中,让运算能快速进行,当运算结束后再从缓存同步到内存之中。如图:
在这里插入图片描述

  高速缓存从下到上越接近 CPU 速度越快,同时容量也越小。现在大部分的处理器都有二级或者三级缓存,从下到上依次为 L3 cache, L2 cache, L1 cache. 缓存又可以分为指令缓存和数据缓存,指令缓存用来缓存程序的代码,数据缓存用来缓存程序的数据

  • L1 Cache,一级缓存,本地 core 的缓存,分成 32K 的数据缓存 L1d 和 32k 指 令缓存 L1i,访问 L1 需要3cycles,耗时大约 1ns;
  • L2 Cache,二级缓存,本地 core 的缓存,被设计为 L1 缓存与共享的 L3 缓存之间的缓冲,大小为 256K,访问 L2 需要 12cycles,耗时大约 3ns;
  • L3 Cache,三级缓存,在同插槽的所有 core共享 L3 缓存,分为多个 2M 的 段,访问 L3 需要 38cycles,耗时大约 12ns;
4.2 计算机底层缓存一致性问题

  CPU-0 读取主存的数据,缓存到 CPU-0 的高速缓存中,CPU-1 也做了同样的事情,而 CPU-1 把 count 的值修改成了 2,并且同步到 CPU-1 的高速缓存,但是这个修改以后的值并没有写入到主存中,CPU-0 访问该字节,由于缓存没有更新,所以仍然是之前的值,就会导致数据不一致的问题

  引发这个问题的原因是因为多核心 CPU 情况下存在指令并行执行,而各个CPU 核心之间的数据不共享从而导致缓存一致性问题,为了解决这个问题,CPU 生产厂商提供了相应的解决方案看图:
在这里插入图片描述

1)总线锁
  当一个 CPU 对其缓存中的数据进行操作的时候,往总线中发送一个 Lock 信号。其他处理器的请求将会被阻塞,那么该处理器可以独占共享内存。总线锁相当于把 CPU 和内存之间的通信锁住了,所以这种方式会导致 CPU 的性能下降,所以 P6 系列以后的处理器,出现了另外一种方式,就是缓存锁。

2)缓存锁
  如果缓存在处理器缓存行中的内存区域在 LOCK 操作期间被锁定,当它执行锁操作回写内存时,处理不在总线上声明 LOCK 信号,而是修改内部的缓存地址,然后通过缓存一致性机制来保证操作的原子性,因为缓存一致性机制会阻止同时修改被两个以上处理器缓存的内存区域的数据,当其他处理器回写已经被锁定的缓存行的数据时会导致该缓存行无效。

所以如果声明了 CPU 的锁机制,会生成一个 LOCK 指令,会产生两个作用:

  1. Lock 前缀指令会引起引起处理器缓存回写到内存,在 P6 以后的处理器中,LOCK 信号一般不锁总线,而是锁缓存
  2. 一个处理器的缓存回写到内存会导致其他处理器的缓存无效

缓存一致性协议
  处理器上有一套完整的协议,来保证 Cache 的一致性,比较经典的应该就是MESI 协议了,它的方法是在 CPU 缓存中保存一个标记位,这个标记为有四种状态:

  • Ø M(Modified) 修改缓存,当前 CPU 缓存已经被修改,表示已经和内存中的数据不一致了
  • Ø I(Invalid) 失效缓存,说明 CPU 的缓存已经不能使用了
  • Ø E(Exclusive) 独占缓存,当前 cpu 的缓存和内存中数据保持一致,而且其他处理器没有缓存该数据
  • Ø S(Shared) 共享缓存,数据和内存中数据一致,并且该数据存在多个 cpu缓存中

每个 Core 的 Cache 控制器不仅知道自己的读写操作,也监听其它 Cache 的读
写操作,嗅探(snooping)"协议

CPU 的读取会遵循几个原则:

  1. 如果缓存的状态是 I,那么就从内存中读取,否则直接从缓存读取
  2. 如果缓存处于 M 或者 E 的 CPU 嗅探到其他 CPU 有读的操作,就把自己的缓
    存写入到内存,并把自己的状态设置为 S
  3. 只有缓存状态是 M 或 E 的时候,CPU 才可以修改缓存中的数据,修改后,缓
    存状态变为 MC

CPU的优化执行
  除了增加高速缓存以为,为了更充分利用处理器内内部的运算单元,处理器可能会对输入的代码进行乱序执行优化,处理器会在计算之后将乱序执行的结果充足,保证该结果与顺序执行的结果一直,但并不保证程序中各个语句计算的先后顺序与输入代码中的顺序一致,这个是处理器的优化执行;还有一个就是编程语言的编译器也会有类似的优化,比如做指令重排来提升性能。

4.3 Java中的线程安全问题

  比如缓存一致性就导致可见性问题、处理器的乱序执行会导致原子性问题、指令重排会导致有序性问题。为了解决这些问题,所以在 JVM 中引入了 JMM 的概念。

4.3.1内存模型JMM

  内存模型定义了共享内存系统中多线程程序读写操作行为的规范,来屏蔽各种硬件和操作系统的内存访问差异,来实现 Java 程序在各个平台下都能达到一致的内存访问效果。

  Java 内存模型的主要目标是定义程序中各个变量的访问规则,也就是在虚拟机中将变量存储到内存以及从内存中取出变量(这里的变量,指的是共享变量,也就是实例对象、静态字段、数组对象等存储在堆内存中的变量。而对于局部变量这类的,属于线程私有,不会被共享)这类的底层细节。通过这些规则来规范对内存的读写操作,从而保证指令执行的正确性。它与处理器有关、与缓存有关、与并发有关、与编译器也有关。他解决了 CPU多级缓存、处理器优化、指令重排等导致的内存访问问题,保证了并发场景下的可见性、原子性和有序性。

内存模型解决并发问题主要采用两种方式:限制处理器优化和使用内存屏障

  Java 内存模型定义了线程和内存的交互方式,在 JMM 抽象模型中,分为主内存、工作内存。主内存是所有线程共享的,工作内存是每个线程独有的。线程对变量的所有操作(读取、赋值)都必须在工作内存中进行,不能直接读写主内存中的变量。并且不同的线程之间无法访问对方工作内存中的变量,线程间的变量值的传递都需要通过主内存来完成,他们三者的交互关系如下
在这里插入图片描述

总结:
  所以,总的来说,JMM 是一种规范,目的是解决由于多线程通过共享内存进行通信时,存在的本地内存数据不一致、编译器会对代码指令重排序、处理器会对代码乱序执行等带来的问题。目的是保证并发编程场景中的原子性、可见性和有序性。

猜你喜欢

转载自blog.csdn.net/weixin_40792878/article/details/86374225
今日推荐