diff --git a/week_03/28/AbstractQueuedSynchronizer.md b/week_03/28/AbstractQueuedSynchronizer.md new file mode 100644 index 0000000000000000000000000000000000000000..b245b6d71d7da21b10fe004b884d29ae74f0d243 --- /dev/null +++ b/week_03/28/AbstractQueuedSynchronizer.md @@ -0,0 +1,194 @@ +# AbstractQueuedSynchronizer 源码分析 + +在同步组件的实现中,AQS是核心部分,同步组件的实现者通过使用AQS提供的模板方法实现同步组件语义,AQS则实现了对同步状态的管理,以及对阻塞线程进行排队,等待通知等等一些底层的实现处理。AQS的核心也包括了这些方面:同步队列,独占式锁的获取和释放,共享锁的获取和释放以及可中断锁,超时等待锁获取这些特性的实现,而这些实际上则是AQS提供出来的模板方法 + +## 独占式锁 +```java +void acquire(int arg):独占式获取同步状态,如果获取失败则插入同步队列进行等待; +void acquireInterruptibly(int arg):与acquire方法相同,但在同步队列中进行等待的时候可以检测中断; +boolean tryAcquireNanos(int arg, long nanosTimeout):在acquireInterruptibly基础上增加了超时等待功能,在超时时间内没有获得同步状态返回false; +boolean release(int arg):释放同步状态,该方法会唤醒在同步队列中的下一个节点 + + +``` + +## 共享式锁 +```java +void acquireShared(int arg):共享式获取同步状态,与独占式的区别在于同一时刻有多个线程获取同步状态; +void acquireSharedInterruptibly(int arg):在acquireShared方法基础上增加了能响应中断的功能; +boolean tryAcquireSharedNanos(int arg, long nanosTimeout):在acquireSharedInterruptibly基础上增加了超时等待的功能; +boolean releaseShared(int arg):共享式释放同步状态 +``` + +## 同步队列 +当共享资源被某个线程占有,其他请求该资源的线程将会阻塞,从而进入同步队列。就数据结构而言,队列的实现方式无外乎两者一是通过数组的形式,另外一种则是链表的形式。AQS中的同步队列则是通过链式方式进行实现。 + +### NODE +```java +volatile int waitStatus //节点状态 +volatile Node prev //当前节点/线程的前驱节点 +volatile Node next; //当前节点/线程的后继节点 +volatile Thread thread;//加入同步队列的线程引用 +Node nextWaiter;//等待队列中的下一个节点 +``` + +### NODE 状态 +```java +int CANCELLED = 1//节点从同步队列中取消 +int SIGNAL = -1//后继节点的线程处于等待状态,如果当前节点释放同步状态会通知后继节点,使得后继节点的线程能够运行; +int CONDITION = -2//当前节点进入等待队列中 +int PROPAGATE = -3//表示下一次共享式同步状态获取将会无条件传播下去 +int INITIAL = 0;//初始状态 +``` + +### 独占锁代码分析 + +```java +public final void acquire(int arg) { + //先看同步状态是否获取成功,如果成功则方法结束返回 + //若失败则先调用addWaiter()方法再调用acquireQueued()方法 + if (!tryAcquire(arg) && + acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) + selfInterrupt(); +} + +private Node addWaiter(Node mode) { + // 1. 将当前线程构建成Node类型 + Node node = new Node(Thread.currentThread(), mode); + // Try the fast path of enq; backup to full enq on failure + // 2. 当前尾节点是否为null? + Node pred = tail; + if (pred != null) { + // 2.2 将当前节点尾插入的方式插入同步队列中 + node.prev = pred; + if (compareAndSetTail(pred, node)) { + pred.next = node; + return node; + } + } + // 2.1. 当前同步队列尾节点为null,说明当前线程是第一个加入同步队列进行等待的线程 + enq(node); + return node; +} + +final boolean acquireQueued(final Node node, int arg) { + boolean failed = true; + try { + boolean interrupted = false; + for (;;) { + // 1. 获得当前节点的先驱节点 + final Node p = node.predecessor(); + // 2. 当前节点能否获取独占式锁 + // 2.1 如果当前节点的先驱节点是头结点并且成功获取同步状态,即可以获得独占式锁 + if (p == head && tryAcquire(arg)) { + //队列头指针用指向当前节点 + setHead(node); + //释放前驱节点 + p.next = null; // help GC + failed = false; + return interrupted; + } + // 2.2 获取锁失败,线程进入等待状态等待获取独占式锁 + if (shouldParkAfterFailedAcquire(p, node) && + parkAndCheckInterrupt()) + interrupted = true; + } + } finally { + if (failed) + cancelAcquire(node); + } +} + +private Node enq(final Node node) { + for (;;) { + Node t = tail; + if (t == null) { // Must initialize + //1. 构造头结点 + if (compareAndSetHead(new Node())) + tail = head; + } else { + // 2. 尾插入,CAS操作失败自旋尝试 + node.prev = t; + if (compareAndSetTail(t, node)) { + t.next = node; + return t; + } + } + } +} + +// shouldParkAfterFailedAcquire()方法主要逻辑是使用compareAndSetWaitStatus(pred, ws, Node.SIGNAL)使用CAS将节点状态由INITIAL设置成SIGNAL,表示当前线程阻塞。当compareAndSetWaitStatus设置失败则说明shouldParkAfterFailedAcquire方法返回false,然后会在acquireQueued()方法中for (;;)死循环中会继续重试,直至compareAndSetWaitStatus设置节点状态位为SIGNAL时shouldParkAfterFailedAcquire返回true时才会执行方法parkAndCheckInterrupt()方法 +private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) { + int ws = pred.waitStatus; + if (ws == Node.SIGNAL) + /* + * This node has already set status asking a release + * to signal it, so it can safely park. + */ + return true; + if (ws > 0) { + /* + * Predecessor was cancelled. Skip over predecessors and + * indicate retry. + */ + do { + node.prev = pred = pred.prev; + } while (pred.waitStatus > 0); + pred.next = node; + } else { + /* + * waitStatus must be 0 or PROPAGATE. Indicate that we + * need a signal, but don't park yet. Caller will need to + * retry to make sure it cannot acquire before parking. + */ + compareAndSetWaitStatus(pred, ws, Node.SIGNAL); + } + return false; +} + +private final boolean parkAndCheckInterrupt() { + //使得该线程阻塞 + LockSupport.park(this); + return Thread.interrupted(); +} +//独占锁释放 +public final boolean release(int arg) { + if (tryRelease(arg)) { + Node h = head; + if (h != null && h.waitStatus != 0) + unparkSuccessor(h); + return true; + } + return false; +} + +private void unparkSuccessor(Node node) { + /* + * If status is negative (i.e., possibly needing signal) try + * to clear in anticipation of signalling. It is OK if this + * fails or if status is changed by waiting thread. + */ + int ws = node.waitStatus; + if (ws < 0) + compareAndSetWaitStatus(node, ws, 0); + + /* + * Thread to unpark is held in successor, which is normally + * just the next node. But if cancelled or apparently null, + * traverse backwards from tail to find the actual + * non-cancelled successor. + */ + + //头节点的后继节点 + Node s = node.next; + if (s == null || s.waitStatus > 0) { + s = null; + for (Node t = tail; t != null && t != node; t = t.prev) + if (t.waitStatus <= 0) + s = t; + } + if (s != null) + //后继节点不为null时唤醒该线程 + LockSupport.unpark(s.thread); +} +``` \ No newline at end of file diff --git a/week_03/28/ReentrantLock.md b/week_03/28/ReentrantLock.md new file mode 100644 index 0000000000000000000000000000000000000000..416e451273574b581f08b166ca28e6b0cded4293 --- /dev/null +++ b/week_03/28/ReentrantLock.md @@ -0,0 +1,172 @@ +# ReentrantLock 源码分析 + +Reentrantlock,可重入锁,顾名思义,的大概功能是,同一个线程,可以多次调用lock方法,多次逻辑意义获得锁。 + +## 公平锁(FairSync) +```java + +public void lock() { + //调用 Sync 实现 + sync.lock(); +} +final void lock() { + acquire(1); +} + + +public final void acquire(int arg) { +// 尝试获取锁,如果获取锁失败,那么将当前线程执行信息,放入到AQS的内部队列中 +if (!tryAcquire(arg) && + acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) + // 中断当前线程 + selfInterrupt(); +} + + +protected final boolean tryAcquire(int acquires) { + // 获取当前线程 + final Thread current = Thread.currentThread(); + // 获取变量state, 这个变量非常重要,全程主要是围绕这个变量来做事 + int c = getState(); + // 当变量state==0 的时候,表示当前锁是空闲的,可以获取 + if (c == 0) { + // 判断当前线程是不是等待最久的线程,就是说判断当前线程是不是在等待队列的第二个元素 + // 因为队列head是当前(之前)拥有锁的线程 + // 如果是,则表示等待时间最久 , 返回false , 表示没有线程比它等的更久 + if (!hasQueuedPredecessors() && + compareAndSetState(0, acquires)) { + // 设置当前执行线程(一个内部变量,为了后面的可重入功能)为 “当前线程” + setExclusiveOwnerThread(current); + return true; + } + } + // state !=0 , 并且当前线程==当前执行器线程(一个内部变量) + else if (current == getExclusiveOwnerThread()) { + // 对state赋值, 由此可以看出,ReentrantLock是一个可重入锁, + // 但是有一点,就是重入多少次,就必须要unlock多少次,以保证最终state==0 + int nextc = c + acquires; + if (nextc < 0) + throw new Error("Maximum lock count exceeded"); + setState(nextc); + return true; + } + return false; +} + +//等待最久的线程被优先获取锁 +public final boolean hasQueuedPredecessors() { + Node t = tail; // 尾部元素 + Node h = head; // 头部元素 + // 队列为空的情况下也是返回false + Node s; + return h != t && ((s = h.next) == null || s.thread != Thread.currentThread()); +} + +//释放锁 +public void unlock() { + sync.release(1); +} + + +public final boolean release(int arg) { + // 修改state值,减一 + if (tryRelease(arg)) { + Node h = head; + // 获取他头结点信息 (头部结点,就是当前拥有锁的线程所拥有) + if (h != null && h.waitStatus != 0) + // 主要作用是唤醒头结点的下一个结点 + unparkSuccessor(h); + return true; + } + return false; +} +private void unparkSuccessor(Node node) { + /* + * If status is negative (i.e., possibly needing signal) try + * to clear in anticipation of signalling. It is OK if this + * fails or if status is changed by waiting thread. + */ + int ws = node.waitStatus; + if (ws < 0) + compareAndSetWaitStatus(node, ws, 0); + /* + * Thread to unpark is held in successor, which is normally + * just the next node. But if cancelled or apparently null, + * traverse backwards from tail to find the actual + * non-cancelled successor. + */ + // 获取它的next结点 + Node s = node.next; + if (s == null || s.waitStatus > 0) { + s = null; + for (Node t = tail; t != null && t != node; t = t.prev) + if (t.waitStatus <= 0) + s = t; + } + if (s != null) + // 唤醒 + LockSupport.unpark(s.thread); +} + +protected final boolean tryRelease(int releases) { + // 减法 + int c = getState() - releases; + // 判断是否是执行线程去调用的unLock() + if (Thread.currentThread() != getExclusiveOwnerThread()) + throw new IllegalMonitorStateException(); + boolean free = false; + if (c == 0) { + // 当state=0,释放成功(锁已经空闲) + free = true; + setExclusiveOwnerThread(null); + } + setState(c); + return free; +} +``` + +## 非公平锁(NonfairSync) + +### Lock +```java +final void lock() { + // 当线程进来之后,会直接判断当前state的值。如果是0 ,他能够直接通过 + // cas操作,设置state的值为1 的话,那么竞争锁成功 + if (compareAndSetState(0, 1)) + setExclusiveOwnerThread(Thread.currentThread()); + else + // 上面设置失败的话,那么直接调用acquire方法 + acquire(1); +} + +protected final boolean tryAcquire(int acquires) { + return nonfairTryAcquire(acquires); +} + +final boolean nonfairTryAcquire(int acquires) { + final Thread current = Thread.currentThread(); + int c = getState(); + if (c == 0) { + // 大致逻辑和公平锁一致,唯一不同的是,这里并不会去判断当前线程是否是等待最久的线程。 + if (compareAndSetState(0, acquires)) { + setExclusiveOwnerThread(current); + return true; + } + } else if (current == getExclusiveOwnerThread()) { + int nextc = c + acquires; + if (nextc < 0) // overflow + throw new Error("Maximum lock count exceeded"); + setState(nextc); + return true; + } + return false; +} +``` + +## 总结:  + +通过对公平锁的分析,在unlock的时候,会对head节点的next节点做唤醒操作, 也就是唤醒下一个节点来竞争锁, +那么体现非公平锁的特性来了, 当唤醒下一个节点来竞争锁的时候, 又有几个其他线程调用了lock方法,这个时候 +另外几个线程和next节点所代表的线程都会去竞争锁,并不保证next节点能够一定获取到锁。 + + diff --git a/week_03/28/Semaphore.md b/week_03/28/Semaphore.md new file mode 100644 index 0000000000000000000000000000000000000000..3c501f18a318179c657ef258cfa2a6942dc1b72d --- /dev/null +++ b/week_03/28/Semaphore.md @@ -0,0 +1,416 @@ +# Semaphore 源码分析 +Semaphore是借助AQS实现的的共享锁,通过构造参数可以给状态变量赋值,用来控制对资源访问的并发度。Semaphore代码很简洁,主要方法就两个,一个是获取资源许可方法acquire,一个是释放资源方法release,这两个方法都是利用内部的同步器对状态变量的控制来实现的 + +```java + +public class Semaphore { + private static final long serialVersionUID = -3222578661600680210L; + /** + * 所有机制都通过AbstractQueuedSynchronizer子类实现 + */ + private final Sync sync; + /** + * 信号量的同步实现。使用AQS状态表示许可证。子类化为公平和非公平版本。 + * + */ + abstract static class Sync extends AbstractQueuedSynchronizer { + private static final long serialVersionUID = 1192457210091910933L; + + Sync(int permits) { + setState(permits); // 设置同步状态的值。 + } + /** + * 返回此信号量中当前可用的许可数。 + * @return 此信号量中的可用许可数 + */ + final int getPermits() { + return getState(); + } + /** + * 非公平锁 + * 仅在调用时此信号量中有给定数目的许可时,才从此信号量中获取这些许可。 + * @param acquires 要获取的许可数 + * @return 获取给定数目的许可(如果提供了)并立即返回 + */ + final int nonfairTryAcquireShared(int acquires) { + for (;;) { + int available = getState(); // 返回同步状态的当前值。 + int remaining = available - acquires; // 正在同步-要获取的许可数 + // 有许可的数量 或者 当前状态值等于期望值(CAS) + if (remaining < 0 || + compareAndSetState(available, remaining)) + return remaining; + } + } + /** + * 如果允许释放许可,则返回 true;否则返回 false。 + * @param releases 要释放的许可数 + * @return 如果允许释放许可,则返回 true;否则返回 false。 + + */ + protected final boolean tryReleaseShared(int releases) { + for (;;) { + int current = getState();// 返回同步状态的当前值。 + int next = current + releases; // 同步数+释放数 + if (next < current) // 溢出 + throw new Error("Maximum permit count exceeded"); + if (compareAndSetState(current, next)) // 当前状态值等于期望值(CAS) + return true; + } + } + /** + * 根据指定的缩减量减小可用许可的数目。 + * 此方法在使用信号量来跟踪那些变为不可用资源的子类中很有用。 + * @param reduction 要移除的许可数 + */ + final void reducePermits(int reductions) { + for (;;) { + int current = getState(); // 返回同步状态的当前值。 + int next = current - reductions; // 同步数-释放数 + if (next > current) // 下溢 + throw new Error("Permit count underflow"); + if (compareAndSetState(current, next))// 当前状态值等于期望值(CAS) + return; + } + } + /** + * 获取并返回立即可用的所有许可。 + * @return 获取的许可数 + */ + final int drainPermits() { + for (;;) { + int current = getState(); // 返回同步状态的当前值。 + // 同步数为0或者当前状态值等于期望值(CAS) + if (current == 0 || compareAndSetState(current, 0)) + return current; + } + } + } + + /** + * 非公平的版本 + * + */ + static final class NonfairSync extends Sync { + private static final long serialVersionUID = -2694183684443567898L; + /** + * 创建具有给定的许可数和非公平设置的 Semaphore。 + * @param permits 初始的可用许可数目。此值可能为负数,在这种情况下,必须在授予任何获取前进行释放。 + */ + NonfairSync(int permits) { + super(permits); + } + /** + * 非公平 + * 仅在调用时此信号量中有给定数目的许可时,才从此信号量中获取这些许可。 + * @param acquires 要获取的许可数 + * @return 获取给定数目的许可(如果提供了)并立即返回 + */ + protected int tryAcquireShared(int acquires) { + return nonfairTryAcquireShared(acquires); + } + } + + /** + * 公平的版本 + * + */ + static final class FairSync extends Sync { + private static final long serialVersionUID = 2014338818796000944L; + /** + * 建具有给定的许可数和公平设置的 Semaphore。 + * @param permits 初始的可用许可数目。此值可能为负数,在这种情况下,必须在授予任何获取前进行释放。 + */ + FairSync(int permits) { + super(permits); + } + /** + * 公平 + * 仅在调用时此信号量中有给定数目的许可时,才从此信号量中获取这些许可。 + * @param acquires 要获取的许可数 + * @return 获取给定数目的许可(如果提供了)并立即返回 + */ + protected int tryAcquireShared(int acquires) { + for (;;) { + if (hasQueuedPredecessors()) // 如果在当前线程之前有一个排队的线程 + return -1; + int available = getState(); // 返回同步状态的当前值。 + int remaining = available - acquires; // 同步数-许可数 + if (remaining < 0 || // 同步数多于许可数或者当前状态值等于期望值(CAS) + compareAndSetState(available, remaining)) + return remaining; + } + } + } + /** + * 创建具有给定的许可数和非公平的公平设置的 Semaphore。 + * @param permits 初始的可用许可数目。此值可能为负数,在这种情况下,必须在授予任何获取前进行释放。 + */ + public Semaphore(int permits) { + sync = new NonfairSync(permits); + } + /** + * 创建具有给定的许可数和给定的公平设置的 Semaphore。 + * @param permits 初始的可用许可数目。此值可能为负数,在这种情况下,必须在授予任何获取前进行释放。 + * @param fair 如果此信号量保证在争用时按先进先出的顺序授予许可,则为 true;否则为 false。 + */ + public Semaphore(int permits, boolean fair) { + sync = fair ? new FairSync(permits) : new NonfairSync(permits); + } + /** + * 从此信号量获取一个许可,在提供一个许可前一直将线程阻塞,否则线程被中断。 + * 获取一个许可(如果提供了一个)并立即返回,将可用的许可数减 1。 + * + * 如果没有可用的许可,则在发生以下两种情况之一前,禁止将当前线程用于线程安排目的并使其处于休眠状态: + * 某些其他线程调用此信号量的 release() 方法,并且当前线程是下一个要被分配许可的线程; + * 或者其他某些线程中断当前线程。 + * + * 如果当前线程: + * 被此方法将其已中断状态设置为 on ; + * 或者 在等待许可时被中断。 + * 则抛出 InterruptedException,并且清除当前线程的已中断状态。 + * @throws InterruptedException 如果当前线程被中断 + */ + public void acquire() throws InterruptedException { + sync.acquireSharedInterruptibly(1); + } + /** + * 从此信号量中获取许可,在有可用的许可前将其阻塞。 + * 获取一个许可(如果提供了一个)并立即返回,将可用的允许数减 1。 + * + * 如果没有可用的许可,则在其他某些线程调用此信号量的 release() 方法, + * 并且当前线程是下一个要被分配许可的线程前,禁止当前线程用于线程安排目的并使其处于休眠状态。 + * + * 如果当前线程在等待许可时被中断,那么它将继续等待,但是与没有发生中断, + * 其将接收允许的时间相比,为该线程分配许可的时间可能改变。 + * 当线程确实从此方法返回后,将设置其中断状态。 + */ + public void acquireUninterruptibly() { + sync.acquireShared(1); + } + + /** + * 仅在调用时此信号量存在一个可用许可,才从信号量获取许可。 + * 获取一个许可(如果提供了一个)并立即返回,其值为 true,将可用的许可数减 1。 + * + * 如果没有可用的许可,则此方法立即返回并且值为 false。 + * 即使已将此信号量设置为使用公平排序策略,但是调用 tryAcquire() 也将 立即获取许可(如果有一个可用), + * 而不管当前是否有正在等待的线程。在某些情况下,此“闯入”行为可能很有用,即使它会打破公平性也如此。 + * + * 如果希望遵守公平设置,则使用 tryAcquire(0, TimeUnit.SECONDS) ,它几乎是等效的(它也检测中断)。 + * @return 如果获取了许可,则返回 true;否则返回 false。 + */ + public boolean tryAcquire() { + return sync.nonfairTryAcquireShared(1) >= 0; + } + /** + * 如果在给定的等待时间内,此信号量有可用的许可并且当前线程未被中断,则从此信号量获取一个许可。 + * 获取一个许可(如果提供了一个)并立即返回,其值为 true,将可用的许可数减 1。 + * + * 如果没有可用的允许,则在发生以下三种情况之一前,禁止将当前线程用于线程安排目的并使其处于休眠状态: + * 其他某些线程调用此信号量的 release() 方法并且当前线程是下一个被分配许可的线程; + * 或者其他某些线程中断当前线程; + * 或者已超出指定的等待时间。 + * + * 如果获取了许可,则返回值为 true。 + * + * 如果当前线程: + * 被此方法将其已中断状态设置为 on ; + * 或者在等待获取许可的同时被中断。 + * 则抛出 InterruptedException,并且清除当前线程的已中断状态。 + * + * 如果超出了指定的等待时间,则返回值为 false。如果该时间小于等于 0,则方法根本不等待。 + * @param timeout 等待许可的最多时间 + * @param unit timeout 参数的时间单位 + * @return 如果获取了许可,则返回 true;如果获取许可前超出了等待时间,则返回 false + * @throws InterruptedException 如果当前线程是已中断的 + */ + public boolean tryAcquire(long timeout, TimeUnit unit) + throws InterruptedException { + return sync.tryAcquireSharedNanos(1, unit.toNanos(timeout)); + } + /** + * 释放一个许可,将其返回给信号量。 + * 释放一个许可,将可用的许可数增加 1。如果任意线程试图获取许可,则选中一个线程并将刚刚释放的许可给予它。 + * 然后针对线程安排目的启用(或再启用)该线程。 + * 不要求释放许可的线程必须通过调用 acquire() 来获取许可。通过应用程序中的编程约定来建立信号量的正确用法。 + */ + public void release() { + sync.releaseShared(1); + } + /** + * 从此信号量获取给定数目的许可,在提供这些许可前一直将线程阻塞,或者线程已被中断。 + * 获取给定数目的许可(如果提供了)并立即返回,将可用的许可数减去给定的量。 + * + * 如果没有足够的可用许可,则在发生以下两种情况之一前,禁止将当前线程用于线程安排目的并使其处于休眠状态: + * 其他某些线程调用此信号量的某个释放方法,当前线程是下一个被分配允许的线程并且可用许可的数目满足此请求; + * 或者其他某些线程中断当前线程。 + * + * 如果当前线程: + * 被此方法将其已中断状态设置为 on ; + * 或者在等待许可时被中断。 + * 则抛出 InterruptedException,并且清除当前线程的已中断状态。 + * + * 任何原本应该分配给此线程的许可将被分配给其他试图获取许可的线程, + * 就好像已通过调用 release() 而使许可可用一样。 + * @param permits 要获取的许可数 + * @throws InterruptedException 如果当前线程已被中断 + */ + public void acquire(int permits) throws InterruptedException { + if (permits < 0) throw new IllegalArgumentException(); // 如果 permits 为负 + sync.acquireSharedInterruptibly(permits); + } + /** + * 从此信号量获取给定数目的许可,在提供这些许可前一直将线程阻塞。 + * 获取给定数目的许可(如果提供了)并立即返回,将可用的许可数减去给定的量。 + * + * 如果没有足够的可用许可,则在其他某些线程调用此信号量的某个释放方法, + * 当前线程是下一个要被分配许可的线程,并且可用的许可数目满足此请求前, + * 禁止当前线程用于线程安排目的并使其处于休眠状态。 + * + * 如果当前的线程在等待许可时被中断,则它会继续等待并且它在队列中的位置不受影响。 + * 当线程确实从此方法返回后,将其设置为中断状态。 + * @param permits 要获取的许可数 + */ + public void acquireUninterruptibly(int permits) { + if (permits < 0) throw new IllegalArgumentException(); // 如果 permits 为负 + sync.acquireShared(permits); + } + /** + * 仅在调用时此信号量中有给定数目的许可时,才从此信号量中获取这些许可。 + * 获取给定数目的许可(如果提供了)并立即返回,其值为 true,将可用的许可数减去给定的量。 + * + * 如果没有足够的可用许可,则此方法立即返回,其值为 false,并且不改变可用的许可数。 + * 即使已将此信号量设置为使用公平排序策略,但是调用 tryAcquire 也将 立即获取许可(如果有一个可用), + * 而不管当前是否有正在等待的线程。在某些情况下,此“闯入”行为可能很有用,即使它会打破公平性也如此。 + * + * 如果希望遵守公平设置,则使用 tryAcquire(permits, 0, TimeUnit.SECONDS) ,它几乎是等效的(它也检测中断)。 + * @param permits 要获取的许可数 + * @return 如果获取了许可,则返回 true;否则返回 false + */ + public boolean tryAcquire(int permits) { + if (permits < 0) throw new IllegalArgumentException(); // 如果 permits 为负 + return sync.nonfairTryAcquireShared(permits) >= 0; + } + /** + * 如果在给定的等待时间内此信号量有可用的所有许可,并且当前线程未被中断,则从此信号量获取给定数目的许可。 + * 获取给定数目的许可(如果提供了)并立即返回,其值为 true,将可用的许可数减去给定的量。 + * + * 如果没有足够的可用许可,则在发生以下三种情况之一前,禁止将当前线程用于线程安排目的并使其处于休眠状态: + * 其他某些线程调用此信号量的某个释放方法,当前线程是下一个被分配许可的线程,并且可用许可的数目满足此请求; + * 或者其他某些线程中断当前线程; + * 或者已超出指定的等待时间。 + * + * 如果获取了许可,则返回值为 true。 + * + * 如果当前线程: + * 被此方法将其已中断状态设置为 on ; + * 或者在等待获取允许的同时被中断。 + * 则抛出 InterruptedException,并且清除当前线程的已中断状态。 + * 任何原本应该分配给此线程的许可将被分配给其他试图获取许可的线程, + * 就好像已通过调用 release() 而使许可可用一样。 + * + * 如果超出了指定的等待时间,则返回值为 false。如果该时间小于等于 0,则方法根本不等待。 + * 任何原本应该分配给此线程的许可将被分配给其他试图获取许可的线程, + * 就好像已通过调用 release() 而使许可可用一样。 + * @param permits 要获取的许可数 + * @param timeout 等待许可的最多时间 + * @param unit timeout 参数的时间单位 + * @return 如果获取了许可,则返回 true;如果获取所有许可前超出了等待时间,则返回 false + * @throws InterruptedException 如果当前线程是已中断的 + */ + public boolean tryAcquire(int permits, long timeout, TimeUnit unit) + throws InterruptedException { + if (permits < 0) throw new IllegalArgumentException(); // 如果 permits 为负 + return sync.tryAcquireSharedNanos(permits, unit.toNanos(timeout)); + } + /** + * 释放给定数目的许可,将其返回到信号量。 + * 释放给定数目的许可,将可用的许可数增加该量。 + * + * 如果任意线程试图获取许可,则选中某个线程并将刚刚释放的许可给予该线程。 + * + * 如果可用许可的数目满足该线程的请求,则针对线程安排目的启用(或再启用)该线程; + * 否则在有足够的可用许可前线程将一直等待。如果满足此线程的请求后仍有可用的许可, + * 则依次将这些许可分配给试图获取许可的其他线程。 + * 不要求释放许可的线程必须通过调用获取来获取该许可。通过应用程序中的编程约定来建立信号量的正确用法。 + * @param permits 要释放的许可数 + */ + public void release(int permits) { + if (permits < 0) throw new IllegalArgumentException(); // 如果 permits 为负 + sync.releaseShared(permits); + } + /** + * 返回此信号量中当前可用的许可数。 + * 此方法通常用于调试和测试目的。 + * @return 此信号量中的可用许可数 + */ + public int availablePermits() { + return sync.getPermits(); + } + /** + * 获取并返回立即可用的所有许可。 + * @return 获取的许可数 + */ + public int drainPermits() { + return sync.drainPermits(); + } + /** + * 根据指定的缩减量减小可用许可的数目。此方法在使用信号量来跟踪那些变为不可用资源的子类中很有用。 + * 此方法不同于 acquire,在许可变为可用的过程中,它不会阻塞等待。 + * @param reduction 要移除的许可数 + */ + protected void reducePermits(int reduction) { + if (reduction < 0) throw new IllegalArgumentException(); // 如果 reduction 是负数 + sync.reducePermits(reduction); + } + /** + * 如果此信号量的公平设置为 true,则返回 true。 + * @return 如果此信号量的公平设置为 true,则返回 true + */ + public boolean isFair() { + return sync instanceof FairSync; + } + /** + * 查询是否有线程正在等待获取。注意,因为同时可能发生取消, + * 所以返回 true 并不保证有其他线程等待获取许可。此方法主要用于监视系统状态。 + * @return 如果可能有其他线程正在等待获取锁,则返回 true + */ + public final boolean hasQueuedThreads() { + return sync.hasQueuedThreads(); + } + /** + * 返回正在等待获取的线程的估计数目。该值仅是估计的数字, + * 因为在此方法遍历内部数据结构的同时,线程的数目可能动态地变化。 + * 此方法用于监视系统状态,不用于同步控制。 + * @return 正在等待此锁的线程的估计数目 + */ + public final int getQueueLength() { + return sync.getQueueLength(); + } + /** + * 返回一个 collection,包含可能等待获取的线程。因为在构造此结果的同时实际的线程 set 可能动态地变化, + * 所以返回的 collection 仅是尽力的估计值。所返回 collection 中的元素没有特定的顺序。 + * 此方法用于加快子类的构造速度,提供更多的监视设施。 + * @return 线程 collection + */ + protected Collection getQueuedThreads() { + return sync.getQueuedThreads(); + } + /** + * 返回标识此信号量的字符串,以及信号量的状态。括号中的状态包括 String 类型的 "Permits =",后跟许可数。 + * 覆盖:类 Object 中的 toString + * @return 标识此信号量的字符串,以及信号量的状态 + */ + public String toString() { + return super.toString() + "[Permits = " + sync.getPermits() + "]"; + } +} +``` + +## 总结 +一个计数信号量。从概念上讲,信号量维护了一个许可集。 +如有必要,在许可可用前会阻塞每一个 acquire(),然后再获取该许可。 +每个 release() 添加一个许可,从而可能释放一个正在阻塞的获取者。 +但是,不使用实际的许可对象,Semaphore 只对可用许可的号码进行计数,并采取相应的行动。 +Semaphore 通常用于限制可以访问某些资源(物理或逻辑的)的线程数目。 \ No newline at end of file diff --git "a/week_03/28/java\345\206\205\345\255\230\346\250\241\345\236\213.md" "b/week_03/28/java\345\206\205\345\255\230\346\250\241\345\236\213.md" new file mode 100644 index 0000000000000000000000000000000000000000..99d00d74a613466c6966cbc3226d5ec19458b247 --- /dev/null +++ "b/week_03/28/java\345\206\205\345\255\230\346\250\241\345\236\213.md" @@ -0,0 +1,137 @@ +# java 内存模型 + +## CPU 内存 交互 + +cpu上加入了高速缓存这样做解决了处理器和内存的矛盾(一快一慢),但是引来的新的问题 - 缓存一致性 +在多核cpu中,每个处理器都有各自的高速缓存(L1,L2,L3),而主内存确只有一个。 + +```java +CPU要读取一个数据时,首先从一级缓存中查找,如果没有找到再从二级缓存中查找,如果还是没有就从三级缓存或内存中查找,每个cpu有且只有一套自己的缓存。 +各个处理器需遵循一些协议保证一致性。【如MSI,MESI等】 +``` + +## 内存屏障(Memory Barrier) +每个CPU又有多级缓存【L1,L2,L3 等】。 因为缓存,提高了数据访问性能,避免每次都读取内存; +带来的问题是,不能实时的和内存发生信息交换,在不同CPU执行的不同线程对同一个变量的缓存值可能不同。 + +硬件层的[内存屏障]分为两种:Load Barrier 和 Store Barrier即读屏障和写屏障。 + + +### 产生内存屏障原因 + +多处理器操作系统,每个处理器都有自己的缓存,存再不同处理器缓存不一致的问题, +由于操作系统存在重排序,导致读取到错误的数据,因此操作系统提供了一些内存屏障以解决这种数据不一致问题。 + +1. 不同CPU执行的不同线程对同一个变量的缓存值可能不同,可以使用volatile解决缓存一致性问题。 +2. 不同硬件对内存屏障的实现方式不一样,java屏蔽掉这些差异,通过jvm生成内存屏障的指令。 +对于读屏障:在指令前插入读屏障,可以让高速缓存中的数据失效,强制从主内存取。 + + +### 内存屏障作用 +cpu执行指令可能是无序的,它有两个比较重要的作用 + +1.阻止屏障两侧指令重排序 +2.强制把写缓冲区/高速缓存中的脏数据等写回主内存,让缓存中相应的数据失效。 + + +### volatile 型变量 + +当我们声明某个变量为volatile修饰时,这个变量就有了线程可见性,volatile通过在读写操作前后添加内存屏障。 + +volatile 的作用 +1.可见性,对于一个该变量的读,一定能看到读之前最后的写入。 +2.原子性,对volatile变量的读写具有原子性,即单纯读和写的操作,都不会受到干扰。 + +## Java内存模型(Java Memory Model ,JMM) + +是一种符合内存模型规范的,屏蔽了各种硬件和操作系统的访问差异的, +保证了Java程序在各种平台下对内存的访问都能保证效果一致的机制及规范。 + +```java +java 内存区域分5部分: + +1. 程序计数器 + +程序计数器是一块很小的内存空间,它是线程私有的,可以认为当前线程的行号指示器。 +一条线程中有多个指令,为了线程切换可以恢复到正确执行位置,每个线程都需有独立的一个程序计数器,不同线程之间的程序计数器互不影响,独立存储。 + +2. Java栈(虚拟机栈) + +方法执行时创建的栈帧,它也是线程私有的,用于存储如下信息 +1)局部变量表 +2)操作栈 +3)动态链接 +4)方法出口 + +方法被调用的过程就对应一个栈帧在虚拟机栈中从入栈到出栈的过程。【栈先进后出,下图栈1先进最后出来】 + +> 局部变量表所需要的内存空间在编译期完成分配,当进入一个方法时,这个方法在栈中需要分配多大的局部变量空间是完全确定的,在方法运行期间不会改变局部变量表大小。 + +> Java虚拟机栈可能出现两种类型的异常: +1)线程请求的栈深度大于虚拟机允许的栈深度,将抛出StackOverflowError。 +2)虚拟机栈空间可以动态扩展,当动态扩展是无法申请到足够的空间时,抛出OutOfMemory异常。 + + + +### 平时说的栈一般指局部变量表部分 + +#### 局部变量表 +一片连续的内存空间,用来存放方法参数,以及方法内定义的局部变量,存放着编译期间已知的数据类型(八大基本类型和对象引用(reference类型) + +#### reference +与基本类型不同的是它不等同本身,即使是String,内部也是char数组组成,它可能是指向一个对象起始位置指针, +也可能指向一个代表对象的句柄或其他与该对象有关的位置。 + +#### returnAddress +它的最小的局部变量表空间单位为Slot,虚拟机没有指明Slot的大小,但在jvm中,long和double类型数据明确规定为64位,这两个类型占2个Slot,其它基本类型固定占用1个Slot。 +指向一条字节码指令的地址【深入理解Java虚拟机】 + + +3. 本地方法栈 + +与虚拟机栈发挥的作用十分相似, 区别是虚拟机栈执行的是Java方法(也就是字节码)服务, +而本地方法栈则为虚拟机使用到的native方法服务,可能底层调用的c或者c++, +我们打开jdk安装目录可以看到也有很多用c编写的文件,可能就是native方法所调用的c代码。 + +4. 堆 + +对于大多数应用来说,堆是java虚拟机管理内存最大的一块内存区域,堆存放的对象是线程共享的,所以多线程的时候也需要同步机制。 + +#### java虚拟机规范中堆 +所有对象实例及数组都要在堆上分配内存,但随着JIT编译器的发展和逃逸分析技术的成熟,这个说法也不是那么绝对,大多数情况是这样的。 + +#### 即时编译器 +可以把Java的字节码,包括需要被解释的指令的程序转换成可以直接发送给处理器的指令的程序 + +#### 逃逸分析 +通过逃逸分析来决定某些实例或者变量是否要在堆中进行分配,如果开启了逃逸分析,即可将这些变量直接在栈上进行分配,而非堆上进行分配。这些变量的指针可以被全局所引用,或者其其它线程所引用。 + +> 堆是所有线程共享的,存放对象实例。堆也是GC所管理的主要区域,因此常被称为GC堆,收集器常使用分代算法,Java堆中还可以细分为新生代和老年代,细分还有Eden(伊甸园)空间等。 +根据虚拟机规范,Java堆可以存在物理上不连续的内存空间,就像磁盘空间只要逻辑是连续的即可。它的内存大小可以设为固定大小,也可以扩展。 +当前主流的虚拟机如HotPot都能按扩展实现(通过设置 -Xmx和-Xms),如果堆中没有内存内存完成实例分配,而且堆无法扩展将报OOM错误(OutOfMemoryError) + + +5. 方法区 + +方法区同堆一样,是所有线程共享的内存区域,又被称为非堆。 + +存储虚拟机加载的类信息、常量、静态变量,如static修饰的变量加载类的时候就被加载到方法区中。 + +#### 运行时常量池 +是方法区的一部分,class文件除了有类的字段、接口、方法等描述信息之外,还有常量池用于存放编译期间生成的各种字面量和符号引用。 +在老版jdk,方法区也被称为永久代【因为没有强制要求方法区必须实现垃圾回收,HotSpot虚拟机以永久代来实现方法区,从而JVM的垃圾收集器可以像管理堆区一样管理这部分区域,从而不需要专门为这部分设计垃圾回收机制。不过自从JDK7之后,Hotspot虚拟机便将运行时常量池从永久代移除了。】 + +> 在jdk1.6及以前常量池分配在永久代中。可通过 -XX:PermSize和-XX:MaxPermSize设置方法区大小。 + +> jdk1.6 方法区超限 将报OOM后面跟着PermGen space说明方法区OOM,即常量池在永久代 +> jdk1.7 或1.8 方法区超限 将报heap space 即常量池在堆中 +> jdk1.8 开始废弃永久代,而使用元空间(Metaspace) + + + + +其中方法区和堆是所有线程共享的,栈,本地方法栈和程序计数器则为线程私有的。 + +``` + + diff --git "a/week_03/28/synchronized\345\216\237\347\220\206.md" "b/week_03/28/synchronized\345\216\237\347\220\206.md" new file mode 100644 index 0000000000000000000000000000000000000000..92f859fbd680cf327cf2acc14bcc62b1dd847454 --- /dev/null +++ "b/week_03/28/synchronized\345\216\237\347\220\206.md" @@ -0,0 +1,58 @@ +# synchronized 原理 + +如果某一个资源被多个线程共享,为了避免因为资源抢占导致资源数据错乱,我们需要对线程进行同步,在开发过程中可以使用它来解决线程安全问题中提到的原子性,可见性,以及顺序性。很多人都会称呼它为重量级锁。但是,随着Java SE 1.6对synchronized进行了各种优化之后,有些情况下它就并不那么重了,Java SE 1.6中为了减少获得锁和释放锁带来的性能消耗而引入的偏向锁和轻量级锁,以及锁的存储结构和升级过程。 + +### Java对象头: +在Hotspot虚拟机中,对象在内存中的布局分为三块区域:对象头、实例数据和对齐填充;Java对象头是实现synchronized的锁对象的基础,一般而言,synchronized使用的锁对象是存储在Java对象头里。它是轻量级锁和偏向锁的关键 +### Mawrk Word: +Mark Word用于存储对象自身的运行时数据,如哈希码(HashCode)、GC分代年龄、锁状态标志、线程持有的锁、偏向线程 ID、偏向时间戳等等。Java对象头一般占有两个机器码(在32位虚拟机中,1个机器码等于4字节,也就是32bit) +### Monitor: +什么是Monitor?我们可以把它理解为一个同步工具,也可以描述为一种同步机制。所有的Java对象是天生的Monitor,每个object的对象里 markOop->monitor() 里可以保存ObjectMonitor的对象。从源码层面看一下monitor对象 +oop.hpp下的oopDesc类是JVM对象的顶级基类,所以每个object对象都包含markOop +到目前位置,对于锁存在哪个位置,我们已经清楚了,锁存在于每个对象的 markOop 对象头中.对于为什么每个对象都可以成为锁呢? 因为每个 Java Object 在 JVM 内部都有一个 native 的 C++ 对象 oop/oopDesc 与之对应,而对应的 oop/oopDesc 都会存在一个markOop 对象头,而这个对象头是存储锁的位置,里面还有对象监视器,即ObjectMonitor,所以这也是为什么每个对象都能成为锁的原因之一。 + +### 乐观锁: +乐观锁是一种乐观思想,即认为读多写少,遇到并发写的可能性低,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,采取在写时先读出当前版本号,然后加锁操作(比较跟上一次的版本号,如果一样则更新),如果失败则要重复读-比较-写的操作。java中的乐观锁基本都是通过CAS操作实现的,CAS是一种更新的原子操作,比较当前值跟传入值是否一样,一样则更新,否则失败。 + +### 悲观锁: +悲观锁是就是悲观思想,即认为写多,遇到并发写的可能性高,每次去拿数据的时候都认为别人会修改,所以每次在读写数据的时候都会上锁,这样别人想读写这个数据就会block直到拿到锁。java中的悲观锁就是Synchronized,AQS框架下的锁则是先尝试cas乐观锁去获取锁,获取不到,才会转换为悲观锁,如RetreenLock。 + +### 自旋锁(CAS): +自旋锁就是让不满足条件的线程等待一段时间,而不是立即挂起。看持有锁的线程是否能够很快释放锁。怎么自旋呢?其实就是一段没有任何意义的循环。虽然它通过占用处理器的时间来避免线程切换带来的开销,但是如果持有锁的线程不能在很快释放锁,那么自旋的线程就会浪费处理器的资源,因为它不会做任何有意义的工作。所以,自旋等待的时间或者次数是有一个限度的,如果自旋超过了定义的时间仍然没有获取到锁,则该线程应该被挂起。JDK1.6中-XX:+UseSpinning开启; -XX:PreBlockSpin=10 为自旋次数; JDK1.7后,去掉此参数,由jvm控制; + +### 偏向锁: +大多数情况下,锁不仅不存在多线程竞争,而且总是由同一线程多次获得,为了让线程获得锁的代价更低而引入了偏向锁。 + +### 轻量级锁: +引入轻量级锁的主要目的是在多没有多线程竞争的前提下,减少传统的重量级锁使用操作系统互斥量产生的性能消耗。当关闭偏向锁功能或者多个线程竞争偏向锁导致偏向锁升级为轻量级锁,则会尝试获取轻量级锁 + +### 重量级锁: +重量级锁通过对象内部的监视器(monitor)实现,其中monitor的本质是依赖于底层操作系统的Mutex Lock实现,操作系统实现线程之间的切换需要从用户态到内核态的切换,切换成本非常高。主要是,当系统检查到锁是重量级锁之后,会把等待想要获得锁的线程进行阻塞,被阻塞的线程不会消耗cup。但是阻塞或者唤醒一个线程时,都需要操作系统来帮忙,这就需要从用户态转换到内核态,而转换状态是需要消耗很多时间的,有可能比用户执行代码的时间还要长。这就是说为什么重量级线程开销很大的。 + + +### wait和notify在synchronized里的应用: +1.wait方法的语义有两个,一个是释放当前的对象锁、另一个是使得当前线程进入阻塞队列, 而这些操作都和监视器是相关的,所以wait必须要获得一个监视器锁。 +2.而对于notify来说也是一样,它是唤醒一个线程,既然要去唤醒,首先得知道它在哪里?所以就必须要找到这个对象获取到这个对象的锁,然后到这个对象的等待队列中去唤醒一个线程。 + +## JDK6对Synchronized的优化 +在JDK6以前synchronized的性能并不高,但在之后进行了优化,锁的状态总共有四种,无锁状态、偏向锁、轻量级锁和重量级锁。随着锁的竞争,锁可以从偏向锁升级到轻量级锁,再升级的重量级锁,但是锁的升级是单向的,也就是说只能从低到高升级,不会出现锁的降级。 +### 偏向锁 +偏向锁是Java 6之后加入的新锁,它是一种针对加锁操作的优化手段。经过研究发现,在大多数情况下,锁不仅不存在多线程竞争,而且总是由同一线程多次获得,因此为了减少同一线程获取锁(会涉及到一些CAS操作,耗时)的代价而引入偏向锁。 +偏向锁的核心思想是,如果一个线程获得了锁,那么锁就进入偏向模式,此时Mark Word 的结构也变为偏向锁结构,当这个线程再次请求锁时,无需再做任何同步操作,即获取锁的过程,这样就省去了大量有关锁申请的操作,从而也就提供程序的性能。 +所以,对于没有锁竞争的场合,偏向锁有很好的优化效果,毕竟极有可能连续多次是同一个线程申请相同的锁。但是对于锁竞争比较激烈的场合,偏向锁就失效了,因为这样场合极有可能每次申请锁的线程都是不相同的,因此这种场合下不应该使用偏向锁,否则会得不偿失。但偏向锁失败后,并不会立即膨胀为重量级锁,而是先升级为轻量级锁。 + +### 轻量级锁 +若偏向锁失败,虚拟机并不会立即升级为重量级锁,它还会尝试使用一种称为轻量级锁的优化手段,此时Mark Word 的结构也变为轻量级锁的结构。轻量级锁能够提升程序性能的依据是“对绝大部分的锁,在整个同步周期内都不存在竞争”,注意这是经验数据。需要了解的是,轻量级锁所适应的场景是线程交替执行同步块的场合,如果存在同一时间访问同一锁的场合,就会导致轻量级锁膨胀为重量级锁。 + +### 自旋锁 +轻量级锁失败后,虚拟机为了避免线程真实地在操作系统层面挂起,还会进行一项称为自旋锁的优化手段。这是基于在大多数情况下,线程持有锁的时间都不会太长,如果直接挂起操作系统层面的线程可能会得不偿失,毕竟操作系统实现线程之间的切换时需要从用户态转换到核心态,这个状态之间的转换需要相对比较长的时间,时间成本相对较高,因此自旋锁会假设在不久将来,当前的线程可以获得锁,因此虚拟机会让当前想要获取锁的线程做几个空循环(这也是称为自旋的原因),一般不会太久,可能是50个循环或100循环,在经过若干次循环后,如果得到锁,就顺利进入临界区。如果还不能获得锁,那就会将线程在操作系统层面挂起,这就是自旋锁的优化方式,这种方式确实也是可以提升效率的。最后没办法也就只能升级为重量级锁了。 + +### 锁消除 +消除锁是虚拟机另外一种锁的优化,这种优化更彻底,Java虚拟机在JIT编译时(可以简单理解为当某段代码即将第一次被执行时进行编译,又称即时编译),通过对运行上下文的扫描,去除不可能存在共享资源竞争的锁,通过这种方式消除没有必要的锁,可以节省毫无意义的请求锁时间。 + +### 锁粗化 +如果虚拟机探测到有这样一串零碎的操作都对同一个对象加锁,将会把加锁同步的范围扩展到整个操作序列的外部,这样就只需要加锁一次就够了。 + + +```java +``` \ No newline at end of file diff --git "a/week_03/28/volatile\345\216\237\347\220\206.md" "b/week_03/28/volatile\345\216\237\347\220\206.md" new file mode 100644 index 0000000000000000000000000000000000000000..710d7ddef7952ae2c75ed9a96f8d9a1914a70697 --- /dev/null +++ "b/week_03/28/volatile\345\216\237\347\220\206.md" @@ -0,0 +1,24 @@ +# volatile 原理 + +## volatile 官方定义 +Java语言规范第三版中对volatile的定义如下: +java编程语言允许线程访问共享变量,为了确保共享变量能被准确和一致的更新,线程应该确保通过排他锁单独获得这个变量。Java语言提供了volatile,在某些情况下比锁更加方便。如果一个字段被声明成volatile,java线程内存模型确保所有线程看到这个变量的值是一致的。 + +## 为什么要使用Volatile +Volatile变量修饰符如果使用 恰当 的话,它比synchronized的 使用和执行成本会更低 ,因为它不会引起线程上下文的切换和调度。 + +## Volatile的实现原理 +Java编程语言允许线程访问共享变量,为了确保共享变量能被准确和一致地更新,线程应该通过排它锁单独获取这个变量 +Java语言提供了Violatile来确保多处理开发中,共享变量的“可见性”,即当另外一个线程修改一个共享变量时,另外一个线程能读到这个修改的值。它是轻量级的synchronized,不会引起线程上下文的切换和调度,执行开销更小。 + +使用Violatile修饰的变量在汇编阶段,会多出一条lock前缀指令,它在多核处理器下回引发两件事情: +1.将当前处理器缓存行的数据写回到系统内存 +2.这个写回内存的操作会使在其他CPU里缓存了该内存地址的数据无效。 + +通常处理器和内存之间都有几级缓存来提高处理速度,处理器先将内存中的数据读取到内部缓存后再进行操作,但是对于缓存写会内存的时机则无法得知,因此在一个处理器里修改的变量值,不一定能及时写会缓存,这种变量修改对其他处理器变得“不可见”了。但是,使用Volatile修饰的变量,在写操作的时候,会强制将这个变量所在缓存行的数据写回到内存中,但即使写回到内存,其他处理器也有可能使用内部的缓存数据,从而导致变量不一致,所以,在多处理器下,为了保证各个处理器的缓存是一致的,就会实现缓存一致性协议,每个处理器通过嗅探在总线上传播的数据来检查自己缓存的值是不是过期,如果过期,就会将该缓存行设置成无效状态,下次要使用就会重新从内存中读取。 + + +```java +Volatile无法保证原子性 +volatile是一种“轻量级的锁”,它能保证锁的可见性,但不能保证锁的原子性。 +``` \ No newline at end of file diff --git "a/week_03/28/\345\210\206\345\270\203\345\274\217\351\224\201\345\216\237\347\220\206.md" "b/week_03/28/\345\210\206\345\270\203\345\274\217\351\224\201\345\216\237\347\220\206.md" new file mode 100644 index 0000000000000000000000000000000000000000..246e098d7b9355297f2b798e660bbfbfd201c858 --- /dev/null +++ "b/week_03/28/\345\210\206\345\270\203\345\274\217\351\224\201\345\216\237\347\220\206.md" @@ -0,0 +1,23 @@ +# 分布式锁原理 + +## 分布式锁,是控制分布式系统之间同步访问共享资源的一种方式 +## 在分布式系统中,常常需要协调他们的动作。如果不同的系统或是同一个系统的不同主机之间共享了一个或一组资源,那么访问这些资源的时候,往往需要互斥来防止彼此干扰来保证一致性,在这种情况下,便需要使用到分布式锁。 + +## 分布式锁需要有如下特性: +1.在分布式系统环境下,同一时间只能被一个机器的的一个线程获取到锁。 +2.高可用的获取锁与释放锁; +3.高性能的获取锁与释放锁; +4.具备可重入特性; +5.具备锁失效机制,防止死锁; +6.具备非阻塞锁特性,即没有获取到锁将直接返回获取锁失败。 + +## 为什么使用分布式锁 +1.目前几乎很多大型网站及应用都是分布式部署的,分布式场景中的数据一致性问题一直是一个比较重要的话题。 +2.分布式的CAP理论告诉我们,任何一个分布式系统都无法同时满足一致性、可用性、和分区容错性,最多只能同时满足两项。 +3.所以,很多系统在设计之初就对这三项做了取舍。在互联网领域的绝大多数的场景中,都需要牺牲强一致性来换取系统的高可用性,系统往往只需要保证最终一致性,只要这个最终时间实在用户可以接受的范围内即可。 +4.在很多场景中,我们为例保证数据的最终一致性,需要很多的技术方案来支持,比如分布式事务、分布式锁等,有时候我们需要保证一个方法在同一个线程执行。 +5.基于数据库实现分布式锁;基于缓存redis等实现分布式锁;基于Zookeeper实现分布式锁等。 + +## 总结: +上面的三种方式,没有在所有场合都是完美的,所以,应根据不同的应用场景选择最适合的实现方式。 +分布式环境中,对资源进行上锁有时候是很重要的,比如抢购某一资源,这时候使用分布式锁就可以很好的控制资源。 \ No newline at end of file