我理解的Java并发基础(三):线程安全与锁

Java线程的状态
新建(New)、运行(Runable)、无限期等待(Waiting)、限期等待(TimedWaiting)、阻塞(Blocked)、结束(End)。html

网上找到的一份线程的状态图

  无限期等待: 线程不会被分配CPU执行时间,等待其余线程显式的唤醒。好比:Object.wait()、Thread.join()等。
  限期等待: 线程也不会被分配CPU执行时间,必定时间后由操做系统唤醒。好比:Thread.sleep(long timeout)、Object.wait(long timeout)、Thread.join(long millis)。
  阻塞: 等待获取排它锁的状态。java

  线程的优先级高是告诉操做系统的调度器以较高的频率执行线程。jvm有10个层级的优先级,windows系统有7个层级,linux系统有100个层级,而Sun的Solaris更是具备2的31次方个层级。jvm与操做系统之间的线程优先级映射不能很好的匹配。建议调整优先级的时候选用MAX_PRIORITY、NORM_PRIORITY和MIN_PRIORITY三种级别。
  线程的优先级默认是5。linux

不一样线程之间对线程状态进行通讯的Api:程序员

  • yield() 能够用来告诉调度器,该线程已经执行完了最重要的部分,能够适当让出CPU了。
  • join() 本线程进入等待状态,直到被join()的线程执行完毕后再继续执行。JDK1.7有fork/join框架。
  • sleep()和yield()都没有释放锁。wait()会释放锁。
  • wait()、sleep()、notify()、notifyAll()能够用来完成线程之间的协做。
  • interrupt()方法能够中断被阻塞的任务。推荐使用Executor的shutdownNow()来中断它启动的全部线程。

Thread.sleep(0)、Thread.sleep(1)、Thread.yeld()的区别数据库

  • Thread.sleep(1):普通的sleep()方法。释放CPU使用权并睡眠1毫秒,以后继续争抢CPU使用权
  • Thread.yeld():释放当前的CPU使用权,有调度器安排其余线程使用CPU。若是当前没有线程争抢CPU,则该线程继续执行。
  • Thread.sleep(0):释放当前CPU使用权,可是,调度器只能安排优先级比当前线程高的线程来使用CPU。若是没有比当前线程优先级高的,则当前线程继续执行。

什么是线程安全问题?
  多线程执行对共享变量进行操做,操做的结果符合程序员的预期,则线程安全。若是操做结果随机且不符合程序员预期则线程不安全。编程

备注
  一般说的线程安全指的是某一个方法或操做是不是线程安全的。当多个方法并行执行的时候,就另说了: 线程绝对安全
  某方法必定是线程安全的,即便与别的方法并行操做同一个资源。好比CopyOnWriteArrayList、AtomicReference<V>、ReentrantReadWriteLock等。
线程相对安全
  好比Vector、HashTable等这类资源,一般都是某个单一操做是线程安全的,但当多个该类对象的方法并行的时候就可能出错,好比在A在遍历,而B在A遍历期间对元素进行了删除。windows

线程安全的实现方法:
1,互斥同步(阻塞式、悲观锁)。
  实现互斥同步的方式:临界区(CriticalSection)、互斥量(Mutex)、信号量(Semaphore)
  Java中最基本的互斥同步手段的关键字是synchronized。synchronized编译后的字节码在同步块先后会造成monitorenter和monitorexit两个字节码指令。数组

2,非阻塞同步(乐观锁)。
  概念:通俗讲,先进行操做,若是期间没有其余线程争抢共享数据,就操做成功了。整个过程没有锁操做。但若是操做期间发现有其余线程在同时操做并产生了冲突,那就采起其余的补偿措施(最多见的补偿措施就是不断的尝试,知道成功为止)。这种策略并不会把线程挂起,因此叫作非阻塞同步。
  这种策略由CPU提供的CAS(Compare-and-Swap)的原子性操做来保障。缓存

