- 多线程系列笔记地址:https://blog.csdn.net/hancoder/category_10274700.html
1、什么是JUC:
- java.util.concurrent
- java.util.concurrent.atomic
- java.util.concurrent.locks
线程
线程的分类
用户空间划分:
- 用户空间
- 内核空间
str = "str123";//用户空间
x=x+2;
file.write(str); // 切换到内核空间
y=x+4;// 切换回用户空间
JVM没有调用CPU的权力,JVM依赖内核去调用CPU。ring0有最高级别的CPU操作权限,ring3有简单级别的操作权限。用户空间只有ring3,内核空间有ring0
用户线程+内核线程
线程的实现可以分为两类:
- 用户级线程
User-Level Thread
:用户进程自己创建,依据线程库,维护在应用程序内部的线程表里,线程依托于主进程执行,所有线程都执行在一条线上,一个线程阻塞了其他线程也会阻塞
- 内核级线程
Kernel-Level Thread
:由内核创建,由内核维护销毁调度,维护在内核线程表里,进程维护在内核进程表里。内核级线程是操作系统实现的。每个线程都可以争抢资源。一个线程阻塞了其他线程不会阻塞。内核线程就是内核的分身,一个分身可以处理一件特定事情。这在处理异步事件如异步IO时特别有用。内核线程的使用是廉价的,唯一使用的资源就是内核栈和上下文切换时保存寄存器的空间。支持多线程的内核叫做多线程内核(Multi-Threads kernel )。- 内核线程只运行在内核态,不受用户态上下文的拖累。
- 处理器竞争:可以在全系统范围内竞争处理器资源;
- 使用资源:唯一使用的资源是内核栈和上下文切换时保持寄存器的空间
- 调度:调度的开销可能和进程自身差不多昂贵
- 同步效率:资源的同步和数据共享比整个进程的数据同步和共享要低一些。
- 内核线程只运行在内核态,不受用户态上下文的拖累。
虽然每个进程都有他自己的虚拟地址空间,但是为了进一步保障运行安全,虚拟地址空间划分为用户空间和内核空间
操作系统运行在内核空间,用户程序运行在用户空间,内核空间由所有进程的地址空间共享,
![]()
但是用户程序不能直接访问内核空间,操作系统保存的进程控制信息自然是在内核空间,这里除了页目录以外还可以找到很多重要的内容,例如进程和父进程id、状态、打开文件句柄表等,
线程就是进程中的执行体,它要有指定的执行入口,通常是某个函数的指令入口,线程执行时要使用从进程虚拟地址空间中分配的栈空间来存储数据(线程栈)
在创建线程时,操作系统会在用户空间和内核空间分别分配两段栈,即用户栈和内核栈。线程切换到内核态执行时会使用内核栈,为的是不允许用户代码对其进行修改以保证安全。操作系统也会记录每个线程的控制信息TCB,例如执行入口,线程栈,线程id等等,在windows中线程控制信息对应TCB,在PCB中可以找到进程拥有的线程列表,同一个进程内的线程,会共享进程的地址空间和句柄表等资源;而在linux中只使用了一个task_struct的结构体,进程在创建子进程时会指定它和自己,使用同一套地址空间和句柄表等资源,用这种方法实现多线程效果。
![]()
如果接下来要执行进程A中的线程a1,执行入口在最下面,cpu的指令指针就会指向线程的执行入口,当前执行用户空间的程序指令,所以栈基ebp和栈指针寄存器esp会记录用户栈的位置,可以看到程序执行时cpu面向的是某个线程,所以才说线程是操作系统调度与指向的基本单位。
一个进程中至少要有一个线程,他要从这个线程开始执行,这被称为它的“主线程”,可以认为主线程是进程中的第一个线程,一般由父进程或操作系统创建的,而进程中的其他线程,一般都是由主线程创建的
![]()
系统调用:线程中发生函数调用时就会在线程栈中分配函数调用栈,而虚拟内存分配,文件操作,网络读写等功能,都是由操作系统实现,再向用户程序暴露接口,所以线程免不了要调用操作系统提供的系统服务,也就是少不了进行“系统调用”。
用户态切换到内核态:CPU中会有一个特权级标志,用于记录当前程序执行再用户态还是内核态,主要标记为内核态时才可以访问内核空间,而目前现存a1处在用户态,还不能访问内核空间,所以系统调用发生时就得切换到内核态,使用线程的内核栈,执行内核空间的系统函数,这被称为从用户态切换到内核态。
最初系统调用是通过软中断触发的,软中断就是通过指令模拟中断,与软中断对应的就是硬件中断,操作系统会按照CPU硬件要求,在内存里存一张中断向量表,用来把各个中断编号映射到相应的处理程序,例如linux中系统调用中断对应的编号为0X80,对应的处理程序就是用来派发系统调用的,为什么说派发系统调用呢?因为操作系统提供了数百个系统调用,不同为每一个都分配一个中断号,所以操作系统又实现了一张系统调用表。用于通过系统调用编号,找到对应的系统函数入口,所以用户程序这里会把要调用的系统函数编号写入特点寄存器,通过寄存器或用户栈来传递其他所需参数,然后用int 0x80来触发系统调用中断,而硬件层面CPU有一个中断控制器,它负责接收中断信号,切换到内核态,保存用户态执行现场,一部分寄存器的值会通过硬件机制保存起来,还有一部门通过寄存器的值会被压入内核栈中,然后去中断向量表这里查询0x80对应的系统调用派发程序入口,而系统调用的派发程序会根据指定的系统调用编号,去系统调用表这里查询对于的系统调用入口并执行…
https://www.bilibili.com/video/BV1H541187UH?from=search&seid=16017166239714027464
java线程1.2使用的是ULT,1.2版本后KLT。java线程与内核线程的关系是一一映射的关系
JVM进程创建大量线程,实际上是JVM在进程里创建"线程栈空间
",线程栈空间
没有创建真正的线程,栈空间里有一些栈帧指令,真正的线程通过"库调用器
"调度内核空间里的“内核线程”去操作CPU
- 用户级线程ULT:用户程序实现,不依赖操作系统核心,应用提供创建、同步、调度和管理线程的函数来控制用户线程。不需要用户态/核心态切换,速度快。内核对ULT无感知,线程阻塞则进程(包括它的所有线程)阻塞。(即在资源管理器中查看线程只能看到QQ线程,看不到QQ里的线程,这些线程由APP管理)
- 内核级线程KLT:系统内核管理线程,内核保存线程的状态和上下文信息,线程组合不会引起进程阻塞。在多处理器系统上,多线程在多处理器上并行进行。线程的创建、调度和管理由内核完成,效率比ULT要慢,比进行操作快。
JVM虚拟机使用的是内核级线程。即java程序中每创建一个线程都是内核级线程
分为用户空间和内核空间。
内核空间只有内核线程能访问,所以java才能调用cpu
操作系统的API可以用,但必须把操作权限提升到内核级线程。
java线程创建是依赖于系统内核,通过JVM调用系统库创建内核线程,内核线程和java-thread是1:1映射关系。
线程创建完后会调用库调用器,陷入到内核空间,然后创建内核线程,内核线程去获取CPU时间片。
如果线程没执行完就失去了CPU时间片,内核会把状态写到内核空间(程序运行状态段),获得CPU时间片时再重新加载运行。
线程是稀缺资源,它的创建和销毁是一个相对偏重且耗资源的操作,而java线程依赖于内核线程,创建线程需要进行操作系统状态切换,为避免资源过度消耗需要设法重用线程执行多个任务。线程池就是一个线程缓存,负责对线程进行统一分配、调优与监控。
什么时候使用线程池:
- 单个任务处理时间比较短
- 需要处理的任务数量较大
线程池优势:
- 重用存在的线程,减少线程创建,消亡的开销,提高性能
- 提高响应速度。当速度到达时,任务可以不需要得等到线程创建就能立即执行
- 提高线程的课管理性,可统一分配,调优和监控
它们在几乎每个方面都是相同的,唯一的区别是判断JVM何时离开。
守护线程是用来服务用户线程的,通过在start()方法前调用thread.setDaemon(true)可以把一个用户线程变成一个守护线程。
java垃圾回收就是一个典型的守护线程。
若JVM中都是守护线程,当前JVM将退出。
形象理解: 兔死狗烹,鸟尽弓藏
线程可以
守护线程
虚拟机必须确保用户线程执行完毕
虚拟机不必等待守护线程执行完毕
如:后台记录操作日志,监控内存,垃圾回收等待
list不安全的原因是可能同一时间操作了同一个索引
for(){
new Thread(()->{list.add();}).start();
}
线程的生命周期
JDK中用Thread.State类(线程的状态)定义了线程的几种状态
要想实现多线程, 必须在主线程中创建新的线程对象。 java语言使用Thread类及其子类的对象来表示线程, 在它的一个完整的生命周期中通常要经历如下的五种状态:
- 新建NEW: 当一个Thread类或其子类的对象被声明并创建时,新生的线程对象处于新建状态
- 就绪READY: 处于新建状态的线程被start()后,将进入线程队列等待CPU时间片,此时它已具备了运行的条件,只是没分配到CPU资源
- 运行RUNNING: 当就绪的线程被调度并获得CPU资源时,便进入运行状态, run()方法定义了线程的操作和功能
- 阻塞BLOCKED: 在某种特殊情况下,被人为挂起或执行输入输出操作时,让出 CPU 并临时中止自己的执行,进入阻塞状态
- 死亡TERMINATED: 线程完成了它的全部工作或线程被提前强制性地中止或出现异常导致结束
两种shutdown的区别
- shutdown:需要执行完队列和正在执行的任务,只是不再接收新任务
- shutdownNow():全部停止(运行的任务运行到安全点就退出)
terminate()是自己实现的
为什么用到并发
- 充分利用多核CPU的计算能力
- 方便进行业务拆分,提升应用性能
并发产生的问题:
- 高并发场景下,导致频繁的上下文切换
- 临界区线程安全问题,容易出现死锁的,产生死锁就会造成系统功能不可用
- 其他
上下文切换:换线程中间要保存上个线程的一些状态,是保存到了主内存中的TSS任务状态段
2、JMM
JMM : Java内存模型
前备知识:https://blog.csdn.net/hancoder/article/details/105740288
首先区分一些名词:
- JVM内存模型:堆栈方法区等
- JMM:java内存模型。只是抽象概念,并不存着
- cpu多核缓存架构:真实存在的,包括L1~L3缓存,寄存器,主内存
工作内存与主内存
java虚拟机有自己的内存模型(Java Memory Model,JMM),JMM可以屏蔽掉各种硬件和操作系统的内存访问差异,以实现让java程序在各种平台下都能达到一致的内存访问效果。
JMM决定一个线程对共享变量的写入何时对另一个线程可见,JMM定义了线程和主内存之间的抽象关系:共享变量存储在主内存(Main Memory)中,每个线程都有一个私有的本地内存(Local Memory),本地内存保存了被该线程使用到的主内存的副本拷贝,线程对变量的所有操作都必须在工作内存中进行,而不能直接读写主内存中的变量。这三者之间的交互关系如下
计算机在执行程序时,每条指令都是在CPU中执行的,而执行指令过程中,势必涉及到数据的读取和写入。由于程序运行过程中的临时数据是存放在主存(物理内存)当中的,这时就存在一个问题,由于CPU执行速度很快,而从内存读取数据和向内存写入数据的过程跟CPU执行指令的速度比起来要慢的多,因此如果任何时候对数据的操作都要通过和内存的交互来进行,会大大降低指令执行的速度。因此在CPU里面就有了高速缓存。

