From 8c0f1992a80073e45c24ccd330a2580b8bdbe486 Mon Sep 17 00:00:00 2001 From: whl <313576743@qq.com> Date: Fri, 27 Dec 2019 11:30:27 +0800 Subject: [PATCH 1/2] commit --- week_03/25/JMM_25.md | 25 +++++ week_03/25/ReenTrantLock_25.md | 163 +++++++++++++++++++++++++++++++++ week_03/25/Semaphore_25.md | 74 +++++++++++++++ week_03/25/synchronized_25.md | 104 +++++++++++++++++++++ week_03/25/volatile_25.md | 94 +++++++++++++++++++ 5 files changed, 460 insertions(+) create mode 100644 week_03/25/JMM_25.md create mode 100644 week_03/25/ReenTrantLock_25.md create mode 100644 week_03/25/Semaphore_25.md create mode 100644 week_03/25/synchronized_25.md create mode 100644 week_03/25/volatile_25.md diff --git a/week_03/25/JMM_25.md b/week_03/25/JMM_25.md new file mode 100644 index 0000000..a3f8d83 --- /dev/null +++ b/week_03/25/JMM_25.md @@ -0,0 +1,25 @@ +Java内存模型描述了一组规范,它定义了程序中变量(实例字段、静态字段、构成数组对象的元素)的访问方式,并围绕原子性、有序性、可见性展开。 + +在Jvm中程序运行的实体是线程,并且每个线程在创建的同时JVM会为其分配一个工作内存用于存储线程私有的数据。而JMM规定了程序中变量必须存储在主内存中,那么当线程需要对程序中的某个变量进行读取、赋值等操作时,必须先从主内存中获取该变量副本的拷贝,然后在工作内存中完成对变量拷贝的操作,之后刷新回主内存。这样的变量操作方式也决定了线程之间的交互方式必须通过主内存完成。 + +方法中的基本数据类型本地变量将直接存储在工作内存的栈帧中;引用数据类型本地变量将引用地址存储在工作内存的栈帧中、引用对象实例存储在主内存中。成员变量、static变量、类信息则存储在主内存中。 +### 一致性问题 +JMM就是为了解决多线程环境下共享变量的一致性问题,而一致性主要包含了下面三个特性 + +#### 1. 原子性 +原子性指一段操作一旦开始就会一直执行直到完成,不会被其他线程打断。一段操作可以是一个操作,也可以是多个操作。基本数据类型的变量读写是具备原子性的;若是引用数据类型的变量读写,我们可以通过JMM提供的lock和unlock来满足原子性需求,尽管不能直接使用这两个操作,但我们可以使用synchronized来实现,synchronized底层就是基于lock、unlock进行实现的。因此我们可以认为synchronized之间的操作是具备原子性的。 + +#### 2. 可见性 +线程对共享变量的修改会先从工作内存通过缓存再回写到主内存,因此共享变量的修改并不能及时地被其他线程看到,也就是可见性问题。如果要满足可见性的需求,可以对变量声明为volatile。volatile使得变量在被线程修改之后能够立即同步到主内存;若读取一个volatile变量时都从主内存中刷新最新的值。可以理解普通变量的操作流程为工作内存->缓存->主内存,而volatile变量的操作流程为工作内存->主内存。 + +除了volatile以外,synchronized、final关键字也都能够保证可见性。synchronized在对一个变量执行unlock操作之前,必须先把此变量同步到主内存;而被final修饰的字段在构造器中一旦被初始化完成,其他线程就能够看到这个final字段了。 + +#### 3. 有序性 +Java程序中天然的有序性可以总结为一句话:如果在本线程中观察,所有的操作都是有序的;如果在另一个线程中观察,所有的操作都是无序的。前半句指线程内部为串行执行,后半句指“指令重排”和“工作内存主内存同步延迟”造成的无序现象。 + +volatile由禁止指令重排保证有序性,而synchronized由“一个变量同一时刻只能被一条线程对其指定lock操作”保证有序性。 + +### 总结 +由此可见synchronized能够解决多线程环境下共享变量的一致性问题,因为它同时满足了原子性、可见性、有序性,而volatile由于不能够保证原子性,因此需要结合Unsafe提供的CAS操作满足原子性,而juc.atomic包下的原子类正是CAS+volatile的实现。例如AtomicInteger,采用了自旋锁的方式解决了一致性问题,相比起synchronized的悲观锁机制,CAS+volatile的乐观锁性能上要更优。 + + diff --git a/week_03/25/ReenTrantLock_25.md b/week_03/25/ReenTrantLock_25.md new file mode 100644 index 0000000..039d760 --- /dev/null +++ b/week_03/25/ReenTrantLock_25.md @@ -0,0 +1,163 @@ +# ReenTrantLock +ReenTrantLock英译为重入锁,而重入锁指的是一个线程获取到锁后再尝试获取锁时会自动获取锁。除了ReenTrantLock以外,synchronized也是重入锁。 + +```java +public class ReentrantLock implements Lock, java.io.Serializable +``` +实现了Lock接口,实现了lock(获取锁)、unlock(释放锁)、newCondition(条件锁)、tryLock(尝试获取锁)、lockInterruptibly(可中断获取锁)方法。 + +### 内部类 +```java +abstract static class Sync extends AbstractQueuedSynchronizer{} + +static final class FairSync extends Sync {} + +static final class NonfairSync extends Sync {} +``` +(1)抽象内部类Sync实现了AQS的部分方法 +(2)内部类NonfairSync实现了Sync,主要用于非公平锁的获取 +(3)内部类FairSync实现了Sync,主要用于公平锁的获取 + +### 主要属性 +```java +private final Sync sync; +``` +Sync在构造方法中被初始化,用于决定是使用公平锁还是非公平锁的方式获取锁。 + +### 构造方法 +```java +/** + * 默认构造创建采用非公平锁 + */ +public ReentrantLock() { + sync = new NonfairSync(); +} + +/** + * 自定义公平/非公平锁 + */ +public ReentrantLock(boolean fair) { + sync = fair ? new FairSync() : new NonfairSync(); +} +``` + +### lock() +假设指定创建公平锁,new ReentrantLock(true) +```java +//ReenTrantLock.lock() +public void lock() { + //调用sync属性的lock方法 + sync.lock(); +} + +//FairSync.lock() +final void lock() { + acquire(1); +} + +//AQS.acquire() +public final void acquire(int arg) { + //尝试获取锁 + //如果失败了就排队 + if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) + selfInterrupt(); +} + +//FairSync.tryAcquire() +protected final boolean tryAcquire(int acquires) { + //获取当前线程 + final Thread current = Thread.currentThread(); + //查看当前状态变量值 + int c = getState(); + //若c == 0, 说明锁还未被占用 + if (c == 0) { + //若没有其他线程在排队等待获取锁, 那么当前线程尝试更新state=1 + //若成功更新state, 说明当前线程获取了锁 + if (!hasQueuedPredecessors() && compareAndSetState(0, acquires)) { + //当前线程获取到了锁, 就将自己设置到exclusiveOwnerThread变量中 + setExclusiveOwnerThread(current); + return true; + } + } + //如果当前线程本身就占有该锁, 尝试获取重入锁 + else if (current == getExclusiveOwnerThread()) { + //将状态变量state + 1 + int nextc = c + acquires; + //若状态变量 + 1后溢出, 则抛出异常 + if (nextc < 0) + throw new Error("Maximum lock count exceeded"); + //否则设置新的state + setState(nextc); + return true; + } + return false; +} +``` + +假设指定创建非公平锁,new ReentrantLock() +```java +//NonfairSync.lock() +final void lock() { + //尝试CAS更新状态变量 + if (compareAndSetState(0, 1)) + //如果更新成功, 把当前线程设置为独占线程 + setExclusiveOwnerThread(Thread.currentThread()); + else + acquire(1); +} + +//NonfairSync.tryAcquire() +protected final boolean tryAcquire(int acquires) { + return nonfairTryAcquire(acquires); +} + +//Sync.nonfairTryAcquire() +final boolean nonfairTryAcquire(int acquires) { + final Thread current = Thread.currentThread(); + int c = getState(); + if (c == 0) { + //如果状态变量的值为0, 尝试CAS更新状态变量的值 + //相比起公平锁, 省略了检查排队线程nonfairTryAcquire()的步骤 + 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 +``` +//ReentrantLock.unlock() +public void unlock() { + sync.release(1); +} + +//Sync.release() +public final boolean release(int arg) { + //调用tryRelease()方法释放锁 + if (tryRelease(arg)) { + Node h = head; + if (h != null && h.waitStatus != 0) + unparkSuccessor(h); + return true; + } + return false; +} +``` + +### ReenTrantLock与synchronized的区别 +1. 两者都是可重入锁 +2. ReenTrantLock支持公平锁而synchronized不支持 +3. ReenTrantLock需要手动加锁、解锁,synchronized只需要手动加锁 +4. ReentrantLock可以通过getHoldCount获取当前线程获取锁的次数 +5. ReenTrantLock可以通过getWaitingThreads()获取处于排队状态的线程 \ No newline at end of file diff --git a/week_03/25/Semaphore_25.md b/week_03/25/Semaphore_25.md new file mode 100644 index 0000000..6e88924 --- /dev/null +++ b/week_03/25/Semaphore_25.md @@ -0,0 +1,74 @@ +### Semaphore +Semaphore信号量,通常用于限制同一时间对共享资源的访问次数,也就是限流。Semaphore中和ReenTrantLock一样包含了实现了AQS的同步器Sync以及Sync的两个子类FairSync、NonFairSync,这说明Semaphore也是区分公平与非公平模式的。 + +### 构造方法 +```java +//默认非公平模式下,传入许可次数 +public Semaphore(int permits) { + sync = new NonfairSync(permits); +} + +//指定模式下,传入许可次数 +public Semaphore(int permits, boolean fair) { + sync = fair ? new FairSync(permits) : new NonfairSync(permits); +} +``` + +### acquire() +```java +public void acquire() throws InterruptedException { + sync.acquireSharedInterruptibly(1); +} +``` +获取一个许可,默认使用可中断的方式,如果尝试获取许可失败,那么进入AQS的队列中排队。 + +### acquireUninterruptibly() +```java +public void acquireUninterruptibly() { + sync.acquireShared(1); +} +``` +获取一个许可,通过非中断的方式,如果尝试获取许可失败,入AQS的队列中排队。 + +### tryAcquire() +```java +//1. 尝试获取一个许可,通过Sync的非公平模式获取,无论是否获取到许可都会发返回,只尝试一次 +public boolean tryAcquire() { + return sync.nonfairTryAcquireShared(1) >= 0; +} + +//2. 一次尝试获取多个许可, 只尝试一次 +public boolean tryAcquire(int permits) { + if (permits < 0) throw new IllegalArgumentException(); + return sync.nonfairTryAcquireShared(permits) >= 0; +} + +//3. 尝试获取一个许可,如果失败则会等待timeout时间,如果这段时间内都没有获取到许可,则返回false,否则返回true +public boolean tryAcquire(long timeout, TimeUnit unit) throws InterruptedException { + return sync.tryAcquireSharedNanos(1, unit.toNanos(timeout)); +} + +//4. 尝试获取多个许可, 如果失败则会等待timeout时间,如果这段时间内都没有获取到许可,则返回false,否则返回true +public boolean tryAcquire(int permits, long timeout, TimeUnit unit) + throws InterruptedException { + if (permits < 0) throw new IllegalArgumentException(); + return sync.tryAcquireSharedNanos(permits, unit.toNanos(timeout)); +} +``` + +### reducePermits(int reduction) +```java +//减少许可次数 +protected void reducePermits(int reduction) { + if (reduction < 0) throw new IllegalArgumentException(); + sync.reducePermits(reduction); +} +``` + +### drainPermits() +```java +//销毁当前可用的许可次数, 对于已经获取的许可没有影响 +public int drainPermits() { + return sync.drainPermits(); +} +``` diff --git a/week_03/25/synchronized_25.md b/week_03/25/synchronized_25.md new file mode 100644 index 0000000..11ac694 --- /dev/null +++ b/week_03/25/synchronized_25.md @@ -0,0 +1,104 @@ +# synchronized +synchronized关键字是Java中最基本的同步手段,声明synchronized的代码块或者方法会在编译时在代码块或方法的前后添加monitorenter、monitorexit字节码指令,这两个字节码指令都需要一个引用类型的参数来指明要锁定和解锁的对象。 + +### 实现原理 +在Java内存模型中,存在lock和unlock指令,分别用于主内存变量的锁定和解锁。lock会把主内存中的变量标识为一条线程独占状态;unlock会把锁定的变量释放,以使其能被其他线程锁定。但是lock和unlock并没有直接提供给用户使用,而是提供了两个更高层次的指令monitorenter和monitorexit来隐式调用lock和unlock。 + +根据JVM规范的要求,在执行monitorenter指令时,首先会去尝试获取对象的锁,如果该对象没有被锁定,或当前线程已经持有该对象的锁,就把锁的计数器+1;在执行monitorexit时会把计数器-1,当计数器减小为0时,锁就释放了。下面通过代码进行测试,尝试对Test.class对象加上两次synchronized锁,并观察其字节码: +```java +public class Test { + public static void sync() { + synchronized (Test.class) { + synchronized (Test.class) { + + } + } + } +} +``` +sync()的字节码指令: +``` +0: ldc #2 // 加载常量池中的Test.class对象到操作数栈 +2: dup +3: astore_0 +4: monitorenter // 加锁, 参数遍历0 +5: ldc #2 // 再次加载常量池中的Test.class对象到操作数栈 +7: dup +8: astore_1 +9: monitorenter // 加锁, 参数变量1 +10: aload_1 +11: monitorexit // 解锁, 参数变量1 +12: goto 20 // 跳转至20行 +15: astore_2 +16: aload_1 +17: monitorexit // 如果参数变量1的monitorexit因为方法执行异常无法解锁, 那么在这里进行解锁 +18: aload_2 +19: athrow +20: aload_0 // 解锁, 参数变量0 +21: monitorexit +22: goto 30 // 跳转至30行 +25: astore_3 +26: aload_0 +27: monitorexit // 如果参数变量0的monitorexit因为方法执行异常无法解锁, 那么在这里进行解锁 +28: aload_3 +29: athrow +30: return +``` +从中我们能够获取到下面的信息: +(1)synchronized是可重入锁 +(2)synchronized锁定的是对象 +(3)可以发现每次加解锁指令结束后会多出一个monitorenter指令,这是为了保证在方法在异常完成时,前面的monitorenter和monitorexit依然能够正确配对执行,这里的monitorenter就是异常结束时释放monitor的指令。 + +### 原子性、可见性、有序性 +synchronized底层通过monitorenter和monitorexit实现,而这两个指令又通过lock、unlock实现,在JMM中lock和unlock必须满足下面4条规则: +1. 一个变量同一时刻只能允许一个线程对其进行lock操作,且lock操作可以被同一个线程执行多次(可重入锁),多次执行lock后,只有执行相同次数的unlock后变量才能被解锁。 +2. 如果对一个变量执行lock操作,将会清空工作内存中此变量的值,在执行引擎使用这个变量之前,需要重新执行load、assigh操作初始化变量值。 +3. 如果一个变量没有被lock锁定,则不允许对其执行unlock操作,也不允许unlock一个被其他线程lock的变量。 +4. 对一个变量执行unlock之前,必须先通过store、write操作将其同步到主内存中。 +规则1保证了synchronized的原子性,规则1、2、4保证了可见性,规则1、3保证了有序性,由此可以得出synchronized是满足一致性的。 + +### 公平锁or非公平锁 +公平锁与非公平锁的概念就像排队与插队,先进入等待池的线程先获取锁的方式就是公平锁,反之不按等待顺序获取锁的方式就是非公平锁,公平锁虽然避免了饥饿线程的出现,但会导致一定的吞吐量下降,更何况Java的默认调度策略很少会出现饥饿线程的调度发生。 +通过下面的代码测试synchronized是否是公平锁: +```java +public class Test { + public static void sync(String str) { + synchronized (Test.class) { + System.out.println(str); + try { + Thread.sleep(1000); + } catch (InterruptedException e) { + e.printStackTrace(); + } + } + } + + public static void main(String[] args) throws InterruptedException { + new Thread(() -> sync("thread1")).start(); + Thread.sleep(100); + new Thread(() -> sync("thread2")).start(); + Thread.sleep(100); + new Thread(() -> sync("thread3")).start(); + Thread.sleep(100); + new Thread(() -> sync("thread4")).start(); + } +} +``` +如果是公平锁,那么按照等待顺序获取锁的结果应该是:1,2,3,4,但结果是无序的,可以得知synchronized是非公平锁。 + +### 锁优化 +JDK1.6之后,synchronized与ReenTrantLock的性能基本持平,而且虚拟机在未来的性能改进中会更偏向于原生的synchronized,所以还是提倡在synchronized能满足需求的情况下,优先考虑使用synchronized关键字来进行同步。优化后的synchronized和ReenTrantLock一样,在很多地方都是用到了CAS操作。下面是一些synchronized的状态: +(1)偏向锁:在大多数情况下,锁不存在多线程竞争,总是由同一线程多次获得。因此为了减少同一个线程多次获取锁导致的性能开销,引入了偏向锁。 +如果某个对象的锁被一个线程获取,那么该锁就进入偏向锁模式,当线程执行完毕后该锁还未被其他线程获取,则同一个线程再次请求该锁时,无需做任何同步操作。 +(2)轻量级锁:偏向锁不适用与锁竞争激烈的场合,因此当第二个线程加入锁争用的时候,偏向锁就会升级为轻量级锁。 +线程会通过自旋的方式尝试获取轻量级锁,该操作不会阻塞,性能比起重量级锁有显著提升。但是在锁争用激烈的情况下,线程会一直自旋,如果没有一直获取到锁,就会导致性能比起重量级锁还低。 +(3)重量级锁:当自旋的线程自旋到达一定次数还没获取到锁,就会进入阻塞状态,该锁升级为重量级锁,它会使其他线程阻塞,导致性能降低。 + +### 总结 + (1)synchronized在编译时会在同步块前后生成monitorenter和monitorexit指令。 + (2)monitorenter和monitorexit需要一个引用类型的参数,也就意味着synchronized锁的是对象 + (3)monitorenter和monitorexit底层是用JMM的lock、unlock实现的 + (4)synchronized是可重入锁 + (5)synchronized是非公平锁 + (6)synchronized同时保证原子、可见、有序性 + (7)synchronized有偏向锁、轻量级锁、重量级锁三种模式 \ No newline at end of file diff --git a/week_03/25/volatile_25.md b/week_03/25/volatile_25.md new file mode 100644 index 0000000..1c59128 --- /dev/null +++ b/week_03/25/volatile_25.md @@ -0,0 +1,94 @@ +### volatile +volatile是Java提供的最轻量级的同步机制,它只能用于修饰变量,并保证volatile变量可见性、有序性。 + +### 可见性 +可见性指:当一个线程修改了主内存中共享变量的值,其他线程能够立即感知到这种变化。下面通过普通变量和volatile变量在多线程环境中的读写操作进行对比来体现volatile提供的可见性。 + +#### 普通变量的读写 +![normal1]{https://mmbiz.qpic.cn/mmbiz_png/C91PV9BDK3wGmic0kl0SoEbapYxQd1jsQP2Y5SHkbS17NnE3exjib3kp4XWsCiaw0xqSDml5IFdTnEibw0bw5UxbUA/640?wx_fmt=png&tp=webp&wxfrom=5&wx_lazy=1&wx_co=1} +线程A先读取到变量x=0并修改为5,在线程A将副本回写到主内存之前线程B读取到变量x=0,此时线程B读取到的就是旧值,造成了不一致情况。 + +![normal2]{https://mmbiz.qpic.cn/mmbiz_png/C91PV9BDK3wGmic0kl0SoEbapYxQd1jsQ3VQlolYI28jic6bCicMXeSpdbCj2Tn5PEnK6jJkRO7W6UribDMSD1ic4Cg/640?wx_fmt=png&tp=webp&wxfrom=5&wx_lazy=1&wx_co=1} +线程A读取并修改x=5回写到主内存,此时线程B没有读取主存中的新值,依旧使用之前读取到的旧值x=0进行运算,造成了不一致情况。 + +基于以上两种情况可以明确,普通变量是无法满足可见性的。 + +#### volatile变量的读写 +JMM规定volatile变量每次修改都必须立即回写到主内存,volatile变量每次使用都必须从主内存中获取最新值,那么上述的第一种情况就变为:线程A先读取到变量x=0并修改为5,立即回写到主内存,此时线程B读取到变量x=5。第二种情况就变为:线程A读取并修改x=5回写到主内存,此时线程B在运算之前先读取主存中的新值x=5。实例代码如下所示: +```java +public class Test { + private static volatile boolean flag; + + public static void finish() { + flag = true; + } + + public static void main(String[] args) throws InterruptedException { + new Thread(() -> { + while(flag == false) { + + } + System.out.println("new thread finish"); + }).start(); + + Thread.sleep(100); + finish(); + System.out.println("main thread finish"); + } +} +``` +如果flag不声明为volatile,那么new Thread永远都只会从工作内存中获取flag=false的值,因此永远都不会跳出循环。若声明flag为volatile,那么在new Thread执行100ms后,main Thread更新flag=true,并且new Thread会立即感知到,进而跳出while循环。 + +### 有序性 +JVM在执行时为了充分利用CPU的处理能力,可能会在不影响执行结果的情况下对指令进行重排,单线程环境下由于是串行执行因此不会造成影响,而多线程环境下所有的操作都是无序的,因此无法保证指令重排后会按代码顺序执行。 + +而volatile禁止了指令重排,保证了多线程环境下执行有序性。 + +### 原子性 +volatile无法保证原子性,如下代码所示: +```java +public class Test { + private static volatile int count = 0; + + public static void increment() { + count++; + } + + public static void main(String[] args) throws InterruptedException { + for (int i = 0; i < 100; i++) { + new Thread(() -> { + for (int j = 0; j < 100; j++) { + increment(); + } + }).start(); + } + Thread.sleep(1000); + System.out.println(count); + } +} +``` +执行结果:9575 + +具体原因我们可以通过increment方法的字节码进行分析: +``` + 0: getstatic #2 // Field count:I + 3: iconst_1 + 4: iadd + 5: putstatic #2 // Field count:I + 8: return +``` +可以看到count++被分解成了4条指令: +(1)getstatic,获取到count当前的值并入栈 +(2)iconst_1,入栈int类型的值1 +(3)iadd,将栈顶两个值相加 +(4)putstatic,将结果写回到count +由于count被volatile修饰,因此步骤1、4都会保证可见性、有序性,但是步骤2、3在执行时,可能count值已经被其他线程修改了,但是此时并没有重新读取主内存中最新的count而是继续执行下面的指令,因此volatile并不能保证原子性。 + +因此,如果要解决原子性的问题,只能通过对increment方法加锁或者使用原子类。那么总结volatile的使用场景,要么操作指令本身就具备原子性,要么只能增加其他约束条件保证原子性。 + + + + + + + -- Gitee From b276a12b940dd55c6b7487cad6ade79470a41995 Mon Sep 17 00:00:00 2001 From: whl <313576743@qq.com> Date: Sun, 29 Dec 2019 17:16:53 +0800 Subject: [PATCH 2/2] commit --- week_03/25/AQS.md | 37 +++++++++++ ...345\270\203\345\274\217\351\224\201_25.md" | 61 +++++++++++++++++++ 2 files changed, 98 insertions(+) create mode 100644 week_03/25/AQS.md create mode 100644 "week_03/25/Java\345\210\206\345\270\203\345\274\217\351\224\201_25.md" diff --git a/week_03/25/AQS.md b/week_03/25/AQS.md new file mode 100644 index 0000000..eebe201 --- /dev/null +++ b/week_03/25/AQS.md @@ -0,0 +1,37 @@ +### AQS +AbstractQueuedSynchronizer,简称AQS,位于J.U.C.locks包下,它是一个用来构建锁和同步器的框架,使用AQS能够简单且高效地构建出广泛的大量同步器,比如ReenTrantLock,Semaphore等,当然也可以自己利用AQS手动构造一个符合自身需求的同步器。 + +### 原理 +AQS的核心思想是:如果被请求的共享资源空闲,则将当前请求资源的线程设置为有效线程,并且将被共享资源设置为锁定状态,如果被请求的共享资源被占用,那么就需要一套线程阻塞等待以及唤醒时锁分配的机制,这个机制就是AQS用队列锁实现的,也就是将暂时获取不到锁的线程加入到队列中。 + +AQS用一个int成员变量标识同步状态,通过内置的FIFO队列完成获取资源线程的排队工作。AQS使用CAS对该同步状态进行原子操作,实现对其值的修改。 +```java +private volatile int state;//共享变量,使用volatile修饰保证线程可见性 +``` +该状态信息可以通过getState,setState,compareAndSetState进行操作: +```java +//返回同步状态的当前值 +protected final int getState() { + return state; +} +// 设置同步状态的值 +protected final void setState(int newState) { + state = newState; +} +//原子地(CAS操作)将同步状态值设置为给定值update如果当前同步状态的值等于expect(期望值) +protected final boolean compareAndSetState(int expect, int update) { + return unsafe.compareAndSwapInt(this, stateOffset, expect, update); +} +``` + +### 资源共享方式: +AQS定义了两种资源共享方式: +独占:只有一个线程能执行,如ReenTrantLock。又可分为公平锁和非公平锁,公平锁按照队列中的排队顺序获取锁;非公平锁当线程要获取锁时,无视队列顺序抢占锁。 +共享:多个线程可以同时执行,如Semaphore/CountDownLatch。 + +不同的自定义同步器争用共享资源的方式也不同,自定义同步器在实现时只需要实现共享资源的state获取与释放即可,至于具体线程等待队列的维护AQS已经帮我们实现好了。 + +### 模版方法模式 +同步器的设计是基于模版方法模式的,如果需要自定义同步器,使用者可以继承AQS并重写指定方法(state的获取和释放),再将AQS组合在自定义同步器组件中实现,并调用其模版方法。 + +以ReenTrantLock为例,state初始为0,表示未锁定状态。线程A执行lock操作时调用tryAcquire()独占该资源并将state设置为加1,当其他线程试图执行lock操作时调用tryAcquire()就会失败,直到线程A执行unlock将state设置为0。当然在释放锁之前,线程A是可以重复获取锁的,此时state = state++,也就是可重入锁。 diff --git "a/week_03/25/Java\345\210\206\345\270\203\345\274\217\351\224\201_25.md" "b/week_03/25/Java\345\210\206\345\270\203\345\274\217\351\224\201_25.md" new file mode 100644 index 0000000..99fc9a1 --- /dev/null +++ "b/week_03/25/Java\345\210\206\345\270\203\345\274\217\351\224\201_25.md" @@ -0,0 +1,61 @@ +### 什么是锁 +在单进程的系统中,当存在多个线程同时修改某个共享变量时,就需要对该变量或者代码块进行加锁,使其在并发环境下能够安全地并发修改变量。 + +而同步的本质是通过加锁实现的,为了实现多个线程在同一时刻同一段代码块只能被一个线程执行,那么需要在某个地方作标记,而这个标记必须被所有线程看到,当代码块的标记不存在时就允许线程对其操作,而当标记存在时其他线程只能等待持有标记的线程结束同步操作后取消标记才能够尝试去设置标记。这个标记就是加锁。 + +不同的位置实现锁的方式也不同,只要能满足所有线程都能够看到标记即可。如synchronized是在对象头设置标记,Lock接口的实现类基本上也都是通过对某个变量声明为volatile以保证其被每个线程可见,然后通过CAS操作满足原子性。 + + +### 什么是分布式 +"任何一个分布式系统都无法同时满足一致性、可用性、分区容错性,最多同时满足两项",目前很多大型网站以及应用都是分布式部署的。基于上述CAP理论,很多系统在设计初就要对三者进行取舍,而绝大多数场景中,都要牺牲强一致性来换取系统的高可用性,系统往往只保证最终的一致性。 + +在许多场景中,为了保证数据的最终一致性,需要很多技术方案支持,例如:分布式事务、分布式锁等。很多时候需要保证一个方法同一个时间只能被一个线程执行,在单机环境中,通过Java提供的并发API就可以解决,但是在分布式环境下,就没那么简单: +* 分布式与单机情况最大的不同在于它不是多线程而是多进程。 + +* 多线程由于可以共享堆内存,因此可以简单地采取内存作为锁标记,而进程之间由于都不在同一台物理机上,因此需要将标记存储在一个所有进程都能看到的地方。 + +### 什么是分布式锁 +当在分布式模型下,数据只有一份或是有限制的数据时,此时需要利用锁控制某一时刻修改数据的进程数。 + +与单机模式下的锁不同,分布式锁不仅需要保证进程可见,也要考虑进程与锁之间的网络问题。 + +分布式锁也是可以将标记存在内存的,只是该内存不是某个进程分配的内存而是公共内存,如Redis、Memcache。至于利用数据库,文件等做锁与单机的实现是一样的,只要保证标记能互斥就行。 + +### 分布式锁要求 +1. 可以保证在分布式部署的集群中,同一个方法在同一时间内只能被同一台机器上的一个线程执行。 + +2. 这把锁需要是可重入锁(避免死锁) + +3. 考虑业务要求,要求该锁是公平锁 + +4. 有高可用的获取锁和释放锁的功能 + +5. 获取锁和释放锁的功能 + +### 基于数据库作分布式锁 +基于表的主键唯一作分布式乐观锁: + +思路:利用主键唯一的特性,如果有多个请求同时提交到数据库,数据库会保证只有一个操作可以成功,那么我们就认为成功操作的线程获取到了该方法的锁,当方法执行完毕时,想要释放锁只需要删除这条数据库记录。 + +缺点: +1. 强依赖数据库的可用性,数据库是个单点,一旦挂掉会导致业务系统不可用。 + +2. 这把锁没有失效时间,一旦解锁操作失败,就会导致锁记录一直在数据库中,其他线程无法再获取锁。 + +3. 这把锁只能是非阻塞的,因为数据库的insert操作,一旦插入失败就会直接报错。没有获取到锁的线程并不会进入排队队列,想要再次获取锁就需要再次触发获取锁的操作。 + +4. 非公平锁,所有等待锁只能凭借运气争夺。 + +5. 在Mysql中采用主键防止冲突,在大并发情况下可能会造成锁表现象。 + +问题解决: +1. 通过多个数据库解决单点问题,数据之前双向同步,一旦挂掉就切换到备库。 + +2. 通过定时任务,每隔一定时间把数据库的超时数据清理一遍。 + +3. 通过while循环解决非阻塞问题,直到insert成功再返回 + +4. 通过数据库表中增加字段解决非重入,记录当前获取锁的机器的主机信息和线程信息,下次获取锁先查询数据库是否已经存在,如果存在直接分配锁即可。 + +5. 通过程序中生成主键防止冲突。 + -- Gitee