关于内存安全,线程安全,死锁(上)

 

1.基本概念

 

这三样东西知识点很多,接触多线程编程必然接触到,专门理一理

也算开个坑,很多细节没有细致解释,后面遇到需要深挖

 

线程安全虽然处处接触到内存,但跟内存安全还不是一回事,内存安全可以被定义为:不访问任何未定义的内存。如:避免缓冲区溢出,避免引用未初始化等。

可以说内存安全涉及到内存的分配回收等偏底层操作。

 

线程安全被定义为:多个线程访问类时,无论采取何种调度方式,主调代码中也不需要额外的同步和协同,都能表现出正确的行为。这里的“类”应该被叫成共享数据,同样是对内存进行操作,线程安全考虑的是其中包含数据的安全性,而非内存地址本身的问题。即考虑的是买下来的房子有没有进贼,而非纠结这块地是不是我的。

 

死锁,定义一般是下在进程上的:死锁是指两个或两个以上的进程在执行过程中,由于竞争资源或者由于彼此通信而造成的一种阻塞的现象,若无外力作用,它们都将无法推进下去在网上找到的解释大多不分进程线程,甚至有写着写着就改口的情况。我们知道,进程是计算机分配资源的基本单位,而线程是系统独立调度和分派的基本单位,线程不占有独立的资源,但上锁的共享数据之于线程一样能带来线程的死锁。

 

 

2.线程安全

 

线程安全的判定围绕着三个性:原子性,可见性,顺序性。

原子性指一件事要么干完,要么全不干;可见性要求一个线程对数据的修改是透明的,即要求大家都能知道;顺序性,由《深入理解java虚拟机》中对JSR提到的happens-before八条规则做了说明,可以看看,由这些规则可以导出的次序才是有序的。

在此理清线程安全几个常见关键字:synchronized,volatile,wait,notify,lock。

其实只有synchronized是真正意义上的java关键词,这里单纯用关键词的表意。

 

Synchronized:

首先是synchronized关键字作用域,总结来说,都是锁对象的:修饰实例方法时,锁住该实例对象;修饰静态方法时,锁住当前类对象;修饰代码块时,锁住给定对象;synchronized static 虽然叫做类锁,但光锁类是没有意义的,jvm只给对象在堆里开辟内存空间,类锁锁住了该类的所有实例对象,还是对象。

考虑到synchronized的实现原理,私以为用锁来比喻有点不够恰当:对象在内存中布局中有一块叫对象头,其中有一条记录叫重量级锁,即synchronized锁,标志位是10,指针指向一个叫monitor的东西,姑且叫做保安,保安的数据结构如下:

ObjectMonitor() {

_header = NULL;

_count = 0; //记录个数

_waiters = 0,

_recursions = 0;

_object = NULL;

_owner = NULL;

_WaitSet = NULL; //处于wait状态的线程,会被加入到_WaitSet

_WaitSetLock = 0 ;

_Responsible = NULL ;

_succ = NULL ;

_cxq = NULL ;

FreeNext = NULL ;

_EntryList = NULL ; //处于等待锁block状态的线程,会被加入到该列表

_SpinFreq = 0 ;

_SpinClock = 0 ;

OwnerIsThread = 0 ;

}

保安会记录来访的人,获取权限的人,来访没能获取权限的人,现在有没有人在访问等,jvm给每个对象(房子)都提供一个这样的保安。

来看看synchronized如何利用monitor机制来确保对对象的安全访问:

通过反编译代码后的结果可以看出来,synchronized是在代码块中数据操作前加上“monitorenter”指令,结束时加上“monitorexit”指令,入指令会访问保安,保安计数器为0时可获取持有权并将计数器加一,如果被占有则进入阻塞状态,出指令会到保安处登记,将计数器置0并退出。

因为保安系统是要依赖操作系统层的mutex lock来实现,而每次要在用户态和核心态直接转化,故时间成本较高,java后来对synchronized做了优化,引入了轻量锁,偏向锁。

 

Volatile:

什么叫可见性,是共享变量的改变能及时一致的更新。我们来硬件层面看看不一致更新导致的问题:

计算机在找数据时找目标并非完全随机的,有时间局部性和空间局部性,所以在cpu中设有高速缓存,速度高的代价就是价格高,所以普遍容量较小。缓存存的就是最近用到的数据,不同缓存都从主存中读取同一条数据,可能A线程处理完了存回主存,B还在用它缓存的,可能B处理完了存回缓存,但未更新到主存,A从主存提取的数据就不是最新的了。这类问题都可以被归结为数据状态不透明,volatile就是用于解决这一问题。

声明了Volatile的变量写操作时,会向处理器发送lock前缀的指令。在P6之前比如奔腾处理器中,处理器在申明了lock的指令执行期间,会将总线锁起,其他CPU无法访问总线就无法访问内存。之后的处理器中,缓存一致协议让每个处理器嗅探其他处理器和内存,包括自己的缓存,嗅探到其他处理器打算改写入一个内存地址,而这个地址为共享状态,则该处理器将自己缓存中的该数据置为无效,并在下次访问时直接从内存中提取。

* 一个很容易混淆的问题:volatile申明的数据可以保证透明性,但不能保证原子性。

很多解释都解释的不是很清楚:比如说自增操作分为三步,分成volatile不能保证三步的原子性,没有写操作不会导致内存值的改变等。我们详细看一下线程1,2对数据的操作:1从内存读取数据inc,放在自己处理器缓存中,2读取后并加1并写入内存,这时确实处理器能保证数据的透明性,1运行的处理器将自己缓存中的inc作废,但是这是有范围的,线程1读取的inc会在主存中开辟一块内存作为副本并下一步用副本数据进行操作,所以从原理上解释应该成透明性的作用域是有限的,缓存一致不代表所有数据都一致。解决这个问题可用状态标记,double check保证。

 

wait,notify:

用法不做表述,需要理解的是,wait和notify的作用对象是线程本身,跟synchronized作用于对象区分开来。Wait要求线程在已经获取锁的状态下放弃资源,进入阻塞状态,等待notify将其唤醒。所以对共享数据的锁比作房子的安保制度,wait和notify操作的是对访问房子的人的行为进行规划。在理清这一概念后了解其工作流程就变得简单了。

 

Lock:

lock要和synchronized放在一起说明,synchronized的缺陷有:锁释放不灵活,容易被死锁;读取操作这一可以并行的操作也被禁止;无法了解控制加减锁。

Lock本质上作为一个接口,提供了一些方法解决:

lock()、tryLock()、tryLock(long time, TimeUnit unit)和lockInterruptibly()是用来获取锁的,unLock()方法是用来释放锁的。

由于一些特质:

Lock可以让等待锁的线程响应中断,而synchronized却不行;

通过Lock可以知道有没有成功获取锁;

Lock可以提高多个线程进行读操作的效率;

在资源竞争非常激烈的时候,lock的效率远远高出synchronized

 

 

后续会总结死锁和多线程框架Executor,为多线程编程打下牢固基础。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值