保证可见性、防止重排序、不能保证原子性。
volatile关键字能够保证线程的可见性,当一个线程修改共享变量时,能够对另外一个线程保证可见,但是它不能够保证共享变量的原子性问题。
因为CPU在读取主内存共享变量的时候,效率非常低,所以每个CPU都会设置对应的高速缓存(L1、L2、L3),用来缓存共享变量主内存中的副本,而每个CPU对应共享变量的副本与副本之间可能会存在数据不一致性的问题。
补充:
多CPU:一个现代计算机通常由两个或者多个CPU。其中一些CPU还有多核。从这一点可以看出,在一个有两个或者多个CPU的现代计算机上同时运行多个线程是可能的。每个CPU在某一时刻运行一个线程是没有问题的。这意味着,如果你的Java程序是多线程的,在你的Java程序中每个CPU上一个线程可能同时(并发)执行。
CPU寄存器:每个CPU都包含一系列的寄存器,它们是CPU内内存的基础。CPU在寄存器上执行操作的速度远大于在主存上执行的速度。这是因为CPU访问寄存器的速度远大于主存。
高速缓存cache:由于计算机的存储设备与处理器的运算速度之间有着几个数量级的差距,所以现代计算机系统都不得不加入一层读写速度尽可能接近处理器运算速度的高速缓存(Cache)来作为内存与处理器之间的缓冲:将运算需要使用到的数据复制到缓存中,让运算能快速进行,当运算结束后再从缓存同步回内存之中,这样处理器就无须等待缓慢的内存读写了。CPU访问缓存层的速度快于访问主存的速度,但通常比访问内部寄存器的速度还要慢一点。每个CPU可能有一个CPU缓存层,一些CPU还有多层缓存。在某一时刻,一个或者多个缓存行(cache lines)可能被读到缓存,一个或者多个缓存行可能再被刷新回主存。
内存:一个计算机还包含一个主存。所有的CPU都可以访问主存。主存通常比CPU中的缓存大得多。
运作原理:通常情况下,当一个CPU需要读取主存时,它会将主存的部分读到CPU缓存中。它甚至可能将缓存中的部分内容读到它的内部寄存器中,然后在寄存器中执行操作。当CPU需要将结果写回到主存中去时,它会将内部寄存器的值刷新到缓存中,然后在某个时间点将值刷新回主存。
一个典型的CPU由运算器、控制器、寄存器等器件组成,这些器件靠内部总线相连。
Java 内存模型(Java Memory Model,JMM)就是一种符合内存模型规范的,屏蔽了各种硬件和操作系统的访问差异的,保证了Java程序在各种平台下对内存的访问都能保证效果一致的机制及规范。
主内存:存放共享变量数据;
工作内存:每个CPU对应共享变量的副本。
JMM有八大同步规范:
因为volatile关键字底层会通过汇编lock前缀指令触发底层锁的机制来解决多个不同CPU之间缓存数据同步的问题。
总线锁:当一个CPU(线程)访问到主内存中的数据时,会往总线发出一个Lock锁的信号,其他的线程不能对该主内存做任何操作,变为阻塞状态。该模式存在非常大的缺陷,会将并行的程序变为串行,没有真正发挥出CPU多核的好处。
MESI缓存一致性协议:
volatile关键字为了能够保证数据的可见性,会及时的将工作内存中的数据刷新到主内存中,导致其它工作内存的数据变为无效状态。比如在做count++的操作时,有的线程会出现操作丢失的情况。
因为CPU默认会以缓存行(大小是2的幂次方,默认为64字节)的形式读取主内存中的数据,如果该变量共享到同一个缓存行,就会影响到整理性能。
例如:线程1修改了long类型变量A,long类型定义变量占用8个字节,由于缓存一致性协议,线程2的变量A副本会失效,线程2在读取主内存中的数据的时候,以缓存行的形式读取,无意间将主内存中的共享变量B也读取到内存中,而主内存中的变量B没有发生变化。
当我们的CPU写入缓存的时候发现缓存区正在被其他cpu站有的情况下,为了能够提高CPU处理的性能可能将后面的读缓存命令优先执行。
注意:不是随便重排序,需要遵循as-ifserial语义。
as-ifserial:不管怎么重排序(编译器和处理器为了提高并行的效率)单线程程序执行结果不会发生改变的,也就是我们编译器与处理器不会对存在数据依赖的关系操作做重排序。
重排序问题是CPU指令重排序优化的过程存在的问题,在单线程的情况下是不会存在问题的,但是在多线程的情况下,指令逻辑无法分辨因果关系,可能指令会存在乱序的问题,导致执行结果发生错误。
因为多个线程在获取实例时,创建实例的时候会存在重排序的问题。
创建一个对象一般会分为三个步骤:
step1:分配对象的内存空间;
step2:调用构造函数初始化;
step3:将对象赋值给变量;
但是由于CPU会进行指令重排,可能会先执行第三步再执行第二步,如果t1线程获取到锁准备创建实例时,这个时候发生了指令重排,先将对象赋值给了变量,而t2线程进来的时候,对象已经不为null了,所以t2线程可以自由访问该对象,可由于对象还没有调用构造函数进行初始化,t2线程获取到的是一个不完整的对象,访问时会发生异常。
悲观锁:没有获取到锁的线程会阻塞等待;
站在MySQL的角度分析:悲观锁比较悲观,当多个线程对同一行数据实现修改的时候, 最终只有一个线程能够修改成功,只要谁能够获取到行锁,则其他线程是不能够对该数据做任何修改操作的,且是阻塞状态。
站在Java锁层面,如果没有获取到锁,则会阻塞等待,后期唤醒的锁的成本就会非常高,需要被CPU重新从就绪状态调度为运行状态。
乐观锁:如果没有获取到锁,当前线程不会阻塞等待,通过死循环控制。乐观锁属于无锁机制,没有竞争锁的流程。
乐观锁比较乐观,通过阈值或者版本号进行比较,如果不一致的情况则通过循环控制修改,当前线程不会被阻塞,效率比较高,但是乐观锁比较消耗CPU资源。
公平锁:比较公平,按照请求锁的顺序进行排列,先请求的则先获取锁,后请求的则后获取锁;-- new ReentrantLock(true)
非公平锁:通过争抢的方式获取锁,效率比公平锁高;-- new ReentrantLock(false)
独占锁:在多线程中,只允许一个线程获取到锁,其他线程都会阻塞等待;
共享锁:多个线程可以同时持有锁,比如ReentrantLock读写锁:读读共享、写写互斥、读写互斥、写读互斥;
在同一个线程中锁可以不断传递的,可以直接获取。
CAS: Compare and Swap,翻译成比较并交换。执行函数CAS(V, E, N)
CAS有3个操作数:V(内存值)、E(旧的预期值)、N(要修改的新值)。当且仅当预期值E和内存值V相同时,将内存值V修改为N,否则什么都不做。
没有获取到锁的线程是不会阻塞的,通过循环控制一直不断的获取锁。
原子类:AtomicBoolean、AtomicInteger、AtomicLong等均使用CAS实现。
CAS的原理:当E(旧的预期值)===V(共享变量中值)时,才会修改V。
基于CAS实现锁机制原理:
实现细节:
CAS获取锁:将该锁的状态从0改为1,如果能够修改成功,则表示获取锁成功,如果修改失败,则表示获取锁失败,但是没获取到锁的线程不会阻塞而是通过循环(自旋)来控制重试;
CAS释放锁:将该锁的状态从1改为0,如果能够修改成功,则表示释放锁成功。
优点:没有获取到锁的线程,会一直在用户态,不会阻塞,没有锁的线程会一直通过循环控制进行重试,效率高。
缺点:通过死循环控制,消耗CPU资源,需要控制循环次数,避免CPU飙升问题。
ABA问题:线程1准备用CAS修改变量值A,在此之前,其它线程将变量的值由A替换为B,又由B替换为A,然后线程1执行CAS时发现变量的值仍然为A,所以CAS成功。但实际上这时的现场已经和最初不同了。ABA问题只是概念产生了冲突,并不影响结果。
解决ABA问题的方案就是给值加一个修改版本号,每次值发生变化,都会修改它的版本号,CAS操作时都对比此版本号。(AtomicMarkableReference)
在使用synchronized关键字进行同步操作时,JVM底层会将一个对象与一个对象监视器(Monitor)进行关联,当且仅当一个Monitor拥有所有者(_owner不为null)后,它将处于锁定状态。线程执行到monitorenter指令时,将会尝试获取当前对象对应的Monitor的所有权。如果Monitor的重入次数(_recursions)为0时,线程可以进入Monitor,并将重入次数置为1,当前线程成为Monitor的所有者,如果还需要重入Monitor的话,会将重入次数加1,当其他线程尝试获取Monitor对象时,会进行阻塞等待,直到Monitor的重入次数置为0时,才能重新尝试获取Monitor的所有权。如果持有Monitor的线程执行monitorexit指令时,会将Monitor的重入次数减1,直到Monitor的重入次数减为0时,表示当前线程退出Monitor,不再是Monitor的所有者,其他线程可以尝试获取Monitor。
Monitor内部有两个非常重要的成员变量:_recursions:记录线程重入锁的次数;_owner:记录当前持有锁的线程ID。
在进入synchronized修饰的方法/代码块时,会执行一次monitorenter指令,但是释放锁的时候,会有两个monitorexit指令(分别插在方法结束处和异常处),可实际上只会执行一个monitorexit指令,要么正常结束,要么抛出异常,所以在抛出异常的时候,synchronized会自动释放锁。
_cxq:存放竞争锁的线程的单向链表,当锁不被任何线程持有时,才会用到;
_WaitSet:等待池,处于wait状态的线程,会被加入到_WaitSet;
_EntryList:锁池:处于等待锁阻塞状态的线程,会被加入到该列表;
锁的消除:当多个线程获取锁时,发现锁的对象就是每个线程私有的对象锁,编译器会做优化,消除synchronized锁;
锁的粗化:JVM检测到一连串的操作都对同一个对象加锁(while循环内执行100次append,没有锁粗化的就要进行100次加锁/解锁),此时 JVM 就会将加锁的范围粗化到这一连串的操作的外部(比如 while 虚幻体外),使得这一连串操作只需要加一次锁即可。
偏向锁→轻量级锁(短暂自旋)→重量级锁
场景一:一直都是同一个线程在获取锁–偏向锁;
场景二:多个线程以间隔的形式获取锁–轻量级锁;
场景三:多个线程同时获取锁–重量级锁;
(1) 互斥条件:一个资源每次只能被一个进程使用。
(2) 请求与保持条件:一个进程因请求资源而阻塞时,对已获得的资源保持不放。
(3) 不剥夺条件:进程已获得的资源,在末使用完之前,不能强行剥夺。
(4) 循环等待条件:若干进程之间形成一种头尾相接的循环等待资源关系。
Lock锁是基于AQS+LockSupport+CAS实现的,在获取锁的时候会把AQS类中state属性变为1,如果当前线程不断重入时,会进行state+1的操作,在释放锁的时候,会进行state-1的操作,直到state变为0时,表示该锁没有被任何线程获取到。另外,没有抢到锁的线程会存放在一个双向链表中,
Lock API:lock()—获取锁/unlock()—释放锁/tryLock()—非阻塞式获取锁;
LockSupport API:public static void park()—阻塞当前线程/public static void unpark(Thread thread)—唤醒当前线程。
AQS(AbstractQueuedSynchronizer)是一个抽象同步队列,它提供了一个FIFO队列,可以看成是一个用来实现同步锁以及其他涉及到同步功能的核心组件,常见的有:ReentrantLock、CountDownLatch等。
AQS是一个抽象类,主要是通过继承的方式来使用,它本身没有实现任何的同步接口,仅仅是定义了同步状态的获取以及释放的方法来提供自定义的同步组件。
AQS底层是基于CAS compareAndSwapInt()方法实现的,底层采用了双向链表的数据结构,使用了模板方法设计模式。
AQS的核心思想是,如果被请求的共享资源空闲,则将当前请求资源的线程设置为有效的工作线程,并将共享资源设置为锁定状态,如果被请求的共享资源被占用,那么就需要一套线程阻塞等待以及被唤醒时锁分配的机制,这个机制AQS是用CLH队列锁实现的,即将暂时获取不到锁的线程加入到队列中。
CLH(Craig,Landin,and Hagersten)队列是一个虚拟的双向队列,虚拟的双向队列即不存在队列实例,仅存在节点之间的关联关系。AQS是将每一条请求共享资源的线程封装成一个CLH锁队列的一个结点(Node),来实现锁的分配。
也就是说:AQS是基于CLH队列,通过改变state状态符,表示获取锁的成功或者失败。
AQS 定义了两种资源共享方式:
Exclusive:独占,只有一个线程能执行,如ReentrantLock;
Share:共享,多个线程可以同时执行,如Semaphore、CountDownLatch、ReadWriteLock、CyclicBarrier;
核心参数:
Node节点:采用双向链表的形式存放正在等待的线程;
state:锁的状态符;
exclusiveOwnerThread:记录持有锁的线程。
Condition是一个接口,其提供的就两个核心方法,await和signal方法。分别对应着Object的wait和notify方法。调用Object对象的这两个方法,需要在同步代码块里面,即必须先获取到锁才能执行这两个方法。同理,Condition调用这两个方法,也必须先获取到锁。注意:await()方法会释放锁。
Semaphore用于限制可以访问某些资源(物理或逻辑的)的线程数目,他维护了一个许可证集合,有多少资源需要限制就维护多少许可证集合,假如这里有N个资源,那就对应于N个许可证,同一时刻也只能有N个线程访问。一个线程获取许可证就调用acquire()方法,用完了释放资源就调用release()方法。可以简单理解为Semaphore信号量可以实现对接口限流,底层是基于AQS实现。
Semaphore工作原理:
CountDownLatch是一种java.util.concurrent包下一个同步工具类,它允许一个或多个线程等待直到在其他线程中一组操作执行完成。和join()方法非常类似,但是join()方法的底层是基于wait()方法实现,而CountDownLatch的底层是基于AQS实现的。
CountDownLatch countDownLatch = new CountDownLatch(2) AQS的state状态为2,调用countDownLatch.await()方法时线程变为阻塞状态且同时释放锁,调用countDownLatch.countDown();方法的时候状态-1 当AQS状态state为0的情况下,则唤醒正在等待的线程。
线程池和数据库连接池非常类似,可以统一管理和维护线程,减少没有必要的开销。
因为频繁的开启线程或者停止,线程需要重新被CPU从就绪到运行状态调度,效率非常低。所以使用线程可以实现复用,从而提高效率。
底层都是基于ThreadPoolExecutor构造函数封装
本质思想:创建一个线程,不会立马停止或者销毁,而是一直实现复用。
线程池核心点:复用机制
corePoolSize:核心线程数量,一直正在保持运行的线程;
maximumPoolSize:最大线程数,线程池允许创建的最大线程数;
keepAliveTime:超出corePoolSize后创建的线程的存活时间;
unit:keepAliveTime的时间单位;
workQueue:任务队列,用于保存待执行的任务;
threadFactory:线程池内部创建线程所用的工厂;
handler:任务无法执行时的处理器;
不会。
例如:配置核心线程数corePoolSize为2、最大线程数maximumPoolSize为5,我们可以通过配置超出corePoolSize核心线程数后创建的线程的存活时间为60s,在60s内线程一直没有任务执行,则会停止该线程。
因为默认的Executors线程池底层是基于ThreadPoolExecutor构造函数封装的,采用无界队列存放缓存任务,会无限缓存任务,从而容易发生内存溢出,会导致最大线程数失效。
核心原理:
专业术语:
如果队列满了且任务总数 > 最大线程数,则当前线程走拒绝策略。
可以自定义拒绝异常,将该任务缓存到 redis、本地文件、mysql 中后期项目启动实现补偿。
自定义线程池需要我们自己配置最大线程数maximumPoolSize,为了高效的并发运行,当然这个不能随便设置。这时需要看我们的业务是 IO 密集型还是 CPU 密集型。
CPU密集型
CPU密集的意思是该任务需要大量的运算,而没有阻塞,CPU 一直全速运行。CPU 密集任务只有在真正的多核CPU上才可能得到加速(通过多线程),而在单核 CPU上,无论你开几个模拟的多线程该任务都不可能得到加速,因为CPU总的运算能力就那些。
CPU密集型任务配置尽可能少的线程数量:以保证每个 CPU 高效的运行一个线程。 一般公式:(CPU 核数+1)个 线程的线程池
IO密集型
IO密集型,即该任务需要大量的IO,即大量的阻塞。在单线程上运行I0密集型的任务会导 致浪费大量的 CPU 运算能力浪费在等待。 所以在IO密集型任务中使用多线程可以大大的加速程序运行,即使在单核 CPU 上,这种加 速主要就是利用了被浪费掉的阻塞时间。
IO密集型时,大部分线程都阻寒,故需要多配置线程数: 公式:CPU 核数 * 2 CPU 核数 / (1 - 阻塞系数) 阻塞系数 在 0.8~0.9 之间
查看 CPU 核数: System.out.println(Runtime.getRuntime().availableProcessors());
FutureTask表示一个异步运算的任务。FutureTask里面可以传入一个Callable的具体实现类,可以对这个异步运算的任务的结果进行等待获取、判断是否已经完成、取消任务等操作。当然,由于FutureTask也是Runnable接口的实现类,所以FutureTask也可以放入线程池中。基于LockSupport实现。
Fork/Join是Java7提供的并行执行任务的框架,是一个把大任务分割成若干个小任务,最终汇总小任务的结果得到大任务结果的框架。
ThreadLocal 提供了线程本地变量,它可以保证访问到的变量属于当前线程,每个线程都保 存有一个变量副本,每个线程的变量都不同。ThreadLocal相当于提供了一种线程隔离,将变量与线程相绑定。Threadlocal适用于在多线程的情况下,可以实现传递数据,实现线程隔离。ThreadLocal是每个线程的局部变量。
synchronized与ThreadLocal都可以实现多线程访问,保证线程安全的问题。
相比来说ThreadLocal效率比synchronized效率更高。
因为每个线程中都有自己独立的ThreadLocalMap对象,key为ThreadLocal,value为变量值。使用ThreadLocal作为Entry对象的key,是弱引用,当ThreadLocal的指向为null时,Entry对象中的key变为null,该对象一直无法被垃圾收集器回收,一直占用到了系统内存,有可能会发生内存泄漏的问题。