多线程与高并发

一、Volatile

1.1 可见性

0
  • read(读取):从主内存读取数据
  • load(载入):将主内存读取到的数据写入工作内存
  • use(使用) :从工作内存读取数据来计算
  • assign(赋值):将计算好的值重新赋值到工作内存中
  • store(存储):将工作内存数据写入主内存
  • write(写入):将store过去的变量值赋值给主内存中的变量
  • lock(锁定) :将主内存变量加锁,标识为线程独占状态
  • unlock(解锁):将主内存变量解锁,解锁后其他线程可以锁定该变量

1.2 禁止指令重排

0
也就是过在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整除。数组对象比普通对象在对象头位置多一个数组长度。
  0
  0

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自己控制,由前一次在同一个锁上的自旋时间及锁的拥有者的状态来决定:
  • 对于某个锁对象,如果自旋等待刚刚成功获得过锁,并且持有锁的线程正在运行中,那么虚拟机就会认为这次自旋也是很有可能再次成功,进而允许自旋等待持续相对更长时间
  • 对于某个锁对象,如果自旋很少成功获得过锁,那在以后尝试获取这个锁时将可能省略掉自旋过程,直接阻塞线程,避免浪费处理器资源。
  重量级锁:重量级锁是依赖对象内部的monitor(监视器/管程)来实现的 ,而monitor 又依赖于操作系统底层的Mutex Lock(互斥锁)实现
  0

2.3 读写锁的锁降级和邮戳锁

a>ReentrantReadWriteLock

  读写锁ReentrantReadWriteLock:并不是真正意义上的读写分离,它只允许读读共存,而读写和写写依然是互斥的。
  锁降级:遵循获取写锁→再获取读锁→再释放写锁的次序,写锁能够降级成为读锁。 如果一个线程占有了写锁,在不释放写锁的情况下,它还能占有读锁,即写锁降级为读锁

b>StampedLock

  写锁饥饿问题:读读共享是优点,但是与此同时也造成了写操作的饥饿问题。读锁没有完成之前,写锁无法获得。使用公平锁能一定程度上缓解锁饥饿问题,但是实在牺牲系统吞吐量的为代价的。
  邮戳锁StampedLock:StampedLock采取乐观获取锁后,其他线程尝试获取写锁时不会被阻塞,这其实是对读锁的优化,所以,在获取乐观读锁后,还需要对结果进行校验。

  StampedLock有三种访问模式

  • Reading(读模式):功能和ReentrantReadWriteLock的读锁类似
  • Writing(写模式):功能和ReentrantReadWriteLock的写锁类似
  • Optimistic reading(乐观读模式):无锁机制,类似于数据库中的乐观锁,支持读写并发,很乐观认为读取时没人修改,假如被修改再实现升级为悲观读模式
StampedLock的缺点
  StampedLock 不支持重入,没有Re开头 StampedLock 的悲观读锁和写锁都不支持条件变量(Condition),这个也需要注意。 使用 StampedLock一定不要调用中断操作,即不要调用interrupt() 方法 如果需要支持中断功能,一定使用可中断的悲观读锁 readLockInterruptibly()和写锁writeLockInterruptibly()

2.4 Synchronized与Lock的区别

  1. 首先synchronized是java内置关键字,在jvm层面,Lock是个java类;
  2. synchronized无法判断是否获取锁的状态,Lock可以判断是否获取到锁;
  3. synchronized会自动释放锁(a 线程执行完同步代码会释放锁 ;b 线程执行过程中发生异常会释放锁),Lock需在finally中手工释放锁(unlock()方法释放锁),否则容易造成线程死锁;
  4. 用synchronized关键字的两个线程1和线程2,如果当前线程1获得锁,线程2线程等待。如果线程1阻塞,线程2则会一直等待下去,而Lock锁就不一定会等待下去,如果尝试获取不到锁,线程可以不用一直等待就结束了;
  5. synchronized的锁可重入、不可中断、非公平,而Lock锁可重入、可判断、可公平(两者皆可)
  6. Lock锁适合大量同步的代码的同步问题,synchronized锁适合代码少量的同步问题

2.5 Synchronized的重入的实现机理

  每个锁对象拥有一个锁计数器和一个指向持有该锁的线程的指针
​   当执行monitorenter时,如果目标锁对象的计数器为零,那么说明它没有被其他线程所持有,Java虚拟机会将该锁对象的持有线程设置为当前线程,并且将其计数器加1。
​   在目标锁对象的计数器不为零的情况下,如果锁对象的持有线程是当前线程,那么 Java 虚拟机可以将其计数器加1,否则需要等待,直至持有线程释放该锁。
​   当执行monitorexit时,Java虚拟机则需将锁对象的计数器减1。计数器为零代表锁已被释放

2.6 死锁

  死锁是指两个或两个以上的线程在执行过程中,因争夺资源而造成的一种互相等待的现象,若无外力干涉那它们都将无法推进下去,如果系统资源充足,进程的资源请求都能够得到满足,死锁出现的可能性就很低,否则就会因争夺有限的资源而陷入死锁
