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;

/**
* @auther zzyy
* @create 2020-04-15 10:41
*/
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;

/**
* @auther zzyy
* @create 2020-04-15 10:41
*/
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,否则什么都不做或重来

image-20220210190358679

硬件级别保证

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;

/**
* @auther zzyy
* @create 2020-04-15 7:51
*/
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)
  • compareAndSet()方法的源代码:

image-20220210192608457

上面三个方法都是类似的,主要对4个参数做一下说明。

  • var1:表示要操作的对象
  • var2:表示要操作对象中属性地址的偏移量
  • var4:表示需要修改数据的期望的值
  • var5/var6:表示需要修改为的新值

image-20220210192653435

引出来一个问题:UnSafe类是什么?

8.3 CAS底层原理?如果知道,谈谈你对UnSafe的理解

image-20220210192820711

  • Unsafe

是CAS的核心类,由于Java方法无法直接访问底层系统,需要通过本地(native)方法来访问,Unsafe相当于一个后门,基于该类可以直接操作特定内存的数据。Unsafe类存在于sun.misc包中,其内部方法操作可以像C的指针一样直接操作内存,因为Java中CAS操作的执行依赖于Unsafe类的方法。

注意Unsafe类中的所有方法都是native修饰的,也就是说Unsafe类中的方法都直接调用操作系统底层资源执行相应任务

  • 变量valueOffset,表示该变量值在内存中的偏移地址,因为Unsafe就是根据内存偏移地址获取数据的。

image-20220210192922269

  • 变量value用volatile修饰,保证了多线程之间的内存可见性。

8.4 原子引用

AtomicInteger原子整型,可否有其它原子类型?

  • AtomicBook
  • AtomicOrder

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;
}

/**
* @auther zzyy
* @create 2018-12-31 17:22
*/
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

image-20220210193829108

自己实现一个自旋锁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;

/**
* @auther zzyy
* @create 2018-12-28 17:57
* 题目:实现一个自旋锁
* 自旋锁好处:循环比较获取没有类似wait的阻塞。
*
* 通过CAS操作完成自旋锁,A线程先进来调用myLock方法自己持有锁5秒钟,B随后进来后发现
* 当前有线程持有锁,不是null,所以只能通过自旋等待,直到A释放锁后B随后抢到。
*/
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();

//暂停一会儿线程,保证A线程先于B线程启动并完成
try { TimeUnit.SECONDS.sleep( 1 ); } catch (InterruptedException e) { e.printStackTrace(); }

new Thread(() -> {
spinLockDemo.myLock();
spinLockDemo.myUnLock();
},"B").start();

}
}

8.6 CAS缺点

循环时间长开销很大。

  • 我们可以看到getAndAddInt方法执行时,有个do while

image-20220210194004012

  • 如果CAS失败,会一直进行尝试。如果CAS长时间一直不成功,可能会给CPU带来很大的开销。

引出来ABA问题???

ABA问题怎么产生的

CAS会导致“ABA问题”。

CAS算法实现一个重要前提需要取出内存中某时刻的数据并在当下时刻比较并替换,那么在这个时间差类会导致数据的变化。

比如说一个线程one从内存位置V中取出A,这时候另一个线程two也从内存中取出A,并且线程two进行了一些操作将值变成了B,然后线程two又将V位置的数据变成A,这时候线程one进行CAS操作发现内存中仍然是A,然后线程one操作成功。

尽管线程one的CAS操作成功,但是不代表这个过程就是没有问题的。

版本号时间戳原子引用
  • AtomicStampedReference

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;

/**
* @auther zzyy
* @create 2018-11-20 17:14
*/
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();

//暂停一会儿线程,main彻底等待上面的ABA出现演示完成。
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);//1
//暂停一会儿线程,
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);//1
//暂停一会儿线程,获得初始值100和初始版本号1,故意暂停3秒钟让t3线程完成一次ABA操作产生问题
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罗汉增强

  • AtomicBoolean

  • AtomicInteger

  • AtomicIntegerArray

  • AtomicIntegerFieldUpdater

  • AtomicLong

  • AtomicLongArray

  • AtomicLongFieldUpdater

  • AtomicMarkableReference

  • AtomicReference

  • AtomicReferenceArray

  • AtomicReferenceFieldUpdater

  • AtomicStampedReference

  • DoubleAccumulator

  • DoubleAdder

  • LongAccumulator

  • LongAdder

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();
}
}

/**
* @auther zzyy
* @create 2020-07-03 17:16
*/
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;

