JUC
线程的基础知识
进程与线程
进程
- 程序:一组计算机能识别和执行的指令。指令要运行 数据要读写 就必须将指令加载至 CPU 数据加载至内存。
- 进程是正在运行的程序的实例。从磁盘加载这个程序的代码至内存 就开启了一个进程。
线程
- 操作系统能够进行运算调度的最小单位 被包含在进程中 是进程中的实际运作单位
- 一条线程指的是进程中一个单一顺序的控制流(指令流) 一个进程中可以并发多个线程 每条线程并行执行不同的任务
并发与并行
-
并发:同一时间应对多种事情的能力。
- 在单核 CPU的情况下 操作系统中有一个组件叫做任务调度器
- 将 CPU 的时间片分给不同的线程使用 将线程轮流使用 CPU 的做法称为并发
-
并行:同一时间动手做多件事情的能力。
- 在多核 CPU 的情况下 多个线程能够同时进行
单核CPU不能实现并行 但能实现并发 单核 CPU 的情况下使用并发编程仍能极大改善程序运行性能。
创建线程的方式
-
继承 Thread 类
public class Thread01 extends Thread { @Override public void run() { System.out.println("创建线程" + Thread.currentThread().getName()); } }
-
实现 Runnable 接口
public class Thread02 implements Runnable { @Override public void run() { System.out.println("创建线程" + Thread.currentThread().getName()); } }
-
实现 Callable 接口
public class Thread03 implements Callable<Void> { @Override public Void call() throws Exception { System.out.println("创建线程" + Thread.currentThread().getName()); return null; } }
-
Thread 的带参构造函数 只能接受 Runnable 接口 如何执行 Callable 接口创建的线程:
public class Demo01 { public static void main(String[] args) { Thread01 thread01 = new Thread01(); thread01.start(); Thread thread02 = new Thread(new Thread02()); thread02.start(); // 利用 FutureTask // class FutureTask<V> implements RunnableFuture<V> // interface RunnableFuture<V> extends Runnable, Future<V> Thread thread03 = new Thread(new FutureTask<>(new Thread03())); thread03.start(); } }
-
线程池
// 构造方法 public ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue, ThreadFactory threadFactory, RejectedExecutionHandler handler)
corePoolSize:核心线程数量
maximumPoolSize:最大线程数量
keepAliveTime:急救线程生存时间
unit:生存时间单位
workQueue:阻塞队列
threadFactory:线程工厂
handler:拒绝策略
-
工作方式:
- 线程池中开始没有线程 当一个任务提交给线程池后 线程池会创建一个新线程来执行任务
- 当需要的线程数达到 corePoolSize 这时再加入新的任务 会被放入 workQueue 直到有空闲的线程
- 如果 workQueue 是有界队列 当任务数量超过 workQueue 长度 会创建
maximumPoolSize - corePoolSize
数量的急救线程 - 如果线程达到了 maximumPoolSize 仍有任务 这时会执行拒绝策略
- 当高峰去过去后 超过 corePoolSize 线程的急救线程 如果 keepAliveTime 时间内没有任务做 会释放资源
-
Executors类中提供的工厂方法
-
newFixedThreadPool:创建固定线程数的线程池
public static ExecutorService newFixedThreadPool(int nThreads) { // corePoolSize = maximumPoolSize = nThreads // 不需要急救线程和超时时间 // 阻塞队列是无界的 可以放任意数量的任务 return new ThreadPoolExecutor(nThreads, nThreads, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<Runnable>()); } public LinkedBlockingQueue() { this(Integer.MAX_VALUE); }
-
newCachedThreadPool:
public static ExecutorService newCachedThreadPool() { // 没有核心线程 全部都是急救线程 存在60秒 return new ThreadPoolExecutor(0, Integer.MAX_VALUE, 60L, TimeUnit.SECONDS, new SynchronousQueue<Runnable>()); }
-
newSingleThreadExecutor:
public static ExecutorService newSingleThreadExecutor() { return new FinalizableDelegatedExecutorService // 一个核心线程 最大线程也为1 没有急救线程 (new ThreadPoolExecutor(1, 1, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<Runnable>())); }
装饰器模式 FinalizableDelegatedExecutorService
不能再调用 ThreadPoolExecutor 中特有的方法了
-
-
任务调度线程池 ScheduledExecutorService
-
在使用任务调度线程池之前 可以使用 java.util.Timer 来实现定时功能
-
Timer 的所有任务都是由一个线程来调度的 因此所有的任务都是串行执行的 同一时间只有一个任务在执行 前一个任务的延迟或异常会影响之后的任务
public static void main(String[] args) { TimerTask task1 = new TimerTask() { @Override public void run() { System.out.println("task1"); try { TimeUnit.SECONDS.sleep(2); } catch (InterruptedException e) { throw new RuntimeException(e); } } }; TimerTask task2 = new TimerTask() { @Override public void run() { System.out.println("task2"); } }; // 此时的 timer 都希望两个任务1秒后执行 但是 task1执行用了2秒 // 会导致 task2 的开始时间延后 Timer timer = new Timer(); timer.schedule(task1, 1000); timer.schedule(task2, 1000); }
-
改为 newScheduledThreadPool 执行
public static void main(String[] args) { ScheduledExecutorService threadPool = Executors.newScheduledThreadPool(2); threadPool.schedule(() -> { System.out.println("任务1,执行时间:" + new Date()); try { TimeUnit.SECONDS.sleep(2); } catch (InterruptedException e) { throw new RuntimeException(e); } }, 1, TimeUnit.SECONDS); threadPool.schedule(() -> System.out.println("任务2,执行时间:" + new Date()), 1, TimeUnit.SECONDS); threadPool.schedule(() -> System.out.println("任务3,执行时间:" + new Date()), 1, TimeUnit.SECONDS); }
-
间隔多长时间开始执行 每多长时间执行一次
public static void main(String[] args) { ScheduledExecutorService threadPool = Executors.newScheduledThreadPool(2); threadPool.scheduleAtFixedRate(() -> { System.out.println("执行时间:" + new Date()); }, 1, 1, TimeUnit.SECONDS); }
-
如果每次任务的执行时间超过了 间隔时间 按照执行任务的时间跑
public static void main(String[] args) { ScheduledExecutorService threadPool = Executors.newScheduledThreadPool(2); threadPool.scheduleAtFixedRate(() -> { System.out.println("执行时间:" + new Date()); try { TimeUnit.SECONDS.sleep(2); } catch (InterruptedException e) { throw new RuntimeException(e); } }, 1, 1, TimeUnit.SECONDS); } // 执行时间:Mon Oct 09 20:07:47 CST 2023 // 执行时间:Mon Oct 09 20:07:49 CST 2023
-
scheduleWithFixedDelay:在每次执行任务时间的基础上 加上每轮的延迟时间
public static void main(String[] args) { ScheduledExecutorService threadPool = Executors.newScheduledThreadPool(2); threadPool.scheduleWithFixedDelay(() -> { System.out.println("执行时间:" + new Date()); try { TimeUnit.SECONDS.sleep(2); } catch (InterruptedException e) { throw new RuntimeException(e); } }, 1, 1, TimeUnit.SECONDS); }
-
常用方法
- sleep
- 调用 sleep 会让当前线程从Running进入Timed Waiting状态
- 其它线程可以使用 interrupt 方法打断正在睡眠的线程 这时 sleep 方法会抛出 InterruptedException
- 睡眠结束后的线程未必会立刻得到执行
- 建议用 TimeUnit 的 sleep 代替 Thread 的 sleep 来获得更好的可读性
try {
TimeUnit.SECONDS.sleep(10);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
- yield
- 调用 yield 会让当前线程从Running进入Runnable就绪状态 然后调度执行其它线程
- 让出 CPU 的时间片 具体的实现依赖操作系统的任务调度
Thread.yield();
- join
- 用 join 加在 start 方法之后 等待线程执行结束
public static void main(String[] args) throws InterruptedException {
Thread thread = new Thread(() -> {
System.out.println("开始");
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println("结束");
r = 10;
});
thread.start();
thread.join();
System.out.println(r);
}
- 有时效的 join
public static void main(String[] args) throws InterruptedException {
Thread thread = new Thread(() -> {
System.out.println("开始");
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println("结束");
r = 10;
});
long start = System.currentTimeMillis();
thread.start();
// 会提前结束
thread.join(1500);
long end = System.currentTimeMillis();
// 拿不到 这里 r=0
System.out.println(r);
// 1508
System.out.println(end - start);
}
public static void main(String[] args) throws InterruptedException {
Thread thread = new Thread(() -> {
System.out.println("开始");
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println("结束");
r = 10;
});
long start = System.currentTimeMillis();
thread.start();
thread.join(1500);
long end = System.currentTimeMillis();
// 能拿到 r=10
System.out.println(r);
// 1015
System.out.println(end - start);
}
Future 接口与 CompletableFuture 的使用
Future 接口
- Future 提供了异步并行计算的能力。可以从Future 的任务中拿到结果。
- Future 的主要实现类 FutureTask 存在的问题:
- get() 方法获取返回值的时候会阻塞 直到有结果程序才能继续执行
- 使用 isDone() 方法 一直轮询会浪费 CPU 的资源
public class Thread03 implements Callable<Integer> {
@Override
public Integer call() throws Exception {
System.out.println("创建线程" + Thread.currentThread().getName());
TimeUnit.SECONDS.sleep(2);
return 10;
}
}
// get() 方法会阻塞主线程执行
public class Demo03 {
public static void main(String[] args) throws ExecutionException, InterruptedException {
FutureTask<Integer> futureTask = new FutureTask<>(new Thread03());
Thread thread03 = new Thread(futureTask);
thread03.start();
Integer result = futureTask.get();
System.out.println(result);
System.out.println("我要处理其它事情...");
}
}
// isDone() 方法会一直轮询
public class Demo02 {
public static void main(String[] args) throws ExecutionException, InterruptedException {
FutureTask<Integer> futureTask = new FutureTask<>(new Thread03());
Thread thread03 = new Thread(futureTask);
thread03.start();
while (true) {
if (futureTask.isDone()) {
System.out.println(futureTask.get());
break;
} else {
TimeUnit.SECONDS.sleep(1);
System.out.println(thread03.getName() + "正在处理...请稍后...");
}
}
}
}
CompletableFuture
为了解决上述 Future 接口的问题 JDK8 中引入了 CompletableFuture 类
-
CompletableFuture 的四个静态构造方法
// 有返回值的
public static CompletableFuture supplyAsync(Supplier supplier)
public static CompletableFuture supplyAsync(Supplier supplier,Executor executor)// 无返回值的
public static CompletableFuture runAsync(Runnable runnable)
public static CompletableFuture runAsync(Runnable runnable, Executor executor)
-
利用 whenComplete 解决 CPU轮询和 阻塞问题
public class Demo01 { public static void main(String[] args) throws ExecutionException, InterruptedException { ExecutorService executorService = Executors.newFixedThreadPool(3); CompletableFuture.supplyAsync(() -> { try { TimeUnit.SECONDS.sleep(2); } catch (InterruptedException e) { throw new RuntimeException(e); } return 1; }, executorService).whenComplete((result, exception) -> { System.out.println("计算完成 结果为:" + result); }).exceptionally(exception -> { exception.printStackTrace(); System.out.println("出现异常返回 null"); return null; }); System.out.println("我要处理其它事情..."); executorService.shutdown(); } } // 打印结果 我要处理其它事情... 计算完成 结果为:1
-
常用方法
-
获得结果和触发计算
public static void main(String[] args) throws Exception { CompletableFuture<Integer> completableFuture = CompletableFuture.supplyAsync(() -> { try { TimeUnit.SECONDS.sleep(2); } catch (InterruptedException e) { throw new RuntimeException(e); } return 1; }); // 获取返回值 System.out.println(completableFuture.get()); // 获取返回值 System.out.println(completableFuture.join()); // 1S后获取返回值 如果计算还没计算出结果会报 java.util.concurrent.TimeoutException System.out.println(completableFuture.get(1, TimeUnit.SECONDS)); // 立马获取返回值 如果没有 返回默认值 System.out.println(completableFuture.getNow(3)); // 返回值是个 boolean 是否打断get方法立即返回括号值 如果计算完毕返回正常的返回值 System.out.println(completableFuture.complete(4)); // 如果 complete 返回的是 true 表示打断了get方法 此时返回括号内的值 4 System.out.println(completableFuture.get()); }
-
对计算结果进行处理
- 正常消费
public static void main(String[] args) { CompletableFuture<Integer> completableFuture = CompletableFuture.supplyAsync(() -> { return 1; }).thenApply(result -> { System.out.println("thenApply 1..."); return result + 10; }).thenApply(result -> { System.out.println("thenApply 2..."); return result + 10; }).exceptionally(throwable -> { throwable.printStackTrace(); return null; }); System.out.println(completableFuture.join()); } // 打印结果 thenApply 1... thenApply 2... 21
- thenApply 出现异常后 当前步骤有异常的话就叫停
- handle 出现异常后 后续代码会继续执行
public static void main(String[] args) { CompletableFuture<Integer> completableFuture = CompletableFuture.supplyAsync(() -> { return 1; }).handle((result,throwable) -> { int i = 1 / 0; System.out.println("handle 1..."); return result + 10; }).handle((result,throwable) -> { System.out.println("handle 2..."); return result + 10; }).exceptionally(throwable -> { throwable.printStackTrace(); return null; }); System.out.println(completableFuture.join()); }
-
对计算结果进行消费
public static void main(String[] args) { CompletableFuture .supplyAsync(() -> 1) // 接受结果并返回结果 .thenApply(result -> { System.out.println("上一步的结果:" + result); return result + 1; }) // 接受结果但不返回结果 .thenAccept(System.out::println) // 既不接受结果 也不返回结果 .thenRun(() -> System.out.println("我自己就能玩...")); }
-
对计算速度进行选用
public static void main(String[] args) { CompletableFuture<String> playA = CompletableFuture.supplyAsync(() -> { try { System.out.println("A come in"); TimeUnit.SECONDS.sleep(2); } catch (InterruptedException e) { e.printStackTrace(); } return "playA"; }); CompletableFuture<String> playB = CompletableFuture.supplyAsync(() -> { try { System.out.println("B come in"); TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); } return "playB"; }); // 谁快用谁 CompletableFuture<String> completableFuture = playA.applyToEither(playB, result -> result + " is winner"); System.out.println(completableFuture.join()); } // 结果打印 A come in B come in playB is winner
-
对计算结果进行合并
public static void main(String[] args) { CompletableFuture<Integer> result1 = CompletableFuture.supplyAsync(() -> { try { TimeUnit.SECONDS.sleep(2); } catch (InterruptedException e) { e.printStackTrace(); } return 100; }); CompletableFuture<Integer> result2 = CompletableFuture.supplyAsync(() -> { try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); } return 200; }); CompletableFuture<Integer> finalResult = result1.thenCombine(result2, Integer::sum); System.out.println(finalResult.join()); // 300 }
-
CAS
CAS(Compare And Swap):比较并交换。用于保证共享变量的原子性更新。包含三个操作数:内存位置、预期值与更新值。执行 CAS 操作时将内存位置的值与预期原值进行比较:
如果相匹配:处理器会自动将该位置更新为新值
如果不相匹配:处理器不做任何操作 多个线程同时执行 CAS 操作只会有一个成功
public static void main(String[] args) {
AtomicInteger atomicInteger = new AtomicInteger(5);
// 期望5 更新为10 返回 true
System.out.println(atomicInteger.compareAndSet(5, 10));
// 最新值为10
System.out.println(atomicInteger.get());
// 期望为5 但此时已经改为10了 返回false
System.out.println(atomicInteger.compareAndSet(5, 10));
}
// compareAndSet 底层掉的是 unsafe 类的方法
public final boolean compareAndSet(int expect, int update) {
return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
}
Unsafe 类
Unsafe 类中的所有方法都是 native 修饰的。基于该类可以直接操作特定的内存数据。
Java 中CAS操作的执行依赖于Unsafe 类的方法。调用Unsafe 类的CAS方法 JVM 会帮我们实现出CAS汇编指令。这是一种完全依赖于硬件的功能。
CAS 是一种系统原语 原语的执行必须是连续的 在执行过程中不会被中断 不会造成数据不一致问题。
自旋锁
// 自旋锁
public class SpinLockDemo {
// 将线程作为原子引用
AtomicReference<Thread> atomicReference = new AtomicReference<>();
// 加锁
public void lock() {
Thread thread = Thread.currentThread();
System.out.println(Thread.currentThread().getName() + "获得锁");
while (!atomicReference.compareAndSet(null, thread)) {
try {
TimeUnit.SECONDS.sleep(1);
System.out.println(Thread.currentThread().getName() + "正在尝试获得锁");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
// 解锁
public void unLock() {
Thread thread = Thread.currentThread();
atomicReference.compareAndSet(thread, null);
System.out.println(Thread.currentThread().getName() + "释放锁");
}
public static void main(String[] args) {
SpinLockDemo spinLockDemo = new SpinLockDemo();
new Thread(() -> {
spinLockDemo.lock();
try {
TimeUnit.SECONDS.sleep(5);
} catch (InterruptedException e) {
e.printStackTrace();
}
spinLockDemo.unLock();
}, "线程1").start();
try {
TimeUnit.MILLISECONDS.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
new Thread(() -> {
spinLockDemo.lock();
try {
TimeUnit.SECONDS.sleep(5);
} catch (InterruptedException e) {
e.printStackTrace();
}
spinLockDemo.unLock();
}, "线程2").start();
}
}
CAS 缺点
-
ABA问题
如果线程1从内存位置V中取出A 线程2也从内存V中取出A
线程1将A的值改为B以后 又将B的值改回了A
此时线程2中预期值和内存中的值是一致的
尽管线程2的操作能够成功 但不代表这个过程没有问题
可以使用带戳记的原子引用
public static void main(String[] args) { Book book1 = new Book(1, "javaBook"); AtomicStampedReference<Book> stampedReference = new AtomicStampedReference<>(book1, 1); System.out.println(stampedReference.getReference() + "\t" + stampedReference.getStamp()); Book book2 = new Book(2, "mysqlBook"); System.out.println(stampedReference.compareAndSet(book1, book2, 1, 2)); System.out.println(stampedReference.getReference() + "\t" + stampedReference.getStamp()); }
-
循环时间开销大
原子操作类
基本类型原子类
AtomicInteger
:整型原子类AtomicBoolean
:布尔型原子类AtomicLong
:长整型原子类
AtomicInteger atomicInteger = new AtomicInteger(10);
System.out.println(atomicInteger.get()); // 获取当前值 10
System.out.println(atomicInteger.getAndAdd(5)); // 获取的是当前值 并加上预期的值 10
System.out.println(atomicInteger.getAndSet(10)); // 获取的是当前值 并设置上预期的值 15
System.out.println(atomicInteger.getAndIncrement()); // 获取的是当前值 并自增1 10
System.out.println(atomicInteger.getAndDecrement()); // 获取的是当前值 并自减1 11
System.out.println(atomicInteger.compareAndSet(10, 20)); // true
数组类型原子类
AtomicIntegerArray
:整型数组原子类AtomicLongrArray
:长整型数组原子类AtomicReferenceArray
:用类型数组原子类
public static void main(String[] args) {
AtomicIntegerArray atomicIntegerArray = new AtomicIntegerArray(new int[1]);
for (int i = 0; i < atomicIntegerArray.length(); i++) {
System.out.println(atomicIntegerArray.get(i)); // 0
System.out.println(atomicIntegerArray.getAndSet(i, 10)); // 0
System.out.println(atomicIntegerArray.getAndIncrement(i)); // 10
System.out.println(atomicIntegerArray.getAndDecrement(i)); // 11
System.out.println(atomicIntegerArray.getAndAdd(i, 5)); // 10
System.out.println(atomicIntegerArray.compareAndSet(i, 15, 20)); // true
System.out.println(atomicIntegerArray.get(i)); // 20
}
}
引用类型原子类
-
AtomicReference
:引用类型原子类 -
AtomicStampedReference
:原子更新带有版本号的引用类型。该类将整数值与引用关联起来,可用于解决原子的更新数据和数据的版本号,可以解决使用 CAS 进行原子更新时可能出现的 ABA 问题。- 解决修改过几次
-
AtomicMarkableReference
:原子更新带有标记的引用类型。该类将 boolean 标记与引用关联起来- 解决是否修改过,它的定义就是将标记戳简化为true/false
Book book1 = new Book(1, "javaBook"); AtomicMarkableReference<Book> reference = new AtomicMarkableReference<>(book1, false); // Book{id=1, bookName='javaBook'} false System.out.println(reference.getReference() + "\t" + reference.isMarked()); Book book2 = new Book(2, "mysqlBook"); // true System.out.println(reference.compareAndSet(book1, book2, false, true)); // Book{id=2, bookName='mysqlBook'} true System.out.println(reference.getReference() + "\t" + reference.isMarked());
对象的属性修改原子类
AtomicIntegerFieldUpdater
:原子更新对象中int类型字段的值AtomicLongFieldUpdater
:原子更新对象中Long类型字段的值AtomicReferenceFieldUpdater
:原子更新对象中引用类型字段的值
使用目的:以一种线程安全的方式操作非线程安全对象内的某些字段
class BankAccount {
String bankName = "CCB";
String bankNo = "110100";
int money = 10;
// 此时考虑到线程安全问题 需要对money字段加锁 是对象锁 锁的是整个对象
public synchronized void transfer() {
money++;
}
}
使用要求:
- 更新的对象的属性必须使用 public volatile 修饰符
- 需要使用静态方法newUpdater()创建一个更新器
class BankAccount {
String bankName = "CCB";
String bankNo = "110100";
public volatile int money = 10;
AtomicIntegerFieldUpdater<BankAccount> newUpdater = AtomicIntegerFieldUpdater.newUpdater(BankAccount.class, "money");
public void transferMoney(BankAccount bankAccount) {
newUpdater.getAndIncrement(bankAccount);
}
}
public static void main(String[] args) throws InterruptedException {
CountDownLatch countDownLatch = new CountDownLatch(10);
BankAccount bankAccount = new BankAccount();
for (int i = 0; i < 10; i++) {
new Thread(() -> {
try {
for (int j = 1; j <= 1000; j++) {
bankAccount.transferMoney(bankAccount);
}
} finally {
countDownLatch.countDown();
}
}).start();
}
countDownLatch.await();
System.out.println(bankAccount.money);
}
原子操作增强类原理深度解析
LongAdder
:一个或多个变量 一起维持初始为零的总和 只能用来加法计算LongAccumulator
DoubleAdder
DoubleAccumulator
实现一个点赞计数器
public class Demo06 {
public static final int _1W = 10000;
public static final int THREAD_NUMBER = 50;
public static void main(String[] args) throws InterruptedException {
ClickNumber clickNumber = new ClickNumber();
CountDownLatch countDownLatch = new CountDownLatch(THREAD_NUMBER);
for (int i = 1; i <= THREAD_NUMBER; i++) {
new Thread(() -> {
try {
for (int j = 1; j <= 100 * _1W; j++) {
clickNumber.clickByLongAdder();
}
} finally {
countDownLatch.countDown();
}
}, String.valueOf(i)).start();
}
countDownLatch.await();
// 返回值 在没有并发更新的时候返回精确值 在存在并发更新的时候 不保证返回精确值
System.out.println(clickNumber.longAdder.sum());
}
}
class ClickNumber {
LongAdder longAdder = new LongAdder();
public void clickByLongAdder() {
longAdder.increment();
}
}
-
LongAdder 为什么这么快
-
如果使用 JDK8 推荐使用 LongAdder 比 AtomicLong 性能好(减少了乐观锁的重试次数)
-
LongAdder 是 Striped64 的子类
- Striped64 中的主要属性
// CPU 的数据 即 cells 数组的最大数量 static final int NCPU = Runtime.getRuntime().availableProcessors(); // cells 数组 长度为2的幂 transient volatile Cell[] cells; // 基础的 value 值 在并发较少的情况下 只累加该值 通过CAS更新 transient volatile long base; // 创建或扩容 Cells 数组的锁标记:0-无锁 1-其它线程获得了锁 transient volatile int cellsBusy;
-
LongAdder 的基本思路是分散热点 将value值分散到一个 Cell数组中 不同线程会命中到数组的不同槽中(多线程id进行hash 得到hash值) 各个线程只对自己槽中的那个值进行 CAS操作。
-
sum() 方法会将 Cell数组中的value和 base累加作为返回值
-
Java 对象内存布局和对象头
对象在堆内存的存储布局可以划分为三个部分:对象头 + 实例数据 + 对齐填充
- 对象头:对象标记 + 类元信息(16个字节 压缩后是12个字节 可以用4个字节存放类元信息)
- 对象标记:哈希码、GC标记、GC次数、同步锁标记、偏向锁持有者(8个字节)
- 类元信息:指向方法区类元信息的指针(8个字节)
- 实例数据:存放属性信息 包含父类的属性信息
- 对齐填充:虚拟机要求对象的起始地址必须是 8 的整数倍
对象标记(MarkWord)
4个bit存储 对象分代年龄 所以最大的年龄为 1111(二进制) = 15(十五)
用 -XX:MaxTenuringThreshold=15 设置 不能超过 15
synchronized 关键字
- 为什么任何一个对象都可以成为一个锁
Java 中的每个类都继承与 Object 类 而每个 Object 在 JVM 内部都有一个 native的C++对象进行对应。
线程在获取锁的时候 实际上就是获得一个监视器对象(monitor) monitor 可以认为是一个同步对象 所有的Java 对象是天生携带 monitor。
- ObjectMonitor 的几个属性:
属性名 | 描述 |
---|---|
_owner | 指向持有 ObjectMonitor 对象的线程 |
_WaitSet | wait 状态的线程队列 |
_EntryList | block 状态的线程队列 |
_recursions | 锁的重入次数 |
_count | 该线程获取锁的次数 count=0 的时表示当前对象没有被占用 |
- Java 的线程是映射到操作系统原生线程之上的 如果阻塞或唤醒一个线程 就需要操作系统的介入。需要在用户态和内核态之间切换 这种切换会消耗大量的系统资源 因为用户态和内核态都有各自专用的内存空间。早期 JDK 版本(JDK5以前)中 synchronized 属于重量级锁 依赖于底层操作系统的 Mutex Lock(系统互斥量) 来实现的 状态切换需要耗费 CPU 的时间。JDK 6 以后为了减少获得锁和释放锁带来的性能消耗 引入了偏向锁和轻量级锁。
偏向锁(101)
当线程 A 第一次竞争到锁的时候 会修改 对象头中 MarkWord 的偏向锁的线程ID 和 偏向锁标记(由0改为1)如果没有其它线程竞争 那么偏向锁永远不需要进行同步 也就是偏向锁在资源没有竞争的情况下消除了同步语句
- 如果偏向线程和当前线程相等 直接进入同步块
- 如果偏向线程和当前线程不等 使用 CAS 来替代 MarkWord 中的偏向线程ID
- 替换成功 表示之前的线程已经不存在了 当前线程变成了该对象 MarkWord 中的新偏向线程
- 替换失败 需要升级为轻量级锁
- 替换逻辑:
- 当有线程来竞争的时候 会使用 CAS 来更新对象头中的 偏向线程ID
- 如果更新失败 会在全局安全点撤销偏向锁 同时检查持有偏向锁的线程是否还在执行
- 如果偏向锁的线程还在执行 来争抢的线程会被取消掉 并出现锁升级
JDK15 以后逐步放弃偏向锁 维护成本高
轻量级锁(000)
多个线程竞争锁 但同一时间只有一个线程竞争 即不存在太激烈的竞争 也没有线程阻塞 本质上是 CAS 自旋
- 轻量级锁如何加锁
- JVM 会为每个线程在当前线程的栈帧中创建存储锁记录的空间 官方成为 Displaced Mark Word
- 若一个线程发现是轻量级锁 会把锁的 Mark Word 复制到自己的 Displaced Mark Word
- 然后尝试使用 CAS 将锁的 Mark Word 替换为指向锁记录的指针
- 如果成功当前线程获得锁 如果失败说明 锁的 Mark Word 已经替换为其它线程的锁记录 当前线程通过自旋来获得锁
- 轻量级锁的释放
- 使用 CAS 操作将 Displaced Mark Word 复制回 Mark Word
- 如果没有竞争操作这个复制就会成功
- 如果有其它线程因为自旋过多导致锁升级为重量级锁 那么CAS就会失败 此时会释放锁 并唤醒阻塞的线程
锁升级后 hashcode 去哪了
- 当一个对象已经计算过 哈希码后就不能进入偏向锁状态了
- 当一个对象正处于偏向锁状态 又收到了计算哈希码的请求 偏向状态会立即撤销
Java 内存模型
JMM(Java 内存模型 Java Memory Model)本身是一种抽象的概念并不真是存在 它仅仅描述一组约定和规范。
通过这组规范定义了程序中 尤其是多线程中 各个变量的读写访问方式
并确定了一个线程对共享变量的写入 以及如何变成对另一个线程可见
关键技术点都围绕多线程的 原子性、可见性和有序性展开。
-
原子性:指一个操作是不可被打断的 即使在多线程环境下 操作也不能被其他线程打扰
-
可见性:每个线程都有自己的工作内存 线程自己的工作内存保存了该线程使用到的变量的主内存的副本 线程对变量的所有操作(读取、赋值等)都应该在自己的工作内存中进行 而不能直接操作主内存 不同线程之间也不能访问对方工作内存中的变量 线程件变量的值传递都需要通过主内存来完成
为什么要有主内存和工作内存
屏蔽各个硬件平台和操作系统的内存访问差异以实现让Java程序再各种平台下都能达到一致性的内存访问效果。
-
有序性:编译器和处理器通常会对指令序列进行重新排序。Java 规范规定 JVM 线程内部维持顺序化语义 即只要程序的最终结果与它顺序执行的结果相同 那么指令的执行顺序和代码的顺序可以不一致 此过程叫 指令重排
happens-before
我们没有时时、处处、次次,添加 volatile 和 synchronized 来完成程序,这是因为 Java 语言中 JMM 原则下,有一个"先行发生"(happens-before)的原则限制和规矩。
- 如果一个操作happens-before另一个操作,那么第一个操作的执行结果将对第二个操作可见,而且第一个操作的执行顺序排在第二个操作之前。
- 如果两个操作之间存在happens-before关系,并不意味着一定要按照happens-before原则制定的顺序来执行。如果重排之后的执行结果与按照happens-before关系来执行的结果一致,那么这种重排序并不非法。
8条规则:
- 一个线程内,按照代码的顺序,写在前面的操作先行发生于写在后面的操作。
- 一个unLock操作先行发生于后面对同一个锁的lock操作
- 对一个 volatile 变量的写操作先行发生于后面对这个变量的读操作(volatile 的可见性)
- 如果操作A先行发生于操作B,而操作B又先行发生于操作C,则可以得出操作A先行发生于操作C
- Thread对象的start()方法先行发生于此线程的每一个动作
- 一个对象的初始化完成(构造函数执行结束)先行发生于它的finalize()
- 线程中的所有操作都优先发生于对此线程的终止检测
- 线程中断规则
volatile
被 volatile 修饰的变量保证了 有序性和可见性 不能保证原子性。
当写一个 volatile 变量的时候 会立即将本地内存中的值刷入主内存
当读一个 volatile 变量的时候 会抛弃本地内存中的值 回到主内存中读取最新的值
- volatile 为什么能保证有序性和可见性:内存屏障。
内存屏障
一类同步屏障指令 CPU或编译器在对内存随机访问的操作中的一个同步点 使得次点之前的所有读写操作都执行后才能执行此点之后的操作 避免了代码的重排。
内存屏障其实就是 JVM 的指令。Java 内存模型的重排规则会要求 Java 编译器在生成 JVM 指令时插入特定的内存屏障指令。
- 内存屏障之前的所有写操作都要回写到主内存
- 内存屏障之后的所有读操作都能获得内存屏障之前的所有写操作的最新结果
内存屏障分类
- 读屏障(Load Barrier):在读指令之前插入读屏障 让工作内存的缓存数据失效 重新回到主内存中获取最新数据
- 写屏障(Store Barrier):在写指令之后插入写屏障 强制把工作内存的数据刷回到主内存中
线程中断机制
- 一个线程不应该由其它线程来强制中断或停止 而是应该由线程自己自行停止
- Java 中没有办法立即停止一条线程 Java 提供了一种用于停止线程的协商机制——中断。即中断标识协商机制
- 中断只是一种协商机制 Java 没有给中断增加任何语法 中断的过程完全需要程序员自行实现。
- 若要中断一个线程 需要手动调用线程的 interrupt 方法 该方法也仅仅将该线程对象的中断标识设置为true
- 接着需要程序员写代码不断检测当前线程的标识位
中断的相关API方法
public void interrupt()
:仅仅设置线程的中断状态为true 发起一个协商而不会立刻停止线程public static boolean interrupted()
:判断线程是否被中断并清除当前中断状态- 返回当前线程的中断状态 测试当前线程是否已被中断
- 将当前线程的中断状态清零并重新设置为false 清除线程的中断状态
public boolean isInterrupted()
:判断当前线程是否被中断
中断运行中的线程
public static void main(String[] args){
Thread t1 = new Thread(() -> {
while (true) {
// 判断中断标记是否为true
if (Thread.currentThread().isInterrupted()) {
System.out.println(Thread.currentThread().getName() + "停止");
break;
}
System.out.println("线程1正在运行...");
}
}, "线程1");
t1.start();
// 通过线程2 将通知线程1 设置中断标记为true
new Thread(t1::interrupt, "线程2").start();
}
- 如果线程处于阻塞状态(例如:sleep/wait/join等状态)在被别的线程调用 interrupt 方法 那么线程将立即退出阻塞状态(interrupt 状态也被清除)并抛出一个 InterruptedException 异常。
public static void main(String[] args){
Thread t1 = new Thread(() -> {
while (true) {
// 判断中断标记是否为true
if (Thread.currentThread().isInterrupted()) {
System.out.println(Thread.currentThread().getName() + "停止");
break;
}
System.out.println("线程1正在运行...");
try {
TimeUnit.SECONDS.sleep(10);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}, "线程1");
t1.start();
// 通过线程2 将通知线程1 设置中断标记为true
new Thread(t1::interrupt, "线程2").start();
}
// Exception in thread "线程1" java.lang.RuntimeException: java.lang.InterruptedException: sleep interrupted
ThreadLocal
- ThreadLocal 提供线程局部变量 这些变量与正常的变量不同 每个线程在访问 ThreadLocal 实例的时候都有自己独立的变量副本 目的是希望将状态和线程关联起来
- 每一个线程都有自己本地的专属副本变量 从而避免了线程安全问题
public class Demo07 {
public static void main(String[] args) {
House house = new House();
// 五个线程 每个线程都有一份 saleVolume
for (int i = 1; i <= 5; i++) {
new Thread(() -> {
int size = new Random().nextInt(5) + 1;
for (int j = 1; j <= size; j++) {
house.saleVolumeByThreadLocal();
}
System.out.println(Thread.currentThread().getName() + "\t" + "号销售卖出:" + house.saleVolume.get());
}).start();
}
}
}
class House {
// 设置当前线程的 saleVolume 为 0
ThreadLocal<Integer> saleVolume = ThreadLocal.withInitial(() -> 0);
// 将本地线程内的 saleVolume + 1
public void saleVolumeByThreadLocal() {
saleVolume.set(1 + saleVolume.get());
}
}
Thread、ThreadLocal、ThreadLocalMap关系
ThreadLocal.ThreadLocalMap threadLocals = null;
- ThreadLocalMap 是以 ThreadLocal 实例为 key 任意对象为 value 的 Entry 对象
ThreadLocal 内存泄露问题
- 在使用线程池的时候线程会复用 每一个线程操作变量完成后需要
house.saleVolume.remove();
static class ThreadLocalMap {
// 为什么此处要将 ThreadLocal 设置为弱引用
static class Entry extends WeakReference<ThreadLocal<?>> {
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
}
- 弱引用在 GC回收的时候 无论 JVM内存是否充足都会被回收
ThreadLocal<String> threadLocal = new ThreadLocal<>();
threadLocal.set("demo");
threadLocal.get();
- 栈帧中局部变量表的 threadLocal 指向堆中的 ThreadLocal
- ThreadLocalMap 中的 key 还引用着堆中的 ThreadLocal
- 如果这个key是强引用 会导致 堆中的 ThreadLocal 不能被GC回收
AQS
AbstractQueuedSynchronizer:抽象队列同步器
- 加锁就会导致阻塞 有阻塞就需要排队 实现排队就必然需要排队
// 表示同步状态 0-表示没有阻塞 1-表示阻塞
private volatile int state;
// FIFO 双向链表队列
static final class Node {
// 定义线程的等待状态
volatile int waitStatus;
}
private transient volatile Node head;
private transient volatile Node tail;
// 阻塞线程
private final boolean parkAndCheckInterrupt() {
LockSupport.park(this);
return Thread.interrupted();
}
private void unparkSuccessor(Node node) {
int ws = node.waitStatus;
if (ws < 0)
compareAndSetWaitStatus(node, ws, 0);
Node s = node.next;
if (s == null || s.waitStatus > 0) {
s = null;
for (Node t = tail; t != null && t != node; t = t.prev)
if (t.waitStatus <= 0)
s = t;
}
if (s != null)
// 唤醒线程
LockSupport.unpark(s.thread);
}
ReentrantLock
对比于 synchronized 具备以下特点:
- 可中断
public static void main(String[] args) throws InterruptedException {
Thread thread = new Thread(() -> {
System.out.println(Thread.currentThread().getName() + "启动...");
// lock.lock();
try {
// 没有竞争就会获取锁 有竞争就进入阻塞队列等待,但可以被打断
lock.lockInterruptibly();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
try {
System.out.println(Thread.currentThread().getName() + "获得了锁...");
} finally {
lock.unlock();
}
},"线程1");
lock.lock(); // 主线程拿到锁
System.out.println(Thread.currentThread().getName() + "获得了锁...");
thread.start(); // 线程1执行 想要去拿锁 此时就有竞争 会等着
try {
TimeUnit.SECONDS.sleep(1);
// 1秒后 主线程中对 线程1 进行打断
System.out.println(Thread.currentThread().getName() + "进行打断...");
thread.interrupt();
}finally {
lock.unlock();
}
}
注意:如果是不可中断模式,那么即使使用了 interrupt 也不会让等待中断
- 可设置超时时间
public class Demo02 {
static ReentrantLock lock = new ReentrantLock();
public static void main(String[] args) {
Thread thread = new Thread(() -> {
System.out.println(Thread.currentThread().getName() + "启动...");
try {
// 此处三秒后成功获得了锁 如果设置的超时时间很短 或者没有设置超时时间 则立马返回 拿不到锁
if (!lock.tryLock(3, TimeUnit.SECONDS)) {
System.out.println(Thread.currentThread().getName() + "获取立刻失败...");
return;
} else {
System.out.println(Thread.currentThread().getName() + "获取成功...");
}
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
try {
System.out.println(Thread.currentThread().getName() + "获得了锁...");
} finally {
lock.unlock();
}
}, "线程1");
new Thread(() -> {
lock.lock();
try {
thread.start();
System.out.println(Thread.currentThread().getName() + "获得了锁...");
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
throw new RuntimeException(e);
} finally {
lock.unlock();
}
}, "线程2").start();
}
}
- 可设置为公平锁(synchronized 是非公平的)
ReentrantLock lock = new ReentrantLock(true);
- 支持多个条件变量
- synchronized 不满足条件的线程都在一间休息室
- ReentrantLock 支持多间休息室
public class Demo03 {
// 锁
static ReentrantLock lock = new ReentrantLock();
// 等烟的休息室
static Condition waitCigaretteQueue = lock.newCondition();
// 等早餐的休息室
static Condition waitBreakfastQueue = lock.newCondition();
public static void main(String[] args) throws InterruptedException {
new Thread(() -> {
try {
lock.lock();
// 当前线程进入烟的休息室
waitCigaretteQueue.await();
System.out.println("等到了它的烟");
} catch (InterruptedException e) {
throw new RuntimeException(e);
} finally {
lock.unlock();
}
}).start();
new Thread(() -> {
try {
lock.lock();
// 当前线程进入早餐的休息室
waitBreakfastQueue.await();
System.out.println("等到了它的早餐");
} catch (InterruptedException e) {
throw new RuntimeException(e);
} finally {
lock.unlock();
}
}).start();
lock.lock();
try {
waitCigaretteQueue.signal();
}finally {
lock.unlock();
}
lock.lock();
try {
waitBreakfastQueue.signal();
}finally {
lock.unlock();
}
}
}
-
和 synchronized 一样 都支持可重入
可重入:指同一个线程如果首次获得了这把锁 那么它就是这把锁的拥有者 它有权利再次使用这把锁 如果是不可重入的锁 那么第二次使用这把锁的时候会把自己锁住
ReentrantReadWriteLock
-
一个资源能够被多个读线程访问 或者被一个写线程访问 但不能同时存在读写线程
- 只允许 读-读 共存 读-写/写-读 依然是互斥的
ReentrantReadWriteLock readWriteLock = new ReentrantReadWriteLock(); ReentrantReadWriteLock.ReadLock readLock = readWriteLock.readLock(); ReentrantReadWriteLock.WriteLock writeLock = readWriteLock.writeLock(); public Integer read() { System.out.println("获取读锁..."); readLock.lock(); try { System.out.println("读取数据..."); return 10; } finally { readLock.unlock(); } } public void write() { System.out.println("获取写锁..."); writeLock.lock(); try { System.out.println("写入数据..."); } finally { writeLock.unlock(); } } public static void main(String[] args) { Demo04 demo04 = new Demo04(); // 读-读 /** * 获取读锁... * 获取读锁... * 读取数据... * 读取数据... */ new Thread(demo04::read, "t1").start(); new Thread(demo04::read, "t2").start(); // 读-写 /** * 获取读锁... * 读取数据... * 获取写锁... * 写入数据... */ new Thread(demo04::read, "t1").start(); new Thread(demo04::write, "t2").start(); }
-
在有读锁的情况下去获取写锁 会使写锁永久等待
ReentrantReadWriteLock readWriteLock = new ReentrantReadWriteLock(); ReentrantReadWriteLock.ReadLock readLock = readWriteLock.readLock(); ReentrantReadWriteLock.WriteLock writeLock = readWriteLock.writeLock(); public void read_write() { System.out.println("获取读锁..."); readLock.lock(); try { System.out.println("获取写锁..."); writeLock.lock(); try { System.out.println("读锁里获取写锁"); }finally { writeLock.unlock(); } } finally { readLock.unlock(); } } public static void main(String[] args) { Demo05 demo05 = new Demo05(); demo05.read_write(); }
-
在有写锁的情况下去获取读锁:锁降级
- 即如果一个线程持有了写锁 在没有释放写锁的情况下 它还可以继续获得读锁
ReentrantReadWriteLock readWriteLock = new ReentrantReadWriteLock(); ReentrantReadWriteLock.ReadLock readLock = readWriteLock.readLock(); ReentrantReadWriteLock.WriteLock writeLock = readWriteLock.writeLock(); public void write_read() { System.out.println("获取写锁..."); writeLock.lock(); try { System.out.println("获取读锁..."); readLock.lock(); try { System.out.println("写锁里获取读锁"); }finally { readLock.unlock(); } } finally { writeLock.unlock(); } } public static void main(String[] args) { Demo06 demo06 = new Demo06(); demo06.write_read(); }
StampedLock
邮戳锁 是在 JDK8 中新增的读写锁 也是对 ReentrantReadWriteLock 的优化
- ReentrantReadWriteLock 实现了读写分离 但是一旦读操作比较多 写操作想要获取锁 就变得比较困难 因为当读锁一直存在 就无法获得写锁 会出现锁饥饿问题
- 为了解决锁饥饿问题
- 可以使用"公平"策略 但是会牺牲吞吐量
- StampedLock 用乐观读锁的方式 —— 当获得写锁的时候不会被阻塞 在获取乐观读锁后 需要对结果进行校验
- 所有获取锁的方法,都返回一个邮戳,stamp为零表示失败,其余都表示成功
- 所有释放锁的方法,都需要一个邮戳,这个stamp必须是和成功获取锁时得到的stamp一致
- StampedLock 是不可重入的
传统的读-写锁模式:
public class StampedLockDemo {
// 写线程没有修改成功,读锁时候写锁无法介入,传统的读写互斥
public static void main(String[] args) throws InterruptedException {
StampedLockDemo resource = new StampedLockDemo();
new Thread(resource::read, "readThread").start();
TimeUnit.SECONDS.sleep(1);
new Thread(() -> {
System.out.println(Thread.currentThread().getName() + " come in");
resource.write();
}, "writeThread").start();
}
static int number = 37;
static StampedLock stampedLock = new StampedLock();
public void read() {
// 传统读模式
long stamp = stampedLock.readLock();
System.out.println(Thread.currentThread().getName() + "读线程准备读取...");
for (int i = 0; i < 4; i++) {
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "正在读取中...");
}
try {
int result = number;
System.out.println(Thread.currentThread().getName() + "获得成员变量值result: " + result);
} finally {
stampedLock.unlockRead(stamp);
}
}
public void write() {
// 获取写锁的邮戳
long stamp = stampedLock.writeLock();
System.out.println(Thread.currentThread().getName() + "写线程准备修改...");
try {
number = number + 13;
} finally {
// 释放写锁的时候 需要传入邮戳 和获取的邮戳需要一致才能成功
stampedLock.unlockWrite(stamp);
}
System.out.println(Thread.currentThread().getName() + "写线程结束修改");
}
}
乐观的读锁模式
public class StampedLockDemo01 {
static int number = 37;
static StampedLock stampedLock = new StampedLock();
public static void main(String[] args) throws InterruptedException {
StampedLockDemo01 resource = new StampedLockDemo01();
new Thread(resource::read, "readThread").start();
TimeUnit.SECONDS.sleep(1);
new Thread(() -> {
System.out.println(Thread.currentThread().getName() + " come in");
resource.write();
}, "writeThread").start();
}
public void write() {
// 获取写锁的邮戳
long stamp = stampedLock.writeLock();
System.out.println(Thread.currentThread().getName() + "写线程准备修改...");
try {
number = number + 13;
} finally {
// 释放写锁的时候 需要传入邮戳 和获取的邮戳需要一致才能成功
stampedLock.unlockWrite(stamp);
}
System.out.println(Thread.currentThread().getName() + "写线程结束修改");
}
public void read() {
// 乐观读模式
long stamp = stampedLock.tryOptimisticRead();
int result = number;
System.out.println(Thread.currentThread().getName() + "读线程准备读取...");
for (int i = 0; i < 4; i++) {
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "正在读取中...");
}
// 校验一下 stamp 如果和获取的时候 不相同说明有人修改了
if (!stampedLock.validate(stamp)) {
System.out.println("有人修改...有其它线程修改了...");
stamp = stampedLock.readLock();
try {
System.out.println("从乐观读升级为悲观读");
result = number;
System.out.println("重新悲观读后result:" + result);
} finally {
stampedLock.unlockRead(stamp);
}
}
System.out.println(Thread.currentThread().getName() + "\t" + "finally value: " + result);
}
}
LockSupport
-
wait / notify
- _owner 属性中的线程发现条件不满足 调用 wait 方法 会进入 _WaitSet 变为 WAITING 状态
- WAITING 和 BLOCKED 状态的线程都会处于阻塞状态 不占用 CPU时间片
- BLOCKED 的线程会在 _owner 线程释放锁的时候被唤醒
- WAITING 的线程会在 _owner 线程调用 notify 或 notifyAll 时被唤醒 被唤醒不意味立刻获得锁 需要进入 _EntryList 重新竞争
wait / notify 方法必须在同步代码块中 不然会报 java.lang.IllegalMonitorStateException
public class Demo02 { final static Object obj = new Object(); public static void main(String[] args) throws InterruptedException { new Thread(() -> { synchronized (obj) { System.out.println("线程1执行..."); try { obj.wait(); } catch (InterruptedException e) { throw new RuntimeException(e); } System.out.println("线程1的其它代码..."); } }).start(); new Thread(() -> { synchronized (obj) { System.out.println("线程2执行..."); try { obj.wait(); } catch (InterruptedException e) { throw new RuntimeException(e); } System.out.println("线程2的其它代码..."); } }).start(); TimeUnit.SECONDS.sleep(2); synchronized (obj) { // obj.notify(); // 唤醒obj上一个线程 obj.notifyAll(); // 唤醒obj上所有等待线程 } } }
-
park/unpark
为什么需要 LockSupport
上述的 wait/notify 和 await/signal 都必须在锁块中
必须先等待 后唤醒
- LockSupport 类使用了一种名为 Permit(许可证) 的概念来做 阻塞和唤醒 线程的功能
- 每个线程都只能由一个许可证 许可证最大上限为1
- 阻塞:没有许可证的线程不能放行 直到线程被颁发许可证才会被唤醒
- 唤醒:调用 unpark(Thread thread) 方法后 会给线程颁发许可证
public static void main(String[] args) throws InterruptedException { Thread thread1 = new Thread(() -> { System.out.println(Thread.currentThread().getName() + "开始执行..."); // 被阻塞 需要许可证 LockSupport.park(); // 如果这里再调用 就还需要一个许可证 // LockSupport.park(); System.out.println(Thread.currentThread().getName() + "被唤醒"); }, "线程1"); thread1.start(); TimeUnit.SECONDS.sleep(1); Thread thread2 = new Thread(() -> { System.out.println(Thread.currentThread().getName() + "开始执行..."); // 给线程1 颁发许可证 LockSupport.unpark(thread1); }, "线程2"); thread2.start(); }
Semaphore
信号量 用来限制能同时访问共享资源的线程上限
public static void main(String[] args) {
// 最多三个线程
Semaphore semaphore = new Semaphore(3);
// 创建10个线程来操作共享资源
for (int i = 0; i < 10; i++) {
new Thread(() -> {
try {
// 获取许可
try {
semaphore.acquire();
System.out.println(Thread.currentThread().getName() + " running...");
// 每个线程需要三秒执行
TimeUnit.SECONDS.sleep(3);
System.out.println(Thread.currentThread().getName() + " ending...");
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}finally {
// 释放许可
semaphore.release();
}
}).start();
}
}
CountdownLatch
用来进行线程同步协作 等待所有线程完成倒计时。
构造函数来初始化等待计数器
await() 方法等待计数器归零
countDown() 方法让计数器减一
public static void main(String[] args) throws InterruptedException {
// 三个计数器
CountDownLatch countDownLatch = new CountDownLatch(3);
new Thread(() -> {
System.out.println("begin...");
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
countDownLatch.countDown();
System.out.println("end...");
}).start();
new Thread(() -> {
System.out.println("begin...");
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
countDownLatch.countDown();
System.out.println("end...");
}).start();
new Thread(() -> {
System.out.println("begin...");
try {
TimeUnit.SECONDS.sleep(3);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
countDownLatch.countDown();
System.out.println("end...");
}).start();
System.out.println("waiting...");
countDownLatch.await();
System.out.println("wait end...");
}
/**
begin...
begin...
waiting...
begin...
end...
end...
end...
wait end...
**/