线程安全与锁优化重点知识

线程安全

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

java语言中的线程安全

为了更加深入的理解线程安全,在这里我们可以不把线程安全当做一个非真即假的二元排他选项来看待,按照线程安全的“安全程度”由弱至强来排序,我们可以将java语言中各种操作共享的数据分为以下5类:

1、不可变

在java语言中(特指JDK1.5以后,即java内存模型被修正之后的java语言),不可变(Immutable)的对象一定是线程安全的,无论是对象的方法实现还是方法的调用者,都不需要再采取任何的线程安全保障措施。

如果共享数据是一个基本数据类型,那么只要在定义时使用final关键字修饰它就可以保证它是不可变的。如果共享数据是一个对象,那就需要保证对象的行为不会对其状态产生任何影响才行,保证对象行为不影响自己状态的途径有很多种,其中最简单的就是把对象中带有状态的变量都声明为final。例子:java.lang.String类,java.lang.integer类

2、绝对线程安全

在java API中标注自己是线程安全的类,大多数都不是绝对的线程安全。

3、相对线程安全

相对的线程安全就是我们通常意义上所讲的线程安全,它需要保证这个对象单独的操作是线程安全的,我们在调用的时候不需要做额外的保障措施,但是对于一些特定顺序的连续调用,就可能需要在调用端使用额外的同步手段来保证调用的正确性。例如:Vector、HashTable、Collectiongs的synchronizedCollection()方法包装的集合等。

4、线程兼容

线程兼容是指对象本身并不是线程安全的,但是可以通过在调用端正确的使用同步手段来保证对象在并发环境中可以安全的使用,我们平常说一个类不是线程安全的,觉大多数时候指的是这一种情况。

5、线程对立

线程对立是指无论调用端是否采用同步措施,都无法在多线程环境中并发使用的的代码。一个线程对立的例子就是Thread类的suspend()和resume()方法。也正是这个原因,所以被jdk声明废弃了。常见的线程对立操作还有System.setIn、System.setOut和System.runFinalizersOnExit()等。

线程安全的实现方法

1、互斥同步

同步是指在多个线程并发访问共享数据时,保证共享数据在同一个时刻只被一个(或者是一些,使用信号量的时候)线程使用。而互斥是实现同步的一种手段,临界区(Critical Section)、互斥量(Mutex)和信号量(Semaphore)都是主要的互斥实现方式。

synchronized关键字

synchronized关键字经过编译后会在同步块前后分别形成monitorenter和monitorexit这两个字节码指令,这两个字节码都需要一个Reference类型的参数来指明要锁定和解锁的对象。如果指定了对象参数,那就是这个对象的Reference;如果没有明确指定,那就根据修饰的是实例方法还是类方法,去取对应的对象实例或class对象来作为锁对象。

虚拟机规范对monitorenter和monitorexit的行为描述中,有两点需要注意

  • synchronized同步块对同一条线程来说是可重入的,不会出现自己把自己锁死的问题
  • 同步块在已进入的线程执行完之前,会阻塞后面其他线程的进入
    • java的线程是映射到操作系统的原生线程上,如果要阻塞或唤醒一个线程,都需要操作系统来帮忙完成,这就需要从用户态转换到核心态,因此状态切换会耗费处理器时间
    • 对于简单的同步块,状态转换消耗的时间有可能比用户代码执行的时间还要长
    • 虚拟机本身也会进行一些优化,譬如在通知操作系统阻塞线程之前会加入一段自旋等待过程,避免频繁的切入到核心态中

重入锁ReetrantLock

相比synchornized,ReentranLock增加了一些高级功能,主要有以下3项:

  • 等待可中断:当前持有锁的线程如果长时间不释放锁,正在等待的线程可以选择放弃等待,改为处理其他事情
  • 公平锁:多个线程在等待同一个锁时,必须按照申请锁的时间顺序来依次获得锁;synchronized中的锁是非公平的,ReentrantLock默认情况下也是非公平的,但可以通过带布尔值的构造函数要求使用公平锁
  • 锁绑定多个条件:一个ReentrantLock对象可以同时绑定多个Condition对象,而在synchronized中,锁对象的wait()和notify()或notifyAll()方法可以实现一个隐含条件,如果要和多余一个的条件关联,就不得不额外添加一个锁
2、非阻塞同步