/**
* @auther zzyy
* @create 2020-04-16 14:59
*/
public class AtomicIntegerArrayDemo
{
public static void main(String[] args)
{
AtomicIntegerArray atomicIntegerArray = new AtomicIntegerArray(new int[5]);
//AtomicIntegerArray atomicIntegerArray = new AtomicIntegerArray(5);
//AtomicIntegerArray atomicIntegerArray = new AtomicIntegerArray(new int[]{1,2,3,4,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;
}

/**
* @auther zzyy
* @create 2018-12-31 17:22
*/
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;

/**
* @auther zzyy
* @create 2018-12-28 17:57
* 题目:实现一个自旋锁
* 自旋锁好处:循环比较获取没有类似wait的阻塞。
*
* 通过CAS操作完成自旋锁,A线程先进来调用myLock方法自己持有锁5秒钟,B随后进来后发现
* 当前有线程持有锁,不是null,所以只能通过自旋等待,直到A释放锁后B随后抢到。
*/
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();
//暂停一会儿线程,保证A线程先于B线程启动并完成
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;

/**
* @auther zzyy
* @create 2021-03-18 15:34
*/
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;

/**
* @auther zzyy
* @create 2020-05-23 10:56
*/
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());
//故意暂停200毫秒,让后面的t4线程拿到和t3一样的版本号
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);
//暂停2秒钟,让t3先完成ABA操作了,看看自己还能否修改
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
    • 原子更新对象中int类型字段的值
  • AtomicLongFieldUpdater
    • 原子更新对象中Long类型字段的值
  • 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);
}
}

/**
* @auther zzyy
* @create 2020-07-14 18:06
* 以一种线程安全的方式操作非线程安全对象的某些字段。
* 需求:
* 1000个人同时向一个账号转账一元钱,那么累计应该增加1000元,
* 除了synchronized和CAS,还可以使用AtomicIntegerFieldUpdater来实现。
*/
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"+"------其它线程正在初始化");
}
}


}

/**
* @auther zzyy
* @create 2021-03-18 17:20
* 多线程并发调用一个类的初始化方法,如果未被初始化过,将执行初始化工作,要求只能初始化一次
*/
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

阿里要命题目

image-20220210201506133

  • 热点商品点赞计算器,点赞数加加统计,不要求实时精确
  • 一个很大的List,里面都是int类型,如何实现加加,说说思路

点赞计数器,看看性能

  • 常用API

image-20220210202329986

入门讲解
  • 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;

/**
* @auther zzyy
* @create 2020-05-30 13:51
*/
public class LongAccumulatorDemo
{

LongAdder longAdder = new LongAdder();
public void add_LongAdder()
{
longAdder.increment();
}

//LongAccumulator longAccumulator = new LongAccumulator((x, y) -> x + y,0);
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());
}
}
  • LongAdderAPIDemo
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;

/**
* @auther zzyy
* @create 2021-03-19 15:59
*/
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);
}
}

/**
* @auther zzyy
* @create 2020-05-21 22:23
* 50个线程,每个线程100W次,总点赞数出来
*/
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());


}
}

image-20220210202737877

源码、原理分析

image-20220210202903470

  • LongAdder是Striped64的子类
1
2
3
4
public class LongAdder extends Striped64 implements Serializable {
private static final long serialVersionUID = 7249069246863182397L;

abstract class Striped64 extends Number {

原理(LongAdder为什么这么快)

官网说明和阿里要求

image-20220210203251975

image-20220210203309488

Striped64
  • Striped64有几个比较重要的成员函数
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
 
/** Number of CPUS, to place bound on table size CPU数量,即cells数组的最大长度 */
static final int NCPU = Runtime.getRuntime().availableProcessors();

/**
* Table of cells. When non-null, size is a power of 2.
cells数组,为2的幂,2,4,8,16.....,方便以后位运算
*/
transient volatile Cell[] cells;

/**基础value值,当并发较低时,只累加该值主要用于没有竞争的情况,通过CAS更新。
* Base value, used mainly when there is no contention, but also as
* a fallback during table initialization races. Updated via CAS.
*/
transient volatile long base;

/**创建或者扩容Cells数组时使用的自旋锁变量调整单元格大小(扩容),创建单元格时使用的锁。
* Spinlock (locked via CAS) used when resizing and/or creating Cells.
*/
transient volatile int cellsBusy;

