02-Java并发编程之JVM&Lock&Tools

CPU

在了解锁之前我们先需要知道CPU是如何工作的,为什么我们使用多线程时会出现不同步的问题?如下图是单CPU和CPU多级缓存示意图
在这里插入图片描述

单CPU和CPU多级缓存示意图

在这里插入图片描述

CPU到硬盘粗略讲是需要经过 一级二级三级缓存=>内存=>硬盘,为什么CPU不能直接从硬盘读取数据,却要先经过内存呢?

  • CPU靠指令集工作,随着CPU的主频越来越高,处理速度越来越快,CPU的处理能力和信息吞吐能力远大于硬盘。
  • 硬盘只是一个存储器,已巨型机为例,计算结果和运行速度最重要,只要在硬盘中读取足够的信息就开始计算了,这样的机器硬盘不如内存重要。
  • 内存比硬盘数据吞吐量大,速度快。在加载系统后(不论是Windows、LINUX,包括DOS),主要使用的数据(80/20定律)都已经加载进了内存中。这样可以加快系统的速度,CPU是火箭的话,缓存就像飞机,内存是火车,硬盘像轮船。简而言之存储的容积越大速度越慢。
  • CPU对数据会有一个预判,这个预判是和程序有关的,每天,甚至每个程序所需的预判数据都不同,如果忽略内存,直接写入硬盘中,硬盘是掉电不复原的,只能删除,这样实际增加了系统开销(是指资源,不是价格)。也包括一次性的其他数据。
缓存 cache 的作用:

CPU 的频率很快,主内存跟不上 cpu 的频率,cpu 需要等待主存,浪费资源。所以 cache 的出现是解决 cpu和内存之间的频率不匹配的问题。

缓存 cache 带来的问题:

并发处理的不同步,例如核心1从内存拿到了一个int i=0运算后+1,但是核心2又去内存取但是这时int i=1还在缓存中,然后核心2拿出来又是0他又+1,你会发现这中间存在一个很大的问题整个运算过程中int不能保证一致,那我核心1做操作核心2又做操作,这样程序就会有很大的问题
解决方案: CPU厂家intel和amd提出协议:总线锁、缓存一致性的解决方案.

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

java线程内存模型根cpu缓存模型类型,是基于cpu缓存模型来建立的。java线程内存模型是标准化的,屏蔽掉了底层不同计算机的区别,使得Java语言编译程序只需生成在Java虚拟机上运行的目标代码(字节码),就可以在多种平台上不加修改地运行。

Heap(堆):java 里的堆是一个运行时的数据区,堆是由垃圾回收来负责的, 堆的优势是可以动态的分配内存大小,生存期也不必事先告诉编译器,因为他是在运行时动态分配内存的,java 的垃圾回收器会定时收走不用的数据,缺点是由于要在运行时动态分配,所有存取速度可能会慢一些(存在扩容导致)
Stack(栈):栈的优势是存取速度比堆要快,仅次于计算机里的寄存器,栈的数据是可以共享的,缺点是存在栈中的数据的大小与生存期必须是确定的,缺乏一些灵活性
栈中主要存放一些基本类型的变量,比如 int,short,long,byte,double,float,boolean,char,对象句柄
在这里插入图片描述
我们的方法都会放到栈里面,每一个栈都会对应一个对象,假如我们在Object1调用了Object2的方法我们的Object里面就会有一个Object2的副本。下图为java并发线程数据同步过程
在这里插入图片描述
加入我现在有2个线程,thread1和thread2,主内存有一个int=0,那threadA和threadB需要运算他们会先从主内存中拷贝一个副本int=0到工作内存中,对工作内存的int运算完毕后会覆盖内存的int。

tips: 这里之所以要提出JVM是因为保证我们java并发编程的3个核心概念。

原子性

即一个操作或者多个操作 要么全部执行并且执行的过程不会被任何因素打断,要么就都不执行

可见性

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

有序性

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

java内存模型的同步过程

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

Volatile
  • Java语言规范第3版中对volatile的定义如下:Java编程语言允许线程访问共享变量,为了确保共享变量能被准确和一致地更新,线程应该确保通过排他锁单独获得这个变量。Java语言 提供了volatile,在某些情况下比锁要更加方便。如果一个字段被声明成volatile,Java线程内存模型确保所有线程看到这个变量的值是一致的。
  • 关键字 volatile 可以说是 Java 虚拟机提供的最轻量级的同步机制,当一个变量定义为 volatile,它具有内存可见性以及禁止指令重排序两大特性。加上与去除volatile分别运行如下代码查看结果
