【JavaEE】多线程笔记第四天(策略锁/CAS/Synchronized原理/JUC常见类/多线程环境使用哈希表)

目录

一、常见锁策略

1、乐观锁 VS 悲观锁

2、读写锁 VS 普通的互斥锁

3、重量级锁 VS 轻量级锁

4、自旋锁 VS 挂起等待锁 

5、公平锁 VS 非公平锁 

6、可重入锁 VS 不可重入锁 

二、CAS

什么是CAS

CAS的实现与应用

CAS的ABA问题

三、Synchronized原理 

synchronized的锁策略

synchronized原理

 四、JUC常见类

ReentrantLock:

Callable接口:

信号量(Semaphore):

CountDownLatch:

 五、多线程环境使用哈希表

一、常见锁策略

1、乐观锁 VS 悲观锁

  • 乐观锁预期锁冲突的概率很低
  • 悲观锁预期锁冲突的概率很高(需要许多准备工作来预防 锁冲突)
  • 悲观锁做的工作更多,付出的成本更高,更低效
  • 乐观锁做的工作更少,付出的成本更低,更高效

synchronized 初始使用的是乐观锁策略,当发现锁竞争比较频繁的时候,就会自动切换成悲观锁策略。

2、读写锁 VS 普通的互斥锁

  • 普通的互斥锁只要两个线程针对一个对象加锁,就会产生互斥
  • 读写锁(ReentrantReadWriteLock.(ReadLock/WriteLock)类)
  1. 加读锁:如果代码只进行读操作,就加读锁
  2. 加写锁:如果代码只进行写操作,就加写锁
  3. 将读写锁分开读锁 和 读锁 之间就是不互斥的,只有 读锁 与 写锁、写锁 和 写锁 之间才互斥

3、重量级锁 VS 轻量级锁

  • 轻量级锁:做的事情更少,开销更小(一般来说,涉及到内核操作的就是重量级锁,纯用户态就是轻量级锁)
  • 重量级锁:做的事情更多,开销更大
  • 通常情况下,可以认为 悲观锁都是重量级锁;乐观锁都是轻量级锁。

4、自旋锁 VS 挂起等待锁 

  • 挂起等待锁通过内核的一些机制来实现(较重)
  • 自旋锁(Spin Lock)通过用户态代码来实现(较轻)

5、公平锁 VS 非公平锁 

  • 公平锁多个线程在等待一把锁,谁先来的谁先获得,遵守先到先得
  • 非公平锁多个线程在等待一把锁,获得锁的概率相同,不遵守先来先得
  • 此处公平指的是:先到先得

6、可重入锁 VS 不可重入锁 

  • 可重入锁一个线程针对一把锁,连续加锁两次,不会出现死锁 外部方法获取锁对象,内部再获取同一把锁时,不会真的去拿锁对象,而是将count+1,当执行完内部方法时,count--,然后执行外部方法时,count变为0,真正释放锁对象 (递归锁)
  • 不可重入锁一个线程针对一把锁,连续加锁两次,出现死锁 外部方法获取到锁对象,进入内部方法,内部方法需要等外部方法释放锁对象,然后获取到该锁对象才能执行下去,而锁对象被外部方法占据着,而外部方法需要内部执行完,才能够释放锁对象,出现死锁

二、CAS

什么是CAS

CAS:全称 Compare and swap ,字面意思就是“比较并交换”,实际CAS做的事情就是:拿着寄存器/某个内存 的值 和 另一个寄存器/内存 的值进行比较,如果值相等,就将两个值交换

一个CAS操作:

  1. 把内存中的某个值,和CPU寄存器A中的值,进行比较
  2. 如果两个值相同,就把另一个寄存器B中的值和内存的值进行交换(把内存的值放到寄存器B,同时把寄存器B的值写给内存)
  3. 返回操作是否成功

深入的的说,也就是:

  1. 比较旧的预期值A,和原数据V是否相等
  2. 如果比较相等,就需要将原数据V修改为新的值B
  3. 返回操作是否成功

需要注意的是,这组操作是通过CPU指令完成的,是原子的,线程安全的且高效

当多个线程同时对某个资源进行CAS操作,只有一个线程操作成功,但并不会阻塞其他线程,其他线程只会收到操作失败的信号(此处可将CAS看成乐观锁的一种实现方式)

 CPU提供了一个单独的CAS指令,通过这一条指令,就能完成上述操作,你们上述操作 是 原子的,线程安全的

