zoukankan      html  css  js  c++  java
  • 深入理解Java虚拟机(十)——线程安全与锁优化

    什么是线程安全

    当多个线程同时访问一个对象的时候,不需要考虑什么额外的操作就能获取正确的值,就是线程安全的。

    线程安全的程度

    1.不可变

    不可变的对象一定是线程安全的,因为值始终只有一个。

    final,这种安全是最直接最纯粹的。

    2.绝对线程安全

    不管运行时环境如何,调用者都不需要任何额外的同步措施。

    往往JDK中说自己是线程安全的,大多不是绝对安全的。

    需要付出巨大的代价,往往不需要绝对安全。

    3.相对线程安全

    通常所说的线程安全,就是相对线程安全,需要保证对象单次的操作是安全的,不需要额外的保障措施,但是对于一些特定顺序的额外调用,就会需要额外的同步措施。

    // 读线程
    Thread t1 = new Thread( new Runnable(){
        public void run(){
            for(int i=0; i < vector.size(); i++){
                System.out.println( vector.get(i) );
            }
        }
    }).start();
    
    // 写线程
    Thread t2 = new Thread( new Runnable(){
        public void run(){
            for(int i=0; i < vector.size(); i++){
                vector.remove(i);
            }
        }
    }).start();
    

    如果上述代码中,读循环被写线程打断了,还删除了所有的元素,但是读循环的越界下标还是原来的数组大小,这样就会出现越界异常。

    需要对整个循环进行加锁。其实也可以采用迭代器进行遍历,如果在迭代的时候,集合被操作了,就会抛出异常。

    // 读线程
    Thread t1 = new Thread( new Runnable(){
        public void run(){
            synchronized( vector ){
                for(int i=0; i<vector.size(); i++){
                    System.out.println( vector.get(i) );
                }
            }
        }
    }).start();
    
    // 写线程
    Thread t2 = new Thread( new Runnable(){
        public void run(){
            synchronized( vector ){
                for(int i=0; i<vector.size(); i++){
                    vector.remove(i);
                }
            }
        }
    }).start();
    

    4.线程对立

    无论怎么同步,都不能实现线程安全,就是线程对立。

    这是有害的,要避免。

    线程安全的实现方法

    互斥同步

    是最常见最主要的并发手段。同步是指同一时刻数据只被一条线程使用。互斥是实现同步的手段。

    属于阻塞同步。

    优先考虑使用synchronized:

    • synchronized是Java语法层面的同步,清晰简单。
    • Lock需要求finally中释放锁,不然出现异常就可能无法释放锁。

    synchronized

    最基本的互斥同步手段是synchronized,通过monitorenter和monitorexit指令实现。

    1. 编译器在同步块的开始和技术位置添加monitorenter和monitorexit指令;
    2. 这两个对象都需要一个reference类型的参数来指明要锁定和解锁的对象。
    3. 如果没有指明对象,那么把当前对象或者类作为锁对象。
    4. 这是一把可重入锁。会阻塞后面想要获取锁的线程。

    当有线程要访问这段代码的时候,会先去尝试获取锁,如果锁的计数器值是0,说明可以获得锁,然后执行monitorenter将计数器值加一,获得锁。在执行代码结束的时候,执行monitorexit将锁的计数器值加1,释放锁。

    ReetrantLock

    重入锁是Lock接口的最常见形式。是可重入的。相比synchronized具有更多高级功能。

    • 等待可中断:线程长期无法获得锁的时候,可以放弃等待。
    • 公平锁:按照等待的顺序释放锁。
    • 绑定多个条件:synchronized可使用wait/notify来实现等待/通知机制,但一个synchronized同步块只能使用一次,若要使用多次,就需要嵌套同步块;但ReentrantLock可以通过newCondition创建多个条件。

    非阻塞同步

    是一种乐观锁,不对共享数据进行加锁,而是直接操作,再进行判断措施。

    JUC中各种整形原子类的自增、自减等操作就使用了CAS。

    CAS操作:存在三个值,分别是共享变量V、预期旧值A、新值B,弱V与A相同,则将V更新成B,不然就循环比较,直到更新完成。

    可能出现ABA问题。通过modCount字段建立修改版本,防止ABA。

    无同步方案

    下面两种情况不需要同步:

    • 可重入代码:某个方法,只要输入的值一样,结果就是一样的,线程怎么切换,值还是不变的。
    • 线程本地存储:把所有共享变量的操作都放在一个线程中,就不存在多线程访问的安全问题了。

    web服务器就是采用线程本地存储,把分别把每个请求封装在一个线程中处理。

    锁优化

    自旋锁

    作用

    避免线程阻塞,而是不断使用CPU循环去获取锁,减少线程切换时候上下文切换的开销。

    问题

    如果长时间不能获得锁,就是浪费CPU资源。

    自适应自旋

    根据以往自旋等待的时间,动态调整下次自旋的等待时间。

    锁清除

    移除不可能存在资源竞争处的锁,降低同步的开销。

    锁粗化

    如果虚拟机探测到对同一个对象反复加锁,则编译器会扩大锁的代码范围,从而降低锁的切换频率。

    轻量级锁

    原理

    利用CAS方法取代互斥同步。

    利用对象头的MarkWord进行CAS操作,具体原理可以看另一篇文章。
    轻量级锁

    与重量级锁比较

    • 重量级锁是悲观锁,总是假设会出现线程竞争,所以总是要进行互斥同步。
    • 轻量级锁是乐观锁,总是假设不会出现竞争,获取利用CAS操作来获得锁。

    偏向锁

    原理

    当线程请求到锁对象后,将锁对象的状态标志位改为01,即偏向模式。然后使用CAS操作将线程的ID记录在锁对象的Mark Word中。以后该线程可以直接进入同步块,连CAS操作都不需要。如果出现其他线程的竞争,那么偏向锁被取消,进入轻量级锁。

    与轻量级锁比较

    • 都是乐观锁。
    • 轻量级锁通过CAS操作代替互斥量,而偏向锁在无竞争的情况下取消了同步。
  • 相关阅读:
    .net下将富文本编辑器文本原样读入word文档
    最大流算法完整代码
    如何用程序删除win 7下SYSTEM权限的目录
    01背包问题的动态规划算法
    使用gem安装jekyll错误记录
    dev机上数据库中批量生成table
    git pull错误记录及解决
    git clone操作到开发机的错误记录
    nginx错误记录
    链表链式结构的写法
  • 原文地址:https://www.cnblogs.com/lippon/p/14117648.html
Copyright © 2011-2022 走看看