课程目标

1. 多线程的发展历史

2. 线程的应用

3. 并发编程的基础

4. 线程安全的问题

特定的指令,计算机不会存储指令,把指令写下来,一次性读取指令,批处理。

然后我们需要把批处理进行隔离、保存它的进度。

进程 —> 线程

单核CPU 只有可能会有一个进程去执行。

什么情况下应该使用多线程

线程出现的目的是什么?解决进程中多任务的实时性的问题?其实简单来说,就是解决“阻塞”的问题。阻塞的意思就是程序运行到某个函数或过程后等待某些事件发生而暂时停止 CPU 占用的情况,也就是说会使得 CPU 闲置。还有一些场景就是比如对于一个函数中的运算逻辑的性能问题,我们可以 通过多线程的技术,使得一个函数中的多个逻辑运算通过多线程技术达到一个并行执行,从而提高性能。

CPU 架构图解:

所以,多线程最终解决的就是“等待”的问题,所以简单总结的使用场景

  • 通过并行计算提高程序执行性能
  • 需要等待网络、I/O响应导致耗费大量的执行时间,可以采用异步线程的方式来减少阻塞

Tomcat 7 以前的 I/O 模型

多线程的应用场景

  • 客户端阻塞 如果客户端只有一个线程,这个线程发起读取文件的操作必须等待 IO 流返回,线程(客户端)才能做其他的事
  • 线程级别阻塞(BIO) : 客户端只有一个线程情况下,会导致整个客户端阻塞。那么我们可以使用多线程,一部分线程在等待 IO 操作返回的同时其他线程可以继续做其他的事。此时从客户端角度来说,客户端没有闲着。
tomcat 模型:

多个客户端都是阻塞的,我只有处理完一个请求才能接收下一个请求。然后客户端就会阻塞。所以 Tomcat 采用了多线程的技术。利用了多线程的技术实现了非阻塞。

如何应用多线程

在 JAVA 中有多个方式来实现多线程。继承 Thread 类、实现 Runable 接口、使用 ExecutorService 、Callable、Future 实现带返回结果的多线程。

  • Thread
  • Runable
  • Callable / Future 可以实现带返回值的线程
继承 Thread 类创建线程

​ Thread 类本质上是实现了 Runable 接口的一个实例,代表一个线程的实例。启动线程的唯一方法就是通过 Thread 类的 start() 方法。 start() 方法是一个 native 方法。它会启动一个新线程,并执行 run() 方法。这种实现多线程很简单,通过自己的类直接 extends Thread , 并重写 run() 方法,就可以启动新线程并执行自己定义的 run() 方法。

public class MyThread extends Thread{
public static void main(String[] args) {
new MyThread().start();
new MyThread().start();
}
@Override
public void run() {
System.out.println("MyThrea run().....");
}
}
实现 Runable 接口创建线程

​ 如果自己的类已经继承了另一个类,就无法直接继承 Thread,此时,可以实现 Runable 接口。

public class RunableDemo implements Runnable {
public static void main(String[] args) {
new Thread(new RunableDemo()).start();
new Thread(new RunableDemo()).start();
}
@Override
public void run() {
System.out.println("[" + Thread.currentThread().getName() + "]" + "runable My run().....");
}
}
[Thread-0]runable My run().....
[Thread-1]runable My run().....
错误的写法:
public class RunableDemo implements Runnable {
// public static void main(String[] args) {
// new Thread(new RunableDemo()).start();
// new Thread(new RunableDemo()).start();
// } public static void main(String[] args) {
new RunableDemo().run();
new RunableDemo().run();
}
@Override
public void run() {
System.out.println("[" + Thread.currentThread().getName() + "]" + "runable My run().....");
}
}
[main]runable My run().....
[main]runable My run().....
实现 Callable 接口通过 FutureTask 包装器来创建 Thread 线程

​ 有的时候,我们可能需要让异步执行的线程在执行完以后,提供一个返回值到当前的主线程,主线程需要这个值进行后续的逻辑处理,那么这个时候,就需要带返回值的线程了。

