Time: 2024-12-22 Sunday 02:08:02
Author: Jackasher
JUC并发编程
Synchronized原理
故事角色
老王 - JVM
小南 - 线程
小女 - 线程
房间 - 对象
房间门上 - 防盗锁 - Monitor
房间门上 - 小南书包 - 轻量级锁
房间门上 - 刻上小南大名 - 偏向锁
批量重刻名 - 一个类的偏向锁撤销到达 20 阈值
不能刻名字 - 批量撤销该类对象的偏向锁,设置该类不可偏向
小南要使用房间保证计算不被其它人干扰(原子性),最初,他用的是防盗锁,当上下文切换时,锁住门。这样,
即使他离开了,别人也进不了门,他的工作就是安全的。
但是,很多情况下没人跟他来竞争房间的使用权。小女是要用房间,但使用的时间上是错开的,小南白天用,小女
晚上用。每次上锁太麻烦了,有没有更简单的办法呢?
小南和小女商量了一下,约定不锁门了,而是谁用房间,谁把自己的书包挂在门口,但他们的书包样式都一样,因
此每次进门前得翻翻书包,看课本是谁的,如果是自己的,那么就可以进门,这样省的上锁解锁了。万一书包不是
自己的,那么就在门外等,并通知对方下次用锁门的方式。
后来,小女回老家了,很长一段时间都不会用这个房间。小南每次还是挂书包,翻书包,虽然比锁门省事了,但仍
然觉得麻烦。
于是,小南干脆在门上刻上了自己的名字:【小南专属房间,其它人勿用】,下次来用房间时,只要名字还在,那
么说明没人打扰,还是可以安全地使用房间。如果这期间有其它人要用这个房间,那么由使用者将小南刻的名字擦
掉,升级为挂书包的方式。
同学们都放假回老家了,小南就膨胀了,在 20 个房间刻上了自己的名字,想进哪个进哪个。后来他自己放假回老
家了,这时小女回来了(她也要用这些房间),结果就是得一个个地擦掉小南刻的名字,升级为挂书包的方式。老
王觉得这成本有点高,提出了一种批量重刻名的方法,他让小女不用挂书包了,可以直接在门上刻上自己的名字
后来,刻名的现象越来越频繁,老王受不了了:算了,这些房间都不能刻名了,只能挂书包
栈帧

Monitor

MarkWord

轻量级锁

偏向锁
偏向锁前面的是线程id,而轻量级锁前面是锁记录地址,重量级锁前面是monitor锁地址,偏向锁加锁的方式就是直接表明线程id

为什么使用hashcode会禁用偏向锁
因为会把位用于方hashcode,没有位置再放54位的线程id了
那为什么不会禁用其他的锁呢?
因为轻量级锁会把hashcode存在栈帧里面,而重量级锁会存在monitor里面
偏向锁撤销
如果有冲突,会被升级为轻量级锁

锁对性能的影响
忽略掉JIT的优化后,竟然差了17倍!

