线程安全与锁优化

1、概述

面向过程的编程思想:站在计算机的角度去抽象和解决问题,以算法为核心,把数据和过程分别作为独立的部分来考虑,数据代表问题空间中的客体,程序代码则用于处理这些数据。

面向对象的编程思想:站在现实的角度去抽象和解决问题,把数据和行为都看做对象的一部分,这样程序猿能以符合现实世界的思维方式去编写和组织程序。

对于“高效并发”,首先要保证并发的正确性,然后在此基础上实现高效。

2、线程安全

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

上述定义要求线程安全的代码必须具备一个特征:代码本身封装了所有必要的正确性保障手段(如互斥同步等),令调用者无需关心多线程的问题,更无须自己采取任何措施来保证多线程的正确调用。

我们可以按照线程安全的“安全程度”由强到弱来排序,将Java中的各种操作共享的数据分为以下五类:不可变、绝对线程安全、相对线程安全、线程兼容和线程对立。

2.1、不可变

在Java语言中,不可变的对象一定是线程安全的,无论是对象的方法实现还是方法的调用者,都不需要再采取任何的线程安全保障措施。只要一个不可变的对象被正确地构建出来(没有发生this引用逃逸的情况),那其外部的可见状态永远也不会改变,永远也不会看到它在多线程中处于不一致的状态,“不可变”带来的安全性是最简单和最纯粹的。

在Java语言中,如果共享数据是一个基本数据类型,那么只要在定义时使用final关键字修饰它就可以保证它是不可变的。如果共享的是一个对象,那就需要保证对象的行为不会对其状态产生任何影响才行,比如java.lang.String类的对象,它是一个典型的不可变对象,我们调用它的subString()、replace()和concat()这些方法都不会影响它原来的值,只会返回一个新构造的字符串对象。

保证对象行为不影响自己状态的途径有很多种,其中最简单的就是把对象中带有状态的变量都声明为final,这样在构造函数结束之后,它就是不可变的。

Java中常见的不可变类型有String、枚举类以及java.lang.Number的部分子类,如Long和Double等数值包装的类型,BigInteger和BigDecimal等大数据类型。但是同为Number子类的原子类AtomicInteger和AtomicLong则并非不可变的。

2.2、绝对线程安全/相对线程安全

相对的线程安全就是我们通常意义上所讲的线程安全,它需要保证对这个对象单独的操作是线程安全的,我们在调用的时候不需要做额外的保障措施,但是对于一些特定顺序的连续调用,就可能需要在调用端使用额外的同步手段来保证调用的正确性。在Java语言中,大部分的线程安全类都是属于相对线程安全的类,例如Vector、HashTable、Collections的SynchronizedCollecttion()方法包装的集合等。何为特定顺序的连续调用(或者复合操作)呢?示例:

    private static Vector<Integer> vector = new Vector<>();

    public static void main(String[] args) {
        while (true) {
            for (int i = 0; i < 10; i++) {
                vector.add(i);
            }

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

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

            removeThread.start();
            printThread.start();
            //不能同时产生过多的线程,否则会导致系统假死
            while (Thread.activeCount() > 20) ;
        }
    }

上述代码在运行了之后,会抛出ArrayIndexOutOfBoundException异常。Vector明明是线程安全的,为啥还会报异常呢?因为在多线程环境中,如果另外一个线程恰好在错误的时间里删除了一个元素,导致序号i已经不可用,再用i访问数组就会抛出异常。因此就需要使用额外的同步手段来保证调用的正确性。

  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));
                        }
                    }
                }
            });

绝对线程安全是什么意思呢?就是“不管运行时环境如何,调用者都不需要额外的同步措施”。通常,一个类要做到绝对线程安全的代价是很大的,甚至有的时候是不切实际的代价。

2.3、线程兼容

是指对象本身不是线程安全的,但是可以通过正确的使用同步手段来保证对象在并发环境中可以安全的使用,Java Api的绝大多数类都是属于线程兼容的,比如ArrayList和HashMap。

2.4、线程对立

无论是否采取了同步措施,都无法在多线程的环境中并发使用的代码。由于Java语言天生就具备多线程的特性,线程对立这种排斥多线程的代码是很少出现的,而且通常是有害的,应当尽量避免。比如Thread类的suspend、resume、System.setIn()、System.setOut()、System.runFinalizersOnExit()等。

2.5、线程安全的实现方法

2.5.1、互斥同步

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

根据虚拟机规范的要求,在执行monitorenter指令时,首先尝试获取对象的锁。如果这个对象没被锁定,或者当前线程已经拥有了那个对象的锁,把锁的计数器加1,相应的,在执行monitorexit指令时会将锁计数器减一,当计数器为0,锁就被释放。如果获取对象的锁失败,那当前线程就要阻塞等待,直到对象的锁被另外一个线程释放为止。

在虚拟机规范对monitorenter和monitorexit的行为描述中,有两点需要注意:1、synchronized同步块对同一条线程来说是可重入的,不会出现自己把自己锁死的问题;2、同步块在已进入的线程执行完之前,会阻塞后面其他线程的进入;

此外,还应该知道synchronized是Java语言中一个重量级的操作。对于简单的同步代码块,状态转换消耗的时间有可能比用户代码执行的时间还要长。

除了synchronized之外,我们还可以使用java.util.concureent包中的重入锁(ReentrantLock)来实现同步,与synchronized相似,ReentrantLock也具备一样的线程重入特性,相比synchronized,ReentrantLock增加了一些高级功能,主要有:等待可中断、可实现公平锁、以及锁可绑定多个条件(Condition对象)。【synchronized与ReentrantLock默认都是非公平的】

ReentrantLock与synchronized的选择:

如果需要使用上述功能,使用ReentrantLock是一个很好的选择。如果基于性能呢?JDK1.6发布之后,人们就发现synchronized与ReentrantLock的性能是基本持平了。如果开发者在JDK1.6或以上的版本部署,那么性能不再是选择ReentrantLock的理由了,虚拟机在未来的性能改进中肯定也会偏向于原生的synchronized,所以还是提倡在synchronized能实现需求的情况下,优先考虑使用synchronized来进行同步。

2.5.2、非阻塞同步(原子变量)

原子变量与非阻塞同步机制

在JDK1.5之后,Java程序才可以使用CAS操作,该操作由sun.miss.Unsafe类里面的compreAndSwapInt和compreAndSwapLong等几个方法包装提供,虚拟机在内部对这些方法做了特殊的处理,即时编译出来的结果就是一条平台相关的处理器CAS指令,没有方法的调用过程,或者可以认为是无条件内联进去了。

由于Unsafe类不是提供给用户程序调用的类(Unsafe.getUnsafe()的代码限制了只有启动类加载器加载的Class才能访问它),因此,如果不采用反射手段,我们只能通过其他的Java Api来间接使用它,如j.u.c包里面的整数原子类,其中的compreAndSet和getAndIncrement等方法都使用了Unsafe类的CAS操作。

2.5.3、无同步方案

1、线程本地存储;

2、可重入代码:可重入代码有一些共同的特征,例如不依赖存储在堆上的数据和公用的系统资源、用到的状态量都由参数中传入、不调用非可重入的方法等。只要满足可重入的要求,那么这段代码就是线程安全的。

3、锁优化

高效并发是从JDK1.5到JDK1.6的一个重要改进,HotSpot虚拟机团队在这个版本上花费了大量的精力去实现各种锁优化的技术,如适应性自旋、锁消除、锁粗化、轻量级锁、偏向锁等,这些技术都是为了在线程之间更高效地共享数据,以及解决竞争问题,从而提高程序的执行效率。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值