Java EE(进阶版)

        这些锁策略能适用于很多中语言,博主是学Java的,所以下面的代码会用Java去写,请大家见谅,但是处理的方法是大差不差的。 

一、常见锁和锁策略:

(一)、乐观锁和悲观锁

1、何为乐观锁和悲观锁呢?

答:乐观锁对应于生活中乐观的人总是想着事情往好的方向发展,而悲观锁对应于生活中悲观的人总是想着事情往坏的方向发展。这两种态度各有优缺点,不能不以场景而定,去说一种好于另外一种。乐观锁和悲观锁是两种思想,主要用于解决并发场景下的数据竞争问题。

乐观锁:乐观锁在进行操作数据时非常乐观,认为别人不会同时修改数据,因此乐观锁不会上锁,只是在执行更新的时候判断一下在此期间别人是否修改了数据:如果别人修改了数据则放弃操作,否则执行操作。

悲观锁:在执行操作数据时比较悲观,认为别人会同时修改数据,因此操作数据时直接把数据锁住,直到操作完成后才会释放锁,而在上锁期间其他人不能修改数据。

2、乐观锁和悲观锁的实现式

2.1、乐观锁的实现机制主要有两种:CAS机制和版本号机制

2.1.1、何为CAS呢?

答:CAS全称Compare and swap,翻译过来就是比较并且交换,从这里就可以明白,一个CAS涉及一下三个操作步骤:

假设内存的原始数据是a,旧的预期值是b,需要修改的新值是c
第一步:比较b和a的值是否相等
第二步:如果返回相等就把c的值写入a中
第三步:返回操作成功

下面这个图片是CAS伪代码,仅提供参考,用于去理解CAS的执行流程

Java EE(进阶版)_第1张图片

看完上述代码可以大致明白CAS的执行过程,但是这里要注意CAS的伪代码并不是原子的,是典型的check and set(判定后设定值),当多个线程同时使用CAS操作的时候,如果不做处理明显会造成线程不安全的操作,但是大佬们在设计CAS的已经充分考虑了这一点了,多个线程使用CAS时候,只允许有一个线程操作成功,其他线程虽然不会阻塞,但是会接收到操作失败的返回值结果。

2.1.2、CAS是如何实现的呢?

答:java的CAS利用的是unsafe类提供的CAS操作,而unsafe的CAS依赖于JVM针对不同操作系统实现的Atomic::cmpxch实现,而Atomic::cmpxch的实现使用了汇编的CAS操作,并且使用CPU硬件提供的lock机制从而保证原子性。

2.1.3、CAS的应用

(1)、原子类的使用(位于java.util.concurr.atomic):

这里只举例AtomicInteger类,直接上代码,具体的使用可以自行探索

public class AtomicCounter {

    private final AtomicInteger counter = new AtomicInteger(0);

    public int getValue() {
//直接中主内存中读取变量的值
        return counter.get();
    }
    public void increment() {
        while(true) {
            int existingValue = getValue();
            int newValue = existingValue + 1;
//执行CAS操作,成功返回true,失败返回false
            if(counter.compareAndSet(existingValue, newValue)) {
                return;
            }
        }
    }
}

(2)、实现自旋锁:

