java 高并发知识点学习总结
Posted wcgstudy
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了java 高并发知识点学习总结相关的知识,希望对你有一定的参考价值。
并发 : 同时拥有两个或多个线程,如果程序在单核处理器上运行,多个线程交替的换入或者换出内存,这些线程是同时存在的,每个线程都处于执行过程中的某个状态,如果运行在多核处理器上,此时,程序中的每个线程都将分配到一个处理器核上,因此可以同时运行。
为什么需要cpu cache: cpu 的频率太快,快到主存跟不上,这样在处理器始终周期内,CPU常常需要等待主存,浪费资源。所以cache得出现,是为了缓解CPU和内存之间速度的不匹配问题。
CPU cache 有什么意义:
1) 时间局部性: 如果某个数据被访问,那么在不久的将来它可能被再次的访问
2) 空间局部性: 如果某个数据被访问是,那么与他相邻的数据块可能会被很快的访问
CPU多级缓存 --缓存一致性 (MESI--缓存协议)
用于保证多个CPU cache 之间缓存共享数据的一致
CPU多级缓存-乱序执行优化
处理器为提高运算速度而做出违背代码原有顺序的优化
java内存模型规范(java Memory Model,JMM)
定义:java内存模型规范规定了一个线程如何以及何时可以看到由其他线程修改过的共享变量的值,以及在必须时如何同步的访问共享变量。
java内存中的堆: 可以在运行时动态的分配运行时内存,因此运行效率相对慢一些。
java中的栈: 优势: 存取速度要比java 中的堆的 存取速度要快,栈中的数据是可以共享的,但是栈中数据的生存期与大小是确定的。栈中主要存放一些基本的数据变量。
通常情况下,当cpu 需要读取主存的时候,会先将主存中的数据读取到缓存中,甚至会将缓存中的某些数据读取到内部寄存器中,
在寄存器中执行某些操作。
java内存模型 - 同步的八种操作
lock(锁定) : 作用于主内存的变量,把一个变量标识为一条线程独占状态
unlock(解锁): 作用于主内存的变量,把一个处于锁定状态的变量释放出来,释放出来之后才可以被其他的线程使用。
read(读取): 作用于主内存的变量,把一个变量从主内存传输到线程的工作内存中,以便随后的load动作使用。
load(载入): 作用于工作内存的变量,他把read操作从主内存中得到的变量值放入到工作内存的变量副本中。
use(使用): 作用于工作内存的变量,把工作内存中的一个变量值传递给执行引擎
assign(赋值): 作用于工作内存的变量, 他把一个从执行引擎接受到的值赋值给工作内存的变量。
store(存储): 作用于工作内存的变量,把工作内存中的一个变量的值传输到主内存中,以便随后的write的操作
write(写入): 作用于主内存的变量,它把store操作从工作内存中一个变量的值传送到主内存的变量中 。
java内存模型--同步规则
如果要把一个变量从主内存中复制到工作内存,就需要按顺序的执行read和load操作,如果把变量从工作内存中同步回之内存中,就要按顺序的执行store 和 write 操作。但java 内存模型只要求上述操作必须按顺序执行,而没有保证必须是连续执行。
不允许read和load、store和write操作之一单独出现
不允许一个线程丢弃它的最近assign的操作,即变量在工作内存中改变了之后必须同步到主内存中。
不允许一个线程无原因的(没有发生过任何assign操作)把数据从工作内存同步回主内存中
一个新的变量只能在主内存中诞生,不允许在工作内存中直接使用一个未被初始化(load或assign)的变量。即就是对一个变量实施use和store操作之前,必须先执行过了assign和load操作
一个变量在同一时刻只允许一条线程对其执行lock操作,但lock操作可以被同一线程重复执行多次,多次执行lock之后,只有执行相同次数的unlock操作,变量才被解锁。lock和unlock必须是成对出现的。
如果对一个变量执行lock操作,将会清空工作内存中此变量的值,在执行引擎使用这个变量前需要重新执行load和assign操作初始化变量的值。
如果一个变量事先没有被lock操作锁定,则不允许对他执行unlock操作,也不允许unlock一个被其他线程锁定的变量
线程安全性 :
原子性: 提供了互斥访问,同一时刻只能有一个线程来对他进行操作
可见性: 一个线程对主内存的修改可以及时的被其他线程观察到
有序性: 一个线程观察其他线程中的指令执行顺序,由于指令重排序的存在,该观察结果一般杂乱无序
CAS线程安全底层原理: 使用了compareAndSwapInt方法,不断的使用当前的数值与底层主存中的值进行比较,只有当想听那个的时候,才进行返回。
actomicXXX类: 保证线程的安全性。 、
JDK1.8 : LongAdder 该类的作用和atomicLong 类的作用是一样的。
LongAdder类的作用: 主要是对热点数据的一个分离,进行单独处理。
在低并发的时候,性能和atomicLong 的性能是一致的,但是高并发的时候,通过分散提高了性能,但是在统计的时候,如果有并发更新也会导致统计误差。
对于需要生成准确的数值,具有全局唯一性数值的时候,atomicLong才是唯一的选择 。
atomicReference,AtomicReferenceFieldUpdate:
atomicReference:
AtomicStampReference: CAS的ABA问题 假如 某个数据被修改为A然后修改为B再修改为A,该数据的版本就会发生变化
原子性锁:
synchronized: 依赖JVM (作用对象的作用范围内同一时刻只能有一个对象对其进行操作)
Lock:依赖特殊的CPU指令,代码实现,ReentrantLock
synchronized:
修饰代码块: 大括号括起来的代码,作用于调用的对象
修饰方法: 整个方法,作用于调用的对象
修饰静态方法: 整个静态方法,作用于所有的对象
修饰类: 括号括起来的部分,作用于所有的对象
synchronized: 在方法定义中synchronized并不是方法的一部分,所以当synchronized修饰的方法被继承的时候,子类是没有synchronized功能的。
可见性: 导致共享变量在线程间不可见的原因:
线程交叉执行
重排序结合线程交叉执行
共享变量更新后的值没有在工作内存与主存之间及时的更新
JMM关于synchronized的两条规定 :
线程解锁前,必须把共享变量的最新值刷新到内存中。
线程加锁时,将清空工作内存中共享变量的值,从而使用共享内存时需要从主内存中重新进行读取
volatile: 通过加入内存屏障和禁止重排序优化来实现
对volatile变量写操作时,会在写操作后加入一条store屏障指令,将本地内存中的共享变量值刷新到主内存中
对volatile变量读操作时,会在读操作前加入一条load屏障指令,从主内存中读取共享变量
volatile不具备原子性:
适用的场景: 1、 对变量的写错做不依赖于当前的值
2、 该变量没有包含在其他变量不变的实时中
很适合做状态标识变量
java内存模型中,允许编译器和处理器对指令进行重排序,但是重排序的过程不会影响到单线程的执行,却会影响到多线程的并发执行正确性
volatile、synchtroniezed、lock
程序次序规则:一个线程内,按照代码顺序,书写在前面的操作现行发生于书写在后面的操作
锁定规则: 一个unlock操作先行发生于后面对同一个锁的lock操作
volatile变量规则: 对一个变量的写操作先行发生于后面对这个变量的读操作
传递规则: 如果操作A先行发生于操作B,而操作B又先行发生于操作C,则可以得出操作A先行发生于操作C
线程启动规则: Thread对象的start() 方法先行发生于此线程的每一个动作
线程中断操作: 对线程interrupt() 方法的调用先行发生于被中断线程的代码检测到中断事件的发生
线程终止规则: 线程中所有的操作都先发生于线程的终止检测,我们可以通过Thread.join()方法结束、Thread.isAlive()的返回值手段检测到线程已经终止执行
对象终结规则: 一个对象的初始化的完成会先行发生于他的finalized() 的方法的时开始
对象发布:
发布对象: 使一个对象能够被当前范围之外的代码所使用
对象逸出: 一种错误的发布。当一个对象还没有构造完成时,就使他被其他线程所见。
展示对象逸出代码示例:
package MyStudyTest; /** * 无论是显示的引用还是隐式的引用都会造成this的逸出,因为构造方法还没有初始化完成,其他的线程就看到了该成员变量的值 * 错误原因: 在对象还没有完成构造函数初始化之前就对其发布 */ public class Example2 private int thisCanBeEscape = 0; public Example2() new InnerClass(); private class InnerClass public InnerClass() System.out.println(""+Example2.this.thisCanBeEscape);
如果一个对象是一个可变对象,就需要对其进行安全的发布,否则就会造成线程的不安全。
安全发布对象的四种方法:
1、在静态初始化函数中初始化一个对象的引用
2、将对象的引用保存到volatile类型域或者AtomicReference对象中。
3、将对象的引用保存到某个正确构造对象的final类型域中
4、将对象的引用保存到一个有锁保护的域中
volatile关键字有两个使用场景:
1、 可以做状态标识量 (因为该关键字会将工作内存空间中的数据刷新到主内存中)
2、可以进行双重检测(因为该关键字可以限制寄存器发生指令重排--针对单例懒汉模式的双重检测机制)
package MyStudyTest; /** * 懒汉模式: * 单例实例在第一次使用的时候创建 * 线程不安全的,只适用于在单线程的情况下使用 */ public class SingleTenExample //私有构造函数,只有当构造函数式私有的时候,才不会被其他的引用随便的创建出来 private SingleTenExample(); //单例 private static volatile SingleTenExample instance = null; //静态工厂方法 /* public static SingleTenExample getInstance() if(instance == null) return new SingleTenExample(); */ //添加synchronized关键字,保证线程安全性 /* public static Synchronized SingleTenExample getInstance() if(instance == null) instance = new SingleTenExample(); */ //以上方法虽然实现了线程的安全性,但是在性能方面较差 //双重同步锁单例模式 //这种方式依然是线程不安全的 //内存寄存器依次完成的指令: // 1、memory = allocate() 分配对象的内存空间 // 2、ctorInstance() 初始化对象 // 3、instance = memory 设置instance指向刚分配的内存 // JVM和cpu优化,发生了指令重排 // 1、memory = allocate() 分配对象的内存空间 // 3、instance = memory 设置instance指向刚分配的内存 // 2、ctorInstance() 初始化对象 // /** * 双重检测机制不一定是线程安全的,原因是由于有指令重排发生的可能, * 当有两个线程同时调用getinstance方法的时候,假如A线程刚好执行到instance = new SingleTenExample(),指令2和3发生了顺序交换, * 那么当线程B请求instance ==null的时候,就会默认已经创建了实例,返回instance,而实际上该线程还没有进行初始化,就会导致线程不安全 * * 注: 通过volatile关键字就能限制发生指令重排 */ public static SingleTenExample getInstance() if (instance == null) // 双重检测机制 // B synchronized (SingleTenExample.class) // 同步锁 if (instance == null) instance = new SingleTenExample(); // A - 3 return instance;
注:静态域和静态代码款的执行顺序不一样,产生的结果就会不一样
使用枚举来实现单例模式,同时也是一种很安全的单例模式
package com.mmall.concurrency.example.singleton; import com.mmall.concurrency.annoations.Recommend; import com.mmall.concurrency.annoations.ThreadSafe; /** * 枚举模式:最安全 */ @ThreadSafe @Recommend public class SingletonExample7 // 私有构造函数 private SingletonExample7() public static SingletonExample7 getInstance() return Singleton.INSTANCE.getInstance(); private enum Singleton INSTANCE; private SingletonExample7 singleton; // JVM保证这个方法绝对只调用一次 Singleton() singleton = new SingletonExample7(); public SingletonExample7 getInstance() return singleton;
对于为什么枚举是最安全的详见博客: https://www.cnblogs.com/wcgstudy/p/11408495.html
不可变对象:
不可变对象需要满足的条件:
1、对象创建以后,其状态就不能修改
2、对象所有的域都是final类型的
3、对象时正确创建的(对象创建期间,this引用没有逸出)
final: 可以用来修饰类、对象、方法
修饰的类不能被继承 ,final修饰的类中的所有方法都会被隐式的指定为final方法
修饰方法: 1、锁定方法不被继承类修改 2、效率(在JDK早期版本中将方法作为一个内嵌方法,来提升效率,但是当方法过于庞大时,效率不会被提升)
2、private修饰的方法会被隐式的转换为final修饰的方法
修饰变量: 1、基本数据类型变量 2、引用数据类型变量 (被初始化之后,不能指向另外的一个对象)
当final修饰map的时候,只是引用不允许指向另外的一个对象,但是里面的值是允许进行修改的 。
java中其他不可变对象的申明方法:
1、 Collections.unmodifiableXXX:Collection、list、set、Map
2、 Guava : ImmutableXXX: Collection、list、set、Map
被以上两个方法修饰的容器中的数据是不可变的
代码演示实例:
package com.mmall.concurrency.example.immutable; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableSet; import com.mmall.concurrency.annoations.ThreadSafe; @ThreadSafe public class ImmutableExample3 private final static ImmutableList<Integer> list = ImmutableList.of(1, 2, 3); private final static ImmutableSet set = ImmutableSet.copyOf(list); private final static ImmutableMap<Integer, Integer> map = ImmutableMap.of(1, 2, 3, 4); private final static ImmutableMap<Integer, Integer> map2 = ImmutableMap.<Integer, Integer>builder() .put(1, 2).put(3, 4).put(5, 6).build(); public static void main(String[] args) System.out.println(map2.get(3));
线程封闭: 就是将对象封装到一个线程中,这样即使这个对象是线程不安全的,也不会有线程不安全的问题存在
实现线程封闭的几种方式:
1、AD-HOC线程封闭: 程序控制实现,最糟糕,可以忽略
2、堆栈封闭: 使用局部变量,无并发问题
3、ThreadLocal 线程封闭: 特别好的线程封闭方法,ThreadLocal底层使用了一个Map实现了线程的封闭
以上是关于java 高并发知识点学习总结的主要内容,如果未能解决你的问题,请参考以下文章