多线程(三)--线程安全与锁

 一、引言:

        JAVA 是一个多线程并发的语言,现在只要有点经验的JAVA程序员,对于多线程、并发等词汇相信并不陌生,但是对于具体的运行原理,很多也都没深入,这里我也分享一部分自己的经验,主要对于线程安全以及锁的一些机制原理,进行介绍。关于线程的基本知识点,前面也说过了,可以了解一下。

 

1.1 什么是线程安全?

      这里我借“JAVA 并发实践”里面的话:当多个线程访问一个对象,如果不考虑这些线程在运行时环境下的调度和交替执行,也不需要额外的同步,或者调用方进行任何其他的协调操作,调用这个对象的行为都可以获得正确的结果,那么这个对象是线程安全的。

      我的理解:多线程访问一个对象,任何情况下,都能保持正确行为,就是对象就是安全的。

      线程安全有强弱划分,分为5类:

      a.不可变

      不可变的对象的,也就是被声明成fianl的对象,只要被正确构建出来,在不发现this逃逸的情况下,其外部状态永远不会改变,永远不会看到多个线程中处于不一致的状态。也就是说所有对象的共享变量都声明成final ,那么就是安全的。

     

       b.绝对线程安全

        在某些情况下,我们希望我们的程序能在任何情况下都是安全的,比如加了final 类型的基本类型变量,这里可以认为是的,但是这种不可变的变量没有太大意义。而像StringBulider 类似的变量,即使加了final 类型,也不能认为是线程绝对安全,final 只能保证地址值不动。

   

       c.线程相对安全

         这里的相对安全比如我们了解的vector,StirngBuffer 等线程安全的类,也许vector 类的所有操作我们都加上内部锁,但是在使用过程中比如:声明一个 vector 的变量,然后A,B 线程并发操作它。假设A线程在增加元素,B线程在遍历获取元素,那么就会出现错误(元素个数不对),因此线程安全性更多的表现为对同一操作的正确执行安全,也是相对的安全。

 

       d.线程兼容

          简单的说就是,这个类本人不是线程安全的,但那时可以使用一些为外部手段,使其完成我们的线程安全。比如ArrayList,HashMap 本身不是线程安全的,但是如果你使用Collections.synchronizedList(Map)就可以达到安全效果,其实现原理很简单,就是对List 或者Map 进行封转,对其主要方法都加上内部锁,相当于集成一个List(Map),全部重写方法加上锁,调用父类执行体。具体的这里不深究。

 

      e.线程对立

        简单的说无论我们是否采用了线程安全的机制(比如加锁),或者其他同步措施,都不能保证多线程并发是安全的。比如Thread 的supend()和resume()方法,一个线程去中断线程,另一个线程去恢复线程。那么并发就容易产生死锁,这里两个方法也就废弃了。其他例子暂时不举了。

 

二、原子性操作

       当我们决定完成一个任务,通常情况下,在计算机中,看似很简单的任务也是有多个不同的步骤共同完成。该步骤是由cpu 的 一些指令完成的。比如我们常见的 i ++ ;这是一个非原子性操作,因为它先.从内存取出i的值,然后再增1,最后再写入内存中,经过三个步骤完成,如果在中间一个步骤被其他线程影响了,那么就可能出现错误。

      举个实际例子:我想完成过安检的过程,我会先取下包,然后放在检验机上,我走过去,然后等待通过检查,最后拿回来。但是实际过程发现有小偷在我将包放到检测机上,还没有进入检查过程中,被拿走了,然后我走过去,发现我的包没过来...这个悲剧的问题就发生了!

      那么如何完成原子性操作呢?

 

三、锁

       3.1 互斥同步

       互斥同步是我们最基本的保障并发安全的一种手段,比如刚才的例子,假设我通过安检这个过程,是不允许其他人接触或者靠近的,有一道独立的空间,也就是说我去通过检查的的行为和小偷接近我,偷我包的行为是互斥的,那么我的行为就很安全的完成了。

       互斥最简单的手段是synchronized 关键字,synchronized 关键字在通过编译之后,会在同步块前后分别形成monitorentor 和 monitorexit 两个字节码指令,这个两个指令都需要一个reference 类型来指明要锁定和解锁的对象,如果synchnronized 明确指定了对象参数,那就是这个对象的reference ,如果没有指明,那么就根据synchronied 修饰的是实例方法还是类方法,然后取对应对象的实例或者Class对象那个作为锁对象。

 

     3.2  synchronizd 工作原理

            Java 线程在执行到synchronied 的时候,会形成两个字节码指令,这里相当于是一个监视器(monitor),监控synchronized 保护的区域,监视器会设置几种状态用来区分请求线程:

             Contention List : 所有请求的线程将被首先放置到该竞争队列

             Entry List: Contention List 的那些有资格成为候选人的线程会被移到Entry List

             Wait Set:那些调用wait 方法被阻塞的线程被放置到这里

             OnDeck :任何时刻最多有一个线程正竞争锁,该线程称为OnDeck

             Owner :获得所的线程叫Owner

             !Owner :释放锁的线程

             下面是状态的转换关系:

             

 

