avatar

并发编程(二)

二、并发编程基础

2.1 临界资源

临界资源是一次仅允许一个进程使用的共享资源。各进程采取互斥的方式,实现共享的资源称作临界资 源。属于临界资源的硬件有,打印机,磁带机等;软件有消息队列,变量,数组,缓冲区等。诸进程间 采取互斥方式,实现对这种资源的共享。

1
2
3
4
5
6
public class Counter {
    protected long count = 0;
    public void add(long value){        
this.count = this.count + value;    
}
}

2.2 线程安全

2.2.1 基本概念
竞态条件:

当两个线程竞争同一资源时,如果对资源的访问顺序敏感,就称存在竞态条件。
导致竞态条件发生的代码区称作临界区。
在临界区中使用适当的同步就可以避免竞态条件,如使用synchronized或者加锁机制。

线程安全:

允许被多个线程同时执行的代码称作线程安全的代码。线程安全的代码不包含竞态条件。

2.2.2 对象的安全
局部基本类型变量

局部变量存储在线程自己的栈中。也就是说,局部变量永远也不会被多个线程共享。所以,基础类型的局部变量是线程安全的。

下面是基础类型的局部变量的一个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class ThreadTest {
   public static void main(String[]args){        
MyThread share = new MyThread();        
for (int i=0;i<50;i++){          
new Thread(share,"线程"+i).start();      
}  
}
}
class MyThread implements Runnable{
   public void run() {        
int a =0;        
++a;
System.out.println(Thread.currentThread().getName()+":"+a);
}
}

无论多少个线程对run()方法中的基本类型a执行++a操作,只是更新当前线程栈的值,不会影响其他线程,也就是不共享数据;

局部的对象引用

对象的局部引用和基础类型的局部变量不太一样,尽管引用本身没有被共享,但引用所指的对象并没有 存储在线程的栈内。所有的对象都存在共享堆中。 如果在某个方法中创建的对象不会逃逸出(即该对象不会被其它方法获得,也不会被非局部变量引用 到)该方法,那么它就是线程安全的。 实际上,哪怕将这个对象作为参数传给其它方法,只要别的线程获取不到这个对象,那它仍是线程安全的。

1
2
3
4
5
6
7
8
public void method1(){  
LocalObject localObject = new LocalObject();
localObject.callMethod();
method2(localObject);
}
public void method2(LocalObject localObject){
localObject.setValue("value");
}
对象成员(成员变量)

对象成员存储在堆上。如果两个线程同时更新同一个对象的同一个成员,那这个代码就不是线程安全的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public class ThreadTest {
   public static void main(String[]args){
NotThreadSafe sharedInstance = new NotThreadSafe();
new Thread(new MyRunnable(sharedInstance)).start();
new Thread(new MyRunnable(sharedInstance)).start();

  }
}
class MyRunnable implements Runnable{
NotThreadSafe instance = null;
public MyRunnable(NotThreadSafe instance){
this.instance = instance;  
}
public void run(){
this.instance.add(" "+Thread.currentThread().getName());
System.out.println(this.instance.builder.toString());
}
}
class NotThreadSafe{
StringBuilder builder = new StringBuilder();
   public void add(String text){
this.builder.append(text);  
}
}

如果两个线程同时调用同一个NotThreadSafe实例上的add()方法,就会有竞态条件问题。

2.2.3 不可变性

通过创建不可变的共享对象来保证对象在线程间共享时不会被修改,从而实现线程安全。如下示例:

1
2
3
4
5
6
7
8
9
10
11
public class ImmutableValue{

private int value = 0;

public ImmutableValue(int value){
this.value = value;
}
public int getValue(){
return this.value;
}
}

请注意ImmutableValue类的成员变量 value 是通过构造函数赋值的,并且在类中没有set方法。这意味着一旦ImmutableValue实例被创建,value 变量就不能再被修改,这就是不可变性。但你可以通过 getValue()方法读取这个变量的值。

2.3 Java内存模型

