JAVA基础-多线程中锁机制


多线程锁

  • 多线程锁机制
    • 锁的定义
    • 锁的分类
      • 公平锁/非公平锁
      • 可重入锁
      • 独享锁/共享锁
      • 互斥锁/读写锁
      • 乐观锁/悲观锁
      • 分段锁
      • 偏向锁/轻量级锁/重量级锁
      • 自旋锁
    • 锁的使用
      • AQS
        • AQS框架展示
        • AQS定义两种资源共享方式
        • AQS常用的几种方法(自定义同步器实现时)
        • 自定义同步器实现acquire(int)实现步骤
      • CAS
        • CAS介绍
        • CAS同步比较交互原理
        • JAVA对CAS的支持(原子类)
        • CAS 的会产生什么问题?
      • Lock
        • Lock接口抽象方法
        • Lock和syncronized的区别
      • ReentrantLock
        • ReentrantLock介绍展示
        • ReentrantLock实现重进入两个核心(锁获取和锁释放)
        • ReentrantLock实现重进入-获取锁
        • ReentrantLock实现重进入-释放锁
      • ReentrantReadWriteLock
        • ReentrantReadWriteLock-ReadWriteLock@ writeLock
        • ReentrantReadWriteLock支持功能
    • 线程锁常见问题
      • 什么是线程死锁
      • 形成死锁的四个必要条件是什么
      • 如何避免线程死锁
      • 死锁与活锁的区别,死锁与饥饿的区别?


多线程锁机制

锁的定义

多线程很容易访问共享的资源,所以需要锁定,防止某线程正在用的被其他线程调用,所以需要先锁定该区域,用完了再释放。这就是锁的概念

锁的分类

公平锁/非公平锁

  • 公平锁是指多个线程按照申请锁的顺序来获取锁。
  • 非公平锁是指多个线程获取锁的顺序并不是按照申请锁的顺序,有可能后申请的线程比先申请的线程优先获取锁。有可能,会造成优先级反转或者饥饿现象。
    对于Java ReentrantLock而言,通过构造函数指定该锁是否是公平锁,默认是非公平锁非公平锁的优点在于吞吐量比公平锁大。
    对于synchronized而言,也是一种非公平锁。由于其并不像ReentrantLock是通过AQS的来实现线程调度,所以并没有任何办法使其变成公平锁。

可重入锁

可重入锁又名递归锁,是指在同一个线程在外层方法获取锁的时候,在进入内层方法会自动获取锁(就是可以重新进入获得锁)对于Java ReentrantLock而言, 其名字是Reentrant Lock即是重新进入锁。对于synchronized而言,也是一个可重入锁。可重入锁的一个好处是可一定程度避免死锁

synchronized void setA() throws Exception{
    Thread.sleep(1000);
    setB();
}
synchronized void setB() throws Exception{
    Thread.sleep(1000);
}

上面的代码就是一个可重入锁的一个特点,如果不是可重入锁的话,setB可能不会被当前线程执行,可能造成死锁。

独享锁/共享锁

独享锁是指该锁一次只能被一个线程所持有;共享锁是指该锁可被多个线程所持有
对于Java ReentrantLock而言,其是独享锁。但是对于Lock的另一个实现类ReadWriteLock,其读锁是共享锁,其写锁是独享锁。读锁的共享锁可保证并发读是非常高效的,读写、写读 、写写的过程是互斥的。独享锁与共享锁也是通过AQS来实现的,通过实现不同的方法,来实现独享或者共享。对于synchronized而言,当然是独享锁。

互斥锁/读写锁

独享锁/共享锁就是一种广义的说法,互斥锁/读写锁就是具体的实现。互斥锁在Java中的具体实现就是ReentrantLock;读写锁在Java中的具体实现就是Read/WriteLock。

乐观锁/悲观锁

