Java线程安全
文章来自 shichang // Welcome!
主页
|
About Me
|
归档
|
标签
线程安全:保证在多线程运行环境中,程序的能正确运行并且正确性有迹可循。主要体现在多个线程对共享资源的抢夺,同时在保证程序能正确运行的情况下减少性能消耗。 三种方式可以保证线程安全: ### 互斥同步 使用互斥来保证线程安全和同步。在Java中有Synchronized关键字和ReentrantLock两种互斥手段。 `Synchronized`关键字是由JVM提供的语法,可以修饰实例方法,静态方法,或者直接对对象使用。Synchronized关键字的使用必须要有一个引用类型的变量,修饰对象和实例方法时锁住的对象是当前对象,修饰静态方法时锁住的对象是当前类的Class对象。 在编译成字节码后,Synchronized关键字被编译成monitorenter和monitorexit两个命令,程序在运行到monitorenter时必须获得对象的锁才能执行后续的命令。在执行monitorexit时,释放掉持有的锁。 `ReentrantLock`是API层面的锁,使用显示的lock(),和unlock()方法对代码段进行加锁和解锁并结合try-catch使用。ReentrantLock比Synchronized多了几个功能:等待可中断、公平锁、绑定多个Condition。 等待可中断指的是,ReentrantLock中的tryLock(long TimeOut,timeunit)方法,设置获取锁的超时时间,如果超时则中断放弃。 公平锁指的是,锁被占用后,剩下的竞争线程进行排队,当锁释放时,由队列中线程逐个获取锁,底层是有AQS实现的。非公平指的是,锁被占用,其他的线程都阻塞,锁被释放时,新加入的线程也会参与竞争,对上一轮竞争的线程来说不公平,不讲究先来后到。 Synchronized和ReentrantLock都是可重入锁,一个线程已经获得了对象锁后再尝试获取锁不会把自己锁死。两者在Java中都是重量锁,没有获取到对象锁的线程将阻塞,等待对象锁的释放然后被唤醒,阻塞和唤醒都需要操作系统的参与,从用户态进入内核态,比较浪费CPU资源。这两者也是悲观锁的代表锁,`悲观锁`认为,如果不使用同步措施,那么程序一定会出现不同步错误。两者的性能上ReentrantLock的性能要高于Synchronized。因为Synchronized是JVM实现的锁,而ReentrantLock是API层面的锁,当代码段出现异常时,JVM会自动释放Synchronized的锁,而Lock则不行。 ### 锁的实现 对象在虚拟机中的内存结构是对象头+实例数据+填充数据,对象头中包含了对象的一些标志信息,如hashCode、分代年龄、锁标志位。对象没有被锁定的时候,该标志位默认为01,有线程想要获取该对象的锁时,线程先在当前线程的栈帧中建立一个锁记录空间,同时将该对象的对象头信息复制到这个锁记录空间中,然后虚拟机使用CAS尝试将对象头信息更新为指向线程锁记录的指针,如果成功,那么该线程就成功获取到了该对象的锁。对象头中的锁标志位变为00. ### 非阻塞同步 在互斥同步中多个线程抢夺共享资源时,未抢夺到资源的线程将会被阻塞,这会导致大量性能的消耗。非阻塞同步基于`乐观锁`的理论,认为即使不使用同步手段,程序也能正常运行下去,当发生了冲突,再采取手段补救。 所以非阻塞同步的重点在于冲突的检测与补救。并且需要检测和补救都是原子操作,如果再采用加锁来保证的话就没有意义了,所以非阻塞同步中使用了硬件的支持,硬件支持以下5种操作: - Test and Set测试并设置 - Swap交换 - Fetch and Increment获取并增加 - CAS(Compare and Swap)比较并交换 - LL/SC(Loaded-Linked, Store-Conditional)加载链接,条件存储 JDK1.5以后Java中才可以使用`CAS`操作,存在三个值,变量在内存中的实际值,变量的过去预期值,新值,只有当变量在内存中的实际值等于过去预期值时,才可以使用新值更新变量在内存中的实际值。Java中的主要应用为`Atomic`原子类,使用原子类可作多线程计数器。 ### 不同步 不同步有两种方法,第一是使用可重入代码,即函数式方法,固定的输入可以得到固定的输出。第二种是使用本地线程存储。 `ThreadLocal`: 本地线程存储在Java中就是java.lang.ThreadLocal类,使用ThreadLocal可以保证某个变量被一个线程独享。在每一个线程的Thread对象中都有一个ThreadLocalMap对象,这个对象以threadLocalHashCode为键,以本地线程变量为值,ThreadLocal就是这个Map的访问入口,每一个ThreadLocal对象都包含了一个独一无二的threadLocalHashCode,使用这个hashCode就可以在Map中找到本地线程变量。 ### 锁优化 Java中Synchronized附带的性能地下,其实有非常多优化的措施。主要有:自旋锁,自适应自旋锁,锁消除,锁粗化,轻量级锁,偏向锁。 - `自旋锁和自适应自旋锁`:在互斥同步中,如果一个线程请求的对象锁正处于繁忙,则该线程将处于阻塞状态,当该对象的锁释放后,又唤醒阻塞的线程。阻塞和唤醒的操作都是由OS参与的重量级操作,OS需要从用户态切换到内核态。自旋操作表示,在线程请求的对象繁忙时不进行阻塞,操作系统让该线程执行一段特定的任务(忙循环),等待对象锁的释放,自适应自旋锁则是根据上一个线程在此对象上的自旋时间确定自旋次数。 - `锁消除`:对于一些同步语句,看起来需要使用同步,但实际上并不会存在逃逸(堆上所有数据都不会被其他线程访问到,可以看做是栈上的数据),对这样的代码进行同步加锁是不必要的,比如StringBuffer类的操作。锁消除就是对一些不需要同步的语句块消除同步处理,直接执行,提高效率和运行性能。 - `锁粗化`:对于一系列连续的对同一个对象的加锁和解锁,可以将这一段连续代码使用一个锁。 - `轻量级锁`:在Synchronized关键字的语义中,使用操作系统的互斥量来进行同步,轻量级锁使用CAS操作来避免使用互斥量进行同步,提高程序的性能。 - `偏向锁`:对象锁偏向于第一个获得锁的线程,在接下来的过程中,如果没有其他线程来竞争该对象锁,那么持有偏向锁的线程将永远不需要再进行同步。 ### Volatile volatile关键字保证可见性和有序性,但不保证原子性。 可见性指,变量在被修改后将立即被同步到主内存中,其他线程对变量进行读取时,都先刷新主内存再进行读取。有序性指,禁止编译器对volatile变量进行指令重排序。 volatile的语义是:一个变量可能会被多个线程进行读取时,可以使用volatile关键字来表示这个变量是‘不稳定的’。编译器对不稳定的变量采取一些稳定的措施。
Pre:
HTTP协议
Next:
线程的同步