Compare-and-Swap(CAS):比较和交换。
  cpu拿到原始值和地址值进行运算,获得计算后的新值。而后拿着原始值和地址值去内存中找到值进行比较,若是相同,就表示没有其余线程有共享操做,用它计算后的新值替换掉内存中的原始值。若是不一样,就拿着第二次从内存中的值再进行一次运算,重复一样的动做,直到成功。
  只比较值的CAS会有一个ABA的问题。主内容中的值为A,线程1拿走去运算了,线程2也拿走去运算了。线程2将新值B设置成功,其余线程拿到B后又设置回了A。这时候线程1拿着运算后的值C来比较了,由于只比较值,因此线程1会认为这个值并无发生变化由于设置成C。于是发生线程安全问题。
  解决办法是拿着内存值的版本号来进行对比。安全

volatile

  被volatile修饰的变量,对其余线程具备“可见性”。
  被volatile修饰的变量,生成的汇编指令,在该变量前会有Lock指令。Lock前缀的指令在多核处理器下会引起两件事情:

  1. 将当前处理器缓存行的数据写会到到系统内存
  2. 这个写会内存的操做回事其余CPU里缓存了该内存地址的数据无效

java线程之间的通讯有4种方式:

  1. A线程写volatile变量, 随后B线程读这个volatile变量。
  2. A线程写volatile变量, 随后B线程用CAS更新这个volatile变量。
  3. A线程用CAS更新一个volatile变量, 随后B线程用CAS更新这个volatile变量。
  4. A线程用CAS更新一个volatile变量, 随后B线程读这个volatile变量。

concurrent包的实现:AtomicXxx类的加减和赋值操做。

关于volatile的原理以及cpu对Lock前缀指令实现可见性的原理参考http://www.cnblogs.com/xrq730/p/7048693.html

synchronized

java中的每个对象均可以做为锁,有如下3种表现形式:

  1. synchronized修饰普通方法,锁对象是当前实例对象
  2. synchronized修饰静态方法,锁对象是当前类的class对象
  3. synchronized做用于同步方法块,锁对象是synchronized()括号里的对象

  synchronized代码块同步指令是monitorenter和monitorexit指令实现的。须要同步的代码前使用monitorenter,同步结束后的代码块后使用monitorexit。monitorenter和monitorexit是必须成对出现的。虚拟机执行monitorenter指令时,会去获取对应的对象的锁,执行monitorexit指令时会还回对应的对象的锁。

  java.util.concurrent.locks包中有ReentrantLock类,也是并发同步时常用的。

ReentrantLock与synchronized的区别
  synchronized是一个java的关键字,表示同步。而ReentrantLock是java的一个class。两者的却别主要体如今ReentrantLock的一些API上:

  1. 构造方法能够选择是否执行公平锁,默认非公平锁;
  2. 尝试获取锁(能够轮询) tryLock()和尝试一段时间内去获取锁tryLock(long timeout, TimeUnit unit);
  3. 锁中断lockInterruptibly(),死锁的时候能够采用锁中断本身来解除死锁;
  4. 一个ReentrantLock对象能够执行newCondition()绑定多个Condition对象来表示多条件执行;
  5. lock()与unlock()容许在不一样的方法体中调用,增长使用的灵活性,虽然极不推荐这样使用;

锁优化
  锁优化主要是编译器或者JVM的的优化,跟开发者没有太大的关系,但做为一名合格的开发者,仍是须要了解的。锁优化的几种方式:自旋锁与自适应自旋、锁消除、锁粗化、轻量级锁、偏向锁

自旋锁
  互斥同步的性能消耗体如今线程阻塞时的线程挂起与恢复很消耗性能,Java虚拟机开发团队注意到在不少应用上锁的状态只会持续不少时间,为了这很短的时间而阻塞线程不是很划算。因而规定,在线程未获取到锁的时候,不是直接挂起,仍是执行一个忙循环(相似while(true))后看是否能获取到锁。这项技术就叫自旋锁。缺点是若是锁占据的时间比较长的时候,白白浪费了自旋时占用的资源。默认自旋次数是10次。
