小白架构师成长之路16-并发编程JMM&Lock&Tools详解

并发编程JMM&Lock&Tools详解

大家里面的案例可以从gitHub下载下来自己看一下
地址:https://github.com/JolyouLu/Juc-study.git 代码在Juc-JMMLockTools下

CPU如何工作

在了解锁之前我们先需要知道CPU是如何工作的,为什么我们使用多线程时会出现不同步的问题

在这里插入图片描述

CPU是如何工作的,CPU到我们的硬盘粗略讲是需要经过 一级二级三级缓存=>内存=>硬盘,为什么CPU不能直接对硬盘IO操作呢,首先我们先了解一下CPU,缓存,内存,硬盘运算速度

  1. CPU的运算速度一般是GHz,GHz=1000MHz,像2.0,就表示CPU的运算速度是2000000000次/秒(速度极快)

  2. 缓存是才CPU内部的相当于一条流水线,CPU只管高速生产,直接交给缓存,再由缓存交给内存(速度仅此CPU)

    参考一篇知乎解释缓存:https://www.zhihu.com/question/22431522

  3. 内存,一般内存是2000-3000MHz这里就可以看出CPU与内存的差距了把,相差最少10倍,所以需要经过缓存再到内存(速度低CPU10倍)

  4. 硬盘,就拿比较好的固态硬盘来说速度一般是3,200 MB/s,CPU到硬盘那就更加不用说了,差距很大(慢)

经过以上的了解,我们大致解了CPU,缓存,内存,硬盘的速度后我们会发现CPU和内存的运算速度有极大差距,频率不匹配这样就知道为什么CPU到硬盘需要经过,那么多步骤了把,但是多核中间存在一个问题了,如核心1从内存拿到了一个int=0运算后+1,但是核心2又去内存取但是这时int=1还在缓存中,然后核心2拿出来又是0他又+1,你会发现这中间存在一个很大的问题整个运算过程中int不能保证一致,那我核心1做操作核心2又做操作,这样子我们的程序就会有很大的问题

解决方案

那怎么解决这个问题呢,CPU厂家intel和amd提出协议:总线锁、缓存一致性的解决方案

缓存一致性

MESI协议缓存状态

状态 描述 监听任务
M修改(Modified) 该 Cache line 有效,数据被修改了,和主内存中的数据不一致,数据只存在于本 Cache 中 缓存行必须时刻监听所有试图读该缓存行相对就主存的操作,这种操作必须在缓存将该缓存行写回主存并将状态变成 S(共享)状态之前被延迟执行
E独享、互斥(Exclusive) 该 Cache line 有效,数据和内存中的数据一致,数据只存在于本Cache 中 缓存行也必须监听其它缓存读主存中该缓存行的操作,一旦有这种操作,该缓存行需要变成 S(共享)状态
S共享(Shared) 该 Cache line 有效,数据和内存中的数据一致,数据存在于很多Cache 中 缓存行也必须监听其它缓存使该缓存行无效或者独享该缓存行的请求,并将该缓存行变成无效(Invalid)。
I无效(Invalid) 该 Cache line 无效

Java 内存模型(JMM)

java虚拟机内存模型其实他是一个虚拟的概念,因为不同的操作系统他们去调用CPU的运算过程是不一样的,为了解决不同操作系统的他们底层的工作原理不一致,为了在不同的操作系统上都能运行JVM就设计出了一套java内存模型规范。

Heap(堆)

java 里的堆是一个运行时的数据区,堆是由垃圾回收来负责的, 堆的优势是可以动态的分配内存大小,生存期也不必事先告诉编译器, 因为他是在运行时动态分配内存的,java 的垃圾回收器会定时收走不用的数据, 缺点是由于要在运行时动态分配,所有存取速度可能会慢一些

Stack(栈)

栈的优势是存取速度比堆要快,仅次于计算机里的寄存器,栈的数据是可以共享的, 缺点是存在栈中的数据的大小与生存期必须是确定的,缺乏一些灵活性 栈中主要存放一些基本类型的变量,比如 int,short,long,byte,double,float,boolean,char, 对象句柄

