一、引言:
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.如果发现不理解,或者我理解错误的,请指出,以免误导他人嘛,非常感谢!
相关推荐
总结来说,线程安全是多线程编程的基础,而原子操作和读写锁是实现线程安全的重要工具。易语言提供了丰富的支持,使得开发者能够方便地在自己的程序中实现线程安全,从而构建高效且稳定的多线程应用程序。通过合理...
本实例将深入探讨如何在LabWindows/CVI中有效地使用线程锁和线程安全变量来实现多线程程序设计。 首先,我们来看线程锁。线程锁,也称为互斥锁,是一种同步机制,用于确保同一时间只有一个线程可以访问特定的资源或...
C++ 多线程编程之三----线程间通讯 C++ 多线程编程中,线程间通讯是非常重要的一部分。线程间通讯可以让不同的线程之间进行信息传递,实现协作和同步。在多线程编程中,线程间通讯可以使用全局变量、自定义消息等...
为了安全地访问共享数据,可以使用锁(如`lock`关键字)或监视器(`Monitor`类),以确保同一时间只有一个线程能访问这部分代码。此外,`volatile`关键字可用于确保多线程环境中的数据一致性,确保所有线程都能看到...
在Python中,由于全局解释器锁(GIL)的存在,Python的多线程并不能实现真正的并行计算,但仍然可能遇到线程安全问题,如数据竞争、死锁等。 数据竞争是多线程环境中常见的问题,当两个或多个线程同时访问并修改...
`ArrayBlockingQueue`、`LinkedBlockingQueue`等是常见的阻塞队列实现,它们在多线程环境中能确保线程安全。 综上所述,线程、线程池、集合和队列是Java并发编程的核心概念,理解和掌握它们对于开发高效、稳定的...
线程的创建、销毁和切换相比进程更快速,因此多线程常用于提高程序的并发性能。 进程与线程的主要区别在于资源分配和独立性。进程是资源分配的基本单位,而线程是调度的基本单位。一个进程内的多个线程可以并发执行...
Java多线程-线程的安全问题与线程的同步机制介绍 在 Java 多线程编程中,线程安全问题是非常重要的一个话题。当多个线程访问同一个资源时,如果只有读操作那么不会出现线程安全问题,但是如果多个线程对资源进行读...
在VC++编程环境中,线程同步是一个至关重要的概念,特别...通过合理使用线程锁,我们可以编写出高效且安全的多线程程序。在提供的源码文件中,我们可以深入学习线程锁的实现细节,以及如何在实际项目中有效地运用它们。
多线程编程的核心是保证三个特性:原子性、可见性和有序性。 - **原子性**:确保操作不可分割,要么全做要么不做,避免并发执行时的中断问题。 - **可见性**:线程间对共享变量的修改能立即被其他线程感知。 - **...
在Java编程语言中,多线程是程序设计中的一个重要概念,尤其在现代计算环境中,它能够充分利用多核处理器的能力,提高程序的并发性和执行效率。本资料“Java多线程基础-01、数组概述”将带你入门Java的多线程世界,...
本文将深入探讨在多线程环境中使用List时遇到的非线程安全问题,并提供相应的解决方案和最佳实践。 List是.NET框架中常用的一个动态数组,它提供了方便的增删改查操作。然而,List并未设计为线程安全的容器,这意味...
线程安全是指在多线程环境下,代码和数据结构不受破坏的能力,确保了共享资源的正确性。 此外,`Thread.Join`方法用于等待指定线程完成,而`IsAlive`属性可以检查线程是否仍在运行。`ThreadPriority`属性可以设置...
三、多线程爬虫的工作原理 1. 分配任务:爬虫首先将待爬取的URL分配到多个线程中。 2. 同步控制:为了避免多个线程同时访问同一URL造成冲突,可以使用锁等同步机制进行控制。 3. 并行下载:每个线程独立下载网页内容...
在多线程环境中,多个线程可能同时执行该方法,因此要特别注意线程安全问题。避免修改共享数据时产生竞态条件,可以使用synchronized关键字进行同步控制,或者使用java.util.concurrent包下的线程安全数据结构。 4....
`多线程.docx`可能是关于Java多线程的深入讲解,涵盖线程安全、线程同步、锁的使用等方面。 总之,理解并熟练掌握Java多线程和锁机制对于编写高效、健壮的并发代码至关重要。通过实践和案例学习,可以更好地理解...
### 操作系统中的多线程创建与读者写者问题解析 #### 多线程概念介绍 在计算机科学中,线程是操作系统能够进行运算调度的最小单位,它被包含在进程之中,是进程中的实际运作单位。一个进程可以拥有多个线程,而每个...