J.U.C

前言

首先我们要编写一些代码的例子,以便更好测试和理解哪个操作是线程安全,那些操作不是线程安全的。

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
@Slf4j
public class Test {
// 请求总数
private static int requestTotal = 8000;
// 线程总数
private static int threadTotal = 200;
// 共享变量
private static int count = 0;
public static void main(String[] args) throws InterruptedException {
ExecutorService threadPool = Executors.newCachedThreadPool();
final Semaphore semaphore = new Semaphore(threadTotal);
final CountDownLatch countDownLatch = new CountDownLatch(requestTotal);
for (int i = 0; i < requestTotal; i++) {
threadPool.execute(() -> {
try {
semaphore.acquire();
add();
semaphore.release();
} catch (Exception e){
log.error("error",e);
} finally {
countDownLatch.countDown();
}
});
}
countDownLatch.await();
threadPool.shutdown();
log.info("共享变量数:{}",count);
}
// 主要关注的点
private static void add(){
count++;
}
}

该代码就是利用多线程执行 count++ 操作!
很明显执行后的结构肯定是小于八千的,主要原因是count++这个操作并非原子性。多线程会影响其count的取值。

Atomic 包

Atomic包里面有很多基本类型的线程安全类,当我们把上诉代码中的Int缓存AtomicInteger此代码就是线程安全的。
看下源码

1
2
3
4
5
6
7
8
9
10
11
12
13
public final int getAndIncrement() {
return unsafe.getAndAddInt(this, valueOffset, 1);
}

public final int getAndAddInt(Object var1, long var2, int var4) {
int var5;
do {
var5 = this.getIntVolatile(var1, var2);
} while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));

return var5;
}
public final native boolean compareAndSwapInt(Object var1, long var2, int var4, int var5);

关键代码是native修饰,并不是由java实现。但是从这简单的代码,我第一次看到就猜到了这不就是乐观锁么。事实也确实如此。compareAndSwapInt 方法就是在作比较成功就交换的动作。一只不断尝试。所以Atomic包下的类基本就是利用这种CAS无锁操作。来实现并发支持。理论上这种循环都是很快就能够完成的。比锁的效率要高。
不建议在有大量更新的情况下使用。更多的是作为一些状态的标记来使用。

其他的Atomic包下的类有:

  • AtomicLong LongAdder
  • ActomicBoolean
  • ActomicReference
  • ActomicIntegerFieldUpdate 更新某个类的字段 字段必须是volatile
  • ActomicStampReference CAS的ABA的问题

就先不多做介绍了。

并发集合

我们都知道我们常用的ArrayList、LinkedList都不是线程安全的,并发情况下会出现数据BUG。
那有哪些List是线程安全的了?

Vector

vector并不是J.U.C下的包。但是他确实是一个线程安全的集合和ArrayList一样,底层都是由数组实现。不同的是它的基本所有的方法都加了synchronized加锁,以保证线程的安全性。

Stack

stack也不是J.U.C下的包。他是一个线程安全的集合。和Vector一样也是加了synchronized保证线程安全性。stack是一个算法中常用的模型。基于FILO原则。

CopyOnWriteArrayList

J.U.C下的并发类,简单看下它的部分内部源码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public boolean add(E e) {
final ReentrantLock lock = this.lock;
lock.lock();
try {
Object[] elements = getArray();
int len = elements.length;
// copy一个新数组
Object[] newElements = Arrays.copyOf(elements, len + 1);
newElements[len] = e;
setArray(newElements);
return true;
} finally {
lock.unlock();
}
}
public E get(int index) {
return get(getArray(), index);
}

重点在add方法,首先利用ReentrantLock这种独占锁保持线程安全。在add操作时,先copy一份数组出来,更新后在写会原数据。这种方式保证了读取的效率。读数据不会受写数据的影响。但是这中缺点也比较明显。

  1. 保证了最终一致性,但是实时性比较差
  2. 由于每次更新都要copy一次整个数据,对于数据量偏大的数据容器引起YGC、FGC的操作。

所以这个比较适合数据量不大,读多写少,而且实时性安全不是很严格的情况下。该数据实际使用的情况不多。

CopyOnWriteArraySet

底层使用了CopyOnWriteArrayList来实现,唯一的区别就是List和Set的区别。


ConcurrentSkipListMap、ConcurrentSkipListSet

ConcurrentSkipListMap底层采用了一种跳表的数据形式,实际是一种链表。底层通过CAS的方式保证了线程安全性。 ConcurrentSkipListSet底层使用了ConcurrentSkipListMap来实现
注意两者都不支持null的处理,同时对于addAll等批量操作,不能保证安全性。只能保证单数据操作的安全性

ConcurrentHashMap

用的最多的并发类,可以查看其他文档。

其他

Collections.synchronizedXXX:collection、List、Set、Map

我们可以通过Collections工具类来直接生成一个线程安全的集合,实现比较简单,通过synchronized关键字来保证线程安全