Java内存模型Java Memory Model,简称JMM。JMM定义了Java 虚拟机(JVM)在计算机内存(RAM)中的工作方式。JVM是整个计算机虚拟模型,所以JMM是隶属于JVM的。

线程之间的通信

线程的通信是指线程之间以何种机制来交换信息。在命令式编程中,线程之间的通信机制有两种共享内 存和消息传递。
在共享内存的并发模型里,线程之间共享程序的公共状态,线程之间通过写-读内存中的公共状态来隐 式进行通信,典型的共享内存通信方式就是通过共享对象进行通信。
在消息传递的并发模型里,线程之间没有公共状态,线程之间必须通过明确的发送消息来显式进行通 信,在java中典型的消息传递方式就是wait()和notify()。

线程之间的同步

同步是指程序用于控制不同线程之间操作发生相对顺序的机制。
在共享内存并发模型里,同步是显式进行的。程序员必须显式指定某个方法或某段代码需要在线程之间 互斥执行。
在消息传递的并发模型里,由于消息的发送必须在消息的接收之前,因此同步是隐式进行的。

Java的并发采用的是共享内存模型

Java线程之间的通信总是隐式进行,整个通信过程对程序员完全透明。如果编写多线程程序的Java程序员不理解隐式进行的线程之间通信的工作机制,很可能会遇到各种奇怪的内存可见性问题。

Java内存模型结构

Java内存模型(简称JMM),JMM决定一个线程对共享变量的写入何时对另一个线程可见。从抽象的角度 来看,JMM定义了线程和主内存之间的抽象关系:线程之间的共享变量存储在主内存(main memory) 中,每个线程都有一个私有的本地内存(local memory),本地内存中存储了该线程以读/写共享变量 的副本。本地内存是JMM的一个抽象概念,并不真实存在。它涵盖了缓存,写缓冲区,寄存器以及其他的硬件和编译器优化。

从上图来看,线程A与线程B之间如要通信的话,必须要经历下面2个步骤:

  1. 首先,线程A把本地内存A中更新过的共享变量刷新到主内存中去。
  2. 然后,线程B到主内存中去读取线程A之前已更新过的共享变量。

2.4 CAS乐观锁

乐观锁:不加锁,假设没有冲突去完成某项操作,如果因为冲突失败就重试,直到成功为止。其实现方式有一种比较典型的就是Compare and Swap( CAS )。
CAS机制当中使用了3个基本操作数:内存地址V,旧的预期值A,要修改的新值B。
更新一个变量的时候,只有当变量的预期值A和内存地址V当中的实际值相同时,才会将内存地址V对应 的值修改为B。
从思想上来说,Synchronized属于悲观锁,悲观地认为程序中的并发情况严重,所以严防死守。CAS属 于乐观锁,乐观地认为程序中的并发情况不那么严重,所以让线程不断去尝试更新。

CAS的缺点:

  1. CPU开销较大在并发量比较高的情况下,如果许多线程反复尝试更新某一个变量,却又一直更新不成功,循环往复,会给CPU带来很大的压力。
  2. 不能保证代码块的原子性 CAS机制所保证的只是一个变量的原子性操作,而不能保证整个代码块的原子性。比如需要保证3个变量 共同进行原子性的更新,就不得不使用Synchronized了。

2.5 Synchronized块

Java中的同步块用synchronized标记。同步块在Java中是同步在某个对象上。所有同步在一个对象上 的同步块在同时只能被一个线程进入并执行操作。所有其他等待进入该同步块的线程将被阻塞,直到执 行该同步块中的线程退出。

有四种不同的同步块:
  • 实例方法

  • 静态方法

  • 实例方法中的同步块

  • 静态方法中的同步块

    上述同步块都同步在不同对象上。实际需要那种同步块视具体情况而定。

实例方法同步

下面是一个同步的实例方法:

1
2
3
public synchronized void add(int value){
this.count += value;
}

注意在方法声明中同步(synchronized )关键字。
Java实例方法同步是同步在拥有该方法的对象上。这样,每个实例其方法同步都同步在不同的对象上, 即该方法所属的实例。只有一个线程能够在实例方法同步块中运行。如果有多个实例存在,那么一个线 程一次可以在一个实例同步块中执行操作。

