并发编程基础知识-一

开始总结一下有关并发方面的知识,先用几篇博客来总结一下并发编程的相关基础知识,之后再对源码进行阅读和分析。

线程和进程

线程是进程的一个执行单元,也是进程内的可调度实体,线程和进程主要有以下区别:

  • 地址空间:进程内的一个执行单元,进程至少有一个线程,它们共享进程的地址空间,各个进程有自己独立的地址空间
  • 资源拥有:进程是资源分配和拥有的单位,同一个进程内的线程共享进程资源。

进程是系统分配资源和调度的基本单位,线程则是进程的一个执行路径,一个进程中至少有一个线程,进程中的多个线程共享进程的资源。

线程的创建和执行方式

  • 继承Thread类重写run()方法
  • 实现Runnable接口的run()方法
  • 使用Callable

这里前两种就不再赘述了,要强调的是在调用start方法之后,线程并没有马上启动,只是处于就绪状态,等待获取cpu资源之后才会进入运行状态。前两种方式都没有返回值,这里我们来说一下第三种,通过实现Callable接口重写call()方法,这里可以通过指定泛型类型来指定它的返回值。

线程通知和等待

Object类是所有类的父类,我们先来回忆一下它当中都有哪些方法。分别是hashCode()、notify()、notifyAll()、equals()、finalize()、getCalss()、clone()、toString()、wait()、wait(long timeout)、wait(long timeout,int nanos)。我们来看其中的部分方法,首先来看wait。

wait()

wait()方法内部是调用了wait(0),即上面的那个带参数的wait()方法。意思是永远的wait下去,调用了共享变量的wait方法之后,该调用线程会被阻塞挂起直到发生以下几种情况才会返回:

  • 其他线程调用了该共享对象的notify或者notifyAll方法
  • 其他线程调用了该线程的interrupt方法,该线程抛出InterruptedException异常返回

要注意的是,在调用wait方法前,要首先获取共享对象的锁,否则回抛出illegalMonitorException异常

wait(long timeout)

带参数的wait(long timeout)方法会阻塞挂起timeout时间之后,自动唤醒自己,这里的唤醒指的是其唤醒之后也要加入到共享资源锁的竞争当中。

wait(long timeout,int nanos)

这个方法是nanos是在给定范围内,1<nanos<999999,之间timeout就会++,并继续调用wait(timeout)操作

另外一个需要注意的点就是阻塞挂起的线程即使不通过notify、notifyAll或者被中断、等待超时操作,而直接从挂起状态编程就绪状态,这就是所谓的虚假唤醒。对于虚假唤醒,我们要将wait方法放在while循环中,不停去测试该线程被唤醒的条件是否满足,不满足则继续等待,而不是使用简单的if。

wait方法只会释放当前获得的共享变量的锁,当前线程持有的其他的共享变量的锁并不会释放。

notify()

一个线程调用了共享对象的notify()方法之后,会唤醒一个在该共享变量上调用了wait之后被挂起的线程,一个共享变量上可能有多个线程在等待获取锁,所有唤醒的线程也是随机的。唤醒之后,并不是立刻就获取到了该共享对象的锁,而是要和其他等待的线程继续竞争。和wai一样,只有获取到了该共享对象的锁才可以调用notify()方法,否则也会抛出IllegalMonitorStateException异常。

notifyAll()

notifyAll()方法可以唤醒所有等待共享资源锁的线程,在共享变量上调用notifyAll方法只会唤醒调用这个方法前调用了wait系列函数而被放入共享变量等待集合里面的线程,如果调用notifyAll方法后,再有线程调用该共变量的wait方法,则是不会被唤醒的。notifyAll只会唤醒在调用该方法之前的线程

join()

join是一个无参且返回值为void的方法,是Thread类中的方法,就是等待调用join的线程之后再返回。也就是说线程A调用了线程B的join方法之后会被阻塞,一直当线程B执行完之后返回之后,线程A再继续执行。如果此时,其他线程调用了线程A的interrupt()方法,线程A就会抛出InterruptedException返回。

大专栏  并发编程基础知识-一eep">sleep

sleep也是Thread类中的方法,当一个执行中的线程调用了sleep方法,他就会在指定时间内让出cpu的占用权,也就是在这期间不参与cpu的调度,但是sleep和wait不同的是,sleep并不会释放自己所占用的资源,比如锁的占用。在sleep的时间到了之后,回正常返回,线程处于就绪状态再次去参与cpu资源的竞争。同样的,在sleep期间,调用interrupt()方法,会抛出InteruptedException异常。

