【Java并发】线程安全与锁优化

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 偏量锁

第一个线程访问的时候不使用任何同步手段,当发生锁竞争,则将偏量锁进行撤销,恢复到未锁定或轻量级。

 


爱家人,爱生活,爱设计,爱编程,拥抱精彩人生!

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

qqchaozai

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值