CAS的实现与应用

  • 基于CAS实现原子类

1.Java标准库里提供了一组原子类,针对一些常用的(int/long ...)进行封装,可以基于CAS的方式进行修改值,并且是线程安全的。

 使用AtomicInterger类对象自增 相当于count++(线程安全)

AtomicInteger num = new AtomicInteger(0);
Thread t1 = new Thread(() -> {
    for (int i = 0; i < 50; i++) {
        // 相当于num++,且是原子的
        num.getAndIncrement();
    }
});
Thread t2 = new Thread(() -> {
    for (int i = 0; i < 50; i++) {
        // 相当于num++,且是原子的
        num.getAndIncrement();
    }
});
t1.start();// 启动 t1线程
t2.start();// 启动 t2线程
t1.join();
t2.join();
System.out.println(num);// 输出100


 

  •  实现自旋锁

纯用户态的轻量级锁,当发现锁被其他进程占有时,另外的线程不挂起等待,而是反复询问,看当前的锁是否被释放

自旋锁时 属于消耗CPU资源,但换来的是第一时间获取到锁,如果当时预期锁竞争不太激烈,就非常适合自旋锁

自旋锁是轻量级锁,也属于乐观锁

当锁对象被其他线程占用时,当前线程就无法获取到锁对象,一直自旋等待(忙等),直到锁对象一为null时,该线程立即就能占用该锁对象,所以自旋锁不适用于锁冲突激烈的场景。

CAS的ABA问题

  • ABA属于CAS的缺陷

  • 在CAS中进行比较时,寄存器A和内存的值相同

我们无法判断是M始终没变,还是M变了一会又变回来了

CAS在比较时,当两次比较的值相等,则认为是没有发生改变,但是实际上却不一样,具体如下:

  1. 一直是A
  2. A--->B--->A

请添加图片描述

 引入 ABA 问题: 假设在线程一第一次取完款的一瞬间,账户里收到转账50元,存款金额修改为500,线程二阻塞等待中,在线程二准备第二次取款时,朋友又转账我50,此时账户余额又变成了100
当第二次进行CAS时,余额从 100 —> 50 —> 100,而系统却确认为余额没有变过,又进行了一次扣钱,导致多扣了一次钱
解决问题: 引入一个 " 版本号 ",括号里为版本号,每次针对余额修改时, 版本号+1
100(1) —> 50(2) —> 100(3),当版本号不一致时,不扣钱

 

三、Synchronized原理 

synchronized的锁策略

synchronized是自适应锁,既是乐观锁,也是悲观锁;既是轻量级锁,也是重量级锁;

轻量级锁部分是基于自旋锁实现,重量级锁是基于挂起等待锁实现,不是读写锁;

是非公平锁,是可重入锁。

synchronized原理

synchronized的作用就是“加锁”,当两个线程针对一个锁对象加锁时,就会出现锁竞争。后面尝试加锁的线程就会阻塞等待,直到前一个线程释放锁。

锁膨胀/锁升级

对于偏向锁:

  1. 并不是真正的加锁,而是做一个标记,如果没有其他线程来竞争这个锁,就不真正的加锁,减少了加锁解锁的开销
  2. 如果有其他线程来竞争,就转为自旋锁,(轻量级锁) ,真正加锁

锁粗化

此处的粗细指的是“锁的粒度”(锁的涉及范围)

  1. 如果锁的粒度比较细,那么多线程之间的并发性更高
  2. 如果锁的粒度比较粗,那么加锁解锁的开销更小

锁消除

有些代码明明不需要加锁,你加锁了,编译器会直接把锁去掉。例如单线程中使用StringBuffer

 四、JUC常见类

ReentrantLock:

可重入锁

  • lock(): 加锁,如果获取不到锁就死等。
  • trylock(超时时间): 加锁,如果获取不到锁,等待一定的时间之后就放弃加锁
  • unlock(): 解锁
  • 和 synchronized 的区别:
  1. synchronized 是一个关键字;ReentrantLock 是一个类
  2. synchronized 不需要手动释放类; ReentrantLock 必须手动释放
  3. synchronized 如果竞争锁对象失败,就会阻塞等待; ReentrantLock 除了阻塞等待,还能trylock,失败了直接返回  
  4. synchronized是非公平锁;ReentrantLock 可以指定公平/非公平锁
  5. 基于synchronized衍生的等待机制是 wait-notify ; 基于ReentrantLock衍生的等待机制是 Condition类

