【JavaEE】锁策略 + synchronized原理 + CAS + JUC下常用类和接口 + 死锁

阿里云国内75折 回扣 微信号:monov8
阿里云国际,腾讯云国际,低至75折。AWS 93折 免费开户实名账号 代冲值 优惠多多 微信号:monov8 飞机:@monov6

目录

锁策略

乐观锁VS悲观锁

轻量级锁VS重量级锁

自旋锁VS挂起等待锁

互斥锁VS读写锁

公平锁VS非公平锁

可重入锁VS不可重入锁

synchronized原理

synchronized特性

synchronized优化机制

加锁过程优化

锁消除

锁粗化

CAS

CAS概念

CAS原理

CAS应用

自旋锁的实现

原子类

CAS中的ABA问题

JUC常用类和接口

线程池常用类和接口

原子类

ReentrantLock类

构造方法

实例方法

Semaphore类

构造方法

实例方法

CountDownLatch类

构造方法

实例方法 

Callable接口

死锁

死锁是什么

死锁产生的原因

解决死锁


锁策略

锁策略不仅在Java的多进程中需要考虑大多数情况下需要考虑锁的情况都有可能涉及到锁策略。


乐观锁VS悲观锁

这是两种类型的锁都是预测锁冲突的大小角度而考虑的。

乐观锁预测多个线程访问同一变量的概率比较小基本不会发生锁冲突。所以每次访问共享变量的时候直接尝试获取变量同时识别当前的数据是否出现了访问冲突乐观锁的实现可以引入一个版本号借助版本号来识别出数据是否发生冲突。

悲观锁预测多个线程访问同一变量的概率比较大很有可能发生锁冲突。所以每次访问共享变量的时候都要先加锁。悲观锁的实现就要先加锁获取到锁再进行操作没有获取到就等待。


轻量级锁VS重量级锁

轻量级锁对于加锁的开销比较小。加锁机制不依赖操作系统提供的mutex锁。仅有少量的内核用户态切换也不容易引发线程调度。

重量级锁对于加锁的开销比较大。加锁机制很依赖操作系统提供的mutex锁。有大量的内核用户态切换容易引发线程调度。

加锁本质


自旋锁VS挂起等待锁

自旋锁获取锁时如果获取失败并不会放弃CPU进行阻塞而是立刻又获取锁。往复循环知道获取到锁为止。

        优点①如果锁释放比较快那么这样就可以在第一时间获取到锁。

                   ②它这种方法由于没有放弃CPU不涉及到线程阻塞和调度所以这是轻量级锁。

         缺点如果锁迟迟不释放那么这样就会一直消耗CPU资源。

挂起等待锁获取锁时如果获取失败直接放弃CPU然后阻塞等待。如果被唤醒了才会进行加锁。

        优点不占用CPU资源。

        缺点①无法第一时间获取到锁。

                   ②由于涉及到线程的阻塞和调度所以这是重量级锁。


互斥锁VS读写锁

互斥锁如果一个线程获取到锁了如果还有想获取该锁就只能进行阻塞等待。不管是这两个的操作是读还是写。

读写锁读写锁提供三种操作①对读操作加锁  ②对写操作加锁  ③解锁

                读锁和读锁之间没有互斥

                读锁和写锁之间存在互斥

                写锁和写锁之间存在互斥


公平锁VS非公平锁

设想一个场景如果多个线程ABC获取锁。若A线程获取锁成功。随后B线程也想获取锁失败然后阻塞等待C线程最后也想获取失败然后阻塞等待。过了一段时间A线程释放了锁

公平锁因为释放前B线程比C线程先要获取锁所以B线程获取到锁。按照先来后到的顺序。

非公平锁B、C线程一起竞争锁都有可能获取到锁。随机分配给都想要获取锁的线程。


可重入锁VS不可重入锁

可重入锁一个线程可以多次获取一把锁而且不会出现死锁的情况。在Java中Reentant开头命名的锁都是可重入锁JDK提供的所有所有现成的Lock实现类也都是可重入的。

