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

文章详细介绍了Java中各种锁的概念和区别,包括乐观锁与悲观锁、轻量级锁与重量级锁、自旋锁与挂起等待锁、互斥锁与读写锁、公平锁与非公平锁、可重入锁与不可重入锁。此外,还讨论了synchronized的原理和优化机制,以及JUC(Java并发包)中的相关类如ReentrantLock、Semaphore、CountDownLatch等,并提到了死锁的概念和解决方案。
摘要由CSDN通过智能技术生成

目录

锁策略

乐观锁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非公平锁

设想一个场景,如果多个线程A,B,C获取锁。若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

step2:locker与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)。当形成死锁时,按照标号由小到大的顺序依次获取锁,这样就可以解决循环等待。


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

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值