Java多线程高阶——锁的策略

1.死锁

1.1造成死锁的4个条件(缺一不可)

  1. 互斥条件
  2. 拥有请求条件
  3. 不可剥夺条件(不主动释放,其他线程得不到资源)
  4. 环路等待条件(按序请求锁)
  • 可通过修改2,4解决死锁问题

1.2死锁解决方案

按序请求锁,来破坏环路等待条件

1.3死锁排除工具

jconsole/vm/jmc

2.锁的策略

2.1乐观锁

乐观锁:它的任务一般情况下不会发生冲突,所以只有在进行数据更新的时候,才会检验并发冲突,如果没有冲突则执行修改,如果有冲突则返回失败。
常见名词:CAS(乐观锁)、ABA、JUC

2.1.1CAS(compare and swap,比较并且交换)

CAS内存中的值:
A(旧值)
B(新值)
V(内存值)

2.1.2CAS实现原理

V=A?true(没有并发冲突)-> V=B
false(并发冲突) ->自旋(将自己的A值修改成V值)
CAS底层实现原理:
Java层面CAS的实现是Unsafe类
unsafe类调用了C++的本地方法,通过调用不同操作系统的本地方法实现
在这里插入图片描述

2.1.3乐观锁的实现Atomic*

线程的解决方法:

  1. 加锁
  2. ThreadLock
  3. Atomic*(乐观锁实现)

乐观锁优点:性能比较高
CAS缺点:带来ABA问题 -> AtomicInteger是存在ABA问题的
ABA解决方案:统一版本号
使用AtomicStampedReference解决ABA问题,原理就是添加版本号,当旧版本号和新的版本号符合时,在进行操作。

2.1.4ABA问题叙述:转账(多线程并发引起的问题)

A向B转账,失误了点了两次,假设每次转账100元,在执行完第一次转账后,C给A转账100元,接着再去执行,A向B转账100元,此时使用Atomic*就会出现ABA问题。因为第一次转账时,A的旧值为100,预期新值为0;第二次转入时,A的旧值为0,预期新值为100,内存校验通过,转入成功;第三次转账时,A的旧值仍旧被识别为100(此100非彼100,一个是A本身有的,一个是C转入的),预期新值为0,内存效验通过;这就造成了ABA问题,再次少了100元。

2.2悲观锁

悲观锁:他认为通常情况下会出现并发冲突,所以在一开始就会加锁
在Java中的应用:synchronize是悲观锁(反过来说不是很准确,比如synchronize也是可重入锁)

2.3共享锁/非共享锁(独占锁)

共享锁:一把锁可以被多个线程拥有,这就叫共享锁;例如:读写锁中的读锁就是共享锁。
非共享锁:一把锁只能被一个线程拥有,synchronize
读写锁:就是把一把锁分成两个,一个用于读数据的锁,另一个叫写锁。
读写锁的具体实现:ReentrantReadWriteLock
读写锁优势:
- 锁的粒度更小,性能好
- 读锁和写锁是互斥的

2.4公平锁

锁的获取顺序必须和线程方法的先后顺序保持一致,就叫做公平锁
非公平锁:锁的获取顺序必须和线程方法的先后顺序无关,就叫做非公平锁(默认锁策略)
非公平锁优点:性能比较高
公平锁优点:执行是有序的,结果也是可以预期的
公平锁:new ReentranLock(true)
非公平锁:new ReentranLock()/new ReentranLock(false)/synchronize

2.5自旋锁

通过死循环一直尝试获取锁。例如:synchronize

2.6可重入锁

当一个线程获取锁后,可重新进入;例如:synchronize、lock
缺点:如果发生死锁,则会一直循环,所以会带来一定程度的额外开销

3.面试题:你是怎么理解乐观锁/悲观锁的,具体怎么实现

答:乐观锁-> CAS -> Atomic*,CAS是由V(内存值)A(预期旧值)B(预期新值)组成,执行的时候,使用V==A对比,true说明没有冲突,可以修改,false说明有冲突,不可修改。CAS是通过调用C++的unsafe中的本地方法(native),C++是通过调用操作系统的Atomic::cmpxchg的原子性操作指令
悲观锁 -> synchronize -> 在java中是将锁ID存在对象头来实现的(存放在偏向锁ID中,每次检查线程ID和偏向锁ID是否相同) - > JVM层面是通过监视器来实现的 - > 操作系统层面是通过mutex(互斥锁)实现的。
synchronize锁优化(锁消除):
在将端口1.6时候有锁升级的过程:
- 无锁
- 偏向锁(第一线程第一次访问,并将线程ID存放在对象头中的偏向锁ID里)
- 轻量级锁(自旋)
- 重量级锁(性能低)

4.单例模型

单例模型:整个程序的运行中,只存在一个对象。
创建方法:

  • 饿汉方式:上来直接先创建对象。优点:不用加锁也是线程安全的。缺点:程序启动之后就会创建,但是创建完成之后可能不会使用,从而浪费系统资源。
  • 懒汉方式:当程序启动之后,并不会进行初始化,什么时候调用什么时候初始化。