静态方法同步

静态方法同步和实例方法同步方法一样,也使用synchronized 关键字。Java静态方法同步如下示例:

1
2
3
public static synchronized void add(int value){ 
count += value;
}

同样,这里synchronized 关键字告诉Java这个方法是同步的。
静态方法的同步是指同步在该方法所在的类对象上。因为在Java虚拟机中一个类只能对应一个类对象, 所以同时只允许一个线程执行同一个类中的静态同步方法。
对于不同类中的静态同步方法,一个线程可以执行每个类中的静态同步方法而无需等待。不管类中的那 个静态同步方法被调用,一个类只能由一个线程同时执行。

实例方法中的同步块

有时你不需要同步整个方法,而是同步方法中的一部分。Java可以对方法的一部分进行同步。
在非同步的Java方法中的同步块的例子如下所示:

1
2
3
4
5
public void add(int value){
   synchronized(this){
this.count += value;  
}
}

示例使用Java同步块构造器来标记一块代码是同步的。该代码在执行时和同步方法一样。
注意Java同步块构造器用括号将对象括起来。在上例中,使用了“this”,即为调用add方法的实例本 身。在同步构造器中用括号括起来的对象叫做监视器对象。上述代码使用监视器对象同步,同步实例方法使用调用方法本身的实例作为监视器对象。
一次只有一个线程能够在同步于同一个监视器对象的Java方法内执行。
下面两个例子都同步他们所调用的实例对象上,因此他们在同步的执行效果上是等效的。

1
2
3
4
5
6
7
8
9
10
11
12
public class MyClass {
   public synchronized void log1(String msg1, String msg2){
log.writeln(msg1);      
log.writeln(msg2);  
}
   public void log2(String msg1, String msg2){      
synchronized(this){          
log.writeln(msg1);          
log.writeln(msg2);      
}  
}
}

在上例中,每次只有一个线程能够在两个同步块中任意一个方法内执行。
如果第二个同步块不是同步在this实例对象上,那么两个方法可以被线程同时执行。

静态方法中的同步块

和上面类似,下面是两个静态方法同步的例子。这些方法同步在该方法所属的类对象上。

1
2
3
4
5
6
7
8
9
10
11
12
public class MyClass {    
public static synchronized void log1(String msg1, String msg2){
log.writeln(msg1);      
log.writeln(msg2);  
}
   public static void log2(String msg1, String msg2){      
synchronized(MyClass.class){          
log.writeln(msg1);          
log.writeln(msg2);      
}  
}
}

这两个方法不允许同时被线程访问。
如果第二个同步块不是同步在MyClass.class这个对象上。那么这两个方法可以同时被线程访问。

Synchronized锁的存储

synchronized用的锁存储在Java对象头,如果对象是数组类型,则虚拟机用3个字宽存储对象头,如果 对象是非数组类型,则用2字宽存储对象头,32位虚拟机,1字宽等于4字节,即32位.

Java对象头的长度

Mark Word的存储结构

Mark Word可能的存储结果:

偏向锁

偏向锁的获取流程:
(1)查看Mark Word中偏向锁的标识以及锁标志位,若是否偏向锁为1且锁标志位为01,则该锁为可偏向状态。
(2)若为可偏向状态,则测试Mark Word中的线程ID是否与当前线程相同,若相同,表示线程已经获得了锁,如果不同,则进入(3)
(3)测试Mark Word的偏向锁的标识是否设置为1,如果没有设置,则使用CAS操作竞争锁,如何设置 了,则尝试使用CAS尝试将Mark Word中线程ID设置为当前线程ID,如果尝试失败,则执行(4)
(4)当前线程通过CAS竞争锁失败的情况下,说明有竞争。当到达全局安全点(在这个时间点,没有 正在执行的代码)时之前获得偏向锁的线程被挂起,偏向锁升级为轻量级锁,然后被阻塞在安全点的线 程继续往下执行同步代码。

轻量级锁

