一、Volatile
1.1 可见性
- read(读取):从主内存读取数据
- load(载入):将主内存读取到的数据写入工作内存
- use(使用) :从工作内存读取数据来计算
- assign(赋值):将计算好的值重新赋值到工作内存中
- store(存储):将工作内存数据写入主内存
- write(写入):将store过去的变量值赋值给主内存中的变量
- lock(锁定) :将主内存变量加锁,标识为线程独占状态
- unlock(解锁):将主内存变量解锁,解锁后其他线程可以锁定该变量
1.2 禁止指令重排
也就是过在Volatile的写 和 读的时候,加入屏障,防止出现指令重排的屏障类型 | 指令示例 | 说明 |
LoadLoad | Load1;LoadLoad;Load2 | 保证Load1的读取操作在Load2及后续读取操作之前执行 |
StoreStore | Store1;StoreStore;Store2 | 在Store2及其后的写操作执行前,保证Store1的写操作已刷新到主内存 |
LoadStore | Load1;LoadStore;Store2 | 在Store2及其后的写操作执行前,保证Load1的读操作已读取结束 |
StoreLoad | Store1;StoreLoad;Load2 | 保证load1的写操作已刷新到主内存之后,load2及其后的读操作才能执行 |
二、锁
2.1 公平锁与非公平锁
Synchronized锁均为非公平锁,Lock锁的实现ReentrantLock默认实现通过构造方法中可以传入参数true->公平锁,false(默认值)非公平锁 公平锁:是指多个线程按照申请锁的顺序来获取锁,类似于排队买饭,先来后到,先来先服务,就是公平的,也就是队列 非公平锁:是指多个线程获取锁的顺序,并不是按照申请锁的顺序,有可能申请的线程比先申请的线程优先获取锁,在高并发环境下,有可能造成优先级翻转,或者饥饿的线程(也就是某个线程一直得不到锁)2.2 Synchronized锁升级 无锁、偏向锁、轻量级锁、重量级锁
a>为什么要进行锁升级优化
JVM中synchronized重量级锁的底层原理monitorenter和monitorexit字节码依赖于底层的操作系统的Mutex Lock来实现的,但是由于使用Mutex Lock需要将当前线程挂起并从用户态切换到内核态来执行,这种切换的代价是非常昂贵的。 1.6以后优化,因为重量级锁获取锁和释放锁需要经过操作系统,是一个重量级的操作。对于重量锁来说,一旦线程获取失败,就要陷入阻塞状态,并且是操作系统层面的阻塞,这个过程涉及用户态到核心态的切换,是一个开销非常大的操作。而研究表明,线程持有锁的时间是比较短暂的,也就是说,当前线程即使现在获取锁失败,但可能很快地将来就能够获取到锁,这种情况下将线程挂起是很不划算的行为。所以要对"synchronized总是启用重量级锁"这个机制进行优化。b>Java对象的内存布局
在Java虚拟机中,普通对象在内存中分为三块区域:对象头、实例数据、对齐填充数据,而对象头包括markword(8字节)和类型指针(开启压缩指针4字节,不开启8字节,如果是32g以上内存,都是8字节),实例数据就是对象的成员变量,padding就是为了保证对象的大小为8字节的倍数,将对象所占字节数补到能被8整除。数组对象比普通对象在对象头位置多一个数组长度。c>锁升级过程
无锁:jvm会有4秒的偏向锁开启的延迟时间,在这个偏向延迟内对象处于为无锁态。如果关闭偏向锁启动延迟、或是经过4秒且没有线程竞争对象的锁,那么对象会进入无锁可偏向状态。 偏向锁:偏向锁是偏向某一个线程,把这个锁加到这个线程上,在加锁的时候如果发现当前锁的竞争线程只有一个线程的话,那么这个锁直接偏向这个线程。一旦有了竞争就升级为轻量级锁,升级为轻量级锁的时候需要撤销偏向锁,偏向锁撤销导致的stw 轻量级锁:也叫自旋锁,当有另外一个线程竞争获取这个锁时,由于该锁已经是偏向锁,当发现对象头 Mark Word 中的线程 ID 不是自己的线程 ID,销偏向锁状态,将锁对象markWord中62位修改成指向自己线程栈中Lock Record的指针(CAS抢)执行在用户态,消耗CPU的资源(自旋锁不适合锁定时间长的场景、等待线程特别多的场景),此时锁标志位为:00。在jdk1.6以前,默认轻量级锁自旋次数是10次,如果超过这个次数或自旋线程数超过CPU核数的一半,就会升级为重量级锁。jdk1.6以后加入了自适应自旋锁 (Adapative Self Spinning),自旋的次数不再固定,由jvm自己控制,由前一次在同一个锁上的自旋时间及锁的拥有者的状态来决定:- 对于某个锁对象,如果自旋等待刚刚成功获得过锁,并且持有锁的线程正在运行中,那么虚拟机就会认为这次自旋也是很有可能再次成功,进而允许自旋等待持续相对更长时间
- 对于某个锁对象,如果自旋很少成功获得过锁,那在以后尝试获取这个锁时将可能省略掉自旋过程,直接阻塞线程,避免浪费处理器资源。
2.3 读写锁的锁降级和邮戳锁
a>ReentrantReadWriteLock
读写锁ReentrantReadWriteLock:并不是真正意义上的读写分离,它只允许读读共存,而读写和写写依然是互斥的。 锁降级:遵循获取写锁→再获取读锁→再释放写锁的次序,写锁能够降级成为读锁。 如果一个线程占有了写锁,在不释放写锁的情况下,它还能占有读锁,即写锁降级为读锁。b>StampedLock
写锁饥饿问题:读读共享是优点,但是与此同时也造成了写操作的饥饿问题。读锁没有完成之前,写锁无法获得。使用公平锁能一定程度上缓解锁饥饿问题,但是实在牺牲系统吞吐量的为代价的。 邮戳锁StampedLock:StampedLock采取乐观获取锁后,其他线程尝试获取写锁时不会被阻塞,这其实是对读锁的优化,所以,在获取乐观读锁后,还需要对结果进行校验。StampedLock有三种访问模式
- Reading(读模式):功能和ReentrantReadWriteLock的读锁类似
- Writing(写模式):功能和ReentrantReadWriteLock的写锁类似
- Optimistic reading(乐观读模式):无锁机制,类似于数据库中的乐观锁,支持读写并发,很乐观认为读取时没人修改,假如被修改再实现升级为悲观读模式
2.4 Synchronized与Lock的区别
- 首先synchronized是java内置关键字,在jvm层面,Lock是个java类;
- synchronized无法判断是否获取锁的状态,Lock可以判断是否获取到锁;
- synchronized会自动释放锁(a 线程执行完同步代码会释放锁 ;b 线程执行过程中发生异常会释放锁),Lock需在finally中手工释放锁(unlock()方法释放锁),否则容易造成线程死锁;
- 用synchronized关键字的两个线程1和线程2,如果当前线程1获得锁,线程2线程等待。如果线程1阻塞,线程2则会一直等待下去,而Lock锁就不一定会等待下去,如果尝试获取不到锁,线程可以不用一直等待就结束了;
- synchronized的锁可重入、不可中断、非公平,而Lock锁可重入、可判断、可公平(两者皆可)
- Lock锁适合大量同步的代码的同步问题,synchronized锁适合代码少量的同步问题
2.5 Synchronized的重入的实现机理
每个锁对象拥有一个锁计数器和一个指向持有该锁的线程的指针。 当执行monitorenter时,如果目标锁对象的计数器为零,那么说明它没有被其他线程所持有,Java虚拟机会将该锁对象的持有线程设置为当前线程,并且将其计数器加1。 在目标锁对象的计数器不为零的情况下,如果锁对象的持有线程是当前线程,那么 Java 虚拟机可以将其计数器加1,否则需要等待,直至持有线程释放该锁。 当执行monitorexit时,Java虚拟机则需将锁对象的计数器减1。计数器为零代表锁已被释放2.6 死锁
死锁是指两个或两个以上的线程在执行过程中,因争夺资源而造成的一种互相等待的现象,若无外力干涉那它们都将无法推进下去,如果系统资源充足,进程的资源请求都能够得到满足,死锁出现的可能性就很低,否则就会因争夺有限的资源而陷入死锁 产生死锁主要原因- 系统资源不足
- 进程运行推进的顺序不合适
- 资源分配不当
2.7 CAS
CAS原理解析LongAdder为什么这么快?
LongAdder的基本思路就是分散热点,将value值分散到一个Cell数组中,不同线程会命中到数组的不同槽中,各个线程只对自己槽中的那个值进行CAS操作,这样热点就被分散了,冲突的概率就小很多。如果要获取真正的long值,只要将各个槽中的变量值累加返回。
sum()会将所有Cell数组中的value和base累加作为返回值,核心的思想就是将之前AtomicLong一个value的更新压力分散到多个value中去, 从而降级更新热点。- 内部有一个base变量,一个Cell[]数组。
- base变量:非竞态条件下,直接累加到该变量上
- Cell[]数组:竞态条件下,累加个各个线程自己的槽Cell[i]中
三、AQS
3.1 CountDownLatch、CyclicBarrier、Semaphore使用场景与区别
3.1.1 CountDownLatch
CountDownLatch门闩基于AQS实现,volatile变量state维持倒数状态,多线程共享变量可见。计数器值递减到0的时候,不能再复原的。
- CountDownLatch通过构造函数初始化传入参数实际为AQS的state变量赋值,维持计数器倒数状态
- 当主线程调用await()方法时,当前线程会被阻塞,当state不为0时进入AQS阻塞队列等待。
- 其他线程调用countDown()时,state值原子性递减,当state值为0的时候,唤醒所有调用await()方法阻塞的线程
3.1.2 CyclicBarrier
CyclicBarrier叫做回环屏障,它的作用是让一组线程全部达到一个状态之后再全部同时执行,而且他有一个特点就是所有线程执行完毕之后是可以重用的。
- 当子线程调用await()方法时,获取独占锁,同时对count递减,进入阻塞队列,然后释放锁
- 当第一个线程被阻塞同时释放锁之后,其他子线程竞争获取锁,操作同1
- 直到最后count为0,执行CyclicBarrier构造函数中的任务,执行完毕之后子线程继续向下执行
3.1.3 Semaphore
Semaphore 通常我们叫它信号量, 可以用来控制同时访问特定资源的线程数量,通过协调各个线程,以保证合理的使用资源。
3.2 AQS原理
AQS是用来构建锁或者其它同步器组件的重量级基础框架及整个JUC体系的基石,通过内置的FIFO队列来完成资源获取线程的排队工作,并通过一个int类变量表示持有锁的状态 如果共享资源被占用,就需要一定的阻塞等待唤醒机制来保证锁分配。这个机制主要用的是CLH队列的变体实现的,将暂时获取不到锁的线程加入到队列中,这个队列就是AQS的抽象表现。它将请求共享资源的线程封装成队列的结点(Node),通过CAS自旋以及LockSupport.park()的方式,维护state变量的状态(0表示没有,1表示阻塞次数用于记录可重入),使并发达到同步的效果。详见AbstractQueuedSynchronizer之AQS四、线程
4.1 线程与进程
- 定义:进程是系统进行资源分配和调度的独立单位,实现了操作系统的并发;线程是进程的子任务,是CPU调度和分派的基本单位,用于保证程序的实时性,实现进程内部的并发
- 开销方面:进程有自己的独立数据空间,程序之间的切换开销大;线程也有自己的运行栈和程序计数器,线程之间的切换开销较小
- 共享空间:进程拥有各自独立的地址空间、资源,所以共享复杂;线程共享所属进程的资源,所以共享简单
- 进程编程调试简单可靠性高,但是创建销毁开销大;线程正相反,开销小,切换速度快,但是编程调试相对复杂。
4.2 线程的状态
线程的六种状态:
-
初始(NEW):新创建了一个线程对象,但还没有调用start()方法。
-
运行(RUNNABLE):Java线程中将就绪(ready)和运行中(running)两种状态笼统的称为“运行”。线程对象创建后,其他线程(比如main线程)调用了该对象的start()方法。该状态的线程位于可运行线程池中,等待被线程调度选中,获取CPU的使用权,此时处于就绪状态(ready)。就绪状态的线程在获得CPU时间片后变为运行中状态(running)。
-
阻塞(BLOCKED):表示线程阻塞于锁。
-
等待(WAITING):进入该状态的线程需要等待其他线程做出一些特定动作(通知或中断)。
-
超时等待(TIMED_WAITING):该状态不同于WAITING,它可以在指定的时间后自行返回。
-
终止(TERMINATED):表示该线程已经执行完毕。
4.3 线程中断
中断只是一种协作机制,Java没有给中断增加任何语法,中断的过程完全需要程序员自己实现。若要中断一个线程,你需要手动调用该线程的interrupt方法,该方法也仅仅是将线程对象的中断标识设成true;接着你需要自己写代码不断地检测当前线程的标识位,如果为true,表示别的线程要求这条线程中断, 此时究竟该做什么需要你自己写代码实现。方法 | 说明 |
public void interrupt() | 实例方法interrupt()仅仅是设置线程的中断状态为true,不会停止线程 |
public static boolean interrupted() | 静态方法,Thread.interrupted(); 判断线程是否被中断,并清除当前中断状态 这个方法做了两件事: 1 返回当前线程的中断状态 2 将当前线程的中断状态设为false |
public boolean isInterrupted() | 实例方法,判断当前线程是否被中断(通过检查中断标志位) |
4.4 线程池
4.4.1 线程池定义
线程是稀缺资源,它的创建与销毁是比较重且耗资源的。而Java线程依赖于内核线程,创建线程需要进行操作系统状态切换,为避免资源过度消耗需要设法重用线程执行多个任务,线程池就是一个线程缓存,负责对线程进行统一分配、调优与监控4.4.2 线程池的优势
- 重用存在的线程,减少创建、消亡的开销,提高性能
- 提高响应速度。当任务到达时,任务可以不需要等到线程创建就能立即执行
- 提高线程的可管理性。线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池也可以进行统一的分配、调优与监控
4.4.3 线程池的状态
- Running:能接收新任务以及处理已经添加的任务
- Shutdown:不接受新任务,可以处理已经添加的任务
- Stop:不接受新任务,不处理已经添加的任务,并且中断正在处理的任务
- Tidying:所有的任务已经终止,ctl记录的任务数量为“0”(ctl负责记录线程池的运行状态与活动线程数)
- Terminated:线程池彻底终止,则线程池转化为terminated状态
源码解读详情参考
public class ThreadPoolExecutor extends AbstractExecutorService { // ctl初始化了线程的状态和线程数量,初始状态为RUNNING并且线程数量为0 // 这里一个Integer既包含了状态也包含了数量,其中int类型一共32位,高3位标识状态,低29位标识数量 private final AtomicInteger ctl = new AtomicInteger(ctlOf(RUNNING, 0)); // 这里指定了Integer.SIZE - 3,也就是32 - 3 = 29,表示线程数量最大取值长度 private static final int COUNT_BITS = Integer.SIZE - 3; // 这里标识线程池容量,也就是将1向左位移上面的29长度,并且-1代表最大取值,二进制就是 000111..111 private static final int CAPACITY = (1 << COUNT_BITS) - 1; // 这里是高三位的状态表示 private static final int RUNNING = -1 << COUNT_BITS; // 111 private static final int SHUTDOWN = 0 << COUNT_BITS; // 000 private static final int STOP = 1 << COUNT_BITS; // 001 private static final int TIDYING = 2 << COUNT_BITS; // 010 private static final int TERMINATED = 3 << COUNT_BITS; // 011 // 获取当前线程池状态:通过传入的c,获取最高三位的值,拿到线程状态吗,最终就是拿 1110 000......和c做&运算得到高3位结果 private static int runStateOf(int c) { return c & ~CAPACITY; } // 获取当前线程数量,最终得到现在线程数量,就是拿c 和 0001 111......做&运算,得到低29位结果 private static int workerCountOf(int c) { return c & CAPACITY; } private static int ctlOf(int rs, int wc) { return rs | wc; } }
标签:状态,获取,对象,并发,线程,中断,多线程,内存 From: https://www.cnblogs.com/bbgs-xc/p/16813162.html