public class VolidateVisiableTest {

  // private static volatile boolean flag = false;
  private static boolean flag = false;

  public static void main(String[] args) throws InterruptedException {
    new Thread(new Runnable() {
      @Override
      public void run() {
        System.out.println("waiting data...");
        while (!flag) {

        }
        System.out.println("======success====");
      }
    }).start();

    Thread.sleep(2000);

    new Thread(new Runnable() {
      @Override
      public void run() {
        prepareData();
      }
    }).start();

  }

  private static void prepareData() {
    System.out.println("=======prepare data========");
    flag = true;
    System.out.println("=======prepare end========");
  }

}

不加volatile运行结果

waiting data...
=======prepare data========
=======prepare end========

如果我们把volatile去处这个程序永远也不会停止,但是通过输出知道线程2已经走完;当我们加上volatile后flag这个值就具有内存可见性,如果有一个线程修改了他另外一个线程也能看到。

Volatile关键字介绍
  • 保证了不同线程对这个变量进行操作时的可见性,即一个线程修改了某个变量的值,这新值对 其他线程来说是立即可见的,可见性的意思是当一个线程修改一个共享变量时,另外一个线程能读到这个修改的值
  • 禁止进行指令重排序,程序执行的顺序按照代码的先后顺序执行
  • Volatile 只可以保证可见性与有序性; 单次操作可以保证原子性,但是不能保证复合原子性。比如: i++
Volatile原理
  • volatile 变量进行写操作时,JVM 会向处理器发送一条 Lock 前缀的指令,将这个变量所在缓存行的数据写会到系统内存。
  • Lock 前缀指令实际上相当于一个内存屏障(也成内存栅栏),它确保指令重排序时不会把其后面的指令排
  • 到内存屏障之前的位置,也不会把前面的指令排到内存屏障的后面;即在执行到内存屏障这句指令时,在它前面的操作已经全部完成
Volatile为什么不能保证原子性
public class VolatileAtoDemo 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 VolatileAtoDemo(), "A");
        Thread t2 = new Thread(new VolatileAtoDemo(), "B");
        Thread t3 = new Thread(new VolatileAtoDemo(), "C");
        Thread t4 = new Thread(new VolatileAtoDemo(), "D");
        t1.start();
        t2.start();
        t3.start();
        t4.start();
    }
}

多次运行后会发现结果并不是按顺序来的且各次运行情况不尽相同,因为有可能在我们改变值i需要回写到内存在我回写我最新的值之前已经有几个线程同时进入并且做加法操作,所以我们会发现有的线程拿到的还是之前的值。如果需要实现原子性需要运用到锁(synchronized、CAS)

Synchronized
  • 一句话解释synchronized:JVM会自动通过使用monitor来加锁与解锁,能保证在同一时刻最多只有一个线程执行指定代码,以达到并发安全的效果,同时具有可重如与不可中断的性质。
  • Synchronized是一个 重量级锁、重入锁、jvm 级别锁 ,他可以保证复合原子性,在方法他是使用:ACC_SYNCHRONIZED修饰方法,代码块:是在代码块前后加上monitorenter\monitorexit
Synchronized原理
public class SynchronizedDemo {
    public static void main(String[] args) {
        //使用方法1 对象锁
        synchronized (SynchronizedDemo.class){
        }
        //调用代码块
        m();
    }
    //使用方法2 定义静态代码块
    public static synchronized void m(){
    }
}

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

方法和代码块(对象锁和类锁):

⚫ 对于普通同步方法,锁是当前实例对象。(同步方法即加上了synchronized修饰)
⚫ 对于静态同步方法,锁是当前类的 Class 对象。
⚫ 对于同步方法块,锁是 Synchonized 括号里配置的对象。

synchronized方法各种使用场景
1.两个线程同时访问一个对象实例的同步方法:相互等待,锁生效。
2.两个线程访问的两个对象实例的同步方法:相互没有影响,并行执行。
3.两个线程访问的是synchronized的静态方法:即使实例不同,锁也生效。
4. 同时访问同步方法和非同步方法:非同步方法不受同步方法影响。
5. 访问同一个对象实例的不同的普通(static)同步方法:因为默认锁对象是this,所以锁生效,会并行执行。
6.同时访问静态synchronized方法和非静态synchronized方法:可以并行执行。因为static synchronized的锁是*.class,
  而non-static synchronized的锁是this,所以并不相互冲突,可以并行执行。