乐观锁与悲观锁不是指具体的什么类型的锁,而是指看待并发同步的角度。

  • 悲观锁:总是假设最坏的情况,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会阻塞直到它拿到锁。传统的关系型数据库里边就用到了很多这种锁机制,比如行锁,表锁等,读锁,写锁等,都是在做操作之前先上锁。再比如 Java 里面的同步原语 synchronized 关键字的实现也是悲观锁
  • 乐观锁:顾名思义,就是很乐观,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,可以使用版本号等机制。乐观锁适用于多读的应用类型,这样可以提高吞吐量,像数据库提供的类似于 write_condition 机制,其实都是提供的乐观锁。在 Java中 java.util.concurrent.atomic 包下面的原子变量类就是使用了乐观锁的一种实现方式 CAS 实现的
  • 在Java中java.util.concurrent.atomic包下面的原子变量类就是使用了乐观锁的一种实现方式CAS(Compare and Swap 比较并交换)实现的。

分段锁

分段锁其实是一种锁的设计,并不是具体的一种锁,对于ConcurrentHashMap而言,其并发的实现就是通过分段锁的形式来实现高效的并发操作,ConcurrentHashMap中的分段锁称为Segment,它即类似于HashMap(JDK7与JDK8中HashMap的实现)的结构,即内部拥有一个Entry数组,数组中的每个元素又是一个链表;同时又是一个ReentrantLock(Segment继承了ReentrantLock)。当需要put元素的时候,并不是对整个HashMap进行加锁,而是先通过hashcode来知道他要放在那一个分段中,然后对这个分段进行加锁,所以当多线程put的时候,只要不是放在一个分段中,就实现了真正的并行的插入。但是,在统计size的时候,可就是获取HashMap全局信息的时候,就需要获取所有的分段锁才能统计。
分段锁的设计目的是细化锁的粒度,当操作不需要更新整个数组的时候,就仅仅针对数组中的一项进行加锁操作。

偏向锁/轻量级锁/重量级锁

这三种锁是指锁的状态,并且是针对synchronized。在Java 5通过引入锁升级的机制来实现高效synchronized。这三种锁的状态是通过对象监视器在对象头中的字段来表明的。

  • 偏向锁是指一段同步代码一直被一个线程所访问,那么该线程会自动获取锁。降低获取锁的代价。
  • 轻量级锁是指当锁是偏向锁的时候,被另一个线程所访问,偏向锁就会升级为轻量级锁,其他线程会通过自旋的形式尝试获取锁,不会阻塞,提高性能
  • 重量级锁是指当锁为轻量级锁的时候,另一个线程虽然是自旋,但自旋不会一直持续下去,当自旋一定次数的时候,还没有获取到锁,就会进入阻塞,该锁膨胀为重量级锁。重量级锁会让其他申请的线程进入阻塞,性能降低。

自旋锁

在Java中,自旋锁是指尝试获取锁的线程不会立即阻塞,而是采用循环的方式去尝试获取锁,这样的好处是减少线程上下文切换的消耗,缺点是循环会消耗CPU

锁的使用

AQS

AQS框架展示

AbstractQueuedSynchronizer,类如其名,抽象的队列式的同步器,AQS定义了一套多线程访问共享资源的同步器框架,许多同步类实现都依赖于它,如常用的ReentrantLock/Semaphore/CountDownLatch…
JAVA基础-多线程中锁机制_第1张图片
它维护了一个volatile int state(代表共享资源)和一个FIFO线程等待队列(多线程争用资源被阻塞时会进入此队列)。这里volatile是核心关键词,具体volatile的语义,在此不述。state的访问方式有三种:

  • getState() 获取资源状态
  • setState() 设置资源状态
  • compareAndSetState() 尝试修改同步状态
AQS定义两种资源共享方式

AQS定义两种资源共享方式:Exclusive(独占,只有一个线程能执行,如ReentrantLock)和Share(共享,多个线程可同时执行,如Semaphore/CountDownLatch)
不同的自定义同步器争用共享资源的方式也不同。自定义同步器在实现时只需要实现共享资源state的获取与释放方式即可,至于具体线程等待队列的维护(如获取资源失败入队/唤醒出队等),AQS已经在顶层实现好了。

