线程安全性
当多个线程访问某个类时,不管运行时环境采用何种调度方式或者这些线程将如何交替执行,并且在主调代码中不需要任何额外的同步或协同,这个类都能表现出正确的行为,那么这个类就是线程安全的。
- 原子性:提供了互斥访问,同一时刻只能有一个线程来对它进行操作
- 可见性:一个线程对主内存的修改可以及时被其他线程观察到
- 有序性:一个线程观察其他线程中的指令执行顺序,由于指令重排序的存在,该观察结果一般杂乱无序
原子性
原子性:提供了互斥访问,同一时刻只能有一个线程来对它进行操作
- atomic包, 以Atomic开头的类,Java底层是使用CAS进行实现
例如:AtomicInteger 调用了java底层的Unsafe.compareAndSwapInt方法,这里是实现CAS的关键,Unsafe类中调用的方法大多数是native,由jvm本地实现 - AtomicLong、LongAdder
- AtomicReference AtomicReferenceFieldUpdater
@Slf4j
@ThreadSafe
public class AtomicExample4 {
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
log.info("count:{}",count.get());
}
}
@Slf4j
public class Test3 {
private static AtomicReferenceFieldUpdater<Test3, Integer> updater =
AtomicReferenceFieldUpdater.newUpdater(Test3.class, Integer.class, "count");
private volatile Integer count = 0; // 必须 非静态的 volatile 修饰的字段
public static void main(String[] args) throws Exception{
Test3 test3 = new Test3();
updater.compareAndSet(test3, 0, 1);
updater.compareAndSet(test3, 0, 2);
updater.compareAndSet(test3, 1, 3);
updater.compareAndSet(test3, 2, 4);
log.info("结果:{}", updater.get(test3));
}
}
-
AtomicStampReference:CAS的ABA问题
该类的核心方法compareAndSet,该类是通过一个重新定义一个stamp的值来标记当前值是否被更改过。 -
AtomicBoolean
@Slf4j
@ThreadSafe
public class Test5 {
// 请求总数
private static int clientTotal = 5000;
// 可同时执行的线程数
private static int threadNum = 200;
private static AtomicBoolean isHappen = new AtomicBoolean(false);
public static void main(String[] args) throws Exception {
final Semaphore semaphore = new Semaphore(threadNum);
final CountDownLatch countDownLatch = new CountDownLatch(clientTotal);
ExecutorService executorService = Executors.newCachedThreadPool();
for (int i = 0; i < clientTotal; i++) {
executorService.execute(() -> {
try {
semaphore.acquire();
update();
semaphore.release();
} catch (InterruptedException e) {
e.printStackTrace();
}
countDownLatch.countDown();
});
}
countDownLatch.await();
executorService.shutdown();
log.info("ishappen:{}", isHappen.get());
}
private static void update() {
if (isHappen.compareAndSet(false, true)) { // 只执行一次
System.out.println("update exec");
}
}
}
可见性
可见性:一个线程对主内存的修改可以及时的被其他线程观察到
JMM关于synchronized的两条规定:
- 线程解锁前,必须把共享变量的最新值刷新到主内存
- 线程加锁时,将清空工作内存中共享变量的值,从而是用共享变量时需要从主内存中重新读取最新的值(注意,加锁与解锁是同一把锁)
synchronized
- 修饰代码块:大括号括起来的代码,作用于调用的对象
- 修饰方法:整个方法,作用于调用的对象
- 修饰静态方法:整个静态方法,作用于所有对象
- 修饰类,括号括起来的部分,作用于所有对象
synchronized:不可中断锁,适合竞争不激烈,可读性好
Lock:可中断锁,多样化同步,竞争激烈时能维持常态
Atomic:竞争激烈时能维持常态,比Lock性能好,只能同步一个值
volatile
通过加入内存屏障和禁止重培训优化来实现:
- 对于volatile变量写操作时,会在写操作后加入一条store屏障指令,将本地内存中的共享变量值刷新到主内存。
- 对于volatile变量读操作时,会在读操作前加入一条load屏障指令,从主存中读取共享变量
有序性
Java内存模型中,允许编译器和处理器对指令进行重排序,但是重排序过程不会影响到单线程程序的执行,却会影响到多线程并发执行的正确性。
有序性:一个线程观察其他线程中的指令执行顺序,由于指令重排序的存在,该观察结果一般杂乱无序
如果两个操作执行的次序无法从happens-before原则中推导出来,那么就不能保证他们的有序性,jvm就可以随意的对他们进行重排序
happen-before原则:
-
程序次序规则:在一个单独的线程中,按照程序代码的执行流顺序,(时间上)先执行的操作happen—before(时间上)后执行的操作。
-
管理锁定规则:一个unlock操作happen—before后面(时间上的先后顺序,下同)对同一个锁的lock操作。
-
volatile变量规则:对一个volatile变量的写操作happen—before后面对该变量的读操作。
-
线程启动规则:Thread对象的start()方法happen—before此线程的每一个动作。
-
线程终止规则:线程的所有操作都happen—before对此线程的终止检测,可以通过Thread.join()方法结束、Thread.isAlive()的返回值等手段检测到线程已经终止执行。
-
线程中断规则:对线程interrupt()方法的调用happen—before发生于被中断线程的代码检测到中断时事件的发生。
-
对象终结规则:一个对象的初始化完成(构造函数执行结束)happen—before它的finalize()方法的开始。
-
传递性:如果操作A happen—before操作B,操作B happen—before操作C,那么可以得出A happen—before操作C。
volatile可以保证一定的有序性,不能保证原子性。
synchronized,lock保证了单线程的运行,因此肯定时有序的
安全策略
不可变对象
- Collections.unmodifiableXXX:Collection、List、Set、Map…
- Guava:ImmutableXXX:Collection、List、Set、Map
线程封闭
- 堆栈封闭:局部变量,无并发问题
- ThreadLocal 线程封闭:特别好的封闭方法
- 使用Filter将用户信息保存到ThreadLocal中
- 在业务方法中使用处理
- 使用拦截器(Inteceptor)在方法执行完后移除
同步容器:
- ArrayList -> Vector, Stack
- HashMap -> HashTable(key, value不能为null)
- Collections.SynchronizedXXX(List,Set,Map)
并发容器
- ArrayList -> CopyOnWriteArrayList
- HashSet/TreeSet -> CopyOnWriteArraySet/ConcurrentSkipListSet
- HashMap/TreeMap -> ConcurrentHashMap/ConcurrentSkipListMap
最佳实践
- 使用本地变量
- 使用不可变类
- 最小化锁的作用域范围
- 使用线程池的Executor,而不是直接new Thread
- 宁可使用同步也不要使用线程的wait和notify
- 使用BlockingQueue实现生产-消费模式
- 使用并发集合而不是加了锁的同步集合
- 使用Semaphore创建有界的访问
- 宁可使用同步代码块,也不使用同步的方法
- 避免使用静态变量(使用时最好能加上final)