import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicReference;
public class Demo
{
    AtomicReference atomicReference = new AtomicReference<>();
    public void lock()
    {
        Thread thread = Thread.currentThread();
        System.out.println(Thread.currentThread().getName()+"\t"+"----come in");
        while (!atomicReference.compareAndSet(null, thread)) {
        }
    }
    public void unLock()
    {
        Thread thread = Thread.currentThread();
        atomicReference.compareAndSet(thread,null);
        System.out.println(Thread.currentThread().getName()+"\t"+"----task over,unLock...");
    }
    public static void main(String[] args)
    {
        Demo spinLockDemo = new Demo();
        new Thread(() -> {
            spinLockDemo.lock();
            //暂停几秒钟线程
            try {
                TimeUnit.SECONDS.sleep(5);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            spinLockDemo.unLock();
        },"A").start();
        //暂停500毫秒,线程A先于B启动
        try {
            TimeUnit.MILLISECONDS.sleep(500);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        new Thread(() -> {
            spinLockDemo.lock();
            spinLockDemo.unLock();
        },"B").start();
    }
}

 java中自旋锁是一种轻量级锁的实现,其优缺点如下:

优点:没有放弃 CPU, 不涉及线程阻塞和调度, 一旦锁被释放, 就能第一时间获取到锁

缺点:如果锁被其他线程持有的时间比较久, 那么就会持续的消耗 CPU 资源,导致CPU做无用功

2.1.4、为什么会提出版本号机制?

给一个例子:

Java EE(进阶版)_第2张图片

假设t1线程工作时间10秒,t2线程工作时间为2秒,由于t2线程的工作时间很短,那么在t1线程工作的时间之内,主内存的共享变量A已经被t2线程修改了多次了,只是恰好最后一次修改的值是共享变量A的初始值,此时用CAS机制判定出来的结果共享变量A虽然是期望值,但是A已经不再是原来的A了,这就是ABA问题。有些业务可能不需要关心中间过程,只要前后值一样就行,但是有些业务却要求变量在中间过程中不能发生改变,显然CAS就无法解决这个问题了,此时就要进行优化了

2.1.5、何为版本号机制?

版本号的机制是给要进行修改的数据中增加一个版本号信息,用于表示当前数据的版本号,每次数据被修改成功的时候,版本号+1。
操作步骤的跟新:
当某个线程查询数据时,将该数据的版本号一起查出来。
当该线程更新数据时,判断当前版本号与之前读取的版本号是否一致,如果一致才进行操作。

下面举两个例子:

(1)、考虑下面这个场景:某款游戏的系统要进行更新玩家的金币数,而金币的跟新结果取决于当前玩家的金币数量,因此就要查询当前玩家的金币数量

//代码仅仅是例子  
public void updateCoins(Integer playerId){
    //根据player_id查询玩家信息
    Player player = query("select coins, level from player where player_id = {0}", playerId);
    //根据玩家当前信息及其他信息,计算新的金币数
    Long newCoins = ……;
    //更新金币数
    update("update player set coins = {0} where player_id = {1}", newCoins, playerId);
}

这段代码很明显,在涉及多个线程操作的时候就会涉及线程安全的问题,很可能会影响玩家的金币数量,但是当我们引入一个版本号的时候就会解决这样的问题了看代码:

代码仅仅是例子
public void updateCoins(Integer playerId){
    //根据player_id查询玩家信息,包含version信息
    Player player = query("select coins, level, version from player where player_id = {0}", playerId);
    //根据玩家当前信息及其他信息,计算新的金币数
    Long newCoins = ……;
    //更新金币数,条件中增加对version的校验
    update("update player set coins = {0} where player_id = {1} and version = {2}", newCoins, playerId, player.version);
}

(2)、假设有三个线程:t1、t2、t3,三者共享的变量A的初始值是200

假设在执行减100的是操作时候出现了卡顿(t1),导致多创建一个减100操作(t2),cpu调度t1线程操作执行的时候,正常执行,A的是变为了100,是预期值,但是在执行t2之前,t3线程给A又加了100,A又变回了200,轮到t2线程执行的时候,发现A的值又变成200了,那我就减100,执行成功,A的值又变成了100。但是大家想一想,t2线程应该减100嘛?

Java EE(进阶版)_第3张图片

 解决:引入版本号

对比上面的没有引入版本号的理解

Java EE(进阶版)_第4张图片

对比可以发现,引入版本号之后就可以很好的解决CAS中潜在的ABA问题

2.2悲观锁的实现机制主要有:synchronized 关键字和 Lock 接口相关类

Java 中悲观锁的实现包括 synchronized 关键字和 Lock 相关类等,我们以 Lock 接口为例,例如 Lock 的实现类 ReentrantLock,类中的 lock() 等方法就是执行加锁,而 unlock()方法是执行解锁。处理资源之前必须要先加锁并拿到锁,等到处理完了之后再解开锁,这就是非常典型的悲观锁思想

3、悲观锁和乐观锁使用场景:

3.1、从功能方面来说,与悲观锁相比,乐观锁的使用受到了更多的限制,不管是CAS还是版本号机制

例如:CAS只能保证单个变量操作的原子性,当涉及到多个变量时,CAS是无能为力的,而synchronized则可以通过对整个代码块加锁来处理;再比如:版本号机制,如果query的时候是表1,而而update的时候是针对表2,也很难通过简单的版本号来实现乐观锁,此时悲观锁就可以使用了

3.2、从锁竞争的激烈程度来说,使用哪一种锁要根据锁竞争的激烈程度来考虑
当竞争不激烈 (出现并发冲突的概率小)时,乐观锁更有优势,因为悲观锁会锁住代码块或数据,其他线程无法同时访问,影响并发,而且而且加锁和释放锁都需要消耗额外的资源。当竞争激烈(出
现并发冲突的概率大)时,悲观锁更有优势,因为乐观锁在执行更新时频繁失败,需要不断重试,浪费CPU资源。

(二)、读写锁

1、何为读写锁?

答:Java读写锁,也就是ReentrantReadWriteLock,其包含了读锁和写锁,其中读锁是可以多线程共享的,即共享锁,而写锁是排他锁,在更改时候不允许其他线程操作。读写锁底层是同一把锁(基于同一个AQS),所以会有同一时刻不允许读写锁共存的限制。

代码演示:

public static void main(String[] args) {
    ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
    ReentrantReadWriteLock.WriteLock writeLock = lock.writeLock();
    ReentrantReadWriteLock.ReadLock readLock = lock.readLock();

    Thread t1 = new Thread(() -> {
        readLock.lock();
        System.out.println(Thread.currentThread().getName() + " read lock ok");
        LockSupport.parkNanos(TimeUnit.SECONDS.toNanos(1));
        readLock.unlock();
    });

    Thread t2 = new Thread(() -> {
        readLock.lock();
        System.out.println(Thread.currentThread().getName() + " read lock ok");
        LockSupport.parkNanos(TimeUnit.SECONDS.toNanos(1));
        readLock.unlock();
    });

    Thread t3 = new Thread(() -> {
        writeLock.lock();
        System.out.println(Thread.currentThread().getName() + " write lock ok");
        writeLock.unlock();
    });

    t1.start();
    t2.start();
    t3.start();
}

结果:Java EE(进阶版)_第5张图片,由此可见,读写锁适用于频繁读,不频繁写的场景

2、java中实现读写锁接口的类ReentrantReadWriteLock

2.1、ReentrantReadWriteLock类的特点:

(1)具有与ReentrantLock类似的公平锁和非公平锁的实现:默认的支持非公平锁,对于二者而言,非公平锁的吞吐量由于公平锁

(2)支持重入:读线程获取读锁之后能够再次获取读锁,写线程获取写锁之后能再次获取写锁,也可以获取读锁

(3)锁能降级:遵循获取写锁、获取读锁在释放写锁的顺序,即写锁能够降级为读锁,读锁不能升级为写锁

提示:锁降级是指:如果当先线程是写锁的持有者,并保持获得写锁的状态,同时又获取到读锁,然后释放写锁的过程,看如下代码演示:

import java.util.concurrent.locks.ReentrantReadWriteLock;

public class ReentrantReadWriteLockDemo {
    private static ReentrantReadWriteLock reentrantLock = new ReentrantReadWriteLock();
    private static ReentrantReadWriteLock.ReadLock readLock = reentrantLock.readLock();
    private static ReentrantReadWriteLock.WriteLock writeLock = reentrantLock.writeLock();

    public static void read() {
        System.out.println(Thread.currentThread().getName() + "开始尝试获取读锁");
        readLock.lock();
        try {
            System.out.println(Thread.currentThread().getName() + "获取读锁,开始执行");
            Thread.sleep(20);
            System.out.println(Thread.currentThread().getName()+ "尝试升级读锁为写锁");
            //读锁升级为写锁(失败)
            writeLock.lock();
            System.out.println(Thread.currentThread().getName() +"读锁升级为写锁成功");
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            readLock.unlock();
            System.out.println(Thread.currentThread().getName() + "释放读锁");
        }
    }

    public static void write() {
        System.out.println(Thread.currentThread().getName() + "开始尝试获取写锁");
        writeLock.lock();
        try {
            System.out.println(Thread.currentThread().getName() + "获取写锁,开始执行");
            Thread.sleep(40);
            System.out.println(Thread.currentThread().getName() +"尝试降级写锁为读锁");
            //写锁降级为读锁(成功)
            readLock.lock();
            System.out.println(Thread.currentThread().getName()+ "写锁降级为读锁成功");
            System.out.println();
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            System.out.println(Thread.currentThread().getName() + "释放写锁");
            writeLock.unlock();
            readLock.unlock();
        }
    }
    public static void main(String[] args) {
        new Thread(() -> write(), "Thread1").start();
        new Thread(() -> read(), "Thread2").start();
    }
}

(三)、公平锁和非公平锁

1、何为公平锁和非公平锁?

答:公平锁:多个线程按照申请锁的顺序去获得锁,线程会直接进入队列去排队,永远都是队列的第一位才能得到锁;非公平锁:多个线程去获取锁的时候,会直接去尝试获取,获取不到,再去进入等待队列,如果能获取到,就直接获取到锁

上图理解:公平vs非公平:

Java EE(进阶版)_第6张图片

Java EE(进阶版)_第7张图片

2、二者的优缺点:

Java EE(进阶版)_第8张图片

(四)、可重入锁和不可重入锁

1、何为可重入锁和不可重入锁?

答:可重入锁:当线程获取某个锁后,还可以继续获取它,可以递归调用,而不会发生死锁;不可重入锁:获取锁后不能重复获取,否则会造成死锁。

2.代码演示不可重入锁:

public class Demo {

    private Thread owner;// 持有锁的线程,为null表示无人占有

    /**
     * 获取锁,锁被占用时阻塞直到锁被释放
     * @throws InterruptedException 等待锁时线程被中断
     */
    public synchronized void lock() throws InterruptedException {
        Thread thread = Thread.currentThread();
        // wait()方法一般和while一起使用,防止因其它原因唤醒而实际没达到期望的条件
        while (owner != null) {
            System.out.println(String.format("%s 等待 %s 释放锁",
                    thread.getName(), owner.getName()));
            wait(); // 阻塞,直到被唤起
        }
        System.out.println(thread.getName() + " 获得了锁");
        owner = thread;//成功上位
    }

    public synchronized void unlock() {
        //只有持有锁的线程才有资格释放锁,别的线程不去调用它
        if (Thread.currentThread() != owner) {
            throw new IllegalMonitorStateException();
        }
        System.out.println(owner.getName() + " 释放了持有的锁");
        owner = null;
        notify();//唤醒一个等待锁的线程,也可以用notifyAll()
    }

    public static void main(String[] args) throws InterruptedException {
        Demo lock = new Demo();
        lock.lock(); // 获取锁
        lock.lock(); // 再次获取锁,造成死锁
    }
}

有上述代码执行的效果的锁是不可重入的锁

3.代码演示可重入锁: 

3.1使用synchronized演示:

public class Demo {

    public static void main(String[] args) throws Exception {
        new Thread(() -> {
            lock(5);
        }).start();
        Thread.sleep(1000);
        System.out.println("我是主线程,我也要来");
        lock(2);
    }

    //可重入锁也被称为递归锁,自己锁自己而不会造成死锁
    private static synchronized void lock(int count) {
        if (count == 0) {
            return;
        }
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName() + " " + count);
        lock(count - 1);
    }

}

3.2使用ReentrantLock演示:

import java.util.concurrent.locks.ReentrantLock;
public class Demo {

