8. CAS 8.1 没有CAS之前
多线程环境不使用原子类 保证线程安全(基本数据类型)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 package com.atguigu.juc.prepare;import java.util.concurrent.atomic.AtomicInteger;public class T3 { volatile int number = 0 ; public int getNumber () { return number; } public synchronized void setNumber () { number++; } }
多线程环境 使用原子类 保证线程安全(基本数据类型)
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 package com.atguigu.juc.prepare;import java.util.concurrent.atomic.AtomicInteger;public class T3 { volatile int number = 0 ; public int getNumber () { return number; } public synchronized void setNumber () { number++; } AtomicInteger atomicInteger = new AtomicInteger (); public int getAtomicInteger () { return atomicInteger.get(); } public void setAtomicInteger () { atomicInteger.getAndIncrement(); } }
8.2 是什么 compare and swap的缩写,中文翻译成比较并交换,实现并发算法时常用到的一种技术。它包含三个操作数——内存位置、预期原值及更新值。
执行CAS操作的时候,将内存位置的值与预期原值比较:
如果相匹配,那么处理器会自动将该位置值更新为新值
如果不匹配,处理器不做任何操作,多个线程同时执行CAS操作只有一个会成功。
原理 CAS (CompareAndSwap)
CAS有3个操作数,位置内存值V,旧的预期值A,要修改的更新值B。当且仅当旧的预期值A和内存值V相同时,将内存值V修改为B,否则什么都不做或重来
硬件级别保证 CAS是JDK提供的非阻塞 原子性操作,它通过硬件保证 了比较-更新的原子性。
它是非阻塞的且自身原子性,也就是说这玩意效率更高且通过硬件保证,说明这玩意更可靠。
CAS是一条CPU的原子指令(cmpxchg指令 ),不会造成所谓的数据不一致问题,Unsafe提供的CAS方法(如compareAndSwapXXX)底层实现即为CPU指令cmpxchg。
执行cmpxchg指令的时候,会判断当前系统是否为多核系统,如果是就给总线加锁,只有一个线程会对总线加锁成功,加锁成功之后会执行cas操作,也就是说CAS的原子性实际上是CPU实现的 , 其实在这一点上还是有排他锁的,只是比起用synchronized, 这里的排他时间要短的多, 所以在多线程情况下性能会比较好
CASDemo代码 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 package com.atguigu.juc.prepare;import java.util.concurrent.atomic.AtomicInteger;public class CASDemo { public static void main (String[] args) throws InterruptedException { AtomicInteger atomicInteger = new AtomicInteger (5 ); System.out.println(atomicInteger.compareAndSet(5 , 2020 )+"\t" +atomicInteger.get()); System.out.println(atomicInteger.compareAndSet(5 , 1024 )+"\t" +atomicInteger.get()); } }
源码分析compareAndSet(int expect,int update)
上面三个方法都是类似的,主要对4个参数做一下说明。
var1:表示要操作的对象
var2:表示要操作对象中属性地址的偏移量
var4:表示需要修改数据的期望的值
var5/var6:表示需要修改为的新值
引出来一个问题:UnSafe类是什么?
8.3 CAS底层原理?如果知道,谈谈你对UnSafe的理解
是CAS的核心类,由于Java方法无法直接访问底层系统,需要通过本地(native)方法来访问,Unsafe相当于一个后门,基于该类可以直接操作特定内存的数据。Unsafe类存在于sun.misc包中,其内部方法操作可以像C的指针一样直接操作内存,因为Java中CAS操作的执行依赖于Unsafe类的方法。
注意Unsafe类中的所有方法都是native修饰的,也就是说Unsafe类中的方法都直接调用操作系统底层资源执行相应任务
变量valueOffset,表示该变量值在内存中的偏移地址,因为Unsafe就是根据内存偏移地址获取数据的。
变量value用volatile修饰,保证了多线程之间的内存可见性。
8.4 原子引用 AtomicInteger原子整型,可否有其它原子类型?
AtomicReferenceDemo 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.atguigu.Interview.study.thread;import lombok.AllArgsConstructor;import lombok.Getter;import lombok.ToString;import java.util.concurrent.atomic.AtomicReference;@Getter @ToString @AllArgsConstructor class User { String userName; int age; } public class AtomicReferenceDemo { public static void main (String[] args) { User z3 = new User ("z3" ,24 ); User li4 = new User ("li4" ,26 ); AtomicReference<User> atomicReferenceUser = new AtomicReference <>(); atomicReferenceUser.set(z3); System.out.println(atomicReferenceUser.compareAndSet(z3,li4)+"\t" +atomicReferenceUser.get().toString()); System.out.println(atomicReferenceUser.compareAndSet(z3,li4)+"\t" +atomicReferenceUser.get().toString()); } }
8.5 自旋锁,借鉴CAS思想 自旋锁(spinlock)是指尝试获取锁的线程不会立即阻塞,而是采用循环的方式去尝试获取锁,当线程发现锁被占用时,会不断循环判断锁的状态,直到获取。这样的好处是减少线程上下文切换的消耗,缺点是循环会消耗CPU
OpenJDK源码里面查看下Unsafe.java
自己实现一个自旋锁SpinLockDemo 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 package com.atguigu.Interview.study.thread;import java.util.concurrent.TimeUnit;import java.util.concurrent.atomic.AtomicReference;public class SpinLockDemo { 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 myUnLock over" ); } public static void main (String[] args) { SpinLockDemo spinLockDemo = new SpinLockDemo (); new Thread (() -> { spinLockDemo.myLock(); try { TimeUnit.SECONDS.sleep( 5 ); } catch (InterruptedException e) { e.printStackTrace(); } spinLockDemo.myUnLock(); },"A" ).start(); try { TimeUnit.SECONDS.sleep( 1 ); } catch (InterruptedException e) { e.printStackTrace(); } new Thread (() -> { spinLockDemo.myLock(); spinLockDemo.myUnLock(); },"B" ).start(); } }
8.6 CAS缺点 循环时间长开销很大。
我们可以看到getAndAddInt方法执行时,有个do while
如果CAS失败,会一直进行尝试。如果CAS长时间一直不成功,可能会给CPU带来很大的开销。
引出来ABA问题??? ABA问题怎么产生的 CAS会导致“ABA问题”。
CAS算法实现一个重要前提需要取出内存中某时刻的数据并在当下时刻比较并替换,那么在这个时间差类会导致数据的变化。
比如说一个线程one从内存位置V中取出A,这时候另一个线程two也从内存中取出A,并且线程two进行了一些操作将值变成了B,然后线程two又将V位置的数据变成A,这时候线程one进行CAS操作发现内存中仍然是A,然后线程one操作成功。
尽管线程one的CAS操作成功,但是不代表这个过程就是没有问题的。
版本号时间戳原子引用
ABADemo
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 package com.atguigu.Interview.study.thread;import java.util.concurrent.atomic.AtomicInteger;import java.util.concurrent.atomic.AtomicStampedReference;public class ABADemo { static AtomicInteger atomicInteger = new AtomicInteger (100 ); static AtomicStampedReference atomicStampedReference = new AtomicStampedReference (100 ,1 ); public static void main (String[] args) { new Thread (() -> { atomicInteger.compareAndSet(100 ,101 ); atomicInteger.compareAndSet(101 ,100 ); },"t1" ).start(); new Thread (() -> { try { Thread.sleep( 500 ); } catch (InterruptedException e) { e.printStackTrace(); }; System.out.println(atomicInteger.compareAndSet(100 , 2019 )+"\t" +atomicInteger.get()); },"t2" ).start(); try { Thread.sleep( 2000 ); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("============以下是ABA问题的解决=============================" ); new Thread (() -> { int stamp = atomicStampedReference.getStamp(); System.out.println(Thread.currentThread().getName()+"\t 首次版本号:" +stamp); try { Thread.sleep( 1000 ); } catch (InterruptedException e) { e.printStackTrace(); } atomicStampedReference.compareAndSet(100 ,101 ,atomicStampedReference.getStamp(),atomicStampedReference.getStamp()+1 ); System.out.println(Thread.currentThread().getName()+"\t 2次版本号:" +atomicStampedReference.getStamp()); atomicStampedReference.compareAndSet(101 ,100 ,atomicStampedReference.getStamp(),atomicStampedReference.getStamp()+1 ); System.out.println(Thread.currentThread().getName()+"\t 3次版本号:" +atomicStampedReference.getStamp()); },"t3" ).start(); new Thread (() -> { int stamp = atomicStampedReference.getStamp(); System.out.println(Thread.currentThread().getName()+"\t 首次版本号:" +stamp); try { Thread.sleep( 3000 ); } catch (InterruptedException e) { e.printStackTrace(); } boolean result = atomicStampedReference.compareAndSet(100 ,2019 ,stamp,stamp+1 ); System.out.println(Thread.currentThread().getName()+"\t" +result+"\t" +atomicStampedReference.getReference()); },"t4" ).start(); } }
下一章介绍AtomicMarkableReference
9. 原子操作类之18罗汉增强
9.1 基本类型原子类
AtomicInteger
AtomicBoolean
AtomicLong
常用API简介
public final int get() //获取当前的值
public final int getAndSet(int newValue)//获取当前的值,并设置新的值
public final int getAndIncrement()//获取当前的值,并自增
public final int getAndDecrement() //获取当前的值,并自减
public final int getAndAdd(int delta) //获取当前的值,并加上预期的值
boolean compareAndSet(int expect, int update) //如果输入的数值等于预期值,则以原子方式将该值设置为输入值(update)
代码示例 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 package com.atguigu.juc.senior.test2;import lombok.Getter;import java.util.concurrent.CountDownLatch;import java.util.concurrent.atomic.AtomicInteger;class MyNumber { @Getter private AtomicInteger atomicInteger = new AtomicInteger (); public void addPlusPlus () { atomicInteger.incrementAndGet(); } } public class AtomicIntegerDemo { public static void main (String[] args) throws InterruptedException { MyNumber myNumber = new MyNumber (); CountDownLatch countDownLatch = new CountDownLatch (100 ); for (int i = 1 ; i <=100 ; i++) { new Thread (() -> { try { for (int j = 1 ; j <=5000 ; j++) { myNumber.addPlusPlus(); } }finally { countDownLatch.countDown(); } },String.valueOf(i)).start(); } countDownLatch.await(); System.out.println(myNumber.getAtomicInteger().get()); } }
9.2 数组类型原子类
AtomicIntegerArray
AtomicLongArray
AtomicReferenceArray
代码示例 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 package com.atguigu.juc.prepare;import java.util.concurrent.atomic.AtomicIntegerArray;public class AtomicIntegerArrayDemo { public static void main (String[] args) { AtomicIntegerArray atomicIntegerArray = new AtomicIntegerArray (new int [5 ]); for (int i = 0 ; i <atomicIntegerArray.length(); i++) { System.out.println(atomicIntegerArray.get(i)); } System.out.println(); System.out.println(); System.out.println(); int tmpInt = 0 ; tmpInt = atomicIntegerArray.getAndSet(0 ,1122 ); System.out.println(tmpInt+"\t" +atomicIntegerArray.get(0 )); atomicIntegerArray.getAndIncrement(1 ); atomicIntegerArray.getAndIncrement(1 ); tmpInt = atomicIntegerArray.getAndIncrement(1 ); System.out.println(tmpInt+"\t" +atomicIntegerArray.get(1 )); } }
9.3 引用类型原子类
AtomicReference
AtomicStampedReference
AtomicMarkableReference
AtomicReference 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.atguigu.Interview.study.thread;import lombok.AllArgsConstructor;import lombok.Getter;import lombok.ToString;import java.util.concurrent.atomic.AtomicReference;@Getter @ToString @AllArgsConstructor class User { String userName; int age; } public class AtomicReferenceDemo { public static void main (String[] args) { User z3 = new User ("z3" ,24 ); User li4 = new User ("li4" ,26 ); AtomicReference<User> atomicReferenceUser = new AtomicReference <>(); atomicReferenceUser.set(z3); System.out.println(atomicReferenceUser.compareAndSet(z3,li4)+"\t" +atomicReferenceUser.get().toString()); System.out.println(atomicReferenceUser.compareAndSet(z3,li4)+"\t" +atomicReferenceUser.get().toString()); } }
自旋锁SpinLockDemo 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 package com.atguigu.Interview.study.thread;import java.util.concurrent.TimeUnit;import java.util.concurrent.atomic.AtomicReference;public class SpinLockDemo { 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 myUnLock over" ); } public static void main (String[] args) { SpinLockDemo spinLockDemo = new SpinLockDemo (); new Thread (() -> { spinLockDemo.myLock(); try { TimeUnit.SECONDS.sleep( 5 ); } catch (InterruptedException e) { e.printStackTrace(); } spinLockDemo.myUnLock(); },"A" ).start(); try { TimeUnit.SECONDS.sleep( 1 ); } catch (InterruptedException e) { e.printStackTrace(); } new Thread (() -> { spinLockDemo.myLock(); spinLockDemo.myUnLock(); },"B" ).start(); } }
AtomicStampedReference
携带版本号的引用类型原子类,可以解决ABA问题
解决修改过几次
状态戳原子引用
代码示例 -ABADemo 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 package com.atguigu.juc.cas;import java.util.concurrent.TimeUnit;import java.util.concurrent.atomic.AtomicInteger;import java.util.concurrent.atomic.AtomicStampedReference;public class ABADemo { static AtomicInteger atomicInteger = new AtomicInteger (100 ); static AtomicStampedReference atomicStampedReference = new AtomicStampedReference (100 ,1 ); public static void main (String[] args) { abaProblem(); abaResolve(); } public static void abaResolve () { new Thread (() -> { int stamp = atomicStampedReference.getStamp(); System.out.println("t3 ----第1次stamp " +stamp); try { TimeUnit.SECONDS.sleep(1 ); } catch (InterruptedException e) { e.printStackTrace(); } atomicStampedReference.compareAndSet(100 ,101 ,stamp,stamp+1 ); System.out.println("t3 ----第2次stamp " +atomicStampedReference.getStamp()); atomicStampedReference.compareAndSet(101 ,100 ,atomicStampedReference.getStamp(),atomicStampedReference.getStamp()+1 ); System.out.println("t3 ----第3次stamp " +atomicStampedReference.getStamp()); },"t3" ).start(); new Thread (() -> { int stamp = atomicStampedReference.getStamp(); System.out.println("t4 ----第1次stamp " +stamp); try { TimeUnit.SECONDS.sleep(3 ); } catch (InterruptedException e) { e.printStackTrace(); } boolean result = atomicStampedReference.compareAndSet(100 , 20210308 , stamp, stamp + 1 ); System.out.println(Thread.currentThread().getName()+"\t" +result+"\t" +atomicStampedReference.getReference()); },"t4" ).start(); } public static void abaProblem () { new Thread (() -> { atomicInteger.compareAndSet(100 ,101 ); atomicInteger.compareAndSet(101 ,100 ); },"t1" ).start(); try { TimeUnit.MILLISECONDS.sleep(200 ); } catch (InterruptedException e) { e.printStackTrace(); } new Thread (() -> { atomicInteger.compareAndSet(100 ,20210308 ); System.out.println(atomicInteger.get()); },"t2" ).start(); } }
AtomicMarkableReference
原子更新带有标记位的引用类型对象
解决是否修改过
它的定义就是将状态戳简化为true|false
类似一次性筷子
状态戳(true/false)原子引用
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 74 75 76 77 78 79 80 81 82 83 84 85 86 87 package com.atguigu.juc.senior.inner.atomic;import java.util.concurrent.TimeUnit;import java.util.concurrent.atomic.AtomicInteger;import java.util.concurrent.atomic.AtomicMarkableReference;import java.util.concurrent.atomic.AtomicStampedReference;public class ABADemo { static AtomicInteger atomicInteger = new AtomicInteger (100 ); static AtomicStampedReference<Integer> stampedReference = new AtomicStampedReference <>(100 ,1 ); static AtomicMarkableReference<Integer> markableReference = new AtomicMarkableReference <>(100 ,false ); public static void main (String[] args) { new Thread (() -> { atomicInteger.compareAndSet(100 ,101 ); atomicInteger.compareAndSet(101 ,100 ); System.out.println(Thread.currentThread().getName()+"\t" +"update ok" ); },"t1" ).start(); new Thread (() -> { try { TimeUnit.SECONDS.sleep(1 ); } catch (InterruptedException e) { e.printStackTrace(); } atomicInteger.compareAndSet(100 ,2020 ); },"t2" ).start(); try { TimeUnit.SECONDS.sleep(2 ); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(atomicInteger.get()); System.out.println(); System.out.println(); System.out.println(); System.out.println("============以下是ABA问题的解决,让我们知道引用变量中途被更改了几次=========================" ); new Thread (() -> { System.out.println(Thread.currentThread().getName()+"\t 1次版本号" +stampedReference.getStamp()); try { TimeUnit.MILLISECONDS.sleep(200 ); } catch (InterruptedException e) { e.printStackTrace(); } stampedReference.compareAndSet(100 ,101 ,stampedReference.getStamp(),stampedReference.getStamp()+1 ); System.out.println(Thread.currentThread().getName()+"\t 2次版本号" +stampedReference.getStamp()); stampedReference.compareAndSet(101 ,100 ,stampedReference.getStamp(),stampedReference.getStamp()+1 ); System.out.println(Thread.currentThread().getName()+"\t 3次版本号" +stampedReference.getStamp()); },"t3" ).start(); new Thread (() -> { int stamp = stampedReference.getStamp(); System.out.println(Thread.currentThread().getName()+"\t =======1次版本号" +stamp); try { TimeUnit.SECONDS.sleep(2 ); } catch (InterruptedException e) { e.printStackTrace(); } boolean b = stampedReference.compareAndSet(100 , 2020 , stamp, stamp + 1 ); System.out.println(Thread.currentThread().getName()+"\t=======2次版本号" +stampedReference.getStamp()+"\t" +stampedReference.getReference()); },"t4" ).start(); System.out.println(); System.out.println(); System.out.println(); System.out.println("============AtomicMarkableReference不关心引用变量更改过几次,只关心是否更改过======================" ); new Thread (() -> { boolean marked = markableReference.isMarked(); System.out.println(Thread.currentThread().getName()+"\t 1次版本号" +marked); try { TimeUnit.MILLISECONDS.sleep(100 ); } catch (InterruptedException e) { e.printStackTrace(); } markableReference.compareAndSet(100 ,101 ,marked,!marked); System.out.println(Thread.currentThread().getName()+"\t 2次版本号" +markableReference.isMarked()); markableReference.compareAndSet(101 ,100 ,markableReference.isMarked(),!markableReference.isMarked()); System.out.println(Thread.currentThread().getName()+"\t 3次版本号" +markableReference.isMarked()); },"t5" ).start(); new Thread (() -> { boolean marked = markableReference.isMarked(); System.out.println(Thread.currentThread().getName()+"\t 1次版本号" +marked); try { TimeUnit.MILLISECONDS.sleep(100 ); } catch (InterruptedException e) { e.printStackTrace(); } markableReference.compareAndSet(100 ,2020 ,marked,!marked); System.out.println(Thread.currentThread().getName()+"\t" +markableReference.getReference()+"\t" +markableReference.isMarked()); },"t6" ).start(); } }
9.4 对象的属性修改原子类
AtomicIntegerFieldUpdater
AtomicLongFieldUpdater
AtomicReferenceFieldUpdater
使用目的 : 以一种线程安全的方式操作非线程安全对象内的某些字段
**使用要求: **因为对象的属性修改类型原子类都是抽象类,所以每次使用都必须使用静态方法newUpdater()创建一个更新器,并且需要设置想要更新的类和属性。
AtomicIntegerFieldUpdaterDemo 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.atguigu.itdachang;import java.util.concurrent.TimeUnit;import java.util.concurrent.atomic.AtomicIntegerFieldUpdater;class BankAccount { private String bankName = "CCB" ; public volatile int money = 0 ; AtomicIntegerFieldUpdater<BankAccount> accountAtomicIntegerFieldUpdater = AtomicIntegerFieldUpdater.newUpdater(BankAccount.class,"money" ); public void transferMoney (BankAccount bankAccount) { accountAtomicIntegerFieldUpdater.incrementAndGet(bankAccount); } } public class AtomicIntegerFieldUpdaterDemo { public static void main (String[] args) { BankAccount bankAccount = new BankAccount (); for (int i = 1 ; i <=1000 ; i++) { int finalI = i; new Thread (() -> { bankAccount.transferMoney(bankAccount); },String.valueOf(i)).start(); } try { TimeUnit.MILLISECONDS.sleep(500 ); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(bankAccount.money); } }
AtomicReferenceFieldUpdater 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 package com.atguigu.juc.atomics;import java.util.concurrent.TimeUnit;import java.util.concurrent.atomic.AtomicReferenceFieldUpdater;class MyVar { public volatile Boolean isInit = Boolean.FALSE; AtomicReferenceFieldUpdater<MyVar,Boolean> atomicReferenceFieldUpdater = AtomicReferenceFieldUpdater.newUpdater(MyVar.class,Boolean.class,"isInit" ); public void init (MyVar myVar) { if (atomicReferenceFieldUpdater.compareAndSet(myVar,Boolean.FALSE,Boolean.TRUE)) { System.out.println(Thread.currentThread().getName()+"\t" +"---init....." ); try { TimeUnit.SECONDS.sleep(2 ); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName()+"\t" +"---init.....over" ); }else { System.out.println(Thread.currentThread().getName()+"\t" +"------其它线程正在初始化" ); } } } public class AtomicIntegerFieldUpdaterDemo { public static void main (String[] args) throws InterruptedException { MyVar myVar = new MyVar (); for (int i = 1 ; i <=5 ; i++) { new Thread (() -> { myVar.init(myVar); },String.valueOf(i)).start(); } } }
面试官问你:你在哪里用了volatile
AtomicReferenceFieldUpdater
9.5 原子操作增强类原理深度解析
DoubleAccumulator
DoubleAdder
LongAccumulator
LongAdder
阿里要命题目
热点商品点赞计算器,点赞数加加统计,不要求实时精确
一个很大的List,里面都是int类型,如何实现加加,说说思路
点赞计数器,看看性能
入门讲解
LongAdder只能用来计算加法,且从零开始计算
LongAccumulator提供了自定义的函数操作
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 long 类型的聚合器,需要传入一个long 类型的二元操作,可以用来计算各种聚合操作,包括加乘等 package com.atguigu.juc.senior.inner.atomic;import java.util.concurrent.atomic.LongAccumulator;import java.util.concurrent.atomic.LongAdder;import java.util.function.LongBinaryOperator;public class LongAccumulatorDemo { LongAdder longAdder = new LongAdder (); public void add_LongAdder () { longAdder.increment(); } LongAccumulator longAccumulator = new LongAccumulator (new LongBinaryOperator () { @Override public long applyAsLong (long left, long right) { return left - right; } },777 ); public void add_LongAccumulator () { longAccumulator.accumulate(1 ); } public static void main (String[] args) { LongAccumulatorDemo demo = new LongAccumulatorDemo (); demo.add_LongAccumulator(); demo.add_LongAccumulator(); System.out.println(demo.longAccumulator.longValue()); } }
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 package com.atguigu.juc.atomics;import java.util.concurrent.atomic.LongAccumulator;import java.util.concurrent.atomic.LongAdder;public class LongAdderAPIDemo { public static void main (String[] args) { LongAdder longAdder = new LongAdder (); longAdder.increment(); longAdder.increment(); longAdder.increment(); System.out.println(longAdder.longValue()); LongAccumulator longAccumulator = new LongAccumulator ((x,y) -> x * y,2 ); longAccumulator.accumulate(1 ); longAccumulator.accumulate(2 ); longAccumulator.accumulate(3 ); System.out.println(longAccumulator.longValue()); } }
LongAdder高性能对比Code演示 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 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 package com.zzyy.study.day524;import java.util.concurrent.CountDownLatch;import java.util.concurrent.atomic.AtomicLong;import java.util.concurrent.atomic.LongAccumulator;import java.util.concurrent.atomic.LongAdder;class ClickNumberNet { int number = 0 ; public synchronized void clickBySync () { number++; } AtomicLong atomicLong = new AtomicLong (0 ); public void clickByAtomicLong () { atomicLong.incrementAndGet(); } LongAdder longAdder = new LongAdder (); public void clickByLongAdder () { longAdder.increment(); } LongAccumulator longAccumulator = new LongAccumulator ((x,y) -> x + y,0 ); public void clickByLongAccumulator () { longAccumulator.accumulate(1 ); } } public class LongAdderDemo2 { public static void main (String[] args) throws InterruptedException { ClickNumberNet clickNumberNet = new ClickNumberNet (); long startTime; long endTime; CountDownLatch countDownLatch = new CountDownLatch (50 ); CountDownLatch countDownLatch2 = new CountDownLatch (50 ); CountDownLatch countDownLatch3 = new CountDownLatch (50 ); CountDownLatch countDownLatch4 = new CountDownLatch (50 ); startTime = System.currentTimeMillis(); for (int i = 1 ; i <=50 ; i++) { new Thread (() -> { try { for (int j = 1 ; j <=100 * 10000 ; j++) { clickNumberNet.clickBySync(); } }finally { countDownLatch.countDown(); } },String.valueOf(i)).start(); } countDownLatch.await(); endTime = System.currentTimeMillis(); System.out.println("----costTime: " +(endTime - startTime) +" 毫秒" +"\t clickBySync result: " +clickNumberNet.number); startTime = System.currentTimeMillis(); for (int i = 1 ; i <=50 ; i++) { new Thread (() -> { try { for (int j = 1 ; j <=100 * 10000 ; j++) { clickNumberNet.clickByAtomicLong(); } }finally { countDownLatch2.countDown(); } },String.valueOf(i)).start(); } countDownLatch2.await(); endTime = System.currentTimeMillis(); System.out.println("----costTime: " +(endTime - startTime) +" 毫秒" +"\t clickByAtomicLong result: " +clickNumberNet.atomicLong); startTime = System.currentTimeMillis(); for (int i = 1 ; i <=50 ; i++) { new Thread (() -> { try { for (int j = 1 ; j <=100 * 10000 ; j++) { clickNumberNet.clickByLongAdder(); } }finally { countDownLatch3.countDown(); } },String.valueOf(i)).start(); } countDownLatch3.await(); endTime = System.currentTimeMillis(); System.out.println("----costTime: " +(endTime - startTime) +" 毫秒" +"\t clickByLongAdder result: " +clickNumberNet.longAdder.sum()); startTime = System.currentTimeMillis(); for (int i = 1 ; i <=50 ; i++) { new Thread (() -> { try { for (int j = 1 ; j <=100 * 10000 ; j++) { clickNumberNet.clickByLongAccumulator(); } }finally { countDownLatch4.countDown(); } },String.valueOf(i)).start(); } countDownLatch4.await(); endTime = System.currentTimeMillis(); System.out.println("----costTime: " +(endTime - startTime) +" 毫秒" +"\t clickByLongAccumulator result: " +clickNumberNet.longAccumulator.longValue()); } }
源码、原理分析
1 2 3 4 public class LongAdder extends Striped64 implements Serializable { private static final long serialVersionUID = 7249069246863182397L ; abstract class Striped64 extends Number {
原理(LongAdder为什么这么快) 官网说明和阿里要求
Striped64
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 static final int NCPU = Runtime.getRuntime().availableProcessors();transient volatile Cell[] cells;transient volatile long base;transient volatile int cellsBusy;
base:类似于AtomicLong中全局的value值。在没有竟争情况下数据直接累加到base上,或者cells扩容时,也需要将数据写入到base上
collide:表示扩容意向,false一定不会扩容,true可能会扩容。
cellsBusy:初始化cells或者扩容cells需要获取锁,0:表示无锁状态1:表示其他线程已经持有了锁
casCellsBusy():通过CAS操作修改cellsBusy的值,CAS成功代表获取锁,返回true
NCPU:当前计算机CPU数量,CelI数组扩容时会使用到
getProbe():获取当前线程的hash值
advanceProbe():重置当前线程的hash值
Cell
是 java.util.concurrent.atomic 下 Striped64 的一个内部类
LongAdder为什么这么快 LongAdder的基本思路就是分散热点 ,将value值分散到一个Cell数组 中,不同线程会命中到数组的不同槽中,各个线程只对自己槽中的那个值进行CAS操作,这样热点就被分散了,冲突的概率就小很多。如果要获取真正的long值,只要将各个槽中的变量值累加返回。
sum()会将所有Cell数组中的value和base累加作为返回值,核心的思想就是将之前AtomicLong一个value的更新压力分散到多个value中去,从而降级更新热点 。
数学表达
内部有一个base变量,一个Cell[]数组。
base变量:非竞态条件下,直接累加到该变量上
Cell[]数组:竞态条件下,累加个各个线程自己的槽Cell[i]中
源码解读深度分析 LongAdder在无竞争的情况,跟AtomicLong一样,对同一个base 进行操作,当出现竞争关系时则是采用化整为零的做法,从空间换时间,用一个数组cells,将一个value拆分进这个数组cells。多个线程需要同时对value进行操作时候,可以对线程id进行hash得到hash值,再根据hash值映射到这个数组cells的某个下标,再对该下标所对应的值进行自增操作。当所有线程操作完毕,将数组cells的所有值和无竞争值base都加起来作为最终结果。
longAdder.increment()
as表示cells 引用
b表示获取的base 值
v表示期望值
m表示cells数组的长度
a表示当前线程命中的cell单元格
add(1L)方法 条件递增,逐步解析
1.最初无竞争时只更新base
2.如果更新base失败后,首次新建一个Cell[]数组
3.当多个线程竞争同一个Cell比较激烈时,可能就要对Cell[]扩容
longAccumulate方法
long x需要增加的值,一般默认都是1.
LongBinaryOperator fn 默认传递的是null
wasUncontended竞争标识,如果是false则代表有竞争。只有cells初始化之后,并且当前线程CAS竞争修改失败,才会是false
可以理解为是线程的hash值
上述代码首先给当前线程分配一个hash值,然后进入一个for(;;)自旋,这个自旋分为三个分支:
CASE1:Cell[]数组已经初始化
CASE2:Cell[]数组未初始化(首次新建)
CASE3:Cell[]数组正在初始化中
步骤1: 刚刚要初始化Cell[]数组(首次新建)未初始化过Cell[]数组,尝试占有锁并首次初始化cells数组
如果上面条件都执行成功就会执行数组的初始化及赋值操作, Cell[] rs = new Cell[2]表示数组的长度为2,rs[h & 1] = new Cell(x) 表示创建一个新的Cell元素,value是x值,默认为1。h & 1类似于我们之前HashMap常用到的计算散列桶index的算法,通常都是hash & (table.len - 1)。同hashmap一个意思。
步骤2: 多个线程尝试CAS修改失败的线程会走到这个分支
步骤3:Cell数组不再为空且可能存在Cell数组扩容
总体代码
下面代码判断当前线程hash后指向的数据位置元素是否为空,如果为空则将Cell数据放入数组中,跳出循环。如果不空则继续循环。
重新设置锁竞争状态
说明当前线程对应的数组中有了数据,也重置过hash值,这时通过CAS操作尝试对当前数中的value值进行累加x操作,x默认为1,如果CAS成功则直接跳出循环。
判断是否超过最大扩容
跟上一步反操作,没有达到·CPU上限,还能扩
6.
使用总结 AtomicLong
线程安全,可允许一些性能损耗,要求高精度时可使用
保证精度,性能代价
AtomicLong是多个线程针对单个热点值value进行原子操作
LongAdder
当需要在高并发下有较好的性能表现,且对值的精确度要求不高时,可以使用
保证性能,精度代价
LongAdder是每个线程拥有自己的槽,各个线程一般只对自己槽中的那个值进行CAS操作
10. 聊聊ThreadLocal 10.1 ThreadLocal简介 大厂面试题
ThreadLocal中ThreadLocalMap的数据结构和关系?
ThreadLocal的key是弱引用,这是为什么?
ThreadLocal内存泄露问题你知道吗?
ThreadLocal中最后为什么要加remove方法?
是什么
稍微翻译一下:
ThreadLocal提供线程局部变量。这些变量与正常的变量不同,因为每一个线程在访问ThreadLocal实例的时候(通过其get或set方法)都有自己的、独立初始化的变量副本。ThreadLocal实例通常是类中的私有静态字段,使用它的目的是希望将状态(例如,用户ID或事务ID)与线程关联起来。
能干嘛 实现每一个线程都有自己专属的本地变量副本 (自己用自己的变量不麻烦别人,不和其他人共享,人人有份,人各一份),主要解决了让每个线程绑定自己的值,通过使用get()和set()方法,获取默认值或将其值更改为当前线程所存的副本的值从而避免了线程安全问题。
api介绍
永远的helloworld讲起
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 package com.atguigu.juc.tl;import java.util.concurrent.TimeUnit;class MovieTicket { int number = 50 ; public synchronized void saleTicket () { if (number > 0 ) { System.out.println(Thread.currentThread().getName()+"\t" +"号售票员卖出第: " +(number--)); }else { System.out.println("--------卖完了" ); } } } public class ThreadLocalDemo { public static void main (String[] args) { MovieTicket movieTicket = new MovieTicket (); for (int i = 1 ; i <=3 ; i++) { new Thread (() -> { for (int j = 0 ; j <20 ; j++) { movieTicket.saleTicket(); try { TimeUnit.MILLISECONDS.sleep(10 ); } catch (InterruptedException e) { e.printStackTrace(); } } },String.valueOf(i)).start(); } } }
不参加总和计算,希望各自分灶吃饭,各凭销售本事提成,按照出单数各自统计
比如某找房软件,每个中介销售都有自己的销售额指标,自己专属自己的,不和别人掺和
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 package com.atguigu.juc.tl;class House { ThreadLocal<Integer> threadLocal = ThreadLocal.withInitial(() -> 0 ); public void saleHouse () { Integer value = threadLocal.get(); value++; threadLocal.set(value); } } public class ThreadLocalDemo { public static void main (String[] args) { House house = new House (); new Thread (() -> { try { for (int i = 1 ; i <=3 ; i++) { house.saleHouse(); } System.out.println(Thread.currentThread().getName()+"\t" +"---" +house.threadLocal.get()); }finally { house.threadLocal.remove(); } },"t1" ).start(); new Thread (() -> { try { for (int i = 1 ; i <=2 ; i++) { house.saleHouse(); } System.out.println(Thread.currentThread().getName()+"\t" +"---" +house.threadLocal.get()); }finally { house.threadLocal.remove(); } },"t2" ).start(); new Thread (() -> { try { for (int i = 1 ; i <=5 ; i++) { house.saleHouse(); } System.out.println(Thread.currentThread().getName()+"\t" +"---" +house.threadLocal.get()); }finally { house.threadLocal.remove(); } },"t3" ).start(); System.out.println(Thread.currentThread().getName()+"\t" +"---" +house.threadLocal.get()); } }
通过上面代码总结
因为每个 Thread 内有自己的实例副本且该副本只由当前线程自己使用
既然其它 Thread 不可访问,那就不存在多线程间共享的问题。
统一设置初始值,但是每个线程对这个值的修改都是各自线程互相独立的
一句话
加入synchronized或者Lock控制资源的访问顺序
人手一份,大家各自安好,没必要抢夺
10.2 从阿里ThreadLocal规范开始
上述翻译:SimpleDateFormat中的日期格式不是同步的。推荐(建议)为每个线程创建独立的格式实例。如果多个线程同时访问一个格式,则它必须保持外部同步。
写时间工具类,一般写成静态的成员变量,不知,此种写法的多线程下的危险性! 课堂上讨论一下SimpleDateFormat线程不安全问题,以及解决方法。
代码实例 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 package com.atguigu.itdachang;import java.text.SimpleDateFormat;import java.util.Date;public class DateUtils { public static final SimpleDateFormat sdf = new SimpleDateFormat ("yyyy-MM-dd HH:mm:ss" ); public static Date parseDate (String stringDate) throws Exception { return sdf.parse(stringDate); } public static void main (String[] args) throws Exception { for (int i = 1 ; i <=30 ; i++) { new Thread (() -> { try { System.out.println(DateUtils.parseDate("2020-11-11 11:11:11" )); } catch (Exception e) { e.printStackTrace(); } },String.valueOf(i)).start(); } } }
源码分析结论 SimpleDateFormat类内部有一个Calendar对象引用,它用来储存和这个SimpleDateFormat相关的日期信息,例如sdf.parse(dateStr),sdf.format(date) 诸如此类的方法参数传入的日期相关String,Date等等, 都是交由Calendar引用来储存的.这样就会导致一个问题如果你的SimpleDateFormat是个static的, 那么多个thread 之间就会共享这个SimpleDateFormat, 同时也是共享这个Calendar引用。
解决1
将SimpleDateFormat定义成局部变量。
缺点:每调用一次方法就会创建一个SimpleDateFormat对象,方法结束又要作为垃圾回收。
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 package com.atguigu.itdachang;import java.text.SimpleDateFormat;import java.util.Date;public class DateUtils { public static final SimpleDateFormat sdf = new SimpleDateFormat ("yyyy-MM-dd HH:mm:ss" ); public static Date parseDate (String stringDate) throws Exception { return sdf.parse(stringDate); } public static void main (String[] args) throws Exception { for (int i = 1 ; i <=30 ; i++) { new Thread (() -> { try { SimpleDateFormat sdf = new SimpleDateFormat ("yyyy-MM-dd HH:mm:ss" ); System.out.println(sdf.parse("2020-11-11 11:11:11" )); sdf = null ; } catch (Exception e) { e.printStackTrace(); } },String.valueOf(i)).start(); } } }
解决2
ThreadLocal,也叫做线程本地变量或者线程本地存储
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 package com.atguigu.itdachang;import java.text.SimpleDateFormat;import java.util.Date;public class DateUtils { private static final ThreadLocal<SimpleDateFormat> sdf_threadLocal = ThreadLocal.withInitial(()-> new SimpleDateFormat ("yyyy-MM-dd HH:mm:ss" )); public static Date parseDateTL (String stringDate) throws Exception { return sdf_threadLocal.get().parse(stringDate); } public static void main (String[] args) throws Exception { for (int i = 1 ; i <=30 ; i++) { new Thread (() -> { try { System.out.println(DateUtils.parseDateTL("2020-11-11 11:11:11" )); } catch (Exception e) { e.printStackTrace(); } },String.valueOf(i)).start(); } } }
其它
DateUtils 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 package com.atguigu.juc.senior.utils;import java.text.ParseException;import java.text.SimpleDateFormat;import java.util.*;public class DateUtils { public static final ThreadLocal<SimpleDateFormat> SIMPLE_DATE_FORMAT_THREAD_LOCAL = ThreadLocal.withInitial(() -> new SimpleDateFormat ("yyyy-MM-dd HH:mm:ss" )); public static String format (Date date) { return SIMPLE_DATE_FORMAT_THREAD_LOCAL.get().format(date); } public static Date parse (String datetime) throws ParseException { return SIMPLE_DATE_FORMAT_THREAD_LOCAL.get().parse(datetime); } }
10.3 ThreadLocal源码分析 Thread,ThreadLocal,ThreadLocalMap 关系 Thread和ThreadLocal
ThreadLocal和ThreadLocalMap
All三者总概括 threadLocalMap实际上就是一个以threadLocal实例为key,任意对象为value的Entry对象。
当我们为threadLocal变量赋值,实际上就是以当前threadLocal实例为key,值为value的Entry往这个threadLocalMap中存放
小总结 近似的可以理解为:
ThreadLocalMap从字面上就可以看出这是一个保存ThreadLocal对象的map(其实是以ThreadLocal为Key),不过是经过了两层包装的
ThreadLocal对象:
JVM内部维护了一个线程版的Map<Thread,T>(通过ThreadLocal对象的set方法,结果把ThreadLocal对象自己当做key,放进了ThreadLoalMap中) ,每个线程要用到这个T的时候,用当前的线程去Map里面获取,通过这样让每个线程都拥有了自己独立的变量 , 人手一份,竞争条件被彻底消除,在并发模式下是绝对安全的变量。
10.4 ThreadLocal内存泄露问题 从阿里面试题开始讲起
什么是内存泄漏
不再会被使用的对象或者变量占用的内存不能被回收,就是内存泄露。
谁惹的祸? 再回首ThreadLocalMap
ThreadLocalMap从字面上就可以看出这是一个保存ThreadLocal对象的map(其实是以它为Key),不过是经过了两层包装的ThreadLocal对象:
(1)第一层包装是使用 WeakReference<ThreadLocal<?>> 将ThreadLocal对象变成一个弱引用的对象;
(2)第二层包装是定义了一个专门的类 Entry 来扩展 WeakReference<ThreadLocal<?>>:
强引用、软引用、弱引用、虚引用分别是什么?
Java 技术允许使用 finalize() 方法在垃圾收集器将对象从内存中清除出去之前做必要的清理工作。
新建一个带finalize()方法的对象MyObject
1 2 3 4 5 6 7 8 9 class MyObject { @Override protected void finalize () throws Throwable { System.out.println(Thread.currentThread().getName()+"\t" +"---finalize method invoked...." ); } }
当内存不足,JVM开始垃圾回收,对于强引用的对象,就算是出现了OOM也不会对该对象进行回收,死都不收。
强引用是我们最常见的普通对象引用,只要还有强引用指向一个对象,就能表明对象还“活着”,垃圾收集器不会碰这种对象。在 Java 中最常见的就是强引用,把一个对象赋给一个引用变量,这个引用变量就是一个强引用。当一个对象被强引用变量引用时,它处于可达状态,它是不可能被垃圾回收机制回收的,即使该对象以后永远都不会被用到JVM也不会回收。因此强引用是造成Java内存泄漏的主要原因之一。
对于一个普通的对象,如果没有其他的引用关系,只要超过了引用的作用域或者显式地将相应(强)引用赋值为 null,一般认为就是可以被垃圾收集的了(当然具体回收时机还是要看垃圾收集策略)。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 public static void strongReference () { MyObject myObject = new MyObject (); System.out.println("-----gc before: " +myObject); myObject = null ; System.gc(); try { TimeUnit.SECONDS.sleep(1 ); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("-----gc after: " +myObject); }
软引用是一种相对强引用弱化了一些的引用,需要用java.lang.ref.SoftReference类来实现,可以让对象豁免一些垃圾收集。
对于只有软引用的对象来说,当系统内存充足时它不会被回收,
当系统内存不足时它会被回收。
软引用通常用在对内存敏感的程序中,比如高速缓存就有用到软引用,内存够用的时候就保留,不够用就回收!
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 package com.atguigu.juc.tl;import java.lang.ref.SoftReference;import java.util.concurrent.TimeUnit;class MyObject { @Override protected void finalize () throws Throwable { System.out.println(Thread.currentThread().getName()+"\t" +"---finalize method invoked...." ); } } public class ReferenceDemo { public static void main (String[] args) { SoftReference<MyObject> softReference = new SoftReference <>(new MyObject ()); System.gc(); try { TimeUnit.SECONDS.sleep(1 ); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("-----gc after内存够用: " +softReference.get()); try { byte [] bytes = new byte [9 * 1024 * 1024 ]; }catch (Exception e){ e.printStackTrace(); }finally { System.out.println("-----gc after内存不够: " +softReference.get()); } } public static void strongReference () { MyObject myObject = new MyObject (); System.out.println("-----gc before: " +myObject); myObject = null ; System.gc(); try { TimeUnit.SECONDS.sleep(1 ); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("-----gc after: " +myObject); } }
弱引用需要用java.lang.ref.WeakReference类来实现,它比软引用的生存期更短,
对于只有弱引用的对象来说,只要垃圾回收机制一运行,不管JVM的内存空间是否足够,都会回收该对象占用的内存。
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 package com.atguigu.juc.tl;import java.lang.ref.SoftReference;import java.lang.ref.WeakReference;import java.util.concurrent.TimeUnit;class MyObject { @Override protected void finalize () throws Throwable { System.out.println(Thread.currentThread().getName()+"\t" +"---finalize method invoked...." ); } } public class ReferenceDemo { public static void main (String[] args) { WeakReference<MyObject> weakReference = new WeakReference <>(new MyObject ()); System.out.println("-----gc before内存够用: " +weakReference.get()); System.gc(); try { TimeUnit.SECONDS.sleep(1 ); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("-----gc after内存够用: " +weakReference.get()); } public static void softReference () { SoftReference<MyObject> softReference = new SoftReference <>(new MyObject ()); System.gc(); try { TimeUnit.SECONDS.sleep(1 ); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("-----gc after内存够用: " +softReference.get()); try { byte [] bytes = new byte [9 * 1024 * 1024 ]; }catch (Exception e){ e.printStackTrace(); }finally { System.out.println("-----gc after内存不够: " +softReference.get()); } } public static void strongReference () { MyObject myObject = new MyObject (); System.out.println("-----gc before: " +myObject); myObject = null ; System.gc(); try { TimeUnit.SECONDS.sleep(1 ); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("-----gc after: " +myObject); } }
假如有一个应用需要读取大量的本地图片:
如果每次读取图片都从硬盘读取则会严重影响性能,
如果一次性全部加载到内存中又可能造成内存溢出。
此时使用软引用可以解决这个问题。
设计思路是:用一个HashMap来保存图片的路径和相应图片对象关联的软引用之间的映射关系,在内存不足时,JVM会自动回收这些缓存图片对象所占用的空间,从而有效地避免了OOM的问题。
Map<String, SoftReference> imageCache = new HashMap<String, SoftReference>();
虚引用需要java.lang.ref.PhantomReference类来实现。
顾名思义,就是形同虚设 ,与其他几种引用都不同,虚引用并不会决定对象的生命周期。如果一个对象仅持有虚引用,那么它就和没有任何引用一样,在任何时候都可能被垃圾回收器回收 ,它不能单独使用也不能通过它访问对象,虚引用必须和引用队列 (ReferenceQueue)联合使用。
虚引用的主要作用是跟踪对象被垃圾回收的状态。 仅仅是提供了一种确保对象被 finalize以后,做某些事情的机制。 PhantomReference的get方法总是返回null ,因此无法访问对应的引用对象。
其意义在于:说明一个对象已经进入finalization阶段,可以被gc回收,用来实现比finalization机制更灵活的回收操作。
换句话说,设置虚引用关联的唯一目的,就是在这个对象被收集器回收的时候收到一个系统通知或者后续添加进一步的处理。
构造方法
引用队列
被回收前需要被引用队列保存下。
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 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 package com.atguigu.juc.tl;import java.lang.ref.*;import java.util.ArrayList;import java.util.List;import java.util.concurrent.TimeUnit;class MyObject { @Override protected void finalize () throws Throwable { System.out.println(Thread.currentThread().getName()+"\t" +"---finalize method invoked...." ); } } public class ReferenceDemo { public static void main (String[] args) { ReferenceQueue<MyObject> referenceQueue = new ReferenceQueue (); PhantomReference<MyObject> phantomReference = new PhantomReference <>(new MyObject (),referenceQueue); List<byte []> list = new ArrayList <>(); new Thread (() -> { while (true ) { list.add(new byte [1 * 1024 * 1024 ]); try { TimeUnit.MILLISECONDS.sleep(600 ); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(phantomReference.get()); } },"t1" ).start(); new Thread (() -> { while (true ) { Reference<? extends MyObject > reference = referenceQueue.poll(); if (reference != null ) { System.out.println("***********有虚对象加入队列了" ); } } },"t2" ).start(); try { TimeUnit.SECONDS.sleep(5 ); } catch (InterruptedException e) { e.printStackTrace(); } } public static void weakReference () { WeakReference<MyObject> weakReference = new WeakReference <>(new MyObject ()); System.out.println("-----gc before内存够用: " +weakReference.get()); System.gc(); try { TimeUnit.SECONDS.sleep(1 ); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("-----gc after内存够用: " +weakReference.get()); } public static void softReference () { SoftReference<MyObject> softReference = new SoftReference <>(new MyObject ()); System.gc(); try { TimeUnit.SECONDS.sleep(1 ); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("-----gc after内存够用: " +softReference.get()); try { byte [] bytes = new byte [9 * 1024 * 1024 ]; }catch (Exception e){ e.printStackTrace(); }finally { System.out.println("-----gc after内存不够: " +softReference.get()); } } public static void strongReference () { MyObject myObject = new MyObject (); System.out.println("-----gc before: " +myObject); myObject = null ; System.gc(); try { TimeUnit.SECONDS.sleep(1 ); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("-----gc after: " +myObject); } }
关系
每个Thread对象维护着一个ThreadLocalMap的引用
ThreadLocalMap是ThreadLocal的内部类,用Entry来进行存储
调用ThreadLocal的set()方法时,实际上就是往ThreadLocalMap设置值,key是ThreadLocal对象,值Value是传递进来的对象
调用ThreadLocal的get()方法时,实际上就是往ThreadLocalMap获取值,key是ThreadLocal对象
ThreadLocal本身并不存储值,它只是自己作为一个key来让线程从ThreadLocalMap获取value,正因为这个原理,所以ThreadLocal能够实现“数据隔离”,获取当前线程的局部变量值,不受其他线程影响~
为什么要用弱引用?不用如何? 1 2 3 4 5 6 public void function01 () { ThreadLocal tl = new ThreadLocal <Integer>(); tl.set(2021 ); tl.get(); }
line1新建了一个ThreadLocal对象,t1 是强引用指向这个对象;
line2调用set()方法后新建一个Entry,通过源码可知Entry对象里的k是弱引用指向这个对象。
为什么源代码用弱引用? 当function01方法执行完毕后,栈帧销毁强引用 tl 也就没有了。但此时线程的ThreadLocalMap里某个entry的key引用还指向这个对象 若这个key引用是强引用,就会导致key指向的ThreadLocal对象及v指向的对象不能被gc回收,造成内存泄漏; 若这个key引用是弱引用就大概率会减少内存泄漏的问题(还有一个key为null的雷)。使用弱引用,就可以使ThreadLocal对象在方法执行完毕后顺利被回收且Entry的key引用指向为null。
–下面这句
此后我们调用get,set或remove方法时,就会尝试删除key为null的entry,可以释放value对象所占用的内存。
弱引用就万事大吉了吗?
1 当我们为threadLocal变量赋值,实际上就是当前的Entry(threadLocal实例为key,值为value)往这个threadLocalMap中存放。Entry中的key是弱引用,当threadLocal外部强引用被置为null(tl=null),那么系统 GC 的时候,根据可达性分析,这个threadLocal实例就没有任何一条链路能够引用到它,这个ThreadLocal势必会被回收,这样一来,ThreadLocalMap中就会出现key为null的Entry,就没有办法访问这些key为null的Entry的value,如果当前线程再迟迟不结束的话,这些key为null的Entry的value就会一直存在一条强引用链:Thread Ref -> Thread -> ThreaLocalMap -> Entry -> value永远无法回收,造成内存泄漏。
2 当然,如果当前thread运行结束,threadLocal,threadLocalMap,Entry没有引用链可达,在垃圾回收的时候都会被系统进行回收。
3 但在实际使用中我们有时候会用线程池去维护我们的线程,比如在Executors.newFixedThreadPool()时创建线程的时候,为了复用线程是不会结束的,所以threadLocal内存泄漏就值得我们小心
key为null的entry,原理解析
ThreadLocalMap使用ThreadLocal的弱引用作为key,如果一个ThreadLocal没有外部强引用引用他,那么系统gc的时候,这个ThreadLocal势必会被回收,这样一来,ThreadLocalMap中就会出现key为null的Entry ,就没有办法访问这些key为null的Entry的value,如果当前线程再迟迟不结束的话(比如正好用在线程池),这些key为null的Entry的value就会一直存在一条强引用链。
虽然弱引用,保证了key指向的ThreadLocal对象能被及时回收,但是v指向的value对象是需要ThreadLocalMap调用get、set时发现key为null时才会去回收整个entry、value,因此弱引用不能100%保证内存不泄露。我们要在不使用某个ThreadLocal对象后,手动调用remoev方法来删除它 ,尤其是在线程池中,不仅仅是内存泄露的问题,因为线程池中的线程是重复使用的,意味着这个线程的ThreadLocalMap对象也是重复使用的,如果我们不手动调用remove方法,那么后面的线程就有可能获取到上个线程遗留下来的value值,造成bug。
set、get方法会去检查所有键为null的Entry对象
从前面的set,getEntry,remove方法看出,在threadLocal的生命周期里,针对threadLocal存在的内存泄漏的问题,都会通过expungeStaleEntry,cleanSomeSlots,replaceStaleEntry这三个方法清理掉key为null的脏entry。
最佳实践
10.5 小总结 ThreadLocal在ThreadLocalMap中是以一个弱引用身份被Entry中的Key引用的,因此如果ThreadLocal没有外部强引用来引用它,那么ThreadLocal会在下次JVM垃圾收集时被回收。这个时候就会出现Entry中Key已经被回收,出现一个null Key的情况,外部读取ThreadLocalMap中的元素是无法通过null Key来找到Value的。因此如果当前线程的生命周期很长,那么其内部的ThreadLocalMap对象也一直生存下来,这些null key就存在一条强引用链的关系一直存在:Thread –> ThreadLocalMap–>Entry–>Value,这条强引用链会导致Entry不会回收,Value也不会回收,但Entry中的Key却已经被回收的情况,造成内存泄漏。
ThreadLocal 并不解决线程间共享数据的问题
ThreadLocal 适用于变量在线程间隔离且在方法间共享的场景
通过隐式的在不同线程内创建独立实例副本避免了实例线程安全的问题
每个线程持有一个只属于自己的专属Map并维护了ThreadLocal对象与具体实例的映射,该Map由于只被持有它的线程访问,故不存在线程安全以及锁的问题
ThreadLocalMap的Entry对ThreadLocal的引用为弱引用,避免了ThreadLocal对象无法被回收的问题
都会通过expungeStaleEntry,cleanSomeSlots,replaceStaleEntry这三个方法回收键为 null 的 Entry 对象的值(即为具体实例)以及 Entry 对象本身从而防止内存泄漏,属于安全加固的方法
群雄逐鹿起纷争,人各一份天下安