目录
10个线程对volatile int count就行++,每个线程++ 1000次,最后结果小于10000,why?怎么解决
AQS(AbstractQueuedSynchronizer) 同步器
CountDownLatch、CyclicBarrier、Semaphore用途、区别
线程
线程状态转化
进程线程区别
进程:资源调度单位
线程:任务调度单位
线程间通信 6
信号、信号量、管道、消息队列、共享内存、socket(套接字)
线程池
创建线程池方法 5个
newFixThreadPool
newSingleThreadPool
newCachedThreadPool
newScheduledThreadPool
newWorkStealingThreadPool(抢占线程池,不是按照顺序执行,jdk1.8加入的)
ExecutorService executor = Executors.newFixedThreadPool(5)
// 对应源码
public static ExecutorService newFixedThreadPool(int nThreads) {
return new ThreadPoolExecutor(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>());
}
线程池初始化参数 6
corePoolSize:核心线程数量;
maxPoolSize:最大线程数量;
keepAliveTime:在线程keepAliveTime空闲后,释放该线程。
workQueue:线程缓冲队列;
拒绝策略:当队列满了,如何处理?
线程缓冲队列实现 3种
ArrayListBlockingQueue(缓冲区大小固定)
LinkedBlockingQueue(缓冲区无限)
SychronousQueue(同步队列,无缓冲区)
拒绝策略 4种
Abort:抛异常,默认
Discard: 直接丢弃
CallerRun:提交该线程的线程来执行
DiscardOldest:丢弃最早提交的
线程池提交任务方法 2种
submit: 需要获取任务返回值 Future对象
execute
线程池任务执行流程
ThreadLocal
https://www.huaweicloud.com/articles/1092806deae3093d5440ff69f161d556.html
线程局部变量,线程间数据隔离
如下图,每个线程有一个自己的ThreadLocalMap。在开启一个新的线程时,会初始化该线程的ThreadLocalMap,如果有ThreadLocal变量set()操作,则把该ThreadLocal的弱引用作为key,value为设置的值。
ThreadLocal线程之间如何实现数据隔离?
每一个线程都一个各自的Map存放map,这样每次获得线程,再获得对应map,从map中获取对应的value即可。
ThreadLocal为什么会造成内存泄露?怎么处理?
因为Map的key是一个弱引用,在GC的时候,不管内存空间足不足都会回收弱引用的对象,如果此key外部没有强引用,则垃圾回收了key,因为该线程没有结束,则value是强引用还存在,出现了内存泄漏。
不过弱引用有一个好处就是在下次ThreadLocalMap get、set、remove的时候,key为null的value都会被清理。
1). 不使用的时候,及时主动调用remove方法进行清理 remove();
2). 将ThreadLocal变量定义成private static,这样就一直存在ThreadLocal的强引用(如上图,static放在方法区,即ThreadLocal ref在方法区指向堆中的ThreadLocal,有了强引用,只有类被回收才会被回收),只有在线程结束之后
https://blog.youkuaiyun.com/JH39456194/article/details/107304997
线程安全
线程安全三要素
原子性:操作不可分隔
可见性:操作对于所有线程可见
有序性:不能指令重排
Volatile
支持可见性、有序性,但是不支持原子性。
volatile修饰的变量,在读取的时候都会从主内存读取,更新时都会从java线程的工作内存更新到主内存。保证读到的都是最新的。
10个线程对volatile int count就行++,每个线程++ 1000次,最后结果小于10000,why?怎么解决
因为count++分为:从主内存读取count值、count+1、写入工作内存&主内存
线程1从主内存读取count值,假如为23,被阻塞;
线程2从主内存读取count值23,执行+1,写入主内存24;
线程1开始执行,count+1,也是24,所以结果小于10000。
解决:
使用AtomicInteger、sychronized、lock等
使用场景,举例
必须同时满足:2
1. 变量赋值不依赖当前值;
2. 该变量不能用于有其他变量的不等式中。
举例:
1. 状态标记量,如A线程while( !flag){do something}; B线程 flag=true。让A线程跳出循环
2. 双重检查的单例
class Singleton{
private volatile static Singleton instace;
public static Singleton getInstance(){ // 没有使用同步方法,而是同步方法块
// 1. 第一次nu11检查 ,利用volatile的统程间可见性,不需要加锁,性能提高
if(instance == null)(
synchronized(Singleton.class) { //锁住类对象,阻塞其他线程
// 2. 第二次nu11检查,以保证不会创建重复的实例
if(instance == null)(
instance = new Singleton(); //volatile禁止重排序
}
}
return instance;
}
instance = new Singleton()包括3步:
1)memory = allocate0; //分配内存空间是new指令的一部分
2)< init >(memory); //初始化对象对应invokespecial调用对象自定义初始化方法
3)instance = memory; //返回分配的内存地址引用是new指令的一部分
2、3可以重排,如果不使用volatile,其他线程在位置1的地方可能就得到一个没有初始化的对象 volatile禁止重排,所以必须初始化之后才能将内存地址复制给栈,所以得到的永远是一个合理对象
Synchronized
特性 5个
原子性、有序性、可见性、
可重入性(允许同一个线程反复获取同一个锁,如sychronized(a){sychronized(a){}},防止死锁,不然自己拥有了该对象的锁,再继续锁的时候如果不允许,则死锁了),通过程序计数器实现,进入count++,出来count--;
不可中断性:一个线程拥有了锁,其他线程只能等待或者阻塞,不可以被中断。
锁不同对象底层实现 3个
对象:对象头、实例数据、对齐填充。对象头包括:Mark word(对象hashcode、GC分代年龄、锁相关信息)、对象指针、如果数组还有数组长度。通过Mark word中锁相关信息实现synchronized,包括锁状态标示(重量级锁、轻量级锁、偏向锁)、持有的锁指针、偏向锁id等。当使用该对象的时候,具体过程见锁优化。
方法:在方法标示增加常量ACC_SYNCHRONIZED, 进入方法的时候会检查是否有ACC_SYNCHRONIZED,如果有,则会隐式调用monitorenter、monitorexit
代码块:在锁的对象前后,monitorenter、monitorexit;针对可重入性,采用程序技术器实现,进入count++,出来count--,直到count=0,才可以被其他线程使用
锁优化 5种
因为锁很重,会有用户态、内核态切换,很耗时耗资源,因此做了锁优化。
1. 自旋锁or自适应锁:忙等待,因为挂起线程恢复线程很长,同步时间短,则可以做一个忙等待;
2. 锁消除:不需要锁的地方去除锁;
3. 锁粗化:同一个代码块多个锁合并为一个锁;
4. 轻量级锁:基于CAS思想,因为大多数情况下都不会出现竞争,标记锁(在对象Mark word中标记为轻量级锁),但是不实际加锁,只有在出现资源冲突的情况下再加锁。线程在使用该对象的时候,先将该对象的Mark word复制出来一份Mark record,然后将record指针写回到该对象Mark word中,如果写回成功(即锁指针的地方为空或者和当前线程相同),则该线程拥有了该锁,如果失败,则自旋锁,自旋结束后还没有获的锁,则膨胀为重量级锁。
5. 偏向锁:默认资源先分给某个线程(在对象Mark word中标记为偏向锁),如果有其他线程访问但是没有竞争,则膨胀为轻量级锁,如果出现竞争,则自旋锁,自旋结束后还没有获的锁,则膨胀为重量级锁。
Lock
lock五个方法
lock(): 获取锁, 如果被其他线程使用,则等待
tryLock(): 尝试获取锁,如果lock成功返回true,失败返回false;
tryLock(time): 在一段时间内尝试获取锁,如果lock成功返回true,失败返回false;
lockInterruptibly(): 获取锁如果需要等待则中断等待
unlock:释放锁
AQS(AbstractQueuedSynchronizer) 同步器
https://www.cnblogs.com/waterystone/p/4920797.html
采用FIFO队列实现,双向链表,节点Node中用一个waitStatus来表示当前线程状态,只有为SIGNAL为才能被挂起
Lock
本质调用的acquire(1)函数
public final void acquire(int arg) { // tryAcquire(arg) 获取锁,成功true,失败false // addWaiter(Node.EXCLUSIVE)将线程加入到队尾 // acquireQueued:是否挂起了线程,如果挂起返回true,执行中断。没有挂起 返回false if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) selfInterrupt(); }
tryAcquire 获取锁,获取成功true,返回;获取失败,加入队尾addWaiter,根据当前情况执行或者等待acquireQueued,如果执行过程中发生中断返回true,做一个中断响应。
说明:
1. tryAcquire不同子类实现不同
NonfairLock非公平锁先CAS获取锁,成功返回true,失败判断当前资源拥有的线程和该线程是否一致,如果一致,则返回成功true,反之false;
FairLock在CAS之前需要判断当前线程是否在等待队列最前面(前面无等待或者等待的就是该线程),如果该线程优先级高,再执行CAS,再当前资源拥有的线程和该线程是否一致,后面与NonfairLock一致;
2. 整体流程
1)tryAcquire判断是否可以获取锁,如果可以获取,返回。如果不可以获取
2)生成新node节点,CAS插入队尾(addWaiter):如果tail不为空,则将该节点CAS插入tail,如果tail为空,插入一个空节点作为head,再将该节点插入空节点后面;返回新生成节点;
3)当前节点插入队尾之后,不会马上挂起该节点,而是执行一个自旋acquireQueued,检查该节点pred节点是否为head,如果是head则尝试获取锁,获取成功则将当前节点设置为head,返回false。
如果仍然没获取到锁,判断当前线程是否需要挂起,如果需要挂起(prev的waitState为signal)则挂起线程LockSupport.park(this)。线程挂起之后就一直等待线程,被唤起(unpack)后,如果prev是首节点则重新尝试获取锁,获取成功,则返回false;如果被唤起是因为中断,返回true;
4)如果线程被中断过,现在线程获取了锁,所以执行selfInterrupt()只是增加一个线程曾经中断过的标示(相当于中断响应,因为在步骤3中pack之后被中断interrupt是没有响应的,所以这里补上)
unLock
1. tryRelease释放当前线程占有资源;
2. 找到队列中第一个有效节点线程,unpack它
共享锁
lock、unlock和独占锁基本一致,lock中将中断响应selfInterrupt放在了自旋中,同时在当前线程获取锁之后如果还有剩余资源会唤醒后续线程;独占锁unlock需等待完全释放资源再唤起其他线程,共享锁无限制。
ReentrentLock
唯一一个实现lock的class,可重入锁。
1. 公平锁:谁等待时间长锁先给谁,new ReentrentLock(true);默认非公平
2. 可中断:lockInterruptibly()
ReentrentReadWriteLock
implement ReadWriteLock,可以通过lock.readLock()获取读锁,lock.writeLock()获取写锁。
如果都是读,不影响,如果读的过程中,另一个线程写,则需要等读完再写;如果写读、写写,都要等。只有读读不需要。
StampedLock
支持读读、读写并行,效率更高,不可重入。读的时候获取乐观锁tryOptimisticRead()(其实没有加锁,获取的是版本号),读完之后,验证版本号validate()是不是和当前一致,如果一致,则做读后操作,如果不一致,获取读悲观锁readLock(), 重新读,释放锁unlock()。
注意:
1. 保证tryOptimisticRead()和validate()代码块之间读的准确性,后续保证不了;
2. 乐观锁其实没有真正锁,获取的是版本号,所以不需要释放。
sychrnonized和lock区别 6点
1. sychronized是关键字,Lock是一个对象;
2. sychronized可以锁对象、类、代码块;Lock只能代码块;
3. sychronized不可中断,lock可以中断;
4. sychronized自动释放,lock需要手动unlock释放;
5. sychronized 非公平锁,lock可以ReentrantLock控制是否为公平锁;
6. 读写锁可以提高读效率。
JUC
CountDownLatch、CyclicBarrier、Semaphore用途、区别
CountDownLatch是倒计时锁 ,是减法,只能减少。初始化countDownlatch大小(new CountDownlatch(3)),让一些线程阻塞(countDownlatch.await()),直到另一些线程完成一些操作之后才唤醒(countDownlatch.countDown(), 当相当于-1,等countDownlatch变为0之后,就会唤醒)。
应用于秒杀(countlatch = 1,每个线程await,等时间到的时候countDown,则所有线程开始),或者多个线程执行结束后进行汇总。
CyclicBarrier是条件满足器,是加法,只能增加。初始化cyclicBarrier为需要满足的线程数以及满足后的操作(new CyclicBarrier(3,() -> {System.out.println("开始干活了");})),一组线程通过屏障(cyclicBarrier.await(),计数++,等计数为3时则做相应操作)。
Semaphore是信号量,可加可减。初始化semaphore大小(new Semaphore(3)),semaphore.acquire() 信号量--,semaphore.release 信号量++。这样可以用来实现线程间共享多个资源,或者并发线程数控制。 比如停车场