线程安全概念:线程安全是多线程编程时的计算机程序代码中的一个概念。在拥有共享数据的多条线程并行执行的程序中,线程安全的代码会通过同步机制保证各个线程都可以正常且正确的执行,不会出现数据污染等意外情况。
线程安全的类:当多个线程访问某个类的方法或者类的实例对象时,不管运行时环境采用何种调度方式或者这些线程将如何交替执行,并且在主调代码中不需要任何额外的同步或协调操作(自身已经实现同步),这个类的方法的执行或者类的实例对象的修改,都能按照预期的结果反馈,那么这个类就是线程安全的。
或者说:一个类或者程序所提供的接口对于线程来说是原子操作或者多个线程之间的切换不会导致该接口的执行结果存在二义性,也就是说我们不用考虑同步的问题。那么这个类或者程序所提供的接口就是线程安全的。
多线程中的原子性,即一个操作或多个操作要么全部执行并且执行过程不能被打断,或者要么全部不执行。
典型的n++,n–操作:
两个线程A和B,分别对共享变量0执行+1操作1000次和-1操作一千次,等待两个线程都执行完,打印共享变量的值。
对于理想结果肯定是0,但是真实的结果每次都是随机数(是CPU调度线程是随机的)
package com.fastech.interview;
public class ThreadAtomicity {
private static int n = 0;
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> {
for(int i = 0;i < 1000;i++){
n++;
}
});
Thread t2 = new Thread(() -> {
for(int i = 0;i < 1000;i++){
n--;
}
});
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println(n);
}
}
原因是我们在写的一行Java代码可能不是原子性的,因为它编译成字节码,或者由JVM把字节码翻译为机器码后就不是一行,也就是多条执行操作。因为一次++或者–操作是分三步执行:
从内存把数据读到CPU
对数据进行更新操作
再把更新后的操作写入内存
我们以n++为例,执行过程以及内存情况见下图所示:
现在回到上面代码,当我们使用start方法启动两条线程,每个线程在执行n++操作或者n–操作时,CPU可能随时切换,比如:线程A在执行++操作时刚执行了两条指令(load n;和add R1 1)CPU就从线程A切换出去,此时save n;指令并没有执行;对于线程B,从内存中拿到的数据此时并不是线程A执行n++操作后的结果(++后的结果并没有写回内存),所以就破坏了线程A的原子性,导致数据不是预期的结果。
synchronized锁
lock锁
Atomic类型
具体操作
package com.fastech.interview;
import java.util.concurrent.atomic.AtomicInteger;
public class ThreadAtomicInteger {
private static AtomicInteger ai = new AtomicInteger(0);
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> {
for(int i = 0;i < 10000;i++){
ai.incrementAndGet();
}
});
Thread t2 = new Thread(() -> {
for(int i = 0;i < 10000;i++){
ai.decrementAndGet();
}
});
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println(ai.get());
}
}
以n++为例子:分为三步:读 计算 写
首先主存中有一个变量n = 0。
线程1从主存中读取变量n,并拷贝一份n = 0的副本到自己的高速缓存中。接下来执行n+1操作。自己副本中n = 1。接下来本应该强制将修改的值立即写入主存,但是时间片用完了(时间片用完了就必须停止,这里还没有来得及写入主存),线程1阻塞了。
然后线程2从主存中读取变量n,并拷贝一份n = 0的副本到自己的高速缓存中。接下来执行n+1操作,自己副本中的n = 1。接下来将n= 1写入主存。在写入主存过程中会通过一个总线嗅探机制告诉其它线程的副本n,你失效了。
过一会线程1又分配到了时间片,这个时候应该强制将修改的值立即写入主存,但是去高速缓存中拿值的时候发现自己的副本已经被标记失效了。然后就得重新去主存中拿n值。重新到主存中拿到的值n = 1。最后把n = 1写入主存。(读和计算的操作是已经执行过了的命令,所以它不会重新计算了,它只差写操作的命令)
本来是两个线程,分别对n执行+1操作,正确值应该为2,但是最终为1,显然是有问题的。
所以volatile只能保证变量的可见性,不能保证原子性。
n++是三个原子操作组合而成的复合操作。它本身就不是原子操作,不能依靠volatile这个保证可见性和有序性的关键字来保证原子性。
当多个线程访问同一变量的时候,其中一个线程修改了这个变量,其他线程必须要立刻知道这个变量被修改的值。
Java线程内存模型(JMM)是基于Cpu缓存模型建立的,它的作用是屏蔽掉不同硬件和操作系统的内存访问差异,实现各种平台具有一致的并发效果。
开始CPU是直接和主存进行交互的,但这样会有一个很大的问题,CPU的计算速度非常快,而主存是硬盘操作,比起CPU会慢很多很多,有时候CPU需要等待主存,导致效率很低。所以在CPU和主存之间加一个高速缓存作为缓冲,虽然高速缓存和CPU之间还存在速度差别,但比直接访问主存的效率高的多。
ps:这里的高速缓存是分成多级缓存,这里只是了解,简单画了一下。
多个线程工作的时候都是在**自己的工作内存中(CPU寄存器)**来执行操作的,不能直接操作主内存,线程之间是不可见的。
由于工作内存与主内存同步延迟,带来了可见性问题
下例程序有可见性问题:
package com.fastech.interview;
public class ThreadVisibility {
private static boolean flag = false;
public static void main(String[] args) throws InterruptedException {
new Thread(() -> {
while (!flag) {
}
System.out.println("t线程执行完毕");
}, "t").start();
Thread.sleep(500);
flag = true;
System.out.println("main线程执行完毕");
}
}
t线程中的while读取的是flag。在main线程中有设置成true,但是A线程的while 循环迟迟不退出,证明A线程读取的flag的值为flase,并没有读到最新的值。
volatile关键字:private static volatile boolean flag = false;
,将变量用这个关键字修饰,就可以保证另一个线程修改这个值时对当前线程可见。
synchronized锁:如果涉及到了synchronized的同步代码块或者是同步方法,获取锁资源之后,将内部涉及到的变量从工作内存中移除,必须去主内存中重新拿数据,而且在释放锁之后,会立即将工作内存中的数据同步到主内存。
具体操作是在while循环里面添加以下代码synchronized (ThreadVisibility.class) {}
lock锁:Lock锁保证可见性的方式和synchronized完全不同,synchronized基于他的内存语义,在获取锁和释放锁时,对工作内存做一个同步到主内存的操作
Lock锁是基于volatile实现的。Lock锁内部在进行加锁和释放锁时,会对一个由volatile修饰的state属性进行加减操作
如果对volatile修饰的属性进行写操作,CPU会执行带有lock前缀的指令,CPU会将修改的数据,从工作内存立即同步到主内存,同时也会将其他的属性也立即同步到主内存中。还会将其他工作内存行中的这个数据设置为无效,必须重新从主内存中拉取(缓存一致性协议)
具体实现
package com.fastech.interview;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class ThreadVisibility {
private static boolean flag = false;
private static Lock lock = new ReentrantLock();
public static void main(String[] args) throws InterruptedException {
new Thread(() -> {
while (!flag) {
try {
lock.lock();
} finally {
lock.unlock();
}
}
System.out.println("t线程执行完毕");
}, "t").start();
Thread.sleep(500);
flag = true;
System.out.println("main线程执行完毕");
}
}
Atomic类型:
具体操作
package com.fastech.interview;
import java.util.concurrent.atomic.AtomicBoolean;
public class ThreadVisibility2 {
private static AtomicBoolean flag = new AtomicBoolean(false);
public static void main(String[] args) throws InterruptedException {
new Thread(() -> {
while (!flag.get()) {
}
System.out.println("t线程执行完毕");
}, "t").start();
Thread.sleep(500);
flag.set(true);
System.out.println("main线程执行完毕");
}
}
有序性指程序执行的顺序按照代码的先后顺序执行。
那为什么会出现不一致的情况呢?这是由于指令重排序的缘故。
在执行程序时,为了提高性能,编译器和处理器常常会做指令重排序;指令重排序不会影响单线程的执行结果,但是在并发情况下,可能会出现诡异的BUG。
先天有序性,即不需要任何额外的代码控制即可保证有序性,java内存模型一共列出了八种Happens-before规则,如果两个线程不能从 happens-before原则观察出来,那么就不能观察他们的有序性,虚拟机可以随意的对他们进行重排序,导致其结果杂乱无序。
ps:第一条规则要注意理解,这里只是程序的运行结果看起来像是顺序执行,虽然结果是一样的,但是jvm为了提高性能可能会进行指令重排序,而jvm在单线程下的指令重排序是不会影响到运行结果的,多线程下的指令重排序是会产生线程安全的问题的。
下列程序就会产生指令重排序
package com.fastech.interview;
public class ThreadOrderliness {
private static int x, y;
private static int a, b;
public static void main(String[] args) throws InterruptedException {
int i = 0;
for (; ; ) {
i++;
x = 0;
y = 0;
a = 0;
b = 0;
Thread t1 = new Thread(() -> {
a = 1; // step1
x = b; // step2
});
Thread t2 = new Thread(() -> {
b = 1; // step3
y = a; // step4
});
t1.start();
t2.start();
t1.join();
t2.join();
if (x == 0 && y == 0) {
System.out.println("第" + i + "次:x=" + x + ",y=" + y);
break;
}
}
}
}
我电脑上的运行结果:第3918次:x=0,y=0
仔细看上诉代码,正常来说只有三个结果:[10],[01],[11],但是为什么会出现[00]呢?
这就是典型的指令重排序了,出现这样的结果就必须保证step2或者step4先于step1和step3执行!
通过thread的join方法保证多线程的顺序执行
具体操作
package com.fastech.interview;
/**
* @ClassName: ThreadOrderliness
* @Description:
* @Author: zhangjin
* @Date: 2023/3/28
*/
public class ThreadOrderliness {
private static int x, y;
private static int a, b;
public static void main(String[] args) throws InterruptedException {
int i = 0;
for (; ; ) {
i++;
x = 0;
y = 0;
a = 0;
b = 0;
Thread t1 = new Thread(() -> {
a = 1;
x = b;
});
t1.start();
t1.join();
Thread t2 = new Thread(() -> {
b = 1;
y = a;
});
t2.start();
t2.join();
if (x == 0 && y == 0) {
System.out.println("第" + i + "次:x=" + x + ",y=" + y);
break;
}
}
}
}
main方法里面先后运行t1,t2,那么t1.start()之后,运行t1.join()。这是会让主线程mian等待新的线程t1执行完了,再执行主线程mian下面的代码,t1.join()是让主线程main wait。
volatile关键字,声明下x,y,a,b变量时加上volatile关键字即可
synchronized锁
Lock锁
ps:Atomic类型无法保证有序性。可以将上面例子中的x,y,a,b变量都换成AtomicInteger类型进行验证!
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-MB0slifb-1679996145813)(.\picture\13965490-6f512bb9647230f6.webp)]
上面五点总结来说,线程不安全的本质原因就是有竞态条件。对于共享资源的修改不能做到原子化、对顺序敏感的时候就会导致线程不安全,上面的所有情况根因都是这个,只是表现形式不同而已。
所以,基本上掌握了这个本质,就可以分析所有线程不安全的情况。
ps:当两个线程竞争同一资源时,如果对资源的访问顺序敏感,就称存在竞态条件。
线程安全性的分类包括:不可变、绝对线程安全、有条件线程安全、线程兼容和线程对立。只要明确地记录下线程安全特性,那么您是否使用这种系统都没关系。这种系统有其局限性 – 各类之间的界线不是百分之百地明确,而且有些情况它没照顾到 – 但是这套系统是一个很好的起点。这种分类系统的核心是为了让调用者明确是否可以或者必须用外部同步操作(或者一系列操作)来确保线程安全。
一个不可变的对象只要构建正确, 其外部可见状态永远不会改变, 永远也不会看到它处于不一致的状态。
不可变(Immutable) 的对象一定是线程安全的,不需要再采取任何的线程安全保障措施。多线程环境下,应当尽量使对象成为不可变,来满足线程安全。
private final int value;
ps:原子类 AtomicInteger 和 AtomicLong 等都是可变的。
线程安全性类的对象操作序列( 读或写其公有字段以及调用其公有方法) 都不会使该对象处于无效状态, 即任何操作都不会违反该类的任何不可变量、前置条件或者后置条件。
绝对线程安全的实现,通常需要付出很大的、甚至不切实际的代价。Java API中提供的线程安全,大多数都不是绝对线程安全。
例如,对于数组集合Vector的操作,如get()
、add()
、remove()
都是有synchronized
关键字修饰。但是在多线程同时调用时也需要手动添加同步手段,保证多线程的安全。
下面的代码看似不需要同步,实际运行过程中会报错。
package com.fastech.interview;
import java.util.Vector;
public class VectorTest {
public static void main(String[] args) {
Vector<Integer> vector = new Vector<>();
while (true) {
for (int i = 0; i < 10; i++) {
vector.add(i);
}
new Thread(() -> {
for (int i = 0; i < vector.size(); i++) {
System.out.println("获取vector的第" + i + "个元素: " + vector.get(i));
}
}).start();
new Thread(() -> {
for (int i=0;i<vector.size();i++){
System.out.println("删除vector中的第" + i+"个元素");
vector.remove(i);
}
}).start();
while (Thread.activeCount()>20) {
return;
}
}
}
}
出现ArrayIndexOutOfBoundsException
异常,原因:某个线程恰好删除了元素i,使得当前线程无法访问元素i。
Exception in thread "Thread-3" java.lang.ArrayIndexOutOfBoundsException: Array index out of range: 28
at java.util.Vector.remove(Vector.java:836)
at com.fastech.interview.VectorTest.lambda$main$1(VectorTest.java:26)
at java.lang.Thread.run(Thread.java:748)
需要将对元素的get和remove构造成同步代码块:
new Thread(() -> {
synchronized (vector) {
for (int i = 0; i < vector.size(); i++) {
System.out.println("获取vector的第" + i + "个元素: " + vector.get(i));
}
}
}).start();
new Thread(() -> {
synchronized (vector) {
for (int i=0;i<vector.size();i++){
System.out.println("删除vector中的第" + i+"个元素");
vector.remove(i);
}
}
}).start();
有条件的线程安全类对于单独的操作可以是线程安全的, 但是某些操作序列可能需要外部同步。
大部分的线程安全类都属于有条件线程安全,如Java容器中的Vector
、HashTable
、ConcurrentHashMap
、通过Collections.synchronizedXXX()
方法包装的集合等。
线程兼容类不是线程安全的, 但可以通过正确使用同步从而在并发环境中安全地使用。或用一个synchronized 块包含每一个方法调用。
如常见的ArrayList
、HashMap
等都是线程兼容的。
线程对立类是那些不管是否调用了外部同步都不能在并发使用时保证其安全的类。
线程对立类很少见, 当类修改静态数据,而静态数据会影响在其它线程中执行的其它类的行为时, 通常会出现线程对立。
线程对立的常见操作有:Thread类的suspend()
和resume()
(已经被JDK声明废除),System.setIn()
和System.setOut()
等。
我们都知道StringBuffer线程安全,StringBuilder线程不安全,但是如何证明呢?
分别用1000个线程写StringBuffer和StringBuilder,
使用CountDownLatch保证在各自1000个线程执行完之后才打印StringBuffer和StringBuilder长度,
观察结果。
package com.fastech.interview;
import java.util.concurrent.CountDownLatch;
public class Test {
public static void main(String[] args) {
StringBuilder stringBuilder = new StringBuilder();
StringBuffer stringBuffer = new StringBuffer();
CountDownLatch latch1 = new CountDownLatch(1000);
CountDownLatch latch2 = new CountDownLatch(1000);
for (int i = 0; i < 1000; i++) {
new Thread(() -> {
try {
stringBuilder.append(1);
} catch (Exception e) {
e.printStackTrace();
} finally {
latch1.countDown();
}
}).start();
}
for (int i = 0; i < 1000; i++) {
new Thread(() -> {
try {
stringBuffer.append(1);
} catch (Exception e) {
e.printStackTrace();
} finally {
latch2.countDown();
}
}).start();
}
try {
latch1.await();
System.out.println(stringBuilder.length());
latch2.await();
System.out.println(stringBuffer.length());
} catch (Exception e) {
e.printStackTrace();
}
}
}
StringBuffer不论运行多少次都是1000长度。
StringBuilder绝大多数情况长度都会小于1000。
StringBuffer线程安全,StringBuilder线程不安全得到证明。
打开StringBuffer源码就会发现所有写操作都被synchronized修饰了,所以所有修改操作都是串行的。
而StringBuilder的写操作则没有使用synchronized进行修饰,也不包含其他串行化修改的算法。
由于HashMap并非是线程安全的,所以在高并发的情况下必然会出现问题,这是一个普遍的问题,接下来了解这个死循环是如何产生的。
如果是在单线程下使用HashMap,自然是没有问题的,如果后期由于代码优化,这段逻辑引入了多线程并发执行,在一个未知的时间点,会发现CPU占用100%,居高不下,通过查看堆栈,你会惊讶的发现,线程都Hang在hashMap的get()方法上,服务重启之后,问题消失,过段时间可能又复现了。
这是为什么?
在了解来龙去脉之前,我们先看看HashMap的数据结构。
在内部,HashMap使用一个Entry数组保存key、value数据,当一对key、value被加入时,会通过一个hash算法得到数组的下标index,算法很简单,根据key的hash值,对数组的大小取模 hash & (length-1),并把结果插入数组该位置,如果该位置上已经有元素了,就说明存在hash冲突,这样会在index位置生成链表。
如果存在hash冲突,最惨的情况,就是所有元素都定位到同一个位置,形成一个长长的链表,这样get一个值时,最坏情况需要遍历所有节点,性能变成了O(n),所以元素的hash值算法和HashMap的初始化大小很重要。
当插入一个新的节点时,如果不存在相同的key,则会判断当前内部元素是否已经达到阈值(默认是数组大小的0.75),如果已经达到阈值,会对数组进行扩容,也会对链表中的元素进行rehash。
HashMap的put方法实现:
1、判断key是否已经存在
public V put(K key, V value) {
if (key == null)
return putForNullKey(value);
int hash = hash(key);
int i = indexFor(hash, table.length);
// 如果key已经存在,则替换value,并返回旧值
for (Entry<K,V> e = table[i]; e != null; e = e.next) {
Object k;
if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
V oldValue = e.value;
e.value = value;
e.recordAccess(this);
return oldValue;
}
}
modCount++;
// key不存在,则插入新的元素
addEntry(hash, key, value, i);
return null;
}
2、检查容量是否达到阈值threshold
void addEntry(int hash, K key, V value, int bucketIndex) {
if ((size >= threshold) && (null != table[bucketIndex])) {
resize(2 * table.length);
hash = (null != key) ? hash(key) : 0;
bucketIndex = indexFor(hash, table.length);
}
createEntry(hash, key, value, bucketIndex);
}
如果元素个数已经达到阈值,则扩容,并把原来的元素移动过去。
3、扩容实现
void resize(int newCapacity) {
Entry[] oldTable = table;
int oldCapacity = oldTable.length;
...
Entry[] newTable = new Entry[newCapacity];
...
transfer(newTable, rehash);
table = newTable;
threshold = (int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1);
}
这里会新建一个更大的数组,并通过transfer方法,移动元素。
void transfer(Entry[] newTable, boolean rehash) {
int newCapacity = newTable.length;
for (Entry<K,V> e : table) {
while(null != e) {
Entry<K,V> next = e.next;
if (rehash) {
e.hash = null == e.key ? 0 : hash(e.key);
}
int i = indexFor(e.hash, newCapacity);
e.next = newTable[i];
newTable[i] = e;
e = next;
}
}
}
移动的逻辑也很清晰,遍历原来table中每个位置的链表,并对每个元素进行重新hash,在新的newTable找到归宿,并插入。
假设HashMap初始化大小为4,插入个3节点,不巧的是,这3个节点都hash到同一个位置,如果按照默认的负载因子的话,插入第3个节点就会扩容。
为了验证效果,假设负载因子是1。
void transfer(Entry[] newTable, boolean rehash) {
int newCapacity = newTable.length;
for (Entry<K,V> e : table) {
while(null != e) {
Entry<K,V> next = e.next;
if (rehash) {
e.hash = null == e.key ? 0 : hash(e.key);
}
int i = indexFor(e.hash, newCapacity);
e.next = newTable[i];
newTable[i] = e;
e = next;
}
}
}
以上是节点移动的相关逻辑。
插入第4个节点时,发生rehash,假设现在有两个线程同时进行,线程1和线程2,两个线程都会新建新的数组。
假设 线程2 在执行到Entry
之后,cpu时间片用完了,这时变量e指向节点a,变量next指向节点b。
线程1继续执行,很不巧,a、b、c节点rehash之后又是在同一个位置7,开始移动节点
第一步,移动节点a
第二步,移动节点b
注意,这里的顺序是反过来的,继续移动节点c
这个时候 线程1 的时间片用完,内部的table还没有设置成新的newTable, 线程2 开始执行,这时内部的引用关系如下:
这时,在 线程2 中,变量e指向节点a,变量next指向节点b,开始执行循环体的剩余逻辑。
Entry<K,V> next = e.next;
int i = indexFor(e.hash, newCapacity);
e.next = newTable[i];
newTable[i] = e;
e = next;
执行一次循环之后的引用关系如下图
执行后,变量e指向节点b,因为e不是null,则继续执行循环体,执行后的引用关系
变量e又重新指回节点a,只能继续执行循环体,这里仔细分析下:
1、执行完Entry
,目前节点a没有next,所以变量next指向null;
2、e.next = newTable[i];
其中 newTable[i] 指向节点b,那就是把a的next指向了节点b,这样a和b就相互引用了,形成了一个环;
3、newTable[i] = e
把节点a放到了数组i位置;
4、e = next;
把变量e赋值为null,因为第一步中变量next就是指向null;
所以最终的引用关系是这样的:
节点a和b互相引用,形成了一个环,当在数组该位置get寻找对应的key时,就会查询该链表然后遍历查询,会产生死循环。
另外,如果线程2把newTable设置成到内部的table,节点c的数据就丢了,看来还有数据遗失的问题。
曾经有人把这个问题报给了Sun,不过Sun不认为这是一个bug,因为在HashMap本来就不支持多线程使用,要并发就用ConcurrentHashMap。
其实这个问题已经在java1.8版本被修复了,只在1.7版本之前存在这个问题。
大致原因是在HashMap扩容的时候链表采用了头插法会使链表反序,两个线程同时扩容的话,在某种场景下会出现循环链表导致死循环。
java1.8修复的方法是将头插法改成了尾插法,避免了这个死循环问题。可以看一下resize的代码,很容易理解。
那么java1.8就不会出现死循环问题了吗?不是的,在多线程操作红黑树的时候依然有可能会导致死循环,不过这个地方的具体原因没有找到详细文章说明,待证明。
所以在并发的情况,发生扩容时,可能会产生循环链表,在执行get的时候,会触发死循环,引起CPU的100%问题,所以一定要避免在并发环境下使用HashMap。
都知道,HashTable是线程安全的,它是给所有方法增加了synchronized。
这样可以达到线程安全吗?答案是可以的。
这种方法的原理是什么?原理是synchronized都需要获取当前实例的锁,static synchronized都需要获取当前类的锁。
synchronized和static synchronized修饰的两个方法之间是同步的吗?不是,因为需要获取的锁不一样。如下代码所示,methodA和methodD是同步的,methodB和methodC是同步的,断点调试会发现当一个线程获取到锁之后,进入RUNNING状态执行后续代码。另一个线程在尝试获取锁的时候会进入到MONITOR状态等待锁。
package com.fastech.interview;
import java.util.concurrent.CountDownLatch;
public class Lock {
public static synchronized void methodA() {
System.out.println("aaa");
}
public synchronized void methodB() {
System.out.println("bbb");
}
public void methodC() {
synchronized (this) {
System.out.println("ccc");
}
}
public void methodD() {
synchronized (Lock.class) {
System.out.println("ddd");
}
}
public static void main(String[] args) throws InterruptedException {
Lock lock = new Lock();
new Thread(Lock::methodA).start();
new Thread(lock::methodB).start();
new Thread(lock::methodC).start();
new Thread(lock::methodD).start();
CountDownLatch countDownLatch = new CountDownLatch(1);
countDownLatch.await();
}
}
HashTable的实现有什么问题?问题很明显,加锁的粒度太粗,我们如果只是为了线程安全,只需要在产生竞态条件的地方加锁就可以了,不需要全部代码加锁,会很影响性能。
我们知道,多线程下一定要用ConcurrentHashMap。但是它是怎么做到线程安全的呢?这个还是很有意思的。
有一篇ConcurrentHashMap是如何实现线程安全的写的很好:
ConcurrentHashMap是如何实现线程安全的
还有看的一个公众号的文章写的很好,记录一下
死磕 java集合之ConcurrentHashMap源码分析(一)
死磕 java集合之ConcurrentHashMap源码分析(二)
死磕 java集合之ConcurrentHashMap源码分析(三)
首先,是的。如果不是,是因为你对ConcurrentHashMap的线程安全有误解。看下面的例子,这段代码不是线程安全的,最终输出的结果未必是2000。
但这不能说明ConcurrentHashMap不是线程安全的,只能说明下面这段代码不是线程安全的
线程安全是需要考虑一个代码范围或者逻辑范围的,在这个范围内如果程序对多线程下各个环节的执行顺序敏感的话,就存在竞态条件,存在竞态条件的代码不做同步的话,就会出现线程不安全的情况。
所以,ConcurrentHashMap提供的方法是线程安全的,但是先get数据、进行计算、然后再put数据这种逻辑是线程不安全的。
package com.fastech.interview;
import java.io.IOException;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CountDownLatch;
public class TestConcurrentHashMap {
static final ConcurrentHashMap<String, Integer> map = new ConcurrentHashMap<>();
public static void increment(String key) {
map.put(key, map.getOrDefault(key, 0) + 1);
}
public static void main(String[] args) {
CountDownLatch latch = new CountDownLatch(2);
new Thread(() -> {
try {
for (int i = 0; i < 1000; i++) {
increment("test");
}
} catch (Exception e) {
e.printStackTrace();
} finally {
latch.countDown();
}
}).start();
new Thread(() -> {
try {
for (int i = 0; i < 1000; i++) {
increment("test");
}
} catch (Exception e) {
e.printStackTrace();
} finally {
latch.countDown();
}
}).start();
try {
latch.await();
System.out.println(map.get("test"));
} catch (Exception e) {
e.printStackTrace();
}
}
}
参考文章地址:
https://blog.csdn.net/qq_58710208/article/details/123946843
https://blog.csdn.net/yuiop123455/article/details/108273951
https://blog.csdn.net/u014454538/article/details/98515807
https://blog.csdn.net/litterfrog/article/details/76862435
https://juejin.im/post/5a66a08d5188253dc3321da0
https://blog.csdn.net/lijianqingfeng/article/details/118462805