相关笔记:www.zgtsky.top
临界资源:一次仅允许一个进程使用的资源成为临界资源
临界区:访问临界资源的代码块
竞态条件:多个线程在临界区内执行,由于代码的执行序列不同而导致结果无法预测,称之为发生了竞态条件
一个程序运行多个线程是没有问题,多个线程读共享资源也没有问题,在多个线程对共享资源读写操作时发生指令交错,就会出现问题
为了避免临界区的竞态条件发生(解决线程安全问题):
管程(monitor):由局部于自己的若干公共变量和所有访问这些公共变量的过程所组成的软件模块,保证同一时刻只有一个进程在管程内活动,即管程内定义的操作在同一时刻只被一个进程调用(由编译器实现)
synchronized:对象锁,保证了临界区内代码的原子性,采用互斥的方式让同一时刻至多只有一个线程能持有对象锁,其它线程获取这个对象锁时会阻塞,保证拥有锁的线程可以安全的执行临界区内的代码,不用担心线程上下文切换
互斥和同步都可以采用 synchronized 关键字来完成,区别:
性能:
成员变量和静态变量:
局部变量:
常见线程安全类:String、Integer、StringBuffer、Random、Vector、Hashtable、java.util.concurrent 包
线程安全的是指,多个线程调用它们同一个实例的某个方法时,是线程安全的
每个方法是原子的,但多个方法的组合不是原子的,只能保证调用的方法内部安全:
Hashtable table = new Hashtable();
// 线程1,线程2
if(table.get("key") == null) {
table.put("key", value);
}
无状态类线程安全,就是没有成员变量的类
不可变类线程安全:String、Integer 等都是不可变类,内部的状态不可以改变,所以方法是线程安全
replace 等方法底层是新建一个对象,复制过去
Map<String,Object> map = new HashMap<>(); // 线程不安全
String S1 = "..."; // 线程安全
final String S2 = "..."; // 线程安全
Date D1 = new Date(); // 线程不安全
final Date D2 = new Date(); // 线程不安全,final让D2引用的对象不能变,但对象的内容可以变
抽象方法如果有参数,被重写后行为不确定可能造成线程不安全,被称之为外星方法:public abstract foo(Student s);
锁对象:理论上可以是任意的唯一对象
synchronized 是可重入、不公平的重量级锁
原则上:
同步代码块格式:
synchronized(锁对象){
// 访问共享资源的核心代码
}
实例:
public class demo {
static int counter = 0;
//static修饰,则元素是属于类本身的,不属于对象 ,与类一起加载一次,只有一个
static final Object room = new Object();
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> {
for (int i = 0; i < 5000; i++) {
synchronized (room) {
counter++;
}
}
}, "t1");
Thread t2 = new Thread(() -> {
for (int i = 0; i < 5000; i++) {
synchronized (room) {
counter--;
}
}
}, "t2");
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println(counter);
}
}
把出现线程安全问题的核心方法锁起来,每次只能一个线程进入访问
synchronized 修饰的方法的不具备继承性
,所以子类是线程不安全的,如果子类的方法也被 synchronized 修饰,两个锁对象其实是一把锁,而且是子类对象作为锁
用法:直接给方法加上一个修饰符 synchronized
//同步方法
修饰符 synchronized 返回值类型 方法名(方法参数) {
方法体;
}
//同步静态方法
修饰符 static synchronized 返回值类型 方法名(方法参数) {
方法体;
}
同步方法底层也是有锁对象的:
如果方法是实例方法:同步方法默认用 this 作为的锁对象
public synchronized void test() {} //等价于
public void test() {
synchronized(this) {}
}
如果方法是静态方法:同步方法默认用类名 .class 作为的锁对象
class Test{
public synchronized static void test() {}
}
//等价于
class Test{
public void test() {
synchronized(Test.class) {}
}
}
线程八锁就是考察 synchronized 锁住的是哪个对象
说明:主要关注锁住的对象是不是同一个
线程不安全:因为锁住的不是同一个对象,线程 1 调用 a 方法锁住的类对象,线程 2 调用 b 方法锁住的 n2 对象,不是同一个对象
class Number{
public static synchronized void a(){
Thread.sleep(1000);
System.out.println("1");
}
public synchronized void b() {
System.out.println("2");
}
}
public static void main(String[] args) {
Number n1 = new Number();
Number n2 = new Number();
new Thread(()->{ n1.a(); }).start();
new Thread(()->{ n2.b(); }).start();
}
线程安全:因为 n1 调用 a() 方法,锁住的是类对象,n2 调用 b() 方法,锁住的也是类对象,所以线程安全
class Number{
public static synchronized void a(){
Thread.sleep(1000);
System.out.println("1");
}
public static synchronized void b() {
System.out.println("2");
}
}
public static void main(String[] args) {
Number n1 = new Number();
Number n2 = new Number();
new Thread(()->{ n1.a(); }).start();
new Thread(()->{ n2.b(); }).start();
}
Monitor
Monitor 被翻译为监视器或管程
每个 Java 对象都可以关联一个 Monitor 对象,Monitor 也是 class,其实例存储在堆中,如果使用 synchronized 给对象上锁(重量级)之后,该对象头的 Mark Word 中就被设置指向 Monitor 对象的指针,这就是重量级锁
一个对象的结构如下:
Mark Word 结构:最后两位是锁标志位
64 位虚拟机 Mark Word:
工作流程:
注意:
字节码
代码:
public static void main(String[] args) {
Object lock = new Object();
synchronized (lock) {
System.out.println("ok");
}
}
0: new #2 // new Object
3: dup
4: invokespecial #1 // invokespecial <init>:()V,非虚方法
7: astore_1 // lock引用 -> lock
8: aload_1 // lock (synchronized开始)
9: dup // 一份用来初始化,一份用来引用
10: astore_2 // lock引用 -> slot 2
11: monitorenter // 【将 lock对象 MarkWord 置为 Monitor 指针】
12: getstatic #3 // System.out
15: ldc #4 // "ok"
17: invokevirtual #5 // invokevirtual println:(Ljava/lang/String;)V
20: aload_2 // slot 2(lock引用)
21: monitorexit // 【将 lock对象 MarkWord 重置, 唤醒 EntryList】
22: goto 30
25: astore_3 // any -> slot 3
26: aload_2 // slot 2(lock引用)
27: monitorexit // 【将 lock对象 MarkWord 重置, 唤醒 EntryList】
28: aload_3
29: athrow
30: return
Exception table:
from to target type
12 22 25 any
25 28 25 any
LineNumberTable: ...
LocalVariableTable:
Start Length Slot Name Signature
0 31 0 args [Ljava/lang/String;
8 23 1 lock Ljava/lang/Object;
说明:
升级过程
synchronized 是可重入、不公平的重量级锁,所以可以对其进行优化
无锁 -> 偏向锁 -> 轻量级锁 -> 重量级锁 // 随着竞争的增加,只能锁升级,不能降级
偏向锁的思想是偏向于让第一个获取锁对象的线程,这个线程之后重新获取该锁不再需要同步操作:
当锁对象第一次被线程获得的时候进入偏向状态,标记为 101,同时使用 CAS 操作将线程 ID 记录到 Mark Word。如果 CAS 操作成功,这个线程以后进入这个锁相关的同步块,查看这个线程 ID 是自己的就表示没有竞争,就不需要再进行任何同步操作
当有另外一个线程去尝试获取这个锁对象时,偏向状态就宣告结束,此时撤销偏向(Revoke Bias)后恢复到未锁定或轻量级锁状态
一个对象创建时:
如果开启了偏向锁(默认开启),那么对象创建后,MarkWord 值为 0x05 即最后 3 位为 101,thread、epoch、age 都为 0
偏向锁是默认是延迟的,不会在程序启动时立即生效,如果想避免延迟,可以加 VM 参数 -XX:BiasedLockingStartupDelay=0
来禁用延迟。JDK 8 延迟 4s 开启偏向锁原因:在刚开始执行代码时,会有好多线程来抢锁,如果开偏向锁效率反而降低
当一个对象已经计算过 hashCode,就再也无法进入偏向状态了
添加 VM 参数 -XX:-UseBiasedLocking
禁用偏向锁
撤销偏向锁的状态:
批量撤销:如果对象被多个线程访问,但没有竞争,这时偏向了线程 T1 的对象仍有机会重新偏向 T2,重偏向会重置对象的 Thread ID
批量重偏向:当撤销偏向锁阈值超过 20 次后,JVM 会觉得是不是偏向错了,于是在给这些对象加锁时重新偏向至加锁线程
批量撤销:当撤销偏向锁阈值超过 40 次后,JVM 会觉得自己确实偏向错了,根本就不该偏向,于是整个类的所有对象都会变为不可偏向的,新建的对象也是不可偏向的
一个对象有多个线程要加锁,但加锁的时间是错开的(没有竞争),可以使用轻量级锁来优化,轻量级锁对使用者是透明的(不可见)
可重入锁:线程可以进入任何一个它已经拥有的锁所同步着的代码块,可重入锁最大的作用是避免死锁
轻量级锁在没有竞争时(锁重入时),每次重入仍然需要执行 CAS 操作,Java 6 才引入的偏向锁来优化
锁重入实例:
static final Object obj = new Object();
public static void method1() {
synchronized( obj ) {
// 同步块 A
method2();
}
}
public static void method2() {
synchronized( obj ) {
// 同步块 B
}
}
创建锁记录(Lock Record)对象,每个线程的栈帧都会包含一个锁记录的结构,存储锁定对象的 Mark Word
让锁记录中 Object reference 指向锁住的对象,并尝试用 CAS 替换 Object 的 Mark Word,将 Mark Word 的值存入锁记录
如果 CAS 替换成功,对象头中存储了锁记录地址和状态 00(轻量级锁) ,表示由该线程给对象加锁
如果 CAS 失败,有两种情况:
当退出 synchronized 代码块(解锁时)
在尝试加轻量级锁的过程中,CAS 操作无法成功,可能是其它线程为此对象加上了轻量级锁(有竞争),这时需要进行锁膨胀,将轻量级锁变为重量级锁
当 Thread-1 进行轻量级加锁时,Thread-0 已经对该对象加了轻量级锁
Thread-1 加轻量级锁失败,进入锁膨胀流程:为 Object 对象申请 Monitor 锁,通过 Object 对象头获取到持锁线程,将 Monitor 的 Owner 置为 Thread-0,将 Object 的对象头指向重量级锁地址,然后自己进入 Monitor 的 EntryList BLOCKED
当 Thread-0 退出同步块解锁时,使用 CAS 将 Mark Word 的值恢复给对象头失败,这时进入重量级解锁流程,即按照 Monitor 地址找到 Monitor 对象,设置 Owner 为 null,唤醒 EntryList 中 BLOCKED 线程
自旋锁
重量级锁竞争时,尝试获取锁的线程不会立即阻塞
,可以使用自旋(默认 10 次)来进行优化,采用循环的方式去尝试获取锁
注意:
优点:不会进入阻塞状态,减少线程上下文切换的消耗
缺点:当自旋的线程越来越多时,会不断的消耗 CPU 资源
自旋锁情况:
自旋成功的情况:
自旋失败的情况:
自旋锁说明:
//手写自旋锁
public class SpinLock {
// 泛型装的是Thread,原子引用线程
AtomicReference<Thread> atomicReference = new AtomicReference<>();
public void lock() {
Thread thread = Thread.currentThread();
System.out.println(thread.getName() + " come in");
//开始自旋,期望值为null,更新值是当前线程
while (!atomicReference.compareAndSet(null, thread)) {
Thread.sleep(1000);
System.out.println(thread.getName() + " 正在自旋");
}
System.out.println(thread.getName() + " 自旋成功");
}
public void unlock() {
Thread thread = Thread.currentThread();
//线程使用完锁把引用变为null
atomicReference.compareAndSet(thread, null);
System.out.println(thread.getName() + " invoke unlock");
}
public static void main(String[] args) throws InterruptedException {
SpinLock lock = new SpinLock();
new Thread(() -> {
//占有锁
lock.lock();
Thread.sleep(10000);
//释放锁
lock.unlock();
},"t1").start();
// 让main线程暂停1秒,使得t1线程,先执行
Thread.sleep(1000);
new Thread(() -> {
lock.lock();
lock.unlock();
},"t2").start();
}
}
锁消除
锁消除是指对于被检测出不可能存在竞争的共享数据的锁进行消除,这是 JVM 即时编译器的优化
锁消除主要是通过逃逸分析来支持,如果堆上的共享数据不可能逃逸出去被其它线程访问到,那么就可以把它们当成私有数据对待,也就可以将它们的锁进行消除(同步消除:JVM 逃逸分析)
锁粗化
对相同对象多次加锁,导致线程发生多次重入,频繁的加锁操作就会导致性能损耗,可以使用锁粗化方式优化
如果虚拟机探测到一串的操作都对同一个对象加锁,将会把加锁的范围扩展(粗化)到整个操作序列的外部
一些看起来没有加锁的代码,其实隐式的加了很多锁:
public static String concatString(String s1, String s2, String s3) {
return s1 + s2 + s3;
}
String 是一个不可变的类,编译器会对 String 的拼接自动优化。在 JDK 1.5 之前,转化为 StringBuffer 对象的连续 append() 操作,每个 append() 方法中都有一个同步块
public static String concatString(String s1, String s2, String s3) {
StringBuffer sb = new StringBuffer();
sb.append(s1);
sb.append(s2);
sb.append(s3);
return sb.toString();
}
扩展到第一个 append() 操作之前直至最后一个 append() 操作之后,只需要加锁一次就可以
多把不相干的锁:一间大屋子有两个功能睡觉、学习,互不相干。现在一人要学习,一人要睡觉,如果只用一间屋子(一个对象锁)的话,那么并发度很低
将锁的粒度细分:
解决方法:准备多个对象锁
public static void main(String[] args) {
BigRoom bigRoom = new BigRoom();
new Thread(() -> { bigRoom.study(); }).start();
new Thread(() -> { bigRoom.sleep(); }).start();
}
class BigRoom {
private final Object studyRoom = new Object();
private final Object sleepRoom = new Object();
public void sleep() throws InterruptedException {
synchronized (sleepRoom) {
System.out.println("sleeping 2 小时");
Thread.sleep(2000);
}
}
public void study() throws InterruptedException {
synchronized (studyRoom) {
System.out.println("study 1 小时");
Thread.sleep(1000);
}
}
}
基本使用
需要获取对象锁后才可以调用 锁对象.wait()
,notify 随机唤醒一个线程,notifyAll 唤醒所有线程去竞争 CPU
Object 类 API:
public final void notify():唤醒正在等待对象监视器的单个线程。
public final void notifyAll():唤醒正在等待对象监视器的所有线程。
public final void wait():导致当前线程等待,直到另一个线程调用该对象的 notify() 方法或 notifyAll()方法。
public final native void wait(long timeout):有时限的等待, 到n毫秒后结束等待,或是被唤醒
说明:wait 是挂起线程,需要唤醒的都是挂起操作,阻塞线程可以自己去争抢锁,挂起的线程需要唤醒后去争抢锁
对比 sleep():
底层原理:
代码优化
虚假唤醒:notify 只能随机唤醒一个 WaitSet 中的线程,这时如果有其它线程也在等待,那么就可能唤醒不了正确的线程
解决方法:采用 notifyAll
notifyAll 仅解决某个线程的唤醒问题,使用 if + wait 判断仅有一次机会,一旦条件不成立,无法重新判断
解决方法:用 while + wait,当条件不成立,再次 wait
@Slf4j(topic = "c.demo")
public class demo {
static final Object room = new Object();
static boolean hasCigarette = false; //有没有烟
static boolean hasTakeout = false;
public static void main(String[] args) throws InterruptedException {
new Thread(() -> {
synchronized (room) {
log.debug("有烟没?[{}]", hasCigarette);
while (!hasCigarette) {//while防止虚假唤醒
log.debug("没烟,先歇会!");
try {
room.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
log.debug("有烟没?[{}]", hasCigarette);
if (hasCigarette) {
log.debug("可以开始干活了");
} else {
log.debug("没干成活...");
}
}
}, "小南").start();
new Thread(() -> {
synchronized (room) {
Thread thread = Thread.currentThread();
log.debug("外卖送到没?[{}]", hasTakeout);
if (!hasTakeout) {
log.debug("没外卖,先歇会!");
try {
room.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
log.debug("外卖送到没?[{}]", hasTakeout);
if (hasTakeout) {
log.debug("可以开始干活了");
} else {
log.debug("没干成活...");
}
}
}, "小女").start();
Thread.sleep(1000);
new Thread(() -> {
// 这里能不能加 synchronized (room)?
synchronized (room) {
hasTakeout = true;
//log.debug("烟到了噢!");
log.debug("外卖到了噢!");
room.notifyAll();
}
}, "送外卖的").start();
}
}
LockSupport 是用来创建锁和其他同步类的线程原语
LockSupport 类方法:
LockSupport.park()
:暂停当前线程,挂起原语LockSupport.unpark(暂停的线程对象)
:恢复某个线程的运行public static void main(String[] args) {
Thread t1 = new Thread(() -> {
System.out.println("start..."); //1
Thread.sleep(1000);// Thread.sleep(3000)
// 先 park 再 unpark 和先 unpark 再 park 效果一样,都会直接恢复线程的运行
System.out.println("park..."); //2
LockSupport.park();
System.out.println("resume...");//4
},"t1");
t1.start();
Thread.sleep(2000);
System.out.println("unpark..."); //3
LockSupport.unpark(t1);
}
LockSupport 出现就是为了增强 wait & notify 的功能:
只负责阻塞当前线程,会释放 CPU
原理:类似生产者消费者
先 unpark:
Guarded Suspension,用在一个线程等待另一个线程的执行结果
public static void main(String[] args) {
GuardedObject object = new GuardedObject();
new Thread(() -> {
sleep(1);
object.complete(Arrays.asList("a", "b", "c"));
}).start();
Object response = object.get(2500);
if (response != null) {
log.debug("get response: [{}] lines", ((List<String>) response).size());
} else {
log.debug("can't get response");
}
}
class GuardedObject {
private Object response;
private final Object lock = new Object();
//获取结果
//timeout :最大等待时间
public Object get(long millis) {
synchronized (lock) {
// 1) 记录最初时间
long begin = System.currentTimeMillis();
// 2) 已经经历的时间
long timePassed = 0;
while (response == null) {
// 4) 假设 millis 是 1000,结果在 400 时唤醒了,那么还有 600 要等
long waitTime = millis - timePassed;
log.debug("waitTime: {}", waitTime);
//经历时间超过最大等待时间退出循环
if (waitTime <= 0) {
log.debug("break...");
break;
}
try {
lock.wait(waitTime);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 3) 如果提前被唤醒,这时已经经历的时间假设为 400
timePassed = System.currentTimeMillis() - begin;
log.debug("timePassed: {}, object is null {}",
timePassed, response == null);
}
return response;
}
}
//产生结果
public void complete(Object response) {
synchronized (lock) {
// 条件满足,通知等待线程
this.response = response;
log.debug("notify...");
lock.notifyAll();
}
}
}
多任务版保护性暂停:
多任务版 GuardedObject图中 Futures 就好比居民楼一层的信箱(每个信箱有房间编号),左侧的 t0,t2,t4 就好比等待邮件的居民,右侧的 t1,t3,t5 就好比邮递员如果需要在多个类之间使用 GuardedObject 对象,作为参数传递不是很方便,因此设计一个用来解耦的中间类,这样不仅能够解耦【结果等待者】和【结果生产者】,还能够同时支持多个任务的管理。和生产者消费者模式的区别就是:这个生产者和消费者之间是一一对应的关系,但是生产者消费者模式并不是。rpc框架的调用中就使用到了这种模式。
public static void main(String[] args) throws InterruptedException {
for (int i = 0; i < 3; i++) {
new People().start();
}
Thread.sleep(1000);
for (Integer id : Mailboxes.getIds()) {
new Postman(id, id + "号快递到了").start();
}
}
@Slf4j(topic = "c.People")
class People extends Thread{
@Override
public void run() {
// 收信
GuardedObject guardedObject = Mailboxes.createGuardedObject();
log.debug("开始收信i d:{}", guardedObject.getId());
Object mail = guardedObject.get(5000);
log.debug("收到信id:{},内容:{}", guardedObject.getId(),mail);
}
}
class Postman extends Thread{
private int id;
private String mail;
//构造方法
@Override
public void run() {
GuardedObject guardedObject = Mailboxes.getGuardedObject(id);
log.debug("开始送信i d:{},内容:{}", guardedObject.getId(),mail);
guardedObject.complete(mail);
}
}
class Mailboxes {
private static Map<Integer, GuardedObject> boxes = new Hashtable<>();
private static int id = 1;
//产生唯一的id
private static synchronized int generateId() {
return id++;
}
public static GuardedObject getGuardedObject(int id) {
return boxes.remove(id);
}
public static GuardedObject createGuardedObject() {
GuardedObject go = new GuardedObject(generateId());
boxes.put(go.getId(), go);
return go;
}
public static Set<Integer> getIds() {
return boxes.keySet();
}
}
class GuardedObject {
//标识,Guarded Object
private int id;//添加get set方法
}
顺序输出 2 1
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> {
while (true) {
//try { Thread.sleep(1000); } catch (InterruptedException e) { }
// 当没有许可时,当前线程暂停运行;有许可时,用掉这个许可,当前线程恢复运行
LockSupport.park();
System.out.println("1");
}
});
Thread t2 = new Thread(() -> {
while (true) {
System.out.println("2");
// 给线程 t1 发放『许可』(多次连续调用 unpark 只会发放一个『许可』)
LockSupport.unpark(t1);
try { Thread.sleep(500); } catch (InterruptedException e) { }
}
});
t1.start();
t2.start();
}
连续输出 5 次 abc
public class day2_14 {
public static void main(String[] args) throws InterruptedException {
AwaitSignal awaitSignal = new AwaitSignal(5);
Condition a = awaitSignal.newCondition();
Condition b = awaitSignal.newCondition();
Condition c = awaitSignal.newCondition();
new Thread(() -> {
awaitSignal.print("a", a, b);
}).start();
new Thread(() -> {
awaitSignal.print("b", b, c);
}).start();
new Thread(() -> {
awaitSignal.print("c", c, a);
}).start();
Thread.sleep(1000);
awaitSignal.lock();
try {
a.signal();
} finally {
awaitSignal.unlock();
}
}
}
class AwaitSignal extends ReentrantLock {
private int loopNumber;
public AwaitSignal(int loopNumber) {
this.loopNumber = loopNumber;
}
//参数1:打印内容 参数二:条件变量 参数二:唤醒下一个
public void print(String str, Condition condition, Condition next) {
for (int i = 0; i < loopNumber; i++) {
lock();
try {
condition.await();
System.out.print(str);
next.signal();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
unlock();
}
}
}
}
异步模式之生产者/消费者:
class ShareData {
private int number = 0;
private Lock lock = new ReentrantLock();
private Condition condition = lock.newCondition();
public void increment() throws Exception{
// 同步代码块,加锁
lock.lock();
try {
// 判断 防止虚假唤醒
while(number != 0) {
// 等待不能生产
condition.await();
}
// 干活
number++;
System.out.println(Thread.currentThread().getName() + "\t " + number);
// 通知 唤醒
condition.signalAll();
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
public void decrement() throws Exception{
// 同步代码块,加锁
lock.lock();
try {
// 判断 防止虚假唤醒
while(number == 0) {
// 等待不能消费
condition.await();
}
// 干活
number--;
System.out.println(Thread.currentThread().getName() + "\t " + number);
// 通知 唤醒
condition.signalAll();
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
}
public class TraditionalProducerConsumer {
public static void main(String[] args) {
ShareData shareData = new ShareData();
// t1线程,生产
new Thread(() -> {
for (int i = 0; i < 5; i++) {
shareData.increment();
}
}, "t1").start();
// t2线程,消费
new Thread(() -> {
for (int i = 0; i < 5; i++) {
shareData.decrement();
}
}, "t2").start();
}
}
异步模式之生产者/消费者:
我们写一个线程间通信的消息队列,要注意区别,像rabbit mq等消息框架是进程间通信的。
public class demo {
public static void main(String[] args) {
MessageQueue queue = new MessageQueue(2);
for (int i = 0; i < 3; i++) {
int id = i;
new Thread(() -> {
queue.put(new Message(id,"值"+id));
}, "生产者" + i).start();
}
new Thread(() -> {
while (true) {
try {
Thread.sleep(1000);
Message message = queue.take();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
},"消费者").start();
}
}
//消息队列类,Java间线程之间通信
class MessageQueue {
private LinkedList<Message> list = new LinkedList<>();//消息的队列集合
private int capacity;//队列容量
public MessageQueue(int capacity) {
this.capacity = capacity;
}
//获取消息
public Message take() {
//检查队列是否为空
synchronized (list) {
while (list.isEmpty()) {
try {
sout(Thread.currentThread().getName() + ":队列为空,消费者线程等待");
list.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//从队列的头部获取消息返回
Message message = list.removeFirst();
sout(Thread.currentThread().getName() + ":已消费消息--" + message);
list.notifyAll();
return message;
}
}
//存入消息
public void put(Message message) {
synchronized (list) {
//检查队列是否满
while (list.size() == capacity) {
try {
sout(Thread.currentThread().getName()+":队列为已满,生产者线程等待");
list.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//将消息加入队列尾部
list.addLast(message);
sout(Thread.currentThread().getName() + ":已生产消息--" + message);
list.notifyAll();
}
}
}
final class Message {
private int id;
private Object value;
//get set
}
public static void main(String[] args) {
ExecutorService consumer = Executors.newFixedThreadPool(1);
ExecutorService producer = Executors.newFixedThreadPool(1);
BlockingQueue<Integer> queue = new SynchronousQueue<>();
producer.submit(() -> {
try {
System.out.println("生产...");
Thread.sleep(1000);
queue.put(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
});
consumer.submit(() -> {
try {
System.out.println("等待消费...");
Integer result = queue.take();
System.out.println("结果为:" + result);
} catch (InterruptedException e) {
e.printStackTrace();
}
});
}
此图来源于其他博客
RUNNABLE <--> WAITING
线程用synchronized(obj)获取了对象锁后
RUNNABLE <–> WAITING
RUNNABLE <–> WAITING
RUNNABLE <--> TIMED_WAITING
t 线程用 synchronized(obj) 获取了对象锁后
调用 obj.wait(long n) 方法时,t 线程从 RUNNABLE --> TIMED_WAITING
t 线程等待时间超过了 n 毫秒,或调用 obj.notify() , obj.notifyAll() , t.interrupt() 时
RUNNABLE <–> TIMED_WAITING
RUNNABLE <–> TIMED_WAITING
RUNNABLE <–> TIMED_WAITING
活跃性相关的一系列问题都可以用ReentrantLock进行解决。
有这样的情况:一个线程需要同时获取多把锁,这时就容易发生死锁t1 线程获得A对象锁,接下来想获取B对象的锁;t2 线程获得B对象锁,接下来想获取A对象的锁例。
检测死锁可以使用 jconsole工具;或者使用 jps 定位进程 id,再用 jstack 定位死锁:
下面使用jstack工具进行演示
D:\我的项目\JavaLearing\java并发编程\jdk8>jps
1156 RemoteMavenServer36
20452 Test25
9156 Launcher
23544 Jps
23848
22748 Test28
D:\我的项目\JavaLearing\java并发编程\jdk8>jstack 22748
2020-07-12 18:54:44
Full thread dump Java HotSpot(TM) 64-Bit Server VM (25.211-b12 mixed mode):
"DestroyJavaVM" #14 prio=5 os_prio=0 tid=0x0000000002a03800 nid=0x5944 waiting on condition [0x0000000000000000]
java.lang.Thread.State: RUNNABLE
//................省略了大部分内容.............//
Found one Java-level deadlock:
=============================
"线程二":
waiting to lock monitor 0x0000000002afc0e8 (object 0x00000000db9f76d0, a java.lang.Object),
which is held by "线程1"
"线程1":
waiting to lock monitor 0x0000000002afe1e8 (object 0x00000000db9f76e0, a java.lang.Object),
which is held by "线程二"
Java stack information for the threads listed above:
===================================================
"线程二":
at com.concurrent.test.Test28.lambda$main$1(Test28.java:39)
- waiting to lock <0x00000000db9f76d0> (a java.lang.Object)
- locked <0x00000000db9f76e0> (a java.lang.Object)
at com.concurrent.test.Test28$$Lambda$2/326549596.run(Unknown Source)
at java.lang.Thread.run(Thread.java:748)
"线程1":
at com.concurrent.test.Test28.lambda$main$0(Test28.java:23)
- waiting to lock <0x00000000db9f76e0> (a java.lang.Object)
- locked <0x00000000db9f76d0> (a java.lang.Object)
at com.concurrent.test.Test28$$Lambda$1/1343441044.run(Unknown Source)
at java.lang.Thread.run(Thread.java:748)
有五位哲学家,围坐在圆桌旁。
他们只做两件事,思考和吃饭,思考一会吃口饭,吃完饭后接着思考。
吃饭时要用两根筷子吃,桌上共有 5 根筷子,每位哲学家左右手边各有一根筷子。
如果筷子被身边的人拿着,自己就得等待
当每个哲学家即线程持有一根筷子时,他们都在等待另一个线程释放锁,因此造成了死锁。这种线程没有按预期结束,执行不下去的情况,归类为【活跃性】问题,除了死锁以外,还有活锁和饥饿者两种情况
很多教程中把饥饿定义为,一个线程由于优先级太低,始终得不到 CPU 调度执行,也不能够结束,饥饿的情况不易演示,讲读写锁时会涉及饥饿问题下面我讲一下一个线程饥饿的例子,先来看看使用顺序加锁的方式解决之前的死锁问题,就是两个线程对两个不同的对象加锁的时候都使用相同的顺序进行加锁。 但是会产生饥饿问题
出现死锁
顺序加锁的解决死锁方案
,但会造成饥饿现象。
活锁指的是 任务或者执行者没有被阻塞,由于某些条件没有满足,导致一直重复尝试,失败,尝试,失败。 活锁和死锁的区别在于,处于活锁的实体是在不断的改变状态,所谓的“活”, 而处于死锁的实体表现为等待;活锁有可能自行解开,死锁则不能。
相对于 synchronized 它具备如下特点
与 synchronized 一样,都支持可重入
基本语法
// 获取锁
reentrantLock.lock();
try {
// 临界区
} finally {
// 释放锁
reentrantLock.unlock();
}
可重入是指同一个线程如果首次获得了这把锁,那么因为它是这把锁的拥有者,因此有权利再次获取这把锁如果是不可重入锁,那么第二次获得锁时,自己也会被锁挡住
package com.concurrent.test;
import lombok.extern.slf4j.Slf4j;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
@Slf4j
/**
* 使用lockInterruptibly获取到锁,这样的在获取等待锁的时候是可以被打断的
*/
public class Test31 {
public static void main(String[] args) {
Lock lock = new ReentrantLock();
Thread thread = new Thread(() -> {
try {
/**
* 注意这里使用的是lockInterruptibly方法,如果使用lock.lock()方法,那么这里
* 等待的时候是不可以被打断的
*/
lock.lockInterruptibly();
} catch (InterruptedException e) {
e.printStackTrace();
log.info("被打断啦");
// 这里如果出了错不要再往下执行了
return;
}
try{
log.info("执行完啦,获取到了锁,没被打断");
}finally {
lock.unlock();
}
}, "thread-1");
lock.lock();
thread.start();
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 打断线程thread,原本它的状态是在等待锁的,我们在它等待锁的时候打断了,不让它继续等待了
thread.interrupt();
log.info("主线程执行结束");
}
}
package com.concurrent.test;
import lombok.extern.slf4j.Slf4j;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
@Slf4j
/**
* 使用trylock来进行指定的等待锁,如果指定时间还是没有等待到锁,就认为无法获取到锁了
*/
public class Test32 {
public static void main(String[] args) {
Lock lock = new ReentrantLock();
Thread thread = new Thread(() -> {
try {
if (!lock.tryLock(2, TimeUnit.SECONDS)){
log.debug("获取等待指定时间后失败,返回");
// 这里如果出了错不要再往下执行了
return;
}
} catch (InterruptedException e) {
e.printStackTrace();
log.info("被打断啦");
// 这里如果出了错不要再往下执行了
return;
}
try{
log.info("执行完啦,获取到了锁,没被打断");
}finally {
lock.unlock();
}
}, "thread-1");
log.info("主线程获取");
lock.lock();
thread.start();
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
log.info("主线程释放锁");
lock.unlock();
log.info("主线程执行结束");
}
}
package org.jio;
import lombok.extern.slf4j.Slf4j;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.ReentrantLock;
@Slf4j
/**
* 使用reentrantlock中的tryLock来获取锁来解决哲学家就餐问题,这样就不会造成死锁!
*/
public class Test33 extends Thread{
public static void main(String[] args) {
Chopstick2 c1 = new Chopstick2("1");
Chopstick2 c2 = new Chopstick2("2");
Chopstick2 c3 = new Chopstick2("3");
Chopstick2 c4 = new Chopstick2("4");
Chopstick2 c5 = new Chopstick2("5");
new Philosopher2("苏格拉底", c1, c2).start();
new Philosopher2("柏拉图", c2, c3).start();
new Philosopher2("亚里士多德", c3, c4).start();
new Philosopher2("赫拉克利特", c4, c5).start();
new Philosopher2("阿基米德", c5, c1).start();
}
}
@Slf4j(topic = "Philosopher")
class Philosopher2 extends Thread{
Chopstick2 left;
Chopstick2 right;
public Philosopher2(String name, Chopstick2 left, Chopstick2 right) {
super(name);
this.left = left;
this.right = right;
}
private void eat() {
log.debug("eating...");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
@Override
public void run() {
while (true) {
try {
if (left.tryLock(2, TimeUnit.SECONDS)){
try {
if (right.tryLock(2, TimeUnit.SECONDS)){
try {
eat();
}finally {
right.unlock();
}
}
}finally {
left.unlock();
}
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
class Chopstick2 extends ReentrantLock {
private String name ;
public Chopstick2(String name) {
this.name = name;
}
@Override
public String toString() {
return "Chopstick{" +
"name='" + name + '\'' +
'}';
}
}
synchronized锁中,在entrylist等待的锁在竞争时不是按照先到先得来获取锁的,所以说synchronized锁时不公平的;ReentranLock锁默认是不公平的,但是可以通过设置实现公平锁。本意是为了解决之前提到的饥饿问题,但是公平锁一般没有必要,会降低并发度,使用trylock也可以实现。
synchronized 中也有条件变量,就是我们讲原理时那个 waitSet 休息室,当条件不满足时进入 waitSet 等待
ReentrantLock 的条件变量比 synchronized 强大之处在于,它是支持多个条件变量的,这就好比
使用要点:
package com.concurrent.test;
import lombok.extern.slf4j.Slf4j;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
@Slf4j
/**
* 等待烟和等待外卖的线程分别在等待不同的两个资源,所以可以使用不同的条件变量
* 使用ReentrantLock可以轻易解决synchronized只有一个等待集合waitset的问题
* 1. await 前需要获得锁
* 2. await 执行后,会释放锁,进入 conditionObject 等待
* 3. await 的线程被唤醒(或打断、或超时)取重新竞争 lock 锁,执行唤醒的线程爷必须先获得锁
* 4. 竞争 lock 锁成功后,从 await 后继续执行
*/
public class Test34 {
static Lock lock = new ReentrantLock();
static Condition waitCigaretteQueue = lock.newCondition();
static Condition waitBreakfastQueue = lock.newCondition();
static volatile boolean hasCigarette = false;
static volatile boolean hasBreakfast = false;
public static void main(String[] args) {
Thread thread = new Thread(() -> {
lock.lock();
try {
while (!hasCigarette){
try {
waitCigaretteQueue.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
log.debug("等到了它的烟");
}finally {
lock.unlock();
}
}, "等烟线程");
thread.start();
Thread thread1 = new Thread(() -> {
try {
lock.lock();
while (!hasBreakfast){
try {
waitBreakfastQueue.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
log.debug("等到了它的早餐");
}finally {
lock.unlock();
}
}, "等待早餐线程");
thread1.start();
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
// new Thread(()->{
// lock.lock();
// try{
// log.info("烟来了");
// // 因为上面只有一个线程在等待,所以只用发送一个signal就行了,不用signalAll
// hasCigarette = true;
// waitCigaretteQueue.signal();
// }finally {
// lock.unlock();
// }
// },"送烟线程").start();
sendCigarette();
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
// new Thread(()->{
// lock.lock();
// try {
// log.info("送早餐来了");
// hasBreakfast = true;
// waitBreakfastQueue.signal();
// }finally {
// lock.unlock();
// }
// },"送早餐程").start();
sendBreakfast();
}
public static void sendCigarette(){
// 必须先获取锁
lock.lock();
try{
log.info("烟来了");
// 因为上面只有一个线程在等待,所以只用发送一个signal就行了,不用signalAll
hasCigarette = true;
waitCigaretteQueue.signal();
}finally {
lock.unlock();
}
}
public static void sendBreakfast(){
lock.lock();
try {
log.info("送早餐来了");
hasBreakfast = true;
waitBreakfastQueue.signal();
}finally {
lock.unlock();
}
}
}
package org.jio;
import lombok.extern.slf4j.Slf4j;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.LockSupport;
import java.util.concurrent.locks.ReentrantLock;
/**
* @author : JAT
* @version : 1.0
* @email : zgt9321@qq.com
* @since : 2024/1/10
**/
@Slf4j
public class MTest01 {
static Object lock = new Object();
static volatile boolean flagt1_2 = false;
static ReentrantLock lockR = new ReentrantLock();
static Condition condt1_2 = lockR.newCondition();
public static void main(String[] args) {
// way1();
// way2();
// way3();
way4();
}
// way4 park
public static void way4() {
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
LockSupport.park();
log.debug(" i am t1 我在后面");
}
}, "t1");
Thread t2 = new Thread(new Runnable() {
@Override
public void run() {
try {
log.debug(" i am t2 我在前面");
LockSupport.unpark(t1);
} finally {
}
}
}, "t2");
log.debug("开始了");
t2.start();
t1.start();
}
// way3 Reetrantlock 条件变量
public static void way3() {
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
lockR.lock();
try {
while (!flagt1_2) {
condt1_2.await();
}
log.debug(" i am t1 我在后面");
flagt1_2 = false;
condt1_2.signalAll();
} catch (InterruptedException e) {
throw new RuntimeException(e);
} finally {
lockR.unlock();
}
}
}, "t1");
Thread t2 = new Thread(new Runnable() {
@Override
public void run() {
lockR.lock();
try {
while (flagt1_2) {
condt1_2.await();
}
log.debug(" i am t2 我在前面");
flagt1_2 = true;
condt1_2.signalAll();
} catch (InterruptedException e) {
throw new RuntimeException(e);
} finally {
lockR.unlock();
}
}
}, "t2");
log.debug("开始了");
t2.start();
t1.start();
}
// way2 Reentrantlock
public static void way2() {
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
lockR.lock();
log.debug(" i am t1 我在后面");
}
}, "t1");
Thread t2 = new Thread(new Runnable() {
@Override
public void run() {
try {
log.debug(" i am t2 我在前面");
} finally {
lockR.unlock();
}
}
}, "t2");
log.debug("开始了");
t2.start();
t1.start();
}
// way1 synchronized方法
public static void way1() {
Thread t1 = new Thread(() -> {
while (true)
synchronized (lock) {
while (!flagt1_2) {
try {
lock.wait();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
log.debug(" i am t1 我在后面");
flagt1_2 = false;
lock.notifyAll();
}
}, "t1");
Thread t2 = new Thread(() -> {
while (true)
synchronized (lock) {
while (flagt1_2) {
try {
lock.wait();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
log.debug(" i am t2 我在前面");
flagt1_2 = true;
lock.notifyAll();
}
}, "t2");
log.debug("开始了");
t1.start();
t2.start();
}
}
wait - notify
package com.concurrent.test;
/**
* 使用notify和wait实现循环打印abcabc
*/
public class Test37 {
public static void main(String[] args) {
WaitNotify waitNotify = new WaitNotify(1, 15);
new Thread(()->{
waitNotify.print("a",1,2);
},"线程一").start();
new Thread(()->{
waitNotify.print("b",2,3);
},"线程二").start();
new Thread(()->{
waitNotify.print("c",3,1);
},"线程三").start();
}
}
/**
* 设置一个打印标记,比如
* 线程输出内容 打印标记 下一个打印标记
* a 1 2
* b 2 3
* c 3 1
* 每个线程执行打印之前,先检查自己的线程跟标记是否相同,如果相同则打印,并设置下一个打印的标记
*/
class WaitNotify{
/**
* fddfdf
* @param str 要打印的内容
* @param flag 线程的打印标记
* @param nextFlag 下一个打印标记
*/
public void print(String str,int flag, int nextFlag){
for (int i=0;i<loopNumber;i++){
synchronized (this){
while (this.flag != flag){
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
// 标记相同则打印
System.out.print(str);
this.flag = nextFlag;
// 修改了打印标记,唤醒其它线程让他们抢啦!
this.notifyAll();
}
}
}
int flag ;
int loopNumber;
public WaitNotify(int flag, int loopNumber) {
this.flag = flag;
this.loopNumber = loopNumber;
}
}
ReentrantLock await-signal
package com.concurrent.test;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.ReentrantLock;
public class Test38 {
public static void main(String[] args) {
AwaitSignal awaitSignal = new AwaitSignal(15);
Condition aCondition = awaitSignal.newCondition();
Condition bCondition = awaitSignal.newCondition();
Condition cCondition = awaitSignal.newCondition();
new Thread(()->{
awaitSignal.print("a",aCondition,bCondition);
},"线程一").start();
new Thread(()->{
awaitSignal.print("b",bCondition,cCondition);
},"线程二").start();
new Thread(()->{
awaitSignal.print("c",cCondition,aCondition);
},"线程三").start();
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
awaitSignal.lock();
try {
aCondition.signal();
}finally {
awaitSignal.unlock();
}
}
}
class AwaitSignal extends ReentrantLock{
int loopNumber ;
public AwaitSignal(int loopNumber) {
this.loopNumber = loopNumber;
}
public void print(String str, Condition current , Condition next ){
for (int i =0;i<loopNumber;i++){
this.lock();
try {
try {
current.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.print(str);
next.signal();
}finally {
this.unlock();
}
}
}
}
park-unpark
package com.concurrent.test;
import java.util.concurrent.locks.LockSupport;
public class Test39 {
static Thread thread2 ;
static Thread thread1;
static Thread thread3;
public static void main(String[] args) {
ParkUnpark parkUnpark = new ParkUnpark(15);
thread1 = new Thread(() -> {
parkUnpark.print("a",thread2);
}, "线程一");
thread2 = new Thread(() -> {
parkUnpark.print("b",thread3);
}, "线程二");
thread3 = new Thread(() -> {
parkUnpark.print("c",thread1);
}, "线程三");
thread1.start();
thread2.start();
thread3.start();
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
LockSupport.unpark(thread1);
}
}
class ParkUnpark{
int loopNumber;
public ParkUnpark(int loopNumber) {
this.loopNumber = loopNumber;
}
public void print(String str , Thread next ){
for (int i=0;i<loopNumber;i++){
LockSupport.park();
System.out.print(str);
LockSupport.unpark(next);
}
}
}