首先说起一下前置知识:java
1.JAVA并发之基础概念算法
2.JAVA并发之进程VS线程编程
在前三章咱们讨论了多线程并发
的优势以及如何加锁来处理并发带来的安全性问题
数据结构
可是加锁也为咱们带来了诸多问题 如:死锁,活锁,线程饥饿等问题 这一章我咱们主要处理锁带来的问题. 首先就是最出名的死锁多线程
1.死锁(Deadlock)
什么是死锁并发
死锁是当线程进入无限期等待状态时发生的状况,由于所请求的锁被另外一个线程持有,而另外一个线程又等待第一个线程持有的另外一个锁 致使互相等待。总结:多个线程互相等待对方释放锁。分布式
例如在现实中的十字路口,锁就像红路灯指示器,一旦锁坏了,就会致使交通瘫痪。 那么该如何避免这个问题呢ide
死锁的解决和预防
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 等等)将其记下。除此以外,每当有线程请求锁,也须要记录在这个数据结构中。当一个线程请求锁失败时,这个线程能够遍历锁的关系图看看是否有死锁发生。
若是检测出死锁,有两种处理手段:
- 释放全部锁,回退,而且等待一段随机的时间后重试。这个和简单的加锁超时相似,不同的是只有死锁已经发生了才回退,而不会是由于加锁的请求超时了。虽然有回退和等待,可是若是有大量的线程竞争同一批锁,它们仍是会重复地死锁,缘由同超时相似,不能从根本上减轻竞争.
- 一个更好的方案是给这些线程设置优先级,让一个(或几个)线程回退,剩下的线程就像没发生死锁同样继续保持着它们须要的锁。若是赋予这些线程的优先级是固定不变的,同一批线程老是会拥有更高的优先级。为避免这个问题,能够在死锁发生的时候设置随机的优先级。
2.活锁(Livelock)
什么是活锁
死锁是一直死等,活锁他不死等,它会一直执行,可是线程就是不能继续,由于它不断重试相同的操做。换句话说,就是信息处理线程并无发生阻塞,可是永远都不会前进了,当他们为了彼此间的响应而相互礼让,使得没有一个线程可以继续前进,那么就发生了活锁
避免活锁
解决“活锁”的方案很简单,谦让时,尝试等待一个随机的时间就能够了。因为等待的时间是随机的,因此同时相撞后再次相撞的几率就很低了。“等待一个随机时间”的方案虽然很简单,却很是有效,Raft 这样知名的分布式一致性算法中也用到了它。
3.饥饿
什么是饥饿
- 高优先级线程吞噬全部的低优先级线程的 CPU 时间。
- 线程被永久堵塞在一个等待进入同步块的状态,由于其余线程老是能在它以前持续地对该同步块进行访问。
- 线程在等待一个自己(在其上调用 wait())也处于永久等待完成的对象,由于其余线程老是被持续地得到唤醒。
饥饿问题最经典的例子就是哲学家问题。如图所示:有五个哲学家用餐,每一个人要活得两把叉子才能够就餐。当 二、4 就餐时,一、三、5 永远没法就餐,只能看着盘中的美食饥饿的等待着。
解决饥饿
Java 不可能实现 100% 的公平性,咱们依然能够经过同步结构在线程间实现公平性的提升。
有三种方案:
- 保证资源充足
- 公平地分配资源
- 避免持有锁的线程长时间执行
这三个方案中,方案一和方案三的适用场景比较有限,由于不少场景下,资源的稀缺性是没办法解决的,持有锁的线程执行的时间也很难缩短。却是方案二的适用场景相对来讲更多一些。 那如何公平地分配资源呢?在并发编程里,主要是使用公平锁。所谓公平锁,是一种先来后到的方案,线程的等待是有顺序的,排在等待队列前面的线程会优先得到资源。
4.性能问题
并发执行必定比串行执行快吗?线程越多执行越快吗?
答案是:并发不必定比串行快。由于有建立线程和线程上下文切换
的开销。
5.上下文切换
什么是上下文切换?
当 CPU 从执行一个线程切换到执行另外一个线程时,CPU 须要保存当前线程的本地数据,程序指针等状态,并加载下一个要执行的线程的本地数据,程序指针等。这个开关被称为“上下文切换”。
减小上下文切换的方法
- 无锁并发编程 - 多线程竞争锁时,会引发上下文切换,因此多线程处理数据时,能够用一些办法来避免使用锁,如将数据的 ID 按照 Hash 算法取模分段,不一样的线程处理不一样段的数据。
- CAS 算法 - Java 的 Atomic 包使用 CAS 算法来更新数据,而不须要加锁。
- 使用最少线程 - 避免建立不须要的线程,好比任务不多,可是建立了不少线程来处理,这样会形成大量线程都处于等待状态。
- 使用协程 - 在单线程里实现多任务的调度,并在单线程里维持多个任务间的切换。
6.资源限制
什么是资源限制
资源限制是指在进行并发编程时,程序的执行速度受限于计算机硬件资源或软件资源。
资源限制引起的问题
在并发编程中,将代码执行速度加快的原则是将代码中串行执行的部分变成并发执行,可是若是将某段串行的代码并发执行,由于受限于资源,仍然在串行执行,这时候程序不只不会加快执行,反而会更慢,由于增长了上下文切换和资源调度的时间。
如何解决资源限制的问题
在资源限制状况下进行并发编程,根据不一样的资源限制调整程序的并发度。
- 对于硬件资源限制,能够考虑使用集群并行执行程序。
- 对于软件资源限制,能够考虑使用资源池将资源复用。
总结
至本章为止,多线程并发的概念篇就结束了,实际操做篇尽情期待 持续关注公众号 JAVA宝典
关注公众号:java宝典