1. 可见性, 原子性与有序性
1.1 可见性
volatile 是java的一个关键字, 提供了一种轻量级的同步机制, 能保证可见性, 但不保证原子性, 通过禁止指令重排来保证有序性.
可见性: 通过实现缓存一致性协议来保证
每个处理器通过嗅探在总线上传播的数据来检查自己缓存的值是不是过期了,当处理器发现自己缓存行对应的内存地址被修改,就会将当前处理器的缓存行设置成无效状态,当处理器对这个数据进行修改操作的时候,会重新从系统内存中把数据读到处理器缓存里。因此,经过分析我们可以得出如下结论:
- Lock前缀的指令会引起处理器缓存写回内存;
- 一个处理器的缓存回写到内存会导致其他处理器的缓存失效;
- 当处理器发现本地缓存失效后,就会从内存中重读该变量数据,即可以获取当前最新值。
这样针对volatile变量通过这样的机制就使得每个线程都能获得该变量的最新值。
JMM(java内存模型)规定:
1.线程解锁前, 必须把共享变量的值刷新回主内存
2.线程加锁前, 必须读取主内存的最新值到工作内存
3.加锁和解锁必须为同一把锁
1.2 原子性
如何保证原子性: i++这种操作并不是原子性操作, 所以要么加锁(如synchronized, Lock) 要么使用AtomicXXX类;
AtomicXXX类实现原理是CAS
1.3 有序性
为了保证有序性, volatile利用内存屏障(memory barrier) 来实现禁止指令重排
内存屏障是一种cpu指令, 作用有两个:
一是保证特定操作的执行顺序 二是保证某些变量的内存可见性
内存屏障的另外一个作用是强制刷出各种cpu缓存的数据, 因此任何cpu上的线程都能读取到这些数据的最新版本
volatile 的应用: 用DCL(双端检测加锁)机制实现的单例, 可以把单例对象加上volatile关键字来保证不会出现因为无序写入导致的部分构造现象
https://www.jianshu.com/p/920d7b18ef25 为什么出现这个问题
https://www.cnblogs.com/ngy0217/p/9006716.html 单例模式实现的几种方式
2. CAS
CAS 比较并交换, 是一种原子操作, 是一条CPU的并发原语, 也可以说是一种乐观锁, 原语是由若干条指令组成的, 原语的执行必须是连续的, 执行过程中不允许被打断, 是CPU的原子指令.
https://segmentfault.com/a/1190000013127775 Java CAS 详细介绍
JUC里面Atomic开头的类, 实现原子性操作就依赖于UnSafe类, UnSafe类可以直接像C的指针一样操作内存中的数据.
CAS缺点:
1:循环时间长的话, 开销比较大.
2:只能保证一个共享变量的原子操作, 多个变量无法保证操作原子性(可以用锁).
3:ABA问题, 如果线程1取出的值为A, 然后线程2随后取出值改为B, 然后又改回A, 然后线程1进行CAS, 就会出现问题.虽然表面上没动过, 值还是A, 但是是已经被替换过的了.
解决ABA问题: 加入一个版本号, 只要修改, 版本号就更新, 比较的时候除了比较值, 还需要比较版本号, 都相同才能继续, 参考 AtomicStampedReference 类.
3. 并发集合类
故障: java.util.ConcurrentModificationException 并发修改异常
解决方案:
1.new Vector(); 底层为加锁synchronized
2.Collections.synchronizedList(new ArrayList()); 底层还是加锁synchronized
3.new CopyOnWriteArrayList(); 底层为使用ReentrantLock, 写时复制.先复制一份新数组, 修改元素, 然后把引用指向新数组..
https://www.cnblogs.com/takumicx/p/9338983.html ReentrantLock介绍
4. 锁
4.1 公平锁和非公平锁
如果一个线程组里,能保证每个线程都能拿到锁,那么这个锁就是公平锁。相反,如果保证不了每个线程都能拿到锁,也就是存在有线程饿死,那么这个锁就是非公平锁。公平的ReentrantLock使用了FIFO队列来保证公平
4.2 可重入锁(递归锁)
指的是同一线程外层函数或者代码获得锁之后, 内层函数或者代码仍然能够获得这把锁, 也可以说, 线程可以进入任何一个它已经拥有的锁 所同步着的 代码块.
可重入锁最大的作用是避免死锁.
ReentrantLock为可重入锁, 而且比原生的Synchronized多增加了一些功能, 比如 Condition, 可以精确阻塞\唤醒线程
4.3 自旋锁(SpinLock)
尝试获取锁的线程不会立即阻塞, 而是循环不停的去尝试获取锁, 这样的好处是不用进行线程上下文切换, 代价是消耗cpu资源.
4.4 独占锁(写锁) 共享锁(读锁) 互斥锁
独占锁: 同一时间只能被一个线程所持有, ReentrantLock, Synchronized都是独占锁.
共享锁: 该锁同一时间可以被锁个线程所持有. ReentrantReadWriteLock的读锁是共享锁, 写锁是独占锁.
互斥锁: 同一时刻只能有一个线程访问该对象, 无论读写.
读写锁和互斥锁的区别:
有的资源同时只允许一个访问,无论读写;于是我们抽象它为“互斥锁”。
有的资源同时只允许一个修改、但可以允许许多个读取(读取时不得写入);于是我们抽象它为“读写锁”。
4.5 其他用锁实现的类
CountDownLatch: 计数类, 传入一个初始值如10, countDown()方法每次调用都会让值减一, await()方法会阻塞直到值减为0才被唤醒.
CyclicBarrier: 与上面刚好相反, 传入一个初始值如10, await()方法会阻塞, 直到await()方法被调用10次之后, 所有调用await()的线程才被唤醒继续执行. 如果传入第二个Runnable参数, 则调用10次之后会执行实现的Runnable参数. 这个类可以被循环使用, 即调用10次之后, 还可以再继续被使用.
5. 阻塞队列
5.1 概念
当阻塞队列是空时, 从队列中获取元素的操作将会被阻塞
当阻塞队列是满时, 往队列里添加元素的操作将会被阻塞
所谓阻塞, 在某些情况下会挂起线程(即阻塞), 一旦条件满足, 被挂起的线程又会被唤醒.
有了阻塞队列(BlockingQueue) 就不需要关心什么时候阻塞线程, 什么时候需要唤醒.这一切都在阻塞队列里被实现.
5.2 实现了BlockingQueue接口的类
6. AQS
放个链接在这, 有空再整理
7. 线程池
7.1 线程池优势
1.降低资源消耗, 通过重复利用已创建的线程降低线程创建和销毁造成的消耗
2.提高响应速度, 当任务到达时, 任务可以不需要等待线程创建马上就可以执行
3.提高线程的可管理性, 线程是稀缺资源, 如果无限制创建, 会消耗系统资源, 降低稳定性, 使用线程池可以进行统一的分配, 调优和监控
7.2 线程池的类型
newScheduledThreadPool: 可以执行延时性和周期性的任务
newFixedThreadPool: 固定线程数量的线程池
newCachedThreadPool: 不固定线程数量的线程池, 适用于短时间执行数量巨大的任务
newSingleThreadExecutor: 只有一个线程的线程池
以上线程池的创建都基于 ThreadPoolExecutor
7.3 线程池的详细参数(ThreadPoolExecutor)
int corePoolSize: 线程池中的常驻核心线程数
int maximumPoolSize: 线程池能容纳的最大线程数, 如果核心线程全部在工作且阻塞队列满了的话, 则会创建额外线程
long keepAliveTime: 多余线程的空闲存活时间, 如果当前线程池线程数量超过corePoolSize并且某个线程空闲时间达到keepAliveTime时, 这个空闲线程会被销毁, 直到剩下corePoolSize个为止
TimeUnit unit: keepAliveTime的时间单位
BlockingQueue workQueue: 阻塞队列, 用来存储被提交但尚未被执行的任务
ThreadFactory threadFactory: 用来产生线程的工厂
RejectedExecutionHandler handler: 如果线程都在工作且阻塞队列满了且当前线程数量已经达到maximumPoolSize 则执行拒绝策略
https://www.cnblogs.com/eric-fang/p/11584142.html jdk内置的4种拒绝策略
https://www.cnblogs.com/yixianyixian/p/7723267.html 线程池的配置策略(CPU密集型/IO密集型) 线程池的监控