AQS常用的几种方法(自定义同步器实现时)

自定义同步器实现时主要实现以下几种方法:

  • isHeldExclusively():该线程是否正在独占资源。只有用到condition才需要去实现它。
  • tryAcquire(int):独占方式。尝试获取资源,成功则返回true,失败则返回false。
  • tryRelease(int):独占方式。尝试释放资源,成功则返回true,失败则返回false。
  • tryAcquireShared(int):共享方式。尝试获取资源。负数表示失败;0表示成功,但没有剩余可用资源;正数表示成功,且有剩余资源。
  • tryReleaseShared(int):共享方式。尝试释放资源,如果释放后允许唤醒后续等待结点返回true,否则返回false。

以ReentrantLock为例,state初始化为0,表示未锁定状态。A线程lock()时,会调用tryAcquire()独占该锁并将state+1。此后,其他线程再tryAcquire()时就会失败,直到A线程unlock()到state=0(即释放锁)为止,其它线程才有机会获取该锁。当然,释放锁之前,A线程自己是可以重复获取此锁的(state会累加),这就是可重入的概念。但要注意,获取多少次就要释放多么次,这样才能保证state是能回到零态的。

再以CountDownLatch以例,任务分为N个子线程去执行,state也初始化为N(注意N要与线程个数一致)。这N个子线程是并行执行的,每个子线程执行完后countDown()一次,state会CAS减1。等到所有子线程都执行完后(即state=0),会unpark()主调用线程,然后主调用线程就会从await()函数返回,继续后余动作。

一般来说,自定义同步器要么是独占方法,要么是共享方式,他们也只需实现tryAcquire-tryRelease、tryAcquireShared-tryReleaseShared中的一种即可。但AQS也支持自定义同步器同时实现独占和共享两种方式,如ReentrantReadWriteLock

自定义同步器实现acquire(int)实现步骤