/***
* 当你想要异步的线程执行你的某一个逻辑,那么在这个运行结束以后
* 我想要拿到子线程运行的结果
*/
public class CallableDemo implements Callable<String> {
public static void main(String[] args) throws Exception {
ExecutorService executorService = Executors.newSingleThreadExecutor(); CallableDemo callableDemo = new CallableDemo(); Future<String> future = executorService.submit(callableDemo);
/***
* 这里可以写其他的业务
* 去写其他东西
*/
String returnValue = future.get(); // 这个地方在阻塞
System.out.println(returnValue);
executorService.shutdown();
} @Override
public String call() throws Exception {
return "darain" + 1;
}
}

如何把多线程用得优雅

合理地利用异步操作,可以大大地提升程序的处理性能,下面这个案例,如何看过 zookeeper 源码的同学应该看到过。

通过阻塞队列以及多线程的方式,实现对请求的异步化处理,提升处理性能。

模仿多个线程处理同一个请求
@Data
public class Request {
private String name;
}
public interface RequestProcessor {
void processorRequest(Request requset);
}
@RequiredArgsConstructor
public class PrintProcessor extends Thread implements RequestProcessor {
LinkedBlockingQueue<Request> linkedBlockingQueue = new LinkedBlockingQueue<>();
private final RequestProcessor nextProcess; @Override
public void processorRequest(Request requset) {
linkedBlockingQueue.add(requset);
} @Override
public void run() {
while (true) {
try {
Request requset = linkedBlockingQueue.take();
out.println("[" + Thread.currentThread().getName() + "] " + "print Data:" + requset);
nextProcess.processorRequest(requset);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
@RequiredArgsConstructor
public class SaveProcessor extends Thread implements RequestProcessor { LinkedBlockingQueue<Request> linkedBlockingQueue = new LinkedBlockingQueue<>(); @Override
public void processorRequest(Request requset) {
linkedBlockingQueue.add(requset);
} @Override
public void run() {
while (true) {
try {
Request requset = linkedBlockingQueue.take();
System.out.println("[" + Thread.currentThread().getName() + "] " + "save data:" + requset);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
/***
* 我们去处理的时候,用异步线程去处理。
* 当我们把一个请求丢过来的时候,不是直接去处理,而是通过异步线程去处理。
* zookeeper 就是类似的处理,一方面,你可以通过你的处理把职责划分开。
* 一方面你可以通过异步线程的处理去提升你程序的性能
* 合理地利用你 CPU 的资源
*
* 这个和 zookeeper 里边非常像
*/
public class Demo {
private final PrintProcessor printProcessor; public Demo() {
SaveProcessor saveProcessor = new SaveProcessor();
saveProcessor.start();
printProcessor = new PrintProcessor(saveProcessor);
printProcessor.start();
} public static void main(String[] args) {
Request requset = new Request();
requset.setName("darian");
new Demo().doTest(requset);
} public void doTest(Request request) {
printProcessor.processorRequest(request);
}
}

就像一个链表一样地,上一个对象的引用指向下一个对象。是不会乱序的。

线程的基础知识

​ 线程作为操作系统调度的最小单元,并且能够让多线程同时执行,极大地提高了程序的性能,在多核的环境下的优势更加明显。但是在多线程的使用过程中如果对它的特性和原理不够了解的话,就容易造成各种问题。

线程的状态(六种)

JAVA 线程既然能够创建,那么也会销毁,所以线程是存在生命周期的。那么我们接下来从线程的生命周期开始去了解线程。

线程一共六种状态

(NEW、RUNNABLE、BLOCKED、WAITING、TIME_WAITING、TERMINATED)

NEW

​ 初始状态,线程被构建,但是还没有调用 #start 方法

RUNNABLE

​ 运行状态,JAVA 线程把操作系统中的就绪和运行两种状态统一称为 "运行中"

BLOCKED

阻塞,表示线程进如等待状态,也就是线程因为某种原因放弃了 CPU 的使用权,阻塞也分为几种情况。

  • 等待阻塞 运行的线程调用了 #wait 方法,JVM 会把当前线程放到等待队列

  • 同步阻塞 synchronized ,运行的线程在获取对象的同步锁时,若该同步锁被其他线程占用了,那么 JVM 会把当前的线程放入到锁池中。

  • 其他阻塞 sleep / join 运行的线程执行 Thread.sleep() 或者 t.join 方法,或者发出了 I/O 请求时, JVM 会把当前线程设置为阻塞状态,当 sleep 结束、join 线程终止、io 处理完毕则线程恢复。

    WAITING

等待 (waiting) 是我们的线程调用了一个 #wait 方法,实际上也会变成一个阻塞。就是我们没有办法继续去运行线程了。

TIME_WAITING

​ 超时等待状态,超时以后自动返回

TERMINATED

​ 终止状态,表示当前线程执行完毕

线程运行状态图:

线程的运行状态有两种状态,

不存在就绪的状态,只是为了描述它的一个状态。

打开 Thread 类,搜索 state 有哪些状态,它写得很清楚。

当运行中的线程的时间片被 CPU 抢占的时候,那么它又会变成一个就绪状态。

线程执行完就是终止。

synchroninzed 就是让这个线程获得锁。获得锁,就意味着,其他线程在调用这个方法的时候,它会阻塞。当我们获得锁的时候。比如说我们现在有两个线程。第一个 T1 线程访问同步代码块。同步代码块里面,首先它会获得一个锁。当 T2 线程进来以后,它是没有办法获得锁的。

线程状态:

public class ThreadStatusDemo {
public static void main(String[] args) {
new Thread(() -> {
while (true) {
try {
TimeUnit.SECONDS.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}, "timewaiting").start(); new Thread(() -> {
while (true) { // 我们在一个循环里边获得一个锁
synchronized (ThreadStatusDemo.class) {
try {
// 然后调用 wait() 方法,是因为它调用 wait 方法之前必须要获得锁
ThreadStatusDemo.class.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}, "waiting").start(); new Thread(new blockDemo(), "blockDemo-0").start();
new Thread(new blockDemo(), "blockDemo-1").start();
} static class blockDemo extends Thread {
@Override
public void run() {
synchronized (blockDemo.class) {
while (true) {
try { // 100 秒,一直让它阻塞
TimeUnit.SECONDS.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
}
通过相应的命令显示线程状态:
  • 打开终端或者命令提示符,键入 JPS ,(JDK 1.5 提供的要给显示当前所有的 JAVA 进程 PID 的命令),可以获得相应进程的 PID
  • 根据上一步骤获得的 PID,继续输入 jstack + pid (jstack 时 JAVA 虚拟机自带的一种堆栈跟踪工具。jstack 会打印出给定的 JAVA 进程 ID 或 core file 或远程调试服务的 java 堆栈信息)

我们在写线程的时候,最好定义一个名称。我们去查看问题的时候,有利于我们去排查问题。

阻塞状态,blockedsynchronized 加锁的情况下,两个线程同时去访问一个方法,这个时候,就会存在 阻塞。

JPS 是 JDK 1.5 以后,显示所有 JAVA 进程的命令。

jstack 30112 可以查看线程的状态。

  • blockDemo-0 获得锁,变成了一个 TIMED_WAITING 的状态。 #sleep
  • blockDemo-1 没有拿到锁 (on object monitor)
  • TIMED_WAITING #sleep 方法
"DestroyJavaVM" #18 prio=5 os_prio=0 tid=0x0000000002a02800 nid=0x697c waiting on condition [0x0000000000000000]
java.lang.Thread.State: RUNNABLE "blockDemo-1" #17 prio=5 os_prio=0 tid=0x0000000029066800 nid=0x5f74 waiting for monitor entry [0x0000000029bff000]
java.lang.Thread.State: BLOCKED (on object monitor) "blockDemo-0" #15 prio=5 os_prio=0 tid=0x0000000029065800 nid=0x36f8 waiting on condition [0x0000000029aff000]
java.lang.Thread.State: TIMED_WAITING (sleeping) "waiting" #13 prio=5 os_prio=0 tid=0x0000000029061000 nid=0x7bd0 in Object.wait() [0x00000000299fe000]
java.lang.Thread.State: WAITING (on object monitor) "timewaiting" #12 prio=5 os_prio=0 tid=0x0000000029094800 nid=0x6bd4 waiting on condition [0x00000000298fe000]
java.lang.Thread.State: TIMED_WAITING (sleeping) "Service Thread" #11 daemon prio=9 os_prio=0 tid=0x0000000027540800 nid=0x8310 runnable [0x0000000000000000]
java.lang.Thread.State: RUNNABLE "C1 CompilerThread3" #10 daemon prio=9 os_prio=2 tid=0x000000002747e000 nid=0x5344 waiting on condition [0x0000000000000000]
java.lang.Thread.State: RUNNABLE "C2 CompilerThread2" #9 daemon prio=9 os_prio=2 tid=0x000000002747d000 nid=0x24c0 waiting on condition [0x0000000000000000]
java.lang.Thread.State: RUNNABLE "C2 CompilerThread1" #8 daemon prio=9 os_prio=2 tid=0x0000000027475800 nid=0x7c30 waiting on condition [0x0000000000000000]
java.lang.Thread.State: RUNNABLE "C2 CompilerThread0" #7 daemon prio=9 os_prio=2 tid=0x0000000027474800 nid=0x5c78 waiting on condition [0x0000000000000000]
java.lang.Thread.State: RUNNABLE "Monitor Ctrl-Break" #6 daemon prio=5 os_prio=0 tid=0x000000002745c800 nid=0xde0 runnable [0x00000000289fe000]
java.lang.Thread.State: RUNNABLE "Attach Listener" #5 daemon prio=5 os_prio=2 tid=0x00000000273ba000 nid=0x3434 waiting on condition [0x0000000000000000]
java.lang.Thread.State: RUNNABLE "Signal Dispatcher" #4 daemon prio=9 os_prio=2 tid=0x0000000027411800 nid=0x839c runnable [0x0000000000000000]
java.lang.Thread.State: RUNNABLE "Finalizer" #3 daemon prio=8 os_prio=1 tid=0x00000000273a3800 nid=0x79bc in Object.wait() [0x00000000286fe000]
java.lang.Thread.State: WAITING (on object monitor) "Reference Handler" #2 daemon prio=10 os_prio=2 tid=0x0000000002afa000 nid=0x4e7c in Object.wait() [0x00000000285ff000]
java.lang.Thread.State: WAITING (on object monitor)

我们很多时候,要多发现线程的信息。

线程的启动和终止

你怎么去启动一个线程?终止?

#start native 方法,告诉 JVM 去启动一个线程。然后调用 #run 方法去执行。

#stop 方法是不建议使用的。 @Deprecated !!它就像我们在 Linux 系统中,kill 命令一样,就是我不知道我当前这个线程是不是还在运行,有没有还没处理完的。没有处理完的话,我强制关闭,就会出现一些数据问题,和一些不可预测地问题出现。 #susped#resume

怎么样优雅的关闭?我们关闭 Tomcat 也好,关闭一些进程也好,我们都会提供一些优雅的方式去关闭。一些指令去执行,一般的中间件都会做一个操作,一般都会先去阻止后续的请求进来,然后等待正在运行的线程执行完以后优雅地停止掉。

#interrupt 优雅中断的方式。

​ 当其他线程通过调用当前线程的 #interrupt 方法,表示向当前线程打个招呼,告诉他可以中断线程的执行了,至于什么时候中断,取决于当前线程自己。线程通过检查自身是或否被中断来进行响应,可以通过 isIntrrupted() 来判断是否被中断。

实现线程终止的逻辑:

public class InterruptDemo {
private static int i;
public static void main(String[] args) throws InterruptedException {
Thread thread = new Thread(() -> {
// 我去判断是否中断这个线程
while (!Thread.currentThread().isInterrupted()) {
i++;
}
System.out.println(i);
}, "interruptDemo");
thread.start();
TimeUnit.SECONDS.sleep(1);
// 通过线程的 interrupt 设置标识为 true
System.out.println(thread.isInterrupted());
thread.interrupt();
System.out.println(thread.isInterrupted());
}
}

这种通过表示为或者中断操作的方式能够使线程在终止时有机会去清理资源,而不是武断地将线程停止。因此更加安全和优雅。

Thread.interrupted

通过 interrupt,设置了一个标识告诉线程可以终止运行了。线程中还提供了静态方法 Thread.interrupted() 对设置中断标识的线程复位。比如在线程,外边的线程调用 thread.interrupt 来设置中断标识,而在线程里边,又通过 Thread.interrupted 把线程的标识进行了复位。

public static void interrupt1() throws InterruptedException {
Thread thread = new Thread(() -> {
while (true) {
boolean interrupted = Thread.currentThread().isInterrupted();
if (interrupted) {
System.out.println("before:" + interrupted);
Thread.interrupted(); // 对线程进行复位,中断标识为 false
System.out.println("after:" + Thread.currentThread().isInterrupted());
}
}
});
thread.start();
TimeUnit.SECONDS.sleep(1);
thread.interrupt(); // 设置中断标识为 true
}
before:true
after:false

其他的线程复位

​ 除了通过 Thread.interrupted 方法对线程中断标识进行复位以外,还有一种被动复位的场景,就是对抛出 interruptedException 异常的方法,在 interruptedException 抛出之前, JVM 会先把线程的中断标识位清除,然后会抛出 InterruptedException 这个时候,如果调用 #isInterrupted 方法,将会返回 false。

public static void interrupt2() throws InterruptedException {
Thread thread = new Thread(() -> {
while (true) {
try {
Thread.sleep(10000);
} catch (InterruptedException e) {
// 抛出该异常,会将复位表示设置为 false
e.printStackTrace();
}
}
});
thread.start();
thread.interrupt(); // 将复位表示设置为 true
TimeUnit.SECONDS.sleep(1);
System.out.println("before:" + thread.isInterrupted());
TimeUnit.SECONDS.sleep(1);
System.out.println("after:" + thread.isInterrupted());
}

通过指令的方式,volatile boolean isStop = false; 这样的一个方式,也是可以的。通过内存的可见。

interrupt 和我们设置标志变量的方式是一样的。

java.lang.Thread#interrupt

  • java.lang.Thread#interrupt0 native 方法

thread.cpp

bool Thread::is_interrupted(Thread* thread, bool clear_interrupted) {
debug_only(check_for_dangling_thread_pointer(thread);)
// Note: If clear_interrupted==false, this simply fetches and
// returns the value of the field osthread()->interrupted().
return os::is_interrupted(thread, clear_interrupted);
}

os_linux.cpp

void os::interrupt(Thread* thread) {
assert(Thread::current() == thread || Threads_lock->owned_by_self(), "possibility of dangling Thread pointer");
OSThread* osthread = thread->osthread();
if (!osthread->interrupted()) {
osthread->set_interrupted(true);
// More than one thread can get here with the same value of osthread,
// resulting in multiple notifications. We do, however, want the store
// to interrupted() to be visible to other threads before we execute unpark().
OrderAccess::fence();
ParkEvent * const slp = thread->_SleepEvent ;
if (slp != NULL) slp->unpark() ;
}
// For JSR166. Unpark even if interrupt status already was set
if (thread->is_Java_thread())
((JavaThread*)thread)->parker()->unpark();
ParkEvent * ev = thread->_ParkEvent ;
if (ev != NULL) ev->unpark() ;
}

内存屏障 fence() ,让标志位改变,让所有线程看见,和 volatile 一个意思。

unpark() 线程。

其实就是通过 unpark 去唤醒

Thread#interrupted 是一个静态方法,对设置的中断标识的线程进行复位。

线程的停止方法之 2

​ 除了通过 #interrupt 标识去中断线程以外,我们可以通过 :

​ 定义一个 volatile 修饰的成员变量,来控制线程的终止。这实际上是应用了 volatile 实现多线程之间的共享变量可见性这一特点来实现的。

public class ThreadStopDemo3 {
// 这种和 interrupted 方式是一样的。
private static volatile boolean stop = true; 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;
}
}

线程的安全性

  • 可见性
  • 原子性
  • 有序性

认识这三个问题。

可见性

/***
* 可见性问题
*/
public class VisableDemo {
// 加上 volatile 之后,才可以停止。
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();
TimeUnit.SECONDS.sleep(1);
stop = true;
}
}

原子性

/***
*
*/
public class AutomicDemo {
private static int count = 0; public static void main(String[] args) throws InterruptedException {
for (int i = 0; i < 1000; i++) {
new Thread(AutomicDemo::inc).start();
}
Thread.sleep(4000);
System.out.println("y运行结果:" + count);
// y运行结果:952 (<= 1000)
} public static void inc() {
try {
Thread.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
count++;
}
}

有序性

没有办法演示。

有序性是指,程序执行的顺序和我们代码编写的顺序是不一致的。它会存在编译器的优化,和指令的优化。就是 CPU 执行过程中的一个指令重排的问题。

JAVA 内存模型中允许编译器和处理器去指令重排序来优化我们的执行。提升我们 CPU 的利用率。

它有一个原则:

在不影响我们代码语义的情况下会进行适当的重排序。

CPU 的高速缓存

JMM 的高速缓存

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

​ 运行时会先去内存中去加载,如果找不到就会去主内存中去加载。

CPU 高速缓存图片:

寄存器:【Rages】

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

  • L1 Cache

    ​ 一级缓存,本地 core 的缓存,分为 32K 的数据缓存 L1d 和 32K 指令缓存 L1i ,访问 L1 需要 3cycles, 耗时大约 1ns.

  • L2 Cache

    ​ 二级缓存,本地 core 的缓存,被设计为 L1 缓存与共享 L3 缓存之间的缓冲,大小为 256K,访问 L2 需要 12 cycles,耗时 3ns

  • L3 Cache

    ​ 三级缓存,在同插槽的所有 core 共享 L3 缓存,分为多个 2M 的端,访问 L3 需要 38 cycles, 耗时大约 12ns

L3 缓存主要是为了解决 CPU 操作的一个延时的问题。

如果缓存中拿不到,就会去主内存中去加载。

缓存一致性问题

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

​ 每个CPU 可以运行一个线程,那么多个 CPU 可以并行运行多个线程。多个线程同时去读取一个共享变量的时候,就会把这个数据都加载到它的高速缓存中来。每个 CPU 的高速缓存池对于其他 CPU 来说是不可见的。

​ 引发这个问题的原因是,多核心 CPU 存在指令并行执行,而各个 CPU 核心之间的数据不共享从而导致缓存一致性问题,为了解决这个问题,CPU 生产厂商提供了相应的解决方案。

CPU 层面提供了两种锁

  • 总线锁
  • 缓存锁

总线锁

​ 锁总线,当我们其中一个 CPU 在执行一个线程,去访问一个数据的时候,会往总线上发起一个 LOCK 信号,那么其他的CPU 再次去请求这个相同的数据进行操作的时候,它就会被阻塞,意味着这个总线锁就是一个排他锁。对于整个 CPU 来说,它是一个排他的,对于 多个 CPU 来说,它会导致性能问题。多核的目的是做负载,提升运行效率。单核的提升达到瓶颈。加了一个总线锁,就又让它串行执行。所以 P6 系列以后的处理器,出现了另外一种方式,就是缓存锁。

缓存锁

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

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

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

缓存一致性协议

处理器上有一套完整的协议,来保证 Cache 的一致性,比较经典的就是 MESI 协议。

一般都是 MESI 的协议。它的方法是在 CPU 中保存了一个标记位,这个标记位有四种状态。

  • M(modify)

    ​ 修改缓存,当前 CPU 缓存已经被修改,表示已经和内存中的数据不一致了

  • I(invalid)

    ​ 失效缓存,说明 CPU 的缓存已经不能使用了

  • E(exclusive)

    ​ 独占缓存,当前的 CPU 的缓存和内存中的数据保持一致,而且其他处理器没有缓存该数据

  • S(shared)

    ​ 共享缓存,数据和内存中数据一致,并且该数据存在多个 CPU 缓存中。

      每个 Core 的 Cache 控制器不仅知道自己的读写操作,也监听其他的 Cache 的读写操作,嗅探(snooping)协议。
CPU 的读取会遵循几个原则:
  1. 如果缓存的状态是 I ,那么就从内存中读取,否则直接从缓存中读取
  2. 如果缓存处于 M 或者 E 的 CPU 嗅探到其他 CPU 有读的操作,就把自己的缓存写入到内存,并把自己的状态设置为 S
  3. 只有缓存状态是 ME 的时候,CPU 才可以修改缓存中的数据,修改后,缓存状态变为 MC。

(怎么去通知缓存失效不需要去关心的)

CPU 的优化执行

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

​ 它会保证一个约束:保证我的语义不会变化。(你可以重排序,你可以对指令乱序执行,但是语义不能发生变化。)CPU 访问主内存的时候,可能是一个交叉访问的。对于同一个 CPU 访问内存是可控的。但是对于多处理器来说,我的访问顺序是可变的,不确定的。乱序执行,有些指令执行的时间比较长,CPU 会比较占用时间。CPU 会进行优化的执行,通过我们编译器的优化,还有乱序访问。CPU 访问主内存的顺序,对多个 CPU 来说是不可控的。

并发编程的问题

​ 原子性,可见性,有序性都是抽象的概念。他们的核心本质就是“缓存一致性问题”和“处理器优化的指令重排序问题”。

  • 可见性问题?
  • 乱序执行(内存乱序访问?)

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

JMM (应用层面) 内存模型

​ 内存模型定义了共享内存系统中多线程程序读写操作行为的规范,来屏蔽各种硬件和操作系统的内存访问差异,来实现 JAVA 程序在各个平台下都能达成一致的内存访问效果。JAVA 内存模型的主要目标是定义程序中各个变量的访问规则,也就是在虚拟机中将变量存储到内存以及从内存中取出变量(这里的变量指的是共享变量,也就是实例对象、静态字段、数组对象等存储在堆内存中的变量。而对于局部变量这类的,属于线程私有,不会被共享)这类的底层细节。通过这些规则来规范对内存的读写操作,从而保证指令执行的正确性。

​ 它与处理器有关、与缓存有关、与并发有关、与编译器有关。他解决了 CPU 多级缓存、处理器优化、指令重拍等导致的内存访问问题,保证了并发场景下的可见性、原子性和有序性。内存模型解决并发问题主要采取两种方式:限制处理器优化和使用内存屏障。

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

JMM 交互图

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

原文链接:https://javaguide.net

来源于: https://javaguide.net

微信公众号:不止极客

百万架构师第四十五课:并发编程的基础|JavaGuide的更多相关文章

  1. NeHe OpenGL教程 第四十五课:顶点缓存

    转自[翻译]NeHe OpenGL 教程 前言 声明,此 NeHe OpenGL教程系列文章由51博客yarin翻译(2010-08-19),本博客为转载并稍加整理与修改.对NeHe的OpenGL管线 ...

  2. 第四十五课:MVC,MVP,MVVM的区别

    前端架构从MVC到MVP,再到MVVM,它们都有不同的应用场景.但MVVM已经被证实为界面开发最好的方案了. MVP 是从经典的模式MVC演变而来,它们的基本思想有相通的地方:Controller/P ...

  3. 潭州课堂25班:Ph201805201 django 项目 第四十五课 mysql集群和负载均衡(课堂笔记)

    2.使用docker安装Haproxy 一.为什么要使用数据库集群和负载均衡? 1.高可用 2.高并发 3.高性能 二.mysql数据库集群方式 三.使用docker安装PXC 1.拉取PXC镜像 d ...

  4. python第四十五课——继承性之多继承

    测试模块 演示多继承的结构和使用: 子类:Child 直接父类(多个):Father.Mother 注意: 由于有多个直接父类,多个父类都要自己给其属性赋值, 避免混淆,我们使用类名.__init__ ...

  5. python第四十五课——继承性之多重继承

    演示多重继承的结构和使用 子类:Dog 直接父类:Animal 间接父类:Creature #生物类 class Creature: def __init__(self,age): print('我是 ...

  6. JAVA学习第四十五课 — 其它对象API(一)System、Runtime、Math类

    一.System类 1. static long currentTimeMillis() 返回以毫秒为单位的当前时间. 实际上:当前时间与协调世界时 1970 年 1 月 1 日午夜之间的时间差(以毫 ...

  7. Java从零开始学四十五(Socket编程基础)

    一.网络编程中两个主要的问题 一个是如何准确的定位网络上一台或多台主机,另一个就是找到主机后如何可靠高效的进行数据传输. 在TCP/IP协议中IP层主要负责网络主机的定位,数据传输的路由,由IP地址可 ...

  8. Python学习笔记(四十五)网络编程(1)TCP编程

    摘抄:https://www.liaoxuefeng.com/wiki/0014316089557264a6b348958f449949df42a6d3a2e542c000/0014320043745 ...

  9. Python之路(第三十五篇) 并发编程:操作系统的发展史、操作系统的作用

    一.操作系统发展史 第一阶段:手工操作 —— 真空管和穿孔卡片 ​ 第一代之前人类是想用机械取代人力,第一代计算机的产生是计算机由机械时代进入电子时代的标志,从Babbage失败之后一直到第二次世界大 ...

  10. 转:【Java并发编程】之十五:并发编程中实现内存可见的两种方法比较:加锁和volatile变量

    转载请注明出处:http://blog.csdn.net/ns_code/article/details/17290021 在http://blog.csdn.net/ns_code/article/ ...

随机推荐

  1. 在docker中使用主机串口通讯

    在进行软件docker化的过程时,很大的一个阻碍就是软件与各种外围硬件设备的交互,网口通信的设备能够很容易地接入容器,但是串口设备则要复杂一些.本文讨论在windows和linux下docker容器使 ...

  2. KTL 用C++14写公式的K线工具 - 0.9.3版

    K,K线,Candle蜡烛图. T,技术分析,工具平台 L,公式Language语言使用c++14,Lite小巧简易. 项目仓库:https://github.com/bbqz007/KTL 国内仓库 ...

  3. PostgreSQL 的历史

    title: PostgreSQL 的历史 date: 2024/12/23 updated: 2024/12/23 author: cmdragon excerpt: PostgreSQL 是一款功 ...

  4. 零基础入门:基于开源WebRTC,从0到1实现实时音视频聊天功能

    本文由微医云技术团队前端工程师张宇航分享,原题"从0到1打造一个 WebRTC 应用",有修订和改动. 1.引言 去年初,突如其来的新冠肺炎疫情让线下就医渠道几乎被切断,在此背景下 ...

  5. dotnet最小webApi开发实践

    dotnet最小webApi开发实践 软件开发过程中,经常需要写一些功能验证代码.通常是创建一个console程序来验证测试,但黑呼呼的方脑袋界面,实在是不讨人喜欢. Web开发目前已是网络世界中的主 ...

  6. C Primer Plus 第6版 第八章 编程练习参考答案

    编译环境VS Code+WSL GCC 源码请到文末下载 . 我给第一题写了Linux shell脚本,感兴趣的同学可以尝试修改并运行一下. /*第1题************************ ...

  7. Appium_WebDriverAgent设置

            在使用真机调试的时候犯了一个错误,我把WebDriverAgent 下载到本地的A目录下,然后进行build安装,这样在模拟器上执行是无法发现问题的,但是使用appium 在真机上执行 ...

  8. WPF 加载外部字体

    例如将字体放入d:/Fonts 文件夹.然后就可以通过类似 btn.FontFamily = new FontFamily("file:///d:/Fonts/#Ashley"); ...

  9. Hutool 实现非对称加密(RSA)

    目录 思路 生成RAS密钥 消息公钥加密.私钥解密 代码Demo 生成 A 的密钥 生成 B 的密钥 A 发送消息给 B B 解密 A 消息 对称加密中,我们只需要一个密钥,通信双方同时持有.而非对称 ...

  10. 一文搞懂企业架构与DDD融合

    大家好,我是汤师爷~ 今天聊聊企业架构与DDD如何进行融合. 企业架构TOGAF 什么是企业架构TOGAF? TOGAF(The Open Group Architecture Framework)是 ...