共享模型之管程
回顾故事:老王有一把算盘,租给其他人用使用;怎么样能租给更多人,得到更多的租金,而不能出现问题;
共享模型代理的问题
- 不同线程同时对同一共享变量进行操作,最终将导致结果出现无法预测
- 根本原因为Java内存模型分主内存和工作内存
- 加上线程在执行过程中可能出现上下文切换
- 具体案例:两个不同线程对一个int共享变量同时做++与--;
- i++对应了JVM字节码如下:
getstatic i // 获取静态变量i
iconst_1 // 准备常量1
iadd //自增
putstatic // 将修改后的值存入静态变量i
解决方案
- 阻塞式:synchronized Lock
- 非阻塞式:原子变量
解决方案之synchronized
- 对象锁,可让同一时刻最多只有一个线程能持续
- 其他线程获取已加锁的对象时,会阻塞
- 加在成员方法上,等同作用于当前对象this
- 加在静态方法或类上,则锁对象为该类
public class TestSynchronized {
public static synchronized void test1(){
// 临界区代码
}
public synchronized void test2(){
// 临界区代码
}
private final TestSynchronized sync = new TestSynchronized();
public void test3(){
// 未受保护代码区
synchronized (sync) {
// 临界区代码
}
// 未受保护代码区
}
}
变量线程安全分析
-
成员变量与静态变量
- 没有多线程共享则安全
- 共享但只读也安全
- 存在共享、且可读写,则需要考虑线程安全问题
-
局部变量
- 本事是线程安全的
- 但,局部变量引用的对象未必安全
- 若该对象没有逃离方法作用访问,则安全
- 若逃离方法作用范围,则需要考虑
-
开闭原则的意义
- private 及 final 在某些情况下能提供一定的线程安全保障
-
常见线程安全类
- String
- Integer
- StringBuffer
- Random
- Vector
- Hashtable
- java.util.concurrent.*
-
注意理解
- 说某些类是线程安全的指的是多个线程调用同一个实例的某个方法时,是线程安全的
- 可以理解为每个方法是原子的
- 但他们多个方法组合起来不一定是原子的
- String、Integer是不可变类,其内部状态不可变,因此他们是线程安全的,但思考String有replace、substring等方法可以改变值,这些方法能保证线程的安全?
Monitor
- 了解Java对象头
-
32位虚拟机
-
对象头占64bit
-
其中包括32bits Mark word 和 32bits klass word
-
数组对象占96bits,因为length还占32bits
-
其中mark word 组成(正常状态)
- 25 bits hascode
- 4 age 分待年龄
- 1 biased_lock 是否偏向锁
- 2 锁状态
-
-
64位虚拟机
-
其中mark word 组成(正常状态)
- 31 bits hascode
- 4 age 分待年龄
- 1 biased_lock 是否偏向锁
- 2 锁状态
- 25 unused
-
-
- 偏向锁
- 轻量级锁
- 重量级锁
- 回顾:《房间挂书包》小故事理解锁膨胀与优化
wait 与 notify
- 多线程工作时协作工作的手段之一
- obj.wait 让进入object监视器线程到waitSet中等着
- obj.notify 从正在waitSet中等待的线程中挑一个唤醒
- obj.notifyAll 则唤醒 waitSet中所有等待的线程
- 前提:必须获得对象锁,才可调用,需与synchronized配合使用
@Slf4j
public class TestWaitNotify {
/**
* 建议加 final,可保证其引用不可变,不会导致过程中加锁解锁出现不同对象
*/
private final static Object lock = new Object();
public static void main(String[] args) {
new Thread(()-> {
synchronized (lock) {
try {
log.info("线程T1开始执行");
// 让当前线程在obj上等待
lock.wait();
// 最多等待1s,1s后 自动唤醒
// lock.wait(1000);
log.info("线程T1执行完毕");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}, "T1").start();
new Thread(()-> {
synchronized (lock) {
try {
log.info("线程T2开始执行");
// 让当前线程在obj上等待
lock.wait();
log.info("线程T2执行完毕");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}, "T2").start();
// 主线程睡眠2s
sleep(2);
// 主线程执行
log.info("主线程开始执行,唤醒其他线程");
synchronized (lock) {
try {
// 随机唤醒一个线程,随机挑选一个
lock.notify();
// 唤醒对象上所有等待的线程
// lock.notifyAll();
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
wait(long n) 与 sleep(long n)
- 一个是对象的方法,一个是线程的方法
- sleep睡眠线程,不会释放对象锁,wait则会
- 他们的线程状态都是Timed_waiting
park 与 unpark
- 暂停会恢复某个线程对象
- park 和 unpark 顺序可以颠倒使用
- 相比notifyAll,唤醒线程可以更精确
/**
* @author qinchen
* @date 2021/7/28 8:50
* @description Park Unpark 学习
* 每个线程都有自己的一个park对象,c写的
* park对象由 _counter _cond _mutex 三个部分组成
* 将 _counter比作干粮,最多只能带一份
* 调用park时,看有没有“干粮”
* 调用unpark 补充”干粮“,然后叫醒线程
*/
@Slf4j(topic = "test.TestParkUnpark")
public class TestParkUnpark {
public static void main(String[] args) {
Thread t1 = new Thread(() -> {
log.info("线程开始执行");
sleep(1);
log.info("准备暂停");
LockSupport.park();
log.info("苏醒了");
}, "t1");
t1.start();
sleep(2);
log.info("准备唤醒");
// unpark 即可在调用park之前调用,也可在之后调用
// 可以吧t1中的sleep 改为 2 main 改为 1,看下效果
// 这个地方与 wait notify 区别较大,其顺序不可颠倒
LockSupport.unpark(t1);
log.info("主线程结束执行");
}
}
重新理解线程的6种状态的互相转变
-
new -> runnable
- t.start()
-
runnable <->waiting
- obj.wait() // r -> w
- obj.notify() | obj.notifyAll() | t.interrupt()
- 竞争锁成功 w -> r
- 竞争锁失败 w -> b
- t.join() // r ->w , t运行结束后 w -> r
- LockSupport.park() // r -> w
- LockSupport.unpark( t ) // w -> r
-
runnable <-> timed_waiting
- 上述带超时时间的操作
-
runnable <-> blocked
- 对象锁竞争失败 r - > b
-
runnable <->terminated
- 线程所有代码执行完毕
锁的活跃性
@Slf4j
public class TestDeadLock {
public static void main(String[] args) {
Object a = new Object();
Object b = new Object();
Thread t1 = new Thread(() -> {
synchronized (a) {
sleep(1);
synchronized (b) {
log.info("执行线程A");
}
}
}, "t1");
Thread t2 = new Thread(() -> {
synchronized (b) {
sleep(0.5);
synchronized (a) {
log.info("执行线程B");
}
}
}, "t2");
t1.start();
t2.start();
log.info("主线程执行完毕");
}
}
-
死锁
- 一个线程需同同时获得多把锁时可能发生
- 经典案例:五位哲学家就餐问题
- 定位
- jps + jstack
- jconsole
Found one Java-level deadlock:
=============================
"t2":
waiting to lock monitor 0x000001cc79545e98 (object 0x0000000741603330, a java.lang.Object),
which is held by "t1"
"t1":
waiting to lock monitor 0x000001cc79543a28 (object 0x0000000741603340, a java.lang.Object),
which is held by "t2"
Java stack information for the threads listed above:
===================================================
"t2":
at cn.com.ebidding.test.TestDeadLock.lambda$main$1(TestDeadLock.java:33)
- waiting to lock <0x0000000741603330> (a java.lang.Object)
- locked <0x0000000741603340> (a java.lang.Object)
at cn.com.ebidding.test.TestDeadLock$$Lambda$2/186276003.run(Unknown Source)
at java.lang.Thread.run(Thread.java:748)
"t1":
at cn.com.ebidding.test.TestDeadLock.lambda$main$0(TestDeadLock.java:24)
- waiting to lock <0x0000000741603340> (a java.lang.Object)
- locked <0x0000000741603330> (a java.lang.Object)
at cn.com.ebidding.test.TestDeadLock$$Lambda$1/1232367853.run(Unknown Source)
at java.lang.Thread.run(Thread.java:748)
Found 1 deadlock.
-
活锁
- 两个线程互相改变对方结束线程的条件
- 最终导致双方永远都无法结束执行
-
饥饿锁
- 一个线程由于优先级太低,始终得不到CPU调度
- 但线程一直不能结束,也没有出现死锁
ReentrantLock
- 特点
- 支持可重入
- 可中断
- 可设置超时时间
- 可设置公平性
- 可支持多条件变量
@Slf4j(topic = "test.TestReentrantLock")
public class TestReentrantLock {
private static ReentrantLock lock = new ReentrantLock();
public static void main(String[] args) {
lock.lock();
try {
log.info("进入main方法的临界代码");
m1();
} finally {
lock.unlock();
}
log.info("执行结束");
}
public static void m1() {
lock.lock();
try {
log.info("进入m1方法");
m2();
} finally {
lock.unlock();
}
}
public static void m2() {
lock.lock();
try {
log.info("进入m2方法");
} finally {
lock.unlock();
}
}
}
// 可中断锁
lock.lockInterruptibly();
// 锁超时
boolean b1 = lock.tryLock(1, TimeUnit.SECONDS);
// 条件锁
Condition condition1 = lock.newCondition();
lock.lock();
condition1.wait();
condition1.signal();
- 回顾案例:送早餐 和 送烟
- 回顾案例:五个哲学家就餐问题,可以用ReentrantLock很好的解决