`
greemranqq
  • 浏览: 975542 次
  • 性别: Icon_minigender_1
  • 来自: 重庆
社区版块
存档分类
最新评论
阅读更多

 一、引言:

        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.如果发现不理解,或者我理解错误的,请指出,以免误导他人嘛,非常感谢!

        

        

  • 大小: 23.5 KB
0
2
分享到:
评论
2 楼 greemranqq 2014-03-13  
xugangqiang 写道
讲的很好,特别是关于sync关键字的, monitorEnter, monitorExit
相当于这里有一道内存屏障,其他线程无法对这块内存进行访问,
这样就保证了这块内存的访问只有一个线程在进行,从而保证了安全性

以前一直不理解什么是内存屏障,这会有点理解了。
楼主最好再review一下文章,看看有没有错别字。

xugangqiang 写道
讲的很好,特别是关于sync关键字的, monitorEnter, monitorExit
相当于这里有一道内存屏障,其他线程无法对这块内存进行访问,
这样就保证了这块内存的访问只有一个线程在进行,从而保证了安全性

以前一直不理解什么是内存屏障,这会有点理解了。
楼主最好再review一下文章,看看有没有错别字。


额,纠结的错别是,好多年了。对了 我对锁的理解,以及内存屏障这块是有区别的,锁是监视器指令,内存屏障是另外一种,当然我没进行深入研究,这篇博文你可以参考,我们共同学习。
http://www.infoq.com/cn/articles/memory_barriers_jvm_concurrency
1 楼 xugangqiang 2014-03-13  
讲的很好,特别是关于sync关键字的, monitorEnter, monitorExit
相当于这里有一道内存屏障,其他线程无法对这块内存进行访问,
这样就保证了这块内存的访问只有一个线程在进行,从而保证了安全性

以前一直不理解什么是内存屏障,这会有点理解了。
楼主最好再review一下文章,看看有没有错别字。

相关推荐

    易语言线程安全之原子锁与读写锁

    总结来说,线程安全是多线程编程的基础,而原子操作和读写锁是实现线程安全的重要工具。易语言提供了丰富的支持,使得开发者能够方便地在自己的程序中实现线程安全,从而构建高效且稳定的多线程应用程序。通过合理...

    CVI 线程锁、线程安全变量实例

    本实例将深入探讨如何在LabWindows/CVI中有效地使用线程锁和线程安全变量来实现多线程程序设计。 首先,我们来看线程锁。线程锁,也称为互斥锁,是一种同步机制,用于确保同一时间只有一个线程可以访问特定的资源或...

    c++ 多线程编程之三----线程间通讯

    C++ 多线程编程之三----线程间通讯 C++ 多线程编程中,线程间通讯是非常重要的一部分。线程间通讯可以让不同的线程之间进行信息传递,实现协作和同步。在多线程编程中,线程间通讯可以使用全局变量、自定义消息等...

    C#内存释放-线程控制-线程启动-线程暂停

    为了安全地访问共享数据,可以使用锁(如`lock`关键字)或监视器(`Monitor`类),以确保同一时间只有一个线程能访问这部分代码。此外,`volatile`关键字可用于确保多线程环境中的数据一致性,确保所有线程都能看到...

    Python应用实战:python多线程-多线程安全问题&lock与rlock.zip

    在Python中,由于全局解释器锁(GIL)的存在,Python的多线程并不能实现真正的并行计算,但仍然可能遇到线程安全问题,如数据竞争、死锁等。 数据竞争是多线程环境中常见的问题,当两个或多个线程同时访问并修改...

    线程-线程池-锁-集合-Map-队列.docx

    `ArrayBlockingQueue`、`LinkedBlockingQueue`等是常见的阻塞队列实现,它们在多线程环境中能确保线程安全。 综上所述,线程、线程池、集合和队列是Java并发编程的核心概念,理解和掌握它们对于开发高效、稳定的...

    进程与线程--小练习

    线程的创建、销毁和切换相比进程更快速,因此多线程常用于提高程序的并发性能。 进程与线程的主要区别在于资源分配和独立性。进程是资源分配的基本单位,而线程是调度的基本单位。一个进程内的多个线程可以并发执行...

    Java多线程-线程的安全问题与线程的同步机制介绍

    Java多线程-线程的安全问题与线程的同步机制介绍 在 Java 多线程编程中,线程安全问题是非常重要的一个话题。当多个线程访问同一个资源时,如果只有读操作那么不会出现线程安全问题,但是如果多个线程对资源进行读...

    vc++中的线程锁(线程锁保持线程同步)

    在VC++编程环境中,线程同步是一个至关重要的概念,特别...通过合理使用线程锁,我们可以编写出高效且安全的多线程程序。在提供的源码文件中,我们可以深入学习线程锁的实现细节,以及如何在实际项目中有效地运用它们。

    多线程-day02.docx

    多线程编程的核心是保证三个特性:原子性、可见性和有序性。 - **原子性**:确保操作不可分割,要么全做要么不做,避免并发执行时的中断问题。 - **可见性**:线程间对共享变量的修改能立即被其他线程感知。 - **...

    Java多线程基础-01、数组概述.rar

    在Java编程语言中,多线程是程序设计中的一个重要概念,尤其在现代计算环境中,它能够充分利用多核处理器的能力,提高程序的并发性和执行效率。本资料“Java多线程基础-01、数组概述”将带你入门Java的多线程世界,...

    C#多线程List的非线程安全性

    本文将深入探讨在多线程环境中使用List时遇到的非线程安全问题,并提供相应的解决方案和最佳实践。 List是.NET框架中常用的一个动态数组,它提供了方便的增删改查操作。然而,List并未设计为线程安全的容器,这意味...

    VB多线程实例 - VBThread1

    线程安全是指在多线程环境下,代码和数据结构不受破坏的能力,确保了共享资源的正确性。 此外,`Thread.Join`方法用于等待指定线程完成,而`IsAlive`属性可以检查线程是否仍在运行。`ThreadPriority`属性可以设置...

    11-多线程爬虫-1(1).zip

    三、多线程爬虫的工作原理 1. 分配任务:爬虫首先将待爬取的URL分配到多个线程中。 2. 同步控制:为了避免多个线程同时访问同一URL造成冲突,可以使用锁等同步机制进行控制。 3. 并行下载:每个线程独立下载网页内容...

    多线程demo程序-轻松掌握多线程技术

    在多线程环境中,多个线程可能同时执行该方法,因此要特别注意线程安全问题。避免修改共享数据时产生竞态条件,可以使用synchronized关键字进行同步控制,或者使用java.util.concurrent包下的线程安全数据结构。 4....

    java多线程、锁的教程跟案例

    `多线程.docx`可能是关于Java多线程的深入讲解,涵盖线程安全、线程同步、锁的使用等方面。 总之,理解并熟练掌握Java多线程和锁机制对于编写高效、健壮的并发代码至关重要。通过实践和案例学习,可以更好地理解...

    操作系统-创建多线程-读者写者

    ### 操作系统中的多线程创建与读者写者问题解析 #### 多线程概念介绍 在计算机科学中,线程是操作系统能够进行运算调度的最小单位,它被包含在进程之中,是进程中的实际运作单位。一个进程可以拥有多个线程,而每个...

Global site tag (gtag.js) - Google Analytics