一文读懂JAVA多线程

简介:

一文读懂JAVA多线程

背景渊源

摩尔定律

提到多线程好多书上都会提到摩尔定律,它是由英特尔创始人之一Gordon Moore提出来的。其内容为:当价格不变时,集成电路上可容纳的元器件的数目,约每隔18-24个月便会增加一倍,性能也将提升一倍。换言之,每一美元所能买到的电脑性能,将每隔18-24个月翻一倍以上。这一定律揭示了信息技术进步的速度。

可是从2003年开始CPU主频已经不再翻倍,而是采用多核,而不是更快的主频。摩尔定律失效。那主频不再提高,核数增加的情况下要想让程序更快就要用到并行或并发编程。

并行与并发

如果CPU主频增加程序不用做任何改动就能变快。但核多的话程序不做改动不一定会变快。

CPU厂商生产更多的核的CPU是可以的,一百多核也是没有问题的,但是软件还没有准备好,不能更好的利用,所以没有生产太多核的CPU。随着多核时代的来临,软件开发越来越关注并行编程的领域。但要写一个真正并行的程序并不容易。

并行和并发的目标都是最大化CPU的使用率,并发可以认为是一种程序的逻辑结构的设计模式。可以用并发的设计方式去设计模型,然后运行在一个单核的系统上。可以将这种模型不加修改的运行在多核系统上,实现真正的并行,并行是程序执行的一种属性真正的同时执行,其重点的是充分利用CPU的多个核心。

多线程开发的时候会有一些问题,比如安全性问题,一致性问题等,重排序问题,因为这些问题然后大家在写代码的时候会加锁等等。这些基础概念大家都懂,本文不再描述。本文主要分享造成这些问题的原因和JAVA解决这些问题的底层逻辑。

多线程

计算机存储体系

要想明白数据一致性问题,要先缕下计算机存储结构,从本地磁盘到主存到CPU缓存,也就是从硬盘到内存,到CPU。一般对应的程序的操作就是从数据库查数据到内存然后到CPU进行计算。这个描述有点粗,下边画个图。

业内画这个图一般都是画的金字塔型状,为了证明是我自己画的我画个长方型的(其实我不会画金字塔)。

CPU多个核心和内存之间为了保证内部数据一致性还有一个缓存一致性协议(MESI),MESI其实就是指令状态中的首字母。M(Modified)修改,E(Exclusive)独享、互斥,S(Shared)共享,I(Invalid)无效。然后再看下边这个图。

太细的状态流转就不作描述了,扯这么多主要是为了说明白为什么会有数据一致性问题,就是因为有这么多级的缓存,CPU的运行并不是直接操作内存而是先把内存里边的数据读到缓存,而内存的读和写操作的时候就会造成不一致的问题。解决一致性问题怎么办呢,两个思路。

  1. 锁住总线,操作时锁住总线,这样效率非常低,所以考虑第二个思路。
  2. 缓存一致性,每操作一次通知(一致性协议MESI),(但多线程的时候还是会有问题,后文讲)

JAVA内存模型

上边稍微扯了一下存储体系是为了在这里写一下JAVA内存模型。

Java虚拟机规范中试图定义一种Java内存模型(java Memory Model) 来屏蔽掉各种硬件和操作系统的内存访问差异,以实现让Java程序在各种平台下都能达到一致的内存访问效果。

内存模型是内存和线程之间的交互、规则。与编译器有关,有并发有关,与处理器有关。

Java内存模型的主要目标是定义程序中各个变量的访问规则,即在虚拟机中将变量存储到内存和从内存中取出变量这样的底层细节。此处的变量与Java编程中所说的变量有所区别,它包括 了实例字段、静态字段和构成数组对象的元素,但不包括局部变量与方法参数,因为后者是线程私有的,不会被共享,自然就不会存在竞争问题。为了获得较好的执行效能,Java内存模型并没有限制执行引擎使用处理器特定寄存器或缓存来和主内存进行交互,也没有限制即时编译器进行调整代码执行顺序这类优化措施。