轻量级锁不是用来替代传统的重量级锁的,而是在没有多线程竞争的情况下,使用轻量级锁能够减少性 能消耗,但是当多个线程同时竞争锁时,轻量级锁会膨胀为重量级锁。
轻量级锁的加锁过程:
(1)当线程执行代码进入同步块时,若Mark Word为无锁状态,虚拟机先在当前线程的栈帧中建立一 个名为Lock Record的空间,用于存储当前对象的Mark Word的拷贝,官方称之为“Dispalced Mark Word”,此时状态如下图:

(2)复制对象头中的Mark Word到锁记录中。
(3)复制成功后,虚拟机将用CAS操作将对象的Mark Word更新为执行Lock Record的指针,并将 Lock Record里的owner指针指向对象的Mark Word。如果更新成功,则执行4,否则执行5。;
(4)如果更新成功,则这个线程拥有了这个锁,并将锁标志设为00,表示处于轻量级锁状态,此时状态图:

(5)如果更新失败,则说明有其他线程竞争锁,当前线程便通过自旋来获取锁。轻量级锁就会膨胀为 重量级锁,Mark Word中存储重量级锁(互斥锁)的指针,后面等待锁的线程也要进入阻塞状态。

重量级锁

即当有其他线程占用锁时,当前线程会进入阻塞状态。

2.6 关键字Volatile

Volatile是轻量级的synchronized,在多处理器环境下,可以保证共享变量的可见性。它不会引起线 程上下文的切换和调度,正确的使用Volatile,比synchronized的使用和执行成本更低。

可见性:

可见性,是指线程之间的可见性,一个线程修改的状态对另一个线程是可见的。也就是一个线程修改一 个共享变量时,另一个线程马上就能看到。比如:用volatile修饰的变量,就会具有可见性。

volatile修饰的变量不允许线程内部缓存和重排序,即直接修改内存。所以对其他线程是可见的。但 是这里需要注意一个问题,volatile只能让被他修饰内容具有可见性,但不能保证它具有原子性。比 如 volatile int a = 0;之后有一个操作 a++;这个变量a具有可见性,但是a++ 依然是一个非原 子操作,也就是这个操作同样存在线程安全问题。
  在 Java 中 volatile、synchronized 和 final 实现可见性。

原子性:

  子是世界上的小单位,具有不可分割性。比如 a=0;(a非long和double类型) 这个操作是不 可分割的,那么我们说这个操作时原子操作。再比如:a++; 这个操作实际是a = a + 1;是可分割 的,所以他不是一个原子操作。非原子操作都会存在线程安全问题,需要我们使用同步技术 (sychronized)来让它变成一个原子操作。一个操作是原子操作,那么我们称它具有原子性。java的 concurrent包下提供了一些原子类,我们可以通过阅读API来了解这些原子类的用法。比如: AtomicInteger、AtomicLong、AtomicReference等。
  在 Java 中 synchronized 和在 lock、unlock 中操作保证原子性。

有序性:

  Java 语言提供了 volatile 和 synchronized 两个关键字来保证线程之间操作的有序性, volatile 是因为其本身包含“禁止指令重排序”的语义,synchronized 是由“一个变量在同一个时 刻只允许一条线程对其进行 lock 操作”这条规则获得的,此规则决定了持有同一个对象锁的两个同步 块只能串行执行。
 Java语言提供了一种稍弱的同步机制,即volatile变量,用来确保将变量的更新操作通知到其他线 程。当把变量声明为volatile类型后,编译器与运行时都会注意到这个变量是共享的,因此不会将该 变量上的操作与其他内存操作一起重排序。volatile变量不会被缓存在寄存器或者对其他处理器不可 见的地方,因此在读取volatile类型的变量时总会返回新写入的值。
  在访问volatile变量时不会执行加锁操作,因此也就不会使执行线程阻塞,因此volatile变量是 一种比sychronized关键字更轻量级的同步机制。
