大纲
之前总结的一版
CPU和内存关系
JVM内存模型
写入时复制
写入时复制(CopyOnWrite,简称COW)思想是计算机程序设计领域中的一种通用优化策略。其核心思想是,如果有多个调用者(Callers)同时访问相同的资源(如内存或者是磁盘上的数据存储),他们会共同获取相同的指针指向相同的资源,直到某个调用者修改资源内容时,系统才会真正复制一份专用副本(private copy)给该调用者,而其他调用者所见到的最初的资源仍然保持不变。这过程对其他的调用者都是透明的(transparently)。此做法主要的优点是如果调用者没有修改资源,就不会有副本(private copy)被创建,因此多个调用者只是读取操作时可以共享同一份资源。
通俗易懂的讲,写入时复制技术就是不同进程在访问同一资源的时候,只有更新操作,才会去复制一份新的数据并更新替换,否则都是访问同一个资源。
JDK 的 CopyOnWriteArrayList/CopyOnWriteArraySet 容器正是采用了 COW 思想,它是如何工作的呢?简单来说,就是平时查询的时候,都不需要加锁,随便访问,只有在更新的时候,才会从原来的数据复制一个副本出来,然后修改这个副本,最后把原数据替换成当前的副本。修改操作的同时,读操作不会被阻塞,而是继续读取旧的数据。这点要跟读写锁区分一下。
JVM的8种原子操作
这个8个原子操作是JVM实现内存屏障的基本
线程的状态示意图
阻塞状态是线程阻塞在进入synchronized关键字修饰的方法或代码块(获取锁)时的状态,但是阻塞在java. concurrent包中Lock接口的线程状态却是等待状态,因为java.concurrent包中Lock接口对于阻塞的实现均使用了LockSupport类中的相关方法。
锁
等待通知
wait()方法会释放锁。
AQS
原理
- Step1:
public final void acquire(int arg) { if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) selfInterrupt(); }
- 首先调用自定义同步器实现的tryAcquire(int arg)方法,该方法保证线程安全的获取同步状态。
- 如果同步状态获取失败,则构造同步节点(独占式Node. EXCLUSIVE,同一时刻只能有一个线程成功获取同步状态)并通过addWaiter(Node node)方法将该节点加入到同步队列的尾部。
- 最后调用acquireQueued(Node node, int arg)方法,使得该节点以“死循环”的方式获取同步状态。
-
Step2:
private Node addWaiter(Node mode) { Node node = new Node(Thread.currentThread(), mode); // Try the fast path of enq; backup to full enq on failure Node pred = tail; if (pred != null) { node.prev = pred; if (compareAndSetTail(pred, node)) { pred.next = node; return node; } } enq(node); return node; }
private Node enq(final Node node) { for (;;) { Node t = tail; if (t == null) { // Must initialize if (compareAndSetHead(new Node())) tail = head; } else { node.prev = t; if (compareAndSetTail(t, node)) { t.next = node; return t; } } } }
- 先使用CAS尝试快速添加尾结点。
- 如果失败说明同步队列没有被初始化或者有其他线程设置了尾结点。如果没有初始化头节点,则创建一个
compareAndSetHead(Node update)
。(这个头节点的Thread为空),然后自旋使用CAS设置尾结点。
-
Step3:
final boolean acquireQueued(final Node node, int arg) { boolean failed = true; try { boolean interrupted = false; for (;;) { final Node p = node.predecessor(); if (p == head && tryAcquire(arg)) { setHead(node); p.next = null; // help GC failed = false; return interrupted; } if (shouldParkAfterFailedAcquire(p, node) && parkAndCheckInterrupt()) interrupted = true; } } finally { if (failed) cancelAcquire(node); } }
只有前驱是头节点的结点才能“死循环”获取同步状态。这样是为了保证同步队列的FIFO原则。最后通过
sendHead(Node node)
设置头结点,应为只有一个结点能获取到同步状态,所以不存在并发安全问题。
-
总结:
- 在获取同步状态时,同步器维护一个同步队列,获取状态失败的线程都会被加入到队列中并在队列中进行自旋;
- 移出队列(或停止自旋)的条件是前驱节点为头节点且成功获取了同步状态。在释放同步状态时,同步器调用tryRelease(int arg)方法释放同步状态,然后唤醒头节点的后继节点。
ReentrantLock
实现原理
ReentrantLock类通过组合一个同步组件Sync来实现锁,这个同步组件继承了AQS,并重写了AQS中的tryAcquire()、tryRelease()等方法,最终实际上还是通过AQS以及重写的tryAcquire()、tryRelease()等方法来实现锁的逻辑。ReentrantLock实现锁的方式,也是JUC包下其他类型的锁的实现方法,通过组合一个自定义的同步组件,这个同步组件需要继承AQS,然后重写AQS中的部分方法即可实现一把自定义的锁,通常这个同步组件被定义成内部类。
公平和非公平锁实现
由FairSync同步组件实现的锁是公平锁,它获取锁的原则是,在同步队列中等待时间最长的线程获取锁,因此称它为公平锁。由NonfairSync同步组件实现的锁是非公平锁,它获取锁的原则是,同步队列外的线程在尝试获取锁时,不会判断队列中有没有线程在排队,而是上来就抢,抢到锁了就走,抢不到了才去排队,因此称它为不公平的。
ReadWrite实现原理
利用AQS的state的同步状态,高16位表示读锁,低16位表示写锁。只能支持锁降级,不能锁升级。
锁升级指的是线程获取到了读锁,在没有释放读锁的前提下,又获取写锁。锁降级指的是线程获取到了写锁,在没有释放写锁的情况下,又获取读锁。
StampedLock
提供乐观读的方式,在读的同时允许写。ReadWriteReentrantLock是悲观锁。 StampedLock支持锁升级。
缺点:
- 不支持重入。
- 不支持条件变量。
- 中断会导致CPU暴涨。
CountDownLatch原理
CountDownLatch利用的是AQS的是共享锁。CountDownLatch在构造方法中指定了计数器的初始值,即AQS中的同步变量state的值。当调用await()方法时,会判断此时state的值是否为0,如果为0,就让当前线程返回,如果不为0,就让当前线程进入同步队列等待。在同一时刻它允许多个线程来访问state,每当调用一次countDown()方法的时候,让state的值减1,当state值为0的时候释放同步队列的结点。
CyclicBarrier原理
CyclicBarrier和CoutDownLatch的底层实现也存在一点区别,CountDownLatch底层是直接通过组合一个继承了AQS的同步组件来实现的,而CyclicBarrier并没有直接借助AQS的同步组件,而是通过组合ReentrantLock这把锁来实现的(ReentrantLock的底层实现依然使用的AQS来实现的,归根结底,CyclicBarrier的底层实现也是AQS)。 由于CyclicBarrier是使用ReentrantLock来实现的,因此它有个属性是lock。在CyclicBarrier中还维护了一个计数器:count。由于CyclicBarrier可以重复使用,即计数器减为0后,将其重置,因此还需要借助另外一个变量来存放count的初始值,这个变量就是parties。CyclicBarrier中有个属性是generation,其类型是一个CyclicBarrier的内部类Generation,它的作用是用来实现await(long timeout,TimeUnit unit)方法的超时等待的功能。
CyclicBarrier与CountDownLacth存在几点区别。
- 首先CyclicBarrier是让所有线程到达屏障后再一起执行后面的逻辑,而CountDownLatch是让一个线程或者一组线程等待其他线程执行完后,自己再接着执行。
- 第二,CyclicBarrier的计数器可以重置,因此可以重复使用,而CountDownLatch的计数器不能重置,不可以重复使用。
- 第三,CyclicBarrier可以在所有线程达到屏障后,先执行一个Runnable任务,然后才打开屏障,这个功能在特殊场景下很有用处。第四,虽然两者最终底层实现都是根据AQS来实现的,但是CyclicBarrier是通过ReentrantLock这个互斥锁来间接使用AQS实现的,而CountDownLatch是直接使用AQS的共享锁来实现的。
作者:天堂同志 链接:juejin.cn/post/684490…
线程池的原理
作者:天堂同志 链接:juejin.cn/post/684490…
简单来说线程池是通过一个int类型来保存线程池的状态和线程的数量的,高3位表示状态、低29位表示线程数量。执行任务的是Worker
,Worker中有一个属性是thread
。创建Worker的时候需要使用全局锁,这也是为什么要先添加队列,后创建线程的原因。
如何关闭线程池
如何处理线程池的异常
如何计算线程数量
-
CPU密集型
对于 CPU 密集型的计算场景,理论上“线程的数量 =CPU 核数”就是最合适的。不过在工程上,线程的数量一般会设置为“CPU 核数 +1”,这样的话,当线程因为偶尔的内存页失效或其他原因导致阻塞时,这个额外的线程可以顶上,从而保证 CPU 的利用率。
-
IO密集型
最佳线程数 =CPU 核数 * [ 1 +(I/O 耗时 / CPU 耗时)]