Callable接口:

描述一个任务,方便返回计算结果,Runnable不方便返回结果。

基于 Callable 实现 1+2+3+...+1000 

import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;

public class 基于Callable实现 {
    //基于 Callable 实现 1+2+3+...+1000
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        Callable<Integer> callable = new Callable<Integer>() {
            @Override
            public Integer call() throws Exception {
                int sum = 0;
                for (int i = 0; i <= 1000; i++) {
                    sum += i;
                }
                return sum;
            }
        };
        //Thread 的构造方法,不能直接传callable 还需要一个中间变量
        FutureTask<Integer> futureTask = new FutureTask<>(callable);
        Thread t = new Thread(futureTask);
        t.start();;
        //get方法产生阻塞,直到call方法计算完成,get方法才会返回
        System.out.println(futureTask.get());
    }
}

信号量(Semaphore):

信号量,用来表示“可用资源的个数”,本质上就是一个计数器。“锁”就是一个二元信号量。

例如停车场:有车进去,车位-1;有车出来,车位+1.

import java.util.concurrent.Semaphore;

public class Semaphore01 {
    public static void main(String[] args) throws InterruptedException {
        //可用资源有 4 个
        Semaphore semaphore = new Semaphore(4);
        //连续 5 个申请资源,第五个申请就会阻塞,直到有人释放资源
        semaphore.acquire();
        System.out.println("申请成功");
        semaphore.acquire();
        System.out.println("申请成功");
        semaphore.acquire();
        System.out.println("申请成功");
        semaphore.acquire();
        System.out.println("申请成功");
        //阻塞 直到有其他线程释放资源
        semaphore.acquire();
        System.out.println("申请成功");

        //释放资源
        semaphore.release();

    }
}

CountDownLatch:

同时等待N个任务执行结束

就像跑步比赛中的终点线,等所有比赛选手跑完后,再公布结果

  • 构造 CountDownLatch 实例,初始化 10 表示 有 10个任务需要完成。
  • 每个线程调用 latch.countDown,表示该线程执行完了
  • 主线程中使用 latch.await(),阻塞等待所有任务执行完毕 
import java.util.concurrent.CountDownLatch;
//同时等待N个任务执行结束
public class 基于CountDownLatch模拟跑步比赛的过程 {
    public static void main(String[] args) throws InterruptedException {
        //1.创建CountDownLatch 实例(参加跑步选手有多少人)
        CountDownLatch countDownLatch = new CountDownLatch(10);

        //2.Thread 创建线程任务(for循环10次 = 10个线程 = 10个参数选手)
        for (int i = 0; i < 10; i++) {
            Thread t = new Thread(() -> {
                //currentThread()方法返回正在被执行的线程的信息。System.out.println("选手出发" + Thread.currentThread().getName());
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println("选手到达" + Thread.currentThread().getName());
                //3.在Thread线程任务中设置,线程结束的终点//countDown()设置线程结束的终点
                countDownLatch.countDown();
            });
            t.start();
        }
        //4.设置await 阻塞等待,只有当所有任务执行完后才会解锁
        countDownLatch.await();
        System.out.println("比赛结束");
    }

}

 

 五、多线程环境使用哈希表

HashMap 本身不是线程安全的,在多线程环境下使用哈希表可以使用 Hashtable、ConcurrentHashMap

  • Hashtable在关键方法上加synchronized (对this加锁,即整个Hashtable加锁),锁冲突的概率很大,效率会很低

  • ConcurrentHashMap针对每个链表的头结点加锁
  1. 减少了锁冲突,针对每个链表的头结点加锁
     
  2. 只是针对写操作加锁,读操作没加锁,只是使用了 volatile
     
  3. 更广泛的使用了CAS,进一步提高效率
     
  4. 当触发扩容操作时,同时会维护一个新的HashMap,一点点将旧的数据搬运到新的上面去,当其他线程读的时候,读旧的表,插入时,插入到新表。

  • 7
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

孙宇航_

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值