acquire(int)此方法是独占模式下线程获取共享资源的顶层入口。如果获取到资源,线程直接返回,否则进入等待队列,直到获取到资源为止,且整个过程忽略中断的影响。这也正是lock()的语义
JAVA基础-多线程中锁机制_第2张图片
acquire(int)方法源码解读

    public final void acquire(int arg) {
        if (!tryAcquire(arg) &&
            acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
            selfInterrupt();
    }

①调用自定义同步器的tryAcquire()尝试直接去获取资源,如果成功则直接返回;
②没成功,则addWaiter()(此方法用于将当前线程加入到等待队列的队尾)将该线程加入等待队列的尾部,并标记为独占模式;
③acquireQueued()使线程在等待队列中休息,有机会时(轮到自己,会被unpark())会去尝试获取资源。获取到资源后才返回。如果在整个等待过程中被中断过,则返回true,否则返回false。
④如果线程在等待过程中被中断过,它是不响应的。只是获取资源后才再进行自我中断selfInterrupt(),将中断补上

CAS

CAS介绍

CAS(Compare and Swap 比较并交换)是乐观锁技术,当多个线程尝试使用CAS同时更新同一个变量时,只有其中一个线程能更新变量的值,而其它线程都失败,失败的线程并不会被挂起,而是被告知这次竞争中失败,并可以再次尝试。

CAS同步比较交互原理

CAS 操作包含三个操作数 —— 内存位置(V)、预期原值(A)和新值(B)
1.如果内存位置的值与预期原值相匹配,那么处理器会自动将该位置值更新为新值 。否则,处理器不做任何操作。无论哪种情况,它都会在CAS指令之前返回该位置的值。(在 CAS 的一些特殊情况下将仅返回 CAS 是否成功,而不提取当前值。)
CAS 有效地说明了“我认为位置 V 应该包含值 A;如果包含该值,则将 B 放到这个位置;否则,不要更改该位置,只告诉我这个位置现在的值即可。”

JAVA对CAS的支持(原子类)

在JDK1.5中新增java.util.concurrent包就是建立在CAS之上的。相对于对于synchronized 这种阻塞算法,CAS是非阻塞算法的一种常见实现。所以java.util.concurrent在性能上有了很大的提升。

以java.util.concurrent包中的AtomicInteger为例,看一下在不使用锁的情况下是如何保证线程安全的。主要理解 getAndIncrement方法,该方法的作用相当于 ++i 操作。

public class AtomicInteger extends Number implements java.io.Serializable {  
    private volatile int value; 
    public final int get() {  
        return value;  
    }  
    public final int getAndIncrement() {  
        for (;;) {  
            int current = get();  
            int next = current + 1;  
            if (compareAndSet(current, next))  
                return current;  
        }  
    }  
    public final boolean compareAndSet(int expect, int update) {  
        return unsafe.compareAndSwapInt(this, valueOffset, expect, update);  
    }  
}
CAS 的会产生什么问题?

1、ABA 问题:
线程T1从从内存位置 V 中取出 A,
①线程 T2 也从内存中取出 A,并且T2进行了一些操作变成了 B,又将V位置的数据变成 A
②这时候线程T1进行 CAS 操作发现内存中仍然是 A,然后线程T1操作成功
③尽管线程T1的 CAS 操作成功,但可能存在潜藏的问题
从Java1.5 开始 JDK 的 atomic包里提供了一个类 AtomicStampedReference 来解决 ABA 问题。
2、循环时间长开销大:
对于资源竞争严重(线程冲突严重)的情况,CAS 自旋的概率会比较大,从而浪费更多的 CPU 资源,效率低于 synchronized。
3、只能保证一个共享变量的原子操作:
当对一个共享变量执行操作时,我们可以使用循环 CAS 的方式来保证原子操作,但是对多个共享变量操作时,循环 CAS 就无法保证操作的原子性,这个时候就可以用锁

Lock

java.util.concurrent.locks.Lock 是一个类似于synchronized 块的线程同步机制。但是 Lock比 synchronized 块更加灵活。Lock是个接口,有个实现类是ReentrantLock

Lock接口抽象方法
  • void lock():获取锁,如果锁不可用,则出于线程调度的目的,当前线程将被禁用,并且在获取锁之前处于休眠状态。
  • boolean tryLock():如果锁可用立即返回true,如果锁不可用立即返回false;
  • boolean tryLock(long time, TimeUnit unit) throws InterruptedException:如果锁可用,则此方法立即返回true。 如果该锁不可用,则当前线程将出于线程调度目的而被禁用并处于休眠状态,直到发生以下三种情况之一为止:
    ①当前线程获取到该锁;
    ②当前线程被其他线程中断,并且支持中断获取锁;
    ③经过指定的等待时间如果获得了锁,则返回true,没获取到锁返回false。
  • void unlock():释放锁。释放锁的操作放在finally块中进行,以保证锁一定被被释放,防止死锁的发生。
Lock和syncronized的区别
  • synchronized是Java语言的关键字。Lock是一个类。
  • synchronized不需要用户去手动释放锁,发生异常或者线程结束时自动释放锁;Lock则必须要用户去手动释放锁,如果没有主动释放锁,就有可能导致出现死锁现象。
  • lock可以配置公平策略,实现线程按照先后顺序获取锁。
  • 提供了trylock方法 可以试图获取锁,获取到或获取不到时,返回不同的返回值 让程序可以灵活处理。
  • lock()和unlock()可以在不同的方法中执行,可以实现同一个线程在上一个方法中lock()在后续的其他方法中unlock(),比syncronized灵活的多

ReentrantLock

ReentrantLock介绍展示

ReentrantLock(重新进入锁定),是一个可重入且独占式的锁,它具有与使用synchronized监视器锁相同的基本行为和语义,但与synchronized关键字相比,它更灵活、更强大,增加了轮询、超时、中断等高级功能默认为非公平锁
JAVA基础-多线程中锁机制_第3张图片
ReentrantLock的内部类Sync继承了AQS,分为公平锁FairSync和非公平锁NonfairSync
1.如果在绝对时间上,先对锁进行获取的请求你一定先被满足,那么这个锁是公平的,反之,是不公平的。
2.公平锁的获取,也就是等待时间最长的线程最优先获取锁,也可以说锁获取是顺序的。ReentrantLock的公平与否,可以通过它的构造函数来决定。

ReentrantLock实现重进入两个核心(锁获取和锁释放)

实现重进入
重进入是指任意线程在获取到锁之后能够再次获取该锁而不会被锁阻塞,该特性的首先需要解决以下两个问题(锁获取和锁释放):

  • 线程再次获取锁:所需要去识别获取锁的线程是否为当前占据锁的线程,如果是,则再次获取成功;
  • 锁的最终释放:线程重复n次获取了锁,随后在第n次释放该锁后,其它线程能够获取到该锁。锁的最终释放要求锁对于获取进行计数自增,计数表示当前线程被重复获取的次数,而被释放时,计数自减,当计数为0时表示锁已经成功释放。
    ReentrantLock是通过自定义同步器来实现锁的获取与释放,我们以非公平锁(默认)实现为例,
ReentrantLock实现重进入-获取锁

获取锁
ReentrantLock的默认构造函数为:

public ReentrantLock() {
    sync = new NonfairSync();
}

即内部同步组件为非公平锁,获取锁的代码为:

public void lock() {
    sync.lock();
}

通过简介中的类图可以看到,Sync类是ReentrantLock自定义的同步组件,它是ReentrantLock里面的一个内部类,它继承自AQS,它有两个子类:公平锁FairSync和非公平锁NonfairSync。ReentrantLock的获取与释放锁操作都是委托给该同步组件来实现的。下面我们来看一看非公平锁的lock()方法:

final void lock() {
    if (compareAndSetState(0, 1))
        setExclusiveOwnerThread(Thread.currentThread());
    else
        acquire(1);
}

该程序首先会通过compareAndSetState(int, int)方法来尝试修改同步状态,如果修改成功则表示获取到了锁,然后调用setExclusiveOwnerThread(Thread)方法来设置获取到锁的线程,该方法是继承自AbstractOwnableSynchronizer类,AQS继承自AOS类,它的主要作用就是记录获取到独占锁的线程 AOS类的定义很简单:

public abstract class AbstractOwnableSynchronizer
    implements java.io.Serializable {
    private static final long serialVersionUID = 3737899427754241961L;
 
    protected AbstractOwnableSynchronizer() { }
 
    // The current owner of exclusive mode synchronization.
    private transient Thread exclusiveOwnerThread;
 
    protected final void setExclusiveOwnerThread(Thread thread) {
        exclusiveOwnerThread = thread;
    }
 
    protected final Thread getExclusiveOwnerThread() {
        return exclusiveOwnerThread;
    }
}

如果同步状态修改失败,则表示没有获取到锁,需要调用acquire(int)方法,该方法定义在AQS中,如下:

public final void acquire(int arg) {
    if (!tryAcquire(arg) &&
        acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
        selfInterrupt();
}

tryAcquire(int)是子类需要重写的方法,在非公平锁中的实现如下:

protected final boolean tryAcquire(int acquires) {
    return nonfairTryAcquire(acquires);
}
final boolean nonfairTryAcquire(int acquires) {
    // 获取当前线程
    final Thread current = Thread.currentThread();
    // 获取同步状态
    int c = getState();
    // 同步状态为0,表示没有线程获取锁
    if (c == 0) {
        // 尝试修改同步状态
        if (compareAndSetState(0, acquires)) {
            // 同步状态修改成功,获取到锁
            setExclusiveOwnerThread(current);
            return true;
        }
    }
    // 同步状态不为0,表示已经有线程获取了锁,判断获取锁的线程是否为当前线程
    else if (current == getExclusiveOwnerThread()) {
        // 获取锁的线程是当前线程
        int nextc = c + acquires;
        if (nextc < 0) // overflow
            throw new Error("Maximum lock count exceeded");
        setState(nextc);
        return true;
    }
    // 获取锁的线程不是当前线程
    return false;
}

nonfairTryAcquire(int)方法首先判断同步状态是否为0,如果是0,则表示该锁还没有被线程持有,然后通过CAS操作获取同步状态,如果修改成功,返回true。如果同步状态不为0,则表示该锁已经被线程持有,需要判断当前线程是否为获取锁的线程,如果是则获取锁,成功返回true。成功获取锁的线程再次获取该锁,只是增加了同步状态的值,这也就实现了可重入锁。

ReentrantLock实现重进入-释放锁

释放锁
成功获取锁的线程在完成业务逻辑之后,需要调用unlock()来释放锁:

public void unlock() {
    sync.release(1);
}
unlock()调用NonfairSync类的release(int)方法释放锁,release(int)方法是定义在AQS中的方法:

public final boolean release(int arg) {
    if (tryRelease(arg)) {
        Node h = head;
        if (h != null && h.waitStatus != 0)
            unparkSuccessor(h);
        return true;
    }
    return false;
}

tryRelease(int)是子类需要实现的方法:
方法作用独占方式。尝试释放资源,成功则返回true,失败则返回false

protected final boolean tryRelease(int releases) {
    // 计算新的状态值
    int c = getState() - releases;
    // 判断当前线程是否是持有锁的线程,如果不是的话,抛出异常
    if (Thread.currentThread() != getExclusiveOwnerThread())
        throw new IllegalMonitorStateException();
    boolean free = false;
    // 新的状态值是否为0,若为0,则表示该锁已经完全释放了,其他线程可以获取同步状态了
    if (c == 0) {
        free = true;
        setExclusiveOwnerThread(null);
    }
    // 更新状态值
    setState(c);
    return free;
}

如果该锁被获取n次,那么前(n-1)次tryRelease(int)方法必须返回false,只有同步状态完全释放了,才能返回true。可以看到,该方法将同步状态是否为0作为最终释放的条件,当状态为0时,将占有线程设为null,并返回true,表示释放成功

ReentrantReadWriteLock

ReentrantReadWriteLock-ReadWriteLock@ writeLock

读写锁内部维护了两个锁,一个用于读操作,一个用于写操作。所有 ReadWriteLock实现都必须保证 writeLock操作的内存同步效果也要保持与相关 readLock的联系。也就是说,成功获取读锁的线程会看到写入锁之前版本所做的所有更新

ReentrantReadWriteLock支持功能

ReentrantReadWriteLock支持以下功能:
1)支持公平和非公平的获取锁的方式;
2)支持可重入。读线程在获取了读锁后还可以获取读锁;写线程在获取了写锁之后既可以再次获取写锁又可以获取读锁;
3)还允许从写入锁降级为读取锁,其实现方式是:先获取写入锁,然后获取读取锁,最后释放写入锁。但是,从读取锁升级到写入锁是不允许的;
4)读取锁和写入锁都支持锁获取期间的中断;
5)Condition支持。仅写入锁提供了一个 Conditon 实现;读取锁不支持 Conditon ,readLock().newCondition() 会抛出 UnsupportedOperationException。
JAVA基础-多线程中锁机制_第4张图片

