由JVM深入了解Java的线程安全与锁优化

目录

•写在前面

•线程安全

Java语言中的线程安全

线程安全的实现方法

•锁优化

自旋锁与自适应自旋

锁消除

锁粗化

轻量级锁

偏向锁


•写在前面

讲道理,在谈及线程安全以及锁优化之前,需要先搞清楚啥是线程,像那些硬概念这里我就不多说,什么线程是比进程更轻量级的调度执行单位,线程的引入,可以把一个进程的资源分配和执行调度分开,各个线程既可以共享进程资源,又可以独立调度(线程是CPU调度的基本单位),什么线程的5种状态:新建、运行、等待、阻塞、结束等。然后还有个要提及的是,实现线程主要有3种方式:使用内核线程实现、使用用户线程实现和使用用户线程加轻量级进程混合实现。具体啥的概念不是这篇文章的内容,我这里就不多说,可以自行去查理解。注意了,线程调度是指系统为线程分配处理器使用权的过程,主要调度方式有两种,分别是协同式线程调度和抢占式线程调度,而java是抢占式线程调度。

•线程安全

这个抽象的定义,可以这么说:多个线程访问一个对象时,如果不用考虑这些线程在运行时环境下的调度和交替,也不需要额外的同步,或者调用方进行其他的协同操作,调用这个方法的行为最终都能够获得正确的结果,那这个对象是线程安全的。

Java语言中的线程安全

在上面我们对线程安全下了一个抽象的定义,那接下来我们就讨论在java语言中,线程安全具体是如何体现的?有哪些操作是线程安全的?我们可以按照线程安全的程度由强至弱来排序,我们可以将java语言中各种操作共享的数据分为以下5类:不可变、绝对线程安全、相对线程安全、线程兼容和线程对立。

1、不可变,其实听到不可变心里就有数了,不可变的对象一定是线程安全的,无论是对象的方法实现还是方法的调用者,都不需要再采取任何的线程安全保障措施。像final(未发生this逃逸)、java.lang.String:进行subString()、replace()、concat()不会影响原来的值,而是构建出新的一个不可变的值并返回。这里举个例子,Integer构造函数的代码,它通过将内部状态变量value定义为final来保障状态不变。

 private final int value;  

  /** 
   * Constructs a newly allocated {@code Integer} object that 
   * represents the specified {@code int} value. 
   * 
   * @param   value   the value to be represented by the 
   *                  {@code Integer} object. 
   */  
  public Integer(int value) {  
      this.value = value;  
  }

2、绝对线程安全:不管运行环境如何,调用者都不需要进行额外的同步措施,线程安全不一定就是真的安全。在JavaAPI中标注自己是线程安全的类有很多,大多数都不是绝对线程安全的,这里使用Vector举例,Vector的内部实现每个方法都用synchronized修饰了的。

Thread removeThread = new Thread(new Runnable() {  
    @Override  
    public void run() {  
        synchronized(vector) {  
            for (int i = 0; i < vector.size(); i++) {  
                vector.remove(i);  
            }  
        }  
    }  
});  

Thread printThread = new Thread(new Runnable() {  
    @Override  
    public void run() {  
        synchronized(vector) {  
            for (int i = 0; i < vector.size(); i++) {  
                System.out.println(vector.get(i));  
            }  
        }  
    }  
});

3、相对线程安全:不需要做额外的同步,但是特定的连续调用顺序的情况下需要同步处理,才能保证调用的结果准确,如Vector\HashTable\Collections的synchronizedCollection()方法包装的集合等。

4、线程兼容:指对象本身不是线程安全的,但是可以通过调用端正确地使用同步手段来保证对象在并发环境中可以安全地使用,Java API大部分类属于线程兼容,如ArrayList、HashMap。

5、线程对立:无论调用端是否采取了同步措施,都无法在多线程环境下并发使用的代码。有害的,应该尽量避免,如Thread类的suspend()和resume()方法,如果两个线程同时持有一个线程对象,一个尝试去中断线程,一个尝试去恢复线程,并且并发进行,无论调用时是否进行了同步,目标线程都存在死锁风险,所以这两个方法已经被声明废弃。

线程安全的实现方法

1、互斥同步

  • 原则:保证共享数据在同一时刻只能被一个线程访问

  • 互斥是方法,同步时目的

  • synchronized:对于同一条线程来说是可重入的,不会出现把自己锁死的情况

  • java.util.concurrent:重入锁ReentrantLock

    • 等待可中断:持锁线程一直没有释放,等待的线程可以放弃等待

    • 公平锁:多个线程等待同一个锁时,必须按照申请锁的时间顺序来依次获得锁

    • 绑定多个条件:ReentrantLock 调用多次newCondition,绑定多个Condition对象可以实现

  • 优先考虑synchronized,性能差不多。

2、非阻塞同步

  • 原则:不把线程挂起,有错误采取补偿措施;

