JAVA--内存模型和线程安全

Java内存模型

为了解决CPU运算速度和物理内存读写速度之间的巨大差异,Java引入了高速缓存的概念,缓存的读写速度和CPU速度差不多。即主内存和工作内存,主内存对应实际的物理内存,每一个线程都有自己的工作内存,所有的数据都存储在主内存上,线程需要用到的数据都来自于主内存的数据拷贝,等到数据处理完成再回写到主内存。

为什么会出现线程安全问题

我们知道线程操作的数据都是来自主线程的拷贝,当多个线程访问同一数据的时候,就可能出现一个线程读数据的时候另一个线程还没有来得及会写到主内存,造成读到的是脏数据,这就是我理解的线程安全问题

怎么判断一个操作是否是线程安全的

Java中自定义了8种先行发生原则

  • 程序顺序原则:同一个线程内部,写在前面的代码总是先发生于后面的代码
  • 锁定原则:对一个锁的unlock操作先行发生于后面对同一个锁的lock操作
  • Volatile变量原则:对Volatile修饰变量的写操作先行发生于后面对变量的读操作
  • 线程开始原则:线程start()方法先行发生于线程的其他所有操作
  • 线程终止原则:线程所有其他操作先行发生于线程的终止操作
  • 对象终止原则:对象的初始化完成先行发生于对象的finalize()方法调用
  • 传递性:A先行发生B,B先行发生C,则A先行发生C

一个操作如果不直接满足这些原则,而且不能由这些原则推导出来,那么它就是线程不安全的

Volatile关键字

Volatile修饰的变量具有2个特性

  • 可见性,写一个Volatile变量时,会把工作内存中的值同步回主内存,读一个Volatile变量时,会把工作线程中的值设置为无效
  • 顺序性,Volatile关键字禁止指令中排序

如何解决线程安全问题

要保证线程安全,就是保证操作满足三大特性

  • 原子性:一个操作那么全部完成,那么都不执行,不会出现中断(即一些执行了一些没有执行)操作,基本数据类型的读取和赋值都是原子操作

  • 可见性:在工作内存的操作会立即同步到主内存,就是说一个线程的操作对其他所有线程立即可见

  • 有序性:JVM可以对指令进行重新排序,但是不管怎么排序,要保证程序执行的结果相同

通过前面可以知道,Volatile关键字可以保证可见性和有序性,如果对Volatile关键字修饰的变量操作也具备原子性的话,那么就是线程安全的。所以它是一个轻量级的线程安全机制

Synchronized关键字的使用和原理

Synchronized关键字可以保证操作具备三大特性

它会在编译后代码的前后增加Monitorenter和Monitorexit两条指令,这个指令都有一个reference属性,用来制定需要加锁和解锁的对象,除了明确指定锁对象,否则Synchronized修饰一般方法时锁对象是对象实例,修饰静态方法时锁对象是类的class对象

//这里需要理解清楚再改

线程执行Monitorexit指令的时候,会尝试获取对象的锁,如果获取失败,说明对象被锁定,线程阻塞。否则把对象加锁,所计数器加1

ReentrantLock

并发包下的可重入锁也可以用来实现线程同步,使用时需要显式的执行lock和unloc操作,并用try/finally来保证任何情况下都能执行解锁操作,而且它提供了更多的高级特性

  • 等待可中断:如果线程等待获取锁的时候太长了,可以选择放弃等待
  • 公平锁,可以让线程按照申请锁的时间顺序排队依次获取锁,使用lockInterruptibly()方法
  • 绑定多个条件,配合condition使用,使用newConditin()方法

锁优化

JDK5以前,Synachronized和ReentrantLock在性能上有很大的差异,但在JDK6后,对Synachronized做了大量的优化,和ReentrantLock相比几乎没有性能差异了,所以通常情况下推荐使用Synachronized毕竟很方便

  • 自旋锁 让线程进入阻塞状态是实现同步的一个主要性能开销,因为要把线程从用户态切换到核心态。自旋锁就是不直接阻塞线程,而是执行一个循环,进入忙等待状态,默认循环10次,如果10次循环后还没有获取到锁,再阻塞线程

  • 自适应自旋锁 自适应是不设置循环的次数,而是根据上一次获得锁的时间等条件自动设置循环的次数

  • 锁粗化...等

猜你喜欢

转载自juejin.im/post/5d751355f265da03ba32665c