十六、JMM
1.Volatile
- Volatile其实就是java虚拟机提供的轻量级的同步机制
- Volatile有三个特点:保证可见性,不保证原子性,禁止指令重排
保证可见性
//不加volatile程序就会死循环,可以保证可见性 private volatile static int num = 0; public static void main(String[] args) { new Thread(()->{//线程1 对内存的变化是不知道的 while (num == 0){ } }).start(); try { TimeUnit.SECONDS.sleep(2); } catch (InterruptedException e) { e.printStackTrace(); } num = 1; System.out.println(num); }
不保证原子性
原子性:即不可分割。线程A在执行任务时,不可被打扰也不可能被分割,要么同时成功,要么同时失败
public class tvolatile { //不保证原子性的验证 private volatile static int num = 0; public void add(){ num++; } public static void main(String[] args) { //理论上结果应该是20000,但其实是达不到20000的 for (int i=0;i<=20;i++){ new Thread(()->{ for (int j=0;j<1000;j++){ add(); } }).start(); } } }
禁止指令重排
什么是指令重排?程序并不是按照我们写的那样执行,编译器会先优化重排,指令并行也可能会重排,内存系统也会重排。处理器在进行指令重排的时候,会考虑数据之间的依赖性
指令重排可能会造成影响的结果,对于多线程的情况,可能会出现影响,解决的方式就是Volatile
Volatile可以避免指令重排:(1)内存屏障:就是一个CPU指令,可以保证特定的操作顺序,可以保证某些变量的内存可见性,利用这些特性就可以使Volatile实现了可见性;(2)Volatile可以保证可见性,但是不能保证原子性,由于内存屏障,可以保证避免指令重排的现象发生
2.JMM是什么
JMM是一种规定,一种概念,即java内存模型。
关于JMM有一些同步的约定:
- 线程解锁前,必须把共享变量立刻刷回主存中
- 线程加锁钱,必须读取主存中的最新值到工作内存中
- 加锁和解锁是同一把锁
3.JMM的8种操作
Read(读取):作用于主内存变量,它把一个变量的值从主内存传输到线程的工作内存中,以便随后的load动作使用;
load(载入):作用于工作内存的变量,它把read操作从主存中变量放入工作内存中;
Use(使用):作用于工作内存中的变量,它把工作内存中的变量传输给执行引擎,每当虚拟机遇到一个需要使用到变量的值,就会使用到这个指令;
assign(赋值):作用于工作内存中的变量,它把一个从执行引擎中接受到的值放入工作内存的变量副本中;
store(存储):作用于主内存中的变量,它把一个从工作内存中一个变量的值传送到主内存中,以便后续的write使用;
write(写入):作用于主内存中的变量,它把store操作从工作内存中得到的变量的值放入主内存的变量中;
lock(锁定):作用于主内存的变量,把一个变量标识为线程独占状态;
unlock(解锁):作用于主内存的变量,它把一个处于锁定状态的变量释放出来,释放后的变量才可以被其他线程锁定;
4.JMM的8种操作的相关规定
不允许read和load、store和write操作之一单独出现。即使用了read必须load,使用了store必须write
不允许线程丢弃他最近的assign操作,即工作变量的数据改变了之后,必须告知主存
不允许一个线程将没有assign的数据从工作内存同步回主内存
一个新的变量必须在主内存中诞生,不允许工作内存直接使用一个未被初始化的变量。就是对变量实施use、store操作之前,必须经过assign和load操作
一个变量同一时间只有一个线程能对其进行lock。多次lock后,必须执行相同次数的unlock才能解锁
如果对一个变量进行lock操作,会清空所有工作内存中此变量的值,在执行引擎使用这个变量前,必须重新load或assign操作初始化变量的值
如果一个变量没有被lock,就不能对其进行unlock操作。也不能unlock一个被其他线程锁住的变量
对一个变量进行unlock操作之前,必须把此变量同步回主内存
十七、单例模式
单例模式(Singleton Pattern)是 Java 中最简单的设计模式之一。这种模式涉及到一个单一的类,该类负责创建自己的对象,同时确保只有单个对象被创建。这个类提供了一种访问其唯一的对象的方式,可以直接访问,不需要实例化该类的对象。
1.饿汉式单例模式
/** * 饿汉式单例模式,一加载就创建单例对象 */ public class Hungry { private Hungry(){ } private static final Hungry hungry = new Hungry(); public static Hungry getInstance(){ return hungry; } }
2.懒汉式单例模式
/** * 懒汉式单例模式 */ public class LazyMan { private LazyMan() { System.out.println(Thread.currentThread().getName()); } private static LazyMan lazyMan; public static LazyMan getInstance(){ if (lazyMan==null){ lazyMan = new LazyMan(); } return lazyMan; } // 多线程并发创建对象存在问题。 public static void main(String[] args) { for (int i = 0; i < 10; i++) { new Thread(()->{ LazyMan.getInstance(); }).start(); } } } // 打印 // Thread-1 // Thread-0 // Thread-4 // Thread-2
3.双重检测锁单例模式DCL
- 相对比较安全创建单例的模式,但是反射可以破坏
/** * 懒汉式单例模式 */ public class LazyMan01 { private LazyMan01(){ System.out.println(Thread.currentThread().getName()); } // 加上volatile关键字禁止指令重排 private volatile static LazyMan01 lazyMan01; /** * 双重检测锁模式,DCL懒汉式 * @return */ public static LazyMan01 getInstance(){ if (lazyMan01==null){ synchronized(LazyMan01.class){ if (lazyMan01==null){ lazyMan01 = new LazyMan01();// 不是原子性操作,可能会有指令重排 /* * 1.分配内存空间 * 2.执行构造器,初始化对象 * 3.把对象指向内存空间 * 正常情况下:按照123执行;发生指令重排,可能会先执行132 * 多线程情况下:如果第一个线程执行了13,此时第二个线程过来可能就会判断lazyMan01不为空,直接就返回了lazyMan01 * 此时,lazyMan01对象内存空是空的。 * */ } } } return lazyMan01; } public static void main(String[] args) { for (int i = 0; i < 10; i++) { new Thread(()->{ LazyMan01.getInstance(); }).start(); } } }
4.静态内部类创建单例模式
/** * 静态内部类创建单例模式 */ public class LazyMan02 { private LazyMan02(){ } public static LazyMan02 getInstance(){ return InnerClass.lazyMan02; } static class InnerClass{ private static final LazyMan02 lazyMan02 = new LazyMan02(); } }
5.枚举创建单例
public enum EnumSingle { INSTANCE; public static EnumSingle getInstance(){ return INSTANCE; } }
十八、CAS详解
1.什么是CAS
如果构造lazy时不加volatile,那么这不是一个原子性操作:他会先分配内存空间,再执行构造方法、初始化对象,然后把这个对象指向这个空间,此时lazy可能没有完成操作。
代码示例:
public class Lazy { private Lazy(){ } private volatile static Lazy lazy; //双重检测锁模式的懒汉式单例:即DCL懒汉式 public static Lazy getInstance(){ //加锁 if (lazy==null){ synchronized (Lazy.class){ if (lazy==null){ lazy = new Lazy();//如果构造lazy时不加volatile,那么这不是一个原子性操作:先分配内存空间,再执行构造方法、初始化对象,然后把这个对象指向这个空间 } } } return lazy;//此时lazy可能没有完成操作 } //多线程并发 public static void main(String[] args) { for (int i=0;i<10;i++){ new Thread(()->{ Lazy.getInstance(); }).start(); } } }
2.compareAndSet()方法
public class CASDemo { public static void main(String[] args) { AtomicInteger atomicInteger = new AtomicInteger(2020); //compareAndSet()方法中有两个参数,except:期望值、update:更新值 //如果我期望的值达到了,就更新,否则就不更新 System.out.println(atomicInteger.compareAndSet(2020,2021)); System.out.println(atomicInteger.get()); atomicInteger.getAndIncrement();//相当于number++ System.out.println(atomicInteger.compareAndSet(2020,2021)); System.out.println(atomicInteger.get()); } }
3.CAS的优点与缺点
优点:
- 比较当前工作内存中的值和主内存中的值,如果这个值是期望的,那么执行,如果不是就一直循环
缺点:
- 循环会耗时
- 一次性只能保证一个共享变量的原子性
- 会出现ABA问题
4.什么是ABA问题
public class CASDemo { public static void main(String[] args) { AtomicInteger atomicInteger = new AtomicInteger(2020); //捣乱的线程 System.out.println(atomicInteger.compareAndSet(2020,2021)); System.out.println(atomicInteger.get()); System.out.println(atomicInteger.compareAndSet(2021,2020)); System.out.println(atomicInteger.get()); //期望的线程 System.out.println(atomicInteger.compareAndSet(2020,2021)); System.out.println(atomicInteger.get()); } } // ABA问题的结果 // 打印 // true // 2021 // true // 2020 // true // 2021
5.如何解决ABA问题:原子引用
- 想解决ABA问题必须引入原子引用,对应的思想是:乐观锁
public class CASDemo02 { public static void main(String[] args) { AtomicStampedReference<Integer> atomicInteger = new AtomicStampedReference<>(1, 1); new Thread(()->{ int stamp = atomicInteger.getStamp();//获得版本号 System.out.println("a1=>"+stamp); try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(atomicInteger.compareAndSet(1, 2,atomicInteger.getStamp(),atomicInteger.getStamp()+1)); System.out.println("a2=>"+atomicInteger.getStamp()); System.out.println(atomicInteger.compareAndSet(2, 1,atomicInteger.getStamp(),atomicInteger.getStamp()+1)); System.out.println("a3=>"+atomicInteger.getStamp()); },"a").start(); new Thread(()->{ int stamp = atomicInteger.getStamp();//获得版本号 System.out.println("b1=>"+stamp); try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); } atomicInteger.compareAndSet(1,4,stamp,stamp+1); System.out.println("b2=>"+atomicInteger.getStamp()); },"b").start(); } }
十、各种锁的理解
1.公平锁与非公平锁
- 公平锁: 非常公平, 不能够插队,必须先来后到
- 非公平锁:非常不公平,可以插队 (默认都是非公平)
public ReentrantLock() { sync = new NonfairSync(); } public ReentrantLock(boolean fair) { sync = fair ? new FairSync() : new NonfairSync(); }
2.可重入锁
可重入锁(也叫递归锁)
Synchronized 版代码示例:
package com.wang.lock; // Synchronized public class Demo01 { public static void main(String[] args) { Phone phone = new Phone(); new Thread(()->{ phone.sms(); },"A").start(); new Thread(()->{ phone.sms(); },"B").start(); } } class Phone{ public synchronized void sms(){ System.out.println(Thread.currentThread().getName() + "sms"); call(); // 这里也有锁(sms锁 里面的call锁) } public synchronized void call(){ System.out.println(Thread.currentThread().getName() + "call"); } }
Lock 版代码示例:
package com.wang.lock; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; public class Demo02 { public static void main(String[] args) { Phone2 phone = new Phone2(); new Thread(()->{ phone.sms(); },"A").start(); new Thread(()->{ phone.sms(); },"B").start(); } } class Phone2{ Lock lock = new ReentrantLock(); public void sms(){ lock.lock(); // 细节问题:lock.lock(); lock.unlock(); // lock 锁必须配对,否则就会死在里面 // 两个lock() 就需要两次解锁 lock.lock(); try { System.out.println(Thread.currentThread().getName() + "sms"); call(); // 这里也有锁 } catch (Exception e) { e.printStackTrace(); } finally { lock.unlock(); lock.unlock(); } } public void call(){ lock.lock(); try { System.out.println(Thread.currentThread().getName() + "call"); } catch (Exception e) { e.printStackTrace(); } finally { lock.unlock(); } } }
3.自旋锁
spinlock又称自旋锁,是为实现保护共享资源而提出的一种轻量级锁机制。
自旋锁与互斥锁比较类似,都是为了解决对某项资源的互斥使用。无论是互斥锁,还是自旋锁,在任何时刻,最多只能有一个持有者,即只能有一个执行单元获得锁。但是两者在调度机制上略有不同。对于互斥锁,如果资源已经被占用,资源申请者只能进入睡眠状态。但是自旋锁不会引起调用者睡眠,而是一直循环在那里看是否该自旋锁的持有者已经释放了锁,"自旋"一词就是因此而得名。锁一旦被释放,就会被等待的线程立即获取,而不需要经过唤醒和上下文切换。
自定义一个锁:
package com.wang.lock; import java.util.concurrent.atomic.AtomicReference; /** - 自旋锁 */ public class SpinlockDemo { // int 0 // Thread null // 原子引用 AtomicReference<Thread> atomicReference = new AtomicReference<>(); // 加锁 public void myLock(){ Thread thread = Thread.currentThread(); System.out.println(Thread.currentThread().getName() + "==> mylock"); // 自旋锁 while (!atomicReference.compareAndSet(null,thread)){ } } // 解锁 // 加锁 public void myUnLock(){ Thread thread = Thread.currentThread(); System.out.println(Thread.currentThread().getName() + "==> myUnlock"); atomicReference.compareAndSet(thread,null);// 解锁 } }
对锁的测试:
package com.haust.lock; import java.util.concurrent.TimeUnit; public class TestSpinLock { public static void main(String[] args) throws InterruptedException { // ReentrantLock reentrantLock = new ReentrantLock(); // reentrantLock.lock(); // reentrantLock.unlock(); // 底层使用的自旋锁CAS SpinlockDemo lock = new SpinlockDemo();// 定义锁 new Thread(()-> { lock.myLock();// 加锁 try { TimeUnit.SECONDS.sleep(5); } catch (Exception e) { e.printStackTrace(); } finally { lock.myUnLock();// 解锁 } },"T1").start(); TimeUnit.SECONDS.sleep(1); new Thread(()-> { lock.myLock(); try { TimeUnit.SECONDS.sleep(1); } catch (Exception e) { e.printStackTrace(); } finally { lock.myUnLock(); } },"T2").start(); } }
测试结果:
4.死锁
定义
- 如果一个进程集合里面的每个进程都在等待这个集合中的其他一个进程(包括自身)才能继续往下执行,若无外力他们将无法推进,这种情况就是死锁,处于死锁状态的进程称为死锁进程。
产生死锁的四个必要条件
(1)互斥条件:进程对所分配到的资源不允许其他进程进行访问,若其他进程访问该资源,只能等待,直至占有该资源的进程使用完成后释放该资源。
(2)请求和保持条件:进程获得一定的资源之后,又对其他资源发出请求,但是该资源可能被其他进程占有,此事请求阻塞,但又对自己获得的资源保持不放。
(3)不可剥夺条件:是指进程已获得的资源,在未完成使用之前,不可被剥夺,只能在使用完后自己释放。
(4)环路等待条件:是指进程发生死锁后,必然存在一个进程–资源之间的环形链。
处理死锁的基本方法
(1)预防死锁:通过设置一些限制条件,去破坏产生死锁的必要条件
(2)避免死锁:在资源分配过程中,使用某种方法避免系统进入不安全的状态,从而避免发生死锁
(3)检测死锁:允许死锁的发生,但是通过系统的检测之后,采取一些措施,将死锁清除掉
(4)解除死锁:该方法与检测死锁配合使用
后记
Java全栈学习路线可参考:【Java全栈学习路线】最全的Java学习路线及知识清单,Java自学方向指引,内含最全Java全栈学习技术清单~