3、无同步

  • 保证线程安全,不一定要同步;

  • 天生线程安全的代码:

    • 可重入代码:如果一个方法,他的计算结果是可以预测的

    • 线程本地存储:java.lang.ThreadLocal

•锁优化

自旋锁与自适应自旋

在互斥同步中,互斥同步对性能最大的影响就是阻塞的实现,挂起线程和恢复线程的操作都需要装入内核态中完成,这些操作给系统的并发性能带来了很大的眼里,同时,虚拟机的开发团队也注意到了在许多应用上,共享数据的锁定状态只会持续很短的时间,为了这段时间去挂起和恢复线程并不值得。如果物理机器有一个以上的处理器,能让两个或者以上的线程同时并行执行,我们就可以让后面请求锁的那个线程“稍等一下”,但不放弃处理器的执行时间,看看持有锁的线程是否很快就会释放锁,为了让线程等待,我们只需要让线程执行一个忙循环(自旋),这项技术就是所谓的自旋锁。自旋等待不能代替阻塞,且先不说对处理器数量的要求,自旋等待本身虽然避免了线程切换的开销,但它是要占用处理器时间的。自旋等待的时间必须要有一定的限度,如果自旋超过了限定的次数仍然没有成功获得锁,就应该使用传统的方式去挂起线程了,自旋次数的默认值是10次。

而自适应的自旋锁就意味着自旋的时间不再固定了,而是由前一次在同一个锁上的自旋时间及锁的拥有者的状态来决定的,如果在同一个锁对象上,自旋等待刚刚成功获得过锁,并且持有锁的线程正在运行中,那么虚拟机就会认为这次自旋也很有可能再次成功,进而它将允许自旋等待持续相对更长的时间,比如100个循环。总结如下:

  • 自旋锁:让线程执行一个忙循环(实际让他瞎忙活一会)
  • 自适应自旋:自旋时间有上一次同一个锁自旋时间和锁的拥有者状态来决定

锁消除

锁消除是指虚拟机即使编译器在运行时,对一些代码上要求同步,但是被监测到不可能存在共享数据竞争的锁进行消除。锁消除的主要判定依据来源于逃逸分析的数据支持,如果判断在一段代码中,堆上的所有数据都不会逃逸出去从而被其他线程访问到,那就可以把他们当做栈上数据对待,认为它们是线程私有的,同时加锁自然就无须进行。总结如下:

  • 虚拟机即时编译时发现有些声明了锁,但是实际上没有用的锁(不存在共享数据竞争),直接进行消除

锁粗化

原则上,我们在编写代码的时候,总是推荐将同步块的作用范围限制得尽量小-只在共享数据的实际作用域中才进行同步,这样是为了使得需要同步的操作数量尽可能变小,如果存在锁竞争,那等待锁的线程也能尽快拿到锁。不过大部分情况下,上面的原则都是正确的,但是如果一系列的连续操作都对同一个对象反复加锁和解锁,甚至加锁的操作是出现在循环体重,那即使没有线程竞争,频繁地进行互斥同步操作也会导致不必要的性能损耗。

  • 本来是锁的范围越小越好,但是对于里面有反复加锁解锁的情况,那就把范围扩大。

轻量级锁

它名字中的“轻量级”是相对于使用操作系统互斥量来实现的传统锁而言的,因此传统的锁机制就成为"重量级"锁,首先要强调一点的是,轻量级锁并不是用来代替重量级锁的,它的本意是在没有多线程竞争的前提下,减少传统的重量级锁使用操作系统互斥量产生的性能消耗。轻量级锁能提升程序同步性能的依据是“对于绝大部分的锁,在整个同步周期内都是不存在竞争的”

  • 没有多线程竞争的前提下,减少传统的重量级锁使用操作系统互斥量产生的性能消耗

偏向锁

偏向锁的目的是消除数据在无竞争情况下的同步原语,进一步提高程序的运行性能。如果说轻量级锁是在无竞争情况下使用CAS操作去消除同步使用的互斥量,那偏向锁就是在无竞争的情况下把整个同步都消除掉,连CAS操作都不做了。偏向锁顾名思义就是这个锁会偏向于第一个获得它的线程,如果在接下来的执行过程中,该锁没有被其他的线程获取,则持有偏向锁的线程将永远不需要再进行同步。

  • 锁会偏向于第一个获得它的线程
  • 当有另外的线程尝试获取时,偏向模式宣告结束

线程安全是说在没有额外或者协同的同步处理的情况下,多线程操作共享数据,依然可以得到准备的结果。Java开发中主要通过互斥同步的方案来达到线程安全的目的,其中用到的Synchronized和ReentrantLock。还有记住ThreadLocal可以给线程存储私有的信息。我们在进行同步的时候是通过锁 来保证的,对于有些没有必要的锁,我们通过一些手段进行优化,进来减少锁带来的性能开销。

发布了78 篇原创文章 · 获赞 440 · 访问量 73万+

猜你喜欢

转载自blog.csdn.net/DBC_121/article/details/103696903