本系列文章主要围绕高并发这一话题展开,分享笔者在并发处理上的学习思路以及踩过的坑。具体思路大体分为三部分:
- Java多线程编程;
- 高并发的解决思路;
- 分布式架构中Redis、Zookeeper分布式锁的应用。
本文将重点讲解第一部分——Java多线程编程。
一、Java内存模型与线程
并发编程主要讨论以下几点:
- 多个线程操作相同资源
- 保证线程安全
- 合理使用资源
通常我们可以将物理计算机中出现的并发问题类比到JVM中的并发。
物理计算机处理器、高速缓存、主内存间交互关系如图:
处理器为提高性能,会对输入代码乱序执行(Out-Of-Order Execution) 优化。
类比Java内存模型,线程、主内存、工作内存交互关系如图:
JMM定义了程序中各个变量访问规则,即在虚拟机中将内存取出和存储的底层细节。
线程A如果要跟线程B要通信的话,必须经历以下两个步骤:
- 线程A把本地内存A中更新过的共享变量的值刷新到主内存中;
- 线程B去主内存中读取A更新过的共享变量的值。
线程的工作内存中保存了该线程使用到变量的主内存副本拷贝(也可理解为此线程的私有拷贝),线程对变量的操作(读取、赋值等)都在工作内存中进行,而不能直接读写主内存中变量。不同线程之间的通信也需要通过主内存来完成。主内存对应Java堆中对象实例数据部分,而工作内存则对应虚拟机栈中部分区域。
在此还有非常重要的点需要提及!
指令重排序
执行程序时,为提高性能,编译器和处理器常常会对指令做出重排序。分三种:
- 编译器优化的重排序;
- 指令并行重排序;
- 内存系统重排序。
内存之间的交互操作
JMM中定义了8种操作来描述工作内存与主内存之间的实现细节:
- lock(锁定):作用于主内存的变量,它把一个变量标识为一条线程独占状态;
- unlock(解锁):作用于主内存的变量,它把一个处于锁定状态的变量释放出来,释放后的变量才可以被其他线程锁定;
- read(读取):作用于主内存的变量,它把一个变量从主内存传输到线程工作内存中,以便后边的load操作;
- load(载入):作用于主内存的变量,它把read操作从主内存中得到的变量值放到工作内存副本中;
- use(使用):作用于工作内存的变量,它把工作内存中一个变量的值传递给执行引擎,每当虚拟机遇到一个需要使用到变量的值的字节码指令时,将会执行这个操作;
- assign(赋值):作用于工作内存的变量,它把从执行引擎接收到的值赋给工作内存,每当虚拟机遇到一个给变量赋值的字节码指令时执行此操作;
- store(存储):作用于工作内存的变量,它把工作内存的变量的值传送到主内存中,以便以后的write操作使用;
- write(写入):作用于主内存的变量,它把store操纵从工作内存中得到的变量值放入到主内存的变量中。
JMM规定了执行上述八种操作时必须满足的规则(与happens-before原则是等效的,即先行发生原则):
- 不允许read和load、store和write操作之一单独出现;
- 不允许一个线程丢弃它的最近assign的操作,即变量在工作内存中改变了之后必须同步到主内存中;
- 不允许一个线程无原因地(没有发生过任何assign操作)把数据从工作内存同步回主内存中;
- 一个新的变量只能在主内存中诞生,不允许在工作内存中直接使用一个未被初始化(load或assign)的变量。即就是对一个变量实施use和store操作之前,必须先执行过了assign和load操作;
- 一个变量在同一时刻只允许一条线程对其进行lock操作,lock和unlock必须成对出现;
- 如果对一个变量执行lock操作,将会清空工作内存中此变量的值,在执行引擎使用这个变量前需要重新执行load或assign操作初始化变量的值;
- 如果一个变量事先没有被lock操作锁定,则不允许对它执行unlock操作;也不允许去unlock一个被其他线程锁定的变量;
- 对一个变量执行unlock操作之前,必须先把此变量同步到主内存中(执行store和write操作)
测试工具
二、线程安全性
原子性:提供了互斥访问,同一时刻只能由一个线程来对它进行操作。
可见性:一个线程对主内存的修改可以及时被其他线程观察到。
有序性:一个线程观察其它线程中指令执行顺序,由于指令重排序的存在,观察的结果一般为杂乱无章的。Java程序的天然有序性可以总结为——如果本线程内观察,所有的操作都是有序的;如果在一个线程观察另一个线程,所有的操作都是无序的。前者指的是线程内的串行语义,后者指的是指令重排序和工作内存和主内存同步延迟现象。
1、原子性-Atomic包
AtomicXXX:
CAS、Unsafe.compareAndSwapInt
通过CAS来保证原子性,即Compare And Swap比较交换:
CAS虽然可以进行高效的进行原子操作,但是CAS仍在存在三大问题:
- ABA问题。在Java1.5开始,JDK的Atomic包里提供了一个类AtomicStampedReference来解决ABA问题。大部分情况下ABA问题并不影响程序并发的正确性,如果需要解决ABA问题,改用传统的互斥同步可能会比原子类更加高效;如果你对大数据开发感兴趣,想系统学习大数据的话,可以加入大数据技术学习交流扣扣君羊:522189307
- 循环时间长开销大;
- 只能保证一个共享变量进行的原子操作。
测试:
public class AtomicExample1 {
// 请求总数
public static int clientTotal = 5000;</