自适应自旋
  虚拟机获取锁的自旋时间再也不固定,而是由虚拟机根据前一次的自旋时间及拥有者的状态来决定。换言之,虚拟机运行时间越长越“聪明”。

锁消除
  虚拟机在运行期的优化。若是虚拟机判断到锁内的变量不会出现共享数据,则会将锁进行消除。

锁粗化
  若是虚拟机探测到有一组零碎的操做都对同一个对象加锁,将会把加锁范围扩展(粗化)到整个操做序列的外部,这样只须要加锁一次就能够了。

前面的文章已经介绍过对象在JVM中的布局方式:
  每个堆中的对象在HosPost虚拟机中都有一个对象头(ObjectHeader),对象头里存储两部分信息:一部分是运行时对象自身的哈希码值(HashCode)、GC分代年龄(Generational GC Age)、锁标志位(占用2个bit的位置)等信息,另外一部分是指向方法去中的类型信息的指针。若是是数组对象的话,还会有额外一部分用来存储数组长度。

  当一个线程想获取对象的锁的时候,先读取对象头的锁标志位信息。若是锁标志位是01,表示该对象上没锁。因而会经过CAS来判断是否能够获取锁。获取成功后将锁标志位置为00,表示轻量级锁。若是CAS失败则表示已经有线程在竞争到锁了,则锁标志位置为10,升级为重量级锁,该线程进入阻塞状态。若是线程在获取对象头的锁标志位已是轻量级锁了,则判断对象锁是否属于当前线程,若是属于则锁重入;若是不属于,则锁标志位置为10,直接升级为重量级锁,线程进入阻塞状态。若是线程在获取对象头的锁标志位已是重量级锁了,则线程直接进入阻塞状态。当持有锁的线程最终释放锁的时候,若是锁的标志位为00(轻量级锁)则表示在此期间只有本身获取了锁,没有线程竞争。若是锁的标志位是10(重量级锁)则表示有线程曾经竞争过锁,则在释放锁的时候唤醒被挂起的线程。

  轻量级锁提高性能的依据是“绝大部分的锁,在同步期内没有其余线程竞争”,这个是经验数据。轻量级锁的本意是在多线程竞争若是存在锁竞争,除了锁自己互斥量的开销外,还额外发生了CAS操做,所以在有竞争的状况下,轻量级锁会比传统的重量级锁更慢。

偏向锁
  Hotspot的做者通过以往的研究发现大多数状况下锁不只不存在多线程竞争,并且老是由同一线程屡次得到,为了让线程得到锁的代价更低而引入了偏向锁。当一个对象首次被一个线程使用CAS请求锁的时候,将对象的锁标志位置为01,偏向锁标志位置为1。之后该线程每次请求该对象锁的时候,连CAS操做都省略掉,直接消除同步来执行。若是有其余线程竞争该对象锁的时候,偏向锁撤销。根据以前的偏向线程是否在执行本来应该是同步的操做的状态来判断升级为轻量级锁仍是重量级锁。 偏向锁能够提升带有同步但无竞争的程序性能。若是程序中大多数的锁老是被多个不一样的线程访问,那偏向模式就是多余的。在具体情形分析下,禁止偏向锁优反而可能提高性能。

  关于锁机制及其原理,参考http://www.infoq.com/cn/articles/java-se-16-synchronized/

避免死锁的几个经常使用方法:

  1. 避免一个线程同时获取多个锁
  2. 避免一个线程在锁内同时占用多个资源,尽可能保障每一个锁只要用一个资源
  3. 尝试使用定时锁,使用lock.tryLock(timeout)来替代使用内部锁机制
  4. 对于数据库锁,加锁和解锁必须在同一个数据库链接里,不然会出现解锁失败的状况

  当死锁不可避免的时候,通常采用鸵鸟策略。

公平性锁与非公平锁
  公平性锁保证了锁的获取按照FIFO原则,而代价是进行大量的线程切换。非公平性锁虽然可能形成线程“饥饿”,但极少的线程切换,保证了其更大的吞吐量。

参考资料:

相关文章
相关标签/搜索