3.Java并发编程-Java多线程之线程安全
线程安全
当多个线程访问某个类时,不管运行时环境采用何种调度方式或者这些进程如何交替执行,并且在主调代码中,不需要任何额外的同步或协同,这个类都能表现出正确的行为,那么就称这个类是线程安全的。
线程安全特性
- 原子性:同一时刻只能有一个线程对它进行操作。
- 可见性:一个线程对主内存的修改,可以及时的被其他线程观察到。
- 有序性:一个线程观察其他线程的指令执行顺序,由于指令重排序的存在,该观察结果一般杂乱无序。
原子性:
一个操作或者多个操作,要么全部执行并且执行的过程不会被任何因素打断,要么就都不执行。
Atomic类:
Atomic包采用CAS算法:
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; }
var1为操作的对象,var2为该对象当前值,var4为该对象增加值,var5为底层当前值。
CAS(Compare And Swap)实现的核心原理:用当前对象的值(工作内存)与底层(主内存)的值进行对比,如果当前值等于底层值,则执行对应的操作,如果不相等,则继续循环取值,直到相等(死循环),不断进行循环尝试。
CAS的操作过程:CAS比较的过程可以通俗的理解为CAS(V,O,N),包含三个值分别为:V内存地址实际存放的值;O预期的值(旧值),N更新的新值,当V和O相同时,也就是说明旧值和内存中实际的值相同,表明该值没有被其他线程修改过,即该旧值就是目前来说最新的值了,自然可以将新值N赋值给V,反之V和O不相同,表明该值已经被其他线程修改过,则该旧值O不是最新的版本,所以不能将该值N赋值给V,返回V即可,当多个线程使用CAS操作一个变量时,只有一一个线程会成功,并成功更新,其余会失败,失败的线程会重新尝试,当然也可以选择挂起线程。
原子类型划分
普通原子类型:提供对boolean、int、long和对象的原子性操作:AtomicBoolean、AtomicInteger、AtomicLong、AtomicReference
AtomicBoolean
//ThreadSafe public class AtomicBooleanTest { private static AtomicBoolean isHappened = new AtomicBoolean(false); public static int clientTotal = 5000;// 请求总数 public static int threadTotal = 200;// 同时并发执行的线程数 public static void main(String[] args) throws Exception { ExecutorService executorService = Executors.newCachedThreadPool(); final Semaphore semaphore = new Semaphore(threadTotal); final CountDownLatch countDownLatch = new CountDownLatch(clientTotal); for (int i = 0; i < clientTotal ; i++) { executorService.execute(() -> { try { semaphore.acquire(); method(); semaphore.release(); } catch (Exception e) { e.printStackTrace(); } countDownLatch.countDown(); }); } countDownLatch.await(); executorService.shutdown(); System.out.println("isHappened: " + isHappened.get()); } private static void method() { if (isHappened.compareAndSet(false, true)) { System.out.println("executing..."); } } }
AtomicInteger:
//ThreadSafe public class AtomicIntegerTest { public static int clientTotal = 5000;// 请求总数 public static int threadTotal = 200;// 同时并发执行的线程数 public static AtomicInteger count = new AtomicInteger(0); public static void main(String[] args) throws Exception { ExecutorService executorService = Executors.newCachedThreadPool(); final Semaphore semaphore = new Semaphore(threadTotal); final CountDownLatch countDownLatch = new CountDownLatch(clientTotal); for (int i = 0; i < clientTotal ; i++) { executorService.execute(() -> { try { semaphore.acquire(); add(); semaphore.release(); } catch (Exception e) { e.printStackTrace(); } countDownLatch.countDown(); }); } countDownLatch.await(); executorService.shutdown(); System.out.println("count:" + count.get()); } private static void add() { count.incrementAndGet(); // count.getAndIncrement(); } }
AtomicLong
//ThreadSafe public class AtomicLongTest { public static int clientTotal = 5000;// 请求总数 public static int threadTotal = 200;// 同时并发执行的线程数 public static AtomicLong count = new AtomicLong(0); public static void main(String[] args) throws Exception { ExecutorService executorService = Executors.newCachedThreadPool(); final Semaphore semaphore = new Semaphore(threadTotal); final CountDownLatch countDownLatch = new CountDownLatch(clientTotal); for (int i = 0; i < clientTotal ; i++) { executorService.execute(() -> { try { semaphore.acquire(); add(); semaphore.release(); } catch (Exception e) { e.printStackTrace(); } countDownLatch.countDown(); }); } countDownLatch.await(); executorService.shutdown(); System.out.println("count:" + count.get()); } private static void add() { count.incrementAndGet(); // count.getAndIncrement(); } }
AtomicReference
//ThreadSafe
public class AtomicReferenceTest {
private static AtomicReference<Integer> count = new AtomicReference<>(0);
public static void main(String[] args) {
count.compareAndSet(0, 2); // 2
count.compareAndSet(0, 1); // no
count.compareAndSet(1, 3); // no
count.compareAndSet(2, 4); // 4
count.compareAndSet(3, 5); // no
System.out.println("result: " + count.get());//4
}
}
原子类型数组:提供对数组元素的原子性操作。AtomicLongArray、AtomicIntegerArray、AtomicReferenceArray
AtomicLongArray
AtomicIntegerArray
AtomicReferenceArray
AtomicLong与LongAdder
AtomicLong的原理是依靠底层的cas来保障原子性的更新数据(在死循环内不断尝试修改目标值,直至修改成功),在要添加或者减少的时候,会使用自循(CLH)方式不断地cas到特定的值,从而达到更新数据的目的。然而在线程竞争激烈的情况下,自循往往浪费很多计算资源才能达成预期效果。
对于普通类型的变量long,double类型的变量,JVM允许将64位的读操作或写操作拆成两个32位的操作。
LongAdder将热点数据分离(将AtomicLong的内部核心数据value分离成一个数组,每个线程访问时,通过hash等算法映射到其中一个数字进行计数,最终的技术结果则为这个数组的求和累加,其中热点数据的value被分离成多个单元的cell,每个cell独自维护内部的值,当前对象的实际值由多个cell累计合成,这样热点就进行了有效的分离,提高了并行度,在AtomicLong的基础上,将单点的更新压力分散到各个节点上,在低并发的情况下,通过对base的直接更新,可以保障和AtomicLong的性能基本一致,而在高并发的情况下则通过分散提高了性能)。
LongAdder的缺点:如果在统计的时候有并发更新,则可能会导致统计的数据存在误差。
|
|
|
|
原子类型字段更新器:提供对指定对象的指定字段进行原子性操作 |
AtomicLongFieldUpdater AtomicIntegerFieldUpdater AtomicReferenceFieldUpdater |
带版本号的原子引用类型:以版本戳的方式解决原子类型的ABA问题 |
AtomicStampedReference AtomicMarkableReference |
原子累加器(JDK1.8):AtomicLong和AtomicDouble的升级类型,专门用于数据统计,性能更高 |
DoubleAccumulator DoubleAdder LongAccumulator LongAdder |
LongAdder
//ThreadSafe
public class LongAdderTest{
public static int clientTotal = 5000;// 请求总数
public static int threadTotal = 200;// 同时并发执行的线程数
public static LongAdder count = new LongAdder();
public static void main(String[] args) throws Exception {
ExecutorService executorService = Executors.newCachedThreadPool();
final Semaphore semaphore = new Semaphore(threadTotal);
final CountDownLatch countDownLatch = new CountDownLatch(clientTotal);
for (int i = 0; i < clientTotal ; i++) {
executorService.execute(() -> {
try {
semaphore.acquire();
add();
semaphore.release();
} catch (Exception e) {
e.printStackTrace();
}
countDownLatch.countDown();
});
}
countDownLatch.await();
executorService.shutdown();
System.out.println("count:" + count);
}
private static void add() {
count.increment();
}
}
AtomicIntegerFieldUpdater
//ThreadSafe
@Data
public class AtomicIntegerFieldUpdaterTest {
private static AtomicIntegerFieldUpdater<AtomicIntegerFieldUpdaterTest> updater =
AtomicIntegerFieldUpdater.newUpdater(AtomicIntegerFieldUpdaterTest.class, "count");
@Getter
public volatile int count = 100;
public static void main(String[] args) {
AtomicIntegerFieldUpdaterTest example = new AtomicIntegerFieldUpdaterTest();
if (updater.compareAndSet(example, 100, 120)) {
System.out.println("update success 1 : " + example.getCount());
}
if (updater.compareAndSet(example, 100, 120)) {
System.out.println("update success 2 : " + example.getCount());
} else {
System.out.println("update failed : " + example.getCount());
}
}
}
update success 1 : 120
update failed : 120
ABA问题:
CAS机制的原理由CPU支持的原子操作,其原子性是在硬件层面进行保证的。
而CAS机制可能会出现ABA问题,即T1读取内存变量为A,T2修改内存变量为B,T2修改内存变量为A,这时T1再CAS操作A时是可行的。但实际上在T1第二次操作A时,已经被其他线程修改过了。
解决方案:添加版本号。
锁实现原子性:
synchronized |
依赖JVM。在类的继承过程中synchronized不具有传递性。synchronized不属于方法声明的一部分。 |
lock |
依赖特殊的CPU指令,代码实现,如ReenTrantLock; |
原子性对比:
Atomic |
竞争激烈时能维持常态,比lock性能好,只能同步一个值。 |
synchronized |
不可中断锁,适合竞争不激烈,可读性好。 |
Lock |
可中断锁,多样化同步,竞争激烈时能维持常态。 |
可见性:
可见性是指当多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即看得到修改的值。
导致共享变量在线程不可见的原因:
线程交叉执行。 |
重排序结合线程交叉执行。 |
共享变量更新后的值没有在工作内存与主存之间及时更新。 |
synchronized关键字
JMM关于synchronized的两条规定:
线程解锁前必须把共享变量的值刷新到主内存。 |
线程加锁时,将清空工作内存中共享变量的值,从而使用共享变量时需要从主内存中重新读取最新的值,注意(加锁与解锁是同一把锁)。 |
volatile关键字
根据JMM,Java中有一块主内存,不同的线程有自己的工作内存,同一个变量值在主内存中只有一份,如果线程用到了这个变量的话,自己的工作内存中有一份一模一样的拷贝。每次进入线程从主内存中拿到变量值,每次执行完线程将变量从工作内存同步回主内存中。
volatile解决的是变量在多个线程之间的可见性,但是无法保证原子性。
public class VolatileTest {
public static int clientTotal = 5000;// 请求总数
public static int threadTotal = 200;// 同时并发执行的线程数
public static volatile int count = 0;
public static void main(String[] args) throws Exception {
ExecutorService executorService = Executors.newCachedThreadPool();
final Semaphore semaphore = new Semaphore(threadTotal);
final CountDownLatch countDownLatch = new CountDownLatch(clientTotal);
for (int i = 0; i < clientTotal ; i++) {
executorService.execute(() -> {
try {
semaphore.acquire();
add();
semaphore.release();
} catch (Exception e) {
System.out.println("exception" + e);
}
countDownLatch.countDown();
});
}
countDownLatch.await();
executorService.shutdown();
System.out.println("count:" + count); //count!=5000
}
private static void add(){
count++;
}
}
volatile的可见性是通过加入内存屏障和禁止重排序来实现。
对volatile进行写操作时,会在写操作后加入一条store屏障指令,将本地内存中的共享变量值刷新到主内存。 |
对volatile进行读操作时,会在写操作后加入一条load屏障指令,从主内存中读取共享变量。 |
可见性-volatile写示意图:
可见性-volatile读示意图:
注意:volatile不能保证原子性;
对变量的写操作不依赖于当前值。 |
对于该变量的值没有包含在具有其他变量的不变的式子在中。 |
可见性--volatile的使用:(作为状态标识量)
volatile boolean inited = false;
//线程1
context = loadContext();
inited = true;
//线程2
while(!inited){
sleep;
}
doSomethingWithConfig(context);
有序性
Java内存模型中,允许编译器和处理器对指令进行重排序,但是重排序过程不会影响到单线程的执行,但却会影响到多线程并发执行的正确性。 |
通过 volatile、synchronized、lock保证可见性,synchronized、lock保证同一时刻只有一个线程执行代码,从而保证有序性。 |
先天有序性-- happens-before原则:
程序次序规则 |
一个线程内,按照代码顺序,书写在前面的操作先行发生于书写在后面的操作。 |
锁定规则 |
一个unlock操作先行发生于后面对同一个锁的lock操作。 |
volatile变量规则 |
对一个变量的写操作先行发生于后面对这个变量的读操作。 |
传递规则 |
如果操作A先行发生于操作B,而操作B又先行发生于操作C,则可以得出操作A先行发生于操作C。 |
线程启动规则 |
Thread对象的start()方法先行发生于此线程的每一个动作。 |
线程中断规则 |
对线程interrupt()方法的调用先行发生于被中断线程的代码检测到中断事件的发生。 |
线程中断规则 |
线程中所有的操作都先行发生于线程的终止检测,我们可以通过Thread.join()方法结束、Thread.isAlive()的返回值手段检测到线程已经终止执行。 |
对象终结规则 |
一个对象的初始化完成先行发生于他的finalize()方法开始。 |
安全发布对象
发布对象
使一个对象能够被当前范围之外的代码所使用。
//NotThreadSafe
public class UnsafePublish {
private String[] states = {"a", "b", "c"};
public String[] getStates() {//public 其他线程可修改该域
return states;
}
public static void main(String[] args) {
UnsafePublish unsafePublish = new UnsafePublish();
System.out.println(Arrays.toString(unsafePublish.getStates()));
unsafePublish.getStates()[0] = "d";//对私有属性修改
System.out.println(Arrays.toString(unsafePublish.getStates()));
}
}
对象逸出
一种错误的发布,当一个对象还没有构造完成时,就使它被其他线程所见。
//对象在没有正确完成构造之前被发布
//NotThreadSafe
//NotRecommend
public class Escape {
private int thisCanBeEscape = 0;
public Escape () {
new InnerClass();//启动一个线程,新线程在所属对象构造完成之前已经看到this
}
private class InnerClass {
public InnerClass() {
System.out.println(Escape.this.thisCanBeEscape);//this引用逸出
}
}
public static void main(String[] args) {
new Escape();
}
}
安全发布对象的四种方法
在静态初始化函数中初始化一个对象引用。 |
将对象的引用保存到volatile类型域或者AtomicReference对象中。 |
将对象的引用保存到某个正确构造对象的final类型域中。 |
将对象的引用保存到一个由锁保护的域中。 |
代码参考设计模式单例模式。
线程安全策略
不可变对象
只要对象发布,它就是线程安全的。
不可变需要满足的条件:
条件 |
实现 |
对象创建以后其状态就不能修改; 对象所有域都是final类型; 对象是正确创建的(在创建期间,this引用没有逸出); |
将类声明为final,不可以被继承。 将所有成员声明为私有,不允许直接访问成员。 对变量不提供set方法,将所有可变的成员声明为final。 通过构造器初始化所有成员,进行深度拷贝。 在get方法中不返回对象的本身,而是克隆对象,并返回对象的引用。 |
final关键字:修饰类、方法、变量
修饰类 |
不能被继承。 |
|
修饰方法 |
锁定方法不能被继承类修改。 |
|
修饰变量 |
基本数据类型变量 |
变量初始化以后要不可以被修改。 |
引用类型变量 |
初始化之后不可以指向新的引用,但是可以修改对象内的值(线程不安全)。 |
Collections提供的不可变对象
Collections.unmodifiableXXX:Collection、List、Set、Map...
根据原来的集合创建一个新的Unmodifiable集合,并且Unmodifiable集合的操作方法throw new UnsupportedOperationException()。
//ThreadSafe
public class CollectionsTest {
private static Map<Integer, Integer> map = new HashMap();
static {
map.put(1, 2);
map.put(3, 4);
map.put(5, 6);
map = Collections.unmodifiableMap(map);
}
public static void main(String[] args) {
map.put(1, 3);
System.out.print(map.get(1));
}
}
Guava提供的不可变对象
ImmutableXXX:Collection、List、Set、Map...
通过重写集合的操作方法抛出异常UnsupportedOperationException。
//ThreadSafe
public class ImmutableTest{
private final static ImmutableList<Integer> list = ImmutableList.of(1, 2, 3);
private final static ImmutableSet set = ImmutableSet.copyOf(list);
private final static ImmutableMap<Integer, Integer> map = ImmutableMap.of(1, 2, 3, 4);
private final static ImmutableMap<Integer, Integer> map2 = ImmutableMap.<Integer, Integer>builder()
.put(1, 2).put(3, 4).put(5, 6).build()
public static void main(String[] args) {
System.out.println(map2.get(3));
}
}
线程封闭
当访问共享的可变数据时,通常需要同步。一种避免同步的方式就是不共享数据。如果仅在单线程内访问数据,就不需要同步,这种技术称为线程封闭(thread confinement)。(把对象封装到一个线程里,只有一个线程可以看到这个对象。就算 对象不是线程安全的,也不会出现任何线程安全仿方面的问题)。
Ad-hoc线程封闭 |
程序控制实现、最糟糕。 |
堆栈封闭 |
局部变量、无并发问题(局部变量被拷贝至线程栈中,因此局部变量不会被线程共享)。 |
ThreadLocal类 |
特别好的线程封闭。 |
ThreadLocal线程封闭
每个Thread线程内部都有一个Map,这个Map以线程本地对象作为key,以线程的对象副本作为value,同时这个Map由ThreadLocal来维护,ThreadLocal来负责向Map设置线程的变量值及获取值,所以对于不同的线程每次获取副本值的时候,别的线程并不能获取当前线程的副本值,于是就形成了副本的隔离,做到了多个线程互不干扰。
三个理论基础
每个线程都有一个自己的ThreadLocal.ThreadLocalMap对象。 |
每一个ThreadLocal对象都有一个循环计数器。 |
ThreadLocal.get()取值,就是根据当前的线程,获取线程中自己的ThreadLocal.ThreadLocalMap,然后在这个Map中根据第二点中循环计数器取得一个特定value值。 |
两个数学问题
ThreadLocal.ThreadLocalMap规定了table的大小必须是2的N次幂。 |
Hash增量设置为0x61c88647,也就是说ThreadLocal通过取模的方式取得table的某个位置的时候,会在原来的threadLocalHashCode的基础上加上0x61c88647。 |
总结
ThreadLocal不需要key,因为线程里面自己的ThreadLocal.ThreadLocalMap不是通过链表法实现的,而是通过开地址法实现的。 |
每次set的时候往线程里面的ThreadLocal.ThreadLocalMap中的table数组某一个位置塞一个值,这个位置由ThreadLocal中的threadLocaltHashCode取模得到,如果位置上有数据了,就往后找一个没有数据的位置。 |
每次get的时候也一样,根据ThreadLocal中的threadLocalHashCode取模,取得线程中的ThreadLocal.ThreadLocalMap中的table的一个位置,看一下有没有数据,没有就往下一个位置找。 |
既然ThreadLocal没有key,那么一个ThreadLocal只能塞一种特定数据。如果想要往线程里面的ThreadLocal.ThreadLocalMap里的table不同位置塞数据 ,比方说想塞三种String、一个Integer、两个Double、一个Date,请定义多个ThreadLocal,ThreadLocal支持泛型"public class ThreadLocal<T>"。 |
ThreadLocal的作用
ThreadLocal不是用来解决共享对象的多线程访问问题的。 |
通过ThreadLocal的set()方法设置到线程的ThreadLocal.ThreadLocalMap里的是是线程自己要存储的对象,其他线程不需要去访问,也是访问不到的。各个线程中的ThreadLocal.ThreadLocalMap以及ThreadLocal.ThreadLocal中的值都是不同的对象。 |
总结
ThreadLocal不是集合,它不存储任何内容,真正存储数据的集合在Thread中。ThreadLocal只是一个工具,一个往各个线程的ThreadLocal.ThreadLocalMap中table的某一位置set一个值的工具而已。 |
同步与ThreadLocal是解决多线程中数据访问问题的两种思路,前者是数据共享的思路,后者是数据隔离的思路。 |
同步是一种以时间换空间的思想,ThreadLocal是一种空间换时间的思想。 |
ThreadLocal既然是与线程相关的,那么对于Java Web来讲,ThreadLocal设置的值只在一次请求中有效。 |
线程安全的类与线程不安全的类
线程安全的类
线程不安全的类 |
对应的线程安全的类 |
StringBuilder |
StringBuffer |
SimpleDateFormat |
JodaTime |
线程不安全的类
线程不安全的类:如果一个类的对象同时可以被多个线程访问,如果不做同步或者特殊的并发处理,那么它就很容易表现出线程不安全的现象(抛出异常,逻辑处理错误等等)。
先检查再执行:线程不安全的点在于分成两个操作之后,即使每一个操作是线程安全的,但是在间隙过程中不是原子性的。
if (condition(a)){//如果两个线程同时访问到该条件就会出现线程不安全问题
handle(a);
}
String、StringBuilder、StringBuffer
public class StringBuilderBufferTest {
public static int clientTotal = 5000;// 请求总数
public static int threadTotal = 200;// 同时并发执行的线程数
public static StringBuilder stringBuilder = new StringBuilder();//@NotThreadSafe
//public static StringBuffer stringBuilder = new StringBuffer();//@ThreadSafe
public static void main(String[] args) throws Exception {
ExecutorService executorService = Executors.newCachedThreadPool();
final Semaphore semaphore = new Semaphore(threadTotal);
final CountDownLatch countDownLatch = new CountDownLatch(clientTotal);
for (int i = 0; i < clientTotal ; i++) {
executorService.execute(() -> {
try {
semaphore.acquire();
update();
semaphore.release();
} catch (Exception e) {
log.error("exception", e);
}
countDownLatch.countDown();
});
}
countDownLatch.await();
executorService.shutdown();
log.info("size:{}", stringBuilder.length());
}
private static void update() {
stringBuilder.append("1");
}
}
这三个类之间的区别主要是在两个方面,即运行速度和线程安全这两方面:
运行速度快慢为:StringBuilder > StringBuffer > String
String最慢的原因
String为字符串常量,而StringBuilder和StringBuffer均为字符串变量,即String对象一旦创建之后该对象是不可更改的,而StringBuilder和StringBuffer的对象是变量,对变量进行操作就是直接对该对象进行更改,而不进行创建和回收的操作,所以速度要比String快很多。
在线程安全上,StringBuilder是线程不安全的,而StringBuffer是线程安全的。
如果一个StringBuffer对象在字符串缓冲区被多个线程使用时,StringBuffer中很多方法可以带有synchronized关键字,所以可以保证线程是安全的,但StringBuilder的方法则没有该关键字,所以不能保证线程安全。所以如果要进行的操作是多线程的,那么就要使用StringBuffer,但是在单线程的情况下,还是建议使用速度比较快的StringBuilder。
String |
适用于少量的字符串操作的情况。 |
StringBuilder |
适用于单线程下在字符缓冲区进行大量操作的情况。 |
StringBuffer |
适用多线程下在字符缓冲区进行大量操作的情况 |
SimpleDateFormat、JodaTime、FastDateFormat
public class DateFormatTest {
//@NotThreadSafe
private static SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyyMMdd");
//@ThreadSafe
//private static DateTimeFormatter dateTimeFormatter = DateTimeFormat.forPattern("yyyyMMdd");
public static int clientTotal = 5000;// 请求总数
public static int threadTotal = 200;// 同时并发执行的线程数
public static void main(String[] args) throws Exception {
ExecutorService executorService = Executors.newCachedThreadPool();
final Semaphore semaphore = new Semaphore(threadTotal);
final CountDownLatch countDownLatch = new CountDownLatch(clientTotal);
for (int i = 0; i < clientTotal ; i++) {
executorService.execute(() -> {
try {
semaphore.acquire();
update();
semaphore.release();
} catch (Exception e) {
log.error("exception", e);
}
countDownLatch.countDown();
});
}
countDownLatch.await();
executorService.shutdown();
}
private static void update() {
try {
//@ThreadSafe 堆栈封闭
//SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyyMMdd");
simpleDateFormat.parse("20180208");
} catch (Exception e) {
log.error("parse exception", e);
}
}
}
总结
SimpleDateFormat是线程不安全的,不能多个线程公用。 |
FastDateFormat是线程安全的,可以直接使用,不必考虑多线程的情况。 |
Joda-Time与以上两种有所区别,不仅仅可以对时间进行格式化输出,而且可以生成瞬时时间值,并与Calendar、Date等对象相互转化,极大的方便了程序的兼容性。 |
Joda-Time的类具有不可变性,因此他们的实例是无法修改的,就跟String的对象一样。 |
ArrayList, HashSet, HashMap均为线程不安全类
同步容器
线程不安全 |
线程安全 |
ArrayList |
Vector, Stack |
HashMap |
HashTable(key、value不能为null) |
Collection.synchronizedXXX(List、Set、Map) |
同步容器中主要使用synchronized实现同步。
//ThreadSafe
public class CollectionsSyncTest {
public static int clientTotal = 5000;// 请求总数
public static int threadTotal = 200;// 同时并发执行的线程数
private static Map<Integer, Integer> map = Collections.synchronizedMap(new HashMap<>());
private static List<Integer> list = Collections.synchronizedList(Lists.newArrayList());
private static Set<Integer> set = Collections.synchronizedSet(Sets.newHashSet());
private static Map<Integer, Integer> map = new Hashtable<>();
public static void main(String[] args) throws Exception {
ExecutorService executorService = Executors.newCachedThreadPool();
final Semaphore semaphore = new Semaphore(threadTotal);
final CountDownLatch countDownLatch = new CountDownLatch(clientTotal);
for (int i = 0; i < clientTotal; i++) {
final int count = i;
executorService.execute(() -> {
try {
semaphore.acquire();
update(count);
semaphore.release();
} catch (Exception e) {
log.error("exception", e);
}
countDownLatch.countDown();
});
}
countDownLatch.await();
executorService.shutdown();
log.info("size:{}", list.size());
}
private static void update(int i) {
list.add(i);
//set.add(i);
//map.put(i,i);
}
}
同步容器不能保证线程安全,在某些情况下也会表现出线程不安全的状态(Vector增加与删除同步进行,会导致数组下标越界)。
对于集合类型的变量再遍历的时候不要做更新操作,如果需要更新,则建议做好标记,遍历完之后在做跟新操作;
推荐使用for循环做遍历时跟新操作。
并发容器
线程不安全 |
线程安全 |
ArrayList |
CopyOnWriteArrayList |
HashSet |
CopyOnWriteArraySet |
TreeSet |
ConcurrentSkipListSet |
HashMap |
ConcurrentHashMap |
TreeMap |
ConcurrentSkipListMap |
CopyOnWriteArrayList
CopyOnWriteArrayList 类的所有可变操作(add,set等等)都是通过创建底层数组的新副本来实现的。当 List 需要被修改的时候,并不直接修改原有数组对象,而是对原有数据进行一次拷贝,将修改的内容写入副本中。写完之后,再将修改完的副本替换成原来的数据,这样就可以保证写操作不会影响读操作了。
//ThreadSafe
public class CopyOnWriteArrayListTest {
public static int clientTotal = 5000;// 请求总数
public static int threadTotal = 200; // 同时并发执行的线程数
private static List<Integer> list = new CopyOnWriteArrayList<>();
public static void main(String[] args) throws Exception {
ExecutorService executorService = Executors.newCachedThreadPool();
final Semaphore semaphore = new Semaphore(threadTotal);
final CountDownLatch countDownLatch = new CountDownLatch(clientTotal);
for (int i = 0; i < clientTotal; i++) {
final int count = i;
executorService.execute(() -> {
try {
semaphore.acquire();
update(count);
semaphore.release();
} catch (Exception e) {
log.error("exception", e);
}
countDownLatch.countDown();
});
}
countDownLatch.await();
executorService.shutdown();
log.info("size:{}", list.size());
}
private static void update(int i) {
list.add(i);
}
}
从 CopyOnWriteArrayList 的名字可以看出,CopyOnWriteArrayList 是满足 CopyOnWrite 的 ArrayList,所谓 CopyOnWrite 的意思:就是对一块内存进行修改时,不直接在原有内存块中进行写操作,而是将内存拷贝一份,在新的内存中进行写操作,写完之后,再将原来指向的内存指针指到新的内存,原来的内存就可以被回收。
CopyOnWriteArrayList 读取操作的实现 |
读取操作没有任何同步控制和锁操作,理由就是内部数组 array 不会发生修改,只会被另外一个 array 替换,因此可以保证数据安全。 |
CopyOnWriteArrayList 写入操作的实现 |
CopyOnWriteArrayList 写入操作 add() 方法在添加集合的时候加了锁,保证同步,避免多线程写的时候会 copy 出多个副本。 |
CopyOnWriteArraySet
HashSet的底层存储结构是一个HashMap,并且HashSet的元素作为该Map的Key进行存储,HashMap的Key的存储是无序并且不可重复,这就解释了HashSet中如何保证元素不重复;
CopyOnWriteArraySet的底层存储结构其实是CopyOnWriteArrayList,它支持并发的原理跟CopyOnWriteArrayList是一样的。
@ThreadSafe
public class CopyOnWriteArraySetExample {
public static int clientTotal = 5000;// 请求总数
public static int threadTotal = 200;// 同时并发执行的线程数
private static Set<Integer> set = new CopyOnWriteArraySet<>();
public static void main(String[] args) throws Exception {
ExecutorService executorService = Executors.newCachedThreadPool();
final Semaphore semaphore = new Semaphore(threadTotal);
final CountDownLatch countDownLatch = new CountDownLatch(clientTotal);
for (int i = 0; i < clientTotal; i++) {
final int count = i;
executorService.execute(() -> {
try {
semaphore.acquire();
update(count);
semaphore.release();
} catch (Exception e) {
log.error("exception", e);
}
countDownLatch.countDown();
});
}
countDownLatch.await();
executorService.shutdown();
log.info("size:{}", set.size());
}
private static void update(int i) {
set.add(i);
}
}
ConcurrentSkipListSet
ConcurrentSkipListSet是线程安全的有序的集合,适用于高并发的场景。
ConcurrentSkipListSet和TreeSet,它们虽然都是有序的集合。它们有以下区别:
它们的线程安全机制不同,TreeSet是非线程安全的,而ConcurrentSkipListSet是线程安全的。 |
ConcurrentSkipListSet是通过ConcurrentSkipListMap实现的,而TreeSet是通过TreeMap实现的。 |
ConcurrentSkipListSet的批量操作不保证线程安全,批量操作底层调用的还是add().remove()等方法,不能保证操作时不会被其他线程打断;
//ThreadSafe
public class ConcurrentSkipListSetTest {
public static int clientTotal = 5000;// 请求总数
public static int threadTotal = 200;// 同时并发执行的线程数
private static Set<Integer> set = new ConcurrentSkipListSet<>();
public static void main(String[] args) throws Exception {
ExecutorService executorService = Executors.newCachedThreadPool();
final Semaphore semaphore = new Semaphore(threadTotal);
final CountDownLatch countDownLatch = new CountDownLatch(clientTotal);
for (int i = 0; i < clientTotal; i++) {
final int count = i;
executorService.execute(() -> {
try {
semaphore.acquire();
update(count);
semaphore.release();
} catch (Exception e) {
log.error("exception", e);
}
countDownLatch.countDown();
});
}
countDownLatch.await();
executorService.shutdown();
log.info("size:{}", set.size());
}
private static void update(int i) {
set.add(i);
}
}
ConcurrentHashMap
ConcurrentHashMap采取了“锁分段”技术来细化锁的粒度:把整个map划分为一系列被成为segment的组成单元,一个segment相当于一个小的hashtable。
这样,加锁的对象就从整个map变成了一个更小的范围——一个segment。ConcurrentHashMap线程安全并且提高性能原因就在于:对map中的读是并发的,无需加锁;
只有在put、remove操作时才加锁,而加锁仅是对需要操作的segment加锁,不会影响其他segment的读写,由此,不同的segment之间可以并发使用,极大地提高了性能。
//ThreadSafe
public class ConcurrentHashMapExample {
public static int clientTotal = 5000;// 请求总数
public static int threadTotal = 200;// 同时并发执行的线程数
private static Map<Integer, Integer> map = new ConcurrentHashMap<>();
public static void main(String[] args) throws Exception {
ExecutorService executorService = Executors.newCachedThreadPool();
final Semaphore semaphore = new Semaphore(threadTotal);
final CountDownLatch countDownLatch = new CountDownLatch(clientTotal);
for (int i = 0; i < clientTotal; i++) {
final int count = i;
executorService.execute(() -> {
try {
semaphore.acquire();
update(count);
semaphore.release();
} catch (Exception e) {
log.error("exception", e);
}
countDownLatch.countDown();
});
}
countDownLatch.await();
executorService.shutdown();
log.info("size:{}", map.size());
}
private static void update(int i) {
map.put(i, i);
}
}
ConcurrentSkipListMap
TreeMap使用红黑树按照key的顺序(自然顺序、自定义顺序)来使得键值对有序存储,但是只能在单线程下安全使用;多线程下想要使键值对按照key的顺序来存储,则需要使用ConcurrentSkipListMap。
ConcurrentSkipListMap的底层是通过跳表来实现的。跳表是一个链表,但是通过使用“跳跃式”查找的方式使得插入、读取数据时复杂度变成了O(logn)。
//ThreadSafe
public class ConcurrentSkipListMapTest {
public static int clientTotal = 5000;// 请求总数
public static int threadTotal = 200;// 同时并发执行的线程数
private static Map<Integer, Integer> map = new ConcurrentSkipListMap<>();
public static void main(String[] args) throws Exception {
ExecutorService executorService = Executors.newCachedThreadPool();
final Semaphore semaphore = new Semaphore(threadTotal);
final CountDownLatch countDownLatch = new CountDownLatch(clientTotal);
for (int i = 0; i < clientTotal; i++) {
final int count = i;
executorService.execute(() -> {
try {
semaphore.acquire();
update(count);
semaphore.release();
} catch (Exception e) {
log.error("exception", e);
}
countDownLatch.countDown();
});
}
countDownLatch.await();
executorService.shutdown();
log.info("size:{}", map.size());
}
private static void update(int i) {
map.put(i, i);
}
}
跳表(SkipList):使用“空间换时间”的算法,令链表的每个结点不仅记录next结点位置,还可以按照level层级分别记录后继第level个结点。在查找时,首先按照层级查找,比如:当前跳表最高层级为3,即每个结点中不仅记录了next结点(层级1),还记录了next的next(层级2)、next的next的next(层级3)结点。
现在查找一个结点,则从头结点开始先按高层级开始查:head->head的next的next的next->。。。直到找到结点或者当前结点q的值大于所查结点,则此时当前查找层级的q的前一节点p开始,在p~q之间进行下一层级(隔1个结点)的查找......直到最终迫近、找到结点。此法使用的就是“先大步查找确定范围,再逐渐缩小迫近”的思想进行的查找。
线程安全策略-总结:
线程限制 |
一个线程被限制的对象,由线程独占,并且只能被占有它的线程修改; |
共享只读 |
一个共享只读的对象,在没有额外同步的情况下,可以被多个线程并发访问,但任何线程都不能修改它; |
线程安全对象 |
一个线程安全的对象或者容器,在内部通过同步机制来保证线程安全,所以其他线程无需额外的同步就可以通过公共接口随意访问它; |
被守护对象 |
被守护对象只能通过获取特定的锁来访问; |
JUC