笔记--深入了解虚拟机(线程安全)

2 篇文章 0 订阅

前言

Java虚拟机在千差万别的物理机上建立来统一的运行平台,实现了在任意一台虚拟机上编译的源程序能在任何一台虚拟机上正常运行。

程序员可以把主要精力放在具体的业务逻辑上,而不是物理硬件的兼容性上。

开发人员如果不了解虚拟机一些技术特性的运行原理,就无法写出最适合虚拟机运行和自优化的代码。

线程安全

google上定义:如果一个对象可以安全地被多个线程同时使用,那它就是线程安全的

《Java Concurrency In Practice》的作者Brian Goctz对线程安全有一个比较恰当的定义:“当多个线程访问一个对象时,如果不用考虑这些线程在运行时环境下的调度和交替执行,也不需要进行额外的同步,或者在调用方进行任何其他的协调操作,调用这个对象的行为都可以获得正确的结果,那么这个对象是线程安全的。”

按照线程安全的“安全程度”由强至弱来排序,可以将Java语言中各种操作共享的数据分为以下5类:

  1. 不可变--不可变的对象一定是安全的。无论是对象的方法实现还是方法的调用者,都不需要再采取任何的线程安全措施保障措施。
  2. 绝对线程安全--不要任何同步保障措施也可以被多线程调用后所得的运行结果正确的类,这样的类是线程安全的。(这里可以与相对线程安全做比较记忆)
  3. 相对线程安全--通常讲的线程安全。他需要保证对这个对象单独的操作是线程安全的,在调用时不要加额外的保障措施,但是对于一些特定顺序的连续调用,就可能需要在调用端使用额外的同步手段来保证调用的正确性。需要添加同步措施synchronized(vector)在使用vector方法时。
  4. 线程兼容--指对象本身并不是线程安全的,但是可以通过在调用端正确的使用同步手段来保证对象在并发环境下可以安全地使用,我们平常说一个类不是线程安全的,绝大多数时候指的是这一种情况
  5. 线程对立--是指无论调用端是否采用同步措施,都无法在多线程环境中并发使用的代码。Java语言天生就具备多线程特性,线程对立这种排斥多线程的代码是很少出现的,而且通常都是有害的,应当尽量避免。例子:Thread类中的suspend()和resume()方法。存在死锁的风险。--所以这两个方法被废弃了。
  • 线程安全的类:Vertor、HashTable、String/StringBuffer
  • 线程不安全类:ArrayList、HashMap、StringBuilder
线程安全与否的因素:(自己总结的)
  • 类内是否使用来final定义方法或变量
  • 编写方法时是否采用同步机制synchronized--(这个得看一些类的源码例如:vector)
  • 在调用方法时,是否在调用端做额外的同步措施---例如synchronized(vector)
线程安全的实现方法

实现线程安全与代码编写有很大的关系,但虚拟机提供的同步和锁机制也起到了非常重要的作用

  1. 互斥同步--常见的一种并发正确性保障手段。同步是指多个线程并发的访问共享数据时,保证共享数据在同一时刻只能被一个线程使用。互斥是同步的一种手段,临界区、信号量、互斥量都是主要的互斥实现方式。最基本的互斥同步手段就是关键字synchronized。                                                                                                                                                                                             synchronized关键字经编译后,会在同步块前后生成monitorenter和monitorexit这两个字节码指令,这两个字节码都需要一个reference类型的参数来指明要锁定和解锁的对象。例如synchronized<vertor>,若没有指定,如synchronized<>,则根据synchronized修饰的是实例方法还是类方法,去取对应的对象实例或Class对象来作为锁对象。
  2. 据JVM规范的要求,在执行monitorenter指令时,首先尝试获取对象的锁。如果这个对象没有被锁定,或者当前线程已经拥有了那个对象的锁,把锁的计数器加1,相应的,在执行monitorexit指令时会将锁计数器减1。