Java内存模型规定了所有的变量都存储在主内存中。每条线程还有自己的工作内存,线程的工作内存中保存了该线程使用到的变量的主内存副本拷贝,线程对变量的所有操作(读取,赋值等 )都必需在工作内存中进行,而不能直接读写主内存中的变量。不同的线程之间也无法直接访问对方工作内存中的变量,线程间变量值的传递均需要通过主内存来完成。

这里所说的主内存、工作内存和Java内存区域中的Java堆、栈、方法区等并不是同一个层次的内存划分,这两者基本上是没有关系的。 如果两者一定要勉强对应起来,那从变量、主内存、工作内存的定义来看,主内存对应Java堆中的对象实例数据部分 ,而工作内存则对应于虚拟机栈中的部分区域。从更底层次上说,主内存就是直接对应于物理硬件的内存,而为了获取更好的运行速度,虚拟机可能会让工作内存优先存储于寄存器和高速缓存中,因为程序运行时主要访问读写的是工作内存。

前边说的都是和内存有关的内容,其实多线程有关系的还有指令重排序,指令重排序也会造成在多线程访问下结束和想的不一样的情况。大段的介绍就不写了要不篇幅太长了(JVM那里书里边有)。主要就是在CPU执行指令的时候会进行执行顺序的优化。画个图看一下吧。

具体理论后文再写先来点干货,直接上代码,一看就明白。

public class HappendBeforeTest {
    int a = 0;
    int b = 0;
    public static void main(String[] args) {
        HappendBeforeTest test = new HappendBeforeTest();
        Thread threada = new Thread() {
            @Override
            public void run() {
                test.a = 1;
                System.out.println("b=" + test.b);
            }
        };
        Thread threadb = new Thread() {
            @Override
            public void run() {
                test.b = 1;
                System.out.println("a=" + test.a);
            }
        };
        threada.start();
        threadb.start();
    }
}

猜猜有可能输出什么?多选

A:a=0,b=1
B:a=1,b=0
C:a=0,b=0
D:a=1,b=1

上边这段代码不太好调,然后我稍微改造了一下。

public class HappendBeforeTest {
    static int a = 0;
    static int b = 0;
    static int x = 0;
    static int y = 0;
    public static void shortWait(long interval) {
        long start = System.nanoTime();
        long end;
        do {
            end = System.nanoTime();
        }
        while (start + interval >= end);
    }
    public static void main(String[] args) throws InterruptedException {
        for (; ; ) {
            Thread threada = new Thread() {
                @Override
                public void run() {
                    a = 1;
                    x = b;
                }
            };
            Thread threadb = new Thread() {
                @Override
                public void run() {
                    b = 1;
                    y = a;
                }
            };
            Thread starta = new Thread() {
                @Override
                public void run() {
                    // 由于线程threada先启动
                    //下面这句话让它等一等线程startb
                    shortWait(100);
                    threada.start();
                }
            };
            Thread startb = new Thread() {
                @Override
                public void run() {
                    threadb.start();
                }
            };
            starta.start();
            startb.start();
            starta.join();
            startb.join();
            threada.join();
            threadb.join();
            a = 0;
            b = 0;
            System.out.print("x=" + x);
            System.out.print("y=" + y);
            if (x == 0 && y == 0) {
                break;
            }
            x = 0;
            y = 0;
            System.out.println();
        }
    }
}

这段代码,a和b初始值为0,然后两个线程同时启动分别设置a=1,x=b和b=1,y=a。这个代码里边的starta和startb线程完全是为了让threada 和threadb 两个线程尽量同时启动而加的,里边只是分别调用了threada 和threadb 两个线程。然后无限循环只要x和y 不同时等于0就初始化所有值继续循环,直到x和y都是0的时候break。你猜猜会不会break。

结果看截图

因为我没有记录循环次数,不知道循环了几次,然后触发了条件break了。从代码上看,在输出A之前必然会把B设置成1,在输出B之前必然会把A设置为1。那为什么会出现同时是零的情况呢。这就很有可能是指令被重排序了。