4.1饿汉模型

package Thread._0525;

/**
 * Created with IntelliJ IDEA.
 * Description:饿汉方式
 * User:吴博
 * Date:2021 05 25
 * Time:20:18
 */

public class Test03 {

    static class Singleton{
        //1.创建一个私有构造函数:防止其他类直接创建
        private Singleton(){
        }
        //2.定义私有变量
        private static Singleton singleton = new Singleton();
        //3.提供公共的获取实例的方法
        public static Singleton getInstance(){
            return singleton;
        }
    }

    public static void main(String[] args) {
        Singleton singleton = Singleton.getInstance();
    }
}

4.2懒汉模型

package Thread._0525;

/**
 * Created with IntelliJ IDEA.
 * Description:懒汉方式V4版本
 * User:吴博
 * Date:2021 05 25
 * Time:20:28
 */
public class Test07 {
    static class Singleton{
        // //1.创建一个私有构造函数:防止其他类直接创建
        private Singleton(){
        }
        //2.定义私有变量
        private static volatile Singleton singleton = null;
        //3.提供公共的获取实例的方法
        public static Singleton getInstance(){
            //判断是否第一次访问
            if(singleton == null){
                synchronized (Singleton.class){
                    if(singleton == null){
                        singleton = new Singleton();
                    }
                }
            }
            return singleton;
        }
    }

    static Singleton s1 = null;
    static Singleton s2 = null;

    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(new Runnable() {
            @Override
            public void run() {
                s1 = Singleton.getInstance();
            }
        });
        t1.start();
        s2 = Singleton.getInstance();
        t1.join();
        System.out.println(s1 == s2);
    }
}

4.3单例模式注意问题

非安全的单例模式——懒汉方式:
public static Singleton getInstance(){
//判断是否第一次访问
if(singleton == null){
singleton = new Singleton();
}
return singleton;
}
多线程模式下初始化singleton都为null,都会去new Singleton(),会导致地址不同,造成线程不安全。

线程不安全解决方法:

  1. 加锁
  2. ThreadLocal

有问题的地方:
方法使用synchronize加锁后,仍旧会出现问题
singleton = new Singleton();处出现问题(原因是编译器优化,会进行指令重排序)

  1. 先在内存中开辟空间
  2. 初始化
  3. 将变量指向内存区域

指令重排序:123改为132
第一次进入后,加锁,new出对象未初始化,所以地址更改了,但不为null了,第二次进入判断后,因为不会null,所以直接return空对象,最后进行比较时,为false

最终版
为了解决指令重排序问题,需要将singleton对象加volatile关键字,并且使用双重校验DCL的方式来检测对象是否为空。

5.自定义阻塞队列

生产者消费者模型 —— 生产者生产数据,消费者消费生产者生产的数据。
当数据量已满的情况下,不要尝试给队列添加数据了,而是阻塞等待。使用wait()/notify Locksupport park/unpark。
当任务队列为空,进行阻塞等待。

package Thread._0525;

import java.util.Random;

/**
 * Created with IntelliJ IDEA.
 * Description:自定义阻塞队列
 * User:吴博
 * Date:2021 05 25
 * Time:21:16
 */
public class Test08 {

    static class MyBlockQueue{
        //实际存储数据的数组
        private int[] values;
        //队首
        private int first;
        //队尾
        private int last;
        //队列元素长度
        private int size;
        //初始化
        public MyBlockQueue(int initial){
            values = new int[initial];
            first = last = size = 0;
        }
        //添加
        public void offer(int num){
            synchronized (this){
                //判断边界值
                if(size == values.length){
                    //队列满
                    try {
                        this.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                values[last] = num;
                last++;
                size++;
                //判断是否为最后一个元素,循环队列
                if(last == values.length){
                    last = 0;
                }
                //尝试唤醒消费zhe
                this.notify();
            }
        }
        //查询方法
        public int poll(){
            int ret = -1;
            synchronized (this){
                if(size == 0){
                    //队列为空
                    try {
                        this.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                //取元素
                ret = values[first];
                first++;
                size--;
                //判断是否为最后一个元素
                if(first == values.length){
                    first = 0;
                }
                //尝试唤醒生产者
                this.notify();
            }
            return ret;
        }
    }

    public static void main(String[] args) {

        MyBlockQueue myBlockQueue = new MyBlockQueue(100);

        Thread t1 = new Thread(new Runnable() {
            @Override
            public void run() {
                while (true){
                    int num = new Random().nextInt(10);
                    System.out.println("生产了随机数" + num);
                    myBlockQueue.offer(num);
                    try {
                        Thread.sleep(500);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        });
        t1.start();

        Thread t2 = new Thread(new Runnable() {
            @Override
            public void run() {
                while (true){
                    int ret = myBlockQueue.poll();
                    System.out.println("消费了数据" + ret);
                }
            }
        });
        t2.start();
    }
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值