虚拟机规范对monitorenter和monitorexit的行为描述中,有两点需要特别注意:

  1. synchronized同步块对同一线程来说是可重入,不会出现自己把自己锁死的情况。
  2. 同步块在已进入的线程执行完之前,会阻塞后面其他进程的进入。
JAVA的线程是映射在操作系统原生线程之上的,如果要阻塞或唤醒一个线程都需要操作系统来帮忙,这就需要从用户态转换到核心态,状态转换需要耗费大量的处理机时间,对于简单的代码同步块,状态转换消耗的时间有可能比用户代码执行的时间还要长。所以,synchronized是一个重量级的操作
JVM本身会进行一定的优化,譬如在通知操作系统阻塞线程之前加入一段 自旋等待过程,避免频繁地切入到核心态之中。

除了synchronized外还可以使用java.util.concurrent包中的重入锁(ReentrantLock)来实现同步。

synchronized与ReenterantLock区别

代码写法上:synchronized表现为API层面上的互斥锁(lock()和unlock()方法配合try/finally语句块来完成)

                          ReentrantLock表现为原生语法层面的互斥锁。

ReentrantLock增加了一些功能:

  1. 等待可中断:是指当持有锁的线程长时间占用锁资源不释放,正在等待的线程可以选择放弃等待,改为处理其他事情,可中断特性对处理执行时间非常长的同步块很有帮助。
  2. 公平锁:是指多个线程在等待一个锁时,必须按照申请锁的时间顺序来依次获得锁;而非公平锁则不保证这一点,在锁被释放时,任何一个等待锁的线程都有机会获得锁。synchronized是非公平锁,ReentrantLock默认是非公平锁,但可以通过带布尔值的构造函数要求使用公平锁。
  3. 锁绑定多个条件:指一个ReentrantLock对象可以同时绑定多个Condition对象。
非阻塞同步

互斥同步最主要的问题就是进行线程阻塞和唤醒所带来的性能问题,因此这种同步也叫阻塞同步。

从处理问题的方式上说,互斥同步属于一种悲观的并发策略。

随着硬件指令集的发展,我们有了另外一个选择:基于冲突检测的乐观并发策略,通俗地说,先进行操作,若没有其他资源争用共享数据,那操作就成功了;如果共享数据有争用,产生了冲突,那就采取其他的补偿措施。这种乐观的并发策略的许多实现都不需要把线程挂起,因此这种同步操作称为非阻塞同步

常用硬件指令:

  • 测试并设置(Test-and-Set)
  • 获取并增加(Fetch-and-Increment)
  • 交换(Swap)
  • 比较并交换(Compare-and-Swap,CAS)---ABA问题、逻辑漏洞
  • 加载链接/条件存储(Load-Linked/Store-Conditional,下文称LL/SC)
无同步方案

要保证线程安全,并不是一定就要进行同步,两者没有因果关系。同步只是保证共享数据争用时的正确性手段,如果一个方法本来就不涉及共享数据,那自然就无须任何同步措施去保证正确性,因此有些代码天生就是安全的。简单介绍其中的两类:

  1. 可重入代码(Reentrant Code):也叫纯代码(Pure Code)可以在代码执行的任何时刻中断它,转而去执行另一段代码,当控制权返回后,原来的程序不会出现任何错误。
  2. 线程本地存储(Thread Local Storage): 如果一段代码所需要的数据必须与其他代码共享,那就看看这些共享数据的代码能否能保证在同一个线程中执行?若能保证,我们就可以把共享数据的可见范围限制在同一个线程之内,这样,无须同步也能保证线程之间不出现数据争用的问题。
Java中如果一个变量要被多个线程访问,可以使用volative关键字声明它为“易变的”,如果某个变量被单个线程独享,Java中并没有单独的关键字来标识,不过可以通过java.util.ThreadLocal类来实现线程本地存储的功能。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值