并发编程基础
CPU多级缓存
- 我们为什么需要CPU缓存呢?因为cpu太快了,快到主存跟不上,cpu常常需要等待主存,浪费资源。所有缓存的出现,是为了缓解cpu与内存之间的速度不匹配问题(cpu>catch>memory)
那么缓存存在的意义是什么呢?这里就涉及到了局部性原理,局部性原理主要分为以下两类:
- 时间局部性:如果数据被访问,那么在不久的将来它将再次被访问
- 空间局部性:如果某个数据被访问,那么与它相邻的数据也会很快被访问
乱序执行优化
什么是乱序执行优化:处理器为提高运算速度而做出违背代码原有顺序的优化
Java内存模型(JMM)
java内存模型规范规定了一个线程如何和何时可以看到由其他线程修改过后的共享变量的值以及在必须时如何同步的访问共享变量
如果线程A与线程B之间要通信的话,线程A把本地内存A中的共享变量刷新到主内存,线程B读取主内存中更新过的共享变量
线程安全性
什么是线程安全性呢?就是说如果你的代码在单线程环境下和多线程环境下的运行结果是一致的,就说明你的代码是线程安全的
线程安全性的三个方面:
- 原子性:互斥访问,同一时刻只能有一个线程访问
- 可见性:一个线程对主内存的修改能被其他线程看到
- 有序性:一个线程观察其他线程中的指令执行顺序
原子性
java中提供了Atomic包来实现原子性,接下来来看看Atomic的源码实现
CAS的ABA问题
ABA问题是指在CAS操作的时候,其他线程将变量的值A改成了B之后又改回了A,本线程使用期望值A与当前变量进行比较的时候发现变量没有变,实际上该值已经被其他线程改变了。
解决方法:每次变量更新的时候,把变量的版本号加一,从而能够解决ABA问题
synchronized原子性(这里做简单描述)
- synchronized:依赖JVM实现
- Lock:代码实现,常见的有ReentrantLock
synchronized作用范围和对象
- 修饰代码块:范围是大括号中的代码块,作用于调用的对象
- 修饰方法:范围是整个方法,作用于调用的对象
- 修饰静态方法:范围是整个静态方法,作用于所有对象
- 修饰类:范围是括号括起来的部分,作用于所有对象
可见性
指一个线程对主内存的修改可以被其他线程及时的观察到
导致线程间不可见的原因:
- 线程交叉执行
- 重排序结合线程交叉执行
- 共享变量更新后的值没有在工作内存与主内存间及时更新
- 使用synchronized,当线程加锁时,将清空工作内存中共享变量的值,从而使用共享变量时需要从主内存中读取最新的值,实现可见性
- volatile通过使用内存屏障和禁止重排序优化来实现可见性
- 对volatile变量写操作时,将本地内存中的共享变量刷新到主内存
- 对volatile变量读操作时,会从主内存中读取共享变量
有序性
java内存模型中,允许编译器对指令进行重排序,但是重排序的过程不会影响到单线程程序的执行,却会影响到多线程并发执行的正确性
Happens-before原则
- 程序次序原则:一个线程内,按照代码顺序,书写在前面的操作先行发生于书写在后面的操作
- 锁定规则:无论在单线程还是在多线程中,同一个锁如果处于被锁定状态,那么必须先对锁进行了释放操作,才能继续进行Lock操作
- volatile变了规则:对一个变量的写操作先行发生于对这个变量的读操作
- 传递规则:如果A操作先于B,B操作先于C,则A先于C
- 线程启动规则:Thread对象的start()方法先行发生于此线程的每一个动作
- 线程中断规则:线程必须执行了interrupt()方法之后才可以被检测到有中断事件发生
- 线程终结规则:线程中所有的操作都先行发生于线程的终止检测。(我们可以通过Thread.join()结束,Thread.isAlive()的返回值手段检测线程已经终止执行)
- 对象终结规则:一个对象的初始化完成先行发生于它的finalize()方法的开始
发布对象
发布对象:使一个对象能够被当前范围之外的代码所使用
对象溢出:一种错误的发布,当一个对象还没有构造完成时,就使它被其他线程看见
如何安全的发布对象
- 在静态初始化函数中初始化一个对象的引用
- 将对象的引用保存到volatile类型域中
- 将对象的引用保存到某个正确构造对象的final类型域中
- 将对象的引用保存到一个由锁保护的域中
单例类的实现方式
-
懒汉式:
单例实例在第一次使用时创建,是线程不安全的:
- 如何使懒汉式变成线程安全的呢?可以在方法上加上synchronized,但是这样做的缺点是会导致性能降低:
- 饿汉式,单例实例在类加载的时候创建,是线程安全的;缺点是当构造方法中含有过多的处理时会导致类加载变慢:
- 双重锁校验模式,线程不安全:
线程不安全的原因是会发生指令重排序,为了解决这个问题,可以用volatile来修饰instance变量,来禁止指令重排: - 枚举类,它是最安全的单例实现方式,相比于懒汉式,更能够保证线程安全性;相比于饿汉式,它在第一次调用时才会初始化,不会造成资源浪费:
线程安全策略
不可变对象
不可变对象需要满足的条件:
- 创建对象后其状态就不能被修改
- 对象所有域都是final类型
final可以修饰类,方法,变量:
- 当final修饰类时不能被继承
- final修饰的方法不会被继承类修改
- final修饰变量,变量初始化之后就不可以改变
final修饰的变量一定是线程安全的嘛?
答:不一定,当final修饰基本变量类型时,对象一旦初始化就不能被改变;当final修饰引用变量类型时,对象初始化之后不能指向其他对象,但是值可以改变;所有如果只是引用不能改变但是值可以改变的话就是线程不安全的
线程封闭
概念:就是把对象那个封装到一个线程里,只有一个线程能看到这个对象
线程封闭的实现方式
- Ad-hoc线程封闭:程序控制实现(忽略)
- 堆栈封闭:使用局部变量,无并发问题
- ThreadLocal线程封闭
常见的线程不安全的类与写法
- StringBuilder线程不安全,与之相对的StringBuffer线程安全
- SimpleDateFormat线程不安全,JodaTime是线程安全的
- ArrayList是线程不安全的,HashSet是线程不安全的,HashMap是线程不安全的,要使他们线程安全,必须当成局部变量来使用。当然相对应的也有线程安全的类
其中线程不安全的写法有:先检查后执行,比如:
if(condition(a)){
handle(a);
}
因为判断和执行是分开来的,不是原子性的,会引发线程不安全的问题
同步容器
Vector是线程安全的,vector中的方法是synchronized修饰的
Stack是线程安全的,Stack中的方法也是synchronized修饰的
HashTable是线程安全的
Collections.synchronizedxxx()静态方法可以创建线程安全的容器
例如这段代码:
private static ArrayList array;
private static List list = Collections.synchronizedList(array);
Collections.synchronizedList()把ArrayLIst变成了同步的容器类
并发容器
CopyOnWriteArrayList(对应于ArrayList)
CopyOnWrite容器如何实现线程安全的:
- 使用volatile修饰数组引用,确保数组引用的内存可见性
- 对容器修改操作进行同步,从而确保同一时刻只能与一条线程修改容器
- 修改时复制容器,确保所有的操作都作用在新数组上,原数组创建以后就不用变化,其他线程可以放心的读
优点:
读操作不需要加锁从而高效
缺点:
- 做写操作时需要拷贝数组就需要消耗内存,如果元素过多可能会导致Young GC或FUll GC
- 不能用于实时读的场景,因为拷贝数组,新增元素都需要时间,CopyOnWriteArrayList可以做到最终一致性,但是无法满足实时性的要求,所有它适用于读多写少的场景
特点:
- 读写分离
- 最终一致性
- 使用时另外开辟空间来解决并发冲突
- 读操作不需要加锁,写操作需要加锁
CopyOnWriteArraySet(相对于Set)
线程安全,与CopyOnWriteArrayList类似
ConcurrentHashMap
看知识点,这里不做赘述
JUC之AQS
AQS底层使用双向链表是队列的一种实现,因此也可以当做一个队列
AQS同步组件
- CountDownLatch
- Semaphore:能够控制同一时间并发线程的数目(具体看信号量知识点)
- CyclicBarrier
- ReentrantLock
- FutureTask
CountDownLatch
使用场景:当程序执行需要等待某个条件完成后才能继续执行后续的操作
调用该类await()的线程会一直处于阻塞状态,直到其他线程调用countDown()方法,使当前计数器变为0,每次调用countDown()时会让计数器的值-1,当减到0时,所有因调用await方法处于等待的线程就会继续往下执行
Semaphor
看信号量知识点
CyclicBarrier
它的功能和countDownLatch类似,他们的区别在于:
- CountDownLatch只能使用一次,CyclicBarrier的计数器可以使用reset方法重置
- CountDownLatch是一个或多个线程等待其他线程完成的关系;CyclicBarrier是多个线程等待,直到都满足了某个关系后再都运行
ReentrantLock
可重入锁,同一线程,外层函数获得锁之后,内存递归函数仍有获得该锁的机会不受影响,synchronized和reentrantlock都是可重入锁
他们的区别:
- synchronized依靠jvm实现,ReentrantLock是jdk实现的
- ReentrantLock需要手动声明加锁释放锁,synchronized由编译器决定锁的加入和释放,所以ReentrantLock灵活度更高
*** ReentrantLock可以指定是公平锁还是非公平锁
FutureTask
Future接口:可以得到别的线程方法的返回值
FutureTask:实现了两个接口,Future和Runnable,所有它既可以作为Runnable被线程执行,也可以作为Future作为Callable的返回值
应用:一个很费时的逻辑需要计算并有返回值的时候,同时这个歌值又不是马上需要,那么可以使用上面的两个组合,用另外一个线程计算返回值,而当前线程在使用这个返回值之前可以做其他的操作,等需要这个返回值时再通过Future得到