首先提及一下前置知识:
在前三章我们讨论了多线程并发
的优点以及如何加锁来处理并发带来的安全性问题
但是加锁也为我们带来了诸多问题 如:死锁,活锁,线程饥饿等问题 这一章我我们主要处理锁带来的问题. 首先就是最出名的死锁
什么是死锁
死锁是当线程进入无限期等待状态时发生的情况,因为所请求的锁被另一个线程持有,而另一个线程又等待第一个线程持有的另一个锁 导致互相等待。总结:多个线程互相等待对方释放锁。
例如在现实中的十字路口,锁就像红路灯指示器,一旦锁坏了,就会导致交通瘫痪。那么该如何避免这个问题呢
1.超时释放锁
>顾名思义,这种避免死锁的方式是在尝试获取锁的时候加一个超时时间,这就意味着,如果一个线程在获取锁的门口等待太久这个线程就会放弃这次请求,退还并释放所有已经获得的锁,再在等待一段随机时间后再次尝试,这段时间其他的线程伙伴可以去尝试拿锁.
public interface Lock {
//自定义异常类
public static class TimeOutException extends Exception{
public TimeOutException(String message){
super(message);
}
}
//无超时锁,可以被打断
void lock() throws InterruptedException;
//超时锁,可以被打断
void lock(long molls) throws InterruptedException,TimeOutException;
//解锁
void unlock();
//获取当前等待的线程
Collection<Thread> getBlockedThread();
//获取当前阻塞的线程数目
int getBlockSize();
}
public class BooleanLock implements Lock{
private boolean initValue;
private Thread currenThread;
public BooleanLock(){
this.initValue = false;
}
private Collection<Thread> blockThreadCollection = new ArrayList<>();
@Override
public synchronized void lock() throws InterruptedException {
while (initValue){
blockThreadCollection.add(Thread.currentThread());
this.wait();
}
//表明此时正在用,别人进来就要锁住
this.initValue = true;
currenThread = Thread.currentThread();
blockThreadCollection.remove(Thread.currentThread());//从集合中删除
}
@Override
public synchronized void lock(long mills) throws InterruptedException, TimeOutException {
if (mills<=0){
lock();
}else {
long hasRemain = mills;
long endTime = System.currentTimeMillis()+mills;
while (initValue){
if (hasRemain<=0)
throw new TimeOutException("Time out");
blockThreadCollection.add(Thread.currentThread());
hasRemain = endTime-System.currentTimeMillis();
}
this.initValue = true;
currenThread = Thread.currentThread();
}
}
@Override
public synchronized void unlock() {
if (currenThread==Thread.currentThread()){
this.initValue = false; //表明锁已经释放
Optional.of(Thread.currentThread().getName()+ " release the lock monitor").ifPresent(System.out::println);
this.notifyAll();
}
}
@Override
public Collection<Thread> getBlockedThread() {
return Collections.unmodifiableCollection(blockThreadCollection);
}
@Override
public int getBlockSize() {
return blockThreadCollection.size();
}
}
public class BlockTest {
public static void main(String[] args) throws InterruptedException {
final BooleanLock booleanLock = new BooleanLock();
// 使用Stream流的方式创建四个线程
Stream.of("T1","T2","T3","T4").forEach(name->{
new Thread(()->{
try {
booleanLock.lock(10);
Optional.of(Thread.currentThread().getName()+" have the lock Monitor").ifPresent(System.out::println);
work();
} catch (InterruptedException e) {
e.printStackTrace();
} catch (Lock.TimeOutException e) {
Optional.of(Thread.currentThread().getName()+" time out").ifPresent(System.out::println);
} finally {
booleanLock.unlock();
}
},name).start();
});
}
//如果是需要一直等待就调用 lock(),如果是超时要退出来就调用超时lock(long millo)
private static void work() throws InterruptedException{
Optional.of(Thread.currentThread().getName()+" is working.....'").ifPresent(System.out::println);
Thread.sleep(40_000);
}
}
运行:
T1 have the lock Monitor
T1 is working.....
T2 time out
T4 time out
T3 time out
2.按顺序加锁
>按照顺序加锁是一种有效防止死锁的机制,但是这种方式,你需要先知道所有可能用到锁的位置,并对这些锁安排一个顺序
3.死锁检测
>死锁检测是一个更好的死锁预防机制,主要用于超时锁和按顺序加锁不可用的场景每当一个线程获得了锁,会在线程和锁相关的数据结构中(map、graph 等等)将其记下。除此之外,每当有线程请求锁,也需要记录在这个数据结构中。当一个线程请求锁失败时,这个线程可以遍历锁的关系图看看是否有死锁发生。
如果检测出死锁,有两种处理手段:
什么是活锁
死锁是一直死等,活锁他不死等,它会一直执行,但是线程就是不能继续,因为它不断重试相同的操作。换句话说,就是信息处理线程并没有发生阻塞,但是永远都不会前进了,当他们为了彼此间的响应而相互礼让,使得没有一个线程能够继续前进,那么就发生了活锁
解决“活锁”的方案很简单,谦让时,尝试等待一个随机的时间就可以了。由于等待的时间是随机的,所以同时相撞后再次相撞的概率就很低了。“等待一个随机时间”的方案虽然很简单,却非常有效,Raft 这样知名的分布式一致性算法中也用到了它。
什么是饥饿
饥饿问题最经典的例子就是哲学家问题。如图所示:有五个哲学家用餐,每个人要活得两把叉子才可以就餐。当 2、4 就餐时,1、3、5 永远无法就餐,只能看着盘中的美食饥饿的等待着。
Java 不可能实现 100% 的公平性,我们依然可以通过同步结构在线程间实现公平性的提高。
有三种方案:
这三个方案中,方案一和方案三的适用场景比较有限,因为很多场景下,资源的稀缺性是没办法解决的,持有锁的线程执行的时间也很难缩短。倒是方案二的适用场景相对来说更多一些。那如何公平地分配资源呢?在并发编程里,主要是使用公平锁。所谓公平锁,是一种先来后到的方案,线程的等待是有顺序的,排在等待队列前面的线程会优先获得资源。
并发执行一定比串行执行快吗?线程越多执行越快吗?
答案是:并发不一定比串行快。因为有创建线程和线程上下文切换
的开销。
当 CPU 从执行一个线程切换到执行另一个线程时,CPU 需要保存当前线程的本地数据,程序指针等状态,并加载下一个要执行的线程的本地数据,程序指针等。这个开关被称为“上下文切换”。
资源限制是指在进行并发编程时,程序的执行速度受限于计算机硬件资源或软件资源。
在并发编程中,将代码执行速度加快的原则是将代码中串行执行的部分变成并发执行,但是如果将某段串行的代码并发执行,因为受限于资源,仍然在串行执行,这时候程序不仅不会加快执行,反而会更慢,因为增加了上下文切换和资源调度的时间。
在资源限制情况下进行并发编程,根据不同的资源限制调整程序的并发度。
至本章为止,多线程并发的概念篇就结束了,实际操作篇尽情期待 持续关注公众号 JAVA宝典
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。