java 锁
java中使用锁,主要是用于解决多线程并发问题
多个线程对某个对象进行操作,就存在并发问题。
java内存规定了,所有变量都储存在主内存中,每个线程又有自己的工作内存
线程的工作内存中保存了该线程中用到的变量的主内存副本拷贝,线程对变量的所有操作都必须在工作内存中进行,而不能直接读写主内存
线程访问一个变量,首先将变量从主内存拷贝到工作内存,对变量的写操作,不会马上同步到主内存。
不同的线程之间也无法直接访问对方工作内存中的变量,线程间变量的传递均需要自己的工作内存和主存之间进行数据同步进行。
注意:锁作用的都是对象,在对象的内存空间中,有标志位标记是有有锁。
并发三要素
原子性:在一个操作中,CPU 不可以在中途暂停然后再调度,即不被中断操作,要么执行完成,要么就不执行。
可见性:多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即看得到修改的值。
有序性:程序执行的顺序按照代码的先后顺序执行。
解决并发问题
volatile:保证可见性,不保证原子性
当写一个volatile变量时,JVM会把本地内存的变量强制刷新到主内存中
这个写操作导致其他线程中的缓存无效,其他线程读,会从主内存读。volatile的写操作对其它线程实时可见
禁止指令重排序
指令重排序是指编译器和处理器为了优化程序性能对指令进行排序的一种手段,需要遵守一定规则
不会对存在依赖关系的指令重排序,例如 a = 1;b = a; a 和b存在依赖关系,不会被重排序
不能影响单线程下的执行结果。比如:a=1;b=2;c=a+b这三个操作,前两个操作可以重排序,但是c=a+b不会被重排序,因为要保证结果是3
使用场景
对于一个变量,单个线程写,其他线程读,这个时候就可以使用volatile来修饰这个变量
1 |
|
初始化一个对象有如下步骤:
分配内存,
初始化对象,
指向内存
如上代码,如果不使用volatile修饰,这时候如果发生指令重排,执行顺序是132,执行到第3的时候,线程B刚好进来了,
并且执行到注释2,这时候判断instance 不为空,直接使用一个未初始化的对象。所以使用volatile关键字来禁止指令重排序。
volatile 原理
在jvm底层,volatile是通过内存屏障来实现的,内存屏障会提供三个功能:
它确保指令重排序时不会把其后面的指令排到内存屏障之前的位置,也不会把前面的指令排到内存屏障的后面;即在执行到内存屏障这句指令时,在它前面的操作已经全部完成;
它会强制将缓存的修改操作立即写到主内存
写操作会导致其它CPU中的缓存行失效,写之后,其它线程的读操作会从主内存读。
valatile局限性
volatile 只能保证可见性,不能保证原子性,写操作对其它线程可见,但是不能解决多个线程同时写的问题
Synchronized
多个线程同时写一个变量
这个时候使用Synchronized,可以保证同一时刻,只有一个线程可执行某个方法或某个代码块
Synchronized锁升级
Java1.6 中为了减少获得锁和释放锁带来的性能消耗而引入的偏向锁和轻量级锁。
偏向锁 :大多数情况下,锁不仅不存在多线程竞争,而且总是由同一线程多次获得,为了让线程获得锁的代价更低而引入了偏向锁。
当一个线程A访问加了同步锁的代码块时,会在对象头中存 储当前线程的id,后续这个线程进入和退出这段加了同步锁的代码块时,不需要再次加锁和释放锁。
轻量级锁 :在偏向锁情况下,如果线程B也访问了同步代码块,比较对象头的线程id不一样,会升级为轻量级锁,并且通过自旋的方式来获取轻量级锁。
重量级锁 :如果线程A和线程B同时访问同步代码块,则轻量级锁会升级为重量级锁,线程A获取到重量级锁的情况下,线程B只能入队等待,进入BLOCK状态。
Synchromized缺陷
不能设置锁超时时间
不能通过代码释放锁
容易造成死锁
ReentrantLock
上面说到Synchronized的缺点,不能设置锁超时时间和不能通过代码释放锁,ReentranLock就可以解决这个问题
在多个条件变量和高度竞争锁的地方 ,
用ReentrantLock更合适,ReentrantLock还提供了Condition,对线程的等待和唤醒等操作更加灵活,一个ReentrantLock可以有多个Condition实例,所以更有扩展性
原文地址(https://www.jianshu.com/p/4eec21c3338e)
JVM内存模型、指令重排、内存屏障概念解析(https://www.cnblogs.com/chenyangyao/p/5269622.html)
在java中有以下锁:
- 公平锁/非公平锁
- 可重入锁
- 互斥锁/读写锁
- 独享锁/共享锁
- 分段锁
- 偏向锁/轻量级锁/重量级锁