线程创建关键的三种方式。
实现runnaable的run方法
继承thread类并重写run 方法
使用futuretask方式(callable)
好处:
继承的好处是方便传参,set或者构造。
runnable只能使用主线程的final变量
java不支持多继承,如果继承了thread,就不能继承其他类。
futuretask可以拿到返回结果。
wait
wait调用前要获取对象的监视器锁,synchronized(共享变量),或者调用它的方法加synchronized修饰
调用wait只会释放当前共享对象的锁,其他的监视器锁不会释放。
notify
被唤醒的线程不能马上从wait方法返回执行,需要获取监视器锁之后才可以返回,这个就涉及到一个竞争过程,
notifyall只能唤醒调用这个方法前的线程。
sleep
让出CPU不让出锁,返回后直接就绪状态准备竞争CPU。
yield
让出CPU使用权,直接就绪状态,线程调度器会从就绪队列里获取一个优先级最高的,也可能是更改让出的那个。
sleep线程会阻塞挂起指定的时间,yield方法线程只是让出自己剩余的时间片,仍然处于就绪状态。
线程中断
线程协作的方法。
B可以调用A的interrupt方法设置A的中断标志位true。
线程上下文切换的时机:时间片用完,被其他线程中断。
破坏死锁的条件只能破坏请求保持和循环等待。
守护线程和用户线程
守护线程不影响JVM退出,main线程运行结束后JVM会创建一个destroyjavaVM的线程等待所有用户线程终止后终止JVM进程。
ThreadLocal
每个线程内部有一个名为threadLocals的成员变量hashmap。线程的本地变量存在这里。
如果线程不消亡,本地变量一直存在可能造成内存溢出,所以使用完毕要remove。
inheritableThreadLocal
为了解决子线程不能获取父线程的变量的问题产生的。
共享变量问题
线程有自己的私有内存,对应实际情况多CPU多个cache的情况,副本可能不一致。
所以并发情况下必要时要通过让缓存失效,线程只能去主存读取一致的信息。
synchronized
同步锁,内部锁(监视器锁)
排他锁
内存语义:synchronized块内变量从工作内存删除,直接从主存读取,退出块时将共享变量刷新回主存。
回引起线程上下文切换并带来线程调度的开销。
volatile
弱同步
确保一个变量更新对其他线程马上可见。
线程写入变量时直接刷新回主存,当其他线程读取变量时会从主存重新获取新值。
写入时相当于线程进入synchronized同步块直接刷新变量到主存,读取时相当于进入同步块,清本地内存变量值,从主存获取最新值。
保证可见性但是不保证原子性。
写入变量不能依赖当前值,读写变量不加锁。
CAS
非阻塞的原子性操作
四个操作数:对象内存位置,对象中的变量的偏移量,变量的预期值,变量新的值
如果预期值是对的就替换,这是一个原子指令。
存在ABA问题,(A是A,但是A可能变成了B又变回了A)JDK为变量加入了时间戳,避免了ABA问题的产生。
unsafe类
反射才能获取。
指令重排序
单线程保证数据依赖顺序不会被打乱,但是多线程会有问题。
volatile变量确保volatile变量写之前的操作不会被重排序到写之后,读之后的操作不会重排序到读之前。
伪共享
多个变量放入一个缓存行,多线程多CPU同时修改这个缓存航,导致缓存失效,只能频繁访问下一级缓存查找。
缓存行读变量附近缓存行大小的元素一起放入(64字节)
JDK8之前用字节填充的办法解决,JDK8之后有一个contended注解(核心类才能用)
悲观锁
对数据被外界修改保持保守态度,认为数据很容易被其他线程修改,所以处理数据前对数据加锁,处理过程中数据处于锁定状态。
乐观锁
乐观锁一般在表中增加version字段或者业务状态,提交时才锁定,不会死锁。
公平锁和非公平锁
公平锁按照获取时间早晚获取锁,非公平随机。没有需求就非公平,公平需要开销。
独占锁和共享锁
独占锁是一种悲观锁,共享锁是一种乐观锁(共享读不影响数据一致性)
可重入锁
一个线程再次获取它自己已经获取的锁时不会被阻塞。synchronized内部锁是一个可重入锁。
关联一个计数器,自己重入计数器加一。
自旋锁
用cpu时间获取了线程调度的开销,很多情况下只需要自旋一小下就可以获取锁。而线程获取锁失败被挂起,获取锁成功被唤醒都需要切换内核状态,开销很大。
针对random类多线程下的缺陷。
老步骤:老种子生成新种子,新种子计算随机数,多线程会用同一个种子生成同一个数。所以要保证原子性,老种子用一次其他线程就不能用。使用一个原子变量来实现老种子,可以保证获取是正确的,然后计算新种子后,用CAS操作去更新种子,这样可能多个线程拿到同一个老种子,但是只有一个能成功更新,失败的线程就循环重新获取更新后的种子再计算。(可知大量竞争原子变量+大量线程自旋重试,降低了并发性能)
ThreadlocalRandom类似于ThreadLocal类,线程自己维护了自己的种子(一个普通的long变量)。
有两个原子性变量初始化调用线程的种子和探针变量会用到,只用一次。
线程安全的,种子放在线程里面的,所以实例里面都是线程无关的通用算法。
内部都是使用unsafe的getandaddLong,JDK7中是拿到当前值,然后加一,再CAS过程,JDK8把这个CAS内置在unsafe类了。
synchronized关键字是阻塞算法,CAS是非阻塞算法,但是存在高并发下自旋浪费CPU资源的问题。
JDK8新增的longadder类高并发下性能更好。高并发情况下维护多个cell。
创建和扩容是CAS操作。
** LongAdder**
三个值:base,锁和cell数组。锁是为了扩容和初始化的时候锁一下。base是基础数值,cell控制增减数值。
多个动态的cell共同维护一个数值的值。
内部用contended注解防止伪共享问题。
sum()并不加锁,所以返回的值并不是一个快照值,一边计算一边可能有变动。
LongAccumulator
比adder更强大,可以提供非0的初始值
可以指定累加规则,比如相乘
这俩都是JDK8新增的特性。
copyOnwriteArrayList
写时复制。
add加锁是原子性的,复制数组长度加一,新数组替换老数组。
get(index)并不加锁,因为写时复制,所以存在弱一致性问题,A获取数组B更新替换原数组,原数组引用计数不为0所以不会立即删除,A获取index还是从原来数组中获取的。
set加锁,remove加锁
迭代器的弱一致性:返回迭代器后,其他线程对list 的增删改查对迭代器是不可见的。
虽然传递的是引用,如果其他线程不改变那就是原本的数组。
如果其他线程改变了数组,替换之后,引用实际上是一个快照,因为有了新的引用和新的数组。
lockSupport
主要作用是挂起和唤醒线程,是创建锁和其他同步类的基础。
AQS
抽象同步队列:锁的底层支持,不同的锁要实现不同的acquire和release方法,并且对state状态的含义进行自己定义。
AQS的node队列,双向,哨兵。
多线程调用lock,一个获取锁,其他进入AQS队列自旋CAS尝试获取锁。
有公平和非公平两种实现,实现公平的方式就是获取锁之前会判断AQS队列中自己是否有前驱节点。
读写锁用state的高16位表示读状态,低16位表示获取到写锁的线程的可重入次数。
StampedLock锁
不变重入,提供了一个乐观读锁,多线程性能更好,不进行CAS操作设置锁的状态,只是简单测试位运算查看是否有线程持有写锁,然后获取stamp版本信息,操作前查看版本是否不可用,然后复制数据到方法栈,一致性可以保证,但是不能保证是最新,相当于读了一个快照。
concurrentLinkedQueue
线程安全的无界,非阻塞队列。
单项链表实现,两个volatile类型的node节点存放队列的首尾节点来维护这个队列。
简单来说还是volatile+CAS用CPU资源获取阻塞的开销,来实现非阻塞 。
SIZE不是很准确,因为不加锁,同理contains也不准确,需要遍历的都不大行。
出队入队都在头尾节点,volatile保证了可见性,offer和poll读使用了CAS操作,保证了原子性。
LinkedBloackingQueue
独占锁实现的阻塞队列(有界链表)(有界就有队列满的情况)
putoffer需要获取putlock,所以只有一个线程可操作。
size操作是准确的,因为有一个变量记录size,并且入队和出队操作是原子性的。
(非阻塞的没有这个size,因为不加锁的情况下,所以没办法简单维护这样一个变量)
ArrayBlockQueue
有界数组阻塞队列。
因为加锁,所以count等变量不需要volatile。
size操作也要获取锁后返回count,(直接读内存)这样就保证了内存可见性,通过加锁而不是volatile。、
PriorityBlockQueue
数组存储元素(物理上)
使用二叉树堆(逻辑上)维护元素优先级
take是阻塞的,用notempty条件变量实现。
put非阻塞,因为无界,所以不需要判满。
扩容先释放锁,其他线程可以入队出队,再CAS保证一个线程扩容成功,不能两个一起扩。
所有并发队列都是通过各种变量和锁保证put和get的原子性和可见性,在一致性上有时根据阻塞和非阻塞的情况有一定的牺牲。
DelayQueue
无界阻塞延迟队列。
每个元素都有一个过期时间,过期的元素才会出队。出队是判断如果没过期就要等待。
内部使用优先级队列。
解决两个问题:
一是执行大量异步任务时性能好,一直创建销毁线程开销。
二是提供了资源管理手段限制线程个数,可以动态新增,保留了统计数据完成任务等。
一个Integer记录线程状态和线程个数
线程池状态 running shutdown stop tidying terminated
加入任务时判断core是否达到最大,判断线程池状态是否可以在阻塞队列增加任务。
如果队列满,尝试新增线程(要控制是否达到最大线程数,还要双重检查线程池状态)
shutdown后不接收新任务,但是工作队列要执行。
shutdownnow 工作队列内也直接抛弃,正在执行的任务被中断,
CountDownLatch
相比于join方法,countDownLatch更灵活,首先建立这个对象,给一个计数,在子线程运行结束finally递减计数,减完主线程就复活。
相比于join更可控,并不一定要等到所有线程执行完,只要在子线程运行过程中控制递减计数即可。而且使用线程池时直接添加runable到线程池,没办法使用join。
底层使用AQS的state表示计数器的值。
主线程调用CountDownLatch的await方法后阻塞,计数器为0时调用AQS的doReleaseShared方法来激活线程。
CyclicBarrier
回环屏障。、
解决的问题是countdownlatch计数器是一次性的,不能重置
所有线程达到一个状态后再全部同时执行。
调用await方法的点就是阻塞点,也就是屏障点,线程都调用await方法后所有线程冲破屏障,继续运行。
Await 调用doawait方法获取阻塞的锁更改count。
相比于屏障,信号量有初始值,通过release方法加一,通过aquire方法获取,等待信号量值为aquire方法参数的值时才能返回。
更新剩余值的时候使用CAS操作。
公平与非公平两种实现,还是检查前驱节点是否还在等待。
非阻塞的共享信号量使用CAS