基础概念

什么是进程 , 什么是线程 ?

进程是一个服务 也就是一个程序的动态表现 线程是进程中的最小执行单元

创建线程的方式

  • 从Thread类继承

  • 实现 Runnable接口

线程的sleep/yeild/join

sleep : Thread#sleep() 不会释放锁 但是 Object#wait()会释放锁 睡眠结束完后 也会回到就绪队列

yeild : 本线程进入等待队列 , 让出一下CPU

join : 让本线程 跑到被调用者线程中去运行 且要在调用线程执行完后 运行

线程的interrupt

线程中断获取响应

synchronized

同步关键字

hospot 实现 : 锁的应该是对象头 一个对象头有32/64位具体看JVM的位数 其中两位 用于标志锁

锁升级 : 无锁 - > 偏向锁 - > 轻量级锁(自旋锁 默认旋10次 如果还得不到锁 就升级) -> 重量级锁(操作系统锁 进入等待队列)

什么时候用自旋锁 ? 什么时候用重量级锁 ?

线程数量少 并且锁定的业务代码运行时间短 就用自旋锁

线程数多 并且等待时间长的 就用重量级锁

可重入

程序执行出现异常时 默认情况下 锁是会被释放的

同步方法和非同步方法 是可以同时执行的

当做锁的对象一定要加final 并且不能用String作为锁对象

volatile

一致性 , 可见性 ( 保证线程可见性 )

底层使用MESI(CPU缓存一致性协议)

保证Jvm指令不会被重排序( 禁止指令重排序 )

底层使用cpu读写屏障来完成的 loadFence storeFence (CPU原语)

volatile修饰引用对象其实是没有什么意义的 引用对象内的数据改变了 其他线程是没法看见的

CPU缓存一致性协议(MESI)

现代CPU的缓存一致性是通过缓存锁 + 总线锁实现
CPU缓存一共有三层
  1. L1 速度非常快 存的数据也很少 本人电脑中 只有256k

  2. L2 速度慢于L1 本人电脑中 只有1M L1 和L2 是CPU独享的 在CPU内部

  3. L3 在主板上 CPU共享 在本人电脑中 只有6M

缓存行

为了提高效率 读取数据的时候 会一次性读取缓存行大小的数据 一般为 64字节

伪共享

同一缓存行的两个不同数据, 被两个不同的CPU锁定 产生互相影响的伪共享问题

解决方法 : 缓存行对齐

CAS(自旋)

各种Atomic类就是使用了CAS保证线程安全的

ABA问题 如果是基础数据类型无所谓 但如果是引用类型 就可能有问题了

解决 : 加一个版本号 一起判断 就OK

所有的CAS 都是使用Unsafe类

JUC同步工具

各种类型锁

ReentrantLock 可重入锁

底层使用的是CAS

  加锁时 Lock.lock() 最后需要手动解锁
synchronizedReentrantLock
重入 可重入 可重入
解锁 可自动解锁 需要手动解锁
维护队列 只有一个队列 可有多个队列 (Condition)
    可尝试获取锁 tryLock() 不管有没有获取都立即返回
公平锁 只有非公平锁 有公平与非公平的切换
  ReentrantLock.unlock() -> 调用 AQS.release(int args)

AQS.release() -> AQS.tryRelease(int arg)调用具体的实现类 -> ReentrantLock.Syn.tryRelease(int arg) 该方法中主要做了以下几件事
1 : int c = getState() - releases 获取state 并且减 arg
2 : 判断c == 0 ? setExclusiveOwnerThread为空 即无线程获取锁
3 : setState(c); 将state值更新

CountDownLatch 门闩

await() 线程阻塞

countDown() 减数 当门闩减到0的时候线程会继续运行

CyclicBarrier 循环栅栏

初始化时 可传入两个参数 ( 数量, 当满了时调用的Runnable)

Phaser 阶段

ReadWriteLock 读写锁

其实也就是共享锁和排他锁

Semaphore 信号灯

可用于限流

Exchanger 交换器

用于两线程之间数据交换 调用exchange() 阻塞 等待第二方调用exchange()时 都会放开阻塞

LockSupport

底层使用的是UnSafe实现的

wait/notify/notifyAll

wait()会释放锁

看Object源码得知

fail-fast or fail-safe

fail-fast(快速失败)

在使用迭代器对集合对象进行遍历的时候,如果A线程对集合进行遍历,正好B线程对集合进行修改(增加、删除、修改)则A线程会抛出ConcurrentModificationException异常。

java.util 下的集合都是快速失败的 不支持并发修改

fail-safe(安全失败)

采用安全失败机制的集合容器,在遍历时不是直接在集合内容上访问的,而是先复制原有集合内容,在拷贝的集合上进行遍历。由于迭代时是对原集合的拷贝进行遍历,所以在遍历过程中对原集合所作的修改并不能被迭代器检测到,所以不会触发Concurrent Modification Exception,例如CopyOnWriteArrayList。

java.util.concurrent 下的集合都是安全失败的 支持并发修改

同步容器

ConcurrentHashMap

ConcurrentSkipListMap

底层实现使用了跳表

CopyOnWriteArrayList

写时复制 当需要添加元素的时候 复制一份原数组 然后操作复制出来的 最后将修改后的数据 set回去

其主要用在读比较多 写比较少的情况下

写操作是线程安全的 读的时候没有加锁

CopyOnWriteArraySet

