线程安全&Java内存模型

目录
  • Java内存模型
  • 关于线程安全
  • Volatile关键字
  • Synchronized关键字
  • 锁的分类
    • 轻量级锁&重量级锁
    • 可重入锁&不可重入锁
    • 读写锁
    • 乐观锁与悲观锁
      • 乐观锁(适合多读场景)
      • 悲观锁(适合多写场景)
    • CAS无锁机制

Java内存模型

Java内存模型(JMM)主要目标是定义多线程的情况下线程访问变量的规则

JMM规定线程之间的共享变量存储在主内存中,每个线程都有一个本地内存(工作内存),本地内存存储了共享变量的副本。

线程安全&Java内存模型_第1张图片

关于线程安全

  • 什么是线程安全问题?

    多个线程同时共享同一个全局变量的操作时候,可能会受到其他线程的干扰,导致数据脏读。(数据一致性问题)

  • 如何解决线程安全问题?

    核心思想:在同一时刻,只能有一个线程执行。

    通过加锁使线程更加安全,也使程序的执行效率更低。

  • 衡量线程安全的3个要素:

    • 原子性:一个操作或者多个操作要么全部执行,要么都不执行
    • 可见性:多个线程访问同一变量,一个线程修改了变量的值,其他线程可以立即看到修改的值
    • 有序性:程序按照代码的顺序先后执行(与指令重排有关)

Volatile关键字

volatile是一种轻量级的同步机制,可以保证可见性【及时将修改的变量刷新到主内存中】,但不能保证原子性,并且禁止重排序。

volatile在多线程下的适用场景:一写多读

volatile如何保证内存可见性?

当一个线程对volatile修饰的变量进行写操作时,该线程中的本地内存的变量会被立刻刷新到主内存中。

当一个线程对volatile修饰的变量进行读操作时,该线程直接读取主内存的变量。

volatile能否保证线程安全?

不能,保证线程安全需要同时具备原子性,可见性和有序性。而volatile只能保证可见性和有序性,无法保证原子性。

Synchronized关键字

核心思想:在多线程执行同一个方法时,只有获取到锁,才能进入方法里面执行

使用方式:

  • 修饰一个类:其作用的范围是synchronized后面括号括起来的部分,作用的对象是这个类的所有对象
  • 修饰一个方法:被修饰的方法称为同步方法,其作用的范围是整个方法,作用的对象是调用这个方法的对象
  • 修饰一个静态的方法:其作用的范围是整个方法,作用的对象是这个类的所有对象
  • 修饰一个代码块:被修饰的代码块称为同步语句块,其作用范围是大括号{}括起来的代码块,作用的对象是调用这个代码块的对象

锁的分类

轻量级锁&重量级锁

轻量级锁:手动上锁解锁,扩展性强。代表:Lock

重量级锁:自动上锁解锁,封装程度高。代表:Synchronized

可重入锁&不可重入锁

可重入锁(递归锁):当一个线程已经获取到锁后,再次请求该锁,就可直接获取。(锁的传递,锁的嵌套)代表:Synchronized,Lock

锁的可重入性避免了大部分死锁情况的产生

不可重入锁:不具备传递性

读写锁

ReentrantReadWriteLock

相对Synchronized效率更高,但在多线程情况下,只支持读读共存,不支持读写,写写。

乐观锁与悲观锁

乐观锁(适合多读场景)

  • 思想:认为不会发生线程冲突(本质上是没有锁的)

  • 执行流程,先读取数据,然后在更新前检查在读取至更新这段时间数据是否被修改

    • 未修改:直接更新数据
    • 已修改:重新读取,再次提交更新(或者放弃操作)

为什么乐观锁适合多读场景?

乐观锁是一种更新前的检查机制,相对于悲观锁来说在多读场景下可以减少锁的性能开销,对于多写场景,乐观锁会一直进入已修改,重新读取,再次提交的循环,反而带来更多的资源消耗。

悲观锁(适合多写场景)

  • 思想:认为一定会发生线程冲突

  • 执行流程:读取数据的时候上锁(其他用户无法读取),直到本次数据更新完成才会释放锁。在多写场景下,能保证较高的数据一致性。

【总的来说,乐观锁回滚重试,悲观锁阻塞事务】

CAS无锁机制

原子类:AtomicBoolean,AtomicInteger,AtomicLong,AtomicReference可保证线程安全,底层使用CAS无锁机制

CAS:Compare and Swap,比较再交换,属于乐观锁的一种

  • CAS原理

    CAS包含3个参数,CAS(V,E,N),V:主内存的变量值,E:本地内存修改前的值,N:本地内存修改后的值

    比较主内存的值和本地内存修改前的值是否一致,若一致,将修改后的值刷新到主内存,若不一致,当前线程放弃更新,将主内存数据刷新到本地内存,再次重试。

  • 优点:非阻塞,不会发生死锁情况,效率更高

  • 缺点:ABA问题(可以通过加入版本号来区分变量是否被修改)

你可能感兴趣的:(线程安全&Java内存模型)