我们知道,并发会引起竞争,那么上图更详细的描述了整个过程,我这里以我和小明和小强一起去上飞机为例子,假设所有通道唯一。

1.我们一起打车来到飞机场,相当于进入了Contention List 

2.然后我们准备去买票柜台(Entry List),但是还没到

3.这是小明发现身份证没带,打电话叫他妈妈送过来,他就只能等待,进入(WaitSet).

4.然后我和小明一起到柜台,如果柜台没有人,那么我们就去(Entry List) 买票。

5.这时候到我和小强一起跑到柜台,但是谁先买,得看OnDesk 的,相当于选择权在她手里,这里的竞争机制    是随机的,也就是说OnDesk 看谁顺眼,谁就能买。(当然大家现实都很文明排队~.~)

6.假设我得到的优先权,那么我就是Owner,只有我买票成功了,才有资格说OK。因为OnDesk 必然会问还 

   还有什么需要帮助的吗?这时候的决定权就在我手里了,然后我会!Owner,然后OnDesk 会以同样的方式    进行下一个人。

7.如果小明的票拿到了(唤醒),那么他也可以去柜台。

8.当然即使Owner 的线程,也可能出现问题,比如买票过程中 - -发现没钱了,等别人给我带,也只能进入        WaitSet 中了。

 

  3.2 重入锁

       synchronized 内部锁是互斥锁,也就是说当A线程请求B线程所占有的一个锁时,只能等待(阻塞),直到B释放它,如果B不释放,那么A就一直等待(阻塞)。也就是同一时间只能由同一线程进入synchronized 的保护块,这能保证它的原子性操作。

       但是相同持有该锁的线程可以再次进入该代码块,它再次请求获得锁的时候,会成功。这里的实现是当线程获得锁的时候,监视器(JVM) 会记录锁的占有者,并且与锁关联的计数器 + 1,当计数器为 0的时候我们才认为该锁没有被占用。

       

	class Parent{
		public synchronized void doSome(){}
	}
	class Child extends Parent{
		public synchronized void doSome(){
			// 如果没有重入锁 ,这里会出现死锁
			super.doSome();
		}
	}

 

 

    3.3 ReentrantLock 

     这个在java.util.concurrent(J.U.C) 下的的显示锁,也具有重入锁的特征,与synchronizd 相比,Lock 锁更加的灵活,因为内部锁synchronized 在阻塞的时候,其他线程必须等待,如果出点问题,可能无限等待下去,而且内部锁机制在状态转换过程中,需要映射到操作系统的原生线程上,这块转换比较耗时的,虽然JVM 也做了一些比如自旋锁的优化,但是还是不够。而Lock 锁,是表现在API 层次的锁,增加了额外的几个功能:

     a. 等待可中断:如果获得锁的线程,长时间不释放锁,正在等待的线程可以选择放弃等待,改为初期其他事情,这样不至于大家都等在那里,浪费时间。

     b.公平锁:当多个线程等待同一个锁时,必须按照申请锁的时间顺序来一次获得锁,可以通过boolean 类型的构造函数使用公平锁。当然此方法吞吐量稍微慢点,并且和线程优先级一样,仅仅是让先申请的的获得更大的大的机会,并不能完全保证它一定是公平的。

     c.绑定多个条件:ReentrantLock  对象可以同时绑定多个Condition 对象,而synchronized 中,锁对象的wait() 和 notify() 或notifyAll() 方法可以实现一个隐含的条件,如果多余一个条件关联的时候,就不得不额外加个锁,而ReentantLock 无需这么做,只需要多次newCondition 方法即可。

         这里简单的理解是:synchronized 阻塞,相当于大家不认识,由工作人员(CPU)调度,自由竞争锁,也不管竞争的人(线程)有啥意外情况。condition 相当于把大家都信息都获取了,比如A(线程) 获得买票(获得锁),结果发现没钱,他可以设置一个条件condition-A 等待,然后让出位置,让另外的人买。假设B(线程)买好票了,发现A有钱了,他可以通过condition-A 唤醒A,让他继续参与买票。相当于大家更和谐,不用一个卡死在前面,后面的人就一直等待,condition 可以多个条件切换工作。这里是通过一个队列进行的,至于具体的实现原理,可以参考:http://ifeve.com/understand-condition/ ,我们以后详细讲解。

          

 

小结:

        1.上面内容我是从深入理解JVM  和 并发实践 等地方copy 的,加入了自己的一些理解,分享

        2.由于都是理论性的东西,因此先介绍一小部分,不至于大家看着很累,但是希望看的时候能融入自己的理解,不然都是天书,没意思。

        3.关于其他锁机制原理等内容,以后慢慢分享吧,等我消化消化

        4.如果发现不理解,或者我理解错误的,请指出,以免误导他人嘛,非常感谢!

        

        

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值