import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
public class Test {
    public static void main(String[] args){
        for(int i = 0; i < 10; i++){
            new Thread(new Runnable() {
                @Override
                public void run() {
                    Cache.put("key", new String(Thread.currentThread().getName() + " joke"));
                }
            }, "threadW-"+ i).start();
            new Thread(new Runnable() {

                @Override
                public void run() {
                    System.out.println(Cache.get("key"));
                }
            }, "threadR-"+ i).start();
            new Thread(new Runnable() {

                @Override
                public void run() {
                    Cache.clear();
                }
            }, "threadC-"+ i).start();
        }

    }
}

class Cache {
    static Map<String, Object> map = new HashMap<String, Object>();
    static ReentrantReadWriteLock rwl = new ReentrantReadWriteLock();
    static Lock r = rwl.readLock();
    static Lock w = rwl.writeLock();
    // 获取一个key对应的value
    public static final Object get(String key) {
        r.lock();
        try {
            System.out.println("get " + Thread.currentThread().getName());
            return map.get(key);
        } finally {
            r.unlock();
        }
    }
    // 设置key对应的value,并返回旧有的value
    public static final Object put(String key, Object value) {
        w.lock();
        try {
            System.out.println("put " + Thread.currentThread().getName());
            return map.put(key, value);
        } finally {
            w.unlock();
        }
    }
    // 清空所有的内容
    public static final void clear() {
        w.lock();
        try {
            System.out.println("clear " + Thread.currentThread().getName());
            map.clear();
        } finally {
            w.unlock();
        }
    }
}

