1.共享问题
1.1 Java共享问题演示
以下的结果可能是正数、负数、零。因为 Java 中对静态变量的自增,自减并不是原子操作。单线程情况下,不会出现指令交错的现象。但是在多线程环境下,可能出现指令交错运行。
//两个线程对共享的值进行修改,一个进行++,一个进行--,查看最后的结果是否为0
public class TestSecurity {
static int count = 0;
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread() {
@Override
public void run() {
for (int i = 0; i < 3000; i++) {
count++;
}
}
};
Thread t2 = new Thread() {
@Override
public void run() {
for (int i = 0; i < 3000; i++) {
count--;
}
}
};
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println(count); //不一定为0
}
}
1.2 临界区
一段代码块内如果存在对共享资源的多线程读写操作,称这段代码块为临界区。
上面程序代码的临界区为对资源的共享部分。
static int count = 0;
//...
// 临界区
{
counter++;
}
//...
// 临界区
{
counter--;
}
1.3 竞态条件
多个线程在临界区内执行,由于代码的执行序列不同而导致结果无法预测,称之为发生了竞态条件。
2. synchronized
- synchronized 采用互斥的方式让同一时刻至多只有一个线程能持有对象锁,其它线程再想获取这个对象锁时就会阻塞住。这样就能保证拥有锁的线程可以安全的执行临界区内的代码,不用担心线程上下文切换。
- synchronized 实际是用对象锁保证了临界区内代码的原子性,临界区内的代码对外是不可分割的,不会被线程切换所打断。
2.1 synchronized语法
synchronized(对象)
{
临界区
}
- 上述问题代码的解决方案
另外准备对象,用于加锁使用。当某个线程持有该对象锁后,其他线程会被阻塞,无法访问临界资源。
public class TestSecurity {
static int count = 0;
//对象锁
static final Object lock = new Object();
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread() {
@Override
public void run() {
for (int i = 0; i < 3000; i++) {
//临界区加锁
synchronized (lock){
count++;
}
}
}
};
Thread t2 = new Thread() {
@Override
public void run() {
for (int i = 0; i < 3000; i++) {
//临界区加锁
synchronized (lock){
count--;
}
}
}
};
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println(count);
}
}
2.2 方法上的 synchronized
在成员方法上面加synchronized表示对this对象加锁,在静态方法上面加synchronized表示对类加锁
class Test{
public synchronized void test() {
}
}
//等价于
class Test{
public void test() {
synchronized(this) {
}
}
}
class Test{
public synchronized static void test() {
}
}
//等价于
class Test{
public static void test() {
synchronized(Test.class) {
}
}
}
3.变量和线程安全分析
3.1 成员变量和静态变量的线程安全问题
- 如果它们没有共享,则线程安全
- 如果它们被共享了,根据它们的状态是否能够改变,又分两种情况
- 如果只有读操作,则线程安全
- 如果有读写操作,则这段代码是临界区,需要考虑线程安全
3.2 局部变量的线程安全问题
- 局部变量是线程安全的
- 但局部变量引用的对象不一定线程安全
- 如果该对象没有逃离方法的作用范围,则是线程安全的
- 如果该对象逃离方法的作用范围,需要考虑线程安全
注: private 或 final 提供安全的意义在于,在一定程度上防止了子类重写父类方法,引入线程安全问题。(在重写的方法中,采用多线程的方式对共享资源进行读写——非线程安全)
3.3 常见线程安全类
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 等都是不可变类(共享的变量只读),因为其内部的状态不可以改变,因此它们的方法都是线程安全的
4 Monitor 管程
4.1 Java对象头
以32位虚拟机为例
# 普通对象 对象头包括 标记字段和Class字段(标识对象的Class类型)
|--------------------------------------------------------------|
| Object Header (64 bits) |
|------------------------------------|-------------------------|
| Mark Word (32 bits) | Klass Word (32 bits) |
|------------------------------------|-------------------------|
# Mark Word 结构
# 01 正常状态; 01 biased_lock:1 偏向锁; 00 轻量级锁; 10 重量级锁; 11 GC
|-------------------------------------------------------|--------------------|
| Mark Word (32 bits) | State |
|-------------------------------------------------------|--------------------|
| hashcode:25 | age:4 | biased_lock:0 | 01 | Normal |
|-------------------------------------------------------|--------------------|
| thread:23 | epoch:2 | age:4 | biased_lock:1 | 01 | Biased |
|-------------------------------------------------------|--------------------|
| ptr_to_lock_record:30 | 00 | Lightweight Locked |
|-------------------------------------------------------|--------------------|
| ptr_to_heavyweight_monitor:30 | 10 | Heavyweight Locked |
|-------------------------------------------------------|--------------------|
| | 11 | Marked for GC |
|-------------------------------------------------------|--------------------|
4.2 Monitor原理
Monitor管程,每个Java对象都可以关联一个Monitor对象,如果使用synchronized给对象上锁(重量级)后,该对象头的Mark Word 中就被设置指向 Monitor 对象的指针
Monitor的结构:

