多线程【进阶版】(中)

简介: 多线程【进阶版】

二. CAS


1. 概念


全称Compare and swap,比较 内存A 和 寄存器R 中的数据值,如果数值相同,就把 内存B和 寄存器R 的数值进行交换;


2. 特点


CAS最大的特点就是一个CAS操作是单条CPU指令,它是原子的,所以既是CAS不加锁,也能保证线程的安全;


在JDK1.8中针对于CAS提供了一个类:

74ab9191ab844b89a5a9e87aa7cdaef2.png

CAS中的ABA问题:


ABA问题是CAS中的面试的高频问题,我们都知道,CAS是对比内存和寄存器的值,看看是否相同,就是通过这个对比,来检测内存是不是改变过,要么相同,要么不同,不同都好区别,但是有一种相同不是真正意义上的相同,而是不确定这个值中间是否发生过改变,改变了原来的东西,但是变回到原来的状态,假设原来是a,然后变成b,后来又变成a,这就是 a->b->a 问题了;


如何解决ABA问题呢???


ABA问题实质上就是一个反复横跳的问题,我们只要约定数据只能单方面变化,要么数据只能增加,要么数据只能减小,那么问题就迎刃而解了;


如果我们要求数据既能增大又能减小,我们可以约定一个版本号变量,约定版本号只能增加,并且每次修改,都会增加一个版本号,这样我们每次对比的时候就是拿版本号去对比,而不是对比数值本身,这样也能很好的解决问题了;


三. Synchronized 原理


3.1 基本特点

1. 开始时是乐观锁, 如果锁冲突频繁, 就转换为悲观锁.

2. 开始是轻量级锁实现, 如果锁被持有的时间较长, 就转换成重量级锁.

3. 实现轻量级锁的时候大概率用到的自旋锁策略

4. 是一种不公平锁

5. 是一种可重入锁

6. 不是读写锁


3.2 加锁步骤

de6ac486613b4125a0c19ed6fe66c1fb.png

过程:


  1. 最开始是没有锁的;
  2. 刚开始加锁,是一个偏向锁状态;
  3. 遇到锁竞争,就转化为轻量级锁状态(自旋锁);
  4. 竞争激烈时,就会变成重量级锁,这一过程交给内核阻塞等待;

关于偏向锁:


偏向锁并非是真的加锁,只是简单的标记一下,想占有这把锁,如果没有锁竞争,那就不加锁,如果有别的线程来竞争这把锁,那么就升级成轻量级锁,这样做既保证了效率,又保证了线程安全;


3.3 锁消除

锁消除是编译阶段做的一种优化手段,用来检测当前代码是否多线程任务 或者 是否有必要加锁,如果没有必要,又把锁给写了,就会在编译过程中自动把锁去掉;


假如在不涉及线程安全的问题时,我们用 synchronized 关键字对一个操作进行加锁了,那么在编译阶段,就会自动把锁进行一个消除,锁消除这个机制是一个智能化的操作,它会根据不同的代码,去判断当前的操作需不需要进行加锁,如果不需要,就会自动消除;


3.4 锁粗化

提到锁的粗化,就要先提到一个概念叫锁的粒度,锁的粒度就是 synchronized 代码块里包含代码的多少,一般认为代码越多,粒度越粗,代码越少,粒度越细;


通常写代码的情况下,我们是希望锁的粒度更小一点,因为这样串行的代码少,并发执行的代码就越多;


举个例子:


假设我们给领导打电话汇报3份工作,分两种情况:


1. 先打个电话,汇报 A 的进展,再挂电话


   再打个电话,汇报 B 的进展,再挂电话


   再打个电话,汇报 C 的进展,再挂电话


每次领导接电话就是一个加锁的过程,别人(其他线程)想要给领导打电话就是处于一个阻塞等待的状态,挂电话就是释放锁;当你挂断电话后,再想去汇报工作B的进展,你不一定能打进去,领导可能和别人正在通话,这样一来,你再想打进去就要阻塞等待一会,这个过程就相当于把锁的粒度拆分的更细了,但是每次都可能会阻塞等待,这样效率并不高,还可能并发其他的问题;