产生死锁主要原因
  1. 系统资源不足
  2. 进程运行推进的顺序不合适
  3. 资源分配不当

2.7 CAS

LongAdder为什么这么快?

  LongAdder的基本思路就是分散热点,将value值分散到一个Cell数组中,不同线程会命中到数组的不同槽中,各个线程只对自己槽中的那个值进行CAS操作,这样热点就被分散了,冲突的概率就小很多。如果要获取真正的long值,只要将各个槽中的变量值累加返回。

  sum()会将所有Cell数组中的value和base累加作为返回值,核心的思想就是将之前AtomicLong一个value的更新压力分散到多个value中去, 从而降级更新热点。
  0
  • 内部有一个base变量,一个Cell[]数组。
  • base变量:非竞态条件下,直接累加到该变量上
  • Cell[]数组:竞态条件下,累加个各个线程自己的槽Cell[i]中

三、AQS

3.1 CountDownLatch、CyclicBarrier、Semaphore使用场景与区别 

3.1.1 CountDownLatch

  CountDownLatch门闩基于AQS实现,volatile变量state维持倒数状态,多线程共享变量可见。计数器值递减到0的时候,不能再复原的。

  1. CountDownLatch通过构造函数初始化传入参数实际为AQS的state变量赋值,维持计数器倒数状态
  2. 当主线程调用await()方法时,当前线程会被阻塞,当state不为0时进入AQS阻塞队列等待。
  3. 其他线程调用countDown()时,state值原子性递减,当state值为0的时候,唤醒所有调用await()方法阻塞的线程

3.1.2 CyclicBarrier

  CyclicBarrier叫做回环屏障,它的作用是让一组线程全部达到一个状态之后再全部同时执行,而且他有一个特点就是所有线程执行完毕之后是可以重用的。

  1. 当子线程调用await()方法时,获取独占锁,同时对count递减,进入阻塞队列,然后释放锁
  2. 当第一个线程被阻塞同时释放锁之后,其他子线程竞争获取锁,操作同1
  3. 直到最后count为0,执行CyclicBarrier构造函数中的任务,执行完毕之后子线程继续向下执行

3.1.3 Semaphore

  Semaphore 通常我们叫它信号量, 可以用来控制同时访问特定资源的线程数量,通过协调各个线程,以保证合理的使用资源。

3.2 AQS原理

​   AQS是用来构建锁或者其它同步器组件的重量级基础框架及整个JUC体系的基石,通过内置的FIFO队列来完成资源获取线程的排队工作,并通过一个int类变量表示持有锁的状态
  0
  如果共享资源被占用,就需要一定的阻塞等待唤醒机制来保证锁分配。这个机制主要用的是CLH队列的变体实现的,将暂时获取不到锁的线程加入到队列中,这个队列就是AQS的抽象表现。它将请求共享资源的线程封装成队列的结点(Node),通过CAS自旋以及LockSupport.park()的方式,维护state变量的状态(0表示没有,1表示阻塞次数用于记录可重入),使并发达到同步的效果。详见AbstractQueuedSynchronizer之AQS

四、线程

4.1 线程与进程

  1. 定义:进程是系统进行资源分配和调度的独立单位,实现了操作系统的并发;线程是进程的子任务,是CPU调度和分派的基本单位,用于保证程序的实时性,实现进程内部的并发
  2. 开销方面:进程有自己的独立数据空间,程序之间的切换开销大;线程也有自己的运行栈和程序计数器,线程之间的切换开销较小
  3. 共享空间:进程拥有各自独立的地址空间、资源,所以共享复杂;线程共享所属进程的资源,所以共享简单
  4. 进程编程调试简单可靠性高,但是创建销毁开销大;线程正相反,开销小,切换速度快,但是编程调试相对复杂。

4.2 线程的状态

   

 

 

   线程的六种状态:

  1. 初始(NEW):新创建了一个线程对象,但还没有调用start()方法。

  2. 运行(RUNNABLE):Java线程中将就绪(ready)和运行中(running)两种状态笼统的称为“运行”。线程对象创建后,其他线程(比如main线程)调用了该对象的start()方法。该状态的线程位于可运行线程池中,等待被线程调度选中,获取CPU的使用权,此时处于就绪状态(ready)。就绪状态的线程在获得CPU时间片后变为运行中状态(running)。

  3. 阻塞(BLOCKED):表示线程阻塞于锁。

  4. 等待(WAITING):进入该状态的线程需要等待其他线程做出一些特定动作(通知或中断)。

  5. 超时等待(TIMED_WAITING):该状态不同于WAITING,它可以在指定的时间后自行返回。

  6. 终止(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; }
}

 

 
 
posted @ 2022-11-25 09:05  MXC肖某某  阅读(301)  评论(0编辑  收藏  举报