1 线程安全
个人定义:共享变量被多线程操作时,可被视作是以‘原子’形态被其他人‘可见’的‘有序’进行,且能够获取正确的操作结果,则它就是线程安全的。
1.1 安全等级
- 不可变:如final修饰变量在没有this逃逸情况下,变量本身是线程安全的,但变量内部的值还是可能被修改的
- 绝对线程安全:字面意思,绝对保证该变量的操作具有原子性有序性可见性
- 相对线程安全:对象提供的操作是安全的,但使用不当会有安全问题,比如集合类的size没有及时获取,导致越界访问
- 线程兼容:即可以通过并发手段实现线程安全
- 线程对立:有的方法只有强制控制实现按特定顺序执行才能安全,寻常控制并发依旧会出问题,比如已经废弃的线程暂停恢复
1.2 线程安全实现方式
- 互斥同步
保证共享变量在同一时刻只能被同一线程访问,比如:synchronized和ReentrantLock,更多细节可以看【java并发】synchronized和ReentrantLock
- 非阻塞同步
互斥同步需要进行线程阻塞和唤醒从而会导致一些性能问题,他是一种悲观锁策略,认为对共享数据的操作一定需要加锁,不然就不安全,那么是否可以通过不阻塞的方式进行线程安全控制呢?
基于冲突检测的乐观锁策略,先默认没有发生共享数据竞争,先执行操作,操作后进行检测,如果确实没有竞争,则操作成功,如果发生了竞争,那么我们放弃上次操作,可以考虑继续循环重试,或换个路子。要实现这种方式,首先必须保证操作和检测具备原子性,即通过一条处理器指令即可完成。
CAS:比较并交换(Compare-and-Swap),它需要3个操作数,内存位置V,旧的预期值A,新值B,如果V符合预期A,则用B更新V值,否则不更新,无论结果如何,都会返回当前V的值。JDK1.5之后,由sun.misc.Unsafe类中的方法包装使用。不过只有启动类加载器加载的Class才能访问它,因此抛开反射,我们只能通过JavaAPI间接使用它,如J.U.C包中的原子类。
来个例子:测试一下阻塞同步和非阻塞同步的效率,使用两种方式自增1亿次
private static AtomicInteger ai = new AtomicInteger(0);
private static volatile int i;
private static int threadNum = 100;
private static int addNum = 1000000;
private static int expect = threadNum * addNum;
public static void main(String[] args) {
long start = System.currentTimeMillis();
System.out.println("start:" + DateFormatUtils.format(start, "yyyy-MM-dd hh:mm:ss:SSS"));
test01();
while (i != expect) {
}
System.out.println(i);
long end = System.currentTimeMillis();
System.out.println("end:" + DateFormatUtils.format(end, "yyyy-MM-dd hh:mm:ss:SSS"));
System.out.println("sync coust ms:" + (end - start));
long start2 = System.currentTimeMillis();
System.out.println("start:" + DateFormatUtils.format(start2, "yyyy-MM-dd hh:mm:ss:SSS"));
testAtomic();
while (ai.get() != expect) {
}
System.out.println(ai.get());
long end2 = System.currentTimeMillis();
System.out.println("end:" + DateFormatUtils.format(end2, "yyyy-MM-dd hh:mm:ss:SSS"));
System.out.println("atomic coust ms:" + (end2 - start2));
}
/**
* 并发修改数据
*/
private static void test01() {
for (int i = 0; i < threadNum; i++) {
new Thread(new Runnable() {
@Override
public void run() {
for (int i = 0; i < addNum; i++) {
add();
}
}
}).start();
}
}
/**
* 并发修改数据
*/
private static void testAtomic() {
for (int i = 0; i < threadNum; i++) {
new Thread(new Runnable() {
@Override
public void run() {
for (int i = 0; i < addNum; i++) {
addAtomic();
}
}
}).start();
}
}
/**
* 阻塞自增
*/
private static void add() {
synchronized (FutureTaskTest.class) {
i++;
}
}
/**
* 原子自增
*/
private static void addAtomic() {
ai.incrementAndGet();
}
结果:发现atomic的效率是sync的两倍以上
start:2019-06-19 11:21:19:051
100000000
end:2019-06-19 11:21:29:420
sync coust ms:10369
start:2019-06-19 11:21:29:421
100000000
end:2019-06-19 11:21:33:586
atomic coust ms:4165
- 线程本地独享
ThreadLocal:每个线程都有一个ThreadLocalMap,Map中KEY为ThreadLocal.ThreadLocalHashCode,Value保存着对应线程的变量。也就是每个线程都有独立的拷贝。
2 锁优化
2.1 自旋锁和自适应自旋
自旋锁:一个线程为了获取某个锁,原本需要进行阻塞挂起,但是有时候某些共享数据锁定时间特别短,稍微再等等就能获取到锁,那么此时,我们选择不挂起,依旧占用当前处理器,让线程进行一个忙循环(自旋),等待锁释放。有时候牺牲一点CPU时间,会比线程的挂起唤醒性能更好。
自旋JDK1.6之后默认开启,默认自旋10次,通过-XX:PreBlockSpin来更改。
自适应:等待次数JVM可能自行调整,如果某个锁曾经自旋等待成功,则JVM会提升自旋等待次数,如果长期自旋失败,则可能放弃自旋。
2.2 锁消除
将检测到不可能存在共享数据(判断堆上数据是否可能发生逃逸)的锁进行消除。因为没有意义。比如StringBuffer的append(),默认添加了同步锁。
2.3 锁粗化
大部分时候锁粒度越细越好,但是当共享变量被频繁访问,如它出现在一个循环体中,那么频繁的加锁解锁也会导致不必要的性能消耗,此时还不如在循环体外部进行加锁,只加一次就行。虚拟机有时会帮我们做这样的优化。
2.4 轻量级锁
对于绝大部分的锁,在整个同步周期内是不存在竞争的。那么我们没必要在第一个线程访问的时候就加重量级的互斥锁,先使用CAS来避免互斥量的开销,当发生锁竞争时,那么再将锁升级为重量级锁。
2.5 偏量锁
第一个线程访问的时候不使用任何同步手段,当发生锁竞争,则将偏量锁进行撤销,恢复到未锁定或轻量级。
爱家人,爱生活,爱设计,爱编程,拥抱精彩人生!