线程锁常见问题

什么是线程死锁

  • 死锁是指两个或两个以上的进程(线程)在执行过程中,由于竞争资源或者由于彼此通信而造成的一种阻塞的现象,若无外力作用,它们都将无法推进下去。此时称系统处于死锁状态或系统产生了死锁,这些永远在互相等待的进程(线程)称为死锁进程(线程)
  • 多个线程同时被阻塞,它们中的一个或者全部都在等待某个资源被释放。由于线程被无限期地阻
    塞,因此程序不可能正常终止。
  • 如下图所示,线程 A 持有资源 2,线程 B 持有资源 1,他们同时都想申请对方的资源,所以这两个线程就会互相等待而进入死锁状态
    JAVA基础-多线程中锁机制_第5张图片

形成死锁的四个必要条件是什么

  • 互斥条件:在一段时间内某资源只由一个进程占用。如果此时还有其它进程请求资源,就只能等待,直至占有资源的进程用毕释放。如上线程一已经占有锁A 线程二也想要锁A,只能等
  • 占有且等待条件:指进程已经保持至少一个资源,但又提出了新的资源请求,而该资源已被其它进程占有,此时请求进程阻塞,但又对自己已获得的其它资源保持不放。线程一有了A还想要B,但是B被线程二占有,线程一又不放弃锁A
  • 不可抢占条件:别人已经占有了某项资源,你不能因为自己也需要该资源,就去把别人的资源抢过来。
  • 循环等待条件:若干进程之间形成一种头尾相接的循环等待资源关系。(比如一个进程集合,A在等B,B在等C,C在等A)

