并发编程
- 并发编程
- Java内存模型(JMM)
- 并发编程核心问题—可见性、原子性、有序性
- volatile关键字
- 原子性
- 原子类
- CAS(Compare-And-Swap 比较并交换)
- ABA问题
- Java中的锁
- 乐观锁和悲观锁
- 可重入锁
- 读写锁
- 分段锁
- 自旋锁
- 共享锁/独占锁
- 公平锁/非公平锁
- 偏向锁/轻量级锁/重量级锁
- 偏向锁状
- 轻量级锁
- 重量级锁
- 对象结构
- sychronized锁实现
- AQS
- ReentrantLock锁实现
- JUC常用类
- ConcurrentHashMap
- CopyOnWriteArrayList
- CopyOnWriteArrayList
- 辅助类CountDownLatch
- 对象引用
- 强引用
- 软引用(SoftReference)内存不足即回收
- 弱引用(Weak Reference)发现即回收
- 虚引用(Phantom Reference):对象回收跟踪
- 线程池
- **TheadPollExecutor**类
- 构造器中各个参数的含义
- 线程池的执行
- 线程池中的队列
- 线程池的拒绝策略
- 关闭线程池
- ThreadLocal
- ThreadLocal 内存泄漏问题
并发编程
并行:同一个节点同时发生
并发:在一段时间内,多个事件交替执行
并发编程:在例如买票、抢购等场景下,有大量请求访问同一资源,会出现线程安全的问题,所以需要通过编程来解决让多个线程依次访问资源,称为并发编程
Java内存模型(JMM)
java内存模型,是java虚拟机规范的一种工作模式
JMM将内存分为主内存和工作内存。变量数据存储在主内存中,线程在操作变量时,会将主内存中的数据复制到工作内存,在工作内存中操作完成后,再写回主内存
并发编程核心问题—可见性、原子性、有序性
基于java内存模型的设计,多线程操作一些共享数据时,会出现三个问题
不可见性:多个线程分别对共享数据进行操作,彼此之间不可见,操作结束写回主内存,可能会出现问题
无序性:为了性能,对一些代码执行的执行顺序进行重排,以提高速度
非原子性:线程切换带来的原子性问题
volatile关键字
共享变量被volatile修饰以后:
1.共享变量被一个线程修改后,对其他线程立即可见
2.在执行过程中不会被重排
3.不能保证对变量操作的原子性
volatile底层实现原理
使用Memory Barrier(内存屏障),内存屏障是一条指令,它可以对编译器和处理器的指令重排做出一i的那个的限制。
有序性实现:volatile修饰的变量在操作前,添加内存屏障,不让他的指令干扰
可见性实现:主要通过Lock前缀指令+MESI缓存一致性协议来实现。操作volatile修饰的变量时,JVM会发送Lock前缀指令给CPU,CPU在执行完操作后,会立即将新值刷新到内存,其他CPU都会对总线嗅探,看自己本地缓存的数据是否被修改,如果修改了,就会将修改的数据存到本地缓存中,主内存就h会加载最新的值。
原子性
通过加锁的方式,让线程互斥执行来保证一次只有一个线程执行
锁:synchronized关键字,是锁的一种实现。synchronized一定能保证原子性,也能够保证可见性和有序性。
原子变量:JUC(java.util.concurrent包)中的locks包和atmic包,可以解决原子性问题
加锁是一种阻塞式方式实现,原子变量是非阻塞方式实现。
原子类
原子类的原子性是通过volatile+CAS实现原子操作,适合与低并发的条件下
CAS(Compare-And-Swap 比较并交换)
CAS是乐观锁的一种实现方式,采用的是自选锁的思想,是一种轻量级的锁机制
底层是通过 Unsafe 类中的 compareAndSwapInt 等方法实现.
CAS包含了三个操作数: 内存值 V、预估值 A、更新值 B
过程:1.第一次将主内存中的值放到工作内存中作为预期值,然后将更新值存入工作内存。
2.将工作内存中的值写入主内存前需要把预期值和主内存的值进行比较。
3.如果主内存的值和预期值相等,将更新值写入主内存,如果不相等,说明有其他线程修改了主内存的值,需要重复上述过程,直到主内存的值和预估值相等。
缺点:CUP的消耗增加
ABA问题
ABA问题,即某个线程将内存值A改为了B,再又A改为了A,当另一个线程是使用预期值去判断时,内存值和与预期值相等,无法判断内存值是否发生过变化
解决方式:通过使用类添加版本号,来避免问题的发生,如原先的内存值为(A,1),线程将(A,1)修改为了(B,2),再由(B,2)修改为(A,3)。此时另一个线程使用预期值(A,1)与内存值(A,3)进行比较,只需要比较版本号 1 和3.即可发现内存值被更新过。
Java中的锁
java中锁的名词不是全指的是锁。还可以指的是锁的特性、锁的状态、锁的设计
乐观锁和悲观锁
乐观锁:认为同一个数据并发的操作是不会发生修改的,不加锁的方式实现是没有问题的,每次操作前判断(CAS)是否成立。
悲观锁:认为同一个数据并发的操作会发生修改,必须加锁
可重入锁
当一个线程获取外层方法的同步锁对象后,可以获取到内部其他同步锁
public class Demo{
synchronized void setA throws Exception{
System.out.println(A);
setB();
}
synchronized void setB throws Exception{
System.out.println(B);
}
}
上面的代码就是一个可重入锁的一个特点,如果不是可重入锁,setB不会被当前线程执行,造成死锁
读写锁
支持读写加锁,如果都是读操作,那么就不加锁,如果存在写操作,就会出现操作互斥
读写锁为了防止脏读
private int data;//共享数据
private ReadWriteLock rwl=new ReentrantReadWriteLock();
public void set(int data){
rwl.writeLock().lock();//获取到写锁
try {
System.out.println(Thread.currentThread().getName()+"准备写入数据");
this.data=data;
System.out.println(Thread.currentThread().getName()+"写入数据"+this.data);
}finally {
rwl.writeLock().unlock();//释放写锁
}
}
public void get(){
rwl.readLock().lock();//取到读锁
try {
System.out.println(Thread.currentThread().getName()+"准备写入数据");
System.out.println(Thread.currentThread().getName()+"写入数据"+this.data);
}finally {
rwl.readLock().unlock();//释放读锁
}
}
分段锁
不是锁,是一种锁的实现思想,用于将数据分段,给每个分段数据加锁提高并发效率
自旋锁
不是锁,以自旋的方式进入锁。自旋锁是比较消耗CPU
共享锁/独占锁
共享锁:该锁可被多个线程共享。读写锁中的读锁是共享锁
独占锁:是指该锁只能被一个线程拥有。Synchronized
公平锁/非公平锁
公平锁:是按请求的顺序来获取锁,先来后到,例如ReentrantLock
非公平锁:没有顺序,谁抢到谁执行。例如synchronized
偏向锁/轻量级锁/重量级锁
synchronized锁的状态存储在同步锁对象的对象头中的区域Mark Word中存储
锁的状态有四种:无锁状态、偏向锁状态、轻量级锁、重量级锁
偏向锁状
代码一直被一个线程访问,那么该线程会自动获取锁
轻量级锁
当锁的状态是偏向锁时,又有一个线程访问,轻量级锁就会升级为轻量级锁,其他线程就会通过自旋的方式获取锁,不会阻塞。
重量级锁
当锁的状态为轻量级锁时,当线程自选到一定的次数就会进入阻塞状态,锁状态升级为重量级锁,等待操作系统调度。
对象结构
在Hotspot虚拟机中,对象在内存中的布局分为三块区域:对象头、实例数据和对齐填充,synchronized使用的锁对象是存储在java对象头中。
对象头中有一块Mark word,用于存储对象自身运行时的数据,如哈希码、GC分代年龄、锁状态、线程持有的锁、偏向锁等等。
下面就是对象头的一些信息:
sychronized锁实现
Java提供的一种原子性内置锁。synchronized基于进入和退出监视器对象来实现方法同步和代码块同步。
同步方法使用 ACC_SYNCHRONIZED标记是否为同步方法,当方法调用时,会检查方法是否被标记,如果被标记,线程进入该方法时,需要monitorenter,退出方法时需要monitorexit.
代码块的同步是利用 monitorenter 和 monitorexit 这两个字节码指令
monitorenter指令:尝试获取对象的锁,如果获取到,把锁的计数器加1
monitorexit:将锁计数器减一,当计数器为0时,锁就被释放
Java 中 synchronized 通过在对象头设置标记,达到了获取锁和释放锁的目的。
AQS
AQS(抽象同步队列),是JUC中的核心组件,其他锁实现的基础
实现原理:
在类中维护一个state变量表示锁是否使用,然后还维护一个队列,以及获取锁,释放锁的方法
当线程创建后,先判断state值,当state=0,没有线程使用,当state=1,线程会去队列等待。
等占有state的线程执行完成将state-1后,会唤醒对列中等待的线程(head中的下一个结点)去获取state;
AbstractQueuedSynchronizer 成员
private transient volatile Node head;
private transient volatile Node tail;
/*使用变量 state 表示锁状态,0-锁未被使用,大于 0 锁已被使用
共享变量 state,使用 volatile 修饰保证线程可见性
*/
private volatile int state;
状态信息通过 getState , setState , compareAndState来操作
protected final int getState() { //获得锁状态
return state;
}
protected final void setState(int newState) {//设置锁状态
state = newState;
}
//使用 CAS 机制设置状态
protected final boolean compareAndSetState(int expect, int update) {
return unsafe.compareAndSwapInt(this, stateOffset, expect, update);
}
获取锁的方式有两种:
tryAcqurie () :尝试获取锁。
acquire():尝试获取锁,获取失败时,进入队列等待。直到获取
public final void acquire(int arg) {
//tryAcquire获取锁成功,方法结束,获取锁失败,执行 acquireQueued(addWaiter(Node.EXCLUSIVE), arg)将线程arg添加到队列中。
if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
addWaiter: 尝试获取锁失败后,将当前线程封装到一个 Node 对象中,添加
到队尾,并返回 Node 节点. acquireQueued: 将线程添加到队列后,以自旋的方式去获取锁
release 释放锁
tryRelease: 释放锁,将 state 值进行修改为 0
unparkSuccessor: 唤醒节点的后继者(如果存在)
AQS的锁模式分为:独占和共享
ReentrantLock锁实现
ReentrantLock基于AQS,可以实现公平锁和非公平锁
ReentrantLock有Sync、NonfairSync、FairSync三个内部类,他们紧密相关
NonfairSync继承了Sync类,实现非公平锁
static final class NonfairSync extends Sync {
//加锁
final void lock() {
//若通过 CAS 设置变量 state 成功,就是获取锁成功,则将当前线程设置为独占线程。
//若通过 CAS 设置变量 state 失败,就是获取锁失败,则进入 acquire 方法进行后续处理。
if (compareAndSetState(0, 1))
setExclusiveOwnerThread(Thread.currentThread());
else
acquire(1);
}
//尝试获取锁,无论是否获得都立即返回
protected final boolean tryAcquire(int acquires) {
return nonfairTryAcquire(acquires);
}
}
FairSync 类也继承了 Sync 类,实现公平锁
static final class FairSync extends Sync {
final void lock() {
// 以独占模式获取对象,忽略中断
acquire(1);//底层实现交由 AbstractQueuedSynchronizer
}
}
JUC常用类
ConcurrentHashMap
ConcurrentHashMap是线程安全的哈希表,在多线程操作中,比Hashtable效率高,内部使用cas+synchronized(分段锁被弃用)。
放弃分段锁的原因:浪费内存,在运行环境中,map中同时进入同一个位置的概率很小,分段所反而会浪费更多的时间。
jdk8放弃分段锁,使用Node锁。提高了性能,并使用CAS操作来保证Node操作的原子性。
过程:
put时,通过hash找到对应链表后,查看是否是第一个Node,如果是,直接用cas原则插入。
如果不是,则直接用链表第一个Node加synchornized锁。
ConcurrentHashMap和Hashtable一样 不支持存储 null 键和 null 值. 这样是为了消除歧义
不能put null是因为 无法分辨key没找到返回null还是有key值为null,所以不能 null.
不等存储null值,是因为当你get(k)获取value时,如果获取到null时,你无法判断是value值为null,还是这个key还没做过映射
CopyOnWriteArrayList
ArrayList是线程不安全的,Vector是线程安全,vector读操作和写操作都加了锁,实际应用中读操作很频繁,且读操作不会修改数据。所以CopyOnWriteArrayList,为了提高性能出现。
CopyOnWriteArrayList修改数据流程:当list需要被修改时,并不直接对原有list进行修改,而是对原有数进行拷贝,将修改的内容写入副本中,修改结束后,将修改完的副本替换成原来的数据
CopyOnWriteArrayList
CopyOnWriteArrayList实现基于CopyOnWriteArrayList,不能存储重复元素数据
辅助类CountDownLatch
CountDownLatch,底层时通过AQS来完成的,一个线程等待其他线程执行完才执行。
过程:创建CountDownLatch 对象指定一个线程数量。每当一个线程执行完毕后,AQS内部的state-1,当state=0时,表示所有线程都执行完毕,然后在闭锁上等待的线程就可以执行
CountDownLatch countDownLatch = new CountDownLatch(6);//设置线程总量
for (int i = 0; i <6 ; i++) {
new Thread(()->{
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("aaaaaaaaaaaaa");
countDownLatch.countDown();
}).start();
}
countDownLatch.await();
System.out.println("main线程执行");//最后执行的内容
对象引用
强引用
对象由引用指向的,如 Object obj=new Object();这种情况下永远不会被垃圾回收器回收
软引用、弱引用、虚引用都是用来标记对象的一种状态
软引用(SoftReference)内存不足即回收
软引用是用来描述一些还有用但非必须的对象,如果内存充足的情况下,可以保留软引用,如果内存不足。经过一次垃圾回收后,内存依然不足,软引用的对象就会被清除
弱引用(Weak Reference)发现即回收
弱引用的对象也是描述非必须的对象,它只能存活到下一次垃圾回收发生为止,当垃圾回收器和工作,就会回收掉弱引用的对象
虚引用(Phantom Reference):对象回收跟踪
虚引用的对象和没有引用几乎是一样的,随时都会被垃圾回收器回收,虚引用必须和引用队列一起使用。虚引用在创建时必须提供一个引用队列作为参数,目的是在这个对象被回收时收到系统通知
线程池
线程池就是事先创建一些线程,每次使用时直接获取,用完不销毁
Executors提供了常见的线程池创建方法:
newSingleThreadExecutor:一个单线程的线程池。如果因异常结束,会再创建一个新的,保证按照提交顺序执行。
newFixedThreadPool:创建固定大小的线程池。根据提交的任务逐个增加线程,直到最大值保持不变。如果因异常结束,会新创建一个线程补充。
newCachedThreadPool:创建一个可缓存的线程池。会根据任务自动新增或回收线程
通常情况下不建议直接只用Executors来创建线程池
线程池的优点:降低资源消耗,提高响应速度,节省创建时间
TheadPollExecutor类
Java.uitl.concurrent.ThreadPoolExecutor 类是线程池中最核心的一个类,ThreadPoolExecutor 继承了 AbstractExecutorService 类,并提供了四个构造器,事实上,通过观察每个构造器的源码具体实现,发现前面三个构造器都是调用的第四个构造器进行的初始化工作。
构造器中各个参数的含义
**corePoolSize:**核心线程池的大小,默认情况,创建线程池,线程池中线程数为0,当有任务来时,就会去创建一个线程执行任务,当线程数目达到corePoolSize,就会把到达的任务放到缓存队列。prestartAllCoreThreads()或者 prestartCoreThread()方法是在没有任务到来之前就创建corePoolSize个线程或一个线程
**maximumPoolSize:**线程池最大线程数。
**keepAliveTime:**表示线程没有任务执行时最多保持多长时间停止。只有当线程池中的线程数量大于corePoolSize时,keepAliveTime才会起作用,直到线程数小于corePoolSize时,keepAliveTime才终止。
unit:参数 keepAliveTime 的时间单位,有 7 种取值
workQueue:一个阻塞队列,用来存储等待执行的任务,这个参数的选择也很重要,会对线程池的运行过程产生重大影响 。
**threadFactory:**线程工厂,主要用来创建线程;
**handler:**表示当拒绝处理任务时的策略
线程池的执行
创建完成ThreadPoolExecutor,当向线程池提交任务时,通常使用 execute方法。
提交执行流程如图:
1.如果线程池中的存在的核心线程数小于corePoolSize时,线程池会创建一个核心线程去执行任务
2.如果核心线程数已满,任务会被放进任务队列workQueue排队执行
3.如果任务队列已满,且线程数小于maximumPoolSize时,创建一个非核心线程执行提交的任务。
4.如果当前线程数达到maximumPoolSize时。直接采用拒绝策略处理
线程池中的队列
ArrayBlockingQueue:是数组实现的有界的阻塞队列,必须给定最大容量
LinkedBlockingQueue:基于链表结构的阻塞队列,按 FIFO 排序任务,容量可以选择进行设置,不设置是一个最大长度为 Integer.MAX_VALU
线程池的拒绝策略
构造方法中的RejectedExecutionHandler用于指定线程池的拒绝策略。拒绝策略用于请求任务太多,线程池处理不过来的情况。
默认有四种类型:
AbortPolicy:直接抛出异常,拒绝执行
CallerRunsPolicy:将任务交给提交任务的线程(如:main方法)来执行此任务
DiscardOleddestPolicy:该策略会丢弃等待时间最长的任务,也就是最后即将被执行的任务,并尝试再次提交当前任务。
DiscardPolicy:直接丢弃当前提交的任务,不执行
excute与submit的区别:execute 适用于不需要关注返回值的场景,submit 方法适用于需要关注返
回值的场景。
关闭线程池
关闭线程池可以调用 shutdownNow 和 shutdown 两个方法来实现
shutdownNow:直接关闭线程池,对正在执行的任务全部发出 interrupt(),停止执行,对还未开始执行的任务全部取消,并且返回还没开始的任务列表.
shutdown:当我们调用 shutdown 后,等待线程池中的任务执行完,关闭线程池。
ThreadLocal
ThreadLocal线程变量,用来创建一个变量,该变量可以被多线程使用且互不干扰。为线程私有。
//创建一个ThreadLocal对象,复制保用来为每个线程会存一份变量,实现线程封闭
private static ThreadLocal<Integer> localNum = new ThreadLocal<Integer>(){
@Override
protected Integer initialValue() {
return 0;
}
};
原理:ThreadLocal是一个泛型类,可以存放任何类型的对象,他的类中定义了一个map,用来存放ThreadLocal对象和变量值。ThreadLocal 实 现 了 一 个 ThreadLocalMap 的静态类ThreadLocalMap类中的get(),set()来改变变量值。
ThreadLocal set方法
//set 方法
public void set(T value) {
//获取当前线程对象
Thread t = Thread.currentThread();
//判断该对象是否已经存入map
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
}
// 获取 threadLocalmap
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
//创建threadLocalmap,将ThreadLocal对象和变量值存入map
void createMap(Thread t, T firstValue) {
t.inheritableThreadLocals = new ThreadLocalMap(this, firstValue);
}
ThreadLocal get方法
public T get() {
//获取当前线程对象
Thread t = Thread.currentThread();
// 获取线程中的ThreadLocalMap对象
ThreadLocalMap map = getMap(t);
if (map != null) {
//获取ThreadLocalMap的Entry对象
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;
}
}
return setInitialValue();
}
private T setInitialValue() {
//设置value为null
T value = initialValue();
//获取当前线程对象
Thread t = Thread.currentThread();
// 获取线程中的ThreadLocalMap对象
ThreadLocalMap map = getMap(t);
//创建map
if (map != null)
map.set(this, value);
else
createMap(t, value);
return value;
}
protected T initialValue() {
return null;
}
ThreadLocal 内存泄漏问题
TreadLocalMap使用ThreadLoal的弱引用为key,如果一个ThreadLocal
不存在外部强引用时,Key(ThreadLocal)势必会被 GC 回收,这样就会导致ThreadLocalMap 中 key 为 null, 而 value 还存在着强引用,无法回收,造成内存泄露。
所以每次使用完ThreadLocal都调用它的remove()放法清除数据。