1. java 内存模型java Memory Model(JMM)
JMM定义了Java 虚拟机(JVM)在计算机内存(RAM)中的工作方式
JMM是隶属于JVM
0内存屏障
Memory Barrier(Memory Fence)
1.1 原子性
解决:加锁
1.2 可见性
解决:volatile。避免线程从自己的工作缓存中查找变量的值,必须到主存中获取它的值
1.3 有序性
指令重排,是 JIT 编译器在运行时的一些优化
1)解决:volatile 修饰的变量,可以禁用指令重排
2、volatile
1)保证可见性和禁止指令重排。不保证原子性。
2)原理
对 volatile 变量的写指令后会加入写屏障
对 volatile 变量的读指令前会加入读屏障
3)应用
双重检查单例加上volatile 修饰
public final class Singleton {
private Singleton() { }
private static volatile Singleton INSTANCE = null;
public static Singleton getInstance() {
// 实例没创建,才会进入内部的 synchronized代码块
if (INSTANCE == null) {
synchronized (Singleton.class) { // t2
// 也许有其它线程已经创建实例,所以再判断一次
if (INSTANCE == null) { // t1
INSTANCE = new Singleton();
}
}
}
return INSTANCE;
}
}
3、CAS
3.1CAS
1)定义CAS 即 Compare and Swap ,它体现的一种乐观锁的思想。
2)场景:
结合 CAS 和 volatile 可以实现无锁并发,适用于竞争不激烈、多核 CPU 的场景下。
因为没有使用 synchronized,所以线程不会陷入阻塞,这是效率提升的因素之一
但如果竞争激烈,可以想到重试必然频繁发生,反而效率会受影响
3)原理
CAS 底层依赖于一个 Unsafe 类来直接调用操作系统底层的 CAS 指令(unsafe.compareAndSwapInt)
cas线程获取的值和共享变量的值是否一样(工作内存和主内存),一样才操作.compareAndSet。
这个只比较了是否一致,但是不能判断是否被修改过(ABA问题)–P120
3.2乐观锁和悲观锁
1)乐观锁(CAS)
最乐观的估计,不怕别的线程来修改共享变量,就算改了也没关系,我吃亏点再重试呗。
2)悲观锁(synchronized)
得防着其它线程来修改共享变量,我上了锁你们都别想改,我改完了解开锁,你们才有机会。
4、原子操作类
juc(java.util.concurrent)中提供了原子操作类,可以提供线程安全的操作
例如:AtomicInteger、AtomicBoolean等,它们底层就是采用 CAS 技术 + volatile 来实现的
5、公平锁、非公平锁、可重入锁、递归锁、自旋锁
1、公平锁、非公平锁
公平锁就是先来后到、非公平锁就是允许加塞,Lock lock = new ReentrantLock(Boolean fair);
默认非公平。
-
**公平锁**是指多个线程按照申请锁的顺序来获取锁,类似排队打饭。
-
**非公平锁**是指多个线程获取锁的顺序并不是按照申请锁的顺序,有可能后申请的线程优先获取锁,在高并发的情况下,有可能会造成优先级反转或者节现象。
2、可重入(递归锁)
-
递归锁是什么
指的时同一线程外层函数获得锁之后,内层递归函数仍然能获取该锁的代码,在同一个线程在外层方法获取锁的时候,在进入内层方法会自动获取锁,也就是说,线程可以进入任何一个它已经拥有的锁所同步着的代码块
-
ReentrantLock/Synchronized 就是一个典型的可重入锁
-
可重入锁最大的作用是避免死锁
-
代码示例
package com.jian8.juc.lock; #### public static void main(String[] args) { Phone phone = new Phone(); new Thread(() -> { try { phone.sendSMS(); } catch (Exception e) { e.printStackTrace(); } }, "Thread 1").start(); new Thread(() -> { try { phone.sendSMS(); } catch (Exception e) { e.printStackTrace(); } }, "Thread 2").start(); } } class Phone{ public synchronized void sendSMS()throws Exception{ System.out.println(Thread.currentThread().getName()+"\t -----invoked sendSMS()"); Thread.sleep(3000); sendEmail(); } public synchronized void sendEmail() throws Exception{ System.out.println(Thread.currentThread().getName()+"\t +++++invoked sendEmail()"); } }
package com.jian8.juc.lock; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; public class ReentrantLockDemo { public static void main(String[] args) { Mobile mobile = new Mobile(); new Thread(mobile).start(); new Thread(mobile).start(); } } class Mobile implements Runnable{ Lock lock = new ReentrantLock(); @Override public void run() { get(); } public void get() { lock.lock(); try { System.out.println(Thread.currentThread().getName()+"\t invoked get()"); set(); }finally { lock.unlock(); } } public void set(){ lock.lock(); try{ System.out.println(Thread.currentThread().getName()+"\t invoked set()"); }finally { lock.unlock(); } } }
3、独占锁(写锁)/共享锁(读锁)/互斥锁
-
概念
-
独占锁:指该锁一次只能被一个线程所持有,对ReentrantLock和Synchronized而言都是独占锁
-
共享锁:只该锁可被多个线程所持有
ReentrantReadWriteLock其读锁是共享锁,写锁是独占锁
-
互斥锁:读锁的共享锁可以保证并发读是非常高效的,读写、写读、写写的过程是互斥的
-
-
代码示例
package com.jian8.juc.lock; import java.util.HashMap; import java.util.Map; import java.util.concurrent.TimeUnit; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantReadWriteLock; /** * 多个线程同时读一个资源类没有任何问题,所以为了满足并发量,读取共享资源应该可以同时进行。 * 但是 如果有一个线程象取写共享资源来,就不应该自由其他线程可以对资源进行读或写 * 总结 * 读读能共存 * 读写不能共存 * 写写不能共存 */ public class ReadWriteLockDemo { public static void main(String[] args) { MyCache myCache = new MyCache(); for (int i = 1; i <= 5; i++) { final int tempInt = i; new Thread(() -> { myCache.put(tempInt + "", tempInt + ""); }, "Thread " + i).start(); } for (int i = 1; i <= 5; i++) { final int tempInt = i; new Thread(() -> { myCache.get(tempInt + ""); }, "Thread " + i).start(); } } } class MyCache { private volatile Map<String, Object> map = new HashMap<>(); private ReentrantReadWriteLock rwLock = new ReentrantReadWriteLock(); /** * 写操作:原子+独占 * 整个过程必须是一个完整的统一体,中间不许被分割,不许被打断 * * @param key * @param value */ public void put(String key, Object value) { rwLock.writeLock().lock(); try { System.out.println(Thread.currentThread().getName() + "\t正在写入:" + key); TimeUnit.MILLISECONDS.sleep(300); map.put(key, value); System.out.println(Thread.currentThread().getName() + "\t写入完成"); } catch (Exception e) { e.printStackTrace(); } finally { rwLock.writeLock().unlock(); } } public void get(String key) { rwLock.readLock().lock(); try { System.out.println(Thread.currentThread().getName() + "\t正在读取:" + key); TimeUnit.MILLISECONDS.sleep(300); Object result = map.get(key); System.out.println(Thread.currentThread().getName() + "\t读取完成: " + result); } catch (Exception e) { e.printStackTrace(); } finally { rwLock.readLock().unlock(); } } public void clear() { map.clear(); } }
4、自旋锁
-
spinlock
是指尝试获取锁的线程不会立即阻塞,而是采用循环的方式去尝试获取锁,这样的好处是减少线程上下文切换的消耗,缺点是循环会消耗CPU
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; }
手写自旋锁:
package com.jian8.juc.lock; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicReference; /** * 实现自旋锁 * 自旋锁好处,循环比较获取知道成功位置,没有类似wait的阻塞 * * 通过CAS操作完成自旋锁,A线程先进来调用mylock方法自己持有锁5秒钟,B随后进来发现当前有线程持有锁,不是null,所以只能通过自旋等待,知道A释放锁后B随后抢到 */ public class SpinLockDemo { public static void main(String[] args) { SpinLockDemo spinLockDemo = new SpinLockDemo(); new Thread(() -> { spinLockDemo.mylock(); try { TimeUnit.SECONDS.sleep(3); }catch (Exception e){ e.printStackTrace(); } spinLockDemo.myUnlock(); }, "Thread 1").start(); try { TimeUnit.SECONDS.sleep(3); }catch (Exception e){ e.printStackTrace(); } new Thread(() -> { spinLockDemo.mylock(); spinLockDemo.myUnlock(); }, "Thread 2").start(); } //原子引用线程 AtomicReference<Thread> atomicReference = new AtomicReference<>(); public void mylock() { Thread thread = Thread.currentThread(); System.out.println(Thread.currentThread().getName() + "\t come in"); while (!atomicReference.compareAndSet(null, thread)) { //这里一直在循环,可以打日志显示这里线程的占用 } } public void myUnlock() { Thread thread = Thread.currentThread(); atomicReference.compareAndSet(thread, null); System.out.println(Thread.currentThread().getName()+"\t invoked myunlock()"); } }