- Monitor中只能有一个Owner,表示获得锁的线程
- 未获得锁的其他线程进入EntryList中等待
- Owner线程执行完同步代码块中的内容后,会唤醒EntryList中等待的线程来竞争锁,非公平的
- 如果Owner线程某些条件不满足,可以调用wait()方法,进入WaitSet 等待notify()唤醒,将资源所有权释放,供其他线程访问
注:synchronized必须进入同一个对象的Monitor才有上述功能;不加synchronized的对象不会关联Monitor管程。
4.3 synchronized原理
4.3.1 轻量级锁
使用场景:如果一个对象虽然有多线程要加锁,但加锁的时间是错开的(没有竞争),则可以使用轻量级锁优化。轻量级锁对使用者是透明的,语法还是synchronized
轻量级锁的加锁过程
- 创建锁记录对象,每个线程的栈帧中包含一个锁记录的结构

- 让锁记录中的Object reference指向锁对象,尝试使用CAS,将lock record地址和Object的Mark Word替换

- 替换成功,则对象头中保存锁记录地址和状态(00 轻量级锁),表示由该线程给对象加锁。(相当于不锁门了,改成在门口放书包,进门前先翻阅书包,看看是哪个线程)

-
替换失败,分两种情况
- 如果其他线程已经持有该Object的轻量级锁,表明有竞争,进入锁膨胀过程
- 如果是自己执行的synchronized锁重入,则再添加一条Lock Record作为重入计数
image-20210615185410308.png
- 退出synchronized代码块(解锁时),分两种情况
- 如果有取值为null的锁记录,表示有重入,让重入计数减一
- 如果锁记录的取值不为null,此时使用CAS将Mark Word的值恢复给对象头
- 成功,解锁成功
- 失败,说明发生竞争,轻量级锁进行了锁膨胀已经升级为重量级锁,进入重量级锁的解锁流程
锁膨胀
如果在尝试加轻量级锁的过程中,发现该对象的Mark Word中已经指向了其他线程的Lock Record,则进入锁膨胀,将轻量级锁变为重量级锁
- 当出现竞争线程时,该线程尝试加锁失败。发现Object对象中已经指向了其他线程的Lock Record。

- 此时当前线程加轻量级锁失败,进入锁膨胀阶段
- 首先为Object对象申请Monitor管程,让Object指向重量级锁地址
- 当前线程进入该Monitor的EntryList,等待唤醒