不可重入锁一个线程可以多次获取一把锁但是出现死锁的情况。


synchronized原理

synchronized特性

根据上面的锁策略我们可以简单分析以下synchronized所具有的特性。

1. 刚开始是乐观锁如果锁冲突的概率变大之后就会升级成悲观锁。

2. 刚开始是轻量级锁如果锁迟迟不释放就会升级成重量级锁。

3. synchronized的轻量级锁很有可能是自旋锁。

4. 它是互斥锁。

5. 它是非公平锁。

6. 它是可重入锁。


synchronized优化机制

synchronized内部的优化机制需要有大概的认识。


加锁过程优化

加锁过程如下图

加锁不是一下子就到重量级锁秉持着能不加就不加能加轻的就加轻的原则来加锁的。


锁消除

编译器和JVM会自动判断锁是否可以消除如果可以消除就不进行加锁。

比如在单线程环境下使用一些自身带有synchronized的类比如StringBuffer。这样加锁和解锁是没有必要的只会浪费资源。


锁粗化

锁的粒度有粗细之分。

粗表示这个锁的范围比较大细则相反。

一般情况下锁越细越好这样释放锁的时候别的线程也可以获取到锁。

不过编译器和JVM会判断如果没有其他线程来抢占该锁就会自动把锁粗化。这样就不用频繁的加锁和解锁了。


CAS

CAS概念

CAS全称Compare And Swap  比较和交换  一个CAS涉及到以下的操作。这些操作都是原子性的相当于CPU当中的一条指令。


CAS原理

操作系统不同JVM对于CAS实现原理有所不同。但是基本上都是按照以下思路来的

Java的CAS利用的是unsafe这个类提供的操作。

unsafe这个类是靠JVM针对不同的操作系统实现的Atomic::cmpxchg。

Atomic::cmpxchg的实现是靠汇编的CAS的操作 + CPU硬件提供的lock机制保证了原子性。
归根结底还是靠硬件提供支持的。

CAS应用

CAS相较于synchronized的使用并不是那么的广泛。主要有自旋锁的实现和原子类。


自旋锁的实现

可以简单的理解为以下两步

step1自旋锁中有一个Thread locker引用——目的为了指向加锁的线程。初始值为Null

step2locker与Null比较如果相等(意味着这个锁没有被其他线程持有)就把当前线程的引用赋              值给locker如果不相等(意味着这个锁已被其他线程持有)就继续尝试直到成功或者变              成挂起等待锁。

原子类

这个类在java.util.concurrent.atomic中。它是为单个变量提供线程安全的编程。 

如下代码实例

import java.util.concurrent.atomic.AtomicInteger;

public class ThreadDemo30 {

    public static void main(String[] args) throws InterruptedException {

        AtomicInteger integer = new AtomicInteger(0);

        Thread t1 = new Thread(() -> {
            for (int i = 0; i < 1000; i++) {
                // 这个方法相当于自增
                integer.getAndIncrement();
            }
        });
        Thread t2 = new Thread(() -> {
            for (int i = 0; i < 1000; i++) {
                integer.getAndIncrement();
            }
        });
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        System.out.println(integer);
    }

}

 

 CAS在其中的作用就用这个自增方法举例。

在这个自增方法中其中初始的 V = 0(也就是我们设定的0)

想要自增时先要记录这个 V的值用 A 保存(有可能此时V就被其他线程修改了)。

自增的值为 B = V + 1;

要想把B赋值给V先要检查 A == V 就把B赋值给V(相等说明未被修改) : 就啥也不干(不相等说明V被修改了)            这一步是原子的不怕有线程安全问题


CAS中的ABA问题

ABA一个变量本来值为A然后变成了B最后又变成A的意思。

在CAS中当一个线程想要把B赋值给V时想要用A与V比较。

A这个变量是共享变量。有可能另外一个线程在比较之前把A变了然后又变回去了。

一般情况下这也不会出bug但是不排除极端情况。

解决方法是引入版本号。比如给A加个版本那么这时则是比较的A的版本号有无变化。

在Java中用AtomicStampedReference<V>来实现带有版本的功能。


