并发编程
内存模型
Java 内存模型是一个规范。规定一个线程如何、何时可以看到一个共享变量由其他线程修改后的值以及在必须时如何的同步访问共享变量,线程之间的通讯必须经过主内存(??存在疑问)
数据存储
内存分配
heep 堆
- 由垃圾回收机制负责回收
- 动态分配大小
- 存取数独较慢
- 对象实列
- 一个对象的成员变量可能跟随对象存储在堆上(对象实列)
stack 栈
- 存取数度较快 次于寄存器
- 栈的数据可以共享?????
- 栈的大小和生存期必须的确定的
- 存储基本类型的变量(int,short,byte…)、对象句柄
- 线程栈中存储:调用栈、本地变量
- 静态成员变量
同步操作(八步)
- Lock 锁定作用于驻内存中的变量,将其标记为1条线程独占状态 (一个线程可对一个变量多次加锁,但必须执行相同次数的解锁)
- Read 将主内存中变量的值读取到工作内存(每个线程有自己的工作内存)中
- load 将读取到工作内存中的值复制到 工作副本中(每个线程都有共享变量自己的副本)
- use 每个线程使用的是自己工作副本中的值
- Assign 将修改后的值赋值工作副本中
- Store 将工作副本中的值传递到主内存中
- Write 将工作副本传递过来的值写回到主内存中
- Unlock 解锁
同步数据规则
- 不允许 (Read,load) 和 (Store,Write) 单一出现,但可以不连续执行。
- 不允许一个线程丢弃掉最近一次的 Assign 操作,当数据在工作副本中发生变化后必须同步到主内存中不允许丢弃调
- 工作副本中的数据必须发生变化才能同步回主内存
- 对于共享变量必须对其进行 Read,load 操作后才能对起进行使用
- 一个变量在同一时刻只能有1个线程对其进行lock操作(统一时刻只有1个线程会lock成功)
- 当1个线程执行lock变脸成功后,会将工作内存中的改变量副本清楚重新执行 Read,load 操作
- 当unlock解锁一个变量时必须将工作副本中的值 Write 回主内存,才能执行unlock
- 特殊规则:final 修饰的变量不允许修改
线程安全
原子性
atomic 原子操作包
AtomicInteger
原子的方式去操作 int 类型数据
//原子的方式增加1
getAndIncrement();
incrementAndGet()
//原子的方式递减1
decrementAndGet()
...... 提供了一些列的原子操作
核心实现
public final int getAndAddInt(Object var1, long var2, int var4) {
int var5;
do {
var5 = this.getIntVolatile(var1, var2);
} while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));
return var5;
}
//核心方法 -----由java底层实现 compareAndSwap 系列方法
public final native boolean compareAndSwapInt(Object var1, long var2, int var4, int var5);
LongAdder
允许将64位的读写操作拆分成3个32位的操作,将核心数据value分离成一个数组,每次线程对每个数组进行操作, 当前数据的值为数组的和
优点 :分散分发点减少循环跟新的次数
确定:当在统计时如果有并发跟新会造成统计的数据有误差
AtomicReferenceFieldUpdater
原子性的更新某个类上的某个字段
static AtomicReferenceFieldUpdater<Test2,Integer> referenceFieldUpdater = AtomicReferenceFieldUpdater.newUpdater(Test2.class,Integer.class, "qwe");
Test2 test2 = new Test2();
referenceFieldUpdater.compareAndSet(test2,0,100);
System.out.println(test2.qwe);
AtomicStampedReference
解决 ABA 问题 stamp 多维护一个字段,保持 stamp 为最新的不会回复的数据
synchronized
synchronized 的作用范围取决于 synchronized 获取的锁的作用域
- 当 synchronized 方法被继承时候子类 不能继承该方法 的 synchronized 机制
lock(自己研究哈)
可见性
synchronized
- 线程解锁前,必须把共享变量的最新值刷新到主内存
- 线程加锁时,将清空工作内存中共享变量的值,从而使用共享变量时需要从出内存中重新读取最新的值(加锁和解锁是同一把锁)
volatile
- 对于 volatile 变量的写操作时,会在**写操作后**加入一天store 屏障指令,将本地内存中的共享变量值刷新到主内存
- 对于volatile 变量的读操作,会在**读操作前** 加入一条load屏障指令,从主内存 中读取共享变量
适用状态表计量
有序性
java内存模型中,允许编译器和处理器对指令进行**重排序,但是重排序过程不会影响到单线程程序的执行,却会影响到多线程并发执行的正确性**,volatile 关键字可以禁止指令重排
程序次序规则:
-
一个线程内,按代码顺序,前面的代码操作优先于后面的代码(单线程)
-
一个unlock操作先行发生于后面对同一个锁的lock操作
-
volatile变量:对一个变量的写操作先行发生于后面对这个变脸的读操作(volatile插入屏障指令对于同时发生的读写操作进行排序使写操作发生在前面)
线程安全发布对象
错误的发布
-
发布对象:使一个对象能够被当前范围之外的代码所使用
对于内部states虽然为私有但是外部却可以通过get获取states地址的引用从而修改他
-
对象逸出:一种错误的发布。当一个对象还没有构建完成时被其他线程所见
- Escape未被初始化完成就被 InerClass 中的使用
@Slf4j public class Escape { private int thisCanEscape = 1; public Escape() { new InerClass(); } private class InerClass { public InerClass() { log.info("{}",Escape.this.thisCanEscape); } } public static void main(String[] args) { new Escape(); } }
正确的发布
-
将对象放入 volatile 域中防治指令重排(1,2,3不赚可能发生指令重排序)
-
在静态域中发布对象
-
通过枚举发布
public class Demo { private Demo() { } public static Demo getDemo() { return DemoEnum.INSTANCE.getInstance(); } private enum DemoEnum { INSTANCE; private Demo demo; DemoEnum() { demo = new Demo(); } public Demo getInstance() { return demo; } } }
不可变对象
-
Collections 下unmodifiable方法将集合变为不不改变,不允许修改添加等
线程封闭
同步容器
- ArrayList -> Vector,Stack(容器中的方法被synchronized同步)
- Vector线程相对安全一些
- Stack继承Vector 数据结构为 栈
- HashMap -> HashTable(key,valeu 不能为空null)
- Collections.synchronizedXXX(List,Set,Map)
同步容器的不当操作
private static java.util.List<Integer> vector = new Vector<>();
public static void main(String[] args) {
while (true) {
for (int i = 0; i < 10; i++) {
vector.add(i);
}
Thread thread1 = new Thread(() -> {
for (int i = 0; i < vector.size(); i++) {
vector.remove(i);
}
});
Thread thread2 = new Thread(() -> {
for (int i = 0; i < vector.size(); i++) {
// 例如thread2想获取i=9的元素的时候,thread1将i=9的元素移除了,导致数组越界
vector.get(i);
// log.info("{}", integer);
}
});
thread1.start();
thread2.start();
}
}
并发容器
-
CopyOnWriteArrayList
- 添加元素时复制一个新的数组,在新的数组上写操作然后将原来的list指向现在的list,因为每次添加都需要复制操作所以适合**读多写少**
当我们往一个容器添加元素的时候,不直接往当前容器添加,而是先将当前容器进行 Copy,复制出一个新的容器,然后新的容器里添加元素,添加完元素之后,再将原容器的引用指向新的容器。
这时候会抛出来一个新的问题,也就是数据不一致的问题。如果写线程还没来得及写会内存,其他的线程就会读到了脏数据。
这就是CopyOnWriteArrayList 的思想和原理。就是拷贝一份写。所以使用条件也很局限,那就是在读多写少的情况下比较好。
-
CopyOnWriteArraySet
- 底层实现类似CopyOnWriteArrayList,使用
-
ConcurrentSkipListSet
-
ConcurrentHashMap(重要)
-
ConcurrentSkipListMap
AQS
AbstractQueuedLongSynchronizer
线程相关类
CountDownLatch
阻塞线程 等待子线程 完成后释放阻塞
//定义10个字线程
final CountDownLatch countDownLatch = new CountDownLatch(10);
//递减锁存器的计数,如果计数到达零,则释放所有等待的线程
new Thread(()->{
//TODO Do some thing
countDownLatch.countDown();
}).start();
//使当前线程在锁存器倒计数至零之前一直等待,除非线程被中断。
countDownLatch.await();
Semaphore
控制:信号量最多允许多少个线程执行
//最多允许10个线程同时执行
final Semaphore semaphore = new Semaphore(10);
new Thread(()->{
//从此信号量获取一个许可,在提供一个许可前一直将线程阻塞(如果获取不到将一直阻塞线程)
semaphore.acquire();
//TODO Do some thing
.......
//释放一个许可,将其返回给信号量
semaphore.release();
}).start();