在这里插入图片描述

我们的方法都会放到栈里面,每一个栈都会对应一个对象,假如我们在Object1调用了Object2的方法我们的Object里面就会有一个Object2的副本

什么是副本

在这里插入图片描述

加入我现在有2个线程,thread1和thread2,主内存有一个int=0,那thread1和thread2需要运算他们会先从主内存中拷贝一个副本int=0到工作内存中,对工作内存的int运算完毕后会覆盖内存的int

为什么提出java内存模型

java内存模型提出是保证我们java并发编程的3个概念

原子性

即一个操作或者多个操作 要么全部执行并且执行的过程不会被任何因素打断,要么就都不执行,比如我们的数据库原子性就是基于我们数据库事务实现的

可见性

可见性是指当多个线程访问同一个变量时,一个线程修改了这个变量的值也就是上面所说的工作内存中的副本,其他线程能够立即看得到修改的值

有序性

即程序执行的顺序按照代码的先后顺序执行,程序顺序和我们的编译运行的执行不一定是一样,因为CPU会做编译优化和指令重排提高运行速度,这时可能就会出现我编译后和我们写的顺序不一致,CPU做编译优化会遵循一些原则保证程序优化后结果不会出错如:shiHappens-before: 传递原则:lock unlock A>B>C A>C的原则

java内存模型的同步过程

在这里插入图片描述

java内存模型的同步分为8个步骤,加锁=>读取主内存数据=>加载到工作内存=>运行=>赋值=>存储回工作内存=>写入主内存=>释放锁。

Volatile

关键字 volatile 可以说是 Java 虚拟机提供的最轻量级的同步机制,当一个变量定义为 volatile,它具有内存 可见性以及禁止指令重排序两大特性,为了更好地了解 volatile 关键字,我们可以先执行如下代码

public class MyVolatile {
    //加上与去处volatile分别运行程序查看结果
    volatile boolean stop = false;

    public void shutDown(){
        stop = true;
    }

    public void doWork(){
        System.out.println("机器工作了");
        while (!stop){
        }
        System.out.println("机器停止了");
    }

    public static void main(String[] args) throws InterruptedException {
        MyVolatile myVolatile = new MyVolatile();
        new Thread(()->{
            myVolatile.doWork();
        }).start();
        //休息1秒 保证doWork先启动
        Thread.sleep(1000);
        new Thread(()->{
            myVolatile.shutDown();
        }).start();
    }
}

最后我们发现如果我们把volatile去处这个程序永远也不会停止,因为第二个Thread执行的shutDown()只是修改了他自己工作内存中的为true,第一个Thread自己的工作内存还是false所以第一个Thread永远也不会停止,当我们加上volatile后stop这个值就具有内存可见性,如果有一个线程修改了他另外一个线程也能看到

介绍

  1. 保证了不同线程对这个变量进行操作时的可见性,即一个线程修改了某个变量的值,这新值对 其他线程来说是立即可见的,可见性的意思是当一个线程修改一个共享变量时,另外一个线程能读到这个修改的值
  2. 禁止进行指令重排序,程序执行的顺序按照代码的先后顺序执行
  3. Volatile 不能保证复合原子性比如 比如: i++

原理

volatile 变量进行写操作时,JVM 会向处理器发送一条 Lock 前缀的指令,将这个变量所在缓存行的数据写会
到系统内存
Lock 前缀指令实际上相当于一个内存屏障(也成内存栅栏),它确保指令重排序时不会把其后面的指令排
到内存屏障之前的位置,也不会把前面的指令排到内存屏障的后面;即在执行到内存屏障这句指令时,在
它前面的操作已经全部完成

什么是原则性操作

volatile 只能保证单个原则性操作如 int i=1;int j=i;这些直接写入都是单个原子性操作,因为他们只用运行一次就可以改变值,那什么是复合原子性操作呢比如i++;i++他需要执行如下步骤获取i值 修改i值 写入i值,可能在你获取修改的时候已经有程序同时进入了,所以就不能保证复合原子性

为什么不能保证原子性