- 当持有锁的线程退出同步代码块时,使用CAS给Mark Word的值恢复给对象头时失败,这时进入重量级锁的解锁流程,按照Monitor地址找到Monitor对象,设置Owner为null,唤醒EntryList中的阻塞线程
4.3.2 自旋优化
使用场景:重量级锁竞争时,可以使用自旋优化。如果当前线程自旋成功(持有锁的线程已经退出同步代码块,解锁),则可以避免当前线程的阻塞。
注:自旋会占用CPU时间,单核CPU自旋浪费资源,自旋优化适用于多核CPU;Java6后自旋是自适应的,刚刚自旋成功的话,则判定这次自旋的成功率会较高,多自旋几次,相反则少自旋几次
4.3.3 偏向锁
使用场景:轻量级锁在没有竞争时,每次还需要执行CAS操作,Java6中引入偏向锁进一步优化,只有第一次使用CAS将线程ID设置到对象的 Mark Word 中,之后发现这个线程ID是自己就表示没有竞争,不用重新CAS。以后不发生竞争,则该对象就归该线程所有。
注:默认开启了偏向锁,偏向锁默认是延迟的,不会在程序启动时立即生效,避免延迟,添加VM参数:-XX:BiasedLockingStartupDelay=0
用于禁用延迟。hashcode第一次用到才会被赋值,当有hashcode时,偏向锁则消失,两者不可同存。
实例测试
//偏向锁测试
public class TestLock {
public static void main(String[] args) {
Dog dog = new Dog();
ClassLayout classLayout = ClassLayout.parseInstance(dog);
new Thread(){
@Override
public void run() {
System.out.println("before");
System.out.println(classLayout.toPrintable());
synchronized (dog){
System.out.println("in");
System.out.println(classLayout.toPrintable());
}
System.out.println("after");
System.out.println(classLayout.toPrintable());
}
}.start();
}
}
class Dog{}
# 结果
before
com.yqj.concurrent2.Dog object internals:
OFF SZ TYPE DESCRIPTION VALUE
0 8 (object header: mark) 0x0000000000000005 (biasable; age: 0)
8 4 (object header: class) 0x2000c143
12 4 (object alignment gap)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
in
com.yqj.concurrent2.Dog object internals:
OFF SZ TYPE DESCRIPTION VALUE
0 8 (object header: mark) 0x000000001b426005 (biased: 0x000000000006d098; epoch: 0; age: 0)
8 4 (object header: class) 0x2000c143
12 4 (object alignment gap)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
after
com.yqj.concurrent2.Dog object internals:
OFF SZ TYPE DESCRIPTION VALUE
0 8 (object header: mark) 0x000000001b426005 (biased: 0x000000000006d098; epoch: 0; age: 0)
8 4 (object header: class) 0x2000c143
12 4 (object alignment gap)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
偏向锁的撤销
- 调用对象的hashCode
偏向锁的对象 MarkWord 中存储的是线程 id,如果调用 hashCode 会导致偏向锁被撤销。这是由于空间有限,两者不能共存。而轻量级锁可以将hashCode保存在锁记录Lock Record中。重量级锁可以将hashCode保存在Monitor中。
- 其他线程使用对象
当有其他线程使用偏向锁对象时,会将偏向锁升级为轻量级锁。
//测试实例
public class TestLock {
public static void main(String[] args) throws InterruptedException {
Dog dog = new Dog();
ClassLayout classLayout = ClassLayout.parseInstance(dog);
Thread t1 = new Thread() {
@Override
public void run() {
synchronized (dog) {
System.out.println(classLayout.toPrintable());
}
//必须使用wait/nodify,因为t1线程不能结束,否则底层线程可能被jvm重用作为t2线程,底层线程的id一样,导致依旧还是偏向锁
synchronized (TestLock.class){
TestLock.class.notify();
}
}
};
t1.start();
Thread t2 = new Thread() {
@Override
public void run() {
synchronized (TestLock.class){
try {
TestLock.class.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("before");
System.out.println(classLayout.toPrintable());
synchronized (dog) {
System.out.println("in");
System.out.println(classLayout.toPrintable());
}
System.out.println("after");
System.out.println(classLayout.toPrintable());
}
};
t2.start();
}
}
class Dog{}
# 结果 原本为偏向锁,后由于其他线程也使用该对象,从而升级为轻量级锁
com.yqj.concurrent2.Dog object internals:
OFF SZ TYPE DESCRIPTION VALUE
0 8 (object header: mark) 0x000000001af67005 (biased: 0x000000000006bd9c; epoch: 0; age: 0)
8 4 (object header: class) 0x2000c205
12 4 (object alignment gap)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
before
com.yqj.concurrent2.Dog object internals:
OFF SZ TYPE DESCRIPTION VALUE
0 8 (object header: mark) 0x000000001af67005 (biased: 0x000000000006bd9c; epoch: 0; age: 0)
8 4 (object header: class) 0x2000c205
12 4 (object alignment gap)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
in
com.yqj.concurrent2.Dog object internals:
OFF SZ TYPE DESCRIPTION VALUE
0 8 (object header: mark) 0x000000001b83f630 (thin lock: 0x000000001b83f630)
8 4 (object header: class) 0x2000c205
12 4 (object alignment gap)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
after
com.yqj.concurrent2.Dog object internals:
OFF SZ TYPE DESCRIPTION VALUE
0 8 (object header: mark) 0x0000000000000001 (non-biasable; age: 0)
8 4 (object header: class) 0x2000c205
12 4 (object alignment gap)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
- 调用wait/notify
批量重偏向
使用场景:当对象被多个线程访问,但不存在竞争,这时偏向了t1线程的对象仍然有机会重新偏向t2,重偏向会重置对象的ID。(当撤销偏向锁第20次后,会给之后的这些对象加锁时重新偏向至加锁线程)
批量撤销
使用场景:当撤销偏向锁再超过40次后(批量重偏向后),整个类的所有对象都会变为不可偏向的,新建的对象也是不可偏向的
5 wait/notify
5.1 wait/notify 原理

- Owner 线程发现条件不满足,调用 wait 方法,即可进入 WaitSet 变为 WAITING 状态
- BLOCKED 和 WAITING 的线程都处于阻塞状态,不占用 CPU 时间片
- BLOCKED 线程会在 Owner 线程释放锁时唤醒
- WAITING 线程会在 Owner 线程调用 notify 或 notifyAll 时唤醒,但唤醒后并不意味者立刻获得锁,仍需进入 EntryList 重新竞争
5.2 wait/notify 使用
属于Object对象的方法,必须获得该对象锁时,才可以调用这几个方法。
public class TestWaitNotify{
private static final Object lock = new Object();
public static void main(String[] args) throws InterruptedException {
List<Thread> list = new ArrayList<>();
//创建5个线程,并启动
for (int i = 0; i < 5; i++) {
Thread t = new Thread(() -> {
synchronized (lock) {
System.out.println("run...");
try {
lock.wait(); //当前线程等待
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + " other...");
}
});
list.add(t);
t.start();
}
TimeUnit.SECONDS.sleep(1);
synchronized (lock) {
System.out.println("wake up...");
// lock.notify(); //唤醒lock对象上的一个线程
lock.notifyAll(); //唤醒lock对象上的全部等待的线程
}
}
}
5.3 与sleep方法的区别
- sleep是Thread方法,而wait是Object方法
- sleep不需要和synchronized配合使用,但wait需要和synchronized一起使用
- sleep睡眠时不会释放对象锁,但wait在等待的时候会释放对象锁
5.4 wait/notify使用的正确方式
synchronized(lock) {
while(条件不成立) {
lock.wait();
}
// 其他操作
}
//另一个线程
synchronized(lock) {
lock.notifyAll();
}
6 park/unpark
6. 1 park/unpark原理
每个线程都有自己的一个Parker对象,该对象由三部分组成,包括 _counter, _cond, _mutex。
三种情况:
- 先调用 park() 阻塞线程

- 当前线程调用park方法
- 检查 _counter,本情况为0,此时获得 _mutex互斥锁
- 线程进入 _cond 条件变量阻塞
- 设置 _counter = 0
- 线程已经处于阻塞状态,调用unpark()恢复线程

- 调用 unpark方法,设置 _counter = 1
- 唤醒 _cond 条件变量中的线程
- 线程恢复运行
- 设置 _counter = 0
- 先调用unpark(),后调用 park()

- 调用 unpark方法,设置 _counter = 1
- 当前线程调用 park方法
- 检查 _counter,此时为1,线程无需阻塞,继续运行
- 设置 _counter = 0
6.2 park/unpark使用
//测试实例
public class TestLock {
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread() {
@Override
public void run() {
System.out.println("thread start");
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("park");
LockSupport.park();
System.out.println("continue");
}
};
t1.start();
//TimeUnit.SECONDS.sleep(1); //在线程t1 park前 先unpark
TimeUnit.SECONDS.sleep(3); //在线程t1 park后 再unpark
System.out.println("unpark");
LockSupport.unpark(t1);
}
}
6.3 与 wait/nodify 的比较
- wait/notify必须配合 Monitor管程一起使用,而 park/unpark不需要
- park/unpark是以线程为单位来阻塞和唤醒指定的线程,而notify只能随机唤醒等待线程,notifyAll只能唤醒全部等待的线程,不够精确
- park/unpark可以先unpark,而 wait/notify 不能先 notify
7 线程状态

几种可以实现状态转换的方法总结:
- New -> Runnable
- 调用 t.start() 方法
- Runnable -> Waiting
-
获取对象锁后,调用 obj.wait() 方法。调用 obj.notify(),obj.notifyAll(),t.interrupt()时,竞争锁成功则线程从 Waiting -> Runnable;竞争锁失败则线程从 Waiting -> Blocked
-
当前线程调用 t.join()方法。当t线程运行结束或者调用了当前线程的interrupt(),让目标线程从 Waiting -> Runnable
-
当前线程调用 LockSupport.park()方法。调用 LockSupport.unpark(目标线程) 或者调用了线程的interrupt(),则会让目标线程从 Waiting -> Runnable
- Runnable -> Timed_Waiting
- 获取对象锁后,调用 obj.wait(long n) 方法。t线程等待时间超过n毫秒,或调用 obj.notify(),obj.notifyAll(),t.interrupt()时,竞争锁成功则线程从 Timed_Waiting -> Runnable;竞争锁失败则线程从 Timed_Waiting -> Blocked
- 当前线程调用 t.join(long n)方法。t线程等待时间超过n毫秒,或当 t 线程运行结束或者调用当前线程的interrupt(),让目标线程从 Timed_Waiting -> Runnable
- 当前线程调用 Thread.sleep(long n) 方法。当前线程等待时间超过了 n 毫秒,让目标线程从 Timed_Waiting -> Runnable
- 当前线程调用 LockSupport.parkNanos(long nanos)等方法。当前线程等待超时,或调用 LockSupport.unpark(目标线程) 或者调用了线程的interrupt(),则会让目标线程从Timed_Waiting -> Runnable
- Runnable -> Blocked
- t 线程调用 synchronized(obj) 获取了对象锁时如果竞争失败,从Runnable -> Blocked。持对象锁的线程执行完同步代码块后,会唤醒Blocked的线程重新竞争,如果t线程竞争成功,则从 Blocked -> Runnable
- Runnable -> Terminated
- 当前线程执行完所有代码
8 活跃性
8.1 死锁
一个线程同时需要获取多把锁,此时便容易发生死锁
- 避免死锁需要注意加锁顺序
- 如果由于某个线程进入了死循环,导致其它线程一直等待,对于这种情况 linux 下可以通过 top 先定位到 CPU 占用高的 Java 进程,再利用 top -Hp 进程id 来定位是哪个线程,最后再用 jstack 排查
死锁测试实例
//测试实例,t1线程获得了lockA,但无法获得lockB。t2线程获得了lockB,但无法获得lockA
public class TestLock {
private static final Object lockA = new Object();
private static final Object lockB = new Object();
public static void main(String[] args) {
Thread t1 = new Thread(() -> {
synchronized (lockA){
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("t1已获得lockA");
synchronized (lockB){
System.out.println("t1已获得lockB");
}
}
}, "t1");
Thread t2 = new Thread(() -> {
synchronized (lockB){
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("t2已获得lockB");
synchronized (lockA){
System.out.println("t2已获得lockA");
}
}
}, "t2");
t1.start();
t2.start();
}
}
哲学家就餐问题(死锁)
public class TestLock {
public static void main(String[] args) {
Chopstick c1 = new Chopstick("1");
Chopstick c2 = new Chopstick("2");
Chopstick c3 = new Chopstick("3");
Chopstick c4 = new Chopstick("4");
Chopstick c5 = new Chopstick("5");
new Philosopher("t1", c1, c2).start();
new Philosopher("t2", c2, c3).start();
new Philosopher("t3", c3, c4).start();
new Philosopher("t4", c4, c5).start();
new Philosopher("t5", c5, c1).start();
}
}
class Chopstick {
String name;
public Chopstick(String name) {
this.name = name;
}
@Override
public String toString() {
return "Chopstick{" +
"name='" + name + '\'' +
'}';
}
}
class Philosopher extends Thread {
String name;
Chopstick left;
Chopstick right;
public Philosopher(String name, Chopstick left, Chopstick right) {
this.name = name;
this.left = left;
this.right = right;
}
private void eat() {
System.out.println(currentThread().getName() + " eat...");
try {
sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
@Override
public void run() {
while (true) {
synchronized (left) {
synchronized (right) {
eat();
}
}
}
}
@Override
public String toString() {
return "Philosopher{" +
"name='" + name + '\'' +
", left=" + left +
", right=" + right +
'}';
}
}
8.2 活锁
活锁出现在两个线程互相改变对付的结束条件,最后均无法结束的现象
public class TestLock {
private static int count = 10;
public static void main(String[] args) {
Thread t1 = new Thread(() -> {
while (count > 0) {
try {
Thread.sleep(200);
} catch (InterruptedException e) {
e.printStackTrace();
}
count--;
System.out.println(count);
}
}, "t1");
Thread t2 = new Thread(() -> {
while (count < 20) {
try {
Thread.sleep(200);
} catch (InterruptedException e) {
e.printStackTrace();
}
count++;
System.out.println(count);
}
}, "t2");
t1.start();
t2.start();
}
}
8.3 饥饿
一个线程由于优先级太低,始终得不到CPU调度执行,也不能结束
9 ReentrantLock
相当于synchronized的改进版本,具有如下特点:
- 可中断
- 可以设置超时时间
- 可以设置为公平锁
- 支持多个条件变量
- 两者都是可重入的
//ReentrantLock语法
// 获取锁
reentrantLock.lock();
try {
// 临界区
} finally {
// 释放锁
reentrantLock.unlock();
}
9.1 可重入
可重入是指同一个线程如果首次获得了这把锁,那么因为它是这把锁的拥有者,因此有权利再次获取这把锁 如果是不可重入锁,那么第二次获得锁时,自己也会被锁挡住
//测试实例,由于可重入性,在一个线程执行过程中的两个方法均可以正常获取锁并执行
public class TestLock {
private static final ReentrantLock lock = new ReentrantLock();
public static void main(String[] args) {
method1();
}
private static void method1() {
lock.lock();
try {
System.out.println("method1");
method2();
}finally {
lock.unlock();
}
}
private static void method2() {
lock.lock();
try {
System.out.println("method2");
}finally {
lock.unlock();
}
}
}
9.2 可打断
加锁等待时,可以通过异常打断
//测试实例,主线程先加锁,t1线程没有获得锁等待,当主线程发送打断后,t1线程可以被成功打断
public class TestLock {
private static ReentrantLock lock = new ReentrantLock();
public static void main(String[] args) {
Thread t1 = new Thread(() -> {
System.out.println("start");
try {
lock.lockInterruptibly();
} catch (InterruptedException e) {
System.out.println("lock interrupted");
return;
}
try {
System.out.println("run...");
}finally {
lock.unlock();
}
}, "t1");
lock.lock();
System.out.println("main lock");
t1.start();
try {
Thread.sleep(2000);
t1.interrupt();
System.out.println("interrupt t1");
} catch (InterruptedException e) {
e.printStackTrace();
}finally {
lock.unlock();
}
}
}
9.3 锁超时
- 立刻失败,没有获得锁立刻返回false
//测试实例,主线程先加锁,t1线程没有获得锁,尝试加锁失败返回
public class TestLock {
private static final ReentrantLock lock = new ReentrantLock();
public static void main(String[] args) {
Thread t1 = new Thread(() -> {
if (!lock.tryLock()){
System.out.println("lock fail");
return;
}
try {
System.out.println("run...");
}finally {
lock.unlock();
}
}, "t1");
lock.lock();
System.out.println("main lock");
t1.start();
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}finally {
lock.unlock();
}
}
}
- 超时失败,等待一段时间后,没有获得锁返回false
//测试实例,主线程先加锁,t1线程没有获得锁,等待指定时间后若依旧没有获得锁则加锁失败返回
public class TestLock {
private static final ReentrantLock lock = new ReentrantLock();
public static void main(String[] args) {
Thread t1 = new Thread(() -> {
try {
if (!lock.tryLock(1,TimeUnit.SECONDS)){
System.out.println("lock fail");
return;
}
} catch (InterruptedException e) {
e.printStackTrace();
}
try {
System.out.println("run...");
}finally {
lock.unlock();
}
}, "t1");
lock.lock();
System.out.println("main lock");
t1.start();
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}finally {
lock.unlock();
}
}
}
- 使用ReentrantLock解决哲学家就餐问题
public class TestLock {
public static void main(String[] args) {
Chopstick c1 = new Chopstick("1");
Chopstick c2 = new Chopstick("2");
Chopstick c3 = new Chopstick("3");
Chopstick c4 = new Chopstick("4");
Chopstick c5 = new Chopstick("5");
new Philosopher("t1", c1, c2).start();
new Philosopher("t2", c2, c3).start();
new Philosopher("t3", c3, c4).start();
new Philosopher("t4", c4, c5).start();
new Philosopher("t5", c5, c1).start();
}
}
//继承了ReentrantLock类
class Chopstick extends ReentrantLock {
String name;
public Chopstick(String name) {
this.name = name;
}
@Override
public String toString() {
return "Chopstick{" +
"name='" + name + '\'' +
'}';
}
}
class Philosopher extends Thread {
String name;
Chopstick left;
Chopstick right;
public Philosopher(String name, Chopstick left, Chopstick right) {
this.name = name;
this.left = left;
this.right = right;
}
private void eat() {
System.out.println(currentThread().getName() + " eat...");
try {
sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
@Override
public void run() {
while (true) {
//尝试加锁,加锁失败直接放弃,避免死锁
if (left.tryLock()) {
try {
if (right.tryLock()) {
try {
eat();
} finally {
right.unlock();
}
}
} finally {
left.unlock();
}
}
}
}
@Override
public String toString() {
return "Philosopher{" +
"name='" + name + '\'' +
", left=" + left +
", right=" + right +
'}';
}
}
9.4 公平锁
ReentrantLock默认是不公平的,不建议使用公平锁,会降低程序的并发度
//设置为公平锁的方式
ReentrantLock lock = new ReentrantLock(true);
9.5 条件变量
- synchronized 中也有条件变量,就是我们讲原理时那个 waitSet 休息室,当条件不满足时进入 waitSet 等待
- ReentrantLock 的条件变量比 synchronized 强大之处在于,它是支持多个条件变量
- synchronized 是那些不满足条件的线程都在一间休息室等消息
- ReentrantLock 支持多间休息室,有专门等烟的休息室、专门等早餐的休息室、唤醒时也是按休息室来唤醒
//测试实例
public class TestLock {
private static final ReentrantLock lock = new ReentrantLock();
private static Condition waitCigaretteQueue = lock.newCondition();
private static Condition waitBreakfastQueue = lock.newCondition();
private static volatile boolean hasCigarette = false;
private static volatile boolean hasBreakfast = false;
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> {
lock.lock();
try {
System.out.println("wait cigarette");
while (!hasCigarette){
try {
waitCigaretteQueue.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("has Cigarette");
}
}finally {
lock.unlock();
}
}, "t1");
Thread t2 = new Thread(() -> {
lock.lock();
try {
System.out.println("wait breakfast");
while (!hasBreakfast){
try {
waitBreakfastQueue.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("has breakfast");
}
}finally {
lock.unlock();
}
}, "t2");
t1.start();
t2.start();
TimeUnit.SECONDS.sleep(1);
sendCigarette();
TimeUnit.SECONDS.sleep(1);
sendBreakfast();
}
private static void sendBreakfast() {
lock.lock();
try {
System.out.println("send breakfast");
hasBreakfast = true;
waitBreakfastQueue.signal();
}finally {
lock.unlock();
}
}
private static void sendCigarette() {
lock.lock();
try {
System.out.println("send cigarette");
hasCigarette = true;
waitCigaretteQueue.signal();
}finally {
lock.unlock();
}
}
}
问题
- synchronized的理解(线程八锁问题)
- 如果把 synchronized(obj) 放在 for 循环的外面,表示对整个for循环加锁,原子性
- 如果 t1 synchronized(obj1) 而 t2 synchronized(obj2),表示分别对不同的对象加锁,无效。锁对象
- 如果 t1 synchronized(obj) 而 t2 没有加,t2不遵守规则随便访问,锁对象
网友评论