这是多线程对于本系列的第四部分,请注意以下几点:
JAVA许多的线—线是怎么来的?
Java多线程内存模型
Java多线程——易失性
如果您阅读了之前关于线程的文章,您将清楚地了解线程的实现原理。有了理论的支持,你将有更好的实践指导。然后本文将重点介绍线程的实践,并简要介绍线程的几种应用。
本文的主要内容:
- 线程安全分类
- 线程同步的实现
- 锁优化
线程安全不是一个既不正确也不错误的二进制世界。如果按照线程安全的“安全度”进行排序,Java可以分为以下几类
不变的。如果数据类型被修改为final类型,则可以保证它是不可变的(除了引用对象之外,finalobject属性不受保证,只保证内存地址)。不可变对象必须是线程安全的,比如字符串类,这是一个典型的不可变对象。调用其substring()、replace()和concat()方法时,它不会影响其原始值,只返回一个新构造的string对象。
绝对线程安全。无论调用方有多严格,都不需要额外的措施来实现任何类的同步。
相对线程安全性。这就是我们通常所说的线程安全。它确保对象上的单个操作是线程安全的,不需要做额外的工作。然而,对于特定顺序的连续呼叫,呼叫者可能需要采用额外的同步手段来确保呼叫的正确性。
线程兼容。这意味着对象本身不是线程安全的。调用者可以正确地使用同步方法,以确保对象可以在并发环境中安全地使用。我们常用的非线程安全类都属于这一类。
线程对立。这意味着,无论调用方是否采取同步措施,都不可能在多线程环境中同时使用代码。我们应该避免这种情况。
绝对安全性和相对安全性在上述情况下可能无法很好地区分。我们用一个例子来区分:
公共类向量测试{ 私有向量<;整数>;vector=新矢量<;整数>;(); 公共空间移除(){ 新线程(){ @推翻 公开募捐{ 对于(inti=0;i<;vector.Size();i++){ vector(一); } } }.开始(); } 公开作废印刷品(){ 新线程(){ @凌驾 公开募捐{ 对于(inti=0;i<;vector.Size();i++){ 系统出来Println(vector); } } }.开始(); } 公共无效添加(整数数据){ vector.add(数据); } 公共静态voidmain(字符串[]args){ 向量测试=新向量测试(); 对于(intj=0;j<;100;j++){ 对于(inti=0;i<;10;i++){ 测验加(i); } 测验移除(); 测验打印(); } } }
在上述代码中运行时将报告一个错误:print方法中出现ArrayIndexOutOfBoundsException异常。当remove线程删除一个元素时,print方法只执行JetLiGet()方法,这个异常就会出现。
我们知道vector是线程安全的,它的get()、remove()、size()、add()方法由NicholasTse同步。然而,在多线程的情况下,如果在方法调用方没有额外的同步,它仍然不是线程安全的。这这就是我们所说的相对线程安全性。它并不保证调用方在任何时候都不需要任何额外的同步措施。
线程安全同步的实现互斥同步是确保并发正确性的常用方法。同步意味着当多个线程同时访问共享数据时,确保共享数据同时只被一个线程使用。Java中常见的互斥同步方法是synchronize和ReentrantLock。
了解多线程的人必须了解这两种锁定方法。我们将不讨论具体用法。让我们来谈谈这两种应用程序场景之间的差异。
synchronize在前一篇文章中也提到它属于重锁。因为JVM线程被映射到操作系统的本机线程,所以在阻塞或唤醒线程时,它需要从用户状态切换到内核。有时,它需要比代码执行时间更多的时间,因此JVM会对一些短代码执行采用同步方法旋转锁以及其他避免频繁进入核思维的方法。
NicholasTse是JVM提供的内置锁,是JVM推荐的。它编写的代码相对简单紧凑。只有当内置锁不能满足需要时,我们才能再次使用ReentrantLock。
ReentrantLock
那么ReentrantLock能提供哪些高级功能呢?让我们来看一个例子;
公共无效synA(){ synchronize(骆家辉){ synchronizeD(骆家辉){ //做某事。。。。 } } } 公共空间(synB){ synchronizeD(骆家辉){ synchronize(骆家辉){ //做某事。。。。 } } }
当多个线程分别调用syna和synB时,NicholasTseD的上述代码容易死锁。为了避免这种情况,在编写时只能强制所有调用的顺序相同。ReentrantLock投票锁为了避免这个问题。
公共无效tryLockA(){ 长停车时间=系统。currentTimeMillis()+10000l 虽然(真的){ if(lock.tryLock()){ 试试看{ if(lockB.tryLock()){ 试试看{ //doSomeThing。。。。。 回来 }终于{ 洛克。解锁(); } } }终于{ 洛克。解锁(); } } if(System.currentTimeMillis()>;停车时间){ 回来 } } }
如果上述trylock方法无法获得所需的锁,可以通过轮询获得,这样程序可以再次获得控制并释放获得的锁。此外,trylock还提供了一种定时重载方法,方便您在一定时间内获得锁。如果不能在指定时间内给出结果,程序将以零结束。
ReentrantLock除了提供轮询和定时锁,它还可以提供可中断锁获取操作,以便在可取消的操作中使用卸扣。此外,它还提供了锁获取操作、公平队列和非块结构锁。这些功能极大地丰富了锁操作的可定制性。
当然,如果你不能使用ReentrantLock的这些高级功能,你最好推荐synchronized。在性能方面,synchronized在java6之后能够与ReentrantLock保持平衡。据官方报道,这方面的性能将在未来得到加强,因为它属于JVM的内置属性,可以执行一些优化,比如优化线程闭合锁对象的锁消除,增加锁的粒度以消除锁同步。这些都很难在ReentrantLock身上实现。
锁优化如上所述,当多线程竞争资源时,其他非竞争线性线程将阻塞并等待,阻塞和唤醒也需要内核的调度,这对于有限的CPU来说太昂贵了。因此,JVM在锁优化上花费了大量精力来提高执行效率。
让我们看看常见的锁优化方法。
旋转锁
在共享数据锁定状态下,有许多方法只能保持很短的时间。在这么短的时间内暂停和恢复线程是不值得的。然后JVM要求等待锁的线程等待片刻,但不放弃相应的执行时间。查看等待的线程是否很快被释放,这减少了线程调度的压力。如果锁被短时间占用,效果非常好。如果时间太长,就会浪费循环资源,浪费资源。
自适应自旋锁
旋转锁无法根据锁被占用的时间长度进行处理。随后,介绍了自适应自旋锁。可选时间不再是固定的,而是由以前的可选时间和同一锁的状态决定。这样,你就会变得聪明。
锁消除
锁消除意味着JVM实时编译器消除了某些代码中需要同步的锁,但检测到共享数据没有竞争。锁消除检测主要基于逃逸分析的数据支持。如果判断堆上的所有数据不会在一段代码中转义,则它们将被视为堆栈上的数据。我们认为线程是私有的,不需要同步。
锁粗化
在编写代码时,始终建议同步块的范围越小越好。如果一系列操作重复地对一个对象进行锁紧和解锁,甚至出现在环体中,那么在时间上就不会有线程竞争,这也会导致不必要的性能损失。对于这类代码,JVM将扩展其锁的粒度,并且只对这部分代码使用一个同步操作。
轻量级锁定
轻量级锁是为了降低传统重量级锁的性能消耗而使用的操作系统互斥,无需多线程竞争。JVM中的对象头分为两部分。第一部分用于存储对象本身的操作数据,称为“Mark字”。这是实现轻量级锁的关键。另一部分用于存储执行方法区域的对象类型数据的指针。当代码进入同步块时,如果同步对象未锁定,则将“Mark世界”中锁定记录的指针标记为“01”。
如果LiuYifeiword更新成功且线程拥有对象的锁,则将执行锁位的指针标记为“00”。如果更新失败,并且当前对象的LiuYifei字没有指向当前线程的堆栈帧,则表示锁对象已被其他线程抢占。如果两个以上的线程竞争同一个锁,那么轻量级锁将不再工作。锁被标记为“10”,并扩展为重量级锁。
轻量级锁基于这样一个事实:大多数锁在同步周期中不竞争,因此它们可以减少互斥造成的性能消耗。当然,如果存在锁竞争,除了互斥之外,还将避免使用互斥的开销,并且将有额外的操作来同步修改标志位。
偏置锁
偏置锁消除了整个同步过程,没有竞争,甚至CAS更新操作。这将有利于第一个线程获得它。如果在下一个执行过程中其他线程没有获得锁,那么持有偏置锁的线程将永远不需要同步。当另一个线程试图获得锁时,宣布偏置模式结束。
最新评论