执行如下测试代码

public class VolatileAtoTest implements Runnable {
    //原子性测试
    static volatile int i =1;
    @Override
    public void run() {
        /***
         * i++; 操作并非为原子性操作。
         什么是原子性操作?简单来说就是一个操作不能再分解。i++ 操作实际上分为 3 步:
         读取 i 变量的值。
         增加 i 变量的值。
         把新的值写到内存中。
         */
        System.out.println(Thread.currentThread().getName() + ": 当前i值: " + i + ", ++后i值: "
                + (++i));
    }
    public static void main(String[] args) {
        Thread t1 = new Thread(new VolatileAtoTest(), "A");
        Thread t2 = new Thread(new VolatileAtoTest(), "B");
        Thread t3 = new Thread(new VolatileAtoTest(), "C");
        Thread t4 = new Thread(new VolatileAtoTest(), "D");
        t1.start();
        t2.start();
        t3.start();
        t4.start();
    }
}

我们会发现有时候的结果并不是按顺序来的,因为有可能在我们改变值i需要回写到内存在我回写我最新的值之前已经有几个线程同时进入并且做加法操作,所以我们会发现有的线程拿到的还是之前的值

Synchronized

介绍

Synchronized是一个 重量级锁、重入锁、jvm 级别锁 ,他可以保证复合原子性,在方法他是使用:ACC_SYNCHRONIZED修饰方法,代码块:是在代码块前后加上 monitorenter\monitorexit

原理

public class MySynchronized {
    public static void main(String[] args) {
        //使用方法1 对象锁
        synchronized (MySynchronized.class){
        }
        //调用代码块
        m();
    }
    //使用方法2 定义静态代码块
    public static synchronized void m(){
    }
}

在这里插入图片描述

sysnchronized底层是使用了一个JVM监听器,监听到一个线程后会别别的线程全部放到同步队列中先,执行监听的那个线程,但监听的线程执行完后会通知队列里面的线程可以出队继续执行

对象锁和类锁区别

  • 对于普通同步方法,锁是当前实例对象
  • 对于静态同步方法,锁是当前类的 Class 对象
  • 对于同步方法块,锁是 Synchonized 括号里配置的对象

Lock和ReentrantLock

除了Synchronized锁,还有一些比Synchronized更加轻量级的锁Lock和ReentrantLock

// ReentrantLock 基本用法
public static ReentrantLock reentrantLock = new ReentrantLock();
try {
    // 用法 1.reentrantLock.tryLock 先尝试过获取锁 获取不到就直接跳过
    if (reentrantLock.tryLock(5, TimeUnit.SECONDS)) { //让线程等待5秒看能不能那到锁 拿不到就取else
        System.out.println("获取");
    } else {
        System.out.println("获取失败");
    }
    // 用法 2.reentrantLock.lock 线程进入后直接加锁(强行获取)
    reentrantLock.lock();
    System.out.println("获取");
    // 用法 3.reentrantLock.lockInterruptibly 线程进入获取不到锁后直接中断
    reentrantLock.lockInterruptibly();
    System.out.println("获取");
} catch (InterruptedException e) {
    e.printStackTrace();
} finally {
    // 不加这个条件会报错 getHoldCount()方法来检查当前线程是否拥有该锁
    if(reentrantLock.isHeldByCurrentThread()) {
        reentrantLock.unlock(); //如果没有锁 解锁会报错
    }
}

一个简单的例子如下

public class MyReentrantLock implements Runnable{
    public static ReentrantLock reentrantLock = new ReentrantLock();

