Java并发编程技术分享之基础干货归纳总结[图]

我们进入正题:介绍并发编程的基础性概念。
并发编程的优势和缺点
并发编程中常见的线程安全问题
如何实现线程之间的通信?
死锁是怎么发生的,如何避免?
嵌套管程锁死、重入锁死、饥饿和公平。
1.并发编程介绍
1.1并发的出现
单CPU时代,单任务在一个时间点只能执行单一程序。
多任务阶段,计算机能在同一时间点并行执行多进程。多个任务或进程共享一个CPU,并交由操作系统来完成多任务间对CPU的运行切换,以使得每个任务都有机会获得一定的时间片运行。
现代的计算机多核CPU,在一个程序内部能拥有多个线程并行执行,多个CPU同时执行该程序。一个进程就包括了多个线程,每个线程负责一个独立的子任务。
进程让操作系统的并发性成为可能,而线程让进程的内部并发成为可能。
一个进程虽然包括多个线程,但是这些线程是共同享有进程占有的资源和地址空间的。

Java并发编程技术分享之基础干货归纳总结[图]

进程是操作系统进行资源分配的基本单位,而线程是操作系统进行调度的基本单位。
1.2并发编程优点
1)资源利用率更好
举例:
一个程序读取文件(5s)和处理文件(2s),处理2个文件。
5秒读取文件A2秒处理文件A5秒读取文件B2秒处理文件B
总共需要14秒。读取文件的时候,CPU空闲等待读取数据,浪费CPU资源。
并发处理:
5秒读取文件A5秒读取文件B+2秒处理文件A2秒处理文件B
总共需要12秒。当第二文件在被读取的时候,利用CPU的空闲去处理第一个文件。
2)程序设计在某些情况下更简单
如上述读取处理文件举例中,如果使用单线程实现,需要每个文件读取和处理的状态;而使用多线程,每个线程处理一个文件的读取和处理,不需要记录文件读取和处理状态,实现更简单。
3)程序响应更快
并发编程缺点
1)设计更复杂
由于多个线程是共同占有所属进程的资源和地址空间的,那么就会存在多个线程同时访问同一个资源的问题,可能导致线程安全问题。避免多线程编程中线程安全设计较复杂。
2)上下文切换的开销
CPU从执行一个线程切换到执行另外一个线程的时候,需要先存储当前线程的本地的数据,程序指针等,然后载入另一个线程的本地数据,程序指针等,最后才开始执行。这种切换称为上下文切换。
对于线程的上下文切换实际上就是存储和恢复CPU状态的过程,它使得线程执行能够从中断点恢复执行。
虽然多线程可以使得任务执行的效率得到提升,但是由于在线程切换时同样会带来一定的开销代价,并且多个线程会导致系统资源占用的增加,所以在进行多线程编程时要注意这些因素。
3)增加资源消耗
线程在运行的时候需要从计算机里面得到一些资源。除了CPU,线程还需要一些内存来维持它本地的堆栈。它也需要占用操作系统中一些资源来管理线程。
2.线程安全问题
竞态条件:当多个线程同时访问同一个资源,其中的一个或者多个线程对这个资源进行了写操作,对资源的访问顺序敏感,就称存在竞态条件。多个线程同时读同一个资源不会产生竞态条件。
临界区:导致竞态条件发生的代码区称作临界区。在临界区中使用适当的同步就可以避免竞态条件。
publicclassCounter{protectedlongcount=0;publicvoidadd(longvalue){this.count=this.count+value;}}
多线程同时执行上面的代码可能会出错:多线程同时执行临界区代码this.count=this.count+value时,同时对同一资源this.count进行写操作,产生了竞态条件。
基本上所有的并发模式在解决线程安全问题时,都采用“序列化访问临界资源”的方案,即在同一时刻,只能有一个线程访问临界资源,也称作同步互斥访问。通常来说,是在访问临界资源的代码前面加上一个锁,当访问完临界资源后释放锁,让其他线程继续访问。
在Java中,提供了两种方式来实现同步互斥访问:synchronized和Lock。
3.线程通信
线程通信的目标是使线程间能够互相发送信号。另一方面,线程通信使线程能够等待其他线程的信号。
通过共享对象通信
//必须是同一个MySignal实例,通过共享变量hasDataToProcess通信publicclassMySignal{protectedbooleanhasDataToProcess=false;publicsynchronizedbooleanhasDataToProcess(){returnthis.hasDataToProcess;}publicsynchronizedvoidsetHasDataToProcess(booleanhasData){this.hasDataToProcess=hasData;}}
单线程A完成某一操作M之后,调用setHasDataToProcess(true),将hasDataToProcess置为true,表示操作M完成。
线程B调用hasDataToProcess()获取hasDataToProcess为true,就知道操作M已经完成。
wait()-notify()/notifyAll()
//A线程调用doWait()等待,B线程调用doNotify()唤醒A线程publicclassMyWaitNotify{MonitorObjectmyMonitorObject=newMonitorObject();publicvoiddoWait(){synchronized(myMonitorObject){try{myMonitorObject.wait();}catch(InterruptedExceptione){...}}}publicvoiddoNotify(){synchronized(myMonitorObject){myMonitorObject.notify();}}}
优化:
增加booleanwasSignalled,记录是否收到唤醒信号。只有没收到过唤醒信号时才可以wait,避免信号丢失导致永久wait。
while()自旋锁,线程被唤醒之后可以保证再次检查条件是否满足,避免虚假信号。
publicclassMyWaitNotify3{MonitorObjectmyMonitorObject=newMonitorObject();booleanwasSignalled=false;publicvoiddoWait(){synchronized(myMonitorObject){while(!wasSignalled){try{myMonitorObject.wait();//如果被虚假唤醒,再回while循环检查条件wasSignalled}catch(InterruptedExceptione){}}wasSignalled=false;}}publicvoiddoNotify(){synchronized(myMonitorObject){wasSignalled=true;myMonitorObject.notify();}}}
4.死锁
死锁:多个线程同时但以不同的顺序请求同一组锁的时候,线程之间互相循环等待锁导致线程一直阻塞。
如果线程1锁住了A,然后尝试对B进行加锁,同时线程2已经锁住了B,接着尝试对A进行加锁,这样线程1持有锁A等待锁B,线程2持有锁B等待锁A,就会发生死锁。
死锁可能不止包含2个线程,可以包含多个线程。如线程1等待线程2,线程2等待线程3,线程3等待线程4,线程4等待线程1。
publicclassTest{staticObjectlockObject1=newObject();staticObjectlockObject2=newObject();publicstaticvoidmain(String[]args){newThread(){@Overridepublicvoidrun(){synchronized(lockObject1){try{Thread.sleep(500);}catch(InterruptedExceptione){e.printStackTrace();}synchronized(lockObject2){System.out.println(1);}}}}.start();newThread(){@Overridepublicvoidrun(){synchronized(lockObject2){synchronized(lockObject1){System.out.println(1);}}}}.start();}}
如何避免死锁?
1)按顺序加锁
多个线程请求的一组锁按顺序加锁可以避免死锁。
死锁:如果线程1锁住了A,然后尝试对B进行加锁,同时线程2已经锁住了B,接着尝试对A进行加锁,发生死锁。
解决:规定锁A和锁B的顺序,某个线程需要同时获取锁A和锁B时,必须先拿锁A再拿锁B。线程1和线程2都先锁A再锁B,不会发生死锁。
问题:需要事先知道所有可能会用到的锁,并对这些锁做适当的排序。
2)加锁时限(超时重试机制)
设置一个超时时间,在尝试获取锁的过程中若超过了这个时限该线程则放弃对该锁请求,回退并释放所有已经获得的锁,然后等待一段随机的时间再重试。
这段随机的等待时间让其它线程有机会尝试获取相同的这些锁,并且让该应用在没有获得锁的时候可以继续运行干点其它事情。
问题:
当线程很多时,等待的这一段随机的时间会一样长或者很接近,因此就算出现竞争而导致超时后,由于超时时间一样,它们又会同时开始重试,导致新一轮的竞争,带来了新的问题。
不能对synchronized同步块设置超时时间。需要创建一个自定义锁,或使用java.util.concurrent包下的工具。
3)死锁检测
主要是针对那些不可能实现按序加锁并且锁超时也不可行的情况。
每当一个线程获得了锁,会在线程和锁相关的数据结构中(比如map)将其记下。当一个线程请求锁失败时,这个线程可以遍历锁的关系图看看是否有死锁发生。
例如:线程1请求锁A,但是锁A这个时候被线程2持有,这时线程1就可以检查一下线程2是否已经请求了线程1当前所持有的锁。
如果线程2确实有这样的请求,那么就是发生了死锁(线程1拥有锁B,请求锁A;线程B拥有锁A,请求锁B)。
当检测出死锁时,可以有两种做法:
释放所有锁,回退,并且等待一段随机的时间后重试。(类似超时重试机制)
给这些线程设置优先级,让一个(或几个)线程回退,剩下的线程就像没发生死锁一样继续保持着它们需要的锁。
5.嵌套管程锁死

线程1获得A对象的锁。线程1获得对象B的锁(A对象锁还未释放)。线程1调用B.wait(),从而释放了B对象上的锁,但仍然持有对象A的锁。线程2需要同时持有对象A和对象B的锁,作文(https://www.isanxia.com)才能向线程1发信号B.notify()。线程2无法获得对象A上的锁,因为对象A上的锁当前正被线程1持有。线程2一直被阻塞,等待线程1释放对象A上的锁。线程1一直阻塞,等待线程2的信号,因此不会释放对象A上的锁。

publicclassLock{protectedMonitorObjectmonitorObject=newMonitorObject();protectedbooleanisLocked=false;publicvoidlock()throwsInterruptedException{synchronized(this){while(isLocked){synchronized(this.monitorObject){this.monitorObject.wait();}}isLocked=true;}}publicvoidunlock(){synchronized(this){this.isLocked=false;synchronized(this.monitorObject){this.monitorObject.notify();}}}}
线程1调用lock()方法,Lock对象锁和monitorObject锁,调用monitorObject.wait()阻塞,但仍然持有Lock对象锁。
线程2调用unlock()方法解锁时,无法获取Lock对象锁,因为线程1一直持有Lock锁,造成嵌套管程锁死。
6.重入锁死
如果一个线程持有某个对象上的锁,那么它就有权访问所有在该对象上同步的块,这就叫可重入。synchronized、ReentrantLock都是可重入锁。
如果一个线程持有锁A,锁A是不可重入的,该线程再次请求锁A时被阻塞,就是重入锁死。
重入锁死举例:
publicclassLock{privatebooleanisLocked=false;publicsynchronizedvoidlock()throwsInterruptedException{while(isLocked){wait();}isLocked=true;}publicsynchronizedvoidunlock(){isLocked=false;notify();}}
如果一个线程两次调用lock()间没有调用unlock()方法,那么第二次调用lock()就会被阻塞,这就出现了重入锁死。
7.饥饿和公平
如果一个线程因为CPU时间全部被其他线程抢走而得不到CPU运行时间,这种状态被称之为饥饿。
导致线程饥饿原因:
高优先级线程吞噬所有的低优先级线程的CPU时间。
线程始终竞争不到锁。
线程调用object.wait()后没有被唤醒。
解决饥饿的方案被称之为公平性,即所有线程均能公平地获得运行机会。关于公平锁会在之后ReentrantLock中详细介绍。
总结
并发编程可以更好的利用CPU资源,更高效快速的响应程序,但是设计较复杂,并且上下文切换会造成一定的消耗。
并发编程中,由于多个线程同时访问同一个资源,可能造成线程安全问题,Java中可以通过synchronized和Lock的方式实现同步解决线程安全问题。
更好的发挥多线程的优势需要线程之间通信,常用的线程通信方式是通过共享对象的状态通信和wait()/notify()。
多个线程同时但以不同的顺序请求同一组锁的时候,线程之间互相循环等待锁导致线程一直阻塞,造成死锁。最常用的解决死锁的方式是按顺序加锁。
线程持有不可重入锁之后再次请求不可重入锁时被阻塞,就是重入锁死。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值