JUC常用类和接口

JUC是java.util.concurrent的简称这个包下面大多是和多线程有关的类和接口。


线程池常用类和接口

已在这篇文章中详细介绍了。

【JavaEE】线程池_p_fly的博客-CSDN博客

原子类

在上述CAS问题中也介绍过了。


ReentrantLock类

这个类在java.util.concurrent.locks包中。

这个类的功能与synchronized关键字的功能很像。都是可重入互斥锁用来保证线程安全的。

构造方法

    // 默认构造方法创建的是非公平锁
    ReentrantLock lock1 = new ReentrantLock();
    // 带有boolean参数的构建方法可以设置是否为公平锁
    // true -- 公平    false -- 非公平
    ReentrantLock lock2 = new ReentrantLock(true);

实例方法

这里只介绍常用的实例方法

import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.ReentrantLock;

public class ThreadDemo31 {

    public static void main(String[] args) {
        ReentrantLock lock1 = new ReentrantLock();
        // 第一种加锁方式
        // 获取不到锁就一直等着
        lock1.lock();

        // 第二种加锁方式
        // 到了设定时间获取不到锁就放弃加锁
        try {
            lock1.tryLock(10, TimeUnit.MINUTES);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        
        // 解锁  建议放到finally中
        // 因为有可能加完锁后执行不到解锁这个操作
        finally {
            lock1.unlock();
        }
    }

}

synchronized唤醒是通过Object类中的wait和notify来随机唤醒线程的

而reentrantlock唤醒是通过Condition类来指定唤醒某个线程的。 


Semaphore类

这个类是把信号量封装起来了。信号量简单理解为可用资源的个数本质上是一个计数器。

信号量最重要的操作就是申请资源(P操作)和释放资源(V操作)。

可以设想一下火车票一趟火车的总票数就是可用资源。当有人买一张那么资源就少一个(P)当有人退票那么资源就会多一个(V)。如果总票数剩余为0要么候补有人退票要么放弃这趟火车另寻它车。

如果资源只有1个的时候它就变成了锁。拥有了锁也就把资源变成0了(资源不能为负数)释放了锁资源又变成1。

构造方法

        // 创建出有4个资源的信号量。获取资源是随机的
        Semaphore semaphore1 = new Semaphore(4);
        
        // 创建出10个资源的信号量。
        // true 尽可能是公平获取资源——先release的先获取(不是一定)
        // false 随机获取与第一种构造方法一样
        Semaphore semaphore2 = new Semaphore(10, true);

实例方法

这里只介绍P操作和V操作的方法。这些方法都是原子性的所以在多线程环境下可以使用。

                try {
                    // acquire() 表示获取资源
                    // 同时也要有异常  因为有可能没有资源这样就得阻塞等待
                    // 只要有阻塞一般都要有InterruptedException这个异常
                    semaphore1.acquire();
                    System.out.println("获取到一个资源");
                    // release() 表示释放资源
                    semaphore1.release();
                    System.out.println("释放一个资源");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }

使用Semaphore来自增一个变量使用两个线程自增(保证线程安全)。

import java.util.concurrent.Semaphore;

class SemaphoreLock {
    private int n = 0;
    public Semaphore semaphore = new Semaphore(1);

    public void add (){
        n++;
    }

    public int getN() {
        return n;
    }

}

public class ThreadDemo33 {