7.方法抛出异常后,会释放锁(RuntimeException()不用强制捕获)
8.方法method1()synchronized修饰,method1()中调用了method2(),
  method2()未被synchronized修饰,method2()还是线程安全的吗?不是,可以被多个线程同时访问
  总结:
a、一把锁只能同时被一个线程获取,没有拿到锁的线程必须等待(对应1,5情况)
b、每个实例都对应有自己的一把锁,不同实例之间互不影响;例外:锁对象是*.class以及
 synchronized修饰的是static方法的时候,所有对象共用同一把类锁(对应第2346种情况);
c、无论是方法正常执行完毕或者方法抛出异常,都会释放锁(对应第7种情况)
Synchronized缺陷与注意点
synchronized缺陷:
1、效率低:锁的释放情况少、试图获得锁时不能设定超时、不能中断一个正在试图获得锁的线程
2、不够灵活:加锁和释放的时机单一,每个锁仅有单一的条件(某个对象),可能是不够的
3、无法知道是否成功获取到锁

synchronized使用注意点:锁对象不能为空、作用域不宜过大、避免死锁
1)锁对象不能为空:必须是一个实例对象,被new过,或者使用其他方法创建好,而不是空对象。
这是因为,锁的信息保存在对象头中,对象都没有,更没有对象头,所以这个锁不能工作
2)作用域不宜过大:将尽可能多的代码使用synchronized包裹,会降低出并发问题的可能性,
因为大部分线程都是串行工作,没有达到多线程编程的目的,影响程序执行的效率

在这里插入图片描述

Lock和ReentrantLock

相比于Synchronized(jvm级别的锁),还有一些轻量级别的锁Lock和ReentrantLock
实现:

try {
  lock.lock();
  // TODO ***
}finally{
  lock.unlock();
}
// 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(); //如果没有锁 解锁会报错
    }
}

一个ReentrantLock例子如下

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

    @Override
    public void run() {
        try {
            if (reentrantLock.tryLock(5, TimeUnit.SECONDS)) { //让线程等待5秒看能不能那到锁 拿不到就取else
                Thread.sleep(3000); //模拟进来的线程都要执行3秒才释放锁
                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) {
        ReentrantLockDemo myReentrantLock = new ReentrantLockDemo();
        IntStream.range(0,2).forEach(i->new Thread(ReentrantLockDemo){
        }.start());

    }
}

Lock与Synchronized的区别以及如何选择
1、Synchronized:jvm 层级的锁 自动加锁自动释放锁
  Lock:依赖特殊的 cpu 指令,代码实现、手动加锁和释放锁、Condition(生产消费模式)
2、如何选择Lock和synchronized关键字
1)建议都不使用,可以使用java.util.concurrent包中的Automic类、countDown等类
2)优先使用现成工具,如果没有就优先使用synchronized关键字,好处是写尽量少的代码就能实现
功能。如果需要灵活的加解锁机制,则使用Lock接口
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 Demo
public class CountDownLatchDemo {
    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);
    }
}

在这里插入图片描述
使用:

⚫ java.util.concurrent.CountDownLatch#await()
⚫ java.util.concurrent.CountDownLatch#countDown()
⚫ java.util.concurrent.CountDownLatch#getCount()
CountDownLatch原理:

CountDownLatch 的构造函数接收一个 int 类型的参数作为计数器,如果你想等待 N 个点完成,这里就传入 N。当我们调用 CountDownLatch 的 countDown 方法时,N 就会减 1,CountDownLatch 的 await 方法会阻塞当前线程,直到 N 变成零。由于 countDown 方法可以用在任何地方,所以这里说的 N 个点,可以是 N 个线程,也可以是 1 个线程里的 N 个执行步骤。用在多个线程时,只需要把这个CountDownLatch 的引用传递到线程里即可。

CountDownLatch场景:
  • 并行计算
  • 依赖启动
  • CountDownLatch 是一次性的,只能通过构造方法设置初始计数量,计数完了无法进行复位,不能达到复用。可以实现类似于:FutureTask 和 Join 等功能
Semaphore

Semaphore(信号量)是用来控制同时访问特定资源的线程数量,它通过协调各个线程,以保证合理的使用公共资源。控制一组线程同时执行,限流效果好于(redis+lua)实现的分布式限流

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个许可
    }
}

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值