并发之共享模型
文章目录
- 并发之共享模型
- 一、多线程带来的共享问题
- 二、解决方案
- 三、方法中的synchronize
- 四、变量的线程安全分析
- 五、习题
- 六、Monitor
- 七、synchronize优化
- 八、wait和notify
- 九、sleep和wait
- 十、park和unpark
- 十一、重新理解线程状态
- 十二、多把锁
- 十三、ReentrantLock
一、多线程带来的共享问题
比如共享资源在被两个线程使用的时候。线程1对变量i=0进行+1操作,但是在+1途中切换到了线程2,线程2对变量i进行-1操作,然后把-1返回到共享变量。但是再切换回线程1已经+1也就是局部变量已经是1,重新赋值给共享变量会导致并发问题
其二是共享资源如果一直被一个线程使用,线程可能会由于sleep,wait,io等操作浪费cpu的使用时间,那么可以把这段时间让给别人
问题分析
i++的字节码是
getstatic i 获取静态变量i,也就是主存的i。
iconst_1 准备常量1
iadd 进行相加
pustatic i
所以我们看到的i++并不是一条原子指令,既然不是原子指令,那么线程自然可以在切换过程中导致这些指令交错执行。最后导致共享资源是个脏数据的问题。
临界区
实际上就是多个线程访问的代码里有共享资源,那么这段代码就是临界区
竟态条件
如果在临界区中多线程发生执行指令序列不同导致结果无法预测的状况就是竟态条件
二、解决方案
①synchronize
线程1上锁后,线程2无法获取锁不能执行临界区,线程2阻塞等待线程1完成释放锁之后才能够使用。可以把synchronize类比为一个房间,每次有锁的人才能够进入房间做事情,就算cpu时间片 用完,只要没有这个锁的钥匙其它线程是无法进入房间的。当用完之后就会释放锁,并且唤醒那些阻塞的线程。
static int count=0;
static Object lock=new Object();
public static void main(String[] args) throws InterruptedException {
Room room = new Room();
Thread t1 = new Thread(() -> {
for (int i = 0; i < 5000; i++) {
// room.increment();
synchronized (lock){
count++;
}
}
}, "t1");
Thread t2 = new Thread(() -> {
for (int i = 0; i < 5000; i++) {
// room.decrement();
synchronized (lock){
count--;
}
}
}, "t2");
t1.start();
t2.start();
t1.join();
t2.join();
// log.debug("{}", room.getCounter());
log.debug("{}",count);
}
思考
①如果synchronize放到for外面?实际上就是要执行完整个for才会把锁放出去-原子性
②如果t1和t2使用的是不同的obj会怎么样?相当于进入了不同的房间,那么锁是没有效果的,两个线程仍然会各自执行临界区的代码块导致执行序列不同。
③如果t1加锁,但是t2没有?那就相当于t2可以随时执行临界区
面向对象改进
实际上就是把那些需要加锁的临界区和共享资源全部封装到一个类上。在方法上加synchronize相当于锁住了this。如果是静态方法那么相当于是锁住了类对象.class
class Room {
private int counter = 0;
public synchronized void increment() {
counter++;
}
public synchronized void decrement() {
counter--;
}
public synchronized int getCounter() {
return counter;
}
}
三、方法中的synchronize
线程八锁
情况1:没有sleep的时候n1执行a和b方法都是锁住自己的对象。所以是互斥的。
情况2:有sleep其实还是一样,执行a和b方法的两个线程看谁先获取到锁。那么谁就先执行。不管里面是不是有sleep,没获取到锁的线程就是要等待
@Slf4j(topic = "c.Test8Locks")
public class Test8Locks {
public static void main(String[] args) {
Number n1 = new Number();
// Number n2 = new Number();
new Thread(() -> {
log.debug("begin");
n1.a();
}).start();
new Thread(() -> {
log.debug("begin");
n1.b();
}).start();
}
}
@Slf4j(topic = "c.Number")
class Number{
public synchronized void a() {
sleep(1);
log.debug("1");
}
public synchronized void b() {
log.debug("2");
}
}
情况3:这种情况c是没有加锁,也就是随意都可以执行。可能出现的结果是3 12,也可能是32 1,也可能是23 1。
@Slf4j(topic = "c.Test8Locks")
public class Test8Locks {
public static void main(String[] args) {
Number n1 = new Number();
// Number n2 = new Number();
new Thread(() -> {
log.debug("begin");
n1.a();
}).start();
new Thread(() -> {
log.debug("begin");
n1.b();
}).start();
new Thread(() -> {
log.debug("begin");
n1.c();
}).start();
}
}
@Slf4j(topic = "c.Number")
class Number{
public synchronized void a() {
sleep(1);
log.debug("1");
}
public synchronized void b() {
log.debug("2");
}
public void c(){
log.debug("3");
}
}
情况4:这种他们绑定的锁都是不同的,相当于就是无锁。最后的结果肯定是21。因为1线程sleep了
@Slf4j(topic = "c.Test8Locks")
public class Test8Locks {
public static void main(String[] args) {
Number n1 = new Number();
Number n2 = new Number();
new Thread(() -> {
log.debug("begin");
n1.a();
}).start();
new Thread(() -> {
log.debug("begin");
n2.b();
}).start();
// new Thread(() -> {
// log.debug("begin");
// n1.c();
// }).start();
}
}
@Slf4j(topic = "c.Number")
class Number{
public synchronized void a() {
sleep(1);
log.debug("1");
}
public synchronized void b() {
log.debug("2");
}
//
// public void c(){
// log.debug("3");
// }
}
情况5-8都是加上了static,那么就根据他们锁的对象来判断是不是同一把锁就可以了
四、变量的线程安全分析
静态变量和成员变量是否有线程安全问题?
如果只是读就没有,如果是读写就要关注临界区
局部变量?
如果是引用类型的话那么就有。
局部变量的值存储在线程的栈帧里面,也就是私有的。而不是像static变量那样先从方法区中取出这个变量然后再进行对应的修改。
情况1:ThreadUnsafe的list是在类里面创建的,那么就会造成,线程处理的是堆里面同一个list导致的线程安全问题
情况2:把list放到method1里面,那么就是一个局部变量的引用,而且每个线程调用方法后都有自己的一个list。那么就不会造成线程安全问题
情况3:有子类重写了这个方法那么也能获取这个list,导致多个线程能够操作list。解决办法就是给方法加上final防止子类重写。
public class TestThreadSafe {
static final int THREAD_NUMBER = 2;
static final int LOOP_NUMBER = 200;
public static void main(String[] args) {
// ThreadSafeSubClass test = new ThreadSafeSubClass();
ThreadUnsafe test=new ThreadUnsafe();
for (int i = 0; i < THREAD_NUMBER; i++) {
new Thread(() -> {
test.method1(LOOP_NUMBER);
}, "Thread" + (i+1)).start();
}
}
}
class ThreadUnsafe {
ArrayList<String> list = new ArrayList<>();
public void method1(int loopNumber) {
for (int i = 0; i < loopNumber; i++) {
method2();
method3();
}
}
private void method2() {
list.add("1");
}
private void method3() {
list.remove(0);
}
}
class ThreadSafe {
public final void method1(int loopNumber) {
ArrayList<String> list = new ArrayList<>();
for (int i = 0; i < loopNumber; i++) {
method2(list);
method3(list);
}
}
public void method2(ArrayList<String> list) {
list.add("1");
}
private void method3(ArrayList<String> list) {
System.out.println(1);
list.remove(0);
}
}
class ThreadSafeSubClass extends ThreadSafe{
// @Override
public void method3(ArrayList<String> list) {
System.out.println(2);
new Thread(() -> {
list.remove(0);
}).start();
}
}
线程安全类
Integer
HashTable
String
Random
Vector
JUC下的类
它们的单个方法是线程安全的,但是多个方法执行的时候就不一样了。
下面的代码出现的问题就是线程1判断成功之后切换,刚好释放了锁,然后就是线程2获取锁进行判断,再次切换线程1获取锁处理put,切换线程2也可以获取锁处理put。因为单个方法执行完就会释放锁。所以这样还是需要整体上加锁才能够继续处理。
不可变类线程安全
String
String和Integer都是不可变的,String本质上就是一个char[]数组。如果是substring方法实际上就是复制一个新的数组出来,然后再给String的char数组进行赋值。replace实际上也是创建数组,然后对比愿数组的旧值,如果是旧值那么直接给新的数组那个位置赋新值。
public String replace(char oldChar, char newChar) {
if (oldChar != newChar) {
int len = value.length;
int i = -1;
char[] val = value; /* avoid getfield opcode */
while (++i < len) {
if (val[i] == oldChar) {
break;
}
}
if (i < len) {
//创建新数组
char buf[] = new char[len];
for (int j = 0; j < i; j++) {
buf[j] = val[j];
}
while (i < len) {
char c = val[i];
//根据原来的数组是旧值的位置改变成新值
buf[i] = (c == oldChar) ? newChar : c;
i++;
}
return new String(buf, true);
}
}
return this;
}
public String substring(int beginIndex) {
if (beginIndex < 0) {
throw new StringIndexOutOfBoundsException(beginIndex);
}
int subLen = value.length - beginIndex;
if (subLen < 0) {
throw new StringIndexOutOfBoundsException(subLen);
}
//实际上就是创建了一个新的String,而不是修改了值
return (beginIndex == 0) ? this : new String(value, beginIndex, subLen);
}
public String(char value[], int offset, int count) {
if (offset < 0) {
throw new StringIndexOutOfBoundsException(offset);
}
if (count <= 0) {
if (count < 0) {
throw new StringIndexOutOfBoundsException(count);
}
if (offset <= value.length) {
this.value = "".value;
return;
}
}
// Note: offset or count might be near -1>>>1.
if (offset > value.length - count) {
throw new StringIndexOutOfBoundsException(offset + count);
}
//实际上就是创建新数组并且进行复制
this.value = Arrays.copyOfRange(value, offset, offset+count);
}
实例分析
这种线程不安全,原因就是这个MyServlet是共享的,而且UserService是在堆里面的。可以被多个线程调用它的方法修改count,可能会引发并发问题。
这种也是有并发问题的,单例共享,可以被多个多线程调用,覆盖start这种变量。
这种不会发生线程安全问题,原因是没有任何变量可以被修改。
这里是会发生线程安全问题,原因是Connection暴露出去,可以被多个线程处理。如果线程1在处理getCon的时候切换到线程2刚好close那么线程1的Connection就没办法继续执行,因为已经被修改了。
这个地方没有线程安全问题,原因是每次都是新创建的一个UserDao相当于是一个局部变量。并不是一个共享资源
这个地方可能会出现线程安全问题是因为通过子类方法把局部变量暴露出去,可能会被子类对象通过线程把这个局部变量进行修改
总结:线程是否安全取决于是否有能够被修改的共享资源
五、习题
出现线程安全问题的是在卖票的时候可能多个线程在卖票,同时取出了共享变量count,最后的count数量就是最后那个线程处理的值。而不是共同处理的值,因为他们的执行序列发生了错误,导致count没有等待处理完就被另外一个线程先读取进来了。
解决办法就是给临界区加锁,实际上就是window的sell。那么为什么不给amountList和window加锁?原因是他们操作的并不是同一个共享资源处理不同自然就不需要加锁。但是之前的HashTable两个操作get和put都是针对同一个共享资源,导致最后的value会被最后的那个线程覆盖。因为判空成功的时候线程1还没有完成put。而且也没有锁住两个操作,而是做一个放一个
@Slf4j(topic = "c.ExerciseSell")
public class ExerciseSell {
public static void main(String[] args) throws InterruptedException {
// 模拟多人买票
TicketWindow window = new TicketWindow(1000);
// 所有线程的集合
List<Thread> threadList = new ArrayList<>();
// 卖出的票数统计
List<Integer> amountList = new Vector<>();
for (int i = 0; i < 2000; i++) {
Thread thread = new Thread(() -> {
// 买票
try {
Thread.sleep(random(10));
} catch (InterruptedException e) {
e.printStackTrace();
}
int amount = window.sell(random(5));
// 统计买票数
amountList.add(amount);
});
threadList.add(thread);
thread.start();
}
for (Thread thread : threadList) {
thread.join();
}
// 统计卖出的票数和剩余票数
log.debug("余票:{}",window.getCount());
log.debug("卖出的票数:{}", amountList.stream().mapToInt(i-> i).sum());
}
// Random 为线程安全
static Random random = new Random();
// 随机 1~5
public static int random(int amount) {
return random.nextInt(amount) + 1;
}
}
// 售票窗口
class TicketWindow {
private int count;
public TicketWindow(int count) {
this.count = count;
}
// 获取余票数量
public int getCount() {
return count;
}
// 售票 synchronized
public int sell(int amount) {
if (this.count >= amount) {
this.count -= amount;
return amount;
} else {
return 0;
}
}
}
Vector自己的方法就已经带锁
public synchronized boolean add(E e) {
modCount++;
ensureCapacityHelper(elementCount + 1);
elementData[elementCount++] = e;
return true;
}
转账问题
这里其实会发生线程安全问题。主要就是在a转账的同时b也进去转账,那么b获取到的肯定就是没有转账的a,相对a也是。那么直接给方法加上synchronize行不行?如果绑定的是本类对象,很明显是不行,因为是两个账户,锁是不同的,进入了不同房间。那么解决方案就是可以使用唯一的Account.class的本类,那么就能够锁住了
@Slf4j(topic = "c.ExerciseTransfer")
public class ExerciseTransfer {
public static void main(String[] args) throws InterruptedException {
Account a = new Account(1000);
Account b = new Account(1000);
Thread t1 = new Thread(() -> {
for (int i = 0; i < 1000; i++) {
a.transfer(b, randomAmount());
}
}, "t1");
Thread t2 = new Thread(() -> {
for (int i = 0; i < 1000; i++) {
b.transfer(a, randomAmount());
}
}, "t2");
t1.start();
t2.start();
t1.join();
t2.join();
// 查看转账2000次后的总金额
log.debug("total:{}", (a.getMoney() + b.getMoney()));
}
// Random 为线程安全
static Random random = new Random();
// 随机 1~100
public static int randomAmount() {
return random.nextInt(100) + 1;
}
}
// 账户
class Account {
private int money;
public Account(int money) {
this.money = money;
}
public int getMoney() {
return money;
}
public void setMoney(int money) {
this.money = money;
}
// 转账
public void transfer(Account target, int amount) {
synchronized(Account.class) {
if (this.money >= amount) {
this.setMoney(this.getMoney() - amount);
target.setMoney(target.getMoney() + amount);
}
}
}
}
六、Monitor
java对象头
包括了markword主要就是存储hashcode,age(gc生命值),biase_lock是不是偏向锁,01加锁的情况
还有就是klassword主要就是指向类对象(类的信息)。
如果是数组那么还包含了数组的长度
Monitor锁
monitor锁是os提供的,成本很高
工作原理
实际上就是把obj的markword前面30bit记录monitor的地址,指向monitor。然后如果有线程要执行临界区代码时就把monitor的owner指向对应的线程。如果又有线程进来,那么看看obj是否关联锁,然后再看看是否有owner,如果有那么就进入到EntryList阻塞等待。等待线程释放锁之后,唤醒EntryList然后重新开始竞争。
字节码的角度
字节码的角度其实就是先把lock的引用复制放到slot1,然后就是monitorentry,把lock的markword指向monitor,并且把hashcode等存入monitor。接着执行业务代码,最后就是取出引用slot1,然后就是monitorexit,解锁。而且对业务代码也就是同步块进行了异常监视,如果出现异常,那么还是会进行解锁操作的
七、synchronize优化
小故事
如果两个线程之间没有竞争那么就可以使用轻量级锁,相当于就是挂书包,如果发现是对方书包那么就在外面等待。后来另一个线程不用了,那么另一个线程就可以刻名字在门外,相当于就是偏向锁,如果这个时候有人来竞争,那么就会升级为书包也就是轻量级锁。后来另一线程回来了,发现那个线程把很多个门都刻上名字,就去找os把那些名字批量刻成自己的。也就是修改偏向锁。最后名字实在刻太多取消了偏向。
偏向锁是单个线程专属的,如果单个线程处理某个代码没有竞争,那么就可以使用偏向锁,如果有竞争那么就可以升级为轻量级锁。
1.轻量级锁
本质就是线程的调用临时区方法的栈帧的锁记录保存对象的引用和对象markword的信息。接着就是把对应锁记录的锁信息与obj进行交换,比如说把01改成了00告诉obj这是一个轻量级锁,而且告诉了obj锁记录的地址,相当于就是给obj贴上是谁的锁的标签。如果是可重入锁,那么锁记录markword部分就是null表示的是这是可重入的,用的是同一个锁。
2.锁膨胀
其实就是竞争轻量级锁的时候,没有地方给竞争的线程放着,那么这个时候就需要把轻量级锁转换成重量级锁monitor,其实就是把obj的markword指向monitor。然后就是monitor的owner指向当前线程的锁记录。把阻塞线程放到等待队列里面。
恢复的时候,CAS尝试把线程的锁记录给恢复过去,但是发现失败。这个时候恢复方式改成了重量级锁的恢复方式,唤醒list,然后owner设置为null,线程重新竞争monitor。如果没有就把monitor保存的hashcode信息恢复。
3.自旋优化
实际上就相当于等红绿灯,如果很快到绿灯就等一会,如果还有很久那么就拉手刹。自旋就是旋多一会等别人释放重量级锁,如果成功一次,那么下次就会确定成功几率加大,自旋多几次。如果没有等到那么就阻塞。
自旋的原因:阻塞会导致线程的上下文切换需要消耗cpu时间和资源。速度相对比较慢。
4.偏向锁
之所以要使用偏向锁是因为轻量级锁的锁重入每次都调用CAS进行对比,CAS是一个OS指令操作所以速度很慢。所以偏向锁是把ThreadId直接赋值给markword,那么下次就能直接在java上对比这个markword。
偏向锁带有延迟性,通常对象创建过一会才会生成
先生成偏向锁-》轻量级锁-》重量级锁
如果给临界区使用偏向锁,那么对应执行线程的id赋值给markword
如果使用了锁的hashcode,那么偏向锁就会被禁止,因为hashcode占用的bit太多
轻量级在锁记录上记录hashcode,重量级在monitor上记录
如果两个线程用同一个偏向级锁,那么锁会变成不可偏向的,升级为轻量级锁
批量重偏向
其实就是多个没有竞争的线程,使用同一个锁,如果jvm发现撤销的锁偏向次数超过20次,那么就会自动偏向另外一个线程。比如t1线程使用一堆锁,锁偏向t1。但如果t2使用这些锁,并且需要撤销锁但偏向超过20次,那么这些锁会全部偏向t2
批量撤销
如果撤销超过40次那么jvm就会撤销所有对象的偏向
5.锁消除
其实就是JIT发现锁的临界区里根本没有共享资源,那么就取消了这个锁
八、wait和notify
小故事
小南需要烟才能工作,但他又要占着锁让别人无法进来。那么这个时候开一个waitSet相当于就是休息室让小南进去休闲,并且释放锁。如果烟到了,那么notify小南就能够继续工作了。
Blocked和Waiting区别
其实就是waiting释放了锁,blocked是没有锁
waiting被notify之后仍然需要进入到entrylist进行等待
wait和notify的规则
线程调用对象wait和notify的时候一定是要使用这个锁成为monitor的主人的时候。这样才能够wait释放锁和被其它获取这个锁的人notify
@Slf4j(topic = "c.TestWaitNotify")
public class TestWaitNotify {
final static Object obj = new Object();
public static void main(String[] args) {
new Thread(() -> {
synchronized (obj) {
log.debug("执行....");
try {
obj.wait(); // 让线程在obj上一直等待下去
} catch (InterruptedException e) {
e.printStackTrace();
}
log.debug("其它代码....");
}
},"t1").start();
new Thread(() -> {
synchronized (obj) {
log.debug("执行....");
try {
obj.wait(); // 让线程在obj上一直等待下去
} catch (InterruptedException e) {
e.printStackTrace();
}
log.debug("其它代码....");
}
},"t2").start();
// 主线程两秒后执行
sleep(0.5);
log.debug("唤醒 obj 上其它线程");
synchronized (obj) {
// obj.notify(); // 唤醒obj上一个线程
obj.notifyAll(); // 唤醒obj上所有等待线程
}
}
}
wait()方法可以限制等待的时间,wait(参数)
九、sleep和wait
区别
sleep:Thread调用,静态方法,而且不会释放锁
wait:所有obj,但是要配合synchronized使用,可以释放锁
扩展
通常锁会加上final防止被修改
正确使用
小南需要烟才能工作,如果是使用sleep不释放锁,那么其它需要等待干活的人就会干等着。但是wait可以让小南释放锁,让其他线程工作,并且唤醒小南
存在问题
会不会有其他线程在等待着锁?如果是那么会不会唤醒错了线程?
@Slf4j(topic = "c.TestCorrectPosture")
public class TestCorrectPostureStep2 {
static final Object room = new Object();
static boolean hasCigarette = false;
static boolean hasTakeout = false;
public static void main(String[] args) {
new Thread(() -> {
synchronized (room) {
log.debug("有烟没?[{}]", hasCigarette);
if (!hasCigarette) {
log.debug("没烟,先歇会!");
try {
room.wait(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
log.debug("有烟没?[{}]", hasCigarette);
if (hasCigarette) {
log.debug("可以开始干活了");
}
}
}, "小南").start();
for (int i = 0; i < 5; i++) {
new Thread(() -> {
synchronized (room) {
log.debug("可以开始干活了");
}
}, "其它人").start();
}
sleep(1);
new Thread(() -> {
synchronized (room) {
hasCigarette = true;
log.debug("烟到了噢!");
room.notify();
}
}, "送烟的").start();
}
}
解决办法
可以通过while多次判断条件是否成立,直接使用notifyAll来唤醒所有的线程。然后线程被唤醒之后可以先再次判断条件是否成立,成立那么往下面执行,如果不成立那么继续wait。
十、park和unpark
与wait和notify的区别
不需要与monitor一起使用
可以精准唤醒和阻塞线程
可以先unpark,但是不能先notify。但是unpark之后park就不起作用了
工作原理
①park,先去到counter里面判断是不是0,如果是那么就让线程进入队列,并且再次把counter设置为0
②unpark,如果线程正在阻塞,那么先把counter置为1,然后唤醒线程,恢复运行,并把counter设置为0
③先unpark后park,那么就unpark补充counter为1,park判断counter是1,认为还有体力就继续执行
十一、重新理解线程状态
情况1:new->runnable
线程start
情况2:runnable->waiting
notify和wait。wait进入阻塞,notify让他们重新竞争锁进入runnable,其他还是进入blocked
情况3
park和unpark
情况4
t.join调用它的线程会进入等待,等待t完成任务或者是被interrupt
情况5-8
其实就是wait,join,sleep加上时间而已,都是从runnable->blocked
情况9
synchronize获取锁失败那么就会进入blocked
情况10
所有代码执行完那么就是terminated
十二、多把锁
一个房间睡觉和学习。但是只有一把锁睡觉的时候不能学习并发度非常低。那么这个时候可以通过细化锁的粒度,分成两把锁,一把是学习房间的锁,一把是卧室的锁,那么就能够让两个功能并发执行。
问题
如果锁太多,一个线程需要多把锁会导致死锁的发生
public class TestMultiLock {
public static void main(String[] args) {
BigRoom bigRoom = new BigRoom();
new Thread(() -> {
bigRoom.study();
},"小南").start();
new Thread(() -> {
bigRoom.sleep();
},"小女").start();
}
}
@Slf4j(topic = "c.BigRoom")
class BigRoom {
private final Object studyRoom = new Object();
private final Object bedRoom = new Object();
public void sleep() {
synchronized (this) {
log.debug("sleeping 2 小时");
Sleeper.sleep(2);
}
}
public void study() {
synchronized (this) {
log.debug("study 1 小时");
Sleeper.sleep(1);
}
}
}
死锁的案例
t1有A但是想要B,t2有B但是想要A
@Slf4j(topic = "c.TestDeadLock")
public class TestDeadLock {
public static void main(String[] args) {
test1();
}
private static void test1() {
Object A = new Object();
Object B = new Object();
Thread t1 = new Thread(() -> {
synchronized (A) {
log.debug("lock A");
sleep(1);
synchronized (B) {
log.debug("lock B");
log.debug("操作...");
}
}
}, "t1");
Thread t2 = new Thread(() -> {
synchronized (B) {
log.debug("lock B");
sleep(0.5);
synchronized (A) {
log.debug("lock A");
log.debug("操作...");
}
}
}, "t2");
t1.start();
t2.start();
}
}
死锁的检查方式
1、jps定位进程id,jstack id来查看程序信息
2、jconsole直接查看进程信息
活锁
其实就是两个线程都在改变对方的解锁条件导致没有释放锁,但没有阻塞。死锁就是含有对方的锁不放开导致线程阻塞。
解决方案
可以通过改变线程执行的时间,让他们交错执行,快速执行解锁条件。
饥饿问题
其实就是线程一直获取(竞争)不到锁导致没有执行。
十三、ReentrantLock
相比synchronize
可以被中断
可以设置获取超时,超时之后就自动放弃获取锁
公平锁,防止饥饿问题
条件变量多
可重入
只要是同一个线程获取同一把锁,那么就能够被使用第二次。(在没有被解锁的时候可被使用第二次)
@Slf4j(topic = "c.test22")
public class MyTest22 {
public static ReentrantLock lock=new ReentrantLock();
public static void main(String[] args) {
lock.lock();
try{
log.debug("开始进入m1");
m1();
}finally {
lock.unlock();
}
}
public static void m1(){
lock.lock();
try {
log.debug("m1进入");
m2();
}finally {
lock.unlock();
}
}
public static void m2(){
lock.lock();
try{
log.debug("m2进入");
}finally {
lock.unlock();
}
}
}
可中断
lockInterrupt,这个方法才能够被其它线程中断等待锁。如果是lock那么就算中断也没有任何效果。这种可中断可以减少死锁的发生。
@Slf4j(topic = "c.test23")
public class MyTest23 {
public static ReentrantLock lock=new ReentrantLock();
public static void main(String[] args) {
Thread t1 = new Thread(() -> {
log.debug("上锁");
lock.lock();
// try {
// lock.lockInterruptibly();
// } catch (InterruptedException e) {
// e.printStackTrace();
// log.debug("无法获取锁");
// }
try {
log.debug("获取到锁");
} finally {
lock.unlock();
}
}, "t1");
t1.start();
lock.lock();
Sleeper.sleep(1);
log.debug("帮助t1解锁");
t1.interrupt();
}
}
超时
这个地方可以使用tryLock来设定获取锁的超时时间,如果超时那么就自动放弃获取锁。而不是一直锁住
@Slf4j(topic = "c.test24")
public class MyTest24 {
public static ReentrantLock lock=new ReentrantLock();
public static void main(String[] args) {
Thread t1 = new Thread(() -> {
log.debug("尝试获得锁");
try {
if(!lock.tryLock(2, TimeUnit.SECONDS)){
log.debug("获取锁失败");
return ;
}
} catch (InterruptedException e) {
e.printStackTrace();
log.debug("获取锁失败1");
return;
}
try{
log.debug("获取锁成功");
}finally {
lock.unlock();
}
}, "t1");
log.debug("main获取锁");
lock.lock();
t1.start();
Sleeper.sleep(1);
log.debug("main解锁");
lock.unlock();
}
}
解决哲学家的问题
思路
可以使用ReentrantLock的tryLock,如果尝试失败你那么就会往下面执行而不是一直等待获得锁,获取不到是不会阻塞线程的。
@Override
public void run() {
while (true) {
if(left.tryLock()){
try{
if(right.tryLock()){
try{
eat();
}finally {
right.unlock();
}
}
}finally {
left.unlock();
}
}
}
}
条件变量
定义
synchronize可以有一把锁,并且通过wait和notify来释放锁进入到waitSet。对于ReentrantLock就相当于有多个休息室waitSet,创建锁之后可以创建多个条件变量(多个房间),可以认为ReentrantLock里面有多个休息室,进入不同的休息室可以通过不同的小锁处理。但实际上释放的锁还是ReentrantLock,然后交给别人使用,只不过通过条件变量可以控制住不同的房间,让同房间但线程区竞争锁。
这里就是使用了条件变量,但他们进入但线程房间不同,操作的方式是相同的。唤醒的房间的线程不相同。
@Slf4j(topic = "c.Test24")
public class Test224 {
static final Object room = new Object();
static boolean hasCigarette = false;
static boolean hasTakeout = false;
static ReentrantLock ROOM = new ReentrantLock();
// 等待烟的休息室
static Condition waitCigaretteSet = ROOM.newCondition();
// 等外卖的休息室
static Condition waitTakeoutSet = ROOM.newCondition();
public static void main(String[] args) {
new Thread(() -> {
ROOM.lock();
try {
log.debug("有烟没?[{}]", hasCigarette);
while (!hasCigarette) {
log.debug("没烟,先歇会!");
try {
waitCigaretteSet.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
log.debug("可以开始干活了");
} finally {
ROOM.unlock();
}
}, "小南").start();
new Thread(() -> {
ROOM.lock();
try {
log.debug("外卖送到没?[{}]", hasTakeout);
while (!hasTakeout) {
log.debug("没外卖,先歇会!");
try {
waitTakeoutSet.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
log.debug("可以开始干活了");
} finally {
ROOM.unlock();
}
}, "小女").start();
sleep(1);
new Thread(() -> {
ROOM.lock();
try {
hasTakeout = true;
waitTakeoutSet.signal();
} finally {
ROOM.unlock();
}
}, "送外卖的").start();
sleep(1);
new Thread(() -> {
ROOM.lock();
try {
hasCigarette = true;
waitCigaretteSet.signal();
} finally {
ROOM.unlock();
}
}, "送烟的").start();
}
}
用锁来固定顺序执行
思路
其实就是两个线程都需要用到这个锁,但是t1线程一定要t2线程运行之后才能运行,那么判断条件就是一个boolean,如果t2运行那么就修改,并且唤醒线程t1。线程t1如果发现t2没有运行那么wait进入等待,如果被虚假唤醒可以通过while来循环进入重新等待。
@Slf4j(topic = "c.25")
public class MyTest25 {
static Object lock=new Object();
static boolean t2Run=false;
public static void main(String[] args) {
Thread t1 = new Thread(() -> {
synchronized (lock){
try {
while(!t2Run){
lock.wait();
}
} catch (InterruptedException e) {
e.printStackTrace();
}
log.debug("1");
}
}, "t1");
Thread t2 = new Thread(() -> {
synchronized (lock){
log.debug("2");
//唤醒线程1
t2Run=true;
lock.notify();
}
}, "t2");
t1.start();
t2.start();
}
}
第二种方法
其实就是使用LockSupport的park方法处理。这种t1如果先执行那么就会park,进入阻塞,然后t2执行之后unpark唤醒t1。如果是t2先执行也没关系,这里线程的unpark会把counter变成1,t1如果park先检查counter发现是1那么就可以继续执行。
@Slf4j(topic = "c.26")
public class MyTest26 {
public static void main(String[] args) {
Thread t1 = new Thread(() -> {
LockSupport.park();
log.debug("1");
}, "t1");
Thread t2 = new Thread(() -> {
log.debug("2");
LockSupport.unpark(t1);
}, "t2");
t1.start();
t2.start();
}
}
轮流打印abc的思路(交替执行)
synchronized方式
其实还是对是否到这个线程的flag进行判断,如果是1那么就t1执行,如果是2那么就t2执行。执行之后还需要唤醒其它线程来查看是不是自己的条件,如果不是那么就继续进入等待条件,符合条件就获取锁继续执行。
public class MyTest27 {
public static void main(String[] args) {
WaitNotify1 waitNotify1 = new WaitNotify1(1, 5);
Thread t1 = new Thread(() -> {
waitNotify1.print("a",1,2);
}, "t1");
Thread t2 = new Thread(() -> {
waitNotify1.print("b",2,3);
}, "t2");
Thread t3 = new Thread(() -> {
waitNotify1.print("c",3,1);
}, "t3");
t1.start();
t2.start();
t3.start();
}
}
class WaitNotify1{
public void print(String s,int waitFlag,int nextFlag){
synchronized (this){
try {
for(int i=0;i<loopNum;i++){
//如果不是1那么就等待
while(this.flag!=waitFlag){
this.wait();
}
System.out.print(s);
this.flag=nextFlag;
this.notifyAll();
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
private int flag;
private int loopNum;
public WaitNotify1(int flag,int loopNum) {
this.flag = flag;
this.loopNum = loopNum;
}
}
ReentrantLock处理顺序执行问题思路
这个地方执行的思路其实就是通过lock开多几个条件变量,条件变量控制的是各个休息室,使用条件变量来吧线程阻塞,之后再释放。a,b,c分别开了3个,然后就是执行一个之后调用另一个condition来解锁另外一个继续执行。一开始需要把a,b,c三个线程都上锁,接着就是手动解锁一个让循环开始执行。
public class MyTest28 {
public static void main(String[] args) {
WaitLock waitLock = new WaitLock(5);
Condition a = waitLock.newCondition();
Condition b = waitLock.newCondition();
Condition c = waitLock.newCondition();
new Thread(()->{
waitLock.print("a",a,b);
},"t1").start();
new Thread(()->{
waitLock.print("b",b,c);
},"t2").start();
new Thread(()->{
waitLock.print("c",c,a);
},"t3").start();
Sleeper.sleep(1);
waitLock.lock();
try{
//唤醒a
a.signal();
}finally {
waitLock.unlock();
}
}
}
@Slf4j(topic = "c.lock")
class WaitLock extends ReentrantLock{
private int loopNum;
public WaitLock(int loopNum) {
this.loopNum = loopNum;
}
public void print(String str,Condition cur,Condition next){
for(int i=0;i<loopNum;i++){
lock();
try{
cur.await();
System.out.print(str);
next.signal();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
unlock();
}
}
}
}
LockSupport执行思路
其实这个地方直接就可以使用park先让三个线程阻塞,然后再手动开启一个线程。如果需要让阻塞的线程开启就需要unpark(t)就是需要传入对应的线程参数,唤醒对应的线程。t1执行完之后,唤醒t2,t2执行完之后唤醒t3,那么这个时候调用的方法就要传入对应的线程参数。而且线程参数是可以共享的,放到方法区,如果是放到main线程上是无法看到的。
@Slf4j(topic = "c.test29")
public class MyTest29 {
static Thread t1;
static Thread t2;
static Thread t3;
public static void main(String[] args) {
WaitPark waitPark = new WaitPark(5);
t1=new Thread(()->{
waitPark.print("a",t2);
},"t1");
t2=new Thread(()->{
waitPark.print("b",t3);
},"t2");
t3=new Thread(()->{
waitPark.print("c",t1);
},"t3");
t1.start();
t2.start();
t3.start();
Sleeper.sleep(1);
LockSupport.unpark(t1);
}
}
@Slf4j(topic = "c.lock")
class WaitPark{
private int loopNum;
public WaitPark(int loopNum) {
this.loopNum = loopNum;
}
public void print(String str,Thread t){
for(int i=0;i<loopNum;i++){
LockSupport.park();
log.debug(str);
LockSupport.unpark(t);
}
}
}