随着硬件指令集的发展,我们除了互斥同步这种“悲观的”并发策略,我们还有另外一个选择:基于冲突检测的乐观并发策略,通俗的说,就是先进行操作,如果没有其他线程争用共享数据,那操作就成功了;如果共享数据有争用,产生冲突,那就再采取其他的补偿措施(最常见的补偿措施就是不断重试,知道成功为止),这种乐观的并发策略的许多实现都不需要把线程挂起,因此这种同步操作称为非阻塞同步(Non-Blocking Synchronization)。

CAS原子操作https://blog.csdn.net/cringkong/article/details/80533917

3、无同步方案

要保证线程安全,并不一定就要进行同步,两者没有因果关系。

可重入代码

这种代码也叫作纯代码(Pure Code),可以在代码执行的任何时刻中断它,转而去执行另外一段代码(包括递归调用它本身),而在控制返回后,原来的程序不会出现任何错误。

可重入代码有一些共同特征,例如不依赖存储在堆上的公共系统资源、用到的状态量都由参数中传入、不调用非可重入的方法等。一个简单的判断方法:如果一个方法,他的返回结果是可预测的,只要输入了相同的数据就能返回相同的结果,那他就满足可重入性的要求。

线程本地存储

如果能保证共享数据的代码能在同一个线程中执行,这样,无需同步也能保证线程之间不出现数据争用的问题。

符合这中特点的应用并不少见,大部分使用消费队列的架构模式(如“生产者-消费者”模式)都会讲产品的消费过程尽量在一个线程中消费完,其中最重要的一个应用实例就是经典web交互模型中的“一个请求对应一个服务器线程”(Thread-per-Request)的处理方式,这种处理方式的广泛应用使得很多web服务端应用都可以使用线程本地存储来解决线程安全问题

java中一个变量要被某个线程独享,可以通过java.lang.ThreadLocal类来实现线程本地存储的功能。


锁优化

1、自旋锁与自适应锁

自旋锁

如果物理机器有一个以上的处理器,能让两个或以上的线程同时并行执行,我们就可以让后面请求锁的那个线程“稍等一下”,但不放弃处理器的执行时间,看看持有锁的线程是否很快会释放锁。为了让线程等待,我们只需要让线程执行一个忙循环(自旋)这项技术就是所谓的自旋锁

自旋锁在jdk 1.4.2中就已经引入,只不过默认是关闭的,可以使用 -XX:+UseSpinning参数来开启,在jdk 1.6中已经改为默认开启了。

注意:自旋等待的时间必须要有一定的限度,如果自旋超过了限定的次数仍然没有成功获得锁,就应该使用传统的方式去挂起线程了。自旋次数的默认值是10次,用户可以使用参数 -XX:PreBlockSpin来更改。

自适应锁

在jdk1.6中引入了自适应的自旋锁。自适应意味着自旋的时间不再固定,而是由前一次在同一个锁上的自旋时间及锁的拥有者的状态来决定。

如果在同一个锁对象上,自旋等待刚刚成功获得过锁,并且持有锁的线程正在运行中,那么虚拟机就会认为这次自旋也很有可能再次成功,进而他将允许自旋等待持续相对更长的时间,比如100个循环。另外,如果对于某个锁,自旋很少成功获得过,那在以后要获取这个锁时将可能省略调自旋过程,以避免浪费处理器资源。

2、锁消除

锁消除是指虚拟机即时编译器在运行时,对一些代码上要求同步,但是被检测到不可能存在共享数据竞争的锁进行消除。

锁消除的主要判断依据来源于逃逸分析的数据支持,如果判断在一段代码中,堆上的所有数据都不会逃逸出去从而被其他线程访问到,那就可以把他们当做栈上数据对待,认为他们是线程私有的,同步锁自然就无需进行了。

3、锁粗化

原则上,我们在编写代码的时候,总是推荐将同步块的作用范围限制的尽量小——只在共享数据的实际作用域中才进行同步,这样是为了使得需要同步的操作数量尽可能变小,如果存在锁竞争,那等待的线程也能尽快拿到锁。

4、轻量级锁

轻量级锁是jdk1.6之中加入的新型锁机制,它名字中的“轻量级”是相对于使用操作系统互斥量来实现的传统锁而言的。首先要强调一点的是,轻量级锁并不是用来代替重量级锁的,它的本意是在没有多线程竞争的前提下,减少传统的重量级锁使用操作系统互斥量产生的性能消耗。

图片

图片

5、偏向锁

图片

具体的解释和理解可以参考:https://blog.csdn.net/zq1994520/article/details/84175573

发布了8 篇原创文章 · 获赞 1 · 访问量 261

猜你喜欢

转载自blog.csdn.net/qq_40635011/article/details/105495705
今日推荐