  • Striped64中一些变量或者方法的定义

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 的一个内部类

image-20220210203730581

LongAdder为什么这么快

LongAdder的基本思路就是分散热点,将value值分散到一个Cell数组中,不同线程会命中到数组的不同槽中,各个线程只对自己槽中的那个值进行CAS操作,这样热点就被分散了,冲突的概率就小很多。如果要获取真正的long值,只要将各个槽中的变量值累加返回。

sum()会将所有Cell数组中的value和base累加作为返回值,核心的思想就是将之前AtomicLong一个value的更新压力分散到多个value中去,从而降级更新热点

image-20220210212623319

数学表达

  • 内部有一个base变量,一个Cell[]数组。
    • base变量:非竞态条件下,直接累加到该变量上
    • Cell[]数组:竞态条件下,累加个各个线程自己的槽Cell[i]中

image-20220210212643001

源码解读深度分析

LongAdder在无竞争的情况,跟AtomicLong一样,对同一个base进行操作,当出现竞争关系时则是采用化整为零的做法,从空间换时间,用一个数组cells,将一个value拆分进这个数组cells。多个线程需要同时对value进行操作时候,可以对线程id进行hash得到hash值,再根据hash值映射到这个数组cells的某个下标,再对该下标所对应的值进行自增操作。当所有线程操作完毕,将数组cells的所有值和无竞争值base都加起来作为最终结果。

image-20220210212954002

longAdder.increment()

  • as表示cells 引用
  • b表示获取的base 值
  • v表示期望值
  • m表示cells数组的长度
  • a表示当前线程命中的cell单元格

image-20220210213929556

add(1L)方法

条件递增,逐步解析

  • 1.最初无竞争时只更新base
  • 2.如果更新base失败后,首次新建一个Cell[]数组
  • 3.当多个线程竞争同一个Cell比较激烈时,可能就要对Cell[]扩容

image-20220210214056619

image-20220210214109769

longAccumulate方法
  • longAccumulate入参说明

long x需要增加的值,一般默认都是1.

LongBinaryOperator fn 默认传递的是null

wasUncontended竞争标识,如果是false则代表有竞争。只有cells初始化之后,并且当前线程CAS竞争修改失败,才会是false

  • 线程hash值:probe

image-20220211161728478

image-20220211161957445

可以理解为是线程的hash值

image-20220211162011228

  • 总纲

image-20220211162048335

上述代码首先给当前线程分配一个hash值,然后进入一个for(;;)自旋,这个自旋分为三个分支:

CASE1:Cell[]数组已经初始化

CASE2:Cell[]数组未初始化(首次新建)

CASE3:Cell[]数组正在初始化中

  • 计算

步骤1: 刚刚要初始化Cell[]数组(首次新建)未初始化过Cell[]数组,尝试占有锁并首次初始化cells数组

image-20220211194856417

如果上面条件都执行成功就会执行数组的初始化及赋值操作, 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修改失败的线程会走到这个分支

image-20220211195043063

步骤3:Cell数组不再为空且可能存在Cell数组扩容

总体代码

image-20220211195252107

  1. 下面代码判断当前线程hash后指向的数据位置元素是否为空,如果为空则将Cell数据放入数组中,跳出循环。如果不空则继续循环。

image-20220211195306335

  1. 重新设置锁竞争状态

image-20220211195343451

  1. 说明当前线程对应的数组中有了数据,也重置过hash值,这时通过CAS操作尝试对当前数中的value值进行累加x操作,x默认为1,如果CAS成功则直接跳出循环。

image-20220211195450956

  1. 判断是否超过最大扩容

image-20220211195507512

  1. 跟上一步反操作,没有达到·CPU上限,还能扩

image-20220211200317651

6.

image-20220211195649917

使用总结

AtomicLong
  • 线程安全,可允许一些性能损耗,要求高精度时可使用
  • 保证精度,性能代价
  • AtomicLong是多个线程针对单个热点值value进行原子操作
LongAdder
  • 当需要在高并发下有较好的性能表现,且对值的精确度要求不高时,可以使用
  • 保证性能,精度代价
  • LongAdder是每个线程拥有自己的槽,各个线程一般只对自己槽中的那个值进行CAS操作

10. 聊聊ThreadLocal

10.1 ThreadLocal简介

大厂面试题