windows的intel处理器有3级处理器,先从主内存中复制到L3中,然后L3–>L2,L2–>L1。L1再复制到CPU的寄存器中。从寄存器中取出来后拿到计算单元中计算,计算完后写会寄存器,然后寄存器再复制到L1,然后L2,然后L3。
但是L3写回到内存的时间是不确定的,得看CPU什么时候空闲,所以就需要缓存一致性协议确保他强制刷回内存。
也就是,当程序在运行过程中,会将运算需要的数据从主存复制一份到CPU的高速缓存当中,那么CPU进行计算时就可以直接从它的高速缓存读取数据和向其中写入数据,当运算结束之后,再将高速缓存中的数据刷新到主存当中。举个简单的例子,比如下面的这段代码:
i=i+1;
当线程执行这个语句时,会先从主存当中读取i的值,然后复制一份到高速缓存当中,然后CPU执行指令对i进行加1操作,然后将数据写入高速缓存,最后将高速缓存中i最新的值刷新到主存当中。(读、改、写 这三个顺序)
这个代码在单线程中运行是没有任何问题的,但是在多线程中运行就会有问题了。在多核CPU中,每条线程可能运行于不同的CPU中,因此每个线程运行时有自己的高速缓存(对单核CPU来说,其实也会出现这种问题,只不过是以线程调度的形式来分别执行的)。本文我们以多核CPU为例。
比如同时有2个线程执行这段代码,假如初始时i的值为0,那么我们希望两个线程执行完之后i的值变为2。但是事实会是这样吗?
可能存在下面一种情况:初始时,两个线程分别读取i的值存入各自所在的CPU的高速缓存当中,然后线程1进行加1操作,然后把i的最新值1写入到内存。此时线程2的高速缓存当中i的值还是0,进行加1操作之后,i的值为1,然后线程2把i的值写入内存。
最终结果i的值是1,而不是2。这就是著名的缓存一致性问题。通常称这种被多个线程访问的变量为共享变量。
1 主内存
所有的变量存在主内存(虽然名字跟物理机的主内存一样,可类比,但此主内存只是虚拟机内存的一部分)
https://www.cnblogs.com/java-chen-hao/p/9968544.html
2 工作内存
- 每条线程都有自己的工作内存(可与高速缓存类比)
- 线程读写变量,必须在自己工作的工作内存中进行,不能直接读写主内存的变量
- 第一次访问这个共享内容的适合加载进入副本,只要不修改,就一直使用这个副本,不会重新去拿。(所以才有后面的volatile关键字)
- 当修改了副本之后,立刻同步到共享区域中。后续使用的是修改后的这个副本。(可能在这个期间别的线程也修改了该值,所以后面会用cas解决)
- 工作内存保存主内存变量的值的拷贝
- 不同线程间,无法直接访问对方工作内存的变量
- 线程间变量值的传递,需要通过主内存
CPU内存结构
- 控制单元
- 运算单元
- 存储单元
CPU多核缓存架构
CPU缓存:
- 一级Cache(L1 Cache)
- 二级Cache(L2 Cache)
- 三级Cache(L3 Cache)
x=1。比如左右CPU都进行i++后同步回主内存,那么就会有一个丢失,结果只为2。
解决方式有:
- 总线加锁:对总线加锁
- 缓存一致性协议
JMM与JVM模型
JMM和JVM内存区域的划分是不同的概念层次,更恰当说JMM描述的是一组规则,通过这组规则控制程序中各个变量在共享数据区域或私有数据区域的访问方式,JMM是围绕原子性、有序性、可见性
展开。
JVM内存:
JMM与CPU缓存模型
JMM模型跟CPU缓存模型结构类似,是基于CPU缓存模型建立起来的,JMM模式是标准化的,屏蔽掉底层不同计算机的区别。对于硬件内存来说只有寄存器、缓存内存、主内存的概念,并没有工作内存和主内存之分,也就是说java内存模型对内存的划分对硬件内存并没有任何影响。
JMM与CPU的映射:
JMM存在的必要性:
由于JVM运行程序的实体是线程,而每个线程创建时JVM都会为其创建一个工作内存(有些地方称为栈空间),用于存储线程私有的数据,线程与主内存中的变量必须通过工作内存间接完成,主要过程是将变量从主内存拷贝的每个线程各自的工作内存空间,然后对变量进行操作,操作完成后将变量写回到主内存,如果两个线程同时对一个主内存中的实例对象的变量进行操作就有可能诱发线程安全问题。
3、并发编程中的三个概念
在并发编程中,我们通常会遇到以下三个问题:原子性问题,可见性问题,有序性问题。我们先看具体看一下这三个概念:
1.原子性
原子性:即一个操作或者多个操作 要么全部执行并且执行的过程不会被任何因素打断,要么就都不执行。synchronized
可以保证代码片段的原子性。
一个很经典的例子就是银行账户转账问题:
比如从账户A向账户B转1000元,那么必然包括2个操作:从账户A减去1000元,往账户B加上1000元。
试想一下,如果这2个操作不具备原子性,会造成什么样的后果。假如从账户A减去1000元之后,操作突然中止。然后又从B取出了500元,取出500元之后,再执行 往账户B加上1000元 的操作。这样就会导致账户A虽然减去了1000元,但是账户B没有收到这个转过来的1000元。
所以这2个操作必须要具备原子性才能保证不出现一些意外的问题。
2.可见性
可见性是指当多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即看得到修改的值。volatile
关键字可以保证共享变量的可见性。
举个简单的例子,看下面这段代码:
1 //线程1执行的代码 CPU1
2 int i = 0;
3 i = 10;
4
5 //线程2执行的代码 CPU2
6 j = i;
假若执行线程1的是CPU1,执行线程2的是CPU2。由上面的分析可知,当线程1执行 i =10这句时,会先把i的初始值加载到CPU1的高速缓存中,然后赋值为10,那么在CPU1的高速缓存当中i的值变为10了,却没有立即写入到主存当中。(use后没有马上assign)
此时线程2执行 j = i,它会先去主存读取i的值并加载到CPU2的缓存当中,注意此时内存当中i的值还是0,那么就会使得j的值为0,而不是10。
这就是可见性问题,线程1对变量i修改了之后,线程2没有立即看到线程1修改的值。
3.有序性
有序性:即程序执行的顺序按照代码的先后顺序执行。java 在编译器以及运行期间的优化,代码的执行顺序未必就是编写代码时候的顺序。volatile
关键字可以禁止指令进行重排序优化。
举个简单的例子,看下面这段代码:
int i = 0;
boolean j = false;
i = 1; //语句1,跟i有关
j = true; //语句2,跟j有关
从代码顺序上看,语句1是在语句2前面的,那么JVM在真正执行这段代码的时候会保证语句1一定会在语句2前面执行吗?不一定,为什么呢?这里可能会发生指令重排序(Instruction Reorder)。
一般来说,处理器为了提高程序运行效率,可能会对输入代码进行优化,它不保证程序中各个语句的执行先后顺序同代码中的顺序一致,但是它会保证程序最终执行结果和代码顺序执行的结果是一致的。
比如上面的代码中,语句1和语句2谁先执行对最终的程序结果并没有影响,那么就有可能在执行过程中,语句2先执行而语句1后执行。
但是重排序也需要遵守一定规则:
1.重排序操作不会对存在数据依赖关系的操作进行重排序。
比如:a=1;b=a; 这个指令序列,由于第二个操作依赖于第一个操作,所以在编译时和处理器运行时这两个操作不会被重排序。
2.重排序是为了优化性能,但是不管怎么重排序,单线程下程序的执行结果不能被改变
比如:a=1;b=2;c=a+b这三个操作,第一步(a=1)和第二步(b=2)由于不存在数据依赖关系,所以可能会发生重排序,但是c=a+b这个操作是不会被重排序的,因为需要保证最终的结果一定是c=a+b=3。
4、八大原子操作
volatile关键字仅能保证变量远大原子操作的写操作的原子性
上面JMM操作主内存和工作内存是通过8大原子操作完成的。
8大原子操作
操作 | 说明 |
---|---|
★read (读取) | 作用于主内存变量,从主内存读取数据,以便随后的load操作 |
★load (载入) | 作用于工作内存的变量,它把read操作从主存中获得的变量放入工作内存的变量副本中 |
use (使用) | 作用于工作内存中的变量副本,它把工作内存中的变量传输给执行引擎,每当虚拟机遇到一个需要使用到变量的值,就会使用到这个指令 |
-------- | ------------------执行引擎------------------------- |
assign (赋值) | 作用于工作内存中的变量副本,它把一个从执行引擎中接收到的值放入工作内存的变量副本中 |
◆store (存储) | 作用于工作内存中的变量,它把一个从工作内存中一个变量的值传送到主内存中,以便后续的write操作。存值到主内存给write备用,还未写入变量 |
◆write (写入) | 作用于主内存中的变量,它把store操作从工作内存中得到的变量的值放入主内存的变量中 |
--------- | -------------以上为6大原子操作------------------------------- |
lock (锁定) | 作用于主内存的变量,把一个变量标识为线程独占状态。lock会阻塞别的线程read |
unlock (解锁) | 作用于主内存的变量,它把一个处于锁定状态的变量释放出来,释放后的变量 才可以被其他线程锁定 |
如果要把一个变量从主内存中复制到工作内存,就需要按顺寻地执行read和load操作,如果把变量从工作内存中同步回主内存中,就要按顺序地执行store和write操作。Java内存模型只要求上述两个操作必须按顺序执行,而没有保证必须是连续执行。也就是read和load之间,store和write之间是可以插入其他指令的,如对主内存中的变量a、b进行访问时,可能的顺序是read a,read b,load b, load a。Java内存模型还规定了在执行上述八种基本操作时,必须满足如下规则:
- (1)不允许【read和load】、【store和write】操作之一单独出现(即不允许一个变量从主存读取了但是工作内存不接受,或者从工作内存发起会写了但是主存不接受的情况),以上两个操作必须按顺序执行,但没有保证必须连续执行,也就是说,read与load之间、store与write之间是可插入其他指令的。
- (2)不允许一个线程丢弃它的最近的assign操作,即变量在工作内存中改变了之后必须把该变化同步回主内存。
- (3)不允许一个线程无原因地(没有发生过任何assign操作)把数据从线程的工作内存同步回主内存中。即值不变化就不要写回
- (4)一个新的变量只能从主内存中“诞生”,不允许在工作内存中直接使用一个未被初始化(load或assign)的变量,换句话说就是对一个变量实施use和store操作之前,必须先执行过了load和assign操作。
- (5)一个变量在同一个时刻只允许一条线程对其执行lock操作,但lock操作可以被同一个条线程重复执行多次,多次执行lock后,只有执行相同次数的unlock操作,变量才会被解锁。
- (6)如果对一个变量执行lock操作,将会清空工作内存中此变量的值,在执行引擎使用这个变量前,需要重新执行load或assign操作初始化变量的值。
- (7)如果一个变量实现没有被lock操作锁定,则不允许对它执行unlock操作,也不允许去unlock一个被其他线程锁定的变量。
- (8)对一个变量执行unlock操作之前,必须先把此变量同步回主内存(执行store+write操作)。
long/double非原子协定
java语言中,除了long和double以外的任何类型的变量的写操作都是原子操作,这里的写是指定的上面的write,这是java语言规范规定,由java虚拟机具体实现。
但是java语言规范特别地规定对于volatile关键字修饰的long/double型变量的写操作具有原子性。
Java内存模型要求8个操作都具有原子性,但对64位的数据类型,long和double,模型定义了相对宽松
允许虚拟机将没有被volatile修饰的64位数据的读写操作,划分为2次32位的操作。允许,并强烈建议,虚拟机将这些操作实现为原子性操作。
目前商用Java虚拟机几乎都选择把64位数据的读写作为原子操作来对待。
对于volatile关键字修饰的long/double型变量的写操作具有原子性
JMM要求lock、unlock、read、load、assign、use、store、write这8个操作都必须具有原子性,但对于64位的数据类型(long和double,具有非原子协定:允许虚拟机将没有被volatile修饰的64位数据的读写操作划分为2次32位操作进行。(与此类似的是,在栈帧结构的局部变量表中,long和double类型的局部变量可以使用2个能存储32位变量的变量槽(Variable Slot)来存储的,关于这一部分的详细分析,详见详见周志明著《深入理解Java虚拟机》8.2.1节)
如果多个线程共享一个没有声明为volatile的long或double变量,并且同时读取和修改,某些线程可能会读取到一个既非原值,也不是其他线程修改值的代表了“半个变量”的数值。不过这种情况十分罕见。因为非原子协议换句话说,同样允许long和double的读写操作实现为原子操作,并且目前绝大多数的虚拟机都是这样做的。
编写代码时,一般不需为long或double专门声明为volatile
测试代码
package com.example.demo;
public class TTest {
//volatile变量,用来确保将变量的更新操作通知到其他线程。
// private static boolean initFlag = false;//注意这里没有volatile修饰,一会儿会添加
private static volatile boolean initFlag = false;
public static void main(String[] args) throws InterruptedException {
new Thread(new Runnable() {
// 线程1
@Override
public void run() {
int i=0;
while (!initFlag) {
try {
Thread.sleep(200);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("循环中...第"+ i++);
}
System.out.println("=====线程1退出循环,不加volatile也能嗅探到");
}
}).start();
Thread.sleep(2000);
new Thread(new Runnable() {
//线程2
@Override
public void run() {
// 修改为true,想让线程1跳出循环
initFlag = true;
System.out.println("线程2修改完毕");
}
}).start();
}
/*
总结:
// 一般讲课讲的是线程2修改值后线程1感知不到,所以一直跳不出循环,但是我测试似乎也能跳出循环,可能跟版本和CPU有关系。难道我的电脑自动开了嗅探机制?其实不是的
// 在循环里加synchronized也会跳出循环来
// 原因:原来CPU1一直在循环很忙,不会把时间片释放出来,但加同步块之后就会阻塞,之前我们知道上下文切换的时候,重新重主内存加载的时候就加载到新值了,就跳出了循环
*/
}
上面的程序运行起来后,每个线程都把initFlag从主内存拷贝一份副本到各自的工作内存,线程2修改完后,只是修改了工作内存的变量,虽然线程2也会最后同步会主内存,但在这期间线程1工作内存中的值还是刚开始读到的值,并不会发现线程2已结修改了,因为线程1一直在忙,没有上下文切换(会存放写主内存和读主内存),没有重新读主内存
lock和unlock是时机在历史的发展过程中有所改变,下面的总线加锁和缓存一致性是不同的方式。
总线加锁
为了解决前面工作内存值不一致的问题,早期是用总线加锁方式解决的。
早期采用总线加锁的方式处理主内存和工作内存数据。现在使用的是MESI缓存一致性协议。
总线加锁:lock是早期的用法,线程1要读主内存数据的时候,就加个lock锁,这样别的线程2就read不了了,直到线程1 write写好后unlock,这样线程2才能read,这样就解决了线程不同步的问题,但是这样多线程性能很差,相当于变成了单线程。
5、缓存一致性
多核处理器里,每个CPU都有自己的高速缓存(一级、二级、三级),而它们又共享同一主内存。
当多个CPU的运算任务都涉及同一块主内存区域,可能导致各自的缓存数据不一致;数据同步回主存时,以谁的缓存数据为准呢?
为解决一致性问题,需要CPU访问缓存时都遵循一些协议,读写时,根据操作协议来。如MSI、MESI、MOSI、Synapse、Firefly、Dragon、Protocol
同时为了使得处理器充分被利用,CPU可能会对输入代码进行乱序执行(Out-of-Order Execution)优化,CPU会在计算后将结果重组,保证结果与顺序执行一致。Java虚拟机的即时编译器也有类似的指令重排序(Instruction Reorder)优化
总线嗅探机制
总线嗅探机制:开启缓存一致性协议后,当store经过总线的时候,别的线程就能嗅探到。别的线程对应的变量就自动失效了,从而其他线程要使用该值的时候需要重新read。
加上volatile关键字之后,就会开启总线嗅探机制和缓存一致性协议。
在Java中,volatile是个很高层面的规范,保证了指令不会被重排序+对volatile变量的写使得当前cpu缓存中的所有变量写回到主存中,从而保证了内存可见性。其实总线嗅探机制是结合缓存一致性协议完成的,总线嗅探机制提供了MESI状态给缓存行。
单例模式的懒汉模式为什么要加volatile?
if(singleton==null){ //1 synchronized(Singleton.class){ //2 if(singleton==null)//3 singleton=new Singleton();//4 // volatile的作用主要跟他有关。 // new对象的步骤:1 看class对象是否加载,没有就先加载class对象 2 分配内存 3 调用构造函数 4 返回地址给引用 // cpu为了优化程序,可能会进行指令重排,打乱 34两步。导致别的线程拿到了还没初始化的对象去使用 // 另外一种解释是 volatile保证了可见性 // T1 T2线程走到步骤1,会将singleton拷贝到各自的工作内存,这时即使T1构造好了,T2也还以为是空对象。 // 前面说了加上volatile之后就保证了缓存一致性,只要有一个store了,其他线程就能嗅探到,从而使那些工作内存的值失效,重新去读 } } return singleton;
具体的实现是靠JVM和cpu(还有操作系统?)合作实现的,不管cpu有没有mesi协议,用了volatile,JVM都会保证可见性,只不过实现方式是不一样的。
有个问题就是:mesi似乎已经保证了线程之间的可见性,那么在实现了mesi协议的cpu上,volatile关键字其实是不是没用的?
答案是:还是有用的,就算在实现了mesi的cpu上,volatile一样不可或缺。除了禁止指令重排序的作用外,由于mesi只是保证了L1-3 的cache之间的可见性,但是cpu和L1之间还有像storebuffer之类的缓存,而volatile规范保证了对它修饰的变量的写指令会使得当前cpu所有缓存写到被mesi保证可见性的L1-3cache中。(具体的实现,以X86体系为例,volatile会被JVM生成带lock前缀的指令)。
volatile也有八大原子操作的lock,但不会在主内存lock,即不会lock整个6大原子操作,而是分为了2回lock,即read+load
锁一下,store+write
锁一下,一个主内存只有一个锁,拿不到锁别的线程就要等待。
为什么分开加锁,因为write内存操作很快,但use和在执行引擎中的操作很慢。
为什么还需要八大原子的lock呢,看似刚才汇编的lock已经解决了缓存一致性。是因为如果两个线程同时assign写 的话,如果线程2提前到达总线被线程1嗅探到,线程1马上去read,但线程2还没write,线程1拿到的还是旧值,读到工作内存值有效。所以在store前加锁保证主内存已经是新值后线程1再read。同理read前到load后也加lock。
CPU的缓存一致性协议MESI
在多核CPU中,内存中的数据会在多个核心中存在数据副本,某一个核心发生修改操作,就产生了数据不一致的问题,而一致性协议正是用于保证多个CPU cache之间缓存共享数据的一致性。
cache的写操作
write through 写通
每次CPU修改cache中的内容会立即更新到内存,也就意味着每次CPU写共享数据,会导致总线事务,因此这种方式常常会引起总线事务的竞争,虽然后高的一致性但是效率非常低。
write back 写回
每次CPU修改了cache中的数据,不会立即更新到内存,而是等到cache line在某一个必须或合适的实际才会更新到内存。
写失效
当一个CPU修改了数据,如果其他CPU有该数据,则通知其为无效;
写更新
当一个CPU修改了数据,如果其他CPU有该数据,则通知其更新;
cache line缓存行
CPU缓存的最小存储单元是缓存行
cache line是cache与内存数据交换的最小单位,根据操作系统一般是32或64byte,在MESI协议中
- 状态 可以是M、E、S、I;
- 地址则是cache line中映射的内存地址;
- 数据则是从内存中读取的数据。
工作方式
当CPU从cache中读取数据时候,会比较地址是否相同,如果相同则检查cache line的状态,再决定该数据是否有效,无效则从主存中获取数据,或根据一致性协议发生一次cache-to-cache的数据推送。
工作效率
当CPU能够从cache中拿到有效数据的时候,消耗几个CPU cycle,如果发生cache miss也就是缓存中没有数据需要从主存中读取,则会消耗几十上百个CPU cycle。
状态介绍
MESI协议将cache line的状态分为modify、exclusive、shared、invalid分别是修改、独占、共享、失效
状态 | 描述 |
---|---|
M(modify) | 当前CPU刚修改完数据,当前CPU拥有最新数据,其他CPU拥有失效数据,而且和主存数据不一致 |
E(exclusive) | 只有当前CPU中有数据,其他CPU中没有改数据,当前CPU的数据和主存的数据是一致的,数据只存在于本Cache中 |
S(shared) | 当前CPU和其他CPU中都有共同的数据,数据和主存中的一致,数据存在于很多Cache中 |
I(invalid) | 当前CPU中的数据失效,数据应该从主存中获取,其他CPU中可能有数据也可能无数据;当前CPU中的数据和主存中的数据被认为不一致。该Cache无效 |
M和E状态下的Cache Line数据是独有的,不同点在于M状态的数据时dirty和内存的不一致,E状态下数据和内存是一致的;
读:

CPU1读X,汇编指令加lock,将CPU该缓存行的状态设为E
独享状态,然后CPU1去总线中时刻监听是否有其他CPU来操作X。如果此时CPU还没有回写的时候,CPU2就发出了读X的请求,CPU1就会监听到(总线嗅探机制),CPU1就会对cache1中的数据做出响应,CPU1将缓存行状态由E
变为S
,CPU2就可以读到X,状态也变为S
,此时X就存在了2个CPU缓存中,两个线程中缓存行都为S
。
注意MESI没有要求改完立即写回,volatile关键字才要求。
修改:

此时CPU1要进行+1操作,在这之前CPU1/2都是S
状态,CPU1锁住缓存行,CPU1将缓存行状态由S
变为M
。CPU2就嗅探到CPU1要修改数据,CPU2就将状态由S
变为I
无效。CPU1就把缓存行修改为X+1=2。修改后CPU1同步回主内存。
同步:

CPU1修改完后不是实时地同步到主内存,CPU2发出读取X发现无效了所以重新去主存读,路过总线的时候,CPU1就感知到信息,CPU1就将修改完的数据同步回内存,然后再同步到Cache2。CPU1由M
变为E
,然后CPU2读的时候,CPU1由E
变为S
,CPU2由I
修改为S
。
前面的解读两个CPU都是由先后顺序的,那会不会两个CPU就修改完后状态都变为M要同步回主内存?答案是不会,因为在同一个周期内,会进行裁决。裁决成功变为M,裁决失败变为I。
缓存一致性协议不起作用的时候:变量跨了两个缓存行(1个缓存行只能存64字节的数据),此时缓存一致性协议就锁不住缓存行了,只能通过总线锁加锁。或者CPU不支持MESI协议
MESI协议状态迁移
在MESI协议中,每个Cache控制器不仅知道自己的读写操作,而且也监听其他Cache的读写操作,每个Cache line所处的状态根据本核和其他核的读写操作在4个状态之间进行迁移。
分为以下四个操作:
- 读本cache LocalRead;
- 写本cache LocalWrite;
- 读其他cache Remote Read;
- 写其他cache Remote Write;
内存屏障
编译器和CPU可以保证输出结果一样的情况下对指令重排序,使性能得到优化,插入一个内存屏障,相当于告诉CPU和编译器限于这个命令的必须先执行,后于这个命令的必须后执行。
内存屏障的另一个作用是强制更新一次不同CPU的缓存,这意味着如果你对一个volatile字段进行写操作,你必须知道:
- 一旦你完成写入,任何访问这个字段的线程将会得到最新的值;
- 在你写入之前,会保证所有之前发生的事已经发生,并且任何更新过的数据值也是可见的,因为内存屏障会把之前的写入值都刷新到缓存。
内存屏障:又称内存栅栏模式一个CPU指令,他的作用有两个,一是保证特性操作的执行顺序,二是保证某些变量的内存可见性(利用该特性实现volatile的内存可见性)
由于编译器和处理器都能执行指令重排序优化。如果在指令间插入一条memory barrier则会告诉编译器和CPU,不管什么指令不管什么指令都不能喝这条Memory barrier指令重排序,也就是通过插入内存屏障禁止在内存屏障前后的指令执行重排序优化。
memory barrier的另外一个作用是强制刷出各种cpu的缓存数据,因此任何cpu上的线程都能读到这些数据的最新版本。总之,volatile变量正是通过内存屏障实现在内存中的语义,即可见性和禁止重排优化。
对于编译器来说,发现一个最优布置来最小化插入屏障的总数几乎不可能为此MM来取保守策略。下面是基于保守策略的JMM内存屏障插入策略:
1)在每个volatile写操作的面插入个st
2)在每个volatile写操作的后面插入
3)在每个volatileLoadLoad读操作的后面插入个屏障
4)在每个volatileLoadstore读操作的后面插入一个屏障。
上述内存屏障插入策略非常保守,但它可以保证在任意处理器平台,任意的程序中都能得到正确的volatile内存语义。
Volatile是如何保证可见性的?
加入volatile关键字时,会多出一个lock前缀指令,lock前缀指令实际上相当于一个内存屏障,它有三个功能:
- 确保指令重排序时不会把其后面的指令重排到内存屏障之前的位置,也不会把前面的指令排到内存屏障后面,即在执行到内存屏障这句指令时,前面的操作已经全部完成;
- 将当前处理器缓存行的数据立即写回系统内存(由volatile先行发生原则保证);
- 这个写回内存的操作会引起在其他CPU里缓存了该内存地址的数据无效。写回操作时要经过总线传播数据,而每个处理器通过嗅探在总线上传播的数据来检查自己缓存的值是不是过期了,当处理器发现自己缓存行对应的内存地址被修改,就会将当前处理器的缓存行设置为无效状态,当处理器要对这个值进行修改的时候,会强制重新从系统内存里把数据读到处理器缓存(也是由volatile先行发生原则保证);
缓存一致性协议有多种,但是日常处理的大多数计算机设备都属于”嗅探(snooping)”协议,它的基本思想是:
所有内存的传输都发生在一条共享的总线上,而所有的处理器都能看到这条总线:缓存本身是独立的,但是内存是共享资源,所有的内存访问都要经过仲裁(同一个指令周期中,只有一个CPU缓存可以读写内存)。
CPU缓存不仅仅在做内存传输的时候才与总线打交道,而是不停在嗅探总线上发生的数据交换,跟踪其他缓存在做什么。所以当一个缓存代表它所属的处理器去读写内存时,其它处理器都会得到通知,它们以此来使自己的缓存保持同步。只要某个处理器一写内存,其它处理器马上知道这块内存在它们的缓存段中已失效。
** 反复思考IA-32手册对lock指令作用的这几段描述,可以得出lock指令的几个作用:
1、锁总线,其它CPU对内存的读写请求都会被阻塞,直到锁释放,不过实际后来的处理器都采用锁缓存替代锁总线,因为锁总线的开销比较大,锁总线期间其他CPU没法访问内存
2、lock后的写操作会回写已修改的数据,同时让其它CPU相关缓存行失效,从而重新从主存中加载最新的数据
3、不是内存屏障却能完成类似内存屏障的功能,阻止屏障两遍的指令重排序
由于效率问题,实际后来的处理器都采用锁缓存来替代锁总线,这种场景下多缓存的数据一致是通过缓存一致性协议来保证的 **
问题
既然CPU有了MESI协议可以保证cache的一致性,那么为什么还需要volatile这个关键词来保证可见性(内存屏障)?或者是只有加了volatile的变量在多核cpu执行的时候才会触发缓存一致性协议?
两个解释结论:
- 多核情况下,所有的cpu操作都会涉及缓存一致性的校验,只不过该协议是弱一致性,
不能保证一个线程修改变量后,其他线程立马可见
,也就是说虽然其他CPU状态已经置为无效,但是当前CPU可能将数据修改之后又去做其他事情,没有来得及将修改后的变量刷新回主存,而如果此时其他CPU需要使用该变量,则又会从主存中读取到旧的值。而volatile则可以保证可见性,即立即刷新回主存,修改操作和写回操作必须是一个原子操作; - 正常情况下,系统操作并不会进行缓存一致性的校验,只有变量被volatile修饰了,该变量所在的缓存行才被赋予缓存一致性的校验功能。
volatile的使用场景
- 状态标志(开关模式)
- 双重检查锁定
- 需要利用顺序性
volatile和synchronized的区别
使用上的区别
volatile只能修饰变量,synchronized只能修饰方法和语句块;
对原子性的保证
synchronized可以保证原子性,volatile不能保证原子性;
对可见性的保证
都可以保证可见性,但实现原理不同,volatile对变量加了lock,synchronized使用monitorEnter和monitorExit;
对有序性的保证
都可以保证有序性,但是synchronized并发退化到串行;
其他
synchronized引起阻塞;
volatile不会引起阻塞;
5、volatile
在 JDK1.2 之前,java的内存模型实现总是从主存(即共享内存)读取变量,是不需要进行特别的注意的。而在当前的 java 内存模型下,线程可以把变量保存本地内存(比如机器的寄存器)中,而不是直接在主存中进行读写。这就可能造成如下问题:一个线程在主存中修改了一个变量的值,而另外一个线程还继续使用它在寄存器中的变量值的拷贝,造成==数据的不一致==。
要解决这个问题,就需要把变量声明为volatile,这就指示 JVM,这个变量是不稳定的,每次使用这个变量都到主存中进行读取。
volatile缓存可见性实现原理:
- JMM内存交互层面:volatile修饰的变量的
read、load、use
操作和assign、store、write
必须是连续的,即修改后必须立即同步回主内存
,使用时必须从主内存刷新
,由此保证volatile的可见性 - 底层实现:通过汇编lock前缀指令,他会锁定变量缓存行取余并写回主内存,这个操作称为“缓存锁定”,缓存一致性机制会组织同时修改两个以上处理器缓存的内存取余数据,一个处理器的缓存回写到内存会导致其处理器的缓存失效。
lock指令查看汇编源码
volatile缓存可见性实现原理:
底层实现主要通过汇编lock前缀指令,它会锁定这块内存区域的缓存(缓存行锁定),并写回到主内存
java程序汇编代码查看
-server -Xcomp
-XX:+UnlockDiagnosticVMOptions
-XX:+PrintAssmbly
-XX:CompileCommand=compileonly,*VolativeVisibilityTest.prepareData
在jdk的jre/bin目录:hsdis-amd64.dll ,hsdis-amd64.lib
在IDEA中选择刚才放的jdk
正常执行程序后,在控制台就输出了
搜索lock就看到对应的行,正常我们initFlag=true的汇编指令是
add dword ptr [rsp],0h......prepareData@9 (line36)
加了volatile后
lock add dword ptr [rsp],0h......prepareData@9 (line36)
lock是什么?
IA-32架构软件开发者手册对lock指令的解释:
- 会将当前处理器缓存行的数据立即写回到系统内存
- 这个写回内存的操作会引起在其他CPU缓存了该内存地址的数据失效(MESI协议)
加了volatile关键字后,因为工作内存的initFlag=false前有lock修饰,所以在线程2的assign后,会马上将工作内存修改后的值同步会主内存。如果没加volatile,assign后就不会马上同步回主内存,别的线程也感知不到。
同步回主内存的过程中经过总线,总线嗅探机制就感知到了,会使线程1的工作内存中对应的值失效,线程1要使用的时候就需要重新read
volatile特点如下:
-
保证可见性。对一个volatile变量的读,总是能看到(任意线程)对这个volatile变量最后的写入,这个新值对于其他线程来说是立即可见的。每次使用变量的时候强制去重写获取副本
-
禁用指令重排序:指令重排序是编译器和处理器为了高效对程序进行优化的手段,下文有详细的分析。
-
不保证原子性。对任意单个volatile变量的读/写具有原子性,但类似于i++、i–这种复合操作不具有原子性,因为自增运算包括读取i的值、i值增加1、重新赋值3步操作,并不具备原子性。
由于volatile只能保证变量的可见性和屏蔽指令重排序,只有满足下面2条规则时,才能使用volatile来保证并发安全,否则就需要加锁(使用synchronized、lock或者java.util.concurrent中的Atomic原子类)来保证并发中的原子性。
- 运算结果不存在数据依赖(重排序的数据依赖性),或者只有单一的线程修改变量的值(重排序的as-if-serial语义)
- 变量不需要与其他的状态变量共同参与不变约束
因为需要在本地代码中插入许多内存屏蔽指令在屏蔽特定条件下的重排序,volatile变量的写操作与读操作相比慢一些,但是其性能开销比锁低很多。
volatile修饰的成员变量在每次被线程访问时,都强迫从共享内存中重读该成员变量的值。而且,当成员变量发生变化时,强迫线程将变化值回写到共享内存。这样在任何时刻,两个不同的线程总是看到某个成员变量的同一个值。
volatile是一种稍弱的同步机制,在访问volatile变量时不会执行加锁操作,也就不会执行线程阻塞,因此volatilei变量是一种比synchronized关键字更轻量级的同步机制。
使用建议:在两个或者更多的线程需要访问的成员变量上使用volatile。当要访问的变量已在synchronized代码块中,或者为常量时,没必要使用volatile。
由于使用volatile屏蔽掉了JVM中必要的代码优化,所以在效率上比较低,因此一定在必要时才使用此关键字。
Thread.sleep后线程会去重新获取主内存的值。有sysnchonized也会重新拿
1、保证可见性
public class Test {
//volatile变量,用来确保将变量的更新操作通知到其他线程。
private static volatile boolean initFlag = false;
public static void main(String[] args) throws InterruptedException {
new Thread(new Runnable() {
@Override
public void run() {
System.out.println("waiting data...");
while (!initFlag) {
}
System.out.println("==============success");
}
}).start();
Thread.sleep(2000);
new Thread(new Runnable() {
@Override
public void run() {
// 修改为true,想让线程1跳出循环
initFlag = true;
System.out.println("线程2修改完毕");
}
}).start();
}
}
线程1:先把initFlag变量read读取出来,再load载入工作内存,use使用线程1执行代码!initFlag
线程2:先把initFlag变量read读取出来,再load载入工作内存,use使用线程2执行代码initFlag=true,再assign重新赋值,store存储并写入主内存,write写入到主内存中的变量。(线程2对缓存行lock加锁,write写入主内存后会解锁unlock,防止initFlag还未write写入主内存就被线程1读取为false)。
线程1:因为initFlag被volatile修饰,volatile使用MESI缓存一致性协议,线程1cpu总线嗅探机制监听到了initFlag值的修改,线程1中initFlag=false失效变为true退出循环继续执行,体现了多线程同步运行共享变量副本的可见性。如果initFlag没有被volatile修饰,线程1将感知不到initFlag的变化,一直循环下去停止不了。
2、不保证原子性
首先需要了解的是,Java中只有对基本类型变量的赋值和读取是原子操作,如i = 1的赋值操作,但是像j = i或者i++这样的操作都不是原子操作,因为他们都进行了多次原子操作,比如先读取i的值,再将i的值赋值给j,两个原子操作加起来就不是原子操作了。
volatile只能保证可见性,但无法保证原子性,which is a necessity for synchronization.
因此,如果不符合下面两个规则的运算场景,
- 运算结果不依赖当前值,或者能够确保只有单一线程修改变量的值
- 变量不需要与其他的状态变量共同参与不变约束
我们需要通過加锁,如synchronized关键字和java.util.concurrent包下的原子类,来保证源自性。如果符合,volatile就能保证同步
举个栗子
一个变量i被volatile修饰,两个线程想对这个变量修改,都对其进行自增操作也就是i++,i++的过程可以分为三步,首先获取i的值,其次对i的值进行加1,最后将得到的新值写会到缓存中。
线程A首先得到了i的初始值100,但是还没来得及修改,就阻塞了。(工作内存中有值了,且已经use到执行引擎中了)
这时线程B开始了,它也得到了i的值,由于i的值未被修改,即使是被volatile修饰,主存的变量还没变化,那么线程B得到的值也是100,之后对其进行加1操作,得到101后,将新值写入到缓存中,再刷入主存中。根据可见性的原则,这个主存的值可以被其他线程可见。
问题来了,线程A已经读取到了i的值为100,也就是说读取的这个原子操作已经结束了,所以这个可见性来的有点晚,线程A阻塞结束后,继续将100这个值加1,得到101,再将值写到缓存,最后刷入主存,所以即便是volatile具有可见性,也不能保证对它修饰的变量具有原子性。
从S变为I后,数据失效,但是次数已经减去了
public class TTest {
// volatile 不保证原子性
private volatile static int num = 0;
public static void add(){
num++; // 不是一个原子性操作
// 既然不是原子操作,那么volatile加不加有什么影响呢?
}
public static void main(String[] args) {
//理论上num结果应该为 2 万
for (int i = 1; i <= 20; i++) {
new Thread(()->{
for (int j = 0; j < 1000 ; j++) {
add();
}
}).start();
}
while (Thread.activeCount()>2){
// main gc或者使用threadi.join
Thread.yield();
}
// 输出当前值,本该是20000,但是却不到20000
System.out.println(Thread.currentThread().getName() + " " + num);
}
}
i++和++i的线程安全分为两种情况:
1、如果i是局部变量(在方法里定义的),那么是线程安全的。因为局部变量是线程私有的,别的线程访问不到,其实也可以说没有线程安不安全之说,因为别的线程对他造不成影响。
2、如果i是全局变量(类的成员变量),那么是线程不安全的。因为如果是全局变量的话,同一进程中的不同线程都有可能访问到。
上面代码哪里不原子了?
网上会说i++不原子。说得太模糊了。
原理:
use后去引擎执行,出来后assign拿到的+1了。
assign后根据八大原子规则,必须写回主内存,不存在丢失。
两个线程同时use,去执行引擎里执行,线程2率先执行完拿到了lock锁然后执行assign操作,线程1执行后因为拿不到变量lock所以阻塞,线程2拿到锁后assign路过总线后引起线程1工作内存里对应的值失效了,没有关系啊,线程1在执行引擎了,出来后他也assign写到工作内存,再store回主内存的时候也触发了总线嗅探机制,线程2的工作内存失效了,主内存的值又是线程1的了,就把刚才线程2的值覆盖了。线程2下次重新read
回忆一下可见性中的例子为什么没什么问题,因为他有执行引擎"!取非"操作,while也只是重复use ,但没有"更改"操作,所以也没有assign操作(八大原子特性,只有修改才assign),只会一个劲use时看工作内存的值,某一时刻线程2把线程1工作内存的值失效了,重新read即可
i++线程问题解决方法:在volatile基础上加上原子性,可以在前面再加上synchronized,也可以AtomicInteger可以保证原子性
import java.util.concurrent.atomic.AtomicInteger;
// volatile 不保证原子性
public class VDemo02 {
// volatile 不保证原子性// 原子类的 Integer
private volatile static AtomicInteger num = new AtomicInteger();
public static void add(){
// num++; // 不是一个原子性操作
num.getAndIncrement(); // AtomicInteger + 1 方法, CAS
}
public static void main(String[] args) {
for (int i = 1; i <= 20; i++) {
new Thread(()->{
for (int j = 0; j < 1000 ; j++) {
add();
}
}).start();
}
while (Thread.activeCount()>2){
// main gc
Thread.yield();
}
// 输出20000,正确
System.out.println(Thread.currentThread().getName() + " " + num);
}
}
这些类的底层都直接和操作系统挂钩!在内存中修改值!Unsafe类是一个很特殊的存在!
3、限制指令重排
什么是 指令重排:你写的程序,计算机并不是按照你写的那样去执行的。
源代码–>编译器优化的重排–> 指令级并行重排序–> 内存系统重排序—> 执行指令序列
- as-if-serial语义:不管怎么重排序(编译器和处理器为了提高并行度),(单线程)程序的执行结果不能被改变。编译器、runtime和处理器都必须遵守as-if-serial语义
- 指令重排序:java语言规范规定JVM线程内部维持顺序华语义,即只要程序的最终结果与它顺序化的情况的结果相等,那么指令的执行顺序可以与代码顺序不一致,此过程叫指令的重排序
- 指令重排序的意义:JVM能根据处理器特性(CPU多级缓存系统、多核处理器等)适当地对机器指令进行重排序,使机器指令能更符合CPU的执行特性,更大限度地发挥机器性能。
这个操作,相当于一个内存屏障(Memory Barrier/Memory Fence),意思是,重排序时,不能把后面的指令重排序到內存屏障之前的位置
lock addl $0x0,(%esp)
汇编指令,把ESP寄存器的值加0,这个是空操作。其作用,是使得本CPU的Cache写入内存,该写入动作,也会引起别的CPU或别的内核无效化(Invalidate)其Cache,相当于对Cache中的变量,做了一次如Java内存模型中的”Store且Write操作”。所以,通过这样一个空操作,可让volatile变量的修改,对其他CPU立即可见
int x = 1; // 1
int y = 2; // 2
x = x + 5; // 3
y = x * x; // 4
我们所期望的执行顺序是:1234,但是可能执行的时顺序可能变成 2134 1324
可不可能是 4123!因为结果不一样
多线程中指令重排可能造成影响的结果: a b x y 这四个值默认都是 0;
线程A | 线程B |
---|---|
x=a=0 | y=b=0 |
b=1 | a=2 |
正常的结果: x = 0;y = 0;但是可能由于指令重排
线程A | 线程B |
---|---|
b=1 | a=2 |
x=a | y=b |
指令重排导致的诡异结果: x = 2;y = 1;
CAS是乐观锁,synchronized和lock是悲观锁
5 synchronized
java的任何一个对象都有唯一一个与之关联的锁。这种锁被称为监视器monitor或者内部锁。内部锁是一种排它锁,他能够保证原子性、可见性和有序性。
发展:
早期,jdk早期的时候,这个synchronized的底层实现是重量级的,重量级到这个synchronized都是要去找操作系统去申请锁的地步,这就会造成synchronized效率非常低,java后来越来越开始处理高并发的程序的时候,很多程序员都不满意,说这个synchrionized用的太重了,我没办法,就要开发新的框架,不用你原生的了
改进
这个锁升级的概念呢,是这样的,原来呢都要去找操作系统,要找内核去申请这把锁,到后期做了对
synchronized的一些改进,他的效率比原来要改变了不少,改进的地方。当我们使用synchronized的时候HotSpot的实现是这样的:上来之后第一个去访问某把锁的线程 比如sync (Object) ,来了之后先在这个Object的头上面markword记录这个线程。(如果只有第一个线程访问的时候实际上是没有给这个Object加锁的,在内部实现的时候,只是记录这个线程的ID(偏向锁))。
内部锁是通过synchronized关键字实现的。
用法:
- 在方法前面加:如public synchronized void sale(){}
- 同步代码块:synchronized(锁句柄){}
- 锁句柄指的是一个对象的引用,可以是实例对象可以是class对象。通常用private final修饰
- 代码块就是临界区
// 基本的卖票例子
import java.time.OffsetDateTime;
/**
* 真正的多线程开发,公司中的开发,降低耦合性
* 线程就是一个单独的资源类,没有任何附属的操作!
* 1、 属性、方法
*/
public class SaleTicketDemo01 {
public static void main(String[] args) {
// 并发:多线程操作同一个资源类, 把资源类丢入线程
Ticket ticket = new Ticket();
// @FunctionalInterface 函数式接口,jdk1.8 lambda表达式 (参数)->{ 代码 }
new Thread(()->{
//匿名函数,传入Runnable
for (int i = 1; i < 40 ; i++) {
ticket.sale();//这个ticket使用的是外部的变量
}
},"A").start();
new Thread(()->{
for (int i = 1; i < 40 ; i++) {
ticket.sale();
}
},"B").start();
new Thread(()->{
for (int i = 1; i < 40 ; i++) {
ticket.sale();
}
},"C").start();
}
}
// 资源类 OOP
class Ticket {
// 属性、方法
private int number = 30;
// 卖票的方式
// synchronized 本质: 队列,锁
public synchronized void sale(){
if (number>0){
System.out.println(Thread.currentThread().getName()+"卖出了"+(number--)+"票,剩余:"+number);
}
}
}
6 ReentrantLock
显示锁是JDK5开始引入的排他锁。显示锁是java.util.concurrent.locks.Lock接口的实例。ReentrantLock就是他的实现类
公平锁:可以先来后到
非公平锁(默认):可以插队
ReentrantLock默认的是非公平锁。
使用方法:
- new ReentrantLock();无参是非公平锁//ReentrantLock(true)是公平锁
- lock.lock(); // 加锁
- lock.newCondition();
- finally=> lock.unlock(); // 解锁
获取锁的几种方法
- lock.lock(); // 加锁,且还会阻塞当前线程直到拿到锁
- lock.tryLock();仅在调用时锁为空闲状态才获取锁。返回true代表拿到锁
- lock.tryLock(long,TimeUnit);//指定时间内获取锁,超时则不获取了
- lock.lockInterruptibly();//能被中断的获取锁,获取到的锁能被响应中断,中断后抛出异常并释放锁
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class SaleTicketDemo02 {
public static void main(String[] args) {
// 并发:多线程操作同一个资源类, 把资源类丢入线程
Ticket2 ticket = new Ticket2();
// @FunctionalInterface 函数式接口,jdk1.8 lambda表达式 (参数)->{ 代码 }
new Thread(()->{
for (int i = 1; i < 40 ; i++) ticket.sale();},"A").start();
new Thread(()->{
for (int i = 1; i < 40 ; i++) ticket.sale();},"B").start();
new Thread(()->{
for (int i = 1; i < 40 ; i++) ticket.sale();},"C").start();
}
}
// Lock三部曲
// 1、 new ReentrantLock();
// 2、 lock.lock(); // 加锁
// 3、 finally=> lock.unlock(); // 解锁
class Ticket2 {
// 属性、方法
private int number = 30;
// 实例锁
Lock lock = new ReentrantLock();
public void sale(){
lock.lock(); // 加锁
try {
// 业务代码
if (number>0){
System.out.println(Thread.currentThread().getName()+"卖出了"+(number--)+"票,剩余:"+number);
}
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock(); // 解锁
}
}
}
Synchronized 和 Lock 区别:
1、Synchronized 内置的Java关键字, Lock 是一个Java类
2、Synchronized 无法判断获取锁的状态,Lock 可以判断是否获取到了锁
3、Synchronized 会自动释放锁,lock 必须要手动释放锁!如果不释放锁,死锁
4、Synchronized 线程 1(获得锁,阻塞)、线程2(等待,傻傻的等);Lock锁就不一定会等待下去(tryLock);
5、Synchronized 可重入锁,不可以中断的,非公平;Lock ,可重入锁,可以判断锁,非公平/公平;
6、Synchronized 适合锁少量的代码同步问题,Lock 适合锁大量的同步代码!
Lock接口提供的 synchronized 不具备的主要特性:
- 尝试性的获取锁: 当前线程尝试获取锁,如果当前锁没有被其它线程获取到,则成果获取并持有。
- 能被中断的获取锁: 与 synchronized 不同的是,获取到锁的线程能够响应中断,当获取到锁的线程被其它线程中断时,中断异常被抛出,同时释放锁。
- 超时获取锁:在指定时间之前获取锁,超时无法获取则返回。
7、生产者和消费者问题
package com.kuang.pc;
/**
* 线程之间的通信问题:生产者和消费者问题! 等待唤醒,通知唤醒
* 线程交替执行 A B 操作同一个变量 num = 0
* A num+1
* B num-1
*/
public class A {
public static void main(String[] args) {
Data data = new Data();
new Thread(()->{
for (int i = 0; i < 10; i++) {
try {
data.increment();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
},"A").start();
new Thread(()->{
for (int i = 0; i < 10; i++) {
try {
data.decrement();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
},"B").start();
new Thread(()->{
for (int i = 0; i < 10; i++) {
try {
data.increment();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
},"C").start();
new Thread(()->{
for (int i = 0; i < 10; i++) {
try {
data.decrement();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
},"D").start();
}
}
// 判断等待,业务,通知
class Data{
// 数字 资源类
private int number = 0;
//+1
public synchronized void increment() throws InterruptedException {
while (number!=0){
//0
// 等待
this.wait();
}
number++;
System.out.println(Thread.currentThread().getName()+"=>"+number);
// 通知其他线程,我+1完毕了
this.notifyAll();
}
//-1
public synchronized void decrement() throws InterruptedException {
while (number==0){
// 1 //虽然拿到了锁,但是没有库存,那么就释放锁等待唤醒
// 等待
this.wait();
}
number--;
System.out.println(Thread.currentThread().getName()+"=>"+number);
// 通知其他线程,我-1完毕了
this.notifyAll();
}
}
问题存在,A B C D 4 个线程! 虚假唤醒
虚拟唤醒
线程在没有被唤醒,中断或者时间耗尽的情况下仍然能够被唤醒,这叫做伪唤醒。虽然在实际中,这种情况很少发生,但是程序一定要测试这个能够唤醒线程的条件,并且在条件不满足时,线程继续等待。换言之,wait操作总是出现在循环中,就像下面这样:
+在执行,-在等待。+好了通知-来消费。如果只有一个-,那么无所谓。现在外面有2个-。
结果另一个+进来, 也wait了。第一个+也进来了,也wait。一个-进来后,两个+都激活了。跳出if!=0,生产,此时就超出了我们要求的缓冲池1。
而如果是while,if 改为 while 判断
synchronized(对象){
while(条件不满足){
对象.wait();
}
对应的逻辑处理
}
Synchronized wait notify
Lock condition1.await condition2.signal
package com.kuang.pc;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class B {
public static void main(String[] args) {
Data2 data = new Data2();
new Thread(()->{
for (int i = 0; i < 10; i++) {
try {
data.increment();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
},"A").start();
new Thread(()->{
for (int i = 0; i < 10; i++) {
try {
data.decrement();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
},"B").start();
new Thread(()->{
for (int i = 0; i < 10; i++) {
try {
data.increment();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
},"C").start();
new Thread(()->{
for (int i = 0; i < 10; i++) {
try {
data.decrement();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
},"D").start();
}
}
// 判断等待,业务,通知
class Data2{
// 数字 资源类
private int number = 0;
Lock lock = new ReentrantLock();
Condition condition = lock.newCondition();
//condition.await(); // 等待
//condition.signalAll(); // 唤醒全部
//+1
public void increment() throws InterruptedException {
lock.lock();
try {
while (number!=0){
//0
// 等待
condition.await();
}
number++;
System.out.println(Thread.currentThread().getName()+"=>"+number);
// 通知其他线程,我+1完毕了
condition.signalAll();
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
//-1
public synchronized void decrement() throws InterruptedException {
lock.lock();
try {
while (number==0){
// 1
// 等待
condition.await();
}
number--;
System.out.println(Thread.currentThread().getName()+"=>"+number);
// 通知其他线程,我-1完毕了
condition.signalAll();
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
}
存在问题:随机的状态,我们想让有序执行ABCD
package com.kuang.pc;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
/**
* A 执行完调用B,B执行完调用C,C执行完调用A
*/
public class C {
public static void main(String[] args) {
Data3 data = new Data3();
new Thread(()->{
for (int i = 0; i <10 ; i++) {
data.printA();
}
},"A").start();
new Thread(()->{
for (int i = 0; i <10 ; i++) {
data.printB();
}
},"B").start();
new Thread(()->{
for (int i = 0; i <10 ; i++) {
data.printC();
}
},"C").start();
}
}
class Data3{
// 资源类 Lock
private Lock lock = new ReentrantLock();
private Condition condition1 = lock.newCondition();
private Condition condition2 = lock.newCondition();
private Condition condition3 = lock.newCondition();
private int number = 1; // 1A 2B 3C
public void printA(){
lock.lock();
try {
// 业务,判断-> 执行-> 通知
while (number!=1){
// 等待
condition1.await();
}
System.out.println(Thread.currentThread().getName()+"=>AAAAAAA");
// 唤醒,唤醒指定的人,B
number = 2;
condition2.signal();
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
public void printB(){
lock.lock();
try {
// 业务,判断-> 执行-> 通知
while (number!=2){
condition2.await();
}
System.out.println(Thread.currentThread().getName()+"=>BBBBBBBBB");
// 唤醒,唤醒指定的人,c
number = 3;
condition3.signal();
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
public void printC(){
lock.lock();
try {
// 业务,判断-> 执行-> 通知
// 业务,判断-> 执行-> 通知
while (number!=3){
condition3.await();
}
System.out.println(Thread.currentThread().getName()+"=>BBBBBBBBB");
// 唤醒,唤醒指定的人,c
number = 1;
condition1.signal();
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
}
8、八锁现象
如何判断锁的是谁!永远的知道什么锁,锁到底锁的是谁!
5.1. 多个线程使用同一把锁-顺序执行
5.2. 多个线程使用同一把锁,其中某个线程里面还有阻塞-顺序先执行
5.3. 多个线程有锁与没锁-随机执行
5.4. 多个线程使用多把锁-随机执行
5.5. Class锁:多个线程使用一个对象-顺序执行
5.6. Class锁:多个线程使用多个对象-顺序执行
5.7. Class锁与对象锁:多个线程使用一个对象-随机执行
5.8. Class锁与对象锁:多个线程使用多个对象-随机执行
如果两个方法有各自的同步,而且非静态的,那么用的对象为同一个this
静态方法锁的是class对象,非静态锁的是实例对象
package com.kuang.lock8;
import java.util.concurrent.TimeUnit;
/**
* 8锁,就是关于锁的8个问题
* 1、问题:标准情况下,两个线程先打印 发短信还是 打电话? 答案:1/发短信 2/打电话
* 1、sendSms延迟4秒,两个线程先打印 发短信还是 打电话? 1/发短信 2/打电话
*/
public class Test1 {
public static void main(String[] args) {
Phone phone = new Phone();
//锁的存在
new Thread(()->{
phone.sendSms();
},"A").start();
// 捕获
try {
TimeUnit.SECONDS.sleep(1);//睡眠
} catch (InterruptedException e) {
e.printStackTrace();
}
new Thread(()->{
phone.call();
},"B").start();
}
}
class Phone{
// synchronized 锁的对象是方法的调用者!、
// 两个方法用的是同一个锁,谁先拿到谁执行!
public synchronized void sendSms(){
try {
TimeUnit.SECONDS.sleep(4);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("发短信");
}
public synchronized void call(){
System.out.println("打电话");
}
}
package com.kuang.lock8;
import java.util.concurrent.TimeUnit;
/**
* 3、 增加了一个普通方法后!先执行发短信还是Hello? 普通方法
* 4、 两个对象,两个同步方法, 发短信还是 打电话? // 打电话
*/
public class Test2 {
public static void main(String[] args) {
// 两个对象,两个调用者,两把锁!
Phone2 phone1 = new Phone2();
Phone2 phone2 = new Phone2();
//锁的存在
new Thread(()->{
phone1.sendSms();
},"A").start();
// 捕获
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
new Thread(()->{
phone2.call();
},"B").start();
}
}
class Phone2{
// synchronized 锁的对象是方法的调用者!
public synchronized void sendSms(){
try {
TimeUnit.SECONDS.sleep(4);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("发短信");
}
public synchronized void call(){
System.out.println("打电话");
}
// 这里没有锁!不是同步方法,不受锁的影响
public void hello(){
System.out.println("hello");
}
}
package com.kuang.lock8;
import java.util.concurrent.TimeUnit;
/**
* 5、增加两个静态的同步方法,只有一个对象,先打印 发短信?打电话?
* 6、两个对象!增加两个静态的同步方法, 先打印 发短信?打电话?
*/
public class Test3 {
public static void main(String[] args) {
// 两个对象的Class类模板只有一个,static,锁的是Class
Phone3 phone1 = new Phone3();
Phone3 phone2 = new Phone3();
//锁的存在
new Thread(()->{
phone1.sendSms();
},"A").start();
// 捕获
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
new Thread(()->{
phone2.call();
},"B").start();
}
}
// Phone3唯一的一个 Class 对象
class Phone3{
// synchronized 锁的对象是方法的调用者!
// static 静态方法
// 类一加载就有了!锁的是Class
public static synchronized void sendSms(){
try {
TimeUnit.SECONDS.sleep(4);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("发短信");
}
public static synchronized void call(){
System.out.println("打电话");
}
}
package com.kuang.lock8;
import java.util.concurrent.TimeUnit;
/**
* 1、1个静态的同步方法,1个普通的同步方法 ,一个对象,先打印 发短信?打电话?
* 2、1个静态的同步方法,1个普通的同步方法 ,两个对象,先打印 发短信?打电话?
*/
public class Test4 {
public static void main(String[] args) {
// 两个对象的Class类模板只有一个,static,锁的是Class
Phone4 phone1 = new Phone4();
Phone4 phone2 = new Phone4();
//锁的存在
new Thread(()->{
phone1.sendSms();
},"A").start();
// 捕获
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
new Thread(()->{
phone2.call();
},"B").start();
}
}
// Phone3唯一的一个 Class 对象
class Phone4{
// 静态的同步方法 锁的是 Class 类模板
public static synchronized void sendSms(){
try {
TimeUnit.SECONDS.sleep(4);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("发短信");
}
// 普通的同步方法 锁的调用者
public synchronized void call(){
System.out.println("打电话");
}
}
new this 具体的一个手机
static Class 唯一的一个模板
9、集合类不安全
List不安全、Vector安全、HashMap不安全,HashTable安全,并行改成串行,效率很低,有时复合操作还存在问题
1、两者都实现了Map集合类的接口,但是HashMap是线程不安全的,没有synchronized;但是,Hashtable是线程安全的,它有synchronized。但是,在线程问题上,值得注意的是: HashTable 虽然有synchronized,是线程安全的,但是在多线程下,如果对于对象的竞争比较的激烈的时候,HashTable的效率是比较低的。因此,如果当前程序如果是单线程,那么HashMap的效率将会比HashTable的效率高一些。
1):那HashMap可不可以实现线程安全呢?A: 可以的 ,使用
2):有没有HashTable 的替代? A:CurrentHashMap!CurrentHashMap 就是解决HashMap线程不安全,和HashTable同步锁效率低的问题。
3): 那下面简单介绍一下CurrentHashMap:HashTable容器在竞争激烈的并发环境下表现出效率低下的原因,是因为所有访问HashTable的线程都必须竞争同一把锁,那假如容器里有多把锁,每一把锁用于锁住容器中的一部分数据,那么当多线程访问容器里不同数据段的数据时,线程间就不会存在锁竞争,从而可以有效的提高并发访问效率,这就是ConcurrentHashMap所使用的锁分段技术,首先将数据分成一段一段的存储,然后给每一段数据配一把锁,当一个线程占用锁访问其中一个段数据时,其他段的数据也能被其他线程访问。
2、在接收的值得方面,HashMap是接收空值的,而HashTable是不接收空值的。
concurrentHashMap采用锁分段机制,concurrentLevel默认为16段segment
//--------List
List<String> list = new Vector<>();
List<String> list = Collections.synchronizedList(new ArrayList<>());
List<String> list = Collections.synchronizedList(new LinkedList<String>());
= new ConcurrentLinkedQueue();
ConcurrentLinkedQueue或LinkedBlockingDeque
List<String> list = new CopyOnWriteArrayList<>();//写时复制
//---------Set
Set<String> set = Collections.synchronizedSet(new HashSet<>());
Set<String> set = new CopyOnWriteArraySet<>();
//---------map
Map<String, String> map = new ConcurrentHashMap<>();
collection.synchronizedMap(hashmap)
写时复制是读写分离的一种表现
package com.kuang.unsafe;
import java.util.*;
import java.util.concurrent.CopyOnWriteArrayList;
// java.util.ConcurrentModificationException 并发修改异常!
public class ListTest {
public static void main(String[] args) {
// 并发下 ArrayList 不安全的吗,Synchronized;
/**
* 解决方案;
* 1、List<String> list = new Vector<>();
* 2、List<String> list = Collections.synchronizedList(new ArrayList<>());
* 3、List<String> list = new CopyOnWriteArrayList<>();
*/
// CopyOnWrite 写入时复制 COW 计算机程序设计领域的一种优化策略;
// 多个线程调用的时候,list,读取的时候,固定的,写入(覆盖)
// 在写入的时候避免覆盖,造成数据问题!
// 读写分离
// CopyOnWriteArrayList 比 Vector Nb 在哪里?
//ArrayList会造成线程不安全
List<String> list = new CopyOnWriteArrayList<>();
for (int i = 1; i <= 10; i++) {
new Thread(()->{
list.add(UUID.randomUUID().toString().substring(0,5));
System.out.println(list);
},String.valueOf(i)).start();
}
}
}
/*
[53d43, c85db, d0c6c, 430b4]
[53d43, c85db, d0c6c, 430b4, c5815, 63888, 29856]
[53d43, c85db, d0c6c, 430b4, c5815, 63888, 29856, 99932, 24a4e]
[53d43, c85db, d0c6c, 430b4, c5815, 63888]
[53d43, c85db, d0c6c, 430b4, c5815]
[53d43, c85db, d0c6c, 430b4, c5815, 63888]
[53d43, c85db, d0c6c, 430b4]
[53d43, c85db, d0c6c, 430b4]
[53d43, c85db, d0c6c, 430b4, c5815, 63888, 29856, 99932, 24a4e, 88374]
[53d43, c85db, d0c6c, 430b4, c5815, 63888, 29856, 99932]
*/
list底层初始10个,扩容1.5倍,15/2=7
hashmap初始16,扩容2倍
hashmap也不安全
package com.kuang.unsafe;
import java.util.Collections;
import java.util.HashSet;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.CopyOnWriteArraySet;
/**
* 同理可证 : ConcurrentModificationException
* //1、Set<String> set = Collections.synchronizedSet(new HashSet<>());
* //2、
*/
public class SetTest {
public static void main(String[] args) {
Set<String> set = new HashSet<>();
// hashmap
// Set<String> set = Collections.synchronizedSet(new HashSet<>());
// Set<String> set = new CopyOnWriteArraySet<>();
for (int i = 1; i <=30 ; i++) {
new Thread(()->{
set.add(UUID.randomUUID().toString().substring(0,5));
System.out.println(set);
},String.valueOf(i)).start();
}
}
}
ConcurrentHashMap<>()
从类图中可以看出来在存储结构中ConcurrentHashMap比HashMap多出了一个类Segment,而Segment是一个可重入锁。
ConcurrentHashMap是使用了锁分段技术技术来保证线程安全的。
锁分段技术:首先将数据分成一段一段的存储,然后给每一段数据配一把锁,当一个线程占用锁访问其中一个段数据的时候,其他段的数据也能被其他线程访问。
属性说明:
我们会发现HashMap和Segment里的属性值基本是一样的,因为Segment的本质上就是一个加锁的HashMap,下面是每个属性的意义:
- table:数据存储区
- size,count: 已存数据的大小
- threshold:table需要扩容的临界值,等于table的大小*loadFactor
- loadFactor: 装载因子
- modCount: table结构别修改的次数
hash算法和table数组长度:
仔细阅读HashMap的构造方法的话,会发现他做了一个操作保证table数组的大小是2的n次方。
如果使用new HashMap(10)新建一个HashMap,你会发现这个HashMap中table数组实际的大小是16,并不是10.
为什么要这么做呢?这就要从HashMap里的hash和indexFor方法开始说了。
//JDK1.7之前
static int hash(int h) {
// This function ensures that hashCodes that differ only by
// constant multiples at each bit position have a bounded
// number of collisions (approximately 8 at default load factor).
h ^= (h >>> 20) ^ (h >>> 12);
return h ^ (h >>> 7) ^ (h >>> 4);
}
/**
* Returns index for hash code h.
*/
static int indexFor(int h, int length) {
return h & (length-1);
}
int hash = hash(key.hashCode());
int i = indexFor(hash, table.length);
HashMap里的put和get方法都使用了这两个方法将key散列到table数组上去。
indexFor方法是通过hash值和table数组的长度-1进行于操作,来确定具体的位置。
为什么要减1呢?因为数组的长度是2的n次方,减1以后就变成低位的二进制码都是1,和hash值做与运算的话,就能得到一个小于数组长度的数了。
那为什么对hashCode还要做一次hash操作呢?因为如果不做hash操作的话,只有低位的值参与了hash的运算,而高位的值没有参加运算。hash方法是让高位的数字也参加hash运算。
假如:数组的长度是16 我们会发现hashcode为5和53的散列到同一个位置.
hashcode:53 00000000 00000000 00000000 00110101
hashcode:5 00000000 00000000 00000000 00000101
length-1:15 00000000 00000000 00000000 00001111
只要hashcode值的最后4位是一样的,那么他们就会散列到同一个位置。
hash方法是通过一些位运算符,让高位的数值也尽可能的参加到运算中,让它尽可能的散列到table数组上,减少hash冲突。
ConcurrentHashMap的初始化:
仔细阅读ConcurrentHashMap的构造方法的话,会发现是由initialCapacity,loadFactor, concurrencyLevel几个参数来初始化segments数组的。
- segmentShift和segmentMask是在定位segment时的哈希算法里需要使用的,让其能够尽可能的散列开。
- initialCapacity:ConcurrentHashMap的初始大小
- loadFactor:装载因子
- concurrencyLevel:预想的并发级别,为了能够更好的hash,也保证了concurrencyLevel的值是2的n次方
segements数组的大小为concurrencyLevel,每个Segement内table的大小为initialCapacity/ concurrencyLevel
ConcurrentHashMap的put和get
int hash = hash(key.hashCode());
return segmentFor(hash).get(key, hash);
可以发现ConcurrentHashMap通过一次hash,两次定位来找到具体的值的。
先通过segmentFor方法定位到具体的Segment,再在Segment内部定位到具体的HashEntry,而第二次在Segment内部定位的时候是加锁的。
ConcurrentHashMap的hash算法比HashMap的hash算法更复杂,应该是想让他能够更好的散列到数组上,减少hash冲突。
HashMap和Segment里modCount的区别:
modCount都是记录table结构被修改的次数,但是对这个次数的处理上,HashMap和Segment是不一样的。
HashMap在遍历数据的时候,会判断modCount是否被修改了,如果被修改的话会抛出ConcurrentModificationException异常。
Segment的modCount在ConcurrentHashMap的containsValue、isEmpty、size方法中用到,ConcurrentHashMap先在不加锁的情况下去做这些计算,如果发现有Segment的modCount被修改了,会再重新获取锁计算。
HashMap和ConcurrentHashMap的区别:
如果仔细阅读他们的源码,就会发现HashMap是允许插入key和value是null的数据的,而ConcurrentHashMap是不允许key和value是null的。这个是为什么呢?ConcurrentHashMap的作者是这么说的:
The main reason that nulls aren’t allowed in ConcurrentMaps (ConcurrentHashMaps, ConcurrentSkipListMaps) is that ambiguities that may be just barely tolerable in non-concurrent maps can’t be accommodated. The main one is that if map.get(key) returns null, you can’t detect whether the key explicitly maps to null vs the key isn’t mapped. In a non-concurrent map, you can check this via map.contains(key), but in a concurrent one, the map might have changed between calls.
为什么重写了equals方法就必须重写hashCode方法呢?
绝大多数人都知道如果要把一个对象当作key使用的话,就需要重写equals方法。重写了equals方法的话,就必须重写hashCode方法,否则会出现不正确的结果。那么为什么不重写hashCode方法就会出现不正确结果了呢?这个问题只要仔细阅读一下HashMap的put方法,看看它是如何确定一个key是否已存在的就明白了。关键代码:
int hash = hash(key.hashCode());
int i = indexFor(hash, table.length);
for (Entry<K,V> e = table[i]; e != null; e = e.next) {
Object k;
if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
V oldValue = e.value;
e.value = value;
e.recordAccess(this);
return oldValue;
}
}
首先通过key的hashCode来确定具体散列到table的位置,如果这个位置已经有值的话,再通过equals方法判断key是否相等。
如果只重写equals方法而不重写hashCode方法的话,即使这两个对象通过equals方法判断是相等的,但是因为没有重写hashCode方法,他们的hashCode是不一样的,这样就会被散列到不同的位置去,变成错误的结果了。所以hashCode和equals方法必须一起重写。
package com.kuang.unsafe;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
// ConcurrentModificationException
public class MapTest {
public static void main(String[] args) {
// map 是这样用的吗? 不是,工作中不用 HashMap
// 默认等价于什么? new HashMap<>(16,0.75);
// Map<String, String> map = new HashMap<>();
// 唯一的一个家庭作业:研究ConcurrentHashMap的原理
Map<String, String> map = new ConcurrentHashMap<>();
for (int i = 1; i <=30; i++) {
new Thread(()->{
map.put(Thread.currentThread().getName(),UUID.randomUUID().toString().substring(0,5));
System.out.println(map);
},String.valueOf(i)).start();
}
}
}
10、常用的辅助类
10.1、CountDownLatch
基础使用:
- 创建:CountDownLatch countDownLatch = new CountDownLatch(6);//填入数字
- countDownLatch.countDown(); // 数量-1
- countDownLatch.await(); // 等待计数器归零,然后再向下执行
每次有线程调用 countDown() 数量-1,假设计数器变为0,countDownLatch.await() 就会被唤醒,继续
执行!
package com.kuang.add;
import java.util.concurrent.CountDownLatch;
// 计数器
public class CountDownLatchDemo {
public static void main(String[] args) throws InterruptedException {
// 总数是6,必须要执行任务的时候,再使用!
CountDownLatch countDownLatch = new CountDownLatch(6);
for (int i = 1; i <=6 ; i++) {
new Thread(()->{
System.out.println(Thread.currentThread().getName()+" Go out");
countDownLatch.countDown(); // 数量-1
},String.valueOf(i)).start();
}
countDownLatch.await(); // 等待计数器归零,然后再向下执行
System.out.println("Close Door");
}
}
10.2、CyclicBarrier
加法计数器。从字面上的意思可以知道,这个类的中文意思是“循环栅栏”。大概的意思就是一个可循环利用的屏障。它的作用就是会让所有线程都等待完成后才会继续下一步行动。
基础用法:
-
创建任务
CyclicBarrier cyclicBarrier = new CyclicBarrier(8,()->{ System.out.println("召唤神龙成功!"); });
-
cyclicBarrier.await(); // 等待。表示自己已经到达栅栏。调用await方法的线程告诉CyclicBarrier自己已经到达同步点,然后当前线程被阻塞。直到parties个参与线程调用了await方法
CountDownLatch 是一次性的,CyclicBarrier 是可循环利用的。
CountDownLatch 参与的线程的职责是不一样的,有的在倒计时,有的在等待倒计时结束。CyclicBarrier 参与的线程职责是一样的。
package com.kuang.add;
import java.util.concurrent.BrokenBarrierException;
import java.util.concurrent.CyclicBarrier;
public class CyclicBarrierDemo {
public static void main(String[] args) {
/**
* 集齐7颗龙珠召唤神龙
*/
// 召唤龙珠的线程
CyclicBarrier cyclicBarrier = new CyclicBarrier(8,()->{
System.out.println("召唤神龙成功!");
});
for (int i = 1; i <=7 ; i++) {
final int temp = i;
// lambda能操作到 i 吗
new Thread(()->{
System.out.println(Thread.currentThread().getName()+"收集"+temp+"个龙珠");
try {
cyclicBarrier.await(); // 等待
} catch (InterruptedException e) {
e.printStackTrace();
} catch (BrokenBarrierException e) {
e.printStackTrace();
}
}).start();
}
}
}
10.3、Semaphore
信号量: 多个共享资源互斥的使用!并发限流,控制最大的线程数!
- Semaphore semaphore = new Semaphore(3);//new
- semaphore.acquire() 获得,假设如果已经满了,等待,等待被释放为止!
- semaphore.release(); 释放,会将当前的信号量释放 + 1,然后唤醒等待的线程!
package com.kuang.add;
import java.util.concurrent.Semaphore;
import java.util.concurrent.TimeUnit;
public class SemaphoreDemo {
public static void main(String[] args) {
// 线程数量:停车位! 限流!
Semaphore semaphore = new Semaphore(3);//new
for (int i = 1; i <=6 ; i++) {
new Thread(()->{
try {
semaphore.acquire();// acquire() 得到
System.out.println(Thread.currentThread().getName()+"抢到车位");
TimeUnit.SECONDS.sleep(2);
System.out.println(Thread.currentThread().getName()+"离开车位");
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
semaphore.release(); // release() 释放
}
},String.valueOf(i)).start();
}
}
}
10.4、ForkJoin分合
ForkJoin 在 JDK 1.7 , 并行执行任务!提高效率。大数据量!
大数据:Map Reduce (把大任务拆分为小任务,把小任务得到的结果合并)
特点:工作窃取,这个小任务执行完了就拿过来别的小任务线程上正在执行的,因为里面维护的是双端队列,所以两端都可以操作
import java.util.concurrent.RecursiveTask;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ForkJoinPool;
import java.util.concurrent.ForkJoinTask;
递归任务有返回值,递归事件没有返回值
-
public class ForkJoinDemo extends RecursiveTask<Long> { compute();} ForkJoinPool forkJoinPool = new ForkJoinPool();//创建池,把任务提交到池中 ForkJoinTask<Long> task = new ForkJoinDemo(0L, 10_0000_0000L); ForkJoinTask<Long> submit = forkJoinPool.submit(task);// 提交任务 Long sum = submit.get();
package com.kuang.forkjoin;
import java.util.concurrent.RecursiveTask;
/**
* 求和计算的任务!
* 3000 6000(ForkJoin) 9000(Stream并行流)
* // 如何使用 forkjoin
* // 1、forkjoinPool 通过它来执行
* // 2、计算任务 forkjoinPool.execute(ForkJoinTask task)
* // 3. 计算类要继承 ForkJoinTask
*/
public class ForkJoinDemo extends RecursiveTask<Long> {
//求和计算
private Long start; // 1
private Long end; // 1990900000
// 临界值
private Long temp = 10000L;
public ForkJoinDemo(Long start, Long end) {
this.start = start;
this.end = end;
}
// 计算方法
@Override
protected Long compute() {
if ((end-start)<temp){
//数太多就走forkjoin,否则走普通方法
Long sum = 0L;
for (Long i = start; i <= end; i++) {
sum += i;
}
return sum;
}else {
// forkjoin 递归
long middle = (start + end) / 2; // 中间值
ForkJoinDemo task1 = new ForkJoinDemo(start, middle);
task1.fork(); // 拆分任务,把任务压入线程队列
ForkJoinDemo task2 = new ForkJoinDemo(middle+1, end);
task2.fork(); // 拆分任务,把任务压入线程队列
return task1.join() + task2.join();
}
}
}
package com.kuang.forkjoin;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ForkJoinPool;
import java.util.concurrent.ForkJoinTask;
import java.util.stream.DoubleStream;
import java.util.stream.IntStream;
import java.util.stream.LongStream;
/**
* 同一个任务,别人效率高你几十倍!
*/
public class Test {
public static void main(String[] args) throws ExecutionException, InterruptedException {
// test1(); // 12224
// test2(); // 10038
// test3(); // 153
}
// 普通程序员
public static void test1(){
Long sum = 0L;
long start = System.currentTimeMillis();
for (Long i = 1L; i <= 10_0000_0000; i++) {
sum += i;
}
long end = System.currentTimeMillis();
System.out.println("sum="+sum+" 时间:"+(end-start));
}
// 会使用ForkJoin
public static void test2() throws ExecutionException, InterruptedException {
long start = System.currentTimeMillis();
ForkJoinPool forkJoinPool = new ForkJoinPool();
ForkJoinTask<Long> task = new ForkJoinDemo(0L, 10_0000_0000L);
ForkJoinTask<Long> submit = forkJoinPool.submit(task);// 提交任务
Long sum = submit.get();
long end = System.currentTimeMillis();
System.out.println("sum="+sum+" 时间:"+(end-start));
}
public static void test3(){
long start = System.currentTimeMillis();
// Stream并行流 () (]
long sum = LongStream.rangeClosed(0L, 10_0000_0000L).parallel().reduce(0, Long::sum);
long end = System.currentTimeMillis();
System.out.println("sum="+"时间:"+(end-start));
}
}
11、读写锁 ReentrantReadWriteLock
之前的ReentrantLock叫做排它锁
ReentrantReadWriteLock适用于读多写少的情况,他实现了ReadWriteLock 接口,这个接口有两个发readLock()/writeLock()
基础使用:
- ReadWriteLock readWriteLock = new ReentrantReadWriteLock();
- //-----写锁-----
- readWriteLock.writeLock().lock();//写锁
- readWriteLock.writeLock().unlock();//写锁解锁
- // -----读锁------
- readWriteLock.readLock().lock();//读锁
- readWriteLock.readLock().unlock();//读锁解锁
解释:
- 读-读 可以共存!
- 读-写 不能共存!
- 写-写 不能共存!
- 总结:只要有写就不共存
package com.kuang.rw;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
/**
* 独占锁(写锁) 一次只能被一个线程占有
* 共享锁(读锁) 多个线程可以同时占有
* ReadWriteLock
* 读-读 可以共存!
* 读-写 不能共存!
* 写-写 不能共存!
*/
public class ReadWriteLockDemo {
public static void main(String[] args) {
MyCache myCache = new MyCache();
// 写入
for (int i = 1; i <= 5 ; i++) {
final int temp = i;
new Thread(()->{
myCache.put(temp+"",temp+"");
},String.valueOf(i)).start();
}
// 读取
for (int i = 1; i <= 5 ; i++) {
final int temp = i;
new Thread(()->{
myCache.get(temp+"");
},String.valueOf(i)).start();
}
}
}
// 加锁的
class MyCacheLock{
private volatile Map<String,Object> map = new HashMap<>();
// 读写锁: 更加细粒度的控制
private ReadWriteLock readWriteLock = new ReentrantReadWriteLock();
//private Lock lock = new ReentrantLock();
// 存,写入的时候,只希望同时只有一个线程写
public void put(String key,Object value){
readWriteLock.writeLock().lock();//写锁
try {
System.out.println(Thread.currentThread().getName()+"写入"+key);
map.put(key,value);
System.out.println(Thread.currentThread().getName()+"写入OK");
} catch (Exception e) {
e.printStackTrace();
} finally {
readWriteLock.writeLock().unlock();//写锁解锁
}
}
// 取,读,所有人都可以读!
public void get(String key){
readWriteLock.readLock().lock();//读锁
try {
System.out.println(Thread.currentThread().getName()+"读取"+key);
Object o = map.get(key);
System.out.println(Thread.currentThread().getName()+"读取OK");
} catch (Exception e) {
e.printStackTrace();
} finally {
readWriteLock.readLock().unlock();//读锁解锁
}
}
}
/** 自定义缓存 */
class MyCache{
private volatile Map<String,Object> map = new HashMap<>();
// 存,写
public void put(String key,Object value){
System.out.println(Thread.currentThread().getName()+"写入"+key);
map.put(key,value);
System.out.println(Thread.currentThread().getName()+"写入OK");
}
// 取,读
public void get(String key){
System.out.println(Thread.currentThread().getName()+"读取"+key);
Object o = map.get(key);
System.out.println(Thread.currentThread().getName()+"读取OK");
}
}
12、阻塞队列与同步队列
阻塞队列
使用:
- ArrayBlockingQueue blockingQueue = new ArrayBlockingQueue<>(3);//创建阻塞对象
- blockingQueue.add();//同理还可以调用下面的方法
如add操作,队列大小不够大时就抛异常或者返回bool值
方式 | 抛出异常 | 有返回值,不抛出异常 | 阻塞 等待 | 超时等待 |
---|---|---|---|---|
添加 | add | offer():false | put() | offer(,) |
移除 | remove | poll():null | take() | poll(,) |
检测队首元素 | element | peek | - | - |
package com.kuang.bq;
import java.util.Collection;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.TimeUnit;
public class Test {
public static void main(String[] args) throws InterruptedException {
test4();
}
/**
* 抛出异常
*/
public static void test1(){
// 队列的大小
ArrayBlockingQueue blockingQueue = new ArrayBlockingQueue<>(3);
System.out.println(blockingQueue.add("a"));//add
System.out.println(blockingQueue.add("b"));
System.out.println(blockingQueue.add("c"));
// IllegalStateException: Queue full 抛出异常!
// System.out.println(blockingQueue.add("d"));
//队列满了,放不下了,add就会抛出异常
System.out.println("=-===========");
System.out.println(blockingQueue.element()); // 查看队首元素是谁
System.out.println(blockingQueue.remove());
System.out.println(blockingQueue.remove());
System.out.println(blockingQueue.remove());
// java.util.NoSuchElementException 抛出异常!
// System.out.println(blockingQueue.remove());
}
/**
* 有返回值,没有异常
*/
public static void test2(){
// 队列的大小 3
ArrayBlockingQueue blockingQueue = new ArrayBlockingQueue<>(3);
System.out.println(blockingQueue.offer("a"));//offer
System.out.println(blockingQueue.offer("b"));
System.out.println(blockingQueue.offer("c"));
System.out.println(blockingQueue.peek());
// System.out.println(blockingQueue.offer("d")); // false 不抛出异常! //没有添加公共,返回一个false,不报错
System.out.println("============================");
System.out.println(blockingQueue.poll());
System.out.println(blockingQueue.poll());
System.out.println(blockingQueue.poll());
System.out.println(blockingQueue.poll()); // null 不抛出异常!
}
/**
* 等待,阻塞(一直阻塞)
*/
public static void test3() throws InterruptedException {
// 队列的大小
ArrayBlockingQueue blockingQueue = new ArrayBlockingQueue<>(3);
// 一直阻塞
blockingQueue.put("a");//put
blockingQueue.put("b");
blockingQueue.put("c");
// blockingQueue.put("d"); // 队列没有位置了,一直阻塞
System.out.println(blockingQueue.take());
System.out.println(blockingQueue.take());
System.out.println(blockingQueue.take());
System.out.println(blockingQueue.take()); // 没有这个元素,一直阻塞
}
/**
* 等待,阻塞(等待超时)
*/
public static void test4() throws InterruptedException {
// 队列的大小
ArrayBlockingQueue blockingQueue = new ArrayBlockingQueue<>(3);
blockingQueue.offer("a");
blockingQueue.offer("b");
blockingQueue.offer("c");
// blockingQueue.offer("d",2,TimeUnit.SECONDS); // 等待超过2秒就退出
System.out.println("===============");
System.out.println(blockingQueue.poll());
System.out.println(blockingQueue.poll());
System.out.println(blockingQueue.poll());
blockingQueue.poll(2,TimeUnit.SECONDS); // 等待超过2秒就退出
}
}
同步队列
SynchronousQueue:没有容量,进去一个元素,必须当代取出来之后,才能再往里面放一个元素
-
- BlockingQueue<String> blockingQueue = new SynchronousQueue<>(); // 同步队列 - blockingQueue.put("1");//调用的方法与阻塞队列的道理同 - blockingQueue.take();
put take
package com.kuang.bq;
import java.sql.Time;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.SynchronousQueue;
import java.util.concurrent.TimeUnit;
/**
* 同步队列
* 和其他的BlockingQueue 不一样, SynchronousQueue 不存储元素
* put了一个元素,必须从里面先take取出来,否则不能在put进去值!
*/
public class SynchronousQueueDemo {
public static void main(String[] args) {
BlockingQueue<String> blockingQueue = new SynchronousQueue<>(); // 同步队列
new Thread(()->{
try {
System.out.println(Thread.currentThread().getName()+" put 1");
blockingQueue.put("1");
System.out.println(Thread.currentThread().getName()+" put 2");
blockingQueue.put("2");
System.out.println(Thread.currentThread().getName()+" put 3");
blockingQueue.put("3");
} catch (InterruptedException e) {
e.printStackTrace();
}
},"T1").start();
new Thread(()->{
try {
TimeUnit.SECONDS.sleep(3);
System.out.println(Thread.currentThread().getName()+"=>"+blockingQueue.take());
TimeUnit.SECONDS.sleep(3);
System.out.println(Thread.currentThread().getName()+"=>"+blockingQueue.take());
TimeUnit.SECONDS.sleep(3);
System.out.println(Thread.currentThread().getName()+"=>"+blockingQueue.take());
} catch (InterruptedException e) {
e.printStackTrace();
}
},"T2").start();
}
}
13、线程池
参考:
https://blog.csdn.net/hancoder/article/details/105740288
线程池:三大方法、7大参数、4种拒绝策略
池化技术
程序的运行,本质:占用系统的资源! 优化资源的使用!=>池化技术
线程池、连接池、内存池、对象池…。 线程创建、销毁。十分浪费资源
池化技术:事先准备好一些资源,有人要用,就来我这里拿,用完之后还给我。
线程池的好处:
1、降低资源的消耗
2、提高响应的速度
3、方便管理。
线程复用、可以控制最大并发数、管理线程
线程池:三大方法
- new ThreadPoolExecutor.AbortPolicy() // 银行满了,还有人进来,不处理这个人的,抛出异常
- new ThreadPoolExecutor.CallerRunsPolicy() // 哪来的去哪里!
- new ThreadPoolExecutor.DiscardPolicy() //队列满了,丢掉任务,不会抛出异常!
- new ThreadPoolExecutor.DiscardOldestPolicy() //队列满了,尝试去和最早的竞争,也不会抛出异常!
ExecutorService 子接口:线程池的主要接口
ThreadPoolExecutor 实现类 //
ScheduledExecutorService 子接口:负责线程的调度
ScheduledThreadPoolExecutor三级实现类
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.*;
// Executors 工具类、3大方法
/**
* new ThreadPoolExecutor.AbortPolicy() // 银行满了,还有人进来,不处理这个人的,抛出异常
* new ThreadPoolExecutor.CallerRunsPolicy() // 哪来的去哪里!
* new ThreadPoolExecutor.DiscardPolicy() //队列满了,丢掉任务,不会抛出异常!
* new ThreadPoolExecutor.DiscardOldestPolicy() //队列满了,尝试去和最早的竞争,也不会抛出异常!
*/
public class Demo01 {
public static void main(String[] args) {
// 自定义线程池!工作 ThreadPoolExecutor
// 最大线程到底该如何定义?
// 1、CPU 密集型,几核,就是几,可以保持CPu的效率最高!
// 2、IO 密集型 >判断你程序中十分耗IO的线程,
// 程序 15个大型任务 io十分占用资源!
// 获取CPU的核数
System.out.println(Runtime.getRuntime().availableProcessors());
List list = new ArrayList();
//ExecutorService threadPool=ExecutorService.newSingleThreadExecutor()
ExecutorService threadPool = new ThreadPoolExecutor(
2,
Runtime.getRuntime().availableProcessors(),
3,
TimeUnit.SECONDS,
new LinkedBlockingDeque<>(3),
Executors.defaultThreadFactory(),
new ThreadPoolExecutor.DiscardOldestPolicy()); //队列满了,尝试去和最早的竞争,也不会抛出异常!
try {
// 最大承载:Deque + max
// 超过 RejectedExecutionException
for (int i = 1; i <= 9; i++) {
// 使用了线程池之后,使用线程池来创建线程
threadPool.execute(()->{
System.out.println(Thread.currentThread().getName()+" ok");
});
}
} catch (Exception e) {
e.printStackTrace();
} finally {
// 线程池用完,程序结束,关闭线程池
threadPool.shutdown();
}
}
}
7大参数
源码分析
public static ExecutorService newSingleThreadExecutor() {
return new FinalizableDelegatedExecutorService
(new ThreadPoolExecutor(1, 1,0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>()));
}
public static ExecutorService newFixedThreadPool(int nThreads) {
return new ThreadPoolExecutor(5, 5,0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>());
}
public static ExecutorService newCachedThreadPool() {
return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
60L, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>());
} // 本质ThreadPoolExecutor()
public ThreadPoolExecutor(int corePoolSize, // 核心线程池大小
int maximumPoolSize, // 最大核心线程池大小
long keepAliveTime, // 超时了没有人调用就会释放
TimeUnit unit, // 超时单位
BlockingQueue<Runnable> workQueue, // 阻塞队列
ThreadFactory threadFactory, // 线程工厂:创建线程的,一般不用动
RejectedExecutionHandler handle) {
// 拒绝策略
if (corePoolSize < 0 ||
maximumPoolSize <= 0 ||
maximumPoolSize < corePoolSize ||
keepAliveTime < 0)
throw new IllegalArgumentException();
if (workQueue == null || threadFactory == null || handler == null)
throw new NullPointerException();
this.acc = System.getSecurityManager() == null ?
null :
AccessController.getContext();
this.corePoolSize = corePoolSize;
this.maximumPoolSize = maximumPoolSize;
this.workQueue = workQueue;
this.keepAliveTime = unit.toNanos(keepAliveTime);
this.threadFactory = threadFactory;
this.handler = handler;
}
//一般只开部分窗口,候客区满了才继续开窗口。候客区即阻塞队列。同样窗口超过时间没人就会关闭
上面demo1的代码
4种拒绝策略
new ThreadPoolExecutor.AbortPolicy() // 银行满了,还有人进来,不处理这个人的,抛出异常
new ThreadPoolExecutor.CallerRunsPolicy() // 哪来的去哪里! 比如交给main线程处理
new ThreadPoolExecutor.DiscardPolicy() //队列满了,丢掉任务,不会抛出异常!
new ThreadPoolExecutor.DiscardOldestPolicy() //队列满了,尝试去和最早的竞争,也不会抛出异常!
池的大小设置
看上面demo1
最大承载=池大小+队列大小
14、四大函数式接口(必须掌握)
新时代的程序员:lambda表达式、链式编程、函数式接口、Stream流式计算
java.util.function.Function、
java.util.function.Predicate、
java.util.function.Consumer、
java.util.function.Supplier
- 函数式接口
new Function<String,String>()
:一个输入,一个输出 - 断定式接口
Predicate<String>,test()
:一个输入,一个输出bool - 消费型接口
Consumer<String>,accept()
:只有输入,没有返回 - 供给型接口
Supplier<Integer>(),get()
:没有输入,只有返回
函数式接口: 只有一个抽象方法的接口
@FunctionalInterface
public interface Runnable {
public abstract void run();
} // 泛型、枚举、反射
// lambda表达式、链式编程、函数式接口、Stream流式计算
// 超级多FunctionalInterface
// 简化编程模型,在新版本的框架底层大量应用!
// foreach(消费者类的函数式接口)
接口有且仅有一个抽象方法
允许定义静态方法
允许定义默认方法
允许java.lang.Object中的public方法
该注解不是必须的,如果一个接口符合"函数式接口"定义,那么加不加该注解都没有影响。加上该注解能够更好地让编译器进行检查。如果编写的不是函数式接口,但是加上了@FunctionInterface,那么编译器会报错
package com.kuang.function;
import java.util.function.Function;
/**
* Function 函数型接口, 有一个输入参数,有一个输出
* 只要是 函数型接口 可以 用 lambda表达式简化
*/
public class Demo01 {
public static void main(String[] args) {
//
// Function<String,String> function = new Function<String,String>() {
// @Override
// public String apply(String str) {
// return str;
// }
// };
//lambda格式
Function<String,String> function = (str)->{
return str;};
System.out.println(function.apply("asd"));
}
}
断定型接口:有一个输入参数,返回值只能是 布尔值!
package com.kuang.function;
import java.util.function.Predicate;
/**
* 断定型接口:有一个输入参数,返回值只能是 布尔值!
*/
public class Demo02 {
public static void main(String[] args) {
// 判断字符串是否为空
// Predicate<String> predicate = new Predicate<String>(){
@Override
public boolean test(String str) {
return str.isEmpty();
}
};
Predicate<String> predicate = (str)->{
return str.isEmpty(); };
System.out.println(predicate.test(""));//方法
}
}
package com.kuang.function;
import java.util.function.Consumer;
/**
* Consumer 消费型接口: 只有输入,没有返回值
*/
public class Demo03 {
public static void main(String[] args) {
// Consumer<String> consumer = new Consumer<String>() {
// @Override
// public void accept(String str) {
// System.out.println(str);
// }
// };
Consumer<String> consumer = (str)->{
System.out.println(str);};
consumer.accept("sdadasd");
}
}
package com.kuang.function;
import java.util.function.Supplier;
/**
* Supplier 供给型接口 没有参数,只有返回值
*/
public class Demo04 {
public static void main(String[] args) {
// Supplier supplier = new Supplier<Integer>() {
// @Override
// public Integer get() {
// System.out.println("get()");
// return 1024;
// }
// };
Supplier supplier = ()->{
return 1024; };
System.out.println(supplier.get());
}
}
15、异步回调
对将来某个事件进行建模
在等第一个任务的结果时候可以先去执行第二个任务
package com.kuang.future;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
/**
* 异步调用: CompletableFuture
* // 异步执行
* // 成功回调
* // 失败回调
*/
public class Demo01 {
public static void main(String[] args) throws ExecutionException, InterruptedException {
// 没有返回值的 runAsync 异步回调
// CompletableFuture<Void> completableFuture = CompletableFuture.runAsync(()->{
// try {
// TimeUnit.SECONDS.sleep(2);
// } catch (InterruptedException e) {
// e.printStackTrace();
// }
// System.out.println(Thread.currentThread().getName()+"runAsync=>Void");
// });
//
// System.out.println("1111");
//
// completableFuture.get(); // 获取阻塞执行结果
// 有返回值的 supplyAsync 异步回调
// ajax,成功和失败的回调
// 返回的是错误信息;
CompletableFuture<Integer> completableFuture = CompletableFuture.supplyAsync(()->{
System.out.println(Thread.currentThread().getName()+"supplyAsync=>Integer");
int i = 10/0;
return 1024;
});
System.out.println(completableFuture.whenComplete((t, u) -> {
System.out.println("t=>" + t); // 正常的返回结果
System.out.println("u=>" + u); // 错误信息:java.util.concurrent.CompletionException: java.lang.ArithmeticException: / by zero
}).exceptionally((e) -> {
System.out.println(e.getMessage());
return 233; // 可以获取到错误的返回结果
}).get());
/**
* succee Code 200
* error Code 404 500
*/
}
}
16、单例模式
package com.kuang.single;
// 饿汉式单例
public class Hungry {
// 可能会浪费空间
private byte[] data1 = new byte[1024*1024];
private byte[] data2 = new byte[1024*1024];
private byte[] data3 = new byte[1024*1024];
private byte[] data4 = new byte[1024*1024];
private Hungry(){
}
private final static Hungry HUNGRY = new Hungry();
public static Hungry getInstance(){
return HUNGRY;
}
}
package com.kuang.single;
import com.sun.corba.se.impl.orbutil.CorbaResourceUtil;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
// 懒汉式单例
public class LazyMan {
private static boolean qinjiang = false;
private LazyMan(){
synchronized (LazyMan.class){
if (qinjiang == false){
qinjiang = true;
}else {
throw new RuntimeException("不要试图使用反射破坏异常");
}
}
}
private volatile static LazyMan lazyMan;
// 双重检测锁模式的 懒汉式单例 DCL懒汉式
public static LazyMan getInstance(){
if (lazyMan==null){
synchronized (LazyMan.class){
if (lazyMan==null){
lazyMan = new LazyMan(); // 不是一个原子性操作
}
}
}
return lazyMan;
}
// 反射!
public static void main(String[] args) throws Exception {
// LazyMan instance = LazyMan.getInstance();
Field qinjiang = LazyMan.class.getDeclaredField("qinjiang");
qinjiang.setAccessible(true);
Constructor<LazyMan> declaredConstructor = LazyMan.class.getDeclaredConstructor(null);
declaredConstructor.setAccessible(true);
LazyMan instance = declaredConstructor.newInstance();
qinjiang.set(instance,false);
LazyMan instance2 = declaredConstructor.newInstance();
System.out.println(instance);
System.out.println(instance2);
}
}
/**
* 1. 分配内存空间
* 2、执行构造方法,初始化对象
* 3、把这个对象指向这个空间
*
* 123
* 132 A
* B // 此时lazyMan还没有完成构造
*/
局部静态类
package com.kuang.single;
// 静态内部类
public class Holder {
private Holder(){
}
public static Holder getInstace(){
return InnerClass.HOLDER;
}
public static class InnerClass{
private static final Holder HOLDER = new Holder();
}
}
枚举
package com.kuang.single;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
// enum 是一个什么? 本身也是一个Class类
public enum EnumSingle {
INSTANCE;
public EnumSingle getInstance(){
return INSTANCE;
}
}
class Test{
public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
EnumSingle instance1 = EnumSingle.INSTANCE;
Constructor<EnumSingle> declaredConstructor = EnumSingle.class.getDeclaredConstructor(String.class,int.class);
declaredConstructor.setAccessible(true);
EnumSingle instance2 = declaredConstructor.newInstance();
// NoSuchMethodException: com.kuang.single.EnumSingle.<init>()
System.out.println(instance1);
System.out.println(instance2);
}
}
17、Atomic与CAS
compared-and-swap
CAS算法是硬件对于并发操作共享数据的支持
- V内存值0
- A预估值0(第一次取的内存值)
- B更新值1
当且仅当V==A时,V=B,否则不进行任何操作
我认为位置 V 应该包含值 A;如果包含该值,则将 B 放到这个位置;否则,不要更改该位置,只告诉我这个位置现在的值即可。
JAVA1.5开始引入了CAS,主要代码都放在JUC的atomic包下:AtomicInteger、AtomicBoolean…
Java提供了一个Unsafe类,其内部方法操作可以像C的指针一样直接操作内存,方法都是native的。
为了让Java程序员能够受益于CAS等CPU指令,JDK并发包中有一个atomic包,它们是原子操作类,它们使用的是无锁的CAS操作,并且统统线程安全。atomic包下的几乎所有的类都使用了这个Unsafe类。
AtomicInteger
public class AtomicInteger extends Number implements java.io.Serializable {
private static final long serialVersionUID = 6214790243416807050L;
// 这个就是封装CAS操作的指针
private static final Unsafe unsafe = Unsafe.getUnsafe();
//原来内部的共享变量,就是这个value,并且使用volatile让其在多个线程之间可见
private volatile int value;
//初始化的构造函数
public AtomicInteger(int initialValue) {
value = initialValue;
}
//获取当前值
public final int get() {
return value;
}
//设置当前的共享变量的值
public final void set(int newValue) {
value = newValue;
}
//使用CAS操作设置新的值,并且返回旧的值
public final int getAndSet(int newValue) {
//使用指针unsafe类的三大原子操作方法之一
return unsafe.getAndSetInt(this, valueOffset, newValue);
}
//把expect与内部的value进行比较,如果相等,那么把value的值设置为update的值
public final boolean compareAndSet(int expect, int update) {
return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
}
//返回value,并把value + 1
public final int getAndIncrement() {
return unsafe.getAndAddInt(this, valueOffset, 1);
}
//自增,并且返回自增后的值 i++
public final int incrementAndGet() {
return unsafe.getAndAddInt(this, valueOffset, 1) + 1;
}
}
AtomicStampedReference(ABA问题、原子引用)
ABA问题:
在CAS的核心算法中,通过死循环不断获取最新的E。如果在此之间,V被修改了两次,但是最终值还是修改成了旧值V,这个时候,就不好判断这个共享变量是否已经被修改过。为了防止这种不当写入导致的不确定问题,原子操作类提供了一个带有时间戳的原子操作类。
带有时间戳的原子操作类AtomicStampedReference
当带有时间戳的原子操作类AtomicStampedReference对应的数值被修改时,除了更新数据本身外,还必须要更新时间戳。
当AtomicStampedReference设置对象值时,对象值以及时间戳都必须满足期望值,写入才会成功。因此,即使对象值被反复读写,写回原值,只要时间戳发生变化,就能防止不恰当的写入。
底层实现为: 通过Pair私有内部类存储数据和时间戳, 并构造volatile修饰的私有实例
接着看AtomicStampedReference类的compareAndSet()方法的实现:
同时对当前数据和当前时间进行比较,只有两者都相等是才会执行casPair()方法,
单从该方法的名称就可知是一个CAS方法,最终调用的还是Unsafe类中的compareAndSwapObject方法
到这我们就很清晰AtomicStampedReference的内部实现思想了,
通过一个键值对Pair存储数据和时间戳,在更新时对数据和时间戳进行比较,
只有两者都符合预期才会调用Unsafe的compareAndSwapObject方法执行数值和时间戳替换,也就避免了ABA的问题。
public class AtomicStampedReference<V> {
//通过一个volatile修饰的Pair对象
private volatile Pair<V> pair;
//嵌套类Pair技能存储对象引用,也存储了时间戳
private static class Pair<T> {
final T reference;
final int stamp;
private Pair(T reference, int stamp) {
this.reference = reference;
this.stamp = stamp;
}
public boolean compareAndSet(V expectedReference,
V newReference,
int expectedStamp,
int newStamp) {
Pair<V> current = pair;
return
expectedReference == current.reference &&
expectedStamp == current.stamp &&
((newReference == current.reference &&
newStamp == current.stamp) ||
casPair(current, Pair.of(newReference, newStamp)));
}
}
}
18、自旋锁
import java.util.concurrent.atomic.AtomicReference;
//自旋锁
public class SpinlockDemo {
// int 0
// Thread null
AtomicReference<Thread> atomicReference = new AtomicReference<>();
// 加锁
public void myLock(){
Thread thread = Thread.currentThread();
System.out.println(Thread.currentThread().getName() + "==> mylock");
// 自旋锁
while (!atomicReference.compareAndSet(null,thread)){
}
}
// 解锁
// 加锁
public void myUnLock(){
Thread thread = Thread.currentThread();
System.out.println(Thread.currentThread().getName() + "==> myUnlock");
atomicReference.compareAndSet(thread,null);
}
}
测试
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.ReentrantLock;
public class TestSpinLock {
public static void main(String[] args) throws InterruptedException {
// ReentrantLock reentrantLock = new ReentrantLock();
// reentrantLock.lock();
// reentrantLock.unlock();
// 底层使用的自旋锁CAS
SpinlockDemo lock = new SpinlockDemo();
new Thread(()-> {
lock.myLock();
try {
TimeUnit.SECONDS.sleep(5);
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.myUnLock();
}
},"T1").start();
TimeUnit.SECONDS.sleep(1);
new Thread(()-> {
lock.myLock();
try {
TimeUnit.SECONDS.sleep(1);
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.myUnLock();
}
},"T2").start();
}
}
19、Stream流式计算
什么是Stream流式计算
大数据:存储 + 计算
集合、MySQL 本质就是存储东西的;
计算都应该交给流来操作!
list.stream()
.filter(u->{
return u.getId()%2==0;})
.filter(u->{
return u.getAge()>23;})
.map(u->{
return u.getName().toUpperCase();})
.sorted((uu1,uu2)->{
return uu2.compareTo(uu1);})
.limit(1)
.forEach(System.out::println);
package com.kuang.stream;
import java.util.Arrays;
import java.util.List;
/**
* 题目要求:一分钟内完成此题,只能用一行代码实现!
* 现在有5个用户!筛选:
* 1、ID 必须是偶数
* 2、年龄必须大于23岁
* 3、用户名转为大写字母
* 4、用户名字母倒着排序
* 5、只输出一个用户!
*/
public class Test {
public static void main(String[] args) {
User u1 = new User(1,"a",21);
User u2 = new User(2,"b",22);
User u3 = new User(3,"c",23);
User u4 = new User(4,"d",24);
User u5 = new User(6,"e",25);
// 集合就是存储
List<User> list = Arrays.asList(u1, u2, u3, u4, u5);
// 计算交给Stream流
// lambda表达式、链式编程、函数式接口、Stream流式计算
list.stream()
.filter(u->{
return u.getId()%2==0;})
.filter(u->{
return u.getAge()>23;})
.map(u->{
return u.getName().toUpperCase();})
.sorted((uu1,uu2)->{
return uu2.compareTo(uu1);})
.limit(1)
.forEach(System.out::println);
}
}
package com.kuang.stream;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
// 有参,无参构造,get、set、toString方法!
@Data
@NoArgsConstructor
@AllArgsConstructor
public class User {
private int id;
private String name;
private int age;
}