    public static void main(String[] args) throws Exception {
        // 构造函数可传入一个布尔,表示是否使用公平锁(什么是公平锁,看上面的讲解)
        ReentrantLock lock = new ReentrantLock(false);
        new Thread(() -> {
            lock.lock();
            System.out.println("A 获取了锁");
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("A 释放了锁");
            lock.unlock();
        }).start();
        new Thread(() -> {
            System.out.println("B 等待锁");
            lock.lock();
            System.out.println("B 获取了锁");
            lock.unlock();
            System.out.println("B 释放了锁");
        }).start();
    }
}

注:这里重量级锁和轻量级锁,我没有进行讲解,会在后面的synchronized中涉及到,不必担心

二、synchronized讲解

总结:从上述的锁策略中,可以得出java中的synchronized的情况(JDK1.8)

1、synchronized特点

1. 开始时是乐观锁, 如果锁冲突频繁, 就转换为悲观锁.
2. 开始是轻量级锁实现, 如果锁被持有的时间较长, 就转换成重量级锁.
3. 实现轻量级锁的时候大概率用到的自旋锁策略
4. 是一种不公平锁
5. 是一种可重入锁
6. 不是读写锁

2、锁的加锁过程

Java EE(进阶版)_第9张图片

从上图看出JVM将synchronized锁分为无状态、偏向锁、轻量级锁、重量级锁四个状态,根据情况,会有升级的情况,下面讲一讲升级的大致原理和过程

2.1、无锁到偏向锁

在讲这个之前要讲一下相关的东西:对象中的对象头

什么是对象头呢?

答:对象头(Object Header)包括两部分信息,第一部分用于存储对象自身的运行时数据, 如哈希码(HashCode)、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等等,官方称它为“Mark Word”,对象头的另外一部分是类型指针,即是对象指向它的类的元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例。

偏向锁不是真正的加锁,举个例子,一段同步的代码,一直只被线程A去访问,也没有其他的线程来访问,线程A每次访问一次就去获取锁,那岂不是浪费了很多资源,锁的创建和销毁是很消耗资源的,所以这种情况下就会进入偏向锁状态,如果后续没有其他线程来竞争该锁, 其他同步操作了就不用进行,避免了加锁解锁的开销,偏向锁本质上相当于 "延迟加锁" . 能不加锁就不加锁, 尽量来避免不必要的加锁开销。

2.2、偏向锁到轻量级锁

在偏向锁的情况下,一旦有第二个线程参与竞争,就会立即膨胀为轻量级锁,企图去获取锁的线程一开始会使用自旋的方式去获取锁,如果循环几次,其他的线程释放了锁,就不需要进行用户态到内核态的切换,但是如果一直获取不到锁,我就一直自旋吗?显然不可能,自旋会占用很多CPU的资源。JDK1.7以后就对自旋锁做了一定的优化,自适应自旋锁的自旋次数不在固定,而是由上一次在同一个锁上的自旋时间及锁的拥有者的状态来决定的,如果超过了次数,就会继续膨胀。

2.3、轻量级锁到重量级锁

如果锁之间的竞争进一步激烈,就会转变为重量级锁,此处重量级锁就是指用到内核提供的mutex,线程获取到锁就会执行加锁状态从用户态到核心态的转换,没有获取到锁的线程会阻塞等待CPU的调度。

3、其他的锁优化操作

3.1、锁消除

Java的JIT机制会通过逃逸分析,去分析加锁的代码段/共享资源,他们是否被一个或者多个线程使用,或者等待被使用,如果通过分析证实,只被一个线程访问,在编译这个代码段的时候就不生成 Synchronized 关键字,仅仅生成代码对应的机器码,换句话说即使在代码段上加上了synchronized锁,只要JIT发现这个代码段只有一个线程在进行访问,就会去掉synchronized,从而提高访问速率。

3.2、锁粗化

锁粗化是JIT 编译器对内部锁具体实现的优化:假设有几个在程序上相邻的同步块代码段上,每个同步块使用的是同一个锁实例,那么 JIT 会在编译的时候将这些同步块合并成一个大同步块,并且使用同一个锁实例。这样避免一个线程反复申请/释放锁,减少资源的消耗。

三、JUC中的ReentrantLock

1、什么是ReentrantLock

答:ReentrantLock是Java中常用的锁,属于乐观锁类型,多线程并发情况下。能保证共享数据安全性,线程间有序性,ReentrantLock通过原子操作和阻塞实现锁原理,一般使用lock获取锁,unlock释放锁

2、ReentrantLock原理

ReentrantLock主要用到unsafe的CAS和PARK两个功能实现锁(CAS + park ),

多个线程同时操作一个数N,使用原子(CAS)操作,原子操作能保证同一时间只能被一个线程修改,而修改数N成功后,返回true,其他线程修改失败,返回false,这个原子操作可以判断线程是否拿到锁,返回true代表获取锁,返回false代表为没有拿到锁。拿到锁的线程,自然是继续执行后续逻辑代码,而没有拿到锁的线程,则调用park,将线程(自己)阻塞,而线程阻塞需要其他线程唤醒,ReentrantLock中用到了链表用于存放等待或者阻塞的线程,每次线程阻塞,先将自己的线程信息放入链表尾部,再阻塞自己;之后需要拿到锁的线程,在调用unlock 释放锁时,从链表中获取阻塞线程,调用unpark 唤醒线程

3、ReentrantLock和synchronized的区别

3.1、从底层上来讲:

synchronized 的实现涉及到锁的升级,具体为无锁、偏向锁、自旋锁、向OS申请重量级锁,ReentrantLock实现则是通过利用CAS自旋机制保证线程操作的原子性和volatile保证数据可见性以实现锁的功能

3.2、从锁的释放来讲:

synchronized 不需要用户去手动释放锁,synchronized 代码执行完后系统会自动让线程释放对锁的占用,而ReentrantLock则需要用户去手动释放锁,如果没有手动释放锁,就可能导致死锁现象。一般通过lock()和unlock()方法配合try/finally语句块来完成,免得忘记释放造成死锁

3.3、从中断上来讲:

ynchronized是不可中断类型的锁,除非加锁的代码中出现异常或正常执行完成; ReentrantLock则可以中断,可通过trylock(long timeout,TimeUnit unit)设置超时方法或者将lockInterruptibly()放到代码块中,调用interrupt方法进行中断。

3.4、从是否能实现公平锁来讲:

synchronized默认为非公平锁 ,而ReentrantLock即可以实现公平锁也可以实现非公平锁,通过构造方法new ReentrantLock时传入boolean值进行选择,为空默认false非公平锁,true为公平锁。

3.5、从能否指定唤醒线程来讲:

synchronized不能指定唤醒,而ReentrantLock通过绑定Condition结合await()/singal()方法实现线程的精确唤醒阻塞线程,而不是像synchronized通过Object类的wait()/notify()/notifyAll()方法要么随机唤醒一个线程要么唤醒全部线程。

四、线程安全的集合类的推荐

(一)、ArrayList

1.自己在线程不安全的地方使用synchronized或者reentrantLock

2.使用Collections.synchronizedList(new ArrayList)创建线程安全的ArraList

3.使用CopyOnWriteArrayList

(二)、Queue

1、ArrayBlockingQueue(基于数组实现的阻塞队列 )

2、LinkedBlockingQueue(基于链表实现的阻塞队列)

3、PriorityBlockingQueue(基于堆实现的带优先级的阻塞队列 )

4、TransferQueue(最多只包含一个元素的阻塞队列 )

(三)、哈希表

1、Hashtable类

1.1、多个线程访问同一个Hashtable会造锁冲突

1.2、关键方法都使用synchronized加锁

1.3、一旦触发扩容, 就由该线程完成整个扩容过程. 这个过程会涉及到大量的元素拷贝, 会导致该线程的执行效率变得很低.

1.4、key值不允许为null

2、ConcurrentHashMap类

2.1、读操作没有加锁,但是使用了 volatile 保证从内存读取结果, 只对写操作进行加锁.,加锁的方式是用 synchronized, 但是不是锁整个对象, 而是用每个链表的头结点作为锁对象, 这样做降低了锁冲突的概率

2.2、充分利用 CAS 特性,size 属性通过 CAS 来更新. 避免出现重量级锁的情况

2.3、扩容需要把旧数组上的全部节点转移到扩容之后的新数组上,节点的转移是从数组的最后一个索引位置开始,一个索引一个索引进行的。每个线程一轮处理有限个数的哈希桶。当旧数组上的全部节点转移到扩容之后的新数组后,ConcurrentHashMap 的 table 成员变量指向扩容之后的新数组,扩容操作完成

五、死锁

1、死锁如何产生的?

死锁是指两个或两个以上的线程在执行过程中,因争夺资源而造成的一种互相等待的现象,若无外力作用,它们会一直僵持下去,造成死等的情况。

举个例子:某计算机系统中只有一台打印机和一台输入 设备,进程P1正占用输入设备,同时又提出使用打印机的请求,但此时打印机正被进程P2 所占用,而P2在未释放打印机之前,又提出请求使用正被P1占用着的输入设备。这样两个进程相互无休止地等待下去,均无法继续执行,此时两个进程陷入死锁状态。

2、死锁产生的必要条件:

1、互斥等待:进程要求对所分配的资源(如打印机)进行排他性控制,即在一段时间内某资源仅为一个进程所占有。此时若有其他进程请求该资源,则请求进程只能等待。

2、不剥夺条件:进程所获得的资源在未使用完毕之前,不能被其他进程强行夺走,即只能由获得该资源的进程自己来释放(只能是主动释放)。

3、请求和保持条件:进程已经保持了至少一个资源,但又提出了新的资源请求,而该资源已被其他进程占有,此时请求进程被阻塞,但对自己已获得的资源保持不放。

4、循环等待条件:存在一种进程资源的循环等待链,链中每一个进程已获得的资源同时被链中下一个进程所请求。即存在一个处于等待状态的进程集合{Pl, P2, ..., pn},其中Pi等 待的资源被P(i+1)占有(i=0, 1, ..., n-1),Pn等待的资源被P0占有,看图:

Java EE(进阶版)_第10张图片这就是循环等待

代码举例:

class DeadLock implements Runnable{

