美文网首页Java
[Java]重学Java-学习多线程需要的一些基础

[Java]重学Java-学习多线程需要的一些基础

作者: AbstractCulture | 来源:发表于2022-04-27 17:37 被阅读0次

    什么是并发

    并发是指一个处理器核心同时接收到了多个请求;
    打个比方,煎饼果子的阿姨每次只能做一个煎饼果子,但是同时有多个人前来买煎饼。

    什么是并行

    通常出现在多核处理器上,多个处理器核心处理多个事件;
    还是以煎饼果子为例,如果有两个阿姨可以同时做煎饼果子,那么就可以并行地做"煎饼"这个任务.

    什么是线程

    操作系统将程序划分成多个任务去执行,每个任务由一个执行线程来驱动,这个执行线程其实上是进程上(我们每个应用就是一个进程)单一顺序的控制流,最后操作系统从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-学习多线程需要的一些基础

        本文链接:https://www.haomeiwen.com/subject/bxslyrtx.html