2. 打通一次电话,直接把A,B,C的工作进展一次性想领导汇报;


这样就避免了阻塞等待的消耗了,也大大的提升了效率;


3.5 JUC常见组件

JUC是 Java.util.concurrent 的缩写,这里是多线程并发的一个类;


1. Callable接口

2a1b79f05c134474b6afcbeda8f5081d.png

这里写一个程序去实现一下这个接口:


public class Demo16 {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        // 创建任务,计算从 1 加到 100 的和
        Callable<Integer> callable = new Callable<Integer>() {
            @Override
            public Integer call() throws Exception {
                int sum = 0;
                for (int i = 0; i < 100; i++) {
                    sum += i;
                }
                return sum;
            }
        };
        // 创建一个线程来完成这个任务
        // Thread 不能直接传 callable, 外面需要再包装一层
        FutureTask<Integer> futureTask = new FutureTask<>(callable);
        Thread thread = new Thread();
        thread.start();
        System.out.println(futureTask.get());
    }
}

上述代码也是线程创建的一种方法;


2. ReentrantLock类


可重入互斥锁 和 synchronized 定位类似,,都是用来实现互斥效果,,保证线程安全;


但是 synchronzied 是基于代码块实现来控制加锁和解锁,而 ReentrantLock 是提供了 lock 和unlock  的方法来进行加锁和解锁;

f419d8b1a4924829b3e1af7e9b743a20.png

虽然 synchronized 已经在绝大多数情况下满足使用了,但是 ReentrantLock 也有自己特殊的方法:


  1. 使用 synchronized 加锁的时候如果发现锁被占有,会进入阻塞状态,而 ReentrantLock 提供了一个 tryLock 方法,如果发现锁被占用,不会阻塞,直接返回 false;
  2. synchronized 是一个非公平锁(不遵循规则),而 ReentrantLock 提供了公平和非公平两种工作模式;
  3. synchronized 搭配 wait 和 notify 进行唤醒等待,如果多个线程 wait 同一个对象,notify 的时候随机唤醒一个,而 ReentrantLock 搭配 Condition 这个类进行唤醒等待,并且它能指定一个线程唤醒;
目录
相关文章
|
Linux API C++
|
关系型数据库 MySQL 编译器
C++进阶 多线程相关(下)
C++进阶 多线程相关(下)
61 0
|
安全 Java 调度
多线程【进阶版】(下)
多线程【进阶版】
65 0
|
2月前
|
存储 安全 Java
多线程进阶
本文介绍了多种锁策略及其应用。首先区分了乐观锁与悲观锁:乐观锁假定冲突较少,悲观锁则预期频繁冲突。接着讨论了自旋锁与挂起等待锁,前者适合冲突少且持有时间短的场景,后者适用于长锁持有时间。随后对比了轻量级锁与重量级锁,前者开销小、效率高,后者开销大、效率低。此外,文章还探讨了公平锁与非公平锁的区别,以及可重入锁如何避免死锁。最后介绍了读写锁,其允许多个读操作并发,但写操作独占资源。通过详细解析各种锁机制的特点及适用场景,本文为读者提供了深入理解并发控制的基础。
43 15
多线程进阶
|
6月前
|
安全 调度
多线程入门
多线程入门
133 1
|
6月前
|
安全 算法 Java
多线程知识点总结
多线程知识点总结
66 3
|
调度
多线程:笔记
多线程:笔记
62 0
|
算法 Ubuntu C++
[总结] C++ 知识点 《四》多线程相关
[总结] C++ 知识点 《四》多线程相关
|
设计模式 Java 数据处理
【Java并发编程系列8】多线程实战
Java多线程的学习,也有大半个月了,从开始学习Java多线程时,就给自己定了一个小目标,希望能写一个多线程的Demo,今天主要是兑现这个小目标。
814 0