一、共享带来的问题
1.临界区
- 当多个线程同时对共享资源进行读写操作时会发生指令交错,导致数据错误。
- 一段代码块内如果存在对共享资源的多线程读写操作,称这段代码块为临界区
例如下面的例子:
package panw.monitor;
import lombok.extern.slf4j.Slf4j;
@Slf4j
public class Test1{
public static int count;
public static void main(String[] args) {
Thread t1 = new Thread(() -> {
for (int i = 0; i < 10000; i++) {
count++;
}
});
Thread t2 = new Thread(() -> {
for (int i = 0; i < 10000; i++) {
count--;
}
});
t1.start();
t2.start();
try {
t1.join();
t2.join();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
log.debug("count={}",count);
}
}
2.竞态条件
多个线程在临界区内执行,由于代码的执行序列不同而导致结果无法预测,称之为发生了竞态条件
二、synchronized 解决方案
1.解决方案
为了避免临界区竞态条件发生,有下面几种方法实现。
- 阻塞:synchronized 、Lock锁
- 非阻塞: cas、原子变量
本章课使用阻塞式的解决方案:**synchronized**
,来解决上述问题,即俗称的【对象锁】,它采用互斥的方式让同一 时刻至多只有一个线程能持有【对象锁】,其他线程想要再获取该【对象锁】就会阻塞,保证了拥有锁的线程可以安全执行完临界区代码。
2.synchronized语法
例:
package panw.monitor;
import lombok.extern.slf4j.Slf4j;
@Slf4j
public class Test1 {
public static int count;
public static final Object lock = new Object();
public static void main(String[] args) {
Thread t1 = new Thread(() -> {
for (int i = 0; i < 10000; i++) {
synchronized (lock) {
count++;
}
}
});
Thread t2 = new Thread(() -> {
for (int i = 0; i < 10000; i++) {
synchronized (lock) {
count--;
}
}
});
t1.start();
t2.start();
try {
t1.join();
t2.join();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
log.debug("count={}", count);
}
}
3.synchronized加在方法上
加在普通方法上:
package panw.monitor;
public class SynchronizedTest {
public synchronized void add() {
System.out.println("add");
}
//等价于
public void add() {
synchronized (this) {
System.out.println("add");
}
}
}
加在静态方法上:
package panw.monitor;
public class SynchronizedTest {
public synchronized static void add() {
System.out.println("add");
}
//等价于
public static void add() {
synchronized (SynchronizedTest.class) {
System.out.println("add");
}
}
}
三、变量的线程安全分析
1.成员变量和静态变量是否线程安全?
- 如果他们没有被多个线程共享,则线程安全
如果他们被共享了,又分两种情况
局部变量时线程安全的
- 如果局部变量引用的对象则有可能又线程安全问题
每个方法都会在各自栈的栈帧中创建局部变量表,不会被其他线程共享
3.常见线程安全类
String
- Integer
- StringBuffer
- Random
- Vector (List的线程安全实现类)
- Hashtable (Hash的线程安全实现类)
-
4.不可变类线程安全性
String、Integer 等都是不可变类,因为其内部的状态不可以改变,因此它们的方法都是线程安全的
有同学或许有疑问,String
有replace
,substring
等方法【可以】改变值啊,那么这些方法又是如何保证线程安全的呢?
这是因为这些方法的返回值都创建了一个新的对象,而不是直接改变String
、Integer
对象本身。四、Monitor
原理
当线程执行到临界区代码时,如果使用了
synchronized
,会先查询synchronized
中绑定的对象是否绑定了一个Monitor
。- 如果没有绑定,则先去和
Monitor
进行绑定,并且将Owner
设为当前线程 - 如果已经绑定,则会去查询该
Monitor
是否已经又Owner
- 如果没有,
Owner
与当前线程绑定 - 如果有,则将该线程放进
EntryList
,进入阻塞状态(Blocked)
- 如果没有,
- 如果没有绑定,则先去和
- 当
Monitor
的Owner
将临界区中代码执行完毕后,Owner便会被清空,此时EntryList
中处于阻塞状态的线程会被叫醒并竞争。 注意:
MarkWord 保存对象自身的运行时数据。
Class Pointer 存储对象的类型指针,该指针指向它的类元数据
1.Mark Word格式
这里我们主要来看一下Mark Word的格式:
解释一下上面的具体意思:unused:代表还未使用
- hashcode:该对象的hashcode
- age:涉及JVM中的垃圾回收,指分代的年龄
- biased_lock:偏向锁标记
- thread:当前得到锁线程
- epoch:偏向锁在CAS锁操作过程中,偏向性标识,表示对象更偏向哪个锁。
- ptr_to_lock_record:轻量级锁中指向线程栈中Lock Record的指针
ptr_to_heavyweight_monitor:指向互斥锁(重量级锁)的指针
2.轻量级锁
使用场景:当一个对象被多个线程所访问,但访问的时间是错开的(不存在竞争),此时就可以使用轻量级锁来优化
创建锁记录(Lock Record)对象,每个线程的栈帧都会包含一个锁记录对象,内部可以存储锁定对象的
Mark Word
(不再一开始就使用Monitor)
- 让锁记录中的
Object Reference
指向锁对象(Object),并尝试用cas
去替换Object
中的Mark Word
,将此Mark Word
放入Lock Record
中保存
注:cas(compare and swap)是一种原子操作,现在你只需要知道它可以将数据进行原子性数据交换即可。
- 如果cas替换成功,则将
Object
的对象头替换为锁记录的地址和状态 00(轻量级锁状态),并由该线程给对象加锁
- 如果cas替换替换失败即对象头已经是00这时还有两种情况
- 如果是其他线程已经持有了
Object
的轻量级锁,这表明当前又竞争,进入锁膨胀过程。 - 如果是自己执行了
synchronized
锁重入,那么会在添加一条Lock Word
作为冲入的计数,该Lock Work
的锁记录的地址为null
- 如果是其他线程已经持有了
- 当退出
synchronized
代码块(解锁时)如果又取值为null
的锁记录,标识有重入,这时重置锁记录直接将该Lock Record
清掉,重入计数减一 当退出
synchronized
代码块(解锁时)锁记录不为null
,这时使用cas将Mark Work
恢复给对象头如果一个线程在给一个对象加轻量级锁时,cas替换操作失败(因为此时其他线程已经给对象加了轻量级锁),此时该线程就会进入锁膨胀,将轻量级锁变成重量级锁
- 此时便会给对象加上重量级锁(使用Monitor)
- 为Object对象申请Monitor锁,将对象头的
Mark Word
改为Monitor
的地址,并且状态改为10(重量级锁) - 并且自己进入
EntryList
中,并进入阻塞状态(blocked)
- 为Object对象申请Monitor锁,将对象头的
当
Thread-0
退出代码块解锁时,先尝试使用cas将Mark Word
的值恢复给对象头,失败,这时进入重量级锁解锁流程,即,通过Monitor
地址找到Monitor
,然后将Owner
置为null
,唤醒EntryList
中Blocked的线程4.自旋锁
重量级锁竞争时,为了提高cpu利用率避免,减少线程上下文切换,可以使用自旋锁来进行优化。
自旋锁指 自己在获取锁失败时进行自旋,如果在自旋过程中使用锁线程退出同步块,释放了锁,就可以不用进入阻塞状态。5.偏向锁
轻量级锁在没有竞争时,每次重入操作都需要进行cas操作,降低性能。
所以引入了偏向锁对性能进行优化:在第一次cas时会将线程的ID写入对象的Mark Word中。此后发现这个线程ID就是自己的,就表示没有竞争,就不需要再次cas,以后只要不发生竞争,这个对象就归该线程所有。如果开启了偏向锁(默认开启),在创建对象时,对象的Mark Word后三位应该是101
- 但是偏向锁默认是有延迟的,不会再程序一启动就生效,而是会在程序运行一段时间(几秒之后),才会对创建的对象设置为偏向状态
如果没有开启偏向锁,对象的Mark Word后三位应该是001
撤销偏向
以下几种情况会使对象的偏向锁失效
调用对象的hashCode方法
- 多个线程使用该对象
- 调用了wait/notify方法(调用wait方法会导致锁膨胀而使用重量级锁)
JVM设置:
-XX:- UseBiasedLocking=false
六、Wait/Notify
1.原理
锁对象调用
wait
方法(obj.wait),就会使当前线程进入WaitSet
中,变为WAITING状态。- 处于BLOCKED和WAITING状态的线程都为阻塞状态,CPU都不会分给他们时间片。但是有所区别:
- BLOCKED状态的线程是在竞争对象时,发现
Monitor
的Owner
已经是别的线程了,此时就会进入EntryList
中,并处于BLOCKED状态 - WAITING状态的线程是获得了对象的锁,但是自身因为某些原因需要进入阻塞状态时,锁对象调用了wait方法而进入了
WaitSet
中,处于WAITING状态
- BLOCKED状态的线程是在竞争对象时,发现
- BLOCKED状态的线程会在锁被释放的时候被唤醒,但是处于WAITING状态的线程只有被锁对象调用了
notify
方法(obj.notify/obj.notifyAll
),才会被唤醒。2.Wait/Notify使用
注:只有当对象被锁以后,才能调用wait和notify方法 ```java package panw.monitor;
public class WaitNotifyTest { final static Object lock = new Object(); static int count; public static void main(String[] args) throws InterruptedException { Thread t1 = new Thread(() -> { while (true) { synchronized (lock) { try { count ++; System.out.println(“t1 count = “ + count); lock.wait(); } catch (InterruptedException e) { throw new RuntimeException(e); } } } }); t1.start(); Thread.sleep(2000); synchronized (lock) {
lock.notify();
}
}
}
<a name="oew4k"></a>
### 3.Wait与Sleep的区别
<a name="eoxGi"></a>
#### 不同点
- Sleep是Thread类的静态方法,Wait是Object的方法,Object又是所有类的父类,所以所有类都有Wait方法。
- Sleep在阻塞的时候不会释放锁,而Wait在阻塞的时候会释放锁
- Sleep不需要与synchronized一起使用,而Wait需要与synchronized一起使用(对象被锁以后才能使用)
<a name="MFQIi"></a>
#### 相同点
- 阻塞状态都为**TIMED_WAITING**
<a name="onSXk"></a>
### 4.虚假唤醒
当有**多个**线程在运行时,对象调用了wait方法,此时这些线程都会进入WaitSet中等待。如果这时使用了**notify**方法,可能会造成**虚假唤醒**(唤醒的不是满足条件的等待线程),这时就需要使用**notifyAll**方法,并进行虚假唤醒的处理。
<a name="PIfw3"></a>
## 七、模式之保护性暂停
<a name="WRgAL"></a>
### 1.定义
用在一个线程等待另一个线程执行结果。<br />如下图:<br />![](https://cdn.nlark.com/yuque/0/2022/jpeg/28810082/1653008406826-290714e8-3778-497b-99b3-e61cd9c89611.jpeg)
<a name="pCoSE"></a>
### 2.具体代码实现
```java
package panw.model;
import lombok.extern.slf4j.Slf4j;
import java.util.HashMap;
@Slf4j
public class Guarded {
public static void main(String[] args) {
HashMap<String, String> map = new HashMap<>();
map.put("a", "b");
GuardedObject guardedObject = new GuardedObject();
Thread t1 = new Thread(() -> {
log.debug("Thread 1 started");
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
}
log.debug("Thread 1 send message"+map);
guardedObject.setResponse(map);
});
Thread t2 = new Thread(() -> {
log.debug("Thread 2 started");
Object response = guardedObject.getResponse();
log.debug("Response: {}", response);
});
t1.start();
t2.start();
}
}
class GuardedObject {
private Object response;
public Object getResponse() {
while (response == null) {
synchronized (this) {
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
return response;
}
public void setResponse(Object response) {
synchronized (this) {
this.response = response;
this.notifyAll();
}
}
@Override
public String toString() {
return "GuardedObject{" +
"response=" + response +
'}';
}
}
3.超时判断
但是政策情况下,如果一个线程没获得另一个线程结果不可能一直死等下去,因此需要进行超时判断,如下进行getResponse()
的修改:
public Object getResponse(long timeout) {
long base = System.currentTimeMillis();
long passTime = 0;
while (response == null) {
long waitTime = timeout - passTime;
if (waitTime <=0) {
break;
}
synchronized (this) {
try {
this.wait(waitTime);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
passTime = System.currentTimeMillis() - base;
}
return response;
}
4.join源码
看了以上的分析,你应该对wait/notify有了更深的了解,下面我们就来看看Thread
中join
方法的源码是如何实现的。
public final void join() throws InterruptedException {
join(0);
}
public final synchronized void join(long millis)
throws InterruptedException {
long base = System.currentTimeMillis();
long now = 0;
if (millis < 0) {
throw new IllegalArgumentException("timeout value is negative");
}
if (millis == 0) {
while (isAlive()) {
wait(0);
}
} else {
while (isAlive()) {
long delay = millis - now;
if (delay <= 0) {
break;
}
wait(delay);
now = System.currentTimeMillis() - base;
}
}
}
public final synchronized void join(long millis, int nanos)
throws InterruptedException {
if (millis < 0) {
throw new IllegalArgumentException("timeout value is negative");
}
if (nanos < 0 || nanos > 999999) {
throw new IllegalArgumentException(
"nanosecond timeout value out of range");
}
if (nanos >= 500000 || (nanos != 0 && millis == 0)) {
millis++;
}
join(millis);
}
一共重载了三个方法,本质上都是调用join(long millis)
,具体实现和上面大同小异。
八、park/unpark
park
和unpark
方法是工具类LockSupport
中的API,它的作用很简单,就是挂起和继续执行线程。
//暂停线程运行
LockSupport.park;
//恢复线程运行
LockSupport.unpark(thread);
1.简单使用
package panw.monitor;
import lombok.extern.slf4j.Slf4j;
import java.util.concurrent.locks.LockSupport;
@Slf4j
public class ParkTest {
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> {
log.debug("t1 park");
LockSupport.park();
log.debug("t1 unpark");
});
t1.start();
Thread.sleep(5000);
LockSupport.unpark(t1);
}
}
2.与wait/notify区别
wait
,notify
和notifyAll
必须配合**Object **``**Monitor**
一起使用,而park
,unpark
不必- park ,unpark 是以线程为单位来阻塞和唤醒线程,而 notify 只能随机唤醒一个等待线程,notifyAll 是唤醒所有等待线程,就不那么精确
- park & unpark 可以先 unpark,而 wait & notify 不能先 notify
这个下面讲原理时会进行详细分析
-
3.原理
LockSupport
中的park
和unpark
方法时调用Unsafe类中的本地方法,我们具体来分析一下park对象:
每个线程都有一个自己的Park对象,并且该对象_counter, _cond,__mutex组成
_counter相当于一个标记位,_cond相当于阻塞队列 线程启动时,会将_counter置为0;
- 重要调用park时,查看_counter是否为0,如果是,就将线程放进阻塞队列_cond中,并将_counter再次置为0,如果之前已经调用过unpark方法,则_counter为1,那就直接将_counter置为0,不阻塞继续运行。
- 调用unpark方法后,会将_counter的值设置为1
- 去唤醒阻塞队列_cond中的线程
- 线程继续运行并将_counter的值设为0
正常先调用park
在调用unpark
过程如下面两图:
下面是先调用unpark
后调用park
的过程:
九、线程中的状态转换
情况一:NEW –> RUNNABLE
当调用了
t.start()
方法时,由 NEW –> RUNNABLE情况二: RUNNABLE <–> WAITING
当调用了t 线程用 synchronized(obj) 获取了对象锁后
当前线程调用 t.join() 方法时,当前线程从 RUNNABLE –> WAITING
- 注意是当前线程在t 线程对象的监视器上等待
t 线程运行结束,或调用了当前线程的 interrupt() 时,当前线程从 WAITING –> RUNNABLE
情况四: RUNNABLE <–> WAITING
当前线程调用 LockSupport.park() 方法会让当前线程从 RUNNABLE –> WAITING
调用 LockSupport.unpark(目标线程) 或调用了线程 的 interrupt() ,会让目标线程从 WAITING –> RUNNABLE
情况五: RUNNABLE <–> TIMED_WAITING
t 线程用 synchronized(obj) 获取了对象锁后
调用 obj.wait(long n) 方法时,t 线程从 RUNNABLE –> TIMED_WAITING
t 线程等待时间超过了 n 毫秒,或调用 obj.notify() , obj.notifyAll() , t.interrupt() 时
当前线程调用 t.join(long n) 方法时,当前线程从 RUNNABLE –> TIMED_WAITING
- 注意是当前线程在t 线程对象的监视器上等待
当前线程等待时间超过了 n 毫秒,或t 线程运行结束,或调用了当前线程的 interrupt() 时,当前线程从 TIMED_WAITING –> RUNNABLE
情况七:RUNNABLE <–> TIMED_WAITING
当前线程调用 Thread.sleep(long n) ,当前线程从 RUNNABLE –> TIMED_WAITING
当前线程等待时间超过了 n 毫秒,当前线程从 TIMED_WAITING –> RUNNABLE
情况八:RUNNABLE <–> TIMED_WAITING
当前线程调用 LockSupport.parkNanos(long nanos) 或 LockSupport.parkUntil(long millis) 时,当前线 程从 RUNNABLE –> TIMED_WAITING
调用 LockSupport.unpark(目标线程) 或调用了线程 的 interrupt() ,或是等待超时,会让目标线程从 TIMED_WAITING–> RUNNABLE
情况九:RUNNABLE <–> BLOCKED
t 线程用 synchronized(obj) 获取了对象锁时如果竞争失败,从 RUNNABLE –> BLOCKED
- 持 obj 锁线程的同步代码块执行完毕,会唤醒该对象上所有 BLOCKED 的线程重新竞争,如果其中 t 线程竞争 成功,从 BLOCKED –> RUNNABLE ,其它失败的线程仍然 BLOCKED
情况十: RUNNABLE <–> TERMINATED
当前线程所有代码运行完毕,进入 TERMINATED十、活跃性
因为某种原因,使得代码一直无法执行完毕,这样的现象叫做活跃性1.死锁
有这样的情况:一个线程需要同时获取多把锁,这时就容易发生死锁
如: ```java package panw.monitor;
public class DeadLock { private static final Object lock1 = new Object(); private static final Object lock2 = new Object();
public static void main(String[] args) {
new Thread(() -> {
synchronized (lock1) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (lock2) {
System.out.println("Thread1");
}
}
}).start();
new Thread(() -> {
synchronized (lock2) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (lock1) {
System.out.println("Thread2");
}
}
}).start();
}
}
这里就涉及到一个经典的**哲学家就餐问题,**它可以这样表述,假设有五位哲学家围坐在一张圆形餐桌旁,做以下两件事情之一:吃饭,或者思考。吃东西的时候,他们就停止思考,思考的时候也停止吃东西。餐桌上每两位哲学家之间有一只餐叉,哲学家吃东西必须且只能使用左右手两边的两只餐叉。<br />假设某一时刻,所有的哲学家都想要吃东西,他们同时拿起了各自左手边的餐叉,然后当他们想要拿起右手边的餐叉时,发现已经被右边的哲学家拿起来了,如果每一个哲学家都不同意放弃自己已经拿到手的餐叉,则整个系统就无法运行下去了,出现了死锁。
```java
package panw.monitor;
import lombok.extern.slf4j.Slf4j;
import java.util.Random;
public class Test {
public static void main(String[] args) {
Fork a = new Fork("a");
Fork b = new Fork("b");
Fork c = new Fork("c");
Fork d = new Fork("d");
Fork e = new Fork("e");
Philosopher aristotle = new Philosopher("Aristotle", a, b);
Philosopher socrates = new Philosopher("Socrates", b, c);
Philosopher plato = new Philosopher("Plato", c, d);
Philosopher kant = new Philosopher("Kant", d, e);
Philosopher sartre = new Philosopher("Sartre", e, a);
new Thread(aristotle).start();
new Thread(socrates).start();
new Thread(plato).start();
new Thread(kant).start();
new Thread(sartre).start();
}
}
@Slf4j
class Philosopher implements Runnable {
private final String name;
private final Fork leftFork;
private final Fork rightFork;
public Philosopher(String name, Fork leftFork, Fork rightFork) {
this.name = name;
this.leftFork = leftFork;
this.rightFork = rightFork;
}
//思考
public void think() {
try {
// log.debug("{} is thinking {} s", name,2);
Thread.sleep(500);
this.eat();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//吃
public void eat() {
synchronized (leftFork) {
synchronized (rightFork) {
try {
log.debug("{} is eating {} s", name,1);
Thread.sleep(1000);
} catch (InterruptedException e) {
}}
}
this.think();
}
@Override
public void run() {
int k= new Random().nextInt(10);
if(k%2==0){
this.eat();
}else{
this.think();
}
}
}
class Fork{
String name;
public Fork(String name) {
this.name = name;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
2.活锁
活锁出现在两个线程互相改变对方的结束条件,后谁也无法结束。
这里做一个简单的演示,但是下面的修改并不是线程安全的,因为这是非原子操作 :
package panw.monitor;
import lombok.extern.slf4j.Slf4j;
@Slf4j
public class LiveLock {
private static final Object lock = new Object();
private static volatile int count=10;
public static void main(String[] args) {
new Thread(()->{
while (count>0){
log.debug(Thread.currentThread().getName()+" count:"+count);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
count--;
}
}).start();
new Thread(()->{
while (count<100){
log.debug(Thread.currentThread().getName()+" count:"+count);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
count++;
}
}).start();
}
}
十一、ReentrantLock
1.和synchronized相比具有的的特点
- 可中断
- 可以设置超时时间
- 可以设置为公平锁 (先到先得)
-
2.基本用法
//获取ReentrantLock对象
private ReentrantLock lock = new ReentrantLock();
//加锁
lock.lock();
try {
//需要执行的代码
}finally {
//释放锁
lock.unlock();
}
1)可重入
可重入是指同一个线程如果首次获得了这把锁,那么因为它是这把锁的拥有者,因此有权利再次获取这把锁
- 如果是不可重入锁,那么第二次获得锁时,自己也会被锁挡住
2)可打断
如果某个线程处于阻塞状态,可以调用其interrupt
方法让其停止阻塞,获得锁失败 ```java package panw.monitor;
import lombok.extern.slf4j.Slf4j;
import java.util.concurrent.locks.ReentrantLock; @Slf4j public class ReentrantLockTest {
public static void main(String[] args) throws InterruptedException {
ReentrantLock lock = new ReentrantLock();
Thread t1 = new Thread(() -> {
try {
lock.lockInterruptibly();
log.debug("t1 get lock");
Thread.sleep(1000000);
} catch (InterruptedException e) {
log.debug("t1 interrupted");
e.printStackTrace();
} finally {
lock.unlock();
}
});
Thread t2 = new Thread(() -> {
try {
lock.lockInterruptibly();
log.debug("t2 get lock");
while (true) {
log.debug("t2 running");
}
} catch (InterruptedException e) {
log.debug("t2 interrupted");
e.printStackTrace();
return;
} finally {
lock.unlock();
}
});
t1.start();
t2.start();
Thread.sleep(100);
t2.interrupt();
}
}
<a name="m4sH1"></a>
#### 3)锁超时
使用`**lock.tryLock**`方法会返回获取锁是否成功。如果成功则返回true,反之则返回false。<br />并且tryLock方法可以**指定等待时间**,参数为:`tryLock(long timeout, TimeUnit unit)`, 其中timeout为最长等待时间,TimeUnit为时间单位<br />**简而言之**就是:获取失败了、获取超时了或者被打断了,不再阻塞,直接停止运行
```java
package panw.monitor;
import lombok.extern.slf4j.Slf4j;
import java.util.concurrent.locks.ReentrantLock;
@Slf4j
public class ReentrantLockTest {
public static void main(String[] args) throws InterruptedException {
ReentrantLock lock = new ReentrantLock();
lock.lock();
Thread t1 = new Thread(() -> {
try {
if(!lock.tryLock()){
log.debug("t1 get lock failed");
return;
}
log.debug("t1 get lock");
} finally {
lock.unlock();
}
});
t1.start();
Thread.sleep(1000);
lock.unlock();
}
4)公平锁
在构造函数设为true
//默认是不公平锁,需要在创建时指定为公平锁
ReentrantLock lock = new ReentrantLock(true);
5)重要条件变量
synchronized 中也有条件变量,就是我们讲原理时那个 waitSet 休息室,当条件不满足时进入waitSet 等待
ReentrantLock 的条件变量比 synchronized 强大之处在于,它是支持多个条件变量的,这就好比
- synchronized 是那些不满足条件的线程都在一间休息室等消息
- 而 ReentrantLock 支持多间休息室,有专门打游戏的休息室、专门吃东西的休息室、唤醒时也是按休息室来唤醒
使用要点:
await
前需要获得锁await
执行后,会释放锁,进入 conditionObject 等待- 使用
signal
把await
的线程唤醒(或打断、或超时)重新竞争 lock 锁 - 竞争 lock 锁成功后,从
await
后继续执行 ```java package panw.monitor;
import lombok.extern.slf4j.Slf4j;
import java.util.concurrent.locks.Condition; import java.util.concurrent.locks.ReentrantLock; @Slf4j public class ReentrantLockTest {
static volatile boolean flag = false;
public static void main(String[] args) throws InterruptedException {
ReentrantLock lock = new ReentrantLock();
Condition c1 = lock.newCondition();
new Thread(()->{
lock.lock();
try {
while (!flag){
log.debug("不满足条件flag={},等待...",flag);
c1.await();
log.debug("满足条件flag={},继续执行...",flag);
}
} catch (InterruptedException e) {
e.printStackTrace();
} finally{
lock.unlock();
}
}).start();
Thread.sleep(1000);
flag= true;
lock.lock();
try {
c1.signal();
}finally {
lock.unlock();
}
}
<a name="NCsxv"></a>
## 十二、同步模式之顺序控制
学到这了,应该对这章有了清醒的认识,那下面就具体来实践一下!
<a name="WTyI2"></a>
### 首先是使用`wait¬ify`来实现交替输出:
```java
package panw.model;
import lombok.extern.slf4j.Slf4j;
public class SequentialControlWaitNotify {
public static void main(String[] args) {
Symbol symbol = new Symbol(3);
new Thread(()->{
symbol.run("a",1,2);
}).start();
new Thread(()->{
symbol.run("b",2,3);
}).start();
new Thread(()->{
symbol.run("c",3,1);
}).start();
}
}
@Slf4j
class Symbol {
private int flag = 1;
private int loopNum;
public Symbol(int loopNum) {
this.loopNum = loopNum;
}
public synchronized void run(String str,int flag,int nextFlag){
for (int i = 0; i < loopNum; i++) {
while (flag!=this.flag){
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.print(str+"\t");
this.flag = nextFlag;
this.notifyAll();
}
}
public int getFlag() {
return flag;
}
public void setFlag(int flag) {
this.flag = flag;
}
public int getLoopNum() {
return loopNum;
}
public void setLoopNum(int loopNum) {
this.loopNum = loopNum;
}
}
使用await&signal
实现:
package panw.model;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.ReentrantLock;
public class SequentialControlAwaitSignal {
static AwaitSignal awaitSignal = new AwaitSignal(3);
static Condition c1 = awaitSignal.newCondition();
static Condition c2 = awaitSignal.newCondition();
static Condition c3 = awaitSignal.newCondition();
public static void main(String[] args) {
new Thread(()-> awaitSignal.run("a",c1,c2)).start();
new Thread(()-> awaitSignal.run("b",c2,c3)).start();
new Thread(()-> awaitSignal.run("c",c3,c1)).start();
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
awaitSignal.lock();
try {
//唤醒一个等待的线程
c1.signal();
}finally {
awaitSignal.unlock();
}
}
}
class AwaitSignal extends ReentrantLock {
private int loopNum;
public AwaitSignal(int loopNum) {
this.loopNum = loopNum;
}
public void run(String str, Condition condition, Condition nextCondition) {
for (int i = 0; i < loopNum; i++) {
lock();
try {
condition.await();
System.out.print(str+"\t");
nextCondition.signal();
} catch (InterruptedException e) {
throw new RuntimeException(e);
} finally {
unlock();
}
}
}
public int getLoopNum() {
return loopNum;
}
public void setLoopNum(int loopNum) {
this.loopNum = loopNum;
}
}
十三、ThreadLocal(待写)
1.定义
ThreadLocal并不是一个Thread,而是Thread的局部变量
2.作用
ThreadLocal是解决线程安全的另一种途径,它通过为每个线程提供一个独立的变量副本解决了变量并发访问的冲突问题。在很多情况下,ThreadLocal比直接使用synchronized同步机制解决线程安全问题更简单,更方便,且结果程序拥有更高的并发性。
3.源码分析
三个类关系
我们先看看Thread、ThreadLocal、ThreadLocalMap之间的关系:
Thread
public
class Thread implements Runnable {
ThreadLocal.ThreadLocalMap threadLocals = null;
ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;
}
ThreadLocal
public class ThreadLocal<T> {
static class ThreadLocalMap {
static class Entry extends WeakReference<ThreadLocal<?>> {
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
}
}
从上述源码进一步证实了每个线程都有一个成员变量,即ThreadLocalMap;
get
public T get() {
Thread t = Thread.currentThread(); //得到当前线程
ThreadLocalMap map = getMap(t); //获取当前线程的成员变量threadLocals
if (map != null) {
ThreadLocalMap.Entry e = map.getEntry(this); 、、通过当前对象获得值
if (e != null) {
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;
}
}
return setInitialValue(); //设置初始值
}
//ThreadLocal中的getMap
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
//ThreadLocal中的setInitialValue
private T setInitialValue() {
T value = initialValue(); //获得初始值,如果没被重写则尾null
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null) {
map.set(this, value);
} else {
createMap(t, value); //创建ThreadLocalMap
}
if (this instanceof TerminatingThreadLocal) {
TerminatingThreadLocal.register((TerminatingThreadLocal<?>) this);
}
return value;
}
//ThreadLocal中的createMap
void createMap(Thread t, T firstValue) {
t.threadLocals = new ThreadLocalMap(this, firstValue); //将Thread类中的threadLocals进行初始化
}
set
public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t); //获取ThreadLocalMap
if (map != null) {
map.set(this, value); //设置map的kv值
} else {
createMap(t, value);
}
}