“Guarded Suspension”(保护性暂停)设计模式
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| package day01;
public class GuardObject { private Object response;
public synchronized Object get() throws InterruptedException { if (response == null) { wait(); } return response; }
public synchronized void complete(Object object) { this.response = object; notifyAll(); }
}
|
虚假唤醒
假唤醒(Spurious Wakeup)是多线程编程中使用 wait 和 notify 时可能出现的现象。即使没有调用 notify 或 notifyAll,线程可能也会被唤醒。这种行为是操作系统和 JVM 的实现细节所导致的。
原因:操作系统和 JVM 的实现细节
1.信号丢失或竞争
在多线程环境中,wait 和 notify 是基于底层操作系统的同步原语实现的。在某些情况下,由于线程调度或者信号竞争,可能会发生信号丢失或额外的唤醒。
2.硬件中断和系统优化
某些硬件中断或系统优化(比如节省唤醒信号的成本)会触发线程错误地离开等待状态。
3.POSIX 标准
POSIX 线程标准允许线程被无故唤醒,Java 的 wait 和 notify 实现也继承了这一行为。因此,Java 开发者需要主动编写防御性代码来处理虚假唤醒。
4.其他线程操作影响
某些情况下,其他线程可能在没有显式调用 notify 的情况下,导致等待线程恢复运行(例如 JVM 的垃圾回收暂停和恢复调度)。
为了应对虚假唤醒,最佳实践是将 wait 放在循环(while)中,而不是条件判断(if)中。
如何让线程循环打印abc
线程分别打印,abc,那么每个线程知道自己什么时候打,例如b需要知道a打印完后给一个信号,这个信号可以设为flag,每个线程需要的flag不一样,先给a一个flag=1开始,打印后,设为2传给b线程,
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40
| package wait_ify;
public class WaitNotify { private int flag;
public int getFlag() { return flag; }
public void setFlag(int flag) { this.flag = flag; }
public void print(String printContent, int currentFlag, int nextFlag) {
while (true) { synchronized (this) { if (flag == currentFlag) { System.out.println(printContent); flag = nextFlag; this.notifyAll(); } else { try { this.wait(); } catch (InterruptedException e) { e.printStackTrace(); } } }
} }
}
|
指令重排
这段代码是有问题的,首先我们要弄清楚INSTANCE = new Singleton()在java虚拟机里面是如何操作的,通常来说,是先new dup init putstatic 但是jvm可能颠倒init和putstatic的顺序,先给地址后初始化,这个时候如果有第二个线程进来,就会判断错误
volatile
1. 读屏障(Load Barrier)
•在读取 volatile 变量前,会插入一条指令,确保在当前线程从主内存加载该变量值之前,清空当前线程工作内存中缓存的共享变量。这保证了读取的值是最新的。
2. 写屏障(Store Barrier)
•在写入 volatile 变量后,会插入一条指令,确保将该变量的新值立即刷新到主内存,并让其他线程的缓存失效,保证其他线程可以看到最新值。
CAS(CompareAndSet)
CAS提供了不上锁的方法实现线程安全,原理是利用操作系统的原子操作,比较和赋值为同时操作,CAS的底层其实就是Volatile
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| package cas;
import java.util.concurrent.atomic.AtomicInteger;
public class Cas { public static void main(String[] args) { AtomicInteger balance = new AtomicInteger(100); int i = balance.get(); balance.compareAndSet(i, 110); System.out.println(balance); } }
|
Integer的线程不安全
当把int换成Integer后,加减就会变成线程不安全的 balance -= amount;
,
•int 是 Java 的基本数据类型,操作 int 是原子性的(单一操作不会被线程中断,如简单的加减赋值)。
•Integer 是 Java 的引用类型,不可变对象(immutable)。每次对 Integer 的值进行操作,实际上是创建了一个新的 Integer 对象,而不是修改原来的值。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38
| package cas2;
public class Account { private Integer balance;
public Integer getBalance() { return balance; }
public void deposit(int amount) { balance += amount; }
public void withdraw(int amount) { balance -= amount; }
public void setBalance(int balance) { this.balance = balance; }
@Override public String toString() { return "Account{" + "balance=" + balance + '}'; } }
|
手写AtomicInteger
核心就是开始操作前都要检查值对不对,对了再操作,CAS
来完成这个比较值和赋值的操作
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34
| package cas;
import java.util.concurrent.atomic.AtomicInteger;
public class Atomic { public static void main(String[] args) { AtomicInteger atomicInteger = new AtomicInteger(100); int i = atomicInteger.updateAndGet(oldValue -> oldValue + 100); System.out.println(i); System.out.println(atomicInteger.get());
updateAndGet(atomicInteger); System.out.println(atomicInteger.get()); }
public static void updateAndGet(AtomicInteger atomicInteger) { int expectedValue = atomicInteger.get(); int newValue = expectedValue + 100; while (true) { if (atomicInteger.compareAndSet(expectedValue, newValue)) { break; } }
} }
|
Consumer和Supplier
黑马突然搞出这两个,我也很懵逼,其实就是java8的lambda表达式,function函数
Supplier实例
1 2 3 4 5 6 7 8 9 10 11 12
| import java.util.function.Supplier;
public class SupplierExample { public static void main(String[] args) { Supplier<Double> randomSupplier = () -> Math.random();
System.out.println("随机数1: " + randomSupplier.get()); System.out.println("随机数2: " + randomSupplier.get()); } }
|
Consumer实例
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| import java.util.function.Consumer; import java.util.Arrays; import java.util.List;
public class ConsumerExample { public static void main(String[] args) { Consumer<String> printConsumer = (str) -> System.out.println("处理数据: " + str);
printConsumer.accept("Hello, Consumer!");
List<String> items = Arrays.asList("A", "B", "C"); items.forEach(printConsumer); } }
|
手写线程池
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46
| package thread_pool;
public class WorkerThread extends Thread { private Runnable task; private BlockingQueue<Runnable> blockingQueue; private ThreadPool threadPool; private boolean isRunning = true;
public WorkerThread(Runnable task, BlockingQueue<Runnable> blockingQueue) { this.task = task; this.blockingQueue = blockingQueue;
}
@Override public void run() { while (isRunning) { try { if (task != null) { task.run(); } if((task = blockingQueue.take()) != null) { task.run(); }
} catch (Exception e) { e.printStackTrace(); } finally { task = null; } }
System.out.println("准备退出"); synchronized (threadPool.getWorkers()) { System.out.println("线程:" + Thread.currentThread().getName() + " 退出"); threadPool.getWorkers().remove(this); } } }
|
我前面发现了这个地方,无法执行,会阻塞,原来是因为队列没有释放锁
1 2 3
| if((task = blockingQueue.take()) != null) { task.run(); }
|