    public static void main(String[] args) throws InterruptedException {

        SemaphoreLock lock = new SemaphoreLock();

        Thread t1 = new Thread(() -> {
            for (int i = 0; i < 100; i++) {
                try {
                    // 获取到资源后才能自增
                    lock.semaphore.acquire();
                    lock.add();
                    // 自增一次后就释放资源
                    lock.semaphore.release();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });
        Thread t2 = new Thread(() -> {
            for (int i = 0; i < 100; i++) {
                try {
                    lock.semaphore.acquire();
                    lock.add();
                    lock.semaphore.release();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });
        t1.start();
        t2.start();
        t1.join();
        t2.join();

        System.out.println(lock.getN());
    }

}

  


CountDownLatch类

通过下面的代码来理解这个类具体有什么作用。

构造方法

        // 创建三个定数器
        // 不能为负数否则会抛出异常
        CountDownLatch count = new CountDownLatch(3);

实例方法 

import java.util.concurrent.CountDownLatch;
//import java.util.concurrent.TimeUnit;

public class ThreadDemo34 {

    public static void main(String[] args) throws InterruptedException {
        // 创建三个定数器(相当于三个选手)
        CountDownLatch count = new CountDownLatch(3);

        // t线程是跑步比赛的裁判
        Thread t = new Thread(() -> {
            try {
                // 这个方法会让 t 线程阻塞
                // 除非计数器被消耗光了才会解除阻塞
                count.await();

                // 这是另外一个会让 t 线程阻塞的方法
                // 有了时间的控制如果超过设定的时间还在阻塞就会解除阻塞
                // count.await(100, TimeUnit.MINUTES);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("跑步比赛已完成");
        });
        // t1/2/3线程是三位选手
        Thread t1 = new Thread(() -> {
            // 调用该方法计数器会减一
            count.countDown();
            System.out.println("t1比赛已完成");
        });
        Thread t2 = new Thread(() -> {
            count.countDown();
            System.out.println("t2比赛已完成");
        });
        Thread t3 = new Thread(() -> {
            count.countDown();
            System.out.println("t3比赛已完成");
        });
        t.start();
        //t.join();
        t1.start();
        t2.start();
        t3.start();
    }

}

  


Callable接口

这个接口和Runnable接口很像二者都是用来描述一个任务的。

不同之处①该接口可以返回一个结果不返回则会抛出异常。

                  ②该接口实现的类不能直接放到Thread类的构造方法需要包装一下。

import java.util.concurrent.*;

public class ThreadDemo35 {

    public static void main(String[] args) {

        // 在t线程下计算1 + 1只要结果

        // 由于这个任务需要返回结果所用用Callable接口描述任务
        // 由于结果的整数所以用Integer类作为泛型参数
        Callable<Integer> callable = new Callable<Integer>() {
            @Override
            public Integer call() throws Exception {
                return 1 + 1;
            }
        };

        // 任务不能直接放到线程里工作
        // Thread t = new Thread(callable) 这样是不行的

        // 需要使用FutureTask包装以下
        // 这个类实现了Runnable接口所以可以放到Thread的构造方法中
        FutureTask<Integer> task = new FutureTask<>(callable);
        Thread t = new Thread(task);
        t.start();
        try {
            // get()方法是拿到结果
            System.out.println(task.get());
            // 结果不可能立刻出来(1+1还是可以立刻出来的)在没出来之前要进行阻塞等待
            // 阻塞就要有InterruptedException异常
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        } catch (ExecutionException e) {
            throw new RuntimeException(e);
        }
    }

}

  


死锁

死锁是什么

死锁是多个线程同时被阻塞它们中一个或多个线程都在等待锁释放导致了僵持的场面程序陷入死循环的局面。

死锁产生的原因

1. 互斥使用。当资源被一个线程使用时其他线程无法使用。

2. 不可抢占。资源请求者不能强已被获取的资源只能等拥有者主动释放。

3. 资源保持。当资源拥有者再去请求其他资源时原本持有的资源也不能放弃。

4. 循环等待。比如A等B释放B等C释放C等A释放资源这样就形成了循环。

当这四个条件都成立的时候才会形成死锁。

解决死锁

上述任何一个条件不成立就可以解决死锁问题。

其中最容易破坏的是 循环等待。通过最常用的死锁阻止技术锁排序

假设有N个线程获取M把锁把M把锁进行编号(1、2......M)。当形成死锁时按照标号由小到大的顺序依次获取锁这样就可以解决循环等待。


有什么错误评论区指出。希望可以帮到你。

阿里云国内75折 回扣 微信号:monov8
阿里云国际,腾讯云国际,低至75折。AWS 93折 免费开户实名账号 代冲值 优惠多多 微信号:monov8 飞机:@monov6
标签: Java