并发与多线程(三) --- 线程同步


一、线程同步是什么?

资源共享的两个原因是资源紧缺和共建需求。线程共享CPU 是从资源紧缺的维度来考虑的,而多线程共享同一变量,通常是从共建需求的维度来考虑的。
在多个线程对同一变量进行写操作时,如果操作没有原子性,就可能产生脏数据。 所谓原子性是指不可分割的一系列操作指令,在执行完毕前不会被任何其他操作中断,要么全部执行,要么全部不执行。如果每个线程的修改都是原子操作,就不存在线程同步问题。有些看似非常简单的操作其实不具备具备原子性,典型的就是i++ 操作,它需要分为三步,即 ILOAD → IINC → ISTORE 。另一方面,更加复杂的CAS (Compare And Swap) 操作却具有原子性。
计算机的线程同步,就是线程之间按某种机制协调先后次序执行,当有一个线程在对内进行操作时,其他线程都不可以对这个内存地址进行操作,知道该线程完成操作。 实现线程同步方式有很多,比如同步方法、锁阻塞队列等。

二、Volatile

从happen before 了解线程操作的可见性。把happen before 定义为方法hb(a,b),表示a happen before b。如果hb(a,b)且hb(b,c),能够推导出hb(a,c)。
CPU 在处理信息时会进行指令优化,分析哪些区数据动作可以合并进行,哪些存数据动作可以合并进行。CPU 拜访一趟遥远的内存,一定会到处看看,是否可以存取合并,以提高执行效率。指令重排示例代码如下:

@Override
public void run(){
   
	(1)
	int x =1;
	int y = 2;
	int z = 3;
	(2)
	x = x + 1;
	(3)
	int sum = x +y + z;
}

happen before 是时钟顺时针的先后,并不能保证线程交互的可见性,在第2处和第3处都是写操作,不会进行指令重排,但是前三行时不互斥的,并且第1处的操作如果放在z=3 赋值操作之后,明显是效率最大化的处理方式. 所以指令重排的最大可能使把第1处和第2处串联依次执行.
happen before 并不能保证线程交互的可见性.那么什么是可见性呢? 可见性是指 某些线程修改共享变量的指令对其他线程来说都是可见的,它反映的是指令执行的实时透明度.

每个线程都有独占的内存区域,如操作栈、本地变量表等。线程本地内存保存了引用变量在堆内存中的副本,线程对变量的所有操作都在本地内存区域中进行,执行结束后再同步到堆内存中去。这里必然有一个时间差,在这个时间差内,该线程对副本的操作,对于其他线程都是不可见的。
当使用volatile 修饰变量时,意味着任何对此变量的操作都会在内存中进行,不会产生副本,以保障共享变量的可见性,局部阻止了指令重排的发生。由此可知,在使用单例设计模式时,即使用双检锁也不一定会拿到最新的数据。
如下代码在高并发场景中会存在问题:

public class LazyInitDemo {
   
    private static TransactionService service =null;
    public static TransactionService getTransactionService(){
   
        if(service == null){
   
            synchronized (this){
   
                if (service == null){
   
                    service = new TransactionService();
                }
            }
        }
        return service ;
    }
    // other methods and fields ...
}

使用者在调用getTransactionService()时,有可能会得到初始化未完成的对象。究其原因,与Java虚拟机的编译优化有关。对Java编译器而言,初始化TransectionService 实例和将对象地址写到service 字段并非原子操作,且这两个阶段的执行顺序是未定义的。 假设某个线程执行 new Transactionservice() 时,构造方法还未被调用,编译器仅仅为该对象分配了内存空间并设为默认值,此时若另一个线程调用 getTransactionService() 方法,由于service != null ,但是此时service对象还没有被赋予真正有效的值,从而无法取到正确的service 单例对象. 这就是著名的双重检查锁定(Double-checked Locking) 问题,对象引用在没有同步的情况下进行读操作,导致用户可能会获取为构造完成的对象.对于此问题,一种较为简单的解决方案是用volatile 关键字修饰目标属性(适用于JDK5 及以上版本) ,这样service就限制了编译器对它的相关读写操作,对它的读写操作进行指令重排,确定对象实例化之后才返回引用.
锁也可以确保变量的可见性,但是实现方式和volatile 略有不同,线程在得到锁时读入副本,释放时

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值