文章目录
线程安全
先说什么是线程安全,简单说就是多线程下并发同时对共享数据进行读写,会导致数据混乱 = 数据不安全
当多线程并发访问临界资源(比如库存为1时很多人抢着一个这时会有大量请求,处理不好就会出现超卖的问题)时,如果破坏原子性、可见性、有序性,会导致数据不一致的问题
-
原子性:指相应的操作是单一不可分割的操作
-
可见性:多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线程能立即看到修改后的值
-
有序性(指令重排):有序性最终表述的现象是CPU是否按照既定代码顺序执行依次执行指令。编译器和CPU为了提高指令的执行效率可能会进行指令重排序,这使得代码的实际执行方式可能不是按照我们所认为的方式进行。
在单线程的情况只要保证最终执行结果正确即可
简单可以使用sycnhronized关键字和lock来保证有序性,
扩展:
JMM
和JVM不是一个东西啊,JMM就是Java内存模型(java memory model)。因为在不同的硬件生产商和不同的操作系统下,内存的访问有一定的差异,所以会造成相同的代码运行在不同的系统上会出现各种问题。所以java内存模型(JMM)屏蔽掉各种硬件和操作系统的内存访问差异,以实现让java程序在各种平台下都能达到一致的并发效果。
Java内存模型规定所有的变量都存储在主内存中,包括实例变量,静态变量,但是不包括局部变量和方法参数。每个线程都有自己的工作内存,线程的工作内存保存了该线程用到的变量和主内存的副本拷贝,线程对变量的操作都在工作内存中进行。线程不能直接读写主内存中的变量。
(就是因为这啊)不同的线程之间也无法访问对方工作内存中的变量。线程之间变量值的传递均需要通过主内存来完成。
如何解决线程不安全
前面介绍了可以使用sycnhronized关键字和lock来保证有序性
-
破坏临界资源
-
只读 就是使用final关键字
-
局部变量:每个线程的局部变量会存在栈帧中,会在每个线程的栈帧内存中被创建多份,因此不存在共享
ThreadLocal
介绍
ThreadLocal也就是线程本地变量,如果你创建了一个ThreadLocal变量,那么访问这个变量的每个线程都会有这个变量大的一个本地拷贝,多个线程操作这个变量的时候,实际是操作自己本地内存里面的变量,从而起到线程隔离的作用,避免了线程安全问题。
底层数据结构和源码
- ThreadLocal是Java中所提供的线程本地存储机制,可以利用该机制将数据缓存在摸个线程内部,该线程可以在任意时刻、任意方法中获取缓存的数据
- ThreadLocal底层是通过ThreadLocalMap来实现的,每个Thread对象(注意不是ThreadLocal对象)中都存在一个ThreadLocalMap, Map的key为ThreadLocal对象,Map的value为需要缓存的值
比如set方法:
public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null) {
map.set(this, value);
} else {
createMap(t, value);
}
}
private void set(ThreadLocal<?> key, Object value) {
// We don't use a fast path as with get() because it is at
// least as common to use set() to create new entries as
// it is to replace existing ones, in which case, a fast
// path would fail more often than not.
Entry[] tab = table;
int len = tab.length;
int i = key.threadLocalHashCode & (len-1);
for (Entry e = tab[i];
e != null;
e = tab[i = nextIndex(i, len)]) {
ThreadLocal<?> k = e.get();
if (k == key) {
e.value = value;
return;
}
if (k == null) {
replaceStaleEntry(key, value, i);
return;
}
}
tab[i] = new Entry(key, value); // 这里就能看出来存的是entry对象
int sz = ++size;
if (!cleanSomeSlots(i, sz) && sz >= threshold)
rehash();
}
内存泄露
因为当ThreadLocal对象使用完之后,应该要把设置的key,value也就是entry对象进行回收,但是线程池中的线程不会进行回收,而线程对象是通过强引用指向ThreadLocalMap,ThreadLocalMap也是通过强引用指向Entry对象,线程不被回收,Entry对象也就不会被回收,从而出现内存泄露。
解决办法是在使用了ThreadLocal对象之后,手动调用ThreadLocal的remove方法,手动清除Entry对象
InheritableThreadLocal
它是ThreadLocal的子类,继承关系如下
有啥区别呢?
InheritableThreadLocal可以使子线程能够获取到父线程获取到的值,如果在子线程中途更改相应的值,在线程之间是感知不到的。
如果要使用这种功能,我们可以使用阿里开源的TransmitableThreadLocal,可以避免InheritableThreadLocal在多线程中出现不一致的情况。
JMM内存模型
Java内存模型(JMM)总结 - 知乎 (zhihu.com)
和JVM不是一个东西啊,JMM就是Java内存模型(java memory model)。因为在不同的硬件生产商和不同的操作系统下,内存的访问有一定的差异,所以会造成相同的代码运行在不同的系统上会出现各种问题。所以java内存模型(JMM)屏蔽掉各种硬件和操作系统的内存访问差异,以实现让java程序在各种平台下都能达到一致的并发效果。
Java内存模型规定所有的变量都存储在主内存中,包括实例变量,静态变量,但是不包括局部变量和方法参数。每个线程都有自己的工作内存,线程的工作内存保存了该线程用到的变量和主内存的副本拷贝,线程对变量的操作都在工作内存中进行。线程不能直接读写主内存中的变量。
(就是因为这啊)不同的线程之间也无法访问对方工作内存中的变量。线程之间变量值的传递均需要通过主内存来完成。
JAVA多线程内存模型跟cpu缓存模型类似,是基于cpu缓存模型来建立的,java线程内存模型是标准化的,屏蔽掉了底层也不同计算机的区别。
volatile关键字
具备两个特性,一是可见性,二是禁止指令重排
以下也详细介绍了volatile关键字的作用和注意事项:严读啊!
这里先学习JVM才能更好的理解volatile关键字,人家第一个问题就是JVM是如何读写volatile修饰的变量的!
Q:volatile如何保证可见性?
[ 文心一言提供 ]
volatile
是Java中的一种关键字,用于提示编译器该变量可能会被多个线程同时修改,因此编译器、运行时和其他并发工具应避免对它们进行某些类型的优化,这可能会影响其他线程中的可见性。
在Java中,volatile
关键字的实现原理主要涉及两个方面:
- 可见性:Java内存模型对volatile变量有特殊的访问规则。对volatile变量的读操作会直接在主内存中进行,而不是从本地缓存中读取。写操作会立即写入主内存,而不是仅仅写入本地缓存。因此,volatile变量对所有线程都是立即可见的。
- 禁止指令重排序优化:Java内存模型允许编译器和处理器对指令进行重排序。但是,对于volatile字段的操作,JMM会禁止编译器和处理器进行重排序。也就是说,volatile字段的读/写操作不会被重排序。
具体来说,当一个线程写入一个volatile变量后,JMM会立即把这个变量的值同步到主内存,以及每次使用前立即从主内存刷新。这个操作是JMM自动完成的,无需程序员手动操作。因此,可以认为volatile变量是“被同步”的变量。
然而,虽然volatile
关键字可以保证可见性和禁止指令重排序,但它不能保证原子性。也就是说,volatile
无法保证复合操作(例如自增或自减等)在多线程环境中的安全性,这时就需要使用synchronized
关键字或java.util.concurrent.atomic
包中的原子类来确保操作的原子性。
JMM数据原子操作
- read(读取):从主内存读取数据
- load(载入):将主内存读取到的数据写入工作内存
- use (使用):从工作内存读取数据来计算
- assign(赋值):将计算好的值重新赋值到工作内存中
- store(存储):将工作内存数据写入主内存
- write(写入):将store过去的变量值赋值给主内存中的变量
- lock(锁定):将主内存变量加锁,标识为线程独占状态
- unlock(解锁):将主内存变量解锁,解锁后其他线程可以锁定该变量
未加volatile关键字的执行流程如下:
对于左线程,read操作读取了主内存的数据 ——> load操作将读取到的数据写入自己的工作内存 ——> 执行while循环这时执行了use操作,进入无限循环
对于右线程,read操作读取了主内存的数据 ——> load操作将读取到的数据写入自己的工作内存 ——> 执行use操作对initFlag进行了assign赋值操作 ——> 然后进行store操作将工作内存的数据写入主内存 ——> 然后执行write操作对主内存的变量进行赋值
缓存一致性协议(MESI)
简单说就是多个cpu从主内存读取同一个数据到各自的高速缓存,当其中某个cpu修改了缓存里的数据,该数据会马上同步回主内存,其他cpu通过总线嗅探机制可以感知到数据的变化从而将自己缓存里的数据失效
Volatile缓存可见性实现原理
底层实现主要通过汇编lock前缀指令,它会锁定这块内存区域的缓存(缓存行锁定)并回写到主内存
用inter cpu的开发手册里介绍就是通过硬件开启了MESI协议,所以左线程能够读到被右线程所修改回写给主线程后的值,这个值会在主线中存在一份,所以左能够读到,从而对自己工作内存中的相应数据进行更新
指令重排序与内存屏障
- 并发编程三大特性:可见性、有序性、原子性
- volatile保证可见性和有序性,但是不保证原子性,保证原子性需要借助synchronized这样的锁机制
- 指令重排序:在不影响单线程程序执行结果的前提下,计算机为了最大限度的发挥机器的性能,会对机器指令重排序优化
- 重排序会遵循as-if-serial与happen-before原则
- 阿里面试题:双重检测锁DCL对象半初始化问题
深入分析:volatile内存屏障+实现原理(JMM和MESI) - 知乎 (zhihu.com)
原子类
介绍
- 不可分割
- 一个操作是不可中断的,即便是多线程的情况下也可以保证
- java.util.concurrent.atomic
- 原子类的作用和锁类似,是为了保证并发情况下的线程安全。不过原子类相对于锁有优势
粒度更细:原子变量可以把竞争范围缩小到变量级别,这是我们可以获得的最细粒度的情况了,通常锁的粒度都要大于原子变量的粒度。
效率更高:通常,使用原子类的效率会比使用锁的效率更高,出了高度竞争的情况
常用类、方法
[ 文心一言提供 ]
- AtomicInteger:用于原子操作整数类型。常见的方法有:
* `get()`:获取当前值。
* `set(int newValue)`:设置当前值。
* `getAndIncrement()`:以原子方式将当前值加1。
* `getAndDecrement()`:以原子方式将当前值减1。
* `compareAndSet(int expect, int update)`:如果当前值等于预期值,则更新为新的值。
- AtomicLong:用于原子操作长整型。常见的方法有:
* `get()`:获取当前值。
* `set(long newValue)`:设置当前值。
* `getAndIncrement()`:以原子方式将当前值加1。
* `getAndDecrement()`:以原子方式将当前值减1。
* `compareAndSet(long expect, long update)`:如果当前值等于预期值,则更新为新的值。
- AtomicReference:用于原子操作引用类型。常见的方法有:
* `get()`:获取当前引用对象。
* `set(T newValue)`:设置当前引用对象。
* `compareAndSet(T expect, T update)`:如果当前引用对象等于预期对象,则更新为新的对象。
- AtomicStampedReference:用于原子操作带有版本号的引用类型。常见的方法有:
* `getReference()`:获取当前引用对象。
* `getStamp()`:获取当前版本号。
* `compareAndSet(V expectedReference, V newReference, int expectedStamp, int newStamp)`:如果当前引用对象和版本号等于预期值和版本号,则更新为新的引用对象和版本号。
- AtomicMarkableReference:用于原子操作带有标记位的引用类型。常见的方法有:
* `getReference()`:获取当前引用对象。
* `isMarked()`:检查是否标记。
* `compareAndSet(V expectedReference, V newReference, boolean expectedMarked, boolean newMarked)`:如果当前引用对象和标记位等于预期值和标记位,则更新为新的引用对象和标记位。
- AtomicIntegerArray:用于原子操作整数数组。常见的方法有:
* `get(int index)`:获取指定位置的元素值。
* `set(int index, int newValue)`:设置指定位置的元素值。
* `compareAndSet(int index, int expect, int update)`:如果指定位置的元素值等于预期值,则更新为新的值。
- AtomicLongArray:用于原子操作长整型数组。常见的方法有:
* `get(int index)`:获取指定位置的元素值。
* `set(int index, long newValue)`:设置指定位置的元素值。
* `compareAndSet(int index, long expect, long update)`:如果指定位置的元素值等于预期值,则更新为新的值。
- AtomicReferenceArray:用于原子操作引用类型数组。常见的方法有:
* `get(int index)`:获取指定位置的元素对象。
* `set(int index, T newValue)`:设置指定位置的元素对象。
* `compareAndSet(int index, T expect, T update)`:如果指定位置的元素对象等于预期对象,则更新为新的对
象。
累加器
Q:LongAdder为什么这么快?
只能用来计算加法,且从零开始计算
减少了乐观锁的重试次数
在深入一点就是减少了CAS导致的CPU空旋,占用CPU的情况,底部就是就是分段CAS,为了防止很多线程导致一直自旋导致cpu被占用,这里线程的值被分配到数组的几个位置,数组会自动缩容和扩容,最后在对和进行统计累加返回。
public class LongAdder extends Striped64 implements Serializable {
...
}
abstract class Striped64 extends Number {
...
}
import java.util.concurrent.atomic.LongAdder;
public class A {
LongAdder longAdder = new LongAdder();
public long getNum() {
return longAdder.longValue();
}
public void increase() {
longAdder.increment();
}
}
public long longValue() {
return sum();
}
public long sum() {
Cell[] as = cells; Cell a;
long sum = base;
if (as != null) {
for (int i = 0; i < as.length; ++i) {
if ((a = as[i]) != null)
sum += a.value;
}
}
return sum;
}
LongAccumulator
提供了自定义的函数操作
- 测试
package com.zky;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.atomic.AtomicLong;
import java.util.concurrent.atomic.LongAccumulator;
import java.util.concurrent.atomic.LongAdder;
class ClickNumber {
int number = 0;
public synchronized void clickBySynchronized() {
number++;
}
AtomicLong atomicLong = new AtomicLong(0);
public void clickByAtomicLong() {
atomicLong.getAndIncrement();
}
LongAdder longAdder = new LongAdder();
public void longAdder() {
longAdder.increment();
}
LongAccumulator longAccumulator = new LongAccumulator((x, y) -> x + y, 0);
public void clickByLongAccumulator() {
longAccumulator.accumulate(1);
}
}
// 需求:50个线程、每个线程100w次,总点赞数出来
public class Math {
public static final int _1W = 10000;
public static final int threadNumber = 50;
public static void main(String[] args) throws InterruptedException {
ClickNumber clickNumber = new ClickNumber();
long startTime;
long endTime;
CountDownLatch countDownLatch1 = new CountDownLatch(threadNumber);
CountDownLatch countDownLatch2 = new CountDownLatch(threadNumber);
CountDownLatch countDownLatch3 = new CountDownLatch(threadNumber);
CountDownLatch countDownLatch4 = new CountDownLatch(threadNumber);
startTime = System.currentTimeMillis();
for (int i = 0; i < threadNumber; i++) {
new Thread(() -> {
try {
for (int j = 0; j < 100 * _1W; j++) {
clickNumber.clickByLongAccumulator();
}
} finally {
countDownLatch3.countDown();
}
}, String.valueOf(i)).start();
}
countDownLatch3.await();
endTime = System.currentTimeMillis();
System.out.println(endTime - startTime + "ms" + " " + clickNumber.longAccumulator.get());
}
}
{
for (int j = 0; j < 100 * _1W; j++) {
clickNumber.clickByLongAccumulator();
}
} finally {
countDownLatch3.countDown();
}
}, String.valueOf(i)).start();
}
countDownLatch3.await();
endTime = System.currentTimeMillis();
System.out.println(endTime - startTime + "ms" + " " + clickNumber.longAccumulator.get());
}
}