指令重排序简单了说是就两行以上不相干的代码在执行的时候有可能先执行的不是第一条。也就是执行顺序会被优化。

如何判断你写的代码执行顺序会不会被优化,要看代码之间有没有Happens-before关系。Happens-before就是不无需任何干涉就可以保证有有序执行,由于篇幅限制Happens-before就不在这里多做介绍。

下面简单介绍一下java里边的一个关键字volatilevolatile简单来说就是来解决重排序问题的。对一个volatile变量的写,一定happen-before后续对它的读。也就是你在写代码的时候不希望你的代码被重排序就使用volatile关键字。volatile还解决了内存可见性问题,在执行执行的时候一共有8条指令lock(锁定)、read(读取)、load(载入)、use(使用)、assign(赋值)、store(存储)、write(写入)、unlock(解锁)(篇幅限制具体指令内容自行查询,看下图大概有个了解)。

volatile主要是对其中4条指令做了处理。如下图

也就是把 load和use关联执行,把assign和store关联执行。众所周知有load必需有read现在load又和use关联也就是要在缓存中要use的时候就必须要load要load就必需要read。通俗讲就是要use(使用)一个变量的时候必需load(载入),要载入的时候必需从主内存read(读取)这样就解决了读的可见性。下面看写操作它是把assign和store做了关联,也就是在assign(赋值)后必需store(存储)。store(存储)后write(写入)。也就是做到了给一个变量赋值的时候一串关联指令直接把变量值写到主内存。就这样通过用的时候直接从主内存取,在赋值到直接写回主内存做到了内存可见性。

无锁编程

我在网上看到大部分写多线程的时候都会写到锁,AQS和线程池。由于网文太多本文就不多做介绍。下面简单写一写CAS。

CAS是一个比较魔性的操作,用的好可以让你的代码更优雅更高效。它就是无锁编程的核心。

CAS书上是这么介绍的:“CAS即Compare and Swap,是JDK提供的非阻塞原子性操作,它通过硬件保证了比较-更新的原子性”。他是非阻塞的还是原子性,也就是说这玩意效率更高。还是通过硬件保证的说明这玩意更可靠。

从上图可以看出,在cas指令修改变量值的时候,先要进行值的判断,如果值和原来的值相等说明还没有被其它线程改过,则执行修改,如果被改过了,则不修改。在java里边java.util.concurrent.atomic包下边的类都使用了CAS操作。最常用的方法就是compareAndSet。其底层是调用的Unsafe类的compareAndSwap方法。

作者:高玉珑