Queue

Queue  
insert offer 如果插入没有成功 就会返回false add 如果插入没有成功 就会抛异常
remove poll 如果队列没有元素了 就会返回null remove 如果队列没有元素了就会抛异常
Examine element 只是检索队列 不改变队列 为空返回异常 peek 只检索队列 不改变队列 为空返回null

BlockingQueue 阻塞队列

BlokingQueue 
insert put 如果队列满了 会阻塞 最大值为Integer.MAX_VALUE
remove take 如果队列空了 会阻塞
priorityQueue 优先队列

内部实现了一个排序 内部是一颗树 二叉小顶堆实现

最小堆

是一颗完全二叉树 其中 父节点一定小于叶子节点

leftNodeNo = parentNo * 2 +1

rightNodeNo = parentNo * 2 + 2

parentNodeNo = (nodeNo - 1) /2

插入节点的时候 算出节点下标 然后逐层比较 直到>= 父节点时

链接 : https://www.cnblogs.com/CarpenterLee/p/5488070.html

DelayQueue

按时间去排序的Queue 按理说 这已经打破了queue的规范了 队列存储的顺序 并不是插入顺序 而是比较后的顺序

一般用来 按时间进行任务调度

SynchronousQueue

这个queue 容量是0的

用来两个线程间传递数据的

TransferQueue

transfer(E e) 该方法 只有被其他线程消费了 才会继续往下走 否则会一直阻塞在那

Queue 和 List的区别 :

Queue添加了很多对线程友好的api 比如 offer poll peek

BlokingQueue 在Queue的基础上 又添加了一些api 比如 put take

put take 实现了阻塞 天然的实现了生产者消费者模型

 

边边角角

PriorityQueue 优先队列

扩容机制 : 如果oldCapacity < 64 ? 扩容两倍 + 2: 扩容50%

初始容量 : 11

ThreadLocal

每个Thread中都维护了一个ThreadLocal->ThreadLocalMap

ThreadLocal 有用在Spring的声明式事务 比如 将数据库连接放入ThreadLocal中

只要是一个事务中的 都是同一个数据库连接

对象四种引用 强/软/弱/虚

强引用

只要引用指向对象 对象就不会被回收

软引用 SoftReference

当一个对象只有一个软引用指向的时候 当系统内存不够用时 才会被GC回收

弱引用 WeakReference

当一个对象只有一个虚引用指向时 只要发生了GC 就会被GC回收

一般用在容器里

WeakHashMap

使用了弱引用

WeakHashMap中的Entry<K,V> K 是使用了弱引用

如果发生gc WeakHashMap中的key会被干掉

每次put的时候 都会清理一遍WeakHashMap

WeakHashMap 的默认初始容量 16 负载因子 0.75f 每次扩容为原本的两倍

ThreadLocal

Thread中维护着一个ThreadLocalMap

ThreadLocalMap中有一个Entry<K,V>

Entry继承WeakReference

  Entry(ThreadLocal<?> k, Object v) {
   super(k);
   value = v;
}
内部 : ThreadLocal 被一个弱引用指向着
外面 : 如果是直接new出来 被一个强引用指向 如果外面的强引用断开了
那么每当发生gc时 ThreadLocal就会被干掉

如果内部不用弱引用指向 那么会造成内存泄漏 ThreadLocal 永远不会被回收
但是 那也只是回收了Key value指向的对象 依旧存在
所以 使用完ThreadLocal后 必须要调用remove()方法
虚引用 PhantomReference

用于管理堆外内存的

new PhantomReference(引用 , 引用队列);

给写JVM的人用的

当与引用指向的对象被回收后 会在Queue中存放进一个对象

线程池

Callable

相当于有返回值的Runnable

Future

存储执行的将来才会产生的结果

get方法是阻塞的

FutureTask

Futrue + Runnable

Executor

主要做线程的执行接口

把线程的定义与线程的执行分开

ExecutorService

线程池的父接口

里面定义了一些生命周期方法和提交任务方法

submit方法是异步调用的

CompletableFuture

管理多个Future的结果

ThreadPoolExecutor

线程池的七个参数
  corePoolSize 
核心线程数
maximumPoolSize
最大线程数
keepAliveTime
当线程的数量大于内核时,这是多余的空闲线程在终止之前等待新任务的最大时间。
unit
时间单位
workQueue
任务队列
threadFactory
线程工厂
handler
拒绝策略 jdk一共提供了四种拒绝策略
1 : AbortPolicy 抛异常
2 : CallerRunsPolicy 在调用者的线程中执行任务
3 : DiscardOldestPolicy 丢弃队列头部的任务(其实也是等待最久的任务) 并且将当前任务尝试处理
4 : DiscardPolicy 不处理 抛弃掉
当执行被阻塞时使用的处理策略,因为达到了线程边界和队列容量
执行流程
  加入一个任务 先看核心线程 如果和心线程没满 起核心线程 
如果核心线程满了 查看任务队列 如果任务队列没满 加入任务队列
如果任务队列满了 起一个新线程去处理
如果达到了最大线程数 并且 任务队列也满了 那么启动拒绝策略
最大处理任务数

最大线程数 + 任务队列长度

ForkJoinPool

分叉出子任务执行 然后最后做汇总

分解汇合任务

用很少的线程可以执行很多的任务(子任务) ThreadPoolExecutor做不到先执行子任务

CPU密集型