[toc]
Volatile volatile是什么?为什么引入了volatile volatile是java虚拟机提供的一种轻量级同步机制
保证可见性
不保证原子性
禁止指令重排(保证了有序性)
高并发下不用synchronized,因为并发性不好;用volatile和juc下面的类;
JMM抽象结构图描述 关于JMM: blog/2018/07/27/15326929363424/#2-3-JAVA内存模型(JMM)
结构图主要描述:
主内存/工作内存/共享变量/拷贝副本
JMM是一种抽象概念,不真实存在,它描述的是一组规则/规范
JMM如何保证同步的?
解锁前,把工作内存的值刷到主内存
加锁时,从主内存读取最新值到工作内存
加锁解锁是同一把锁
JMM三大特性是什么?
可见性
原子性
有序性
三大特性使线程安全性获得保证
可见性是什么意思? 某线程如果修改了主内存的共享变量,对其他线程是可见的。
volatile保证可见性代码演示 demo
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 package com.mxx.juc; import java.util.concurrent.TimeUnit; public class VolatileDemo { volatile int num = 1 ;public void changeNum () {this .num = 30 ;} public static void main (String[] args) {VolatileDemo vd = new VolatileDemo(); new Thread(()->{System.out.println("进入线程" +Thread.currentThread().getName()+",num=" +vd.num); try {TimeUnit.SECONDS.sleep(3 ); } catch (InterruptedException e) { e.printStackTrace(); } vd.changeNum(); System.out.println("线程" +Thread.currentThread().getName()+"修改num,num=" +vd.num); },"a" ).start(); while (vd.num==1 ){} System.out.println(Thread.currentThread().getName()+"线程结束,num=" +vd.num); } }
原子性是什么意思? 某个线程在做具体业务时,要么整体成功,要么整体失败,保证数据一致性
volatile不保证原子性代码演示 demo
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 package com.mxx.juc;import java.util.concurrent.TimeUnit;public class VolatileDemo { volatile int num = 1 ; public void addNum () { num++; } public static void main (String[] args) { VolatileDemo vd = new VolatileDemo(); for (int i = 0 ; i < 20 ; i++) { new Thread(()->{ for (int j = 0 ; j < 1000 ; j++) { vd.addNum(); } },"线程" +i).start(); } while (Thread.activeCount()>2 ){ Thread.yield(); } System.out.println(Thread.currentThread().getName()+"线程: num=" + vd.num); } }
volatile不保证原子性理论解释(num++为什么不安全) num++出现问题底层逻辑(javap -c):
获得:各线程从主内存读num到工作内存
修改:各线程在各自工作内存做+1操作,工作内存中的操作互相不可见。
写回:线程在写回前被挂起了,写回的时候相互覆盖 ,造成数值丢失。
volatile不保证原子性问题怎么解决?
加synchronized,并发性能不好
juc的atomic,比如AtomicInteger的getAndIncrement()
AtomicInteger保证原子性代码演示 demo
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 AtomicInteger num2 = new AtomicInteger(1 ); public void addNumByAtomic () { num2.getAndIncrement(); } public static void main (String[] args) { VolatileDemo vd = new VolatileDemo(); for (int i = 0 ; i < 20 ; i++) { new Thread(()->{ for (int j = 0 ; j < 1000 ; j++) { vd.addNum(); vd.addNumByAtomic(); } },"线程" +i).start(); } while (Thread.activeCount()>2 ){ Thread.yield(); } System.out.println(Thread.currentThread().getName()+"线程: num=" + vd.num); System.out.println(Thread.currentThread().getName()+"线程: num2=" + vd.num2); }
* 有序性之Happens-before原则 java内存模型一个列出了八种Happens-before规则,如果两个操作的次序不能从这八种规则中推倒出来,则不能保证有序性
更详细的:blog/2018/07/28/15327607157902/#4-4-有序性
Volatile通过什么实现可见性? 通过加入 内存屏障 和 禁止重排序优化 来实现
什么是指令重排?造成什么问题?
Java内存模型中,为提高性能,允许编译器和处理器对指令进行重排序。
重排时会考虑到指令间的数据依赖性
不会影响单线程环境下程序执行
多线程下,线程交替执行,由于优化重排的存在,两线程使用的变量能否保证一致性是无法确定的。结果无法预测。
* 指令重排造成的不安全举例 1 2 3 4 5 6 7 8 9 10 11 12 13 14 int a=0 ;boolean flag = false ;public void m1 () { a=1 ; flag=true ; } public void m2 () { if (flag){ a = a+5 ; System.out.println("revalue=" +a); } }
解释:
线程1执行m1,线程2执行m2
在不重排时,一定是按照123步骤执行,结果为6
如果发生重排,比如1和2交换了顺序,当m1执行完2时,线程切换,执行m1,这时可以进入if函数,a结果为5
* 什么是内存屏障? 详细:blog/2018/07/28/15327607157902/#4-3-可见性
关键字:
load屏障/store屏障
volatile读/volatile写
内存屏障还能强制刷出cpu缓存,保证数据最新
如何保证有序性? 保证有序性:volatile、synchronized、Lock
你在哪些地方用到了volatile?
单例模式在多线程下不安全
读写锁/手写缓存
cas底层源码分析
单例模式在多线程下不安全代码演示 demo
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 public class SingletonDemo { private static SingletonDemo instance = null ; private SingletonDemo () { System.out.println(Thread.currentThread().getName()+"\t调用构造方法" ); } public static SingletonDemo getInstance () { if (instance == null ){ instance = new SingletonDemo(); } return instance; } public static void main (String[] args) { for (int i = 0 ; i < 10 ; i++) { new Thread(()->{ SingletonDemo.getInstance(); },"线程" +i).start(); } } }
! 单例模式在多线程下不安全解决方案? DCL双端检索机制+volatile
为什么只用DCL不能保证线程安全? 简答:在创建对象过程中发生指令重排。检测到intance不为null但对象却没有完全创建成功。
双端检索机制:在加锁前和加锁后都进行一次判断
demo:
1 2 3 4 5 6 7 8 9 10 public static SingletonDemo getInstance () { if (instance == null ){ synchronized (SingletonDemo.class ) { if (instance == null ){ instance = new SingletonDemo(); } } } return instance; }
为什么以上代码不一定安全?
因为有指令重排序的存在
原因在于:某线程读到instance不为nul时,instance的引用对象可能还没有完成初始化(new SingletonDemo()做到一半) instance = new SingletonDemo()分为以下步骤: 1)分配对象内存空间 2)初始化对象 3)设置instance指向内存地址,此时instance != nul 其中步骤2 3可能发生重排,
因此多线程下,当线程a访问instance!=null时,instance实例却未必初始化完成(还没做2);此时切到线程b,线程b直接取intance实例,这个实例是未完成初始化的实例。因此线程不安全。
如何解决?
1 2 private static volatile SingletonDemo instance = null ;
CAS 提问线路:CAS—> Unsafe—> CAS底层原理 —> 原子引用更新 —> 如何规避ABA问题
compareAndSet怎么用? 比较并交换(compareAndSet)
1 2 3 4 5 6 7 8 9 10 11 12 13 public static void main (String[] args) { AtomicInteger atomicInteger = new AtomicInteger(5 ); System.out.println(atomicInteger.compareAndSet(5 , 10 )+ "\t" + atomicInteger); System.out.println(atomicInteger.compareAndSet(5 , 20 )+ "\t" + atomicInteger); }
CAS底层原理简述?
Compare-And-Swap。是一条CPU并发原语。(原语:操作系统范畴,依赖硬件,不被中断。)
功能是判断内存某个位置的值是否为预期值(Compare),是就更新(Swap),这个过程是原子的。
功能描述:
判断内存某个位置的值是否为预期值(Compare),是就更新(Swap),这个过程是原子的。
cas有三个操作数,内存值V,旧预期值A,要更新的值B。仅当预期值A=内存值V时,才将内存值V修改为B,否则什么都不做。
自旋:比较并交换,直到比较成功
底层靠Unsafe类保证原子性。
getAndIncrement() 源码解析(用了cas保证线程安全) 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 public final int getAndIncrement () { return unsafe.getAndAddInt(this , valueOffset, 1 ); } public final int getAndAddInt (Object var1, long var2, int var4) { int var5; do { var5 = this .getIntVolatile(var1, var2); } while (!this .compareAndSwapInt(var1, var2, var5, var5 + var4)); return var5; }
简述:
调用了Unsafe类的getAndAddInt
getAndAddInt使用cas一直循环尝试修改主内存
对Unsafe的理解? Unsave类
该类所有方法都是native修饰,直接调用底层资源。sun.misc包中。
可以像C的指针一样直接操作内存。java的CAS操作依赖Unsafe类的方法。
! CAS有哪些缺点?
循环时间长,开销大
如果cas失败,就一直do while尝试。如果长时间不成功,可能给CPU带来很大开销。
只能保证一个共享变量的原子操作
如果时多个共享变量,cas无法保证原子性,只能加锁,锁住代码段。
引出来ABA问题。
ABA问题 简述ABA问题和解决方案? 简述版:
ABA问题描述: 线程1做CAS操作将A改为B再改为A,而线程2再做CAS时修改成功了,这不符合设计思想。
怎么解决:AtomicStampReference时间戳原子引用
ABA问题描述?问题出在哪? ABA问题描述:
比如线程1从内存位置V中取出A,此时线程2也取出A。且线程2做了一次cas将值改为了B,然后又做了一次cas将值改回了A。此时线程1做cas发现内存中还是A,则线程1操作成功。这个时候实际上A值已经被其他线程改变过,这与设计思想是不符合的。
这个过程问题出在哪?
如果只在乎结果,ABA不介意B的存在,没什么问题
如果B的存在会造成影响,需要通过AtomicStampReference,加时间戳解决。
原子更新引用是啥? AtomicStampReference,使用时间戳,解决cas中出现的ABA问题。
AtomicReference使用代码演示 demo
1 2 3 4 5 6 7 8 9 10 11 12 13 public static void main (String[] args) { User z3 = new User("z3" ,18 ); User l4 = new User("l4" ,19 ); AtomicReference<User> atomicReference = new AtomicReference<>(z3); System.out.println(atomicReference.compareAndSet(z3, l4) + "\t" + atomicReference.get().toString()); System.out.println(atomicReference.compareAndSet(z3, l4) + "\t" + atomicReference.get().toString()); }
AtomicReference存在ABA问题代码验证 demo
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 AtomicReference atomicReference = new AtomicReference<Integer>(100 ); public static void main (String[] args) { ABADemo abaDemo = new ABADemo(); new Thread(()->{ abaDemo.atomicReference.compareAndSet(100 ,101 ); abaDemo.atomicReference.compareAndSet(101 ,100 ); },"1" ).start(); new Thread(()->{ try {TimeUnit.SECONDS.sleep(1 );} catch (InterruptedException e) { e.printStackTrace();} System.out.println(abaDemo.atomicReference.compareAndSet(100 , 2020 )+"\t" +abaDemo.atomicReference.get()); },"2" ).start(); }
AtomicStampReference解决ABA问题代码验证 解决思路:每次变量更新的时候,把变量的版本号加一,这样只要变量被某一个线程修改过,该变量版本号就会发生递增操作,从而解决了ABA变化
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 AtomicStampedReference atomicStampedReference = new AtomicStampedReference<Integer>(100 ,1 ); public static void main (String[] args) { ABADemo abaDemo = new ABADemo(); new Thread(()->{ try {TimeUnit.SECONDS.sleep(1 );} catch (InterruptedException e) { e.printStackTrace();} System.out.println("线程1在ABA前的版本号:" +abaDemo.atomicStampedReference.getStamp()); abaDemo.atomicStampedReference.compareAndSet(100 ,101 ,abaDemo.atomicStampedReference.getStamp(),abaDemo.atomicStampedReference.getStamp()+1 ); abaDemo.atomicStampedReference.compareAndSet(101 ,100 ,abaDemo.atomicStampedReference.getStamp(),abaDemo.atomicStampedReference.getStamp()+1 ); System.out.println("线程1在ABA后的版本号:" +abaDemo.atomicStampedReference.getStamp()); },"1" ).start(); new Thread(()->{ int stamp = abaDemo.atomicStampedReference.getStamp(); System.out.println("线程2在修改操作前的版本号:" +stamp); try {TimeUnit.SECONDS.sleep(2 );} catch (InterruptedException e) { e.printStackTrace();} System.out.println(abaDemo.atomicStampedReference.compareAndSet(100 ,2020 ,stamp,abaDemo.atomicStampedReference.getStamp()+1 )+ "\t" + abaDemo.atomicStampedReference.getReference()); },"2" ).start(); }
集合类不安全 ArrayList线程不安全演示-什么故障?什么原因?怎么解决? ArrayList底层是一个数组,默认大小10,超过就扩容,扩原值的一半10+5=15
线程不安全,因为add方法没有加锁。
不安全案例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 public class ContainerNotSafeDemo { public static void main (String[] args) { List<String> list = Collections.synchronizedList(new ArrayList<>()); for (int i = 1 ; i <= 30 ; i++) { new Thread(()->{ list.add(UUID.randomUUID().toString().substring(0 ,4 )); System.out.println(Thread.currentThread().getName()+"\t" +list); },String.valueOf(i)).start(); } } }
CopyOnWriteArrayList原理?它有什么好? 参考:https://blog.csdn.net/linsongbin1/article/details/54581787
add源码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 public boolean add (E e) { final ReentrantLock lock = this .lock; lock.lock(); try { Object[] elements = getArray(); int len = elements.length; Object[] newElements = Arrays.copyOf(elements, len + 1 ); newElements[len] = e; setArray(newElements); return true ; } finally { lock.unlock(); } }
解释写时复制:
写操作时,先从原有的数组中拷贝一份出来,然后在新的数组做写操作,写完之后,再将原来的数组引用指向到新数组。整个add操作都是在锁的保护下进行的 。
读操作时,如果写完成且引用指向新数组,则读到的是最新数据;否则读到的是原始数组数据。可见读操作是不加锁的 。
CopyOnWriteArrayList 缺点&使用场合
消耗内存 。写操作,拷贝数组,消耗内存,数组大的话可能导致gc
不能实时读 。拷贝新增需要时间,读到的可能是旧数据,能保证最终一致性,但不满足实时要求。
因此,适合读多写少 的场景。
CopyOnWriteArrayList透露的思想
读写分离,提高并发
最终一致性
通过另辟空间,来解决并发冲突
集合类不安全之Set-演示/故障/解决 Set同理
HashSet > Collections.synchronizedSet() > CopyOnWriteArraySet
且CopyOnWriteArraySet底层还是用的CopyOnWriteArrayList
HashSet底层是HashMap, add(key,一个常量)
集合类不安全之Map-演示/故障/解决 Map类似
HashMap > Collections.synchronizedMap() > ConcurrentHashMap
HashMap底层实现原理-jdk7 可参考:
map的存储结构是什么?
Hash表(数组+链表)
key-value构成一个entry对象
数组容量:默认16,始终保持 2^n(为了存取高效,减少碰撞,数据分配均匀)
new HashMap<>()底层发生了什么?
创建了长度=16的 Entry table。
源码分析
capacity : 当前数组容量
loadFactor:负载因子,默认为 0.75。
threshold:扩容的阈值,等于 capacity * loadFactor。当元素个数超过这个值就触发扩容。
MAXIMUM_CAPACITY = 1 << 30:最大的容量为 2 ^ 30
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 public HashMap () { this (DEFAULT_INITIAL_CAPACITY, DEFAULT_LOAD_FACTOR); } public HashMap (int initialCapacity, float loadFactor) { if (initialCapacity < 0 ) throw new IllegalArgumentException("Illegal initial capacity: " + initialCapacity); if (initialCapacity > MAXIMUM_CAPACITY) initialCapacity = MAXIMUM_CAPACITY; if (loadFactor <= 0 || Float.isNaN(loadFactor)) throw new IllegalArgumentException("Illegal load factor: " + loadFactor); this .loadFactor = loadFactor; threshold = initialCapacity; init(); }
map.put(key1,value1)底层发生了什么?
求key1的hash值,调用hashCode(),该hash值用来计算Entry数组下标,得到存放位置。
如果该位置为空,则添加成功。
如果不为空,意味着该位置存在链表:
如果hash值与其他key不同,则添加成功。
如果key1的hash值有key与之相同,则调用equal(),继续比较:
如果equal返回false,则添加成功。
如果equal返回true,则使用value1替换旧值(修改操作)
map.get(key1)底层发生了什么?
根据key1计算hash,找到对应数组下标。
遍历该位置处的链表,找到key1.equal(key2)为true的entry,返回其value。
扩容的原理?
扩容后,数组大小为原来的 2 倍。
ConcurrentHashMap底层原理-jdk7 关键词:Segment数组/基于分段锁/提高并发
引入一个Segment数组。每个Segment单元都包含一个与HashMap结构差不多hash表
读取过程:
先取key的hash值,取高sshift位决定属于哪个Segment单元。
接着就是HashMap那一套
Segment继承jucReetrantLock,上锁方便,即分段锁。因此segment[1]锁了,不影响其他Segment单元并发。
HashMap底层实现原理-jdk8 与jdk7的不同的地方:
new HashMap()时,不创建长度为16的数组。
底层使用Node[], 而不是Entry[]
数组结构采用
数组+链表+红黑树
触发时机:当某索引位置链表长度>8 且 数组长度>64时,次索引位置的链表改为红黑树
红黑树的关键性质: 从根到叶子最长的可能路径不多于最短的可能路径的两倍长。结果是这棵树大致上是平衡的。
目的:加快检索速度
! ConcurrentHashMap底层原理-jdk8 参考:https://www.jianshu.com/p/5dbaa6707017
底层结构
和 1.8 HashMap 结构类似,也是数组+链表+红黑树的。取消了Segment 分段锁
那如何保证线程安全的?
CAS + synchronized + volatile 来保证并发安全性 ,具体的如下
put方法逻辑
源代码解析:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 final V putVal (K key, V value, boolean onlyIfAbsent) { if (key == null || value == null ) throw new NullPointerException(); int hash = spread(key.hashCode()); int binCount = 0 ; for (Node<K,V>[] tab = table;;) { Node<K,V> f; int n, i, fh; if (tab == null || (n = tab.length) == 0 ) tab = initTable(); else if ((f = tabAt(tab, i = (n - 1 ) & hash)) == null ) { if (casTabAt(tab, i, null , new Node<K,V>(hash, key, value, null ))) break ; } else if ((fh = f.hash) == MOVED) tab = helpTransfer(tab, f); else { V oldVal = null ; synchronized (f) { if (tabAt(tab, i) == f) { if (fh >= 0 ) { binCount = 1 ; for (Node<K,V> e = f;; ++binCount) { K ek; if (e.hash == hash && ((ek = e.key) == key || (ek != null && key.equals(ek)))) { oldVal = e.val; if (!onlyIfAbsent) e.val = value; break ; } Node<K,V> pred = e; if ((e = e.next) == null ) { pred.next = new Node<K,V>(hash, key, value, null ); break ; } } } else if (f instanceof TreeBin) { Node<K,V> p; binCount = 2 ; if ((p = ((TreeBin<K,V>)f).putTreeVal(hash, key, value)) != null ) { oldVal = p.val; if (!onlyIfAbsent) p.val = value; } } } } if (binCount != 0 ) { if (binCount >= TREEIFY_THRESHOLD) treeifyBin(tab, i); if (oldVal != null ) return oldVal; break ; } } } addCount(1L , binCount); return null ; }
解析:对当前的table进行无条件自循环直到put成功(cas自旋) 。
如果数组下标没有Node节点,就用CAS+自旋 添加链表头节点。
如果有Node节点,就加synchronized,添加链表或红黑树节点。
get操作,由于数组被volatile修饰了,因此不用担心数组的可见性问题。
1 volatile Node<K,V>[] table;
Map类的各种对比
HashMap和ConcurrentHashMap对比
HashMap和HashTable的对比
HashTable和ConcurrentHashMap对比
Java锁 Java 15锁,列举一些? 1.公平锁 / 非公平锁 2.可重入锁 / 不可重入锁 3.独享锁 / 共享锁 4.互斥锁 / 读写锁 5.乐观锁 / 悲观锁 6.分段锁 7.偏向锁 / 轻量级锁 / 重量级锁 8.自旋锁
公平和非公平锁是什么?两者区别(优缺点)?两种锁举例? 是什么?
公平锁:多个线程按照申请锁的顺序来获取锁。
非公平锁:多个线程获取锁的顺序不是按照申请舒顺序来的,有可能后申请的线程比先申请的线程优先获取锁。高并发下,有可能造成优先级反转或者饥饿现象。
区别:
公平锁:保证顺序(队列,FIFO),性能下降。
非公平:先尝试直接占有锁,如果尝试失败,再采用类似公平锁的方式。优点在于吞吐量比公平锁大。
举例:
ReentrantLock可以指定创建公平锁或非公平锁,无参构造默认创建非公平锁。
synchronized是非公平的。
可重入锁是什么?与不可重入的区别?可重入锁举例?作用?实现原理? 是什么?
也叫递归锁
当一个线程获取某个对象锁后,可以再次获取同一把对象锁。
即,线程可进入任何他所拥有的对象锁所同步着的代码块。
区别?
1 2 3 public synchronized void m1 () { m1(); }
可重入锁:某线程进入外层m1后,可以再次进入递归m1方法。也叫递归锁。
不可重入锁:某线程进入外部m1后,不可再进如内部m1,必须等待锁释放。这里就造成了死锁。
举例:
ReentrantLock/synchronized就是典型的可重入锁
作用:
实现原理:
计数器:进入最外层计数器=1,每递归一次,计数器+1,每退出一层,计数器-1,直到计数器=0,说明退出了最外层,此时该线程释放锁对象,其他线程才能获取该锁。
可重入锁代码验证 synchronized
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 public class SynchronizedLockDemo { public synchronized void m1 () { System.out.println(Thread.currentThread().getName() + "----m1----" ); m2(); } public synchronized void m2 () { System.out.println(Thread.currentThread().getName() + "----m2----" ); } public static void main (String[] args) { SynchronizedLockDemo rd = new SynchronizedLockDemo(); for (int i = 0 ; i < 5 ; i++) { new Thread(()->{ rd.m1(); },String.valueOf(i)).start(); } } }
ReentrantLock
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 public class ReentrantLockDemo { Lock lock = new ReentrantLock(); public void m1 () { lock.lock(); try { System.out.println(Thread.currentThread().getName() + "----m1----" ); m2(); }finally { lock.unlock(); } } public void m2 () { lock.lock(); try { System.out.println(Thread.currentThread().getName() + "----m2----" ); }finally { lock.unlock(); } } public static void main (String[] args) { ReentrantLockDemo rd = new ReentrantLockDemo(); for (int i = 0 ; i < 5 ; i++) { new Thread(()->{ rd.m1(); },String.valueOf(i)).start(); } } }
自旋锁是什么?优点?缺点?
自旋锁(spinlock)是指尝试获取锁的对象不会立即阻塞,而是采用循环的方式取尝试获取锁。好处是减少线程上下文切换的消耗,缺点是循环会消耗CPU
手写一个自旋锁 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 public class SpinLockDemo { public static void main (String[] args) { SpinLockDemo sDemo = new SpinLockDemo(); new Thread(()->{ sDemo.myLock(); System.out.println(Thread.currentThread().getName()+"一顿操作5秒..." ); try {TimeUnit.SECONDS.sleep(5 );} catch (InterruptedException e) {e.printStackTrace();} sDemo.myUnLock(); },"A" ).start(); try {TimeUnit.SECONDS.sleep(1 );} catch (InterruptedException e) {e.printStackTrace();} new Thread(()->{ sDemo.myLock(); System.out.println(Thread.currentThread().getName()+"一顿操作" ); sDemo.myUnLock(); },"B" ).start(); } AtomicReference atomicThreadRef = new AtomicReference<Thread>(); public void myLock () { System.out.println(Thread.currentThread().getName()+"尝试获取锁" ); while (!atomicThreadRef.compareAndSet(null ,Thread.currentThread())){ } System.out.println(Thread.currentThread().getName()+"成功获取锁" ); } public void myUnLock () { atomicThreadRef.compareAndSet(Thread.currentThread(),null ); System.out.println(Thread.currentThread().getName()+"释放锁" ); } }
独占锁和共享锁是什么?举例?优缺点比较? 是什么?
独占锁:写锁,该锁只能被一个线程所持有。
共享锁:读锁,该锁可被多个线程所持有。
举例
ReentrantLock和sychronized都是独占锁。
ReentrantReadWriteLock,其读锁是共享锁,写锁是独占锁。
优缺:
共享锁保证并发读是非常高效的;读写、写读、写写过程是互斥的。
验证读写锁ReentrantReadWriteLock 并发读写不安全演示
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 public class ReadWriteUnsafeDemo { Map<Integer,String> map = new HashMap<>(); public void myPut (Integer key, String value) { System.out.println(Thread.currentThread().getName() + "\t" + "正在写入:" +key); map.put(key ,value); System.out.println(Thread.currentThread().getName() + "\t" + "写入完成:" +key); } public void myGet (Integer key) { System.out.println(Thread.currentThread().getName() + "\t" + "正在读取:" +key); String value = map.get(key); System.out.println(Thread.currentThread().getName() + "\t" + "读取完成:" +key+"," +value); } public static void main (String[] args) { ReadWriteUnsafeDemo demo = new ReadWriteUnsafeDemo(); for (int i=0 ; i<5 ; ++i){ final int tmp = i; new Thread(()->{ demo.myPut(tmp,String.valueOf(tmp)); },String.valueOf(i)).start(); } for (int i=0 ; i<5 ; ++i){ final int tmp = i; new Thread(()->{ demo.myGet(tmp); },String.valueOf(i)).start(); } } }
验证读写锁ReentrantReadWriteLock
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 public class ReadWriteLockDemo { volatile Map<Integer,String> map = new HashMap<>(); ReadWriteLock rwLock = new ReentrantReadWriteLock(); public void myPut (Integer key, String value) { rwLock.writeLock().lock(); try { System.out.println(Thread.currentThread().getName() + "\t" + "正在写入" ); map.put(key ,value); System.out.println(Thread.currentThread().getName() + "\t" + "写入完成:" +key); }catch (Exception e){ e.printStackTrace(); }finally { rwLock.writeLock().unlock(); } } public void myGet (Integer key) { rwLock.readLock().lock(); try { System.out.println(Thread.currentThread().getName() + "\t" + "正在读取" ); String value = map.get(key); System.out.println(Thread.currentThread().getName() + "\t" + "读取完成:" +value); }catch (Exception e){ e.printStackTrace(); }finally { rwLock.readLock().unlock(); } } public static void main (String[] args) { ReadWriteLockDemo demo = new ReadWriteLockDemo(); for (int i=0 ; i<5 ; ++i){ final int tmp = i; new Thread(()->{ demo.myPut(tmp,String.valueOf(tmp)); },String.valueOf(i)).start(); } for (int i=0 ; i<5 ; ++i){ final int tmp = i; new Thread(()->{ demo.myGet(tmp); },String.valueOf(i)).start(); } } }
什么是乐观锁/悲观锁?举例? 悲观锁
总是假设最坏的情况,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,Java中synchronized和ReentrantLock等独占锁就是悲观锁思想的实现 。
乐观锁
总是假设最好的情况,每次去拿数据的时候都认为别人不会修改,所以不会上锁,采用版本号+cas的方式去保证线程安全。乐观锁适用于多读写少的应用类型,这样可以提高吞吐量。atomic包的类就是基于cas实现乐观锁的。
什么是乐观读/悲观读?
悲观读:在没有任何读写锁的时候才能取得写入的锁,可用于实现悲观读取(读优先,没有读时才能写),读多写少的场景下可能会出现线程饥饿。
乐观读:如果读多写少,就乐观的认为读写同时发生的情况少,因此不采用完全锁定的方式,而是采用cas实现乐观锁。
ReentrantReadWriteLock是乐观读还是悲观读? 读优先:在没有任何读写锁的时候才能取得写入的锁,可用于实现悲观读取(读优先,没有读时才能写),读多写少的场景下可能会出现线程饥饿。
StempedLock作用? 参考:blog/2018/10/27/20181027234153307/#7-5-ReentrantLock与锁
它控制锁有三种模式(写、悲观读、乐观读)。
核心代码:
1 2 3 4 5 private final StampedLock sl = new StampedLock();long stamp = sl.tryOptimisticRead(); long stamp = sl.readLock(); long stamp = lock.writeLock();