相关文章
|
9天前
|
安全 Java 测试技术
Java并行流陷阱:为什么指定线程池可能是个坏主意
本文探讨了Java并行流的使用陷阱,尤其是指定线程池的问题。文章分析了并行流的设计思想,指出了指定线程池的弊端,并提供了使用CompletableFuture等替代方案。同时,介绍了Parallel Collector库在处理阻塞任务时的优势和特点。
|
5天前
|
安全 Java 开发者
深入解读JAVA多线程:wait()、notify()、notifyAll()的奥秘
在Java多线程编程中,`wait()`、`notify()`和`notifyAll()`方法是实现线程间通信和同步的关键机制。这些方法定义在`java.lang.Object`类中,每个Java对象都可以作为线程间通信的媒介。本文将详细解析这三个方法的使用方法和最佳实践,帮助开发者更高效地进行多线程编程。 示例代码展示了如何在同步方法中使用这些方法,确保线程安全和高效的通信。
25 9
|
8天前
|
存储 安全 Java
Java多线程编程的艺术:从基础到实践####
本文深入探讨了Java多线程编程的核心概念、应用场景及其实现方式,旨在帮助开发者理解并掌握多线程编程的基本技能。文章首先概述了多线程的重要性和常见挑战,随后详细介绍了Java中创建和管理线程的两种主要方式:继承Thread类与实现Runnable接口。通过实例代码,本文展示了如何正确启动、运行及同步线程,以及如何处理线程间的通信与协作问题。最后,文章总结了多线程编程的最佳实践,为读者在实际项目中应用多线程技术提供了宝贵的参考。 ####
|
5天前
|
监控 安全 Java
Java中的多线程编程:从入门到实践####
本文将深入浅出地探讨Java多线程编程的核心概念、应用场景及实践技巧。不同于传统的摘要形式,本文将以一个简短的代码示例作为开篇,直接展示多线程的魅力,随后再详细解析其背后的原理与实现方式,旨在帮助读者快速理解并掌握Java多线程编程的基本技能。 ```java // 简单的多线程示例:创建两个线程,分别打印不同的消息 public class SimpleMultithreading { public static void main(String[] args) { Thread thread1 = new Thread(() -> System.out.prin
|
8天前
|
Java
JAVA多线程通信:为何wait()与notify()如此重要?
在Java多线程编程中,`wait()` 和 `notify()/notifyAll()` 方法是实现线程间通信的核心机制。它们通过基于锁的方式,使线程在条件不满足时进入休眠状态,并在条件满足时被唤醒,从而确保数据一致性和同步。相比其他通信方式,如忙等待,这些方法更高效灵活。 示例代码展示了如何在生产者-消费者模型中使用这些方法实现线程间的协调和同步。
22 3
|
7天前
|
安全 Java
Java多线程集合类
本文介绍了Java中线程安全的问题及解决方案。通过示例代码展示了使用`CopyOnWriteArrayList`、`CopyOnWriteArraySet`和`ConcurrentHashMap`来解决多线程环境下集合操作的线程安全问题。这些类通过不同的机制确保了线程安全,提高了并发性能。
|
8天前
|
Java
java小知识—进程和线程
进程 进程是程序的一次执行过程,是系统运行的基本单位,因此进程是动态的。系统运行一个程序即是一个进程从创建,运行到消亡的过程。简单来说,一个进程就是一个执行中的程序,它在计算机中一个指令接着一个指令地执行着,同时,每个进程还占有某些系统资源如CPU时间,内存空间,文件,文件,输入输出设备的使用权等等。换句话说,当程序在执行时,将会被操作系统载入内存中。 线程 线程,与进程相似,但线程是一个比进程更小的执行单位。一个进程在其执行的过程中产生多个线程。与进程不同的是同类的多个线程共享同一块内存空间和一组系统资源,所以系统在产生一个线程,或是在各个线程之间做切换工作时,负担要比
20 1
|
8天前
|
Java UED
Java中的多线程编程基础与实践
【10月更文挑战第35天】在Java的世界中,多线程是提升应用性能和响应性的利器。本文将深入浅出地介绍如何在Java中创建和管理线程,以及如何利用同步机制确保数据一致性。我们将从简单的“Hello, World!”线程示例出发,逐步探索线程池的高效使用,并讨论常见的多线程问题。无论你是Java新手还是希望深化理解,这篇文章都将为你打开多线程的大门。
|
9天前
|
安全 Java 编译器
Java多线程编程的陷阱与最佳实践####
【10月更文挑战第29天】 本文深入探讨了Java多线程编程中的常见陷阱,如竞态条件、死锁、内存一致性错误等,并通过实例分析揭示了这些陷阱的成因。同时,文章也分享了一系列最佳实践,包括使用volatile关键字、原子类、线程安全集合以及并发框架(如java.util.concurrent包下的工具类),帮助开发者有效避免多线程编程中的问题,提升应用的稳定性和性能。 ####
36 1
|
13天前
|
存储 设计模式 分布式计算
Java中的多线程编程:并发与并行的深度解析####
在当今软件开发领域,多线程编程已成为提升应用性能、响应速度及资源利用率的关键手段之一。本文将深入探讨Java平台上的多线程机制,从基础概念到高级应用,全面解析并发与并行编程的核心理念、实现方式及其在实际项目中的应用策略。不同于常规摘要的简洁概述,本文旨在通过详尽的技术剖析,为读者构建一个系统化的多线程知识框架,辅以生动实例,让抽象概念具体化,复杂问题简单化。 ####