[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与操作系统是如何协同工作的,同时,它也规定了在多线程环境中,什么时候当前线程的操作对其他线程可见,何时对共享变量进行同步访问。

JMM

堆负责大部分对象的内存分配(就是说有部分数据是存在于栈里面的),由于Java是运行时分配内存以及运行时编译,所以存取速度相对没那么快。
栈相对于堆来说,它更快,但是它分配的空间必须是确定性的,所以通常存放一些基本的数据类型,对象引用、变量、调用栈等。
在多线程的环境下,栈是线程独占的,不共享;而堆上的数据是共享的,因此多线程的问题主要是出现在这种共享变量的访问中。

线程不安全的原因

多个线程访问共享资源,但是又不知道谁被分配到时间片执行(CPU还会中断,所以每行代码都可能会停顿)。
假如A、B线程从主内存访问到的count值为10,然后读到自己的工作内存中,做count++,都得到11,然后同时回写到主内存中。这个时候,做了2次count++,只得到11的结果。

缓存和局部性原理

也许你会好奇,为什么要有cpu cache这种东西,这是因为CPU的频率太快了,快到连主存都跟不上,这样在处理器时间周期内,CPU常常需要等待主存,浪费了资源。所以Cache的出现,是为了缓解CPU和内存之间速度的不匹配问题。
CPU>>Cache>>Memory

时间局部性: 如果某个数据被访问,那么在不久的将来它可能再次被访问。
空间局部性: 如果某个数据被访问,那么与它相邻的数据很快也可能被访问。

如何保证线程安全性

原子性

提供了互斥访问,同一时刻只能有一个线程对它进行操作。

可见性

一个线程对主内存的修改可以及时的被其他线程观察到

有序性

一个线程观察其他线程中的指令执行顺序,由于指令重排序的存在,该观察结果一般杂乱无序

死锁

死锁产生的必要条件:

  • 互斥条件:系统要求对所分配的资源进行排他性控制,即在一段时间内某个资源仅为一个进程所占有(比如:打印机,同一时间只能一个人打印)。此时若有其他进程请求该资源,则请求只能等待,直到有资源释放了位置;
  • 请求和保持条件:进程已经持有了一个资源,但是又要访问一个新的被其他进程占用的资源那么就会阻塞,并且对自己占用的一个资源保持不放;
  • 不剥夺条件:进程对已经获取的资源未使用完之前不能被剥夺,只能使用完之后自己释放。
  • 环路等待条件:存在一种进程资源的循环等待链,链中每一个进程已获得的资源同时被链中下一个进程所请求。

笔者暂时只想到这么多,后面有新的我会补充

你可能感兴趣的:([Java]重学Java-学习多线程需要的一些基础)