1 CAS--Compare And Swap
最原初的锁,比较然后交换。Cpu硬件保证的原子性。抢不到就一直抢=自旋=while(if)
java.util.concurrent.atomic包下用的就是CAS。
这种不将资源锁起来就操作的锁叫做乐观锁。八股文喜欢说CAS有什么问题,进而引出ABA问题,但我觉得这样引出容易误导,ABA问题本就不是CAS考虑范围内的,而不是一开始有什么设计缺陷。
AtomicStampedReference
当要求改的是1时刻的A变量,但实际上2时刻的A被另一线程改成B,最后又改为A(ABA问题)。CAS不知道,只看变量对上了就去执行,就出错了。就引入了一个版本号机制,多判断一个版本号(也可以用时间戳)。Java有AtomicStampedReference实现了版本号控制的CAS。
2 AQS--AbstractQueuedSynchronizer
被称为悲观锁,就是默认难抢到,也就意味着要维护一个表示着资源数量的state(上锁),一个放在待处理的线程的队列FIFO。还有一个待实现的各具特色的处理释放资源的方法。
状态state
state是volatile修饰的,并被并发修改,所以修改state的方法都需要保证线程安全。
FIFO队列
- AQS会维护一个等待的线程队列,把线程都放到这个队列里,这个队列是双向链表形式。
- AQS就是”排队管理器”,当多个线程争用同一把锁时,必须有排队机制将那些没能拿到锁的线程串在一起。当锁释放时,锁管理器就会挑选一个合适的线程来占有这个刚刚释放的锁。
实现获取/释放等方法
这里的获取和释放方法,是利用AQS的协作工具类里最重要的方法,是由协作类自己去实现的,并且含义各不相同;需要每个实现类重写tryAcquire和tryRelease等方法。
Semaphore
- 维护一个许可证的数量(state)用于控制同时访问某个共享资源的线程数量;
- 获取就是acquire方法,作用是获取一个许可证,释放就是release方法。
- 一般用于对某资源的限量并发访问上。
CountDownLatch
用于让线程等待其他线程先完成操作在执行。一般用于线程要依赖其他线程完成后才能动的情况。
- 初始化计数器: 创建CountDownLatch 时指定一个初始计数值(state)。
- 等待线程阻塞: 调用 await()的线程会被阻塞,直到计数器变为0。
- 任务完成通知: 其他线程完成任务后调用 countDown(),使计数器减1。
- 唤醒等待线程: 当计数器减到0时,所有等待的线程会被唤醒。
ReentrantLock
用于独占加锁、解锁,支持同一线程反复加锁(可重入)。
- 初始化锁状态:底层基于 AQS,state=0 代表无锁空闲;创建对象可指定是否为公平锁。
- 可重入计数:同一线程再次加锁,直接把 state+1,不阻塞、不竞争,靠 state 记录重入次数。
- 手动解锁:调用
unlock(),每次把 state 减 1;必须逐层解锁,直到 state 归 0 才算完全释放锁。 - 唤醒排队线程:当 state 降到 0,AQS 会唤醒等待队列里的下一个线程,继续抢占锁执行。
- 扩展等待唤醒:可创建多个
Condition,实现精准分组等待、精准唤醒,替代笼统的 wait/notify。
CyclicBarrier
CyclicBarrier 允许一组线程互相等待,直到到达一个公共的屏障点。
当所有线程都到达这个屏障点后,它们可以继续执行后续操作,并且这个屏障可以被重置循环使用。
3 Java 关键字
Synchronized
java提供优化过的锁。自适应改变加锁机制,底层是用C++写的
- 无锁:还未有线程来过。
- 偏向锁:也就是默认只有一个线程会来抢,所以只记线程ID(第一次也还是CAS去抢ID),之后对上ID就用
- 轻量锁:其实就是少量线程开始并发了,CAS去抢锁。
- 重量锁:CAS自旋抢不到浪费CPU资源,就会进入阻塞休眠
_owner // 当前持有锁的线程
_recursions // 重入次数计数器!!!
_EntryList // 抢不到锁,阻塞排队的线程(入口队列)
_WaitSet // 调用wait(),休眠等待的线程(等待队列)
_cxq // 竞争自旋队列- 这个控制机制锁膨胀机制,不可回退。
- 为非公平锁,当新线程进来时先CAS抢,抢不到再进Entry队列,wait()方法主动休眠,notify()随机唤醒一个,notifyAll全部唤醒一个竞争到。
- 可重入机制
volatile
volatile关键字可以保证可见性,但不能保证原子性,因此不能完全保证线程安全。volatile关键字用于修饰变量,当一个线程修改了volatile修饰的变量的值,其他线程能够立即看到最新的值,从而避免了线程之间的数据不一致。
实现线程启动
- 继承Thread抽象类,重写run方法,start启动
- 实现Runnable,重写run方法,start启动
- 实现Callable接口与FutureTask
- 线程池Executor
线程方法
- interrupt()方法,如果在运行,打上中断标记,如果睡了就唤醒抛出异常。
- stop()暴力停止。
- sleep()睡觉,让cpu,不让资源
- wait和notify方法必须实现synchronized!
ThreadLocal
每个线程有一个私有空间,为ThreadLocalMap。以ThreadLocal为key,值为value存数据。
4 Executors
先创建核心线程池,然后队列,队列也满了创建非核心线程,全满了就拒绝。非核心线程空闲后会被回收。
线程池的创建
- 还有线程池的使用原则:不能创建后不关闭,否则会导致线程泄露,JVM无法退出;
- 任务队列的容量要合理设置,太大可能导致内存溢出,太小容易触发拒绝策略;
- CPU密集型:corePoolSize=CPU核数+1(避免过多线程竞争CPU)
- IO密集型:corePoolSize=CPU核数x2(或更高,具体看IO等待时间)
- 线程submit 和 execute 的区别在于 submit 能提交Callable有返回值,还能通过 Future捕获任务执行中的异常,而execute只能提交Runnable,异常会直接抛出。
//获取CPU核心数 用于合理设置线程数
int corePoolSize = Runtime.getRuntime().availableProcessors() * 2;
//手动配置线程池
ThreadPoolExecutor threadPool = new ThreadPoolExecutor(
corePoolsize,// 核心线程数 线程池长期维持的最小线程数
corePoo1Size*2,//最大线程数线程池能容纳的最多线程数
60L,//非核心线程存活时间,多久后销毁
TimeUnit.SECONDS,//存活时间单位
new ArrayBlockingQueue<>(100),//任务阻塞队列 核心线程忙时 新任务存这里
Executors.defaultThreadFactory(),//线程创建工厂 用于设置线程名 优先级等
new ThreadPoolExecutor.AbortPolicy()
);// 拒绝策略 队列满且线程数达最大时 如何处理新任务拒绝策略
- 默认的 AbortPolicy(直接抛异常)
- CallerRunsPolicy (让提交任务的主线程自己执行,缓解压力)
- DiscardPolicy(直接丢弃新任务)
- DiscardOldestPolicy(丢弃队列里最旧的任务,再提交新任务),
- 自定义拒绝策略,通过实现接口可以自定义任务拒绝策略。
线程池种类
- 看眼就好,理解下设计思路,阿里开发手册规定必须自己new ThreadPoolExecutor
- ScheduledThreadPool: 可以设置定期的执行任务,它支持定时或周期性执行任务,比如每隔10秒钟执行一次任务。
- FixedThreadPool: 它的核心线程数和最大线程数是一样的,所以可以把它看作是固定线程数的线程池,它的特点是线程池中的线程数除了初始阶段需要从0开始增加外。
- CachedThreadPool: 可以称作可缓存线程池,它的特点在于线程数是几乎可以无限增加的,而当线程闲置时还可以对线程进行回收。使用SynchronousQueue,队列的容量为0,实际不存储任何任务,它只负责对任务进行中转和传递,所以效率比较高。
- SingleThreadExecutor: 使用唯一的线程去执行任务。这种线程池由于只有一个线程,所以非常适合用于所有任务都需要按被提交的顺序依次执行的场景。
- SingleThreadScheduledExecutor: 它实际和 ScheduledThreadPool 线程池非常相似,它只是ScheduledThreadPool 的一个特例,内部只有一个线程。