    @Override
    public void run() {
        try {
            if (reentrantLock.tryLock(5, TimeUnit.SECONDS)) { //让线程等待5秒看能不能那到锁 拿不到就取else
                Thread.sleep(3000); //模拟进来的线程都要执行6秒才释放锁
                System.out.println("获取");
            } else {
                System.out.println("获取失败");
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            if(reentrantLock.isHeldByCurrentThread()) {// 不加这个条件会报错 getHoldCount()方法来检查当前线程是否拥有该锁
                reentrantLock.unlock(); //如果没有锁 解锁会报错
            }
        }
    }

    public static void main(String[] args) {
        MyReentrantLock myReentrantLock = new MyReentrantLock();
        IntStream.range(0,2).forEach(i->new Thread(myReentrantLock){
        }.start());

    }
}

与Synchronized的区别

Synchronized:jvm 层级的锁 自动加锁自动释放锁
Lock:依赖特殊的 cpu 指令,代码实现、手动加锁和释放锁、Condition(生产消费模式)

AbstractQueuedSynchronizer

AbstractQueuedSynchronizer是java并发编程的核心类,是JUC的一个标准,java中锁的实现都用到了AbstractQueuedSynchronizer

队列同步器 AbstractQueuedSynchronizer(以下简称同步器) 
java.util.concurrent.locks.AbstractQueuedSynchronizer#acquire 独占式获取同步状态
java.util.concurrent.locks.AbstractQueuedSynchronizer#acquireInterruptibly 独占式获取同步状态,未获取可以 中断
java.util.concurrent.locks.AbstractQueuedSynchronizer#acquireShared 共享式获取同步状态 
java.util.concurrent.locks.AbstractQueuedSynchronizer#acquireSharedInterruptibly 共享式获取同步状态,未获 取可以中断 
java.util.concurrent.locks.AbstractQueuedSynchronizer#release 独占释放锁
java.util.concurrent.locks.AbstractQueuedSynchronizer#releaseShared 共享式释放锁 

实现原理使用的是 队列+双向链表

CountDownLatch

CountDownLatch(同步工具类)允许一个或多个线程等待其他线程完成操作
CountDownLatch 时,需要指定一个整数值,此值是线程将要等待的操作数。当某个线程为了要执行这些操 作而等待时,需要调用 await 方法。await 方法让线程进入休眠状态直到所有等待的操作完成为止。当等待 的某个操作执行完成,它使用 countDown 方法来减少 CountDownLatch 类的内部计数器。当内部计数器递 减为 0 时,CountDownLatch 会唤醒所有调用 await 方法而休眠的线程们

//CountDownLatch 例子
public class CountDownLatch01 {
    private final static int threadCount = 100;
    public static void main(String[] args) throws InterruptedException {
        ExecutorService executorService = Executors.newCachedThreadPool();
        CountDownLatch countDownLatch = new CountDownLatch(threadCount); //初始化一个数量
        for (int i =0;i< threadCount;i++){
            final int threadNum = i;
            executorService.execute(()->{
                try {
                    test(threadNum);
                } catch (Exception e) {
                    e.printStackTrace();
                }finally {
                    countDownLatch.countDown(); //每次执行完减一
                }
            });
        }
        countDownLatch.await(50, TimeUnit.MILLISECONDS);
        //等待50毫秒就增加执行如下代码,不管别的线程有没有跑完
        System.out.println("结束");
        executorService.shutdown();
    }

    private static void test(int threadNum) throws Exception {
        Thread.sleep(50);
        System.out.println(threadNum);
        Thread.sleep(50);
    }
}

Semaphore

Semaphore(信号量)是用来控制同时访问特定资源的线程数量,它通过协调各个线程,以保证合理的使用公共资源,控制一组线程同时执行 ,一般用于限流

public class Semaphore01 {
    //创建5个许可证
    private static Semaphore semaphore=new Semaphore(5);

    public static void main(String[] args) {
        //创建二十个线程同时进行秒杀
        for (int i=0;i<20;i++){
            final int j=i;
            new Thread(()->{
                try {
                    action(j);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }).start();
        }
    }

    public static void action(int i) throws InterruptedException {
        //每次进入许可-1,最大5个许可
        semaphore.acquire();
        System.out.println(i+"在京东秒杀iphonex");
        System.out.println(i+"秒杀成功");
        semaphore.release();
        //每次结束许可+1,最大5个许可
    }
}
发布了33 篇原创文章 · 获赞 22 · 访问量 953

猜你喜欢

转载自blog.csdn.net/weixin_44642403/article/details/104160847
今日推荐