[Java]重学Java-学习多线程需要的一些基础
什么是并发
并发是指一个处理器核心同时接收到了多个请求;
打个比方,煎饼果子的阿姨每次只能做一个煎饼果子,但是同时有多个人前来买煎饼。
什么是并行
通常出现在多核处理器上,多个处理器核心处理多个事件;
还是以煎饼果子为例,如果有两个阿姨可以同时做煎饼果子,那么就可以并行地做"煎饼"这个任务.
什么是线程
操作系统将程序划分成多个任务去执行,每个任务由一个执行线程来驱动,这个执行线程其实上是进程上(我们每个应用就是一个进程)单一顺序的控制流,最后操作系统从CPU上分配时间片到线程中执行任务。
线程类Thread的运行时数据区域
以下引用《Java编程思想》中的总结:
- 程序计数器,指明要执行的下一个 JVM 字节码指令。
- 用于支持 Java 代码执行的栈,包含有关此线程已到达当时执行位置所调用方法的信息。它也包含每个正在执行的方法的所有局部变量(包括原语和堆对象的引用)。每个线程的栈通常在 64K 到 1M 之间 [^1] 。
- 第二个则用于 native code(本机方法代码)执行的栈
- thread-local variables (线程本地变量)的存储区域
- 用于控制线程的状态管理变量
资源共享
线程在栈中存储自己独有的数据,这部分数据是不共享的;
但是,在堆中分配的数据,是进程内共享的。而多线程访问堆中的数据时,通常会引发线程安全问题,这些都是由于资源被共享了,而数据到达了每个线程的工作内存中进行了独立计算,在不加任何保护措施的情况下,对同一个数据进行了操作。
下面演示一段线程不安全的代码:
package com.tea.modules.java8.thread.synchronize;
import com.tea.modules.java8.thread.annotation.ThreadSafe;
import com.tea.modules.java8.thread.annotation.ThreadUnSafe;
import lombok.extern.slf4j.Slf4j;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Semaphore;
/**
* @author jaymin
* @since 2022/2/19 15:56
*/
@Slf4j
public class CountWithSynchronizedTest {
/**
* 请求总数
*/
public static final int clientTotal = 5000;
/**
* 同时并发线程数
*/
public static final int threadTotal = 200;
public static int count = 0;
public static void main(String[] args) throws InterruptedException {
ExecutorService executorService = Executors.newCachedThreadPool();
final Semaphore semaphore = new Semaphore(threadTotal);
final CountDownLatch countDownLatch = new CountDownLatch(clientTotal);
for (int i = 0; i < clientTotal; i++) {
executorService.execute(() -> {
try {
semaphore.acquire();
add();
semaphore.release();
} catch (InterruptedException e) {
log.error("并发错误:", e);
}
countDownLatch.countDown();
});
}
countDownLatch.await();
log.info("count:{}",count);
executorService.shutdown();
}
private static void add() {
count++;
}
}
稍微解释一下这段代码,我申请了一个线程池,然后一个信号量用来控制同一时刻只能并发200个线程,然后定义了一个计数器从0数开始自增到5000,每次执行count++;
那么这里我们期望得到的是5000这个结果值。
- Result
count:4945
这里多个线程同时对count进行了自增,结果并没返回期望的5000;这就需要解决线程安全问题,什么是线程安全,最简单的理解就是,你期望的结果就是程序所输出的结果。
CPU多级缓存和Java内存模型
多级缓存由于CPU访问寄存器的速度,远大于访问主存的速度,所以在寄存器和主存中,还存在着高速缓存,它是为了解决这两种媒介直接速度的差异而存在的。
如果CPU需要访问主存的数据,会先加载到CPU高速缓存中,也可以将数据加载到寄存器中,进行运算。
如果CPU需要往主存写入数据,也是先刷新到高速缓存中,再在一个时间点将数据刷新到主存。
- JMM
JMM(Java Memory Model)
定义了JVM与操作系统是如何协同工作的,同时,它也规定了在多线程环境中,什么时候当前线程的操作对其他线程可见,何时对共享变量进行同步访问。
堆负责大部分对象的内存分配(就是说有部分数据是存在于栈里面的),由于Java是运行时分配内存以及运行时编译,所以存取速度相对没那么快。
栈相对于堆来说,它更快,但是它分配的空间必须是确定性的,所以通常存放一些基本的数据类型,对象引用、变量、调用栈等。
在多线程的环境下,栈是线程独占的,不共享;而堆上的数据是共享的,因此多线程的问题主要是出现在这种共享变量的访问中。
线程不安全的原因
多个线程访问共享资源,但是又不知道谁被分配到时间片执行(CPU还会中断,所以每行代码都可能会停顿)。
假如A、B线程从主内存访问到的count值为10,然后读到自己的工作内存中,做count++,都得到11,然后同时回写到主内存中。这个时候,做了2次count++,只得到11的结果。
缓存和局部性原理
也许你会好奇,为什么要有cpu cache这种东西,这是因为CPU的频率太快了,快到连主存都跟不上,这样在处理器时间周期内,CPU常常需要等待主存,浪费了资源。所以Cache的出现,是为了缓解CPU和内存之间速度的不匹配问题。
CPU>>Cache>>Memory
时间局部性: 如果某个数据被访问,那么在不久的将来它可能再次被访问。
空间局部性: 如果某个数据被访问,那么与它相邻的数据很快也可能被访问。
如何保证线程安全性
原子性
提供了互斥访问,同一时刻只能有一个线程对它进行操作。
可见性
一个线程对主内存的修改可以及时的被其他线程观察到
有序性
一个线程观察其他线程中的指令执行顺序,由于指令重排序的存在,该观察结果一般杂乱无序
死锁
死锁产生的必要条件:
- 互斥条件:系统要求对所分配的资源进行排他性控制,即在一段时间内某个资源仅为一个进程所占有(比如:打印机,同一时间只能一个人打印)。此时若有其他进程请求该资源,则请求只能等待,直到有资源释放了位置;
- 请求和保持条件:进程已经持有了一个资源,但是又要访问一个新的被其他进程占用的资源那么就会阻塞,并且对自己占用的一个资源保持不放;
- 不剥夺条件:进程对已经获取的资源未使用完之前不能被剥夺,只能使用完之后自己释放。
- 环路等待条件:存在一种进程资源的循环等待链,链中每一个进程已获得的资源同时被链中下一个进程所请求。
笔者暂时只想到这么多,后面有新的我会补充