  • ThreadLocal中ThreadLocalMap的数据结构和关系?
  • ThreadLocal的key是弱引用,这是为什么?
  • ThreadLocal内存泄露问题你知道吗?
  • ThreadLocal中最后为什么要加remove方法?

是什么

image-20220210214439309

稍微翻译一下:

ThreadLocal提供线程局部变量。这些变量与正常的变量不同,因为每一个线程在访问ThreadLocal实例的时候(通过其get或set方法)都有自己的、独立初始化的变量副本。ThreadLocal实例通常是类中的私有静态字段,使用它的目的是希望将状态(例如,用户ID或事务ID)与线程关联起来。

能干嘛

实现每一个线程都有自己专属的本地变量副本(自己用自己的变量不麻烦别人,不和其他人共享,人人有份,人各一份),主要解决了让每个线程绑定自己的值,通过使用get()和set()方法,获取默认值或将其值更改为当前线程所存的副本的值从而避免了线程安全问题。

image-20220210214634917

api介绍

image-20220210214654771

永远的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("--------卖完了");
}
}
}

/**
* @auther zzyy
* @create 2021-03-23 15:03
* 三个售票员卖完50张票务,总量完成即可,吃大锅饭,售票员每个月固定月薪
*/
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);
}
}

/**
* @auther zzyy
* @create 2021-03-23 15:03
*
* 2 分灶吃饭,各个销售自己动手,丰衣足食
*/
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();//如果不清理自定义的 ThreadLocal 变量,可能会影响后续业务逻辑和造成内存泄露等问题
}
},"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规范开始

image-20220210215559400

非线程安全的SimpleDateFormat

image-20220210215832974

上述翻译: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;

/**
* @auther zzyy
* @create 2020-07-17 16:42
*/
public class DateUtils
{
public static final SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
/**
* 模拟并发环境下使用SimpleDateFormat的parse方法将字符串转换成Date对象
* @param stringDate
* @return
* @throws Exception
*/
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();
}
}
}

image-20220210215951706

源码分析结论

SimpleDateFormat类内部有一个Calendar对象引用,它用来储存和这个SimpleDateFormat相关的日期信息,例如sdf.parse(dateStr),sdf.format(date) 诸如此类的方法参数传入的日期相关String,Date等等, 都是交由Calendar引用来储存的.这样就会导致一个问题如果你的SimpleDateFormat是个static的, 那么多个thread 之间就会共享这个SimpleDateFormat, 同时也是共享这个Calendar引用。

image-20220210220134171

image-20220210220111877

解决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;

/**
* @auther zzyy
* @create 2020-07-17 16:42
*/
public class DateUtils
{
public static final SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
/**
* 模拟并发环境下使用SimpleDateFormat的parse方法将字符串转换成Date对象
* @param stringDate
* @return
* @throws Exception
*/
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;

/**
* @auther zzyy
* @create 2020-07-17 16:42
*/
public class DateUtils
{
private static final ThreadLocal<SimpleDateFormat> sdf_threadLocal =
ThreadLocal.withInitial(()-> new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"));

/**
* ThreadLocal可以确保每个线 程都可以得到各自单独的一个SimpleDateFormat的对象,那么自然也就不存在竞争问题了。
* @param stringDate
* @return
* @throws Exception
*/
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();
}
}
}

其它

  • 加锁
  • 第3方时间库

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.*;

/**
* @auther zzyy
* @create 2020-05-03 10:14
*/
public class DateUtils
{
/*
1 SimpleDateFormat如果多线程共用是线程不安全的类
public static final SimpleDateFormat SIMPLE_DATE_FORMAT = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");

public static String format(Date date)
{
return SIMPLE_DATE_FORMAT.format(date);
}

public static Date parse(String datetime) throws ParseException
{
return SIMPLE_DATE_FORMAT.parse(datetime);
}*/

//2 ThreadLocal可以确保每个线程都可以得到各自单独的一个SimpleDateFormat的对象,那么自然也就不存在竞争问题了。
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);
}


//3 DateTimeFormatter 代替 SimpleDateFormat
/*public static final DateTimeFormatter DATE_TIME_FORMAT = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");

public static String format(LocalDateTime localDateTime)
{
return DATE_TIME_FORMAT.format(localDateTime);
}

public static LocalDateTime parse(String dateString)
{

return LocalDateTime.parse(dateString,DATE_TIME_FORMAT);
}*/
}



10.3 ThreadLocal源码分析

Thread,ThreadLocal,ThreadLocalMap 关系

Thread和ThreadLocal

image-20220210221911505

ThreadLocal和ThreadLocalMap

image-20220210221932997

All三者总概括

threadLocalMap实际上就是一个以threadLocal实例为key,任意对象为value的Entry对象。

当我们为threadLocal变量赋值,实际上就是以当前threadLocal实例为key,值为value的Entry往这个threadLocalMap中存放