    private static Object obj1 = new Object();
    private static Object obj2 = new Object();
    private boolean flag;

    public DeadLock(boolean flag){
        this.flag = flag;
    }

    @Override
    public void run(){
        System.out.println(Thread.currentThread().getName() + "运行");

        if(flag){
            synchronized(obj1){
                System.out.println(Thread.currentThread().getName() + "已经锁住obj1");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                synchronized(obj2){
                    // 执行不到这里
                    System.out.println("1秒钟后,"+Thread.currentThread().getName()
                            + "锁住obj2");
                }
            }
        }else{
            synchronized(obj2){
                System.out.println(Thread.currentThread().getName() + "已经锁住obj2");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                synchronized(obj1){
                    // 执行不到这里
                    System.out.println("1秒钟后,"+Thread.currentThread().getName()
                            + "锁住obj1");
                }
            }
        }
    }
}
public class Demo {

    public static void main(String[] args) {

        Thread t1 = new Thread(new DeadLock(true), "线程1");
        Thread t2 = new Thread(new DeadLock(false), "线程2");

        t1.start();
        t2.start();
    }
}

2、死锁的避免

1、加锁顺序

//参考案例
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

可以观察发现线程2和线程3只有在获取了锁A之后才能尝试获取锁C,换句话说获取锁A是获取锁C的必要条件。因为线程1已经拥有了锁A,所以线程2和3需要一直等到锁A被释放。然后在它们尝试对B或C加锁之前,必须成功地对A加了锁 。

2、加锁时限

给尝试获取锁的线程加一个超时时间,这也就意味着在尝试获取锁的过程中若超过了这个时限该线程则放弃对该锁请求。若一个线程没有在给定的时限内成功获得所有需要的锁,则会进行回退并释放所有已经获得的锁,然后等待一段随机的时间再重试。这段随机的等待时间让其它线程有机会尝试获取相同的这些锁。

3、死锁检测

使用jdk自带的工具jconsole去查看哪个线程造成了阻塞,在根据代码逐步分析,这里的检测方法只是我现在用到的方法,大家可以试一试,我的方法仅仅提供参考,大家可以在评论区发表自己的看法和意见。

                                  最后:祝福大家新年快乐,天天向上。

你可能感兴趣的:(JavaEE,java,开发语言,并发)