yield()

再来看一个Thread类中的方法,yield()方法可以让出自己所占用的cpu,在调用yield方法之后,线程会处于就绪状态,然后线程调度器会从线程就绪队列中获取一个线程优先级最高的线程,当然也有可能是刚刚让出执行权的线程继续执行

线程中断

我们来看看上面一直提到的线程中断,线程中断是一种线程间的协作模式,通过设置线程的中断标志并不能直接 终止该线程的执行,而是被中断的线程根据中断状态自行处理。在我们上文提到的wait、join、sleep状态下,调用interrupt()方法会出现异常。关于线程中断我们来看三个方法:

  • void interrupt():中断线程,当线程A运行的时候,线程B可以调用线程A的interrupt()方法来设置线程A的中断标志为true并立即返回。设置标志仅仅是设置标志,线程A实际并没有被中断,它会继续往下执行。
  • boolean isInterrupted():检测当前线程是否被中断,如果是返回true,否则返回false。
  • boolean interrupted():检测当前线程是否被中断,如果是则返回true,否则返回false。与isInturrupted不同的是,该方法如果发现当前线程被中断,则会清除中断标志。这里有一个大坑:在interrupted()内部是获取当前调用线程的中断标志而不是调用interrupted()方法的实例对象的中断标志。

线程上下文切换

多线程编程中,线程数一般都大于cpu数,而每个cpu同一时刻只能被一个线程使用。为了让用户感觉多个线程是同时进行的,cpu资源的分配采用了时间片轮转算法,cpu给每个线程分配一个时间片,线程在时间片内占用cpu执行任务,线程执行完之后就会让出cpu的使用权给其他线程,自己处于就绪状态,这就是线程的上下文切换。

线程的上下文切换的时机有:

  • 当前线程的cpu时间片使用完处于就绪状态
  • 当前线程被其他线程中断时

死锁

死锁就是指两个或两个以上的线程在执行的过程中,因互相争夺资源而出造成的互相等待的现象。这也是我们在操作系统里老生常谈的话题,我们来回顾一下,产生死锁的四个原因:

  • 互斥条件:指线程对已经获取到的资源进行排他性保护,即该资源同时只能一个线程占有,如果此时还有其他线程请求并获取该资源,则请求者只能等待,直至占有资源的线程释放该资源。
  • 请求并持有:指线程已经至少得到了一个资源,但是又提出了新的资源请求,但是新资源已经被其他新的线程占有,所以当前线程会被阻塞,但阻塞的同时并不释放自己已经获取的资源
  • 不可剥夺条件:指线程获取到的资源在自己使用完之前不能被其他线程抢占,只有在自己使用完毕之后才由自己释放该资源
  • 环路等待条件:指在发生死锁的时候,必然存在一个线程—资源的环形链,即线程{T0,T1,T2,T3…Tn}中的T0正在等待一个T1占用的资源,T1正在等待T2的资源……Tn正在等待T0的资源。

那么如何避免死锁呢?互斥条件和不可剥夺条件,是不可能被破坏的,我们要避免死锁,只需要破坏掉请求并持有或者环路等待条件其中之一即可。简单举个例子两个资源AB,线程1获取顺序是AB线程2获取顺序是BA,这样就会发生死锁,我们要破坏掉资源请求顺序,我们把线程B获取顺序改为AB,这样线程2在请求A的时候会发生阻塞,在线程1释放掉AB之后会唤醒线程2再去获取锁,这样就完美的解决了死锁问题。

守护线程和用户线程

java中的线程分为两类,分别为daemon线程和user线程,在jvm启动的时候,main方法所在的线程就是一个用户线程,伴随着用户线程启动的还有若干守护线程,比如垃圾回收线程。守护线程和用户线程最大的区别之一就是,当最后一个非守护线程结束时,JVM会正常退出,而不是当前是否有守护线程,也就是说守护线程是否结束并不影响JVM的退出,只要有一个用户线程未结束,正常情况下,jvm就不会退出。可以通过setDeamon的方式来设置是否为守护线程。

猜你喜欢

转载自www.cnblogs.com/lijianming180/p/12099796.html