image-20220210222022495

小总结

近似的可以理解为:

ThreadLocalMap从字面上就可以看出这是一个保存ThreadLocal对象的map(其实是以ThreadLocal为Key),不过是经过了两层包装的

ThreadLocal对象:

image-20220210222251982

JVM内部维护了一个线程版的Map<Thread,T>(通过ThreadLocal对象的set方法,结果把ThreadLocal对象自己当做key,放进了ThreadLoalMap中),每个线程要用到这个T的时候,用当前的线程去Map里面获取,通过这样让每个线程都拥有了自己独立的变量
人手一份,竞争条件被彻底消除,在并发模式下是绝对安全的变量。

10.4 ThreadLocal内存泄露问题

从阿里面试题开始讲起

image-20220210222637148

什么是内存泄漏

  • 不再会被使用的对象或者变量占用的内存不能被回收,就是内存泄露。

谁惹的祸?

再回首ThreadLocalMap

image-20220210224928922

image-20220210230049205

ThreadLocalMap从字面上就可以看出这是一个保存ThreadLocal对象的map(其实是以它为Key),不过是经过了两层包装的ThreadLocal对象:

(1)第一层包装是使用 WeakReference<ThreadLocal<?>> 将ThreadLocal对象变成一个弱引用的对象;

(2)第二层包装是定义了一个专门的类 Entry 来扩展 WeakReference<ThreadLocal<?>>:

强引用、软引用、弱引用、虚引用分别是什么?

image-20220210225114626

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....");
}
}

/**
* @auther zzyy
* @create 2021-03-24 10:31
*/
public class ReferenceDemo
{
public static void main(String[] args)
{
//当我们内存不够用的时候,soft会被回收的情况,设置我们的内存大小:-Xms10m -Xmx10m
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....");
}
}

/**
* @auther zzyy
* @create 2021-03-24 10:31
*/
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()
{
//当我们内存不够用的时候,soft会被回收的情况,设置我们的内存大小:-Xms10m -Xmx10m
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机制更灵活的回收操作。

换句话说,设置虚引用关联的唯一目的,就是在这个对象被收集器回收的时候收到一个系统通知或者后续添加进一步的处理。

构造方法

image-20220210225740044

引用队列

被回收前需要被引用队列保存下。

image-20220210225755381

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....");
}
}

/**
* @auther zzyy
* @create 2021-03-24 10:31
*/
public class ReferenceDemo
{
public static void main(String[] args)
{
ReferenceQueue<MyObject> referenceQueue = new ReferenceQueue();
PhantomReference<MyObject> phantomReference = new PhantomReference<>(new MyObject(),referenceQueue);
//System.out.println(phantomReference.get());

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()
{
//当我们内存不够用的时候,soft会被回收的情况,设置我们的内存大小:-Xms10m -Xmx10m
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);
}
}
  • GCRoots和四大引用小总结

image-20220210230535356

关系

image-20220210225255780

image-20220210225307981

每个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>(); //line1
tl.set(2021); //line2
tl.get(); //line3
}
  • line1新建了一个ThreadLocal对象,t1 是强引用指向这个对象;
  • line2调用set()方法后新建一个Entry,通过源码可知Entry对象里的k是弱引用指向这个对象。

image-20220210230826142

为什么源代码用弱引用?

当function01方法执行完毕后,栈帧销毁强引用 tl 也就没有了。但此时线程的ThreadLocalMap里某个entry的key引用还指向这个对象
若这个key引用是强引用,就会导致key指向的ThreadLocal对象及v指向的对象不能被gc回收,造成内存泄漏;
若这个key引用是弱引用就大概率会减少内存泄漏的问题(还有一个key为null的雷)。使用弱引用,就可以使ThreadLocal对象在方法执行完毕后顺利被回收且Entry的key引用指向为null。

image-20220211112827448–下面这句

  • 此后我们调用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,原理解析

image-20220211113046785

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()

image-20220211113146230

image-20220211113159196

  • get()

image-20220211115952162

image-20220211120005838

  • remove()

image-20220211120018754

  • 结论

从前面的set,getEntry,remove方法看出,在threadLocal的生命周期里,针对threadLocal存在的内存泄漏的问题,都会通过expungeStaleEntry,cleanSomeSlots,replaceStaleEntry这三个方法清理掉key为null的脏entry。

最佳实践

image-20220210230715345

image-20220210230740917

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 对象本身从而防止内存泄漏,属于安全加固的方法
  • 群雄逐鹿起纷争,人各一份天下安