并发五-J.U.C
18. JUC-原子类
18.1 原子整数
J.U.C并发包提供了:(基本类型)
- AtomicBoolean
- AtomicInteger
- AtomicLong
以AtomicInteger为例:
-
package com.sunyang.concurrentstudy; import java.util.concurrent.atomic.AtomicInteger; /** * @program: ConcurrentStudy * @description: Dmeo * @author: SunYang * @create: 2021-08-07 20:01 **/ public class AtomicIntegerDemo { public static void main(String[] args) { AtomicInteger i = new AtomicInteger(0); System.out.println(i.incrementAndGet()); // ++i 自增并获取值。 System.out.println(i.getAndIncrement()); // i++ 获取值并自增 System.out.println(i.decrementAndGet()); // --i System.out.println(i.getAndDecrement()); // i-- System.out.println(i.addAndGet(10)); // 10+=i System.out.println(i.getAndAdd(10)); // i+=10 System.out.println(i.get()); // 获取值 } }
-
1 1 1 1 10 10 20
-
package com.sunyang.concurrentstudy; import java.util.concurrent.atomic.AtomicInteger; import java.util.function.IntUnaryOperator; /** * @program: ConcurrentStudy * @description: Dmeo * @author: SunYang * @create: 2021-08-07 20:01 **/ public class AtomicIntegerDemo { public static void main(String[] args) { AtomicInteger i = new AtomicInteger(5); // 读取到的值 设置值 i.updateAndGet(operand -> operand * 10); System.out.println(i.get()); // 获取值 50 } }
-
// 手写 updateAndGet public static int updateAndSetDemo(AtomicInteger i, IntUnaryOperator operator) { while(true) { int prev = i.get(); int next = operator.applyAsInt(prev); if (i.compareAndSet(prev, next)) { return next; } }}
-
// 真正底层public final int updateAndGet(IntUnaryOperator updateFunction) { int prev, next; do { prev = get(); next = updateFunction.applyAsInt(prev); } while (!compareAndSet(prev, next)); return next;}
18.2 原子引用类型
- AtomicReference
- AtomicMarkableReference
- AtomicStampedReference
18.2.1 AtomicReference
-
package com.sunyang.concurrentstudy;import java.math.BigDecimal;import java.util.ArrayList;import java.util.List;import java.util.concurrent.atomic.AtomicReference;/** * @program: ConcurrentStudy * @description: Demo * @author: SunYang * @create: 2021-08-07 20:31 **/public class AtomicReferenceDecimalDemo { public static void main(String[] args) { DecimalAccount.demo(new DecimalAccountCas(new BigDecimal("10000"))); }}class DecimalAccountCas implements DecimalAccount { private AtomicReference<BigDecimal> balances; public DecimalAccountCas(BigDecimal balances) { this.balances = new AtomicReference<>(balances); } @Override public BigDecimal getBalance() { return balances.get(); } @Override public void withdraw(BigDecimal amount) { while (true) { BigDecimal prev = balances.get(); BigDecimal next = prev.subtract(amount); if (balances.compareAndSet(prev, next)) { break; } } }}interface DecimalAccount { // 获取余额 BigDecimal getBalance(); // 取款 void withdraw(BigDecimal amount); /** * 方法内会启动1000个线程,每个线程做-10操作 * 如果初始余额为10000,那么正确的结果应该是0 **/ static void demo(DecimalAccount account) { List<Thread> ts = new ArrayList<>(); long start = System.nanoTime(); // 1000 个线程只是为了演示,其实不符合我们CAS的使用场景。 for (int i = 0; i < 1000; i++) { ts.add(new Thread(() -> { account.withdraw(BigDecimal.TEN); })); } ts.forEach(Thread::start); ts.forEach(t -> { try { t.join(); } catch (InterruptedException e) { e.printStackTrace(); } }); long end = System.nanoTime(); System.out.println(account.getBalance() + "cost: " + (end - start) / 1000_000 + "ms"); }}
18.2.2 原子引用ABA问题
-
package com.sunyang.concurrentstudy;import com.sun.webkit.ThemeClient;import lombok.extern.slf4j.Slf4j;import java.util.concurrent.TimeUnit;import java.util.concurrent.atomic.AtomicReference;/** * @program: ConcurrentStudy * @description: Demo * @author: SunYang * @create: 2021-08-07 20:52 **/@Slf4j(topic = "c.Demo")public class ABADemo { static AtomicReference<String> ref = new AtomicReference<>("A"); public static void main(String[] args) throws InterruptedException { log.debug("main start"); String prev = ref.get(); abatest(); TimeUnit.SECONDS.sleep(1); log.debug("change A -> C {}", ref.compareAndSet(prev, "C")); } private static void abatest() throws InterruptedException { new Thread(() -> { log.debug("change A -> B {}", ref.compareAndSet(ref.get(), "B")); }).start(); TimeUnit.SECONDS.sleep(1); new Thread(() -> { log.debug("change B -> A {}", ref.compareAndSet(ref.get(), "A")); }).start(); }}
-
20:59:00 [main] c.Demo - main start20:59:00 [Thread-0] c.Demo - change A -> B true20:59:01 [Thread-1] c.Demo - change B -> A true20:59:02 [main] c.Demo - change A -> C true
-
主线程是感知不到ref被其他线程修改了的,他只能知道,现在最新值和我当时拿到的值是否一致。
18.2.3 AtomicStampedReference
-
只要其他线程**【动过了】**共享变量,那么自己的CAS就算失败。这时,仅比较值是不够的,需要再加一个版本号。
-
为什么要既判断版本号又要判断值,是因为,如果只判断版本号,那么只能判断其他线程是否改动过共享变量,不能判断本线程内其他位置是否在你获取完值后更改了共享变量的值。例如下面代码中标记所示
-
package com.sunyang.concurrentstudy;import lombok.extern.slf4j.Slf4j;import java.util.concurrent.TimeUnit;import java.util.concurrent.atomic.AtomicReference;import java.util.concurrent.atomic.AtomicStampedReference;/** * @program: ConcurrentStudy * @description: Demo * @author: SunYang * @create: 2021-08-07 21:04 **/@Slf4j(topic = "c.Demo")public class AtomicStampedReferenceDemo { static AtomicStampedReference<String> ref = new AtomicStampedReference<>("A", 0); public static void main(String[] args) throws InterruptedException { log.debug("main start"); String prev = ref.getReference(); int stamp = ref.getStamp(); // 如果在此更改了 prev 的值,只判断版本号是不行的,因为值已经被改变了。 // prev = "B"; log.debug("{}", stamp); abatest(); TimeUnit.SECONDS.sleep(1); log.debug("change A -> C {}", ref.compareAndSet(prev, "C", stamp, stamp + 1)); } private static void abatest() throws InterruptedException { new Thread(() -> { int stamp = ref.getStamp(); log.debug("{}", stamp); log.debug("change A -> B {}", ref.compareAndSet(ref.getReference(), "B", stamp, stamp + 1)); }).start(); TimeUnit.SECONDS.sleep(1); new Thread(() -> { int stamp = ref.getStamp(); log.debug("{}", stamp); log.debug("change B -> A {}", ref.compareAndSet(ref.getReference(), "A", stamp, stamp + 1)); }).start(); }}
-
21:27:48 [main] c.Demo - main start21:27:48 [main] c.Demo - 021:27:48 [Thread-0] c.Demo - 021:27:48 [Thread-0] c.Demo - change A -> B true21:27:49 [Thread-1] c.Demo - 121:27:49 [Thread-1] c.Demo - change B -> A true21:27:50 [main] c.Demo - change A -> C false
18.2.4 AtomicMarkableReference
-
AtomicStampedReference我们可以知道,引用变量中途被改了多少次。但有时候,我们不关心更改了多少次,我们只关心是否更改过。就用AtomicMarkableReference
-
package com.sunyang.concurrentstudy;import lombok.extern.slf4j.Slf4j;import java.util.concurrent.TimeUnit;import java.util.concurrent.atomic.AtomicMarkableReference;/** * @program: ConcurrentStudy * @description: Demo * @author: SunYang * @create: 2021-08-07 22:13 **/@Slf4j(topic = "c.Demo")public class AtomicMarkableReferenceDemo { public static void main(String[] args) throws InterruptedException { GarbageBag bag = new GarbageBag("装满了垃圾"); AtomicMarkableReference<GarbageBag> ref = new AtomicMarkableReference<>(bag, true); log.debug("start...."); GarbageBag prev = ref.getReference(); log.debug(prev.toString()); new Thread(() -> { log.debug("保洁阿姨,start...."); bag.setDesc("空垃圾袋"); ref.compareAndSet(bag, bag, true, false); log.debug(bag.toString()); }, "保洁阿姨").start(); TimeUnit.SECONDS.sleep(1); log.debug("想换一只垃圾袋?"); boolean success = ref.compareAndSet(prev, new GarbageBag("空垃圾袋"), true, false); log.debug("换了吗?" + success); log.debug(ref.getReference().toString()); }}class GarbageBag { String desc; public GarbageBag (String desc) { this.desc = desc; } public void setDesc(String desc) { this.desc = desc; } @Override public String toString() { return super.toString() + " " + desc; }}
-
22:27:33 [main] c.Demo - start....22:27:33 [main] c.Demo - com.sunyang.concurrentstudy.GarbageBag@6483f5ae 装满了垃圾22:27:33 [保洁阿姨] c.Demo - 保洁阿姨,start....22:27:33 [保洁阿姨] c.Demo - com.sunyang.concurrentstudy.GarbageBag@6483f5ae 空垃圾袋22:27:34 [main] c.Demo - 想换一只垃圾袋?22:27:34 [main] c.Demo - 换了吗?false22:27:34 [main] c.Demo - com.sunyang.concurrentstudy.GarbageBag@6483f5ae 空垃圾袋
18.3 原子数组
- AtomicIntegerArray
- AtomicLongArray
- AtomicReferenceArray
- 因为原子引用类型和原子整数只能保证引用地址的线程安全性问题,不能保证引用对象内部数据的线程安全。
18.3.1 案例
-
package com.sunyang.concurrentstudy;import lombok.extern.slf4j.Slf4j;import java.util.ArrayList;import java.util.Arrays;import java.util.List;import java.util.concurrent.atomic.AtomicInteger;import java.util.concurrent.atomic.AtomicIntegerArray;import java.util.function.BiConsumer;import java.util.function.Consumer;import java.util.function.Function;import java.util.function.Supplier;/** * @program: ConcurrentStudy * @description: Demo * @author: SunYang * @create: 2021-08-08 10:02 **/@Slf4j(topic = "c.Demo")public class AtomicArrayDemo { public static void main(String[] args) { demo( () -> new int[10], (array) -> array.length, (array, index) -> array[index]++, array -> System.out.println(Arrays.toString(array)) ); demo( () -> new AtomicIntegerArray(10), (array) -> array.length(), (array, index) -> array.incrementAndGet(index), atomicIntegerArray -> System.out.println(atomicIntegerArray) ); } /** * 参数1:提供数组,可以是线程不安全数组,也可以是线程安全数组 * 参数2:获取数组长度的方法 * 参数3:自增方法,回传array index * 参数4: 打印数组的方法 * **/ // supplier 提供者 无中生有 ()-> 结果 // function 函数 礼尚往来 一个参数一个结果 (参数) ->结果, 两个参数一个结果,BiFunction(参数1,参数2) ->结果 // consumer 消费者 有来无回 一个参数无结果 (参数) ->void , 两个参数无结果 BiConsumer(参数1,参数2) ->void private static <T> void demo( Supplier<T> arraySupplier, Function<T, Integer> lengthFun, BiConsumer<T, Integer> putConsumer, Consumer<T> printConsumer) { List<Thread> ts = new ArrayList<>(); T array = arraySupplier.get(); int length = lengthFun.apply(array); for (int i = 0; i < length; i++) { ts.add(new Thread(() -> { for (int j = 0; j < 10000; j++) { putConsumer.accept(array, j % length); } })); } ts.forEach(t -> t.start()); ts.forEach(t -> { try { t.join(); } catch (InterruptedException e) { e.printStackTrace(); } }); printConsumer.accept(array); }}
-
[9900, 9902, 9902, 9899, 9905, 9906, 9905, 9906, 9904, 9902][10000, 10000, 10000, 10000, 10000, 10000, 10000, 10000, 10000, 10000]
18.4 字段更新器
原子数组保护的是数组里面的元素,字段更新器保护的是某个对象中的属性(成员变量),保证多个线程访问同一个对象时对象成员变量的线程安全性问题,
-
AtomicReferenceFieldUpdater // 域 字段 只要是引用类型就行
-
AtomicIntegerFieldUpdater // 必须是整型的
-
AtomicLongFieldUpdater // 属性字段必须是长整型的
-
package com.sunyang.concurrentstudy;import java.util.concurrent.atomic.AtomicReferenceFieldUpdater;/** * @program: ConcurrentStudy * @description: Demo * @author: SunYang * @create: 2021-08-08 10:36 **/public class AtomicFieldDemo { public static void main(String[] args) { Student stu = new Student(); AtomicReferenceFieldUpdater<Student, String> updater = AtomicReferenceFieldUpdater.newUpdater(Student.class, String.class, "name"); System.out.println(updater.compareAndSet(stu, null, "张三")); System.out.println(stu); }}class Student { volatile String name ; @Override public String toString() { return "Student{" + "name='" + name + '\'' + '}'; }}
-
true Student{name='张三'}
18.5 原子累加器
18.5.1 案例
-
package com.sunyang.concurrentstudy; import java.util.ArrayList; import java.util.List; import java.util.concurrent.atomic.AtomicLong; import java.util.concurrent.atomic.LongAdder; import java.util.function.Consumer; import java.util.function.Supplier; /** * @program: ConcurrentStudy * @description: Demo * @author: SunYang * @create: 2021-08-08 10:47 **/ public class AtomicLongAddrDemo { public static void main(String[] args) { for (int i = 0; i < 5; i++) { demo( () -> new AtomicLong(0), (adder) -> adder.getAndIncrement() ); demo( () -> new LongAdder(), adder -> adder.increment() ); } } private static <T> void demo (Supplier<T> adderSupplier, Consumer<T> action) { T adder = adderSupplier.get(); List<Thread> ts = new ArrayList<>(); for (int i = 0; i < 4; i++) { ts.add(new Thread(() -> { for (int j = 0; j < 50_0000; j++) { action.accept(adder); } })); } long start = System.nanoTime(); ts.forEach(t -> t.start()); ts.forEach(t -> { try { t.join(); } catch (InterruptedException e) { e.printStackTrace(); } }); long end = System.nanoTime(); System.out.println(adder + " cost: "+ (end -start)/1000_000); } }
-
2000000 cost: 1572000000 cost: 652000000 cost: 1432000000 cost: 602000000 cost: 1482000000 cost: 582000000 cost: 1012000000 cost: 722000000 cost: 1362000000 cost: 41
-
性能提升的原因,就是在有竞争时,设置多个累加单元,Thread-0累加Cell【0】,而Thread-1累加Cell【1】… 最后及那个结果汇总,这样他们在累加时操作不同的Cell变量。因此减少了CAS重试次数,从而提高性能,在竞争激烈的时候会先用两个累加单元,当在激烈的时候再加,但是不能超过CPU核心数。
18.5.2 LongAdder 源码分析
-
LongAdder是并发大师@author Doug Lea (大哥李)的作品
-
LongAdder类有几个关键域
-
// 累加单元数组,懒惰初始化transient volatile Cell[] cells;// 基础值,如果没有竞争,则用cas累加这个域transient volatile long base;// 在cells创建 或扩容时,置为1 表示加锁 加锁方式用的CAS 加锁标记位 transient volatile int cellsBusy;
-
18.5.3 原理之伪共享
-
@sun.misc.Contended // 防止缓存行伪共享 static final class Cell { volatile long value; Cell(long x) { value = x; } // 最重要的方法 用cas方式进行累加prev表示旧值,next表示新值 final boolean cas(long cmp, long val) { return UNSAFE.compareAndSwapLong(this, valueOffset, cmp, val); }
18.5.3.1 CPU缓存
-
因为CPU与内存的速度差异很大,是需要靠预读数据至缓存来提升效率
-
而缓存以缓存行位单位,每个缓存行对应着一块内存,一般是64byte字节(8个long)但也有128字节的,
-
缓存的加入会造成数据副本的产生,即同一份数据可能存在不同核心的缓存行中,就会出现数据不一致问题,这个问题通过MESI缓存一致性协议加上嗅探机制来解决缓存不一致问题。如果有一个cpu更改了数据,那么另一个缓存中的数据会随之失效。
-
因为 Cell 是数组形式,在内存中是连续存储的,一个 Cell 为 24 字节(16 字节的对象头和 8 字节的 value),因 此缓存行可以存下 2 个的 Cell 对象。这样问题来了:
-
Core-0 要修改 Cell[0]
-
Core-1 要修改 Cell[1]
-
-
无论谁修改成功,都会导致对方 Core 的缓存行失效,比如 Core-0 中 Cell[0]=6000, Cell[1]=8000 要累加 Cell[0]=6001, Cell[1]=8000 ,这时会让 Core-1 的缓存行失效
-
@sun.misc.Contended 用来解决这个问题,它的原理是在使用此注解的对象或字段的前后各增加 128 字节大小的 padding,从而让 CPU 将对象预读至缓存时占用不同的缓存行,这样,不会造成对方缓存行的失效
18.5.4 源码之add
-
public void add(long x) { Cell[] as; long b, v; int m; Cell a; if ((as = cells) != null || !casBase(b = base, b + x)) { boolean uncontended = true; if (as == null || (m = as.length - 1) < 0 || (a = as[getProbe() & m]) == null || !(uncontended = a.cas(v = a.value, v + x))) longAccumulate(x, null, uncontended); } }
-
longAccumulate(x, null, uncontended);
19. Unasfe
19.1 概述
- Unsafe 对象提供了非常底层的,操作内存、线程的方法,Unsafe 对象不能直接调用,只能通过反射获得
19.2 Unsafe的CAS基本应用
-
package com.sunyang.concurrentstudy; import sun.misc.Unsafe; import java.lang.reflect.Field; /** * @program: ConcurrentStudy * @description: Demo * @author: SunYang * @create: 2021-08-08 13:26 **/ public class UnsafeDemo { public static void main(String[] args) throws NoSuchFieldException, IllegalAccessException { Field theUnsafe = Unsafe.class.getDeclaredField("theUnsafe"); theUnsafe.setAccessible(true); Unsafe unsafe = (Unsafe) theUnsafe.get(null); System.out.println(unsafe); // 1 获取到属性的偏移地址(偏移量)通过对象和域中的偏移地址,就能得到成员变量(域)中的值 long idOffset = unsafe.objectFieldOffset(TestUnsafe.class.getDeclaredField("id")); long nameOffset = unsafe.objectFieldOffset(TestUnsafe.class.getDeclaredField("name")); TestUnsafe testUnsafe = new TestUnsafe(); // 2 执行CAS操作 unsafe.compareAndSwapInt(testUnsafe, idOffset, 0, 1); unsafe.compareAndSwapObject(testUnsafe, nameOffset, null, "张三"); System.out.println(testUnsafe); } } class TestUnsafe { volatile int id; volatile String name; public int getId() { return id; } public void setId(int id) { this.id = id; } public String getName() { return name; } public void setName(String name) { this.name = name; } @Override public String toString() { return "TestUnsafe{" + "id=" + id + ", name='" + name + '\'' + '}'; } }
-
sun.misc.Unsafe@45ee12a7TestUnsafe{id=1, name='张三'}
19.3 原子整数类手写实现
-
package com.sunyang.concurrentstudy; import sun.misc.Unsafe; import java.lang.reflect.Field; /** * @program: ConcurrentStudy * @description: Demo * @author: SunYang * @create: 2021-08-08 14:47 **/ public class UnsafeAtomicDemo { private static final Unsafe unsafe; static { try { Field theUnsafe = Unsafe.class.getDeclaredField("theUnsafe"); theUnsafe.setAccessible(true); unsafe = (Unsafe) theUnsafe.get(null); } catch (NoSuchFieldException | IllegalAccessException e) { e.printStackTrace(); throw new Error(e); } } public static Unsafe getUnsafe(){ return unsafe; } public static void main(String[] args) { Account.demo(new MyAtomicInteger(10000)); } } class MyAtomicInteger implements Account{ private volatile int value; private static final long valueOffset; static final Unsafe UNSAFE; static { UNSAFE = UnsafeAtomicDemo.getUnsafe(); try { valueOffset = UNSAFE.objectFieldOffset(MyAtomicInteger.class.getDeclaredField("value")); } catch (NoSuchFieldException e) { e.printStackTrace(); throw new RuntimeException(e); } } public int getValue() { return value; } public void decrement(int amount) { while(true){ int prev = this.value; int next = prev - amount; if (UNSAFE.compareAndSwapInt(this, valueOffset, prev, next)){ break; } } } public MyAtomicInteger(int value) { this.value = value; } @Override public Integer getBalance() { return getValue(); } @Override public void withdraw(Integer amount) { decrement(amount); } }
28. J.U.C-AQS
28.1 AQS原理
281.1. 概述
- 全称是AbstractQueued,是阻塞式锁和相关的同步器工具的框架。
- 特点:
- 用state来表示资源的状态(分为独占模式和共享模式),子类需要定义如何维护这个状态,控制如何获取锁和释放锁。
- getState - 获取state状态
- setState - 设置state状态
- compareAndSetState - CAS机制设置state状态
- 独占模式是只有一个线程能够访问资源,而共享模式可以允许多个线程资源访问资源
- 提供了基于FIFO的等待队列,类似于Monitor的EntryList
- 条件变量来实现等待,唤醒机制,支持多个条件变量,类似于MonitorWaitSet
- 用state来表示资源的状态(分为独占模式和共享模式),子类需要定义如何维护这个状态,控制如何获取锁和释放锁。
子类主要实现这样一些方法(默认抛出UnsupportedOperationExeception)
- tryAcquire // 加锁
- tryRelease // 解锁
- tryAcquireShared
- tryReleaseShared
- isHeldExclusively // 是否持有独占锁
获取锁的姿势
-
// 如果获取锁失败 if(!tryAcquire(arg)) { // 失败 入队,可以选择阻塞当前线程 暂停和恢复运行 用的是park 和unpark机制 }
-
// 如果释放锁成功 if(tryRelease(arg)) { // 让阻塞线程恢复运行 }
28.1.2 理解
- AQS:
- 他的acquire() release() 方法是AQS的固定机制,比如怎么把线程阻塞,怎么加入队列等等,
- 而tryAcquire() tryRelease() 等是自己实现的,自己决定是否可重入,是否可共享等。
- 然后Lock可以通过调用AQS方法实现锁机制。
- 例如:ReenTrantLock是实现了Lock接口 重写了Lock接口中的lock和unlock和tryLock等方法,而重写这些方法的实现是因为他创建了一个内部类继承了AQS,因为AQS中acquire() release() 方法是固定机制,已经为我们写好了具体的加锁和解锁流程,但是我们要自己去实现一些(tryAcquire() tryRelease())方法,通过控制这些方法的实现方式,来定制一些特殊的需求(例如是否可重入等)这样就能实现一个可重入锁或非可重入锁等。。
28.2 实现不可重入锁
代码示例
-
package com.sunyang.concurrentstudy; import lombok.extern.slf4j.Slf4j; import java.util.concurrent.TimeUnit; import java.util.concurrent.locks.AbstractQueuedSynchronizer; import java.util.concurrent.locks.Condition; import java.util.concurrent.locks.Lock; /** * @Author: sunyang * @Date: 2021/8/11 * @Description: */ @Slf4j(topic = "c.Demo") public class MyReentrantLockDemo { public static void main(String[] args) { MyLock lock = new MyLock(); new Thread(() -> { lock.lock(); try { log.debug("t1 加锁成功"); try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); } } finally { log.debug("t1 解锁...."); lock.unlock(); } }, "t1").start(); new Thread(() -> { lock.lock(); try { log.debug("t2 加锁成功"); } finally { log.debug("t2 解锁...."); lock.unlock(); } }, "t2").start(); } } class MyLock implements Lock{ class MySync extends AbstractQueuedSynchronizer{ @Override protected boolean tryAcquire(int arg) { if(compareAndSetState(0, 1)) { // 加上了锁 setExclusiveOwnerThread(Thread.currentThread()); return true; } return false; } @Override protected boolean tryRelease(int arg) { setExclusiveOwnerThread(null); setState(0); return true; } @Override // 是否持有独占锁 protected boolean isHeldExclusively() { return getState() == 1; } public Condition newCondition() { return new ConditionObject(); } } private MySync sync = new MySync(); @Override // 加锁,不成功进入等待队列等待 public void lock() { sync.acquire(1); } @Override // 加锁,可打断 public void lockInterruptibly() throws InterruptedException { sync.acquireInterruptibly(1); } @Override // 尝试加锁,成功返回true,失败返回false public boolean tryLock() { return sync.tryAcquire(1); } @Override // 尝试加锁,带超时时间 public boolean tryLock(long time, TimeUnit unit) throws InterruptedException { return sync.tryAcquireNanos(1, unit.toNanos(time)); } @Override // 解锁 public void unlock() { sync.release(1); } @Override // 创建条变量 public Condition newCondition() { return sync.newCondition(); } }
-
11:14:03 [t1] c.Demo - t1 加锁成功 11:14:05 [t1] c.Demo - t1 解锁.... 11:14:05 [t2] c.Demo - t2 加锁成功 11:14:05 [t2] c.Demo - t2 解锁....
28.3 ReentrantLock 原理
28.3.1 概览图
28.3.2 非公平锁实现原理
加锁流程
(此为JDK8流程,11失败改为短路与的方式)
无竞争:
-
构造器,默认为非公平锁实现
-
static final class NonfairSync extends Sync { private static final long serialVersionUID = 7316153563782823691L; /** * Performs lock. Try immediate barge, backing up to normal * acquire on failure. */ final void lock() { if (compareAndSetState(0, 1)) // 设置状态 setExclusiveOwnerThread(Thread.currentThread());// 设置当前锁的拥有者为当前线程 else acquire(1); } protected final boolean tryAcquire(int acquires) { return nonfairTryAcquire(acquires); } }
-
NonfairSync继承自AQS
-
没有竞争时
有竞争的加锁:
-
第一个竞争者出现
-
static final class NonfairSync extends Sync { private static final long serialVersionUID = 7316153563782823691L; /** * Performs lock. Try immediate barge, backing up to normal * acquire on failure. */ final void lock() { if (compareAndSetState(0, 1)) setExclusiveOwnerThread(Thread.currentThread()); else acquire(1); // 加锁失败走此流程 } protected final boolean tryAcquire(int acquires) { return nonfairTryAcquire(acquires); } }
-
public final void acquire(int arg) { if (!tryAcquire(arg) && // 然后会再用CAS尝试加一次锁(ReentrantLock重写的非公平锁的tryAcuire方法),如果加上了就加上了,如果加不上就进入等待队列 acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) selfInterrupt(); }
-
final boolean nonfairTryAcquire(int acquires) { final Thread current = Thread.currentThread(); int c = getState(); if (c == 0) { // 如果这时也就是在再次尝试的过程中被解锁了,那么它还可以获得锁,这里体现了非公平性:不去检查AQS队列中是否有等待线程。 if (compareAndSetState(0, acquires)) { setExclusiveOwnerThread(current); return true; } } else if (current == getExclusiveOwnerThread()) { // 否则他会判断当前所的拥有者是否是当前线程(也就是我这个线程本身)持有的锁,如果是则将状态位加1,也就是锁重入的原理,解锁时会逐一递减。 int nextc = c + acquires; if (nextc < 0) // overflow throw new Error("Maximum lock count exceeded"); setState(nextc); return true; } return false; }
-
如果再次尝试失败了,那么会进入到**acquireQueued(addWaiter(Node.EXCLUSIVE), arg)**流程
-
// addWaiter(Node.EXCLUSIVE) 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; }
- Thread-1执行流程
-
- CAS 尝试将state由0改为1,结果失败。(第一次进入的CAS)
- 进入tryAcquire()逻辑,这时state仍然为1,Thread-0没有释放锁,结果仍然失败(第二次)
- 接下来进入addWaiter()逻辑,构造Node队列(双向链表)
- 他会创建一个双向链表,其中第一个Node称为Dummy(哑元也就是头节点)或哨兵,用来占位,并不关联线程
- Node的创建是懒惰的。图中的head并不是指的双线链表的头节点,只是一个元素用来存储双向链表的头节点的地址。类似于指针、
- 图中黄色三角表示该Node的waitStatus状态,其中0为默认正常状态
- 第一个线程直接抢占同步器,第二个线程抢不到锁后初始化队列,并创建哑元和第一个Node节点,第三个线程产生Node节点2,并将其插入到node1节点后。
-
然后进入**acquireQueued(addWaiter(Node.EXCLUSIVE), arg))**方法
-
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); } }
-
他会在一个死循环中,判断当前节点的前驱节点是否是头节点,
-
如果是头节点就代表他是除了头结点的第一个节点,那么他会在次调用tryAcquire()方法尝试加锁(第三次)
-
如果再次失败,会进入shouldParkAfterFailedAcquire(p, node) 方法(是否将加锁失败后的线程加入阻塞队列,进入阻塞状态。)
-
进入shouldParkAfterFailedAcquire(p, node) 方法后,也就是第一次进入此方法时,会将该节点的前驱Node即Head(也就是哑元节点)的waitStatus改为-1,这次返回false,然后会在尝试一次(第四次)
-
waitStatus为-1表示他有责任唤醒他的后继节点。进入阻塞后得有一个节点去唤醒她。
-
当再次进入shouldParkAfterFailedAcquire时就会返回true,这时因为其前驱节点Node的waitStatus已经是-1,这次返回true
-
然后进入parkAndCheckInterrupt()进入阻塞Thread-1 park (灰色表示)
-
-
再次有多个线程经历上述过程竞争失败,变成这个样
释放锁流程
-
Thread-0释放锁,会先进入
-
// 第一步 public void unlock() { sync.release(1); } // 第二步 AQS中的方法 public final boolean release(int arg) { if (tryRelease(arg)) { // 如果tryRelease成功,进入 Node h = head; // 判断当前队列是否为null 并且head节点(哑元节点)的waitStatus状态是否不为0, if (h != null && h.waitStatus != 0) // 如果不为0 则唤醒他的后继节点 在这里唤醒 // if (shouldParkAfterFailedAcquire(p, node) && // parkAndCheckInterrupt()) // interrupted = true; unparkSuccessor(h); return true; } return false; } // 本类重写的 tryRelease方法 protected final boolean tryRelease(int releases) { // 会将state的值减一 int c = getState() - releases; // 判断当前线程是否是该锁的拥有者,如果不是则抛出异常, if (Thread.currentThread() != getExclusiveOwnerThread()) throw new IllegalMonitorStateException(); boolean free = false; // 如果等于0 则表示,只有一层锁,直接解锁 if (c == 0) { free = true; setExclusiveOwnerThread(null); } // 设置state值,如果不为0就是设置state值-1 减少锁重入的计数,如果为0则设置为0;解锁。 setState(c); return free; }
-
-
设置setExclusiveOwnerThread为null 设置state为0;
-
当前队列不为null,并且head的waitStatus=-1,进入unparkSuccessor流程
-
找到队列中离head最近的一个node(没取消的)unpark恢复其运行,本例中即为Thread-1
-
回到Thread的acquireQueued流程
有竞争的释放锁
如果这时候有其他线程来竞争(非公平的体现),例如这时又Thjread-4 来了
- 在Thread-1别唤醒后,需要进入CAS获取锁的时候,没抢过Thread-4,就会再次进入阻塞
- Thread-4被设置为setExclusiveOwnerThread(Thread-4)state=1
- Thread-1再次进入acquireQueued流程,获取锁失败,重新进入park阻塞。
28.3.3 打断原理
不可打断模式
个人理解:
-
在此模式下,即使它被打断,仍会驻留在 AQS 队列中,一直要等到获得锁后方能得知自己被打断了(但是仍然不会被打断,只是知道自己被打断了)
-
以为要执行被打断需要满足两个条件(不可打断模式下)
-
public final void acquire(int arg) { // 要满足以下两个条件才会执行 selfInterrupt();方法 但是acquireQueued() 方法 if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) selfInterrupt(); } static void selfInterrupt() { Thread.currentThread().interrupt(); }
-
但是acquireQueued方法返回true的条件是if (p == head && tryAcquire(arg)) {} 也就是说tryAcquire(arg)为true,但是这个方法返回为true的情况是必须获得锁才会返回true,所以和能调用他的selfInterrupt();条件相斥,所以lock()不可打断模式永远不会被打断。
-
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); } }
-
学习到的解释:
-
在此模式下,即使它被打断,仍会驻留在 AQS 队列中,一直要等到获得锁后方能得知自己被打断了
-
// Sync 继承自 AQS static final class NonfairSync extends Sync { // ... private final boolean parkAndCheckInterrupt() { // 如果打断标记已经是 true, 则 park 会失效 LockSupport.park(this); // interrupted 会清除打断标记 return Thread.interrupted(); } 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; failed = false; // 还是需要获得锁后, 才能返回打断状态 return interrupted; } if ( shouldParkAfterFailedAcquire(p, node) && parkAndCheckInterrupt() ) { // 如果是因为 interrupt 被唤醒, 返回打断状态为 true interrupted = true; } } } finally { if (failed) cancelAcquire(node); } } public final void acquire(int arg) { if ( !tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg) ) { // 如果打断状态为 true selfInterrupt(); } } static void selfInterrupt() { // 重新产生一次中断 Thread.currentThread().interrupt(); } }
可打断模式
-
public void lockInterruptibly() throws InterruptedException { sync.acquireInterruptibly(1); }
-
public final void acquireInterruptibly(int arg) throws InterruptedException { if (Thread.interrupted()) throw new InterruptedException(); // 如果没有获得到锁, 进入 ㈠ if (!tryAcquire(arg)) doAcquireInterruptibly(arg); }
-
// ㈠ 可打断的获取锁流程 private void doAcquireInterruptibly(int arg) throws InterruptedException { final Node node = addWaiter(Node.EXCLUSIVE); boolean failed = true; try { for (;;) { final Node p = node.predecessor(); if (p == head && tryAcquire(arg)) { setHead(node); p.next = null; // help GC failed = false; return; } if (shouldParkAfterFailedAcquire(p, node) && parkAndCheckInterrupt()) // 在 park 过程中如果被 interrupt 会进入此 // 这时候抛出异常, 而不会再次进入 for (;;) // 如果被打断直接抛出异常 throw new InterruptedException(); } } finally { if (failed) cancelAcquire(node); } }
28.3.4 公平锁原理
-
static final class FairSync extends Sync { private static final long serialVersionUID = -3000897897090466540L; final void lock() { acquire(1); } // 从AQS继承过来的,方便阅读 public final void acquire(int arg) { if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) selfInterrupt(); } /** * Fair version of tryAcquire. Don't grant access unless * recursive call or no waiters or is first. */ protected final boolean tryAcquire(int acquires) { final Thread current = Thread.currentThread(); int c = getState(); if (c == 0) { if (!hasQueuedPredecessors() && // 与非公平锁的区别在于此处,公平锁要先去AQS队列中去检查是否线程,如果没有才去竞争锁, compareAndSetState(0, acquires)) { setExclusiveOwnerThread(current); return true; } } else if (current == getExclusiveOwnerThread()) { int nextc = c + acquires; if (nextc < 0) throw new Error("Maximum lock count exceeded"); setState(nextc); return true; } return false; } // public final boolean hasQueuedPredecessors() { // The correctness of this depends on head being initialized // before tail and on head.next being accurate if the current // thread is first in queue. Node t = tail; // Read fields in reverse initialization order Node h = head; Node s; return h != t && // 判断队列中有没有线程,和除了头节点的第一个节点的线程是不是当前线程或者头节点的next指针为空。 ((s = h.next) == null || s.thread != Thread.currentThread()); } }
28.3.5 条件变量原理
- 每个条件变量其实就对应着一个等待队列,其实现类是ConditionObject
await流程
public final void await() throws InterruptedException {
if (Thread.interrupted())
throw new InterruptedException();
Node node = addConditionWaiter();
int savedState = fullyRelease(node);
int interruptMode = 0;
while (!isOnSyncQueue(node)) {
LockSupport.park(this);
if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
break;
}
if (acquireQueued(node, savedState) && interruptMode != THROW_IE)
interruptMode = REINTERRUPT;
if (node.nextWaiter != null) // clean up if cancelled
unlinkCancelledWaiters();
if (interruptMode != 0)
reportInterruptAfterWait(interruptMode);
}
-
开始Thread-0持有锁,调用await,进入ConditionObject的addConditionWaaiter流程
-
private Node addConditionWaiter() { Node t = lastWaiter; // If lastWaiter is cancelled, clean out. if (t != null && t.waitStatus != Node.CONDITION) { unlinkCancelledWaiters(); t = lastWaiter; } Node node = new Node(Thread.currentThread(), Node.CONDITION); if (t == null) firstWaiter = node; else t.nextWaiter = node; lastWaiter = node; return node; }
-
创建新的Node状态为-2(Node.CONDITION),关联Thread-0,加入等待队列尾部
-
接下来进入AQS的fullRelease(因为可能会有锁重入,所以要将状态归0)流程,释放同步器上的所有锁
-
int savedState = fullyRelease(node);
-
-
unpark AQS队列中的下一个节点,竞争锁,假设没有其他竞争线程,那么Thread-1竞争成功。
-
park 阻塞Thread -0
-
while (!isOnSyncQueue(node)) { LockSupport.park(this); if ((interruptMode = checkInterruptWhileWaiting(node)) != 0) break; }
-
signal流程
假设Thread-1 要来唤醒Thread-0
-
public final void signal() { if (!isHeldExclusively()) // 判断是否是锁的持有者,如果不是则抛出异常。 throw new IllegalMonitorStateException(); Node first = firstWaiter; // 找到条件变量队列中的第一个节点。 if (first != null) doSignal(first);}
-
进入ConditionObject的doSignal流程,取得等待队列中第一个Node,即Thread-0所在Node
-
private void doSignal(Node first) { do { if ( (firstWaiter = first.nextWaiter) == null) lastWaiter = null; first.nextWaiter = null; // 将他从条件变量队列中断开 } while (!transferForSignal(first) && // 将节点转移到AQS等待队列 (first = firstWaiter) != null);}
- [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-C2SqRTSu-1631354371415)(C:\Users\Administrator\Desktop\整理\并发.assets\image-20210811201546343.png)]
-
执行transferForSignal()流程,将该Node加入到AQS队列尾部,将Thread-0的waitStatus改为0,Thread-3的waitStatus改为-1(transferForSignal()如果加入失败,就会找下一个节点,失败的原因就是有可能他被打断了,或者超时了,就是说这个线程自己放弃了对锁的竞争。)
-
Thread-1释放锁,进入unlock流程,略
28.4 ReetrantReadWriteLock
(读写锁)
当读操作远远高于写操作时,这时候使用读写锁让读-读可以并发,提高性能。
类似于数据库中的select…from…lock in share mode
提供一个数据容器类内部分别使用读锁保护数据read()方法,写锁保护数据的write()方法。
读-读
-
package com.sunyang.concurrentstudy;import lombok.extern.slf4j.Slf4j;import java.util.concurrent.TimeUnit;import java.util.concurrent.locks.ReentrantReadWriteLock;/** * @program: ConcurrentStudy * @description: Demo * @author: SunYang * @create: 2021-08-11 20:36 **/@Slf4j(topic = "c.Demo")public class ReentrantReadWriteLockDemo { public static void main(String[] args){ DataContainer dataContainer = new DataContainer(); new Thread(() -> { try { dataContainer.read(); } catch (InterruptedException e) { e.printStackTrace(); } }, "t1").start(); new Thread(() -> { try { dataContainer.read(); } catch (InterruptedException e) { e.printStackTrace(); } }, "t2").start(); }}@Slf4j(topic = "c.Demo")class DataContainer { // 要保护的共享数据 private Object data; // 创建读写锁对象 private ReentrantReadWriteLock lock = new ReentrantReadWriteLock(); private ReentrantReadWriteLock.ReadLock rLock = lock.readLock(); private ReentrantReadWriteLock.WriteLock wLock = lock.writeLock(); public Object read() throws InterruptedException { log.debug("获取读锁。。。。"); rLock.lock(); try { log.debug("读取"); TimeUnit.SECONDS.sleep(1); return data; } finally { log.debug("释放读锁。。。。"); rLock.unlock(); } } public void write(Object data){ log.debug("获取写锁。。。。"); wLock.lock(); try { log.debug("写入"); this.data = data; } finally { log.debug("释放写锁"); wLock.unlock(); } }}
-
20:48:38 [t2] c.Demo - 获取读锁。。。。20:48:38 [t1] c.Demo - 获取读锁。。。。20:48:38 [t2] c.Demo - 读取20:48:38 [t1] c.Demo - 读取20:48:39 [t1] c.Demo - 释放读锁。。。。20:48:39 [t2] c.Demo - 释放读锁。。。。
读-写
-
package com.sunyang.concurrentstudy;import lombok.extern.slf4j.Slf4j;import java.util.concurrent.TimeUnit;import java.util.concurrent.locks.ReentrantReadWriteLock;/** * @program: ConcurrentStudy * @description: Demo * @author: SunYang * @create: 2021-08-11 20:36 **/@Slf4j(topic = "c.Demo")public class ReentrantReadWriteLockDemo { public static void main(String[] args) throws InterruptedException { DataContainer dataContainer = new DataContainer(); new Thread(() -> { try { dataContainer.read(); } catch (InterruptedException e) { e.printStackTrace(); } }, "t1").start(); TimeUnit.MILLISECONDS.sleep(100); new Thread(() -> { // dataContainer.read(); dataContainer.write(10); }, "t2").start(); }}@Slf4j(topic = "c.Demo")class DataContainer { // 要保护的共享数据 private Object data; // 创建读写锁对象 private ReentrantReadWriteLock lock = new ReentrantReadWriteLock(); private ReentrantReadWriteLock.ReadLock rLock = lock.readLock(); private ReentrantReadWriteLock.WriteLock wLock = lock.writeLock(); public Object read() throws InterruptedException { log.debug("获取读锁。。。。"); rLock.lock(); try { log.debug("读取"); TimeUnit.SECONDS.sleep(1); return data; } finally { log.debug("释放读锁。。。。"); rLock.unlock(); } } public void write(Object data){ log.debug("获取写锁。。。。"); wLock.lock(); try { log.debug("获取到写锁。。。。"); log.debug("写入"); this.data = data; } finally { log.debug("释放写锁"); wLock.unlock(); } }}
-
20:54:40.801 [t1] c.Demo - 获取读锁。。。。20:54:40.812 [t1] c.Demo - 读取20:54:40.899 [t2] c.Demo - 获取写锁。。。。20:54:41.815 [t1] c.Demo - 释放读锁。。。。20:54:41.816 [t2] c.Demo - 获取到写锁。。。。20:54:41.816 [t2] c.Demo - 写入20:54:41.816 [t2] c.Demo - 释放写锁
写-写
-
package com.sunyang.concurrentstudy; import lombok.extern.slf4j.Slf4j; import java.util.concurrent.TimeUnit; import java.util.concurrent.locks.ReentrantReadWriteLock; /** * @program: ConcurrentStudy * @description: Demo * @author: SunYang * @create: 2021-08-11 20:36 **/ @Slf4j(topic = "c.Demo") public class ReentrantReadWriteLockDemo { public static void main(String[] args) throws InterruptedException { DataContainer dataContainer = new DataContainer(); new Thread(() -> { try { dataContainer.write(10); } catch (InterruptedException e) { e.printStackTrace(); } }, "t1").start(); TimeUnit.MILLISECONDS.sleep(100); new Thread(() -> { try { dataContainer.write(10); } catch (InterruptedException e) { e.printStackTrace(); } }, "t2").start(); } } @Slf4j(topic = "c.Demo") class DataContainer { // 要保护的共享数据 private Object data; // 创建读写锁对象 private ReentrantReadWriteLock lock = new ReentrantReadWriteLock(); private ReentrantReadWriteLock.ReadLock rLock = lock.readLock(); private ReentrantReadWriteLock.WriteLock wLock = lock.writeLock(); public Object read() throws InterruptedException { log.debug("获取读锁。。。。"); rLock.lock(); try { log.debug("读取"); TimeUnit.SECONDS.sleep(1); return data; } finally { log.debug("释放读锁。。。。"); rLock.unlock(); } } public void write(Object data) throws InterruptedException { log.debug("获取写锁。。。。"); wLock.lock(); try { log.debug("获取到写锁。。。。"); log.debug("写入"); TimeUnit.SECONDS.sleep(1); this.data = data; } finally { log.debug("释放写锁"); wLock.unlock(); } } }
-
20:56:49.073 [t1] c.Demo - 获取写锁。。。。 20:56:49.079 [t1] c.Demo - 获取到写锁。。。。 20:56:49.079 [t1] c.Demo - 写入 20:56:49.184 [t2] c.Demo - 获取写锁。。。。 20:56:50.091 [t1] c.Demo - 释放写锁 20:56:50.092 [t2] c.Demo - 获取到写锁。。。。 20:56:50.092 [t2] c.Demo - 写入 20:56:51.109 [t2] c.Demo - 释放写锁
注意事项
- 读锁不支持条件变量
- 重入时不支持升级,即持有读锁的情况下去获取写锁,会导致获取写锁失败,会导致获取写锁永久等待
- 重入时支持降级:即持有写锁的情况下去获取读锁。
应用之缓存
28.4.1 读写锁原理
读写锁用的时同一个Sync同步器,因此等待队列,state等也是同一个
t1 w.lock, t2 r.lock
- t1成功上锁,流程与ReentrantLock加锁相比没有特殊之处,不同是写锁状态占了state的低16位,而读锁使用的是state的高16位
28.4.2 写锁加锁原理
t1.w.lock
-
public void lock() { sync.acquire(1); }
-
public final void acquire(int arg) { if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) selfInterrupt(); }
-
protected final boolean tryAcquire(int acquires) { /* * Walkthrough: * 1. 如果读取计数非零或写入计数非零且所有者是不同的线程,则失败。 * 2. 如果计数会饱和,则失败。 (这只会在 count 已经非零时发生。) * 3. 否则,如果该线程是可重入获取或队列策略允许,则该线程有资格获得锁定。如果是,请更新状态并设 * 置所有者。 */ Thread current = Thread.currentThread(); int c = getState(); int w = exclusiveCount(c); if (c != 0) { // (Note: if c != 0 and w == 0 then shared count != 0) // 因为c != 0 才能进入到此,所以 c!=0 但是 w==0 代表别人加了读锁。所以直接返回false 加锁失败 如果w != 0 说明加了写锁,然后在判断是不是自己线程加的写锁,如果是则进行重入计数,如果不是返回false。 if (w == 0 || current != getExclusiveOwnerThread()) return false; if (w + exclusiveCount(acquires) > MAX_COUNT) throw new Error("Maximum lock count exceeded"); // Reentrant acquire setState(c + acquires); return true; } // 非公平锁writerShouldBlock一直返回false if (writerShouldBlock() || !compareAndSetState(c, c + acquires)) return false; // 如果没成功 则返回false说明在此期间已经被加锁了。 setExclusiveOwnerThread(current); // 如果成功了,则设置拥有者为当前线程 return true; }
28.4.3 读锁加锁原理
t2.r.lock
-
t2线程执行r.lock(),这时进入读锁的sync.acquireShared(1)流程,首先会进入tryAcquireShared流程,如果有写锁占据并且不是本线程所占据的写锁,那么tryAcquireShared返回-1表示失败(本例返回失败)。如果是本线程占据的写锁,则会继续获取读锁。
-
tryAcquireShared返回值表示
- -1 表示失败
- 0 表示成功,但后继节点不会继续唤醒(信号量时用,这里用不到,只会返回-1或者正数)
- 正数表示成功,而且数值是还有几个后续节点需要唤醒,读写锁返回1
-
public void lock() { sync.acquireShared(1); }
-
public final void acquireShared(int arg) { if (tryAcquireShared(arg) < 0) doAcquireShared(arg); }
-
protected final int tryAcquireShared(int unused) { /* * Walkthrough: * 1. 如果另一个线程持有写锁,则失败。 * 2. 否则,该线程有资格获得锁写入状态,因此询问它是否应该因为队列策略而阻塞。如果没有,请尝试通过 * CASing 状态和更新计数来授予。请注意,步骤不检查可重入获取,它被推迟到完整版本以避免在更典型的非 * 可重入情况下检查保持计数。 * 3. 如果第 2 步由于线程显然不符合条件或 CAS 失败或计数饱和而失败,则链接到具有完整重试循环的版 * 本。 */ Thread current = Thread.currentThread(); int c = getState(); if (exclusiveCount(c) != 0 && // 判断写锁是否为 0 如果不为0 判断是否是当前线程持有的写锁,不是则返回-1,是则继续向下运行 getExclusiveOwnerThread() != current) return -1; int r = sharedCount(c); if (!readerShouldBlock() && r < MAX_COUNT && compareAndSetState(c, c + SHARED_UNIT)) { if (r == 0) { firstReader = current; firstReaderHoldCount = 1; } else if (firstReader == current) { firstReaderHoldCount++; } else { HoldCounter rh = cachedHoldCounter; if (rh == null || rh.tid != getThreadId(current)) cachedHoldCounter = rh = readHolds.get(); else if (rh.count == 0) readHolds.set(rh); rh.count++; } return 1; } return fullTryAcquireShared(current); }
-
这时会进入sync.doAcquireShared(1)流程,首先也是调用addWaiter添加节点,不同之处在于节点被设置为Node.SHARED(共享)模式而非EXCLUSIVE(独占)模式,注意此时t2仍处于活跃状态。
-
public final void acquireShared(int arg) { if (tryAcquireShared(arg) < 0) doAcquireShared(arg); }
-
private void doAcquireShared(int arg) { // 代码意思同上 final Node node = addWaiter(Node.SHARED); boolean failed = true; try { boolean interrupted = false; for (;;) { final Node p = node.predecessor(); if (p == head) { int r = tryAcquireShared(arg); if (r >= 0) { setHeadAndPropagate(node, r); p.next = null; // help GC if (interrupted) selfInterrupt(); failed = false; return; } } if (shouldParkAfterFailedAcquire(p, node) && parkAndCheckInterrupt()) interrupted = true; } } finally { if (failed) cancelAcquire(node); } }
-
-
t2线程会看自己的节点是不是老二,如果是,还会再次调用tryAcquireShared(1)来尝试获取锁。
-
如果没成功,在doAcquireShared内for(;;)循环一次,把前驱节点的waitStatus改为-1,再for(;;)循环一次尝试tryAcquireShared(1)如果还不成功,那么再parkAndCheckInterrupt()处park
28.4.4 读写锁解锁原理
t3 r.lock t4 w.lock
这种状态下,假设又有t3加读锁,t4加写锁,这期间t1仍然持有锁,就会变成下面这样
28.4.5 写锁解锁流程
t1.w.unlock
-
这时或走到写锁的sync.release(1)流程,调用sync.tryRelease(1)成功 变成下面这个样子
-
public void unlock() { sync.release(1);}
-
public final boolean release(int arg) { if (tryRelease(arg)) { Node h = head; if (h != null && h.waitStatus != 0) // 执行完tryRelease后查看头节点是否为空及头节点状态是否不等于0, 然后进行唤醒后继节点流程 unparkSuccessor(h); return true; } return false;}
-
protected final boolean tryRelease(int releases) { if (!isHeldExclusively()) throw new IllegalMonitorStateException(); int nextc = getState() - releases; boolean free = exclusiveCount(nextc) == 0; // 查看解锁部分是否为0 if (free) setExclusiveOwnerThread(null); setState(nextc); return free; }
解锁后唤醒t2,t3
-
接下来执行唤醒流程sync.unparkSuccessor, 即让老二恢复运行,这时t2在doAcquireShared内parkAndCheckInterrupt() 处恢复运行。
-
这回再来一次for(;;)执行tryAcquireShared成功则让读锁计数加一
-
private void doAcquireShared(int arg) { final Node node = addWaiter(Node.SHARED); boolean failed = true; try { boolean interrupted = false; for (;;) { final Node p = node.predecessor(); if (p == head) { int r = tryAcquireShared(arg); if (r >= 0) { setHeadAndPropagate(node, r); p.next = null; // help GC if (interrupted) selfInterrupt(); failed = false; return; } } if (shouldParkAfterFailedAcquire(p, node) && parkAndCheckInterrupt()) // 在此处被唤醒后执行for(;;) 中的tryAcquireShared(arg);去再次抢锁 interrupted = true; } } finally { if (failed) cancelAcquire(node); } }
-
protected final int tryAcquireShared(int unused) { Thread current = Thread.currentThread(); int c = getState(); if (exclusiveCount(c) != 0 && getExclusiveOwnerThread() != current) return -1; int r = sharedCount(c); if (!readerShouldBlock() && r < MAX_COUNT && compareAndSetState(c, c + SHARED_UNIT)) { // 加锁,此例为加锁成功,高位加一,因为是读锁。 if (r == 0) { firstReader = current; firstReaderHoldCount = 1; } else if (firstReader == current) { firstReaderHoldCount++; } else { HoldCounter rh = cachedHoldCounter; if (rh == null || rh.tid != getThreadId(current)) cachedHoldCounter = rh = readHolds.get(); else if (rh.count == 0) readHolds.set(rh); rh.count++; } return 1; } return fullTryAcquireShared(current); }
-
-
这时t2已经恢复运行,接下来t2调用 setHeadAndPropagate(node, 1); 然后将他原本所在的节点置为头节点
-
然后,在setHeadAndPropagate方法内还会检查下一个节点是否是shared,如果是则调用doReleaseShared()将head的状态从-1改为0并唤醒老二,这时t3在doAcquireShraed内parkAndCheckInterrupt()处运行。
-
private void setHeadAndPropagate(Node node, int propagate) { Node h = head; // Record old head for check below setHead(node); if (propagate > 0 || h == null || h.waitStatus < 0 || (h = head) == null || h.waitStatus < 0) { Node s = node.next; // 当前节点的下一个 if (s == null || s.isShared()) // 判断是不是空:如果是空,将头结点的waitStatus设为0,如果不是空,则判断下一个节点是不是共享,是的话,唤醒她。 doReleaseShared(); } }
-
private void doReleaseShared() { for (;;) { Node h = head; if (h != null && h != tail) { int ws = h.waitStatus; if (ws == Node.SIGNAL) { // 将头结点的状态从-1 改为0,防止其他线程重复唤醒。 if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0)) continue; // loop to recheck cases unparkSuccessor(h); // 对头结点的后继节点唤醒。 } else if (ws == 0 && !compareAndSetWaitStatus(h, 0, Node.PROPAGATE)) continue; // loop on failed CAS } if (h == head) // loop if head changed break; } }
-
t3被唤醒后再来一次for(;;)执行tryAcquireShared成功则让读锁计数加一。
-
这时t3已经恢复运行,接下来t3调用setHeadAndPropagate(node, 1), 他原本所在节点被设置为头节点,重复上述步骤
-
判断下一个节点不是shared了,因此不会唤醒t4所在节点。
28.4.6 读锁解锁原理
t2.r.unlock t3.r.lock
-
t2进入unlock(),调用sync.releaseShared(1)中,调用tryReleaseShared(1)让计数减一,但由于计数还不为零。
-
public void unlock() { sync.releaseShared(1);}
-
public final boolean releaseShared(int arg) { if (tryReleaseShared(arg)) { doReleaseShared(); return true; } return false;}
-
protected final boolean tryReleaseShared(int unused) { Thread current = Thread.currentThread(); if (firstReader == current) { // assert firstReaderHoldCount > 0; if (firstReaderHoldCount == 1) firstReader = null; else firstReaderHoldCount--; } else { HoldCounter rh = cachedHoldCounter; if (rh == null || rh.tid != getThreadId(current)) rh = readHolds.get(); int count = rh.count; if (count <= 1) { readHolds.remove(); if (count <= 0) throw unmatchedUnlockException(); } --rh.count; } for (;;) { int c = getState(); int nextc = c - SHARED_UNIT; if (compareAndSetState(c, nextc)) return nextc == 0; }}
-
private void doReleaseShared() { for (;;) { Node h = head; if (h != null && h != tail) { int ws = h.waitStatus; if (ws == Node.SIGNAL) { if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0)) continue; // loop to recheck cases unparkSuccessor(h); } else if (ws == 0 && !compareAndSetWaitStatus(h, 0, Node.PROPAGATE)) continue; // loop on failed CAS } if (h == head) // loop if head changed break; } }
-
t3 进入sync.releaseShared(1)中,调用tryReleaseShraed(1)让计数减一,这回计数为0,进入doReleaseShared()将头节点从-1改为0并唤醒老二,即
-
之后t4在acquireQueued中parkAndCheckInterrupt处恢复运行,再次for(;;)这次自己是老二,并且没有其他竞争,tryAcquire(1)成功,修改头节点,流程结束。
28.5 StampedLock
28.5.1 概览
-
该类自JDK 8 加入,是为了进一步优化读性能,他的特点是在使用读锁,写锁时都必须配合**【戳】**使用
-
加解读锁
-
long stamp = lock.readLock(); lock.unlockRead(stamp);
-
-
加解写锁
-
long stamp = lock.writeLock(); lock.unlockWrite(stamp);
-
-
乐观读,StampedLock支持tryOptimisticRead() 方法(乐观读),这个乐观读并没有加锁,只有在验戳的时候发现戳已经被改变了,才会进行锁升级,将其升级为读锁。
-
读取完毕后许哟啊做一次**【戳校验】**,如果校验通过,表示这期间确实没有写操作,数据可以安全使用,如果校验没通过,重新获取读锁,保证数据安全、
-
long stamp = lock.tryOptimisticRead(); // 验戳 if (!ck.validate(stamp)){ // 锁升级 }
-
28.5.2 代码示例
读-读
-
package com.sunyang.concurrentstudy; import lombok.extern.slf4j.Slf4j; import java.util.concurrent.TimeUnit; import java.util.concurrent.locks.StampedLock; /** * @Author: sunyang * @Date: 2021/8/12 * @Description: */ @Slf4j(topic = "c.Demo") public class StampedLockDemo { public static void main(String[] args) { DataContainerStamped dataContainerStamped = new DataContainerStamped(1); new Thread(() -> { try { dataContainerStamped.read(1); } catch (InterruptedException e) { e.printStackTrace(); } }, "t1").start(); new Thread(() -> { try { dataContainerStamped.read(0); } catch (InterruptedException e) { e.printStackTrace(); } }, "t2").start(); } } // 定义一个数据容器 保护共享数据 @Slf4j(topic = "c.Demo") class DataContainerStamped { private int data; private final StampedLock lock = new StampedLock(); public DataContainerStamped(int data) { this.data = data; } // 参数测试用,用来传表示读取花了多久的时间,自己定义传过来 public int read(int readTime) throws InterruptedException { long stamp = lock.tryOptimisticRead(); log.debug("optimistic read locking....{}", stamp); TimeUnit.SECONDS.sleep(readTime); if (lock.validate(stamp)) { log.debug("read finish....{}", stamp); return data; } log.debug("验戳失败,锁升级。。。。。"); try { stamp =lock.readLock(); log.debug("read lock {}", stamp); TimeUnit.SECONDS.sleep(readTime); log.debug("read finish.....{}", stamp); return data; } finally { log.debug("read unlock ......{}", stamp); lock.unlockRead(stamp); } } public void write(int data) throws InterruptedException { long stamp = lock.writeLock(); log.debug("write lock {}", stamp); try { TimeUnit.SECONDS.sleep(2); this.data = data; } finally { log.debug("write unlock {}", stamp); lock.unlockWrite(stamp); } } }
-
15:31:04.527 [t1] c.Demo - optimistic read locking....256 15:31:04.527 [t2] c.Demo - optimistic read locking....256 15:31:04.531 [t2] c.Demo - read finish....256 15:31:05.540 [t1] c.Demo - read finish....256
读-写
-
package com.sunyang.concurrentstudy; import lombok.extern.slf4j.Slf4j; import java.util.concurrent.TimeUnit; import java.util.concurrent.locks.StampedLock; /** * @Author: sunyang * @Date: 2021/8/12 * @Description: */ @Slf4j(topic = "c.Demo") public class StampedLockDemo { public static void main(String[] args) throws InterruptedException { DataContainerStamped dataContainerStamped = new DataContainerStamped(1); new Thread(() -> { try { dataContainerStamped.read(1); } catch (InterruptedException e) { e.printStackTrace(); } }, "t1").start(); TimeUnit.SECONDS.sleep(1); new Thread(() -> { try { dataContainerStamped.write(110); } catch (InterruptedException e) { e.printStackTrace(); } }, "t2").start(); } } // 定义一个数据容器 保护共享数据 @Slf4j(topic = "c.Demo") class DataContainerStamped { private int data; private final StampedLock lock = new StampedLock(); public DataContainerStamped(int data) { this.data = data; } // 参数测试用,用来传表示读取花了多久的时间,自己定义传过来 public int read(int readTime) throws InterruptedException { long stamp = lock.tryOptimisticRead(); log.debug("optimistic read locking....{}", stamp); TimeUnit.SECONDS.sleep(readTime); if (lock.validate(stamp)) { log.debug("read finish....{}", stamp); return data; } log.debug("验戳失败,锁升级。。。。。"); try { stamp =lock.readLock(); log.debug("read lock {}", stamp); TimeUnit.SECONDS.sleep(readTime); log.debug("read finish.....{}", stamp); return data; } finally { log.debug("read unlock ......{}", stamp); lock.unlockRead(stamp); } } public void write(int data) throws InterruptedException { long stamp = lock.writeLock(); log.debug("write lock {}", stamp); try { TimeUnit.SECONDS.sleep(2); this.data = data; } finally { log.debug("write unlock {}", stamp); lock.unlockWrite(stamp); } } }
-
15:33:56.424 [t1] c.Demo - optimistic read locking....256 15:33:57.424 [t2] c.Demo - write lock 384 15:33:57.439 [t1] c.Demo - 验戳失败,锁升级。。。。。 15:33:59.427 [t2] c.Demo - write unlock 384 15:33:59.427 [t1] c.Demo - read lock 513 15:34:00.440 [t1] c.Demo - read finish.....513 15:34:00.440 [t1] c.Demo - read unlock ......513
28.5.3 缺点
- 不支持条件变量
- 不支持可重入
28.6 Semaphore(信号量)
- 信号量,用来限制能同时访问共享资源的线程上限,允许多个线程同时访问,但是限制数量。
- 类比停车场,停车场的停车位数量是固定的,并且停车场门口会有指示牌告诉你还有多少可用停车位,如果满了,则不可以进入,如果满了,有别的车走了,就+1,然后就可以进车了。
28.6.1 代码示例
-
package com.sunyang.concurrentstudy; import lombok.extern.slf4j.Slf4j; import java.util.concurrent.Semaphore; import java.util.concurrent.TimeUnit; /** * @Author: sunyang * @Date: 2021/8/12 * @Description: */ @Slf4j(topic = "c.Demo") public class SemaphoreDemo { public static void main(String[] args) { // 参数1: 许可量:同一时间允许有多少线程多共享资源进行访问, // 参数2: 公平 非公平 同其他锁的公平非公平理解。 Semaphore semaphore = new Semaphore(3); // 假设10个线程同时运行 for (int i = 0; i < 10; i++) { new Thread(() -> { // 要先获得一个许可,也就是停车位,如果停车位有余位。 try { semaphore.acquire(); } catch (InterruptedException e) { e.printStackTrace(); } log.debug("running....."); try { TimeUnit.SECONDS.sleep(1); log.debug("end....."); } catch (InterruptedException e) { e.printStackTrace(); } finally { semaphore.release(); } }).start(); } } }
-
16:02:30.151 [Thread-0] c.Demo - running..... 16:02:30.151 [Thread-2] c.Demo - running..... 16:02:30.151 [Thread-1] c.Demo - running..... 16:02:31.158 [Thread-0] c.Demo - end..... 16:02:31.158 [Thread-1] c.Demo - end..... 16:02:31.158 [Thread-2] c.Demo - end..... 16:02:31.158 [Thread-3] c.Demo - running..... 16:02:31.158 [Thread-5] c.Demo - running..... 16:02:31.158 [Thread-4] c.Demo - running..... 16:02:32.162 [Thread-5] c.Demo - end..... 16:02:32.162 [Thread-4] c.Demo - end..... 16:02:32.162 [Thread-3] c.Demo - end..... 16:02:32.162 [Thread-6] c.Demo - running..... 16:02:32.162 [Thread-7] c.Demo - running..... 16:02:32.162 [Thread-8] c.Demo - running..... 16:02:33.175 [Thread-6] c.Demo - end..... 16:02:33.175 [Thread-8] c.Demo - end..... 16:02:33.175 [Thread-7] c.Demo - end..... 16:02:33.175 [Thread-9] c.Demo - running..... 16:02:34.175 [Thread-9] c.Demo - end.....
28.6.2 Semaphore原理
加锁流程
-
Semaphore 有点像一个停车场,permits就好像停车位数量,当线程获得了permits就像是获得了停车位,然后停车场显示空余车位减一
-
刚开始,permits(state)为3,这时5个线程来获取资源。
-
-
public Semaphore(int permits) { sync = new NonfairSync(permits); }
-
static final class NonfairSync extends Sync { private static final long serialVersionUID = -2694183684443567898L; NonfairSync(int permits) { super(permits); } protected int tryAcquireShared(int acquires) { return nonfairTryAcquireShared(acquires); } }
-
abstract static class Sync extends AbstractQueuedSynchronizer { private static final long serialVersionUID = 1192457210091910933L; Sync(int permits) { setState(permits); } }
-
-
假设其中Thread-1,Thread-2,Thread-4 CAS竞争成功,而Thread-0和Thread-3竞争失败,进入AQS队列park阻塞
-
Thread-1要获取资源会调用acquire() Thread-2 和3流程相同。
-
public void acquire() throws InterruptedException { sync.acquireSharedInterruptibly(1); }
-
public final void acquireSharedInterruptibly(int arg) throws InterruptedException { if (Thread.interrupted()) throw new InterruptedException(); if (tryAcquireShared(arg) < 0) doAcquireSharedInterruptibly(arg); }
-
protected int tryAcquireShared(int acquires) { return nonfairTryAcquireShared(acquires); }
-
final int nonfairTryAcquireShared(int acquires) { for (;;) { int available = getState(); int remaining = available - acquires; if (remaining < 0 || compareAndSetState(available, remaining)) return remaining; } }
-
Thread-0执行过程 其他同上,
-
final int nonfairTryAcquireShared(int acquires) { for (;;) { int available = getState(); // 到这一步, 0 - 1 = -1 int remaining = available - acquires; // remaining 小于0 if (remaining < 0 || compareAndSetState(available, remaining)) // 直接返回-1 return remaining; } }
-
public final void acquireSharedInterruptibly(int arg) throws InterruptedException { if (Thread.interrupted()) throw new InterruptedException(); if (tryAcquireShared(arg) < 0) // 然后此条件成立 doAcquireSharedInterruptibly(arg); // 进入此方法 }
-
private void doAcquireSharedInterruptibly(int arg) throws InterruptedException { final Node node = addWaiter(Node.SHARED); // 加到等待队列,并且上设置为共享 boolean failed = true; try { for (;;) { final Node p = node.predecessor(); // 判断此节点是不是老二 if (p == head) { int r = tryAcquireShared(arg); // 如果是老二,再次尝试获取信号量流程 if (r >= 0) { // 说明获取成功 setHeadAndPropagate(node, r);// 此方法用来将此节点出队(等待队列),并查看下一个节点是否为空,如果为空,将其waitStatus置为0,如果不为空,则判断其是否为共享节点,如果是,则将头节点waitStatus置为0,防止重复唤醒,然后再唤醒她,如果不是则什么不做。 p.next = null; // help GC failed = false; return; } } // 如果再次尝试失败,则走此方法,第一次走shouldParkAfterFailedAcquire(p, node)会将头节点waitStatus置为-1 并返回false,第二次会返回true if (shouldParkAfterFailedAcquire(p, node) && // 然后走此方法,进入park。 parkAndCheckInterrupt()) throw new InterruptedException(); } } finally { if (failed) cancelAcquire(node); } }
-
解锁流程
-
这时Thread-4释放了permits,状态如下
-
public void release() { sync.releaseShared(1);}
-
public final boolean releaseShared(int arg) { if (tryReleaseShared(arg)) { doReleaseShared(); return true; } return false;}
-
protected final boolean tryReleaseShared(int releases) { for (;;) { int current = getState(); // 拿到state 这里是0 int next = current + releases; // 将state加1, 这里等于1 if (next < current) // overflow // 判断加的release是否为负 throw new Error("Maximum permit count exceeded"); if (compareAndSetState(current, next)) // CAS 释放锁 return true; }}
-
private void doReleaseShared() { for (;;) { Node h = head; if (h != null && h != tail) { int ws = h.waitStatus; if (ws == Node.SIGNAL) { // 判断头节点的waitStatus状态是否为-1,如果是则有需要唤醒的后继节点 if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0)) // 将waitStatus置为0, continue; // loop to recheck cases // 唤醒后继节点 unparkSuccessor(h); } else if (ws == 0 && !compareAndSetWaitStatus(h, 0, Node.PROPAGATE)) continue; // loop on failed CAS } if (h == head) // loop if head changed break; }}
-
private void doAcquireInterruptibly(int arg) throws InterruptedException { final Node node = addWaiter(Node.EXCLUSIVE); boolean failed = true; try { for (;;) { final Node p = node.predecessor(); if (p == head && tryAcquire(arg)) { setHead(node); p.next = null; // help GC failed = false; return; } if (shouldParkAfterFailedAcquire(p, node) && parkAndCheckInterrupt()) // 唤醒在此处park的节点 throw new InterruptedException(); } } finally { if (failed) cancelAcquire(node); }}
-
接下来Thread-0竞争成功,permits再次设置为0,设置自己为head节点,断开原来的head节点,unpark接下来的Thread-3节点,但由于permits是0,因此Thread-3在尝试不成功后再次进入park状态阻塞。
28.7 CountDownLatch
倒计时锁
用来进行线程同步协作,等待所有线程完成倒计时
其中构造参数用来初始化等待计数值,await()用来等待计数归零,countDown() 用来让计数减一
源码:
-
private static final class Sync extends AbstractQueuedSynchronizer { private static final long serialVersionUID = 4982264981922014374L; Sync(int count) { setState(count); } int getCount() { return getState(); } protected int tryAcquireShared(int acquires) { return (getState() == 0) ? 1 : -1; } protected boolean tryReleaseShared(int releases) { // Decrement count; signal when transition to zero for (;;) { int c = getState(); if (c == 0) return false; int nextc = c-1; if (compareAndSetState(c, nextc)) return nextc == 0; } }}
28.7.1 配合普通线程代码示例
-
package com.sunyang.concurrentstudy;import lombok.extern.slf4j.Slf4j;import java.util.concurrent.CountDownLatch;import java.util.concurrent.TimeUnit;/** * @program: ConcurrentStudy * @description: Demo * @author: SunYang * @create: 2021-08-12 19:57 **/@Slf4j(topic = "c.Demo")public class CountdownLatchDemo { public static void main(String[] args) throws InterruptedException { CountDownLatch latch = new CountDownLatch(3); new Thread(() -> { log.debug("begin...."); try { TimeUnit.SECONDS.sleep(1); log.debug("end...."); latch.countDown(); } catch (InterruptedException e) { e.printStackTrace(); } }, "t1").start(); new Thread(() -> { log.debug("begin...."); try { TimeUnit.SECONDS.sleep(2); log.debug("end...."); latch.countDown(); } catch (InterruptedException e) { e.printStackTrace(); } }, "t2").start(); new Thread(() -> { log.debug("begin...."); try { TimeUnit.SECONDS.sleep(3); log.debug("end...."); latch.countDown(); } catch (InterruptedException e) { e.printStackTrace(); } }, "t3").start(); log.debug("main waiting...."); latch.await(); log.debug("main ending..."); }}
-
20:06:55.259 [t2] c.Demo - begin....20:06:55.260 [main] c.Demo - main waiting....20:06:55.259 [t1] c.Demo - begin....20:06:55.260 [t3] c.Demo - begin....20:06:56.285 [t1] c.Demo - end....20:06:57.274 [t2] c.Demo - end....20:06:58.288 [t3] c.Demo - end....20:06:58.290 [main] c.Demo - main ending...
28.7.2 配合线程池
-
package com.sunyang.concurrentstudy; import lombok.extern.slf4j.Slf4j; import java.util.concurrent.CountDownLatch; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.TimeUnit; /** * @program: ConcurrentStudy * @description: Demo * @author: SunYang * @create: 2021-08-12 19:57 **/ @Slf4j(topic = "c.Demo") public class CountdownLatchDemo { public static void main(String[] args) throws InterruptedException { CountDownLatch latch = new CountDownLatch(3); ExecutorService pool = Executors.newFixedThreadPool(4); pool.submit(() -> { try { log.debug("begin...."); TimeUnit.SECONDS.sleep(1); latch.countDown(); log.debug("end.....{}", latch.getCount()); } catch (InterruptedException e) { e.printStackTrace(); } }); pool.submit(() -> { try { log.debug("begin...."); TimeUnit.SECONDS.sleep(2); latch.countDown(); log.debug("end.....{}", latch.getCount()); } catch (InterruptedException e) { e.printStackTrace(); } }); pool.submit(() -> { try { log.debug("begin...."); TimeUnit.SECONDS.sleep(3); latch.countDown(); log.debug("end.....{}", latch.getCount()); } catch (InterruptedException e) { e.printStackTrace(); } }); pool.submit(() -> { try { log.debug("waiting...."); TimeUnit.SECONDS.sleep(1); latch.await(); log.debug("wait end....."); } catch (InterruptedException e) { e.printStackTrace(); } }); } }
-
20:18:05.821 [pool-1-thread-2] c.Demo - begin.... 20:18:05.821 [pool-1-thread-3] c.Demo - begin.... 20:18:05.821 [pool-1-thread-1] c.Demo - begin.... 20:18:05.822 [pool-1-thread-4] c.Demo - waiting.... 20:18:06.838 [pool-1-thread-1] c.Demo - end.....2 20:18:07.834 [pool-1-thread-2] c.Demo - end.....1 20:18:08.856 [pool-1-thread-3] c.Demo - end.....0 20:18:08.853 [pool-1-thread-4] c.Demo - wait end.....
28.7.3 简单模拟王者
-
package com.sunyang.concurrentstudy; import lombok.extern.slf4j.Slf4j; import java.util.Arrays; import java.util.Random; import java.util.concurrent.CountDownLatch; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.TimeUnit; /** * @program: ConcurrentStudy * @description: Demo * @author: SunYang * @create: 2021-08-12 19:57 **/ @Slf4j(topic = "c.Demo") public class CountdownLatchDemo { public static void main(String[] args) throws InterruptedException { CountDownLatch latch = new CountDownLatch(10); ExecutorService service = Executors.newFixedThreadPool(10); Random random = new Random(); String[] all = new String[10]; for (int j = 0; j < 10; j++) { int finalJ = j; service.submit(() -> { for (int i = 0; i <= 100; i++) { try { TimeUnit.MILLISECONDS.sleep(random.nextInt(100)); } catch (InterruptedException e) { e.printStackTrace(); } all[finalJ] = i + "%"; System.out.print("\r" + Arrays.toString(all)); } latch.countDown(); }); } service.shutdown(); latch.await(); System.out.println("\n" + "游戏开始。。。"); } }
-
[100%, 100%, 100%, 100%, 100%, 100%, 100%, 100%, 100%, 100%] 游戏开始。。。
28.8 CyclicBarrier
循环栅栏,用来进行线程协作,等待线程满足某个计数,构建时设置【计数个数】,每个线程执行到某个需要同步的时刻调用await() 方法进行等待,当等待的线程数满足计数个数时,继续执行。并且这个计数可以重用,但是CountDownLatch不可重用。
28.8.1 使用示例
注意:线程数要和计数数相同,否则会出现问题。
-
package com.sunyang.concurrentstudy; import lombok.extern.slf4j.Slf4j; import java.util.concurrent.*; /** * @program: ConcurrentStudy * @description: Demo * @author: SunYang * @create: 2021-08-12 21:04 **/ @Slf4j(topic = "c.Demo") public class CyclicBarrierDemo { public static void main(String[] args) { ExecutorService service = Executors.newFixedThreadPool(2); CyclicBarrier barrier = new CyclicBarrier(2, () -> { log.debug("task1 task2 finish...."); }); for (int i = 0; i < 3; i++) { service.submit(() -> { try { log.debug("task1 begin..."); TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); } try { log.debug("task1 end..."); barrier.await(); // 2 - 1 = 1 } catch (InterruptedException | BrokenBarrierException e) { e.printStackTrace(); } }); service.submit(() -> { try { log.debug("task2 begin..."); TimeUnit.SECONDS.sleep(2); } catch (InterruptedException e) { e.printStackTrace(); } try { log.debug("task2 end..."); barrier.await(); // 1- 1 = 0 两个线程开始运行。 } catch (InterruptedException | BrokenBarrierException e) { e.printStackTrace(); } }); } service.shutdown(); } }
28.9 线程集合类概述
线程安全集合类可以分为三大类:
-
遗留的线程安全集合如:Hashtable, Vector
-
使用Collections装饰的线程安全集合,如:
- Collections.synchronizedCollection
- Collections.synchronizedList
- Collections.synchronizedMap
- Collections.synchronizedSet
- Collections.synchronizedNavigableMap
- Collections.synchronizedNavigableSet
- Collections.synchronizedSortedMap
- Collections.synchronizedSortedSet
-
java.util.concurrent.* (J.U.C下)
重点:java.util.concurrent.*下的线程安全集合类,可以发现他们有规律,里面包含三类关键词:
- Blocking 大部分实现基于锁,并提供用来阻塞的方法(典型例子,阻塞队列-BlockingQueue)通常用的锁是ReentrantLock。
- CopyOnWrite之类容器是利用修改时拷贝的方式来保证多线程并发时的线程安全问题,适用于读多写少的场景,因为修改开销较重,
- Concurrent类型的容器
- 内部很多操作使用CAS优化,并且会采用多把锁,保证更高的并发度和吞吐量
- 弱一致性问题
- 遍历时弱一致性,例如,当利用迭代器遍历时,如果容器发生修改,迭代器仍然可以继续进行遍历,这时内容是旧的。(fail-safe)
- 求大小弱一致性,size操作未必100%准确
- 读取弱一致性
- 其他:
- 对于非安全容器来讲,遍历时如果发生了修改, 使用fail-fast机制也就是让遍历立刻失败,抛出ConcurrentModificationException,不再继续遍历。
额外扩展
- fail-safe(安全失败)
- fail-fast(快速失败)list遍历里面 add
28.10 ConcurrentHashMap
28.10.1 练习:单词计数
-
package com.sunyang.concurrentstudy; import lombok.extern.slf4j.Slf4j; import java.io.BufferedReader; import java.io.FileInputStream; import java.io.IOException; import java.io.InputStreamReader; import java.util.ArrayList; import java.util.List; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.atomic.LongAdder; import java.util.function.BiConsumer; import java.util.function.Supplier; /** * @Author: sunyang * @Date: 2021/8/13 * @Description: */ @Slf4j(topic = "c.Demo") public class ConcurrentHashMapWordCount { public static void main(String[] args) { demo(() -> new ConcurrentHashMap<String, LongAdder>(), (map, words) -> { for (String word : words) { // 如果缺少一个key,则计算一个值value,然后将key value 放入map LongAdder value = map.computeIfAbsent(word, (key) -> new LongAdder()); // 执行累加 value.increment(); // Integer counter = map.get(word); // int newValue = counter == null ? 1 : counter + 1; // map.put(word, newValue); } } ); } private static <V> void demo(Supplier<Map<String,V>> supplier, BiConsumer<Map<String,V>,List<String>> consumer) { Map<String, V> counterMap = supplier.get(); List<Thread> ts = new ArrayList<>(); for (int i = 1; i <= 26; i++) { int idx = i; Thread thread = new Thread(() -> { List<String> words = readFromFile(idx); consumer.accept(counterMap, words); }); ts.add(thread); } ts.forEach(t->t.start()); ts.forEach(t-> { try { t.join(); } catch (InterruptedException e) { e.printStackTrace();} }); System.out.println(counterMap); } public static List<String> readFromFile(int i) { ArrayList<String> words = new ArrayList<>(); try (BufferedReader in = new BufferedReader(new InputStreamReader(new FileInputStream("tmp/" + i +".txt")))) { while(true) { String word = in.readLine(); if(word == null) { break; } words.add(word); } return words; } catch (IOException e) { throw new RuntimeException(e); } } }
-
{a=200, b=200, c=200, d=200, e=200, f=200, g=200, h=200, i=200, j=200, k=200, l=200, m=200, n=200, o=200, p=200, q=200, r=200, s=200, t=200, u=200, v=200, w=200, x=200, y=200, z=200}
28.10.2 HashMap并发死链问题
- 要在JDK7下,因为7的链表是头插法,这样就会导致并发死链问题,8以后扩容机制和hash的计算方法都改变了。
- 原因就是在多线程环境下使用了非线程安全的map集合
- JDK8虽然将扩容算法做了调整,不再将元素加入链表头,(而是保持和扩容前一样的顺序,就是尾插法),但仍不意味着在多线程环境下能偶安全扩容,还会出现其他问题,(如扩容丢数据)
28.10.3 源码
重要属性和内部类
数据结构: Node[] table , 首先是一个数组,数组的元素是一个链表; 如下图: 数组叫做桶,数组的单个元素中的链表叫做bin;
哈希表:数组加链表实现的哈希表
-
// 默认是0// 当初始化时,为-1// 当扩容时 ,为-(1 + 扩容线程数)// 当初始化或扩容完成后,为 下一次的扩容的阈值大小 也就是通量的3/4private transient volatile int sizeCtl;// 单链表节点 整个ConcurrentHash 就是一个Node[]static class Node<K, V> implements Map.Entry<K,V> { final int hash; // 哈希码 final K key; // 键 volatile V val; // 值 volatile Node<K,V> next; // 下一个节点}// hash 表transient volatile Node<K, V>[] table;// 扩容时新的hash表transient volatile Node<K,V>[] nextTable;// 扩容时如果某个bin迁移完毕,就用ForwardingNode作为旧table bin的头节点// 当扩容时,我们会将旧的哈希表下每个bin下的节点迁移到新的哈希表下的每个bin 下// 当迁移完这个bin下的所有节点后,就代表这个bin已经迁移完毕,就会相应的下标下加一个ForWardingNode头节点,这样其他线程来了以后就知道这个bin已经处理完了,就不会再对这个bin做别的操作。// 如果在扩容过程中其他线程getkey 发现这个key下的头节点是ForWardingNode,就会去新的哈希表中取值。static final class ForWardingNode<K,V> extends Node<K,V> {}// 用在compute以及computeIfAbsent时,用来占位,计算完成后替换成普通Node节点static final class ReservationNode<K,V> extends Node<K,V> {}// 作为treebin(也可以说是红黑树) 的头节点, 存储root 和 first// 还可以避免DOS攻击// 当链表长度大于等于8时,会先尝试扩容,当扩容后的数组长度大于等于64时,才会将链表转换为红黑树// 然后当有节点被删除时,当节点数小于等于6时,会退化为链表结构。static final class TreeNode<K,V> extends Node<K,V> {}// 作为treebin的节点, 红黑树下的每个子节点,存储 parent,left, rightstatic final class TreeNode<K,V> extends Node<K,V> {}
- ForWardingNode
重要方法
-
// 获取Node[] 中第i个Node 获取链表头中第I个Nodestatic final <K,V> Node<K,V> tabAt(Node<K,V>[] tab, int i) { return (Node<K,V>)U.getObjectVolatile(tab, ((long)i << ASHIFT) + ABASE);}// CAS修改Node[]中第i个Node的值,c为旧值,v为新值static final <K,V> boolean casTabAt(Node<K,V>[] tab)// 直接修改Node[] 中第i的Node的值,v 为新值static final <K,V> boolean setTabAt(Node<K,V>[] tab, int i, Node<K,V> v)
构造器分析
可以看到实现了懒惰初始化,在构造方法中仅仅计算了table的大小,以后在第一次使用时才会真正创建
-
/**根据给定的元素数 ( initialCapacity )、表密度 ( loadFactor ) 和并发更新线程数 ( concurrencyLevel ) 创建一个具有初始表大小的新空映射。参数:initialCapacity – 初始容量。 给定指定的负载因子,实现会执行内部大小调整以容纳这么多元素。loadFactor – 用于建立初始表大小的负载因子(表密度)concurrencyLevel – 估计的并发更新线程数。 实现可以使用这个值作为调整大小的提示。抛出:IllegalArgumentException – 如果初始容量为负或负载因子或 concurrencyLevel 为非正**/public ConcurrentHashMap(int initialCapacity, float loadFactor, int concurrencyLevel) { if (!(loadFactor > 0.0f) || initialCapacity < 0 || concurrencyLevel <= 0) throw new IllegalArgumentException(); // 如果初始容量小于并发度,会将初始容量改为和并发度相同 if (initialCapacity < concurrencyLevel) // Use at least as many bins initialCapacity = concurrencyLevel; // as estimated threads // tableSizeFor仍然保证计算的大小是2^n 即 16 32 64 long size = (long)(1.0 + (long)initialCapacity / loadFactor); int cap = (size >= (long)MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : tableSizeFor((int)size); this.sizeCtl = cap;}
-
initialCapacity 初始容量
-
loadFactor 负载因子 3/4 0.75
-
concurrencyLevel 并发度
get流程
-
因为他的get方法全程没有加锁,所以性能很高。
-
/**返回指定键映射到的值,如果此映射不包含键的映射,则返回null 。更正式地说,如果此映射包含从键k到值v的映射,使得key.equals(k) ,则此方法返回v ; 否则返回null 。 (最多可以有一个这样的映射。)抛出:NullPointerException – 如果指定的键为空**/public V get(Object key) { Node<K,V>[] tab; Node<K,V> e, p; int n, eh; K ek; // spread 方法能够确保返回结果是正整数,因为哈希码有可能为负数,但是负数在后续过程中有特殊用途,所以要为正数 int h = spread(key.hashCode()); // 哈希表不为空且长度不为0 if ((tab = table) != null && (n = tab.length) > 0 && // (n-1) & h 与哈希值做按位与,找到数组中的下标(桶下标),然后返回其头节点 判断是否为空 (e = tabAt(tab, (n - 1) & h)) != null) { // 判断头节点哈希值和key的哈希值是否相同 if ((eh = e.hash) == h) { // 如果key的地址和头节点key的地址相同,则就是要查的节点 或者,地址不同,但是值相同也可 if ((ek = e.key) == key || (ek != null && key.equals(ek))) return e.val; } hash值为负值表示正在扩容,这个时候查的是ForwardingNode的find方法来定位到nextTable来 //eh=-1,说明该节点是一个ForwardingNode(哈希值为-1),正在迁移,此时调用ForwardingNode的find方法去nextTable里找。 //eh=-2,说明该节点是一个TreeBin,此时调用TreeBin的find方法遍历红黑树,由于红黑树有可能正在旋转变色,所以find里会有读写锁。 //eh>=0,说明该节点下挂的是一个链表,直接遍历该链表即可。 else if (eh < 0) // ForwardingNode和TreeBin会调用其相应的find方法去寻找 找到则返回,找不到返回null return (p = e.find(h, key)) != null ? p.val : null; // 正常遍历链表,用equals比较 while ((e = e.next) != null) { if (e.hash == h && // 哈希值相等且(key的对象或者值相等)则返回 ((ek = e.key) == key || (ek != null && key.equals(ek)))) return e.val; } } return null;}
put流程
一下数组简称table,链表简称bin
-
/** 将指定的键映射到此表中的指定值。 键和值都不能为空。 可以通过使用等于原始键的键调用get方法来检索该值。 参数: key – 与指定值关联的键 value – 要与指定键关联的值 返回: 与key关联的前一个值,如果没有key映射,则为null 抛出: NullPointerException – 如果指定的键或值为 null **/ public V put(K key, V value) { // 每次用新值覆盖旧值 return putVal(key, value, false); } /** put 和 putIfAbsent 的实现 onlyIfAbsent 如果取值为真,只有第一次put键和值才会把他们放入map,第二次put相同key的值不会做任何操作,也就是不会用新值覆盖掉旧址 */ final V putVal(K key, V value, boolean onlyIfAbsent) { // 与普通HashMap不同,这个key和value均不可为null,否则抛出异常。 if (key == null || value == null) throw new NullPointerException(); // 其中spread方法回综合高低位,具有更好的hash性,并保证返回为正整数 int hash = spread(key.hashCode()); int binCount = 0; // 将 table 赋值给tab 后 死循环 for (Node<K,V>[] tab = table;;) { // f 是链表头节点 // fh 是链表头节点的hash // i 是链表在table中的下标 也就是链表在桶的哪个下标下的位置 Node<K,V> f; int n, i, fh; // 第一种情况 创建哈希表 // 如果为空或者长度为0,则代表还未创建,则创建哈希表 if (tab == null || (n = tab.length) == 0) // 初始化table使用了CAS,无需synchronized创建成功,其他线程无需进入阻塞,提升了性能,进入下一轮循环。 tab = initTable(); // 见下面详细解析 // 第二种情况 创建链表头节点 // 如果头节点为null,则创建头节点 else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) { // 用CAS将头节点置为此节点,如果成功,则put完成结束,如果put失败,则重新进入循环,走下一个else if语句 添加链表头使用CAS,无需synchronized if (casTabAt(tab, i, null, new Node<K,V>(hash, key, value, null))) break; // no lock when adding to empty bin } // 第三种情况 帮忙扩容 // 如果当前链表的头节点的哈希值为MOVED也就是-1(ForwardingNode),他就知道别的线程正在进行扩容,那么他并不会进入阻塞,等待其他线程扩容结束,而是可以协助其他线程完成扩容,因为扩容时,只是锁住要进行迁移的链表也就是bin,其他bin并不会被锁住,所以其他线程可以继续操作未被锁住的bin进行协助迁移(扩容) else if ((fh = f.hash) == MOVED) tab = helpTransfer(tab, f); // 第四种情况 发生了桶下标冲突 加锁(当前Bin(链表)) else { V oldVal = null; // 锁住链表头节点 synchronized (f) { // 再次确认链表头节点是否被移动 if (tabAt(tab, i) == f) { // 判断头节点的哈希值是否为负数,如果不为负数则为链表 // 链表 if (fh >= 0) { binCount = 1; // 遍历链表 for (Node<K,V> e = f;; ++binCount) { K ek; // 找到相同key if (e.hash == hash && ((ek = e.key) == key || (ek != null && key.equals(ek)))) { oldVal = e.val; // 更新 值 if (!onlyIfAbsent) e.val = value; break; } Node<K,V> pred = e; // 已经是最后一个节点,新增Node,追加至链表尾 if ((e = e.next) == null) { pred.next = new Node<K,V>(hash, key, value, null); break; } } } // 红黑树 else if (f instanceof TreeBin) { Node<K,V> p; binCount = 2; // 先将头节点转换成红黑树的头节点, 然后put节点,如果添加putTreeVal则返回null,如果查找则返回节点,意思就是,如果已经存在key则返回节点,然后将新值替换旧值,如果key不存在,则添加。 if ((p = ((TreeBin<K,V>)f).putTreeVal(hash, key, value)) != null) { oldVal = p.val; if (!onlyIfAbsent) p.val = value; } } } // 释放表头节点的锁 } // 判断是树化还是扩容 // binCount链表长度 if (binCount != 0) { // 如果链表长度 >= 树化阈值(8),进行链表转化为红黑树 if (binCount >= TREEIFY_THRESHOLD) // 在这个方法中还会判断数组长度是否>= 64 如果 >=64才会树化。否则用扩容解决链表长度过长问题 treeifyBin(tab, i); if (oldVal != null) return oldVal; break; } } } // 增加size计数 类似于LongAdder使用多个累加单元 addCount(1L, binCount); return null; }
initTable()
-
// 使用 sizeCtl 中记录的大小初始化表。private final Node<K,V>[] initTable() { Node<K,V>[] tab; int sc; // 判断是否已经被创建 while ((tab = table) == null || tab.length == 0) { if ((sc = sizeCtl) < 0) // 将sizectl赋值给sc // 表示已经有其他线程在进行初始化,则这个线程进行礼让。 Thread.yield(); // lost initialization race; just spin // 用CAS的方式试图将sizeCtl(sc)的值设置为-1表示正在进行创建哈希表(初始化table) // U.compareAndSwapInt(this, SIZECTL, sc, -1)如果sc 值没改变,那么他就是传入的初始容量的值,就是构造方法中传入的初始容量大小。那么就将sizeCtl的值设置为-1,但是这时候SC的值还是容量大小没变。 else if (U.compareAndSwapInt(this, SIZECTL, sc, -1)) { try { // 获得锁,创建table 这时其他线程会在while()中循环yield直至table创建 if ((tab = table) == null || tab.length == 0) { int n = (sc > 0) ? sc : DEFAULT_CAPACITY; @SuppressWarnings("unchecked") Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n]; table = tab = nt; // 下一次扩容的阈值 sc = n - (n >>> 2); } } finally { // 最后将阈值赋值给sizeCtl sizeCtl = sc; } break; } } return tab;}
addCount()
-
/** 添加计数,如果表太小且尚未调整大小,则启动传输。 如果已经调整大小,则在工作可用时帮助执行转移。 传输后重新检查占用情况,以查看是否已经需要再次调整大小,因为调整大小是滞后添加的。 参数: x - 要添加的计数 检查 - 如果 <0,不检查调整大小,如果 <= 1 只检查是否无竞争 **/ private final void addCount(long x, int check) { CounterCell[] as; long b, s; // 判断是否已经有了累加单元数组 if ((as = counterCells) != null || // 如果没有,则会向基础单元(baseCount)上进行累加 !U.compareAndSwapLong(this, BASECOUNT, b = baseCount, s = b + x)) { CounterCell a; long v; int m; boolean uncontended = true; // 判断有没有累加单元数组counterCells if (as == null || (m = as.length - 1) < 0 || // 如果有累加单元数组,但是没有累加单元(cell) (a = as[ThreadLocalRandom.getProbe() & m]) == null || // 执行累加 ,如果celss CAS 累加计数失败, 都会进入下面的fullAddCount !(uncontended == U.compareAndSwapLong(a, CELLVALUE, v = a.value, v + x))) { // 创建累加单元数组和cell,以及累加重试 fullAddCount(x, uncontended); return; } // 检查链表长度是否小于等于1 如果小于等于1直接返回,否则判断是否需要扩容 if (check <= 1) return; // 获取元素个数 s = sumCount(); } if (check >= 0) { Node<K,V>[] tab, nt; int n, sc; while (s >= (long)(sc = sizeCtl) && (tab = table) != null && (n = tab.length) < MAXIMUM_CAPACITY) { int rs = resizeStamp(n); if (sc < 0) { if ((sc >>> RESIZE_STAMP_SHIFT) != rs || sc == rs + 1 || sc == rs + MAX_RESIZERS || (nt = nextTable) == null || transferIndex <= 0) break; // newtable 已经创建了,需要帮忙扩容 if (U.compareAndSwapInt(this, SIZECTL, sc, sc + 1)) transfer(tab, nt); } // 需要扩容,这时newtable未创建 else if (U.compareAndSwapInt(this, SIZECTL, sc, (rs << RESIZE_STAMP_SHIFT) + 2)) // newtable是懒惰初始化,所以这里传的是空 transfer(tab, null); s = sumCount(); } } }
size()
-
size计算实际发生在put,remove改变集合元素的操作之中,
-
没有竞争发生,向baseCount累加计数
-
有竞争发生,新建counterCells(累加单元数组),向其中的一个cell(累加单元)累加计数
-
counterCells初始有两个cell
-
如果计数竞争比较激烈,会创建新的cell来累加计数
-
-
public int size() { long n = sumCount(); return ((n < 0L) ? 0 : (n > (long)Integer.MAX_VALUE) ? Integer.MAX_VALUE : (int)n);}
-
final long sumCount() { CounterCell[] as = counterCells; CounterCell a; long sum = baseCount; if (as != null) { for (int i = 0; i < as.length; ++i) { if ((a = as[i]) != null) sum += a.value; } } return sum;}
transfer()
-
// 将每个 bin 中的节点移动和/或复制到新表. 扩容流程 // 参数:原始table 新的table 因为新的table是懒加载,在第一次创建的时候进行初始化 private final void transfer(Node<K,V>[] tab, Node<K,V>[] nextTab) { int n = tab.length, stride; if ((stride = (NCPU > 1) ? (n >>> 3) / NCPU : n) < MIN_TRANSFER_STRIDE) stride = MIN_TRANSFER_STRIDE; // subdivide range // 如果新table为空 则创建table if (nextTab == null) { // initiating try { @SuppressWarnings("unchecked") Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n << 1]; nextTab = nt; } catch (Throwable ex) { // try to cope with OOME sizeCtl = Integer.MAX_VALUE; return; } nextTable = nextTab; transferIndex = n; } int nextn = nextTab.length; ForwardingNode<K,V> fwd = new ForwardingNode<K,V>(nextTab); boolean advance = true; boolean finishing = false; // to ensure sweep before committing nextTab // 以一个bin为单位进行迁移 for (int i = 0, bound = 0;;) { Node<K,V> f; int fh; while (advance) { int nextIndex, nextBound; if (--i >= bound || finishing) advance = false; else if ((nextIndex = transferIndex) <= 0) { i = -1; advance = false; } else if (U.compareAndSwapInt (this, TRANSFERINDEX, nextIndex, nextBound = (nextIndex > stride ? nextIndex - stride : 0))) { bound = nextBound; i = nextIndex - 1; advance = false; } } if (i < 0 || i >= n || i + n >= nextn) { int sc; if (finishing) { nextTable = null; table = nextTab; sizeCtl = (n << 1) - (n >>> 1); return; } if (U.compareAndSwapInt(this, SIZECTL, sc = sizeCtl, sc - 1)) { if ((sc - 2) != resizeStamp(n) << RESIZE_STAMP_SHIFT) return; finishing = advance = true; i = n; // recheck before commit } } // 查看这个链表的表头是否为null else if ((f = tabAt(tab, i)) == null) // 如果为null就将表头置为ForwardingNode advance = casTabAt(tab, i, null, fwd); // 如果标头的哈希值为-1则表示此bin迁移完成 else if ((fh = f.hash) == MOVED) advance = true; // already processed else { // 否则就是还未进行迁移,锁住链表头或红黑树头节点,进行迁移 synchronized (f) { if (tabAt(tab, i) == f) { Node<K,V> ln, hn; // 哈希值大于等于0 代表是链表,进行链表的迁移工作 if (fh >= 0) { int runBit = fh & n; Node<K,V> lastRun = f; for (Node<K,V> p = f.next; p != null; p = p.next) { int b = p.hash & n; if (b != runBit) { runBit = b; lastRun = p; } } if (runBit == 0) { ln = lastRun; hn = null; } else { hn = lastRun; ln = null; } for (Node<K,V> p = f; p != lastRun; p = p.next) { int ph = p.hash; K pk = p.key; V pv = p.val; if ((ph & n) == 0) ln = new Node<K,V>(ph, pk, pv, ln); else hn = new Node<K,V>(ph, pk, pv, hn); } setTabAt(nextTab, i, ln); setTabAt(nextTab, i + n, hn); setTabAt(tab, i, fwd); advance = true; } // 如果是红黑树节点,进行红黑树节点迁移工作。 else if (f instanceof TreeBin) { TreeBin<K,V> t = (TreeBin<K,V>)f; TreeNode<K,V> lo = null, loTail = null; TreeNode<K,V> hi = null, hiTail = null; int lc = 0, hc = 0; for (Node<K,V> e = t.first; e != null; e = e.next) { int h = e.hash; TreeNode<K,V> p = new TreeNode<K,V> (h, e.key, e.val, null, null); if ((h & n) == 0) { if ((p.prev = loTail) == null) lo = p; else loTail.next = p; loTail = p; ++lc; } else { if ((p.prev = hiTail) == null) hi = p; else hiTail.next = p; hiTail = p; ++hc; } } ln = (lc <= UNTREEIFY_THRESHOLD) ? untreeify(lo) : (hc != 0) ? new TreeBin<K,V>(lo) : t; hn = (hc <= UNTREEIFY_THRESHOLD) ? untreeify(hi) : (lc != 0) ? new TreeBin<K,V>(hi) : t; setTabAt(nextTab, i, ln); setTabAt(nextTab, i + n, hn); setTabAt(tab, i, fwd); advance = true; } } } } } }
28.11 LinkedBlockingQueue
28.11.1 基本的入队出队
-
public class LinkedBlockingQueue<E> extends AbstractQueue<E> implements BlockingQueue<E>, java.io.Serializable { static class Node<E> { E item; // 节点所关联的元素 /** * Next节点三种情况 * - 真正的后继节点 * - 节点自己本身 发生在出队时 * - null, 没有后继结点了,已经是队尾了 */ Node<E> next; Node(E x) { item = x; } } }
-
初始化链表last(尾节点指针) = head(头结点指针)= new Node; Dummy(哑元节点或哨兵节点)用来占位,item和next为null Dummy可以提高并发性
入队
-
当一个节点入队 last = last.next = node;
-
再来一个节点入队 last = last.next = node;
出队
-
Node<E> h = head; Node<E> first = h.next; h.next = h; // help GC head = first; E x = first.item; first.item = null; return x;
-
Node h = head;
-
first = h.next
-
h.next = h 指向自己,不让他乱指向别的节点,帮助垃圾回收
-
head = first
-
E x = first.item; first.item = null; return x;
加锁分析
高明之处在于用了两把锁和dummy节点
- 用一把锁,同一时刻,最多只允许有一个线程(生产者或者消费者,二选一)执行
- 用两把锁,同一时刻,可以允许两个线程同时(一个生产者与一个消费者)执行
- 消费者与消费者仍然串行(锁头节点)
- 消费者与消费者仍然串行(锁尾节点)
线程安全分析
-
当节点总数大于2时(包括Dummy节点),putLock保证的是last节点的线程安全,takeLock保证的是head节点的线程安全。两把锁保证了入队和出队没有竞争。
-
当节点数等于2时(即一个Dummy节点,一个正常节点)这时候,仍然时两把锁锁两个对象,不会竞争
-
当节点数等于1时(就一个Dummy节点)这时take线程会被notEmpty条件阻塞,有竞争,会阻塞(等待非空条件结束)
-
// 用户 put(阻塞) offfer(非阻塞) private final ReentrantLock putLock = enw ReentrantLock(); // 用户 take(阻塞) poll(非阻塞) private final ReentrantLock takeLock = new ReentrantLock();
put操作
-
public void put(E e) throws InterruptedException { // 不允许有空值 if (e == null) throw new NullPointerException(); // Note: convention in all put/take/etc is to preset local var // holding count negative to indicate failure unless set. int c = -1; Node<E> node = new Node<E>(e); final ReentrantLock putLock = this.putLock; // 用来维护元素计数 final AtomicInteger count = this.count; // 加了一个可被打断的锁 putLock.lockInterruptibly(); try { // 如果元素个数等于容量上限,代表队列满了,进入等待 while (count.get() == capacity) { // notFull是一个条件变量,进入此条件变量等待 等待不满 notFull.await(); } // 有空位,入队且计数加一 enqueue(node); c = count.getAndIncrement(); // 除了自己put以外,队列还有空位,由自己叫醒其他put线程。 if (c + 1 < capacity) notFull.signal(); } finally { putLock.unlock(); } // 如果队列中有一个元素,叫醒take 线程 if (c == 0) // 都用signal而不是signalAll是为了减少竞争 signalNotEmpty(); }
take()
-
public E take() throws InterruptedException { E x; int c = -1; final AtomicInteger count = this.count; final ReentrantLock takeLock = this.takeLock; takeLock.lockInterruptibly(); try { while (count.get() == 0) { notEmpty.await(); } x = dequeue(); c = count.getAndDecrement(); if (c > 1) notEmpty.signal(); } finally { takeLock.unlock(); } // 如果队列中只有一个空位时,叫醒put线程 // 如果有多个线程进行出队,第一个线程满足c==capacity,但后续线程c < capacity if (c == capacity) // 这里调用的是notFull.signal()而不是notFull.signalAll()是为了减少竞争 signalNotFull(); return x; }
-
由 put 唤醒 put 是为了避免信号不足
性能比较
主要列举 LinkedBlockingQueue 与 ArrayBlockingQueue 的性能比较
- Linked 支持有界,Array 强制有界
- Linked 实现是链表,Array 实现是数组
- Linked 是懒惰的,而 Array 需要提前初始化 Node 数组
- Linked 每次入队会生成新 Node,而 Array 的 Node 是提前创建好的
- Linked 两把锁,Array 一把锁
28.12 ConcurrentLinkedQueue
ConcurrentLinkedQueue的设计与LinkedBlockingQueue非常像,也是
- 两把【锁】,同一时刻,可以允许两个线程同时(一个生产者与一个消费者)执行
- Dummy节点的引入让两把【锁】将来锁住的是不同对象,避免竞争
- 只是这个【锁】使用了CAS实现,别的是RenntrantLock或者Synchronized
应用
- Tomcat的Connector结构,Acceptor作为生产者向Poller消费者传递事件信息时,正是采用了ConcurrentLinkedQueue将SocketChannel给Poller使用
28.13 CopyOnWriteArrayList
CopyOnWriteArraySet只不过有是一个去重的行为,是一个不可重复的,用的还是CopyOnWriteArrayList的方法,只不过是在添加元素时调用了
-
public boolean add(E e) { return al.addIfAbsent(e); 如果不存在则添加 }
底层实现采用了**【写入时拷贝】的思想,增删改操作会将底层数组拷贝一份,更改操作再新数组上执行,这时不影响其他线程的【并发读】,【读写分离】** 读-读 读写 非阻塞,写写 阻塞
以新增为例(JDK8 源码,JDK11用的synchronized锁)
-
public boolean add(E e) { final ReentrantLock lock = this.lock; // 上锁是为了让写-写操作阻塞 因为读操作不需要加锁,所以读-读 读-写不会阻塞,可以并发 lock.lock(); try { // 获取旧数组 Object[] elements = getArray(); int len = elements.length; // 拷贝新的数组(这了是比较耗时的操作,但不影响其他读线程) Object[] newElements = Arrays.copyOf(elements, len + 1); // 添加新元素 newElements[len] = e; // 替换旧的数组 setArray(newElements); return true; } finally { lock.unlock(); } }
其他读操作并未加锁,例如:
-
public boolean add(E e) { final ReentrantLock lock = this.lock; lock.lock(); try { Object[] elements = getArray(); int len = elements.length; Object[] newElements = Arrays.copyOf(elements, len + 1); newElements[len] = e; setArray(newElements); return true; } finally { lock.unlock(); } }
适用于【读多写少】的应用场景
Get弱一致性
-
不容易复现,但是问题确实存在
迭代器弱一致性
-
这里的迭代器弱一致性,并不是说,迭代器是弱一致性,而是看你用的什么List。
-
package com.sunyang.concurrentstudy; /** * @program: ConcurrentStudy * @description: Demo * @author: SunYang * @create: 2021-08-14 16:55 **/ public class CopyOnWriteListDemo { public static void main(String[] args) { CopyOnWriteArrayList<Integer> list = new CopyOnWriteArrayList<>(); list.add(1); list.add(2); list.add(3); Iterator<Integer> iter = list.iterator(); new Thread(() -> { list.remove(0); System.out.println(list); }).start(); sleep1s(); while (iter.hasNext()) { System.out.println(iter.next()); } } }
-
强一致性和高并发二者不可得兼
-
数据库的 MVCC 都是弱一致性的表现
进行出队,第一个线程满足c==capacity,但后续线程c < capacity
if (c == capacity)
// 这里调用的是notFull.signal()而不是notFull.signalAll()是为了减少竞争
signalNotFull();
return x;
}
- 由 put 唤醒 put 是为了避免信号不足
##### 性能比较
主要列举 LinkedBlockingQueue 与 ArrayBlockingQueue 的性能比较
- Linked 支持有界,Array 强制有界
- Linked 实现是链表,Array 实现是数组
- Linked 是懒惰的,而 Array 需要提前初始化 Node 数组
- Linked 每次入队会生成新 Node,而 Array 的 Node 是提前创建好的
- Linked 两把锁,Array 一把锁
### 28.12 ConcurrentLinkedQueue
ConcurrentLinkedQueue的设计与LinkedBlockingQueue非常像,也是
- 两把【锁】,同一时刻,可以允许两个线程同时(一个生产者与一个消费者)执行
- Dummy节点的引入让两把【锁】将来锁住的是不同对象,避免竞争
- 只是这个【锁】使用了CAS实现,别的是RenntrantLock或者Synchronized
应用
- Tomcat的Connector结构,Acceptor作为生产者向Poller消费者传递事件信息时,正是采用了ConcurrentLinkedQueue将SocketChannel给Poller使用
- [外链图片转存中...(img-go5Pfg5H-1631354371431)]
### 28.13 CopyOnWriteArrayList
CopyOnWriteArraySet只不过有是一个去重的行为,是一个不可重复的,用的还是CopyOnWriteArrayList的方法,只不过是在添加元素时调用了
- ```java
public boolean add(E e) {
return al.addIfAbsent(e); 如果不存在则添加
}
底层实现采用了**【写入时拷贝】的思想,增删改操作会将底层数组拷贝一份,更改操作再新数组上执行,这时不影响其他线程的【并发读】,【读写分离】** 读-读 读写 非阻塞,写写 阻塞
以新增为例(JDK8 源码,JDK11用的synchronized锁)
-
public boolean add(E e) { final ReentrantLock lock = this.lock; // 上锁是为了让写-写操作阻塞 因为读操作不需要加锁,所以读-读 读-写不会阻塞,可以并发 lock.lock(); try { // 获取旧数组 Object[] elements = getArray(); int len = elements.length; // 拷贝新的数组(这了是比较耗时的操作,但不影响其他读线程) Object[] newElements = Arrays.copyOf(elements, len + 1); // 添加新元素 newElements[len] = e; // 替换旧的数组 setArray(newElements); return true; } finally { lock.unlock(); } }
其他读操作并未加锁,例如:
-
public boolean add(E e) { final ReentrantLock lock = this.lock; lock.lock(); try { Object[] elements = getArray(); int len = elements.length; Object[] newElements = Arrays.copyOf(elements, len + 1); newElements[len] = e; setArray(newElements); return true; } finally { lock.unlock(); } }
适用于【读多写少】的应用场景
Get弱一致性
-
[外链图片转存中…(img-2iDVFYv2-1631354371431)]
-
[外链图片转存中…(img-DF6Sk1vN-1631354371432)]
-
不容易复现,但是问题确实存在
迭代器弱一致性
-
这里的迭代器弱一致性,并不是说,迭代器是弱一致性,而是看你用的什么List。
-
package com.sunyang.concurrentstudy; /** * @program: ConcurrentStudy * @description: Demo * @author: SunYang * @create: 2021-08-14 16:55 **/ public class CopyOnWriteListDemo { public static void main(String[] args) { CopyOnWriteArrayList<Integer> list = new CopyOnWriteArrayList<>(); list.add(1); list.add(2); list.add(3); Iterator<Integer> iter = list.iterator(); new Thread(() -> { list.remove(0); System.out.println(list); }).start(); sleep1s(); while (iter.hasNext()) { System.out.println(iter.next()); } } }
-
强一致性和高并发二者不可得兼
-
数据库的 MVCC 都是弱一致性的表现
-
并发高和一致性是矛盾的,需要权衡