如何避免线程死锁

  • 避免一个线程同时获得多个锁
  • 避免一个线程在锁内同时占用多个资源,尽量保证每个锁只占用一个资源
  • 尝试使用定时锁,使用lock.tryLock(timeout)来替代使用内部锁机制

死锁与活锁的区别,死锁与饥饿的区别?

  • 死锁:是指两个或两个以上的进程(或线程)在执行过程中,因争夺资源而造成的一种互相等待的现象,若无外力作用,它们都将无法推进下去。
  • 活锁:任务或者执行者没有被阻塞,由于某些条件没有满足,导致一直重复,尝试,失败,尝试,失败
  • 活锁和死锁的区别在于,处于活锁的实体是在不断的改变状态,这就是所谓的“活”, 而处于死锁的实体表现为等待;活锁有可能自行解开,死锁则不能
  • 饥饿:一个或者多个线程因为种种原因无法获得所需要的资源,导致一直无法执行的状态。
    Java 中导致饥饿的原因:
    1、高优先级线程吞噬所有的低优先级线程的 CPU 时间。
    2、线程被永久堵塞在一个等待进入同步块的状态,因为其他线程总是能在它之前持续地对该
    同步块进行访问。
    3、线程在等待一个本身也处于永久等待完成的对象(比如调用这个对象的 wait 方法),因为其
    他线程总是被持续地获得唤醒

你可能感兴趣的:(多线程,java)