当一个变量定义为 volatile 之后,将具备两种特性:
保证此变量对所有的线程的可见性,这里的“可见性”,如本文开头所述,当一个线程修改了这个 变量的值,volatile 保证了新值能立即同步到主内存,以及每次使用前立即从主内存刷新。但普 通变量做不到这点,普通变量的值在线程间传递均需要通过主内存来完成。 禁止指令重排序优化。有volatile修饰的变量,赋值后多执行了一个“load addl $0x0, (%esp)”操作,这个操作相当于一个内存屏障(指令重排序时不能把后面的指令重排序到内存屏 障之前的位置),只有一个CPU访问内存时,并不需要内存屏障;(什么是指令重排序:是指CPU 采用了允许将多条指令不按程序规定的顺序分开发送给各相应电路单元处理。

2.7 本地线程

Java中的ThreadLocal类允许我们创建只能被同一个线程读写的变量。因此,如果一段代码含有一个 ThreadLocal变量的引用,即使两个线程同时执行这段代码,它们也无法访问到对方的ThreadLocal变 量。
如何创建ThreadLocal变量
以下代码展示了如何创建一个ThreadLocal变量:

1
private ThreadLocal myThreadLocal = new ThreadLocal();

我们可以看到,通过这段代码实例化了一个ThreadLocal对象。我们只需要实例化对象一次,并且也不需要知道它是被哪个线程实例化。虽然所有的线程都能访问到这个ThreadLocal实例,但是每个线程却 只能访问到自己通过调用ThreadLocal的set()方法设置的值。即使是两个不同的线程在同一个 ThreadLocal对象上设置了不同的值,他们仍然无法访问到对方的值。
如何访问ThreadLocal变量
一旦创建了一个ThreadLocal变量,你可以通过如下代码设置某个需要保存的值:

1
myThreadLocal.set("A thread local value”);

可以通过下面方法读取保存在ThreadLocal变量中的值:

1
String threadLocalValue = (String) myThreadLocal.get();

ThreadLocal例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class ThreadLocalExample {
   public static class MyRunnable implements Runnable {
       private ThreadLocal threadLocal = new ThreadLocal();
       @Override        
public void run() {            
threadLocal.set((int) (Math.random() * 100D));            
try {            
Thread.sleep(2000);          
} catch (InterruptedException e) {

          }            
System.out.println(threadLocal.get());      
}  
}
   public static void main(String[] args) {        
MyRunnable sharedRunnableInstance = new MyRunnable();
Thread thread1 = new Thread(sharedRunnableInstance);
Thread thread2 = new Thread(sharedRunnableInstance);
thread1.start();        
thread2.start();  
}
}

上面的例子创建了一个MyRunnable实例,并将该实例作为参数传递给两个线程。两个线程分别执行 run()方法,并且都在ThreadLocal实例上保存了不同的值。如果它们访问的不是ThreadLocal对象并 且调用的set()方法被同步了,则第二个线程会覆盖掉第一个线程设置的值。但是,由于它们访问的是 一个ThreadLocal对象,因此这两个线程都无法看到对方保存的值。也就是说,它们存取的是两个不同 的值。
关于InheritableThreadLocal
InheritableThreadLocal类是ThreadLocal类的子类。ThreadLocal中每个线程拥有它自己的值,与 ThreadLocal不同的是,InheritableThreadLocal允许一个线程以及该线程创建的所有子线程都可以 访问它保存的值。

2.8、多线程问题

2.8.1 死锁
死锁的产生

死锁是两个或更多线程阻塞着等待其它处于死锁状态的线程所持有的锁。死锁通常发生在多个线程同时 但以不同的顺序请求同一组锁的时候。
例如,如果线程1锁住了A,然后尝试对B进行加锁,同时线程2已经锁住了B,接着尝试对A进行加锁,这 时死锁就发生了。线程1永远得不到B,线程2也永远得不到A,并且它们永远也不会知道发生了这样的事 情。为了得到彼此的对象(A和B),它们将永远阻塞下去。这种情况就是一个死锁。

更复杂的死锁

死锁可能不止包含2个线程,这让检测死锁变得更加困难。

线程1等待线程2,线程2等待线程3,线程3等待线程4,线程4等待线程1。

数据库的死锁

更加复杂的死锁场景发生在数据库事务中。一个数据库事务可能由多条SQL更新请求组成。当在一个事 务中更新一条记录,这条记录就会被锁住避免其他事务的更新请求,直到第一个事务结束。同一个事务 中每一个更新请求都可能会锁住一些记录。
当多个事务同时需要对一些相同的记录做更新操作时,就很有可能发生死锁,例如:

1
2
3
4
Transaction 1, request 1, locks record 1 for update 
Transaction 2, request 1, locks record 2 for update
Transaction 1, request 2, tries to lock record 2 for update.
Transaction 2, request 2, tries to lock record 1 for update.

因为锁发生在不同的请求中,并且对于一个事务来说不可能提前知道所有它需要的锁,因此很难检测和避免数据库事务中的死锁。

死锁的避免

加锁顺序
当多个线程需要相同的一些锁,但是按照不同的顺序加锁,死锁就很容易发生。
如果能确保所有的线程都是按照相同的顺序获得锁,那么死锁就不会发生。看下面这个例子:

1
2
3
Thread 1:  lock A   lock B
Thread 2:   wait for A   lock C (when A locked)
Thread 3:   wait for A   wait for B   wait for C

如果一个线程(比如线程3)需要一些锁,那么它必须按照确定的顺序获取锁。它只有获得了从顺序上 排在前面的锁之后,才能获取后面的锁。
例如,线程2和线程3只有在获取了锁A之后才能尝试获取锁C(译者注:获取锁A是获取锁C的必要条件)。因为线程1已经拥有了锁A,所以线程2和3需要一直等到锁A被释放。然后在它们尝试对B或C加锁之前,必须成功地对A加了锁。
按照顺序加锁是一种有效的死锁预防机制。但是,这种方式需要你事先知道所有可能会用到的锁(译者 注:并对这些锁做适当的排序),但总有些时候是无法预知的。

加锁时限

另外一个可以避免死锁的方法是在尝试获取锁的时候加一个超时时间,这也就意味着在尝试获取锁的过 程中若超过了这个时限该线程则放弃对该锁请求。若一个线程没有在给定的时限内成功获得所有需要的 锁,则会进行回退并释放所有已经获得的锁,然后等待一段随机的时间再重试。这段随机的等待时间让 其它线程有机会尝试获取相同的这些锁,并且让该应用在没有获得锁的时候可以继续运行(译者注:加 锁超时后可以先继续运行干点其它事情,再回头来重复之前加锁的逻辑)。
以下是一个例子,展示了两个线程以不同的顺序尝试获取相同的两个锁,在发生超时后回退并重试的场景:

1
2
3
4
5
6
7
8
9
10
Thread 1 locks A 
Thread 2 locks B
Thread 1 attempts to lock B but is blocked
Thread 2 attempts to lock A but is blocked
Thread 1's lock attempt on B times out
Thread 1 backs up and releases A as well
Thread 1 waits randomly (e.g. 257 millis) before retrying.
Thread 2's lock attempt on A times out
Thread 2 backs up and releases B as well
Thread 2 waits randomly (e.g. 43 millis) before retrying.

在上面的例子中,线程2比线程1早200毫秒进行重试加锁,因此它可以先成功地获取到两个锁。这时, 线程1尝试获取锁A并且处于等待状态。当线程2结束时,线程1也可以顺利的获得这两个锁(除非线程2 或者其它线程在线程1成功获得两个锁之前又获得其中的一些锁)。
需要注意的是,由于存在锁的超时,所以我们不能认为这种场景就一定是出现了死锁。也可能是因为获得了锁的线程(导致其它线程超时)需要很长的时间去完成它的任务。

此外,如果有非常多的线程同一时间去竞争同一批资源,就算有超时和回退机制,还是可能会导致这些 线程重复地尝试但却始终得不到锁。如果只有两个线程,并且重试的超时时间设定为0到500毫秒之间, 这种现象可能不会发生,但是如果是10个或20个线程情况就不同了。因为这些线程等待相等的重试时间 的概率就高的多(或者非常接近以至于会出现问题)。

**这种机制存在一个问题,在Java中不能对synchronized同步块设置超时时间。你需要创建一个自定义锁,或使用Java5中java.util.concurrent包下的工具。**

死锁检测

死锁检测是一个更好的死锁预防机制,它主要是针对那些不可能实现按序加锁并且锁超时也不可行的场景。
每当一个线程获得了锁,会在线程和锁相关的数据结构中(map、graph等等)将其记下。除此之外, 每当有线程请求锁,也需要记录在这个数据结构中。
当一个线程请求锁失败时,这个线程可以遍历锁的关系图看看是否有死锁发生。例如,线程A请求锁7, 但是锁7这个时候被线程B持有,这时线程A就可以检查一下线程B是否已经请求了线程A当前所持有的锁。如果线程B确实有这样的请求,那么就是发生了死锁(线程A拥有锁1,请求锁7;线程B拥有锁7,请 求锁1)。
当然,死锁一般要比两个线程互相持有对方的锁这种情况要复杂的多。线程A等待线程B,线程B等待线 程C,线程C等待线程D,线程D又在等待线程A。线程A为了检测死锁,它需要递进地检测所有被B请求的锁。从线程B所请求的锁开始,线程A找到了线程C,然后又找到了线程D,发现线程D请求的锁被线程A自 己持有着。这是它就知道发生了死锁。

那么当检测出死锁时,这些线程该做些什么呢?

一个可行的做法是释放所有锁,回退,并且等待一段随机的时间后重试。这个和简单的加锁超时类似, 不一样的是只有死锁已经发生了才回退,而不会是因为加锁的请求超时了。虽然有回退和等待,但是如果有大量的线程竞争同一批锁,它们还是会重复地死锁(编者注:原因同超时类似,不能从根本上减轻竞争)。
一个更好的方案是给这些线程设置优先级,让一个(或几个)线程回退,剩下的线程就像没发生死锁一 样继续保持着它们需要的锁。如果赋予这些线程的优先级是固定不变的,同一批线程总是会拥有更高的 优先级。为避免这个问题,可以在死锁发生的时候设置随机的优先级。

2.8.2 饥饿和公平

如果一个线程因为CPU时间全部被其他线程抢走而得不到CPU运行时间,这种状态被称之为“饥饿”。而该线程被“饥饿致死”正是因为它得不到CPU运行时间的机会。解决饥饿的方案被称之为“公平性” – 即所有线程均能公平地获得运行机会。

Java中导致饥饿的原因

在Java中,下面三个常见的原因会导致线程饥饿:

  • 高优先级线程吞噬所有的低优先级线程的CPU时间

    你能为每个线程设置独自的线程优先级,优先级越高的线程获得的CPU时间越多,线程优先级值设置在1到10之间,而这些优先级值所表示行为的准确解释则依赖于你的应用运行平台。对大多数应用来说,你好是不要改变其优先级值。

  • 线程被永久堵塞在一个等待进入同步块的状态

    Java的同步代码区也是一个导致饥饿的因素。Java的同步代码区对哪个线程允许进入的次序没有任何保障。这就意味着理论上存在一个试图进入该同步区的线程处于被永久堵塞的风险,因为其他线程总是能持续地先于它获得访问,这即是“饥饿”问题,而一个线程被“饥饿致死”正是因为它得不到CPU运行时间的机会。

  • 线程在等待一个本身(在其上调用wait())也处于永久等待完成的对象

    如果多个线程处在wait()方法执行上,而对其调用notify()不会保证哪一个线程会获得唤醒,任何线程都有可能处于继续等待的状态。因此存在这样一个风险:一个等待线程从来得不到唤醒,因为其他等待线程总是能被获得唤醒。

文章作者: Frosro
文章链接: https://frosro.github.io/2021/01/08/%E5%B9%B6%E5%8F%91%E7%BC%96%E7%A8%8B%EF%BC%88%E4%BA%8C%EF%BC%89/
版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 BETTER LATE THAN NEVER
打赏
  • 微信
    微信
  • 支付宝
    支付宝

评论