【Java】线程安全

在开始说线程安全之前,需要先对Java内存模型乐观锁、悲观锁有一定认识。
如果你阅读了上面的文章,你会明白,所谓线程不安全,其实是无法满足原子性、顺序性、可见性。那么,在Java多线程编程中,如何配合乐观锁、悲观锁以满足原子性、顺序性、可见性呢?

1.Atomic

Atomic是对乐观锁CAS的一种实现,以满足JMM的原子性。Atomic从JDK1.5开始提供,包为java.util.concurrent.atomic。这个包里面提供了一组原子变量的操作类,这些类可以保证在多线程环境下,当某个线程在执行atomic的方法时,不会被其他线程打断。
我们经常可以看到自加运算符count++,它并不是原子操作,count++需要经过读取、修改、写入三个步骤。假设count = 1,有两个线程,都拷贝了count的副本到自己的工作内存中,每个线程count = 1,那么当A线程执行count++后,A线程的count = 2,会尝试更新主内存的count,而B线程执行count++后,B线程的count = 2,也会尝试更新主内存的count。使用Atomic的类可以解决原子性问题。
下面例举一些Atomic的常见类:

  • 基本类型:
    • AtomicBoolean:布尔型
    • AtomicInteger:整型
    • AtomicLong:长整型
  • 数组:
    • AtomicIntegerArray:数组里的整型
    • AtomicLongArray:数组里的长整型
    • AtomicReferenceArray:数组里的引用类型
  • 引用类型:
    • AtomicReference:引用类型
    • AtomicStampedReference:带有版本号的引用类型
    • AtomicMarkableReference:带有标记位的引用类型
  • 对象的属性:
    • AtomicIntegerFieldUpdater:对象的属性是整型
    • AtomicLongFieldUpdater:对象的属性是长整型
    • AtomicReferenceFieldUpdater:对象的属性是引用类型
  • JDK8新增DoubleAccumulator、LongAccumulator、DoubleAdder、LongAdder是对AtomicLong等类的改进。比如LongAccumulator与LongAdder在高并发环境下比AtomicLong更高效。
  • 解决CAS中的ABA问题,Java里的实现是AtomicStampedReference和AtomicMarkableReference类。

2.synchronized

synchronized是Java的关键字,是对悲观锁的一种实现,可以满足JMM中原子性、顺序性、可见性。

2.1.原理

在JDK1.6之前,synchronized的底层原理是通过对一个对象的monitor(监视器)的获取与释放来实现同步。是依赖于底层操作系统的 Mutex Lock 实现,操作系统实现线程之间的切换需要从用户态到内核态的切换,切换成本非常高。
对于代码块,经过编译之后会在代码块的前后分别形成monitorenter(获取锁)和monitorexit(释放锁)这两个字节码指令。monitorenter插入到代码块的开始位置,monitorexit插入到代码块结束处和异常处。在执行monitorenter指令时,首先要去尝试获取对象的锁。如果这个对象没被锁定,或者当前线程已经持有了那个对象的锁,就把锁的计数器的值+1,而在执行monitorexit指令时会将锁计数器的值-1。一旦计数器的值为0,锁随即就被释放了。
对于方法,是通过ACC_SYNCHRONIZED这个修饰符来完成的。JVM可以从方法常量池的方法表结构中的ACC_SYNCHRONIZED得知方法是否被声明为同步方法。当方法调用时,调用指令会检查方法的ACC_SYNCHRONIZED是否被设置,如果设置了,执行线程就要求先持有monitor对象,然后才能执行方法,最后当方法执行完(无论是正常完成还是非正常完成)时释放monitor对象。

2.2.用法

synchronized可以:

  • 修饰一个代码块,被修饰的代码块称为同步语句块,其作用的范围是{}括起来的代码,作用的对象是调用这个代码块的对象。
    • 一个线程访问一个对象中的同步代码块时,其他试图访问该对象(注意是该对象)的线程将被阻塞,不过仍可访问该对象中的非同步代码块。
  • 修饰一个方法,被修饰的方法称为同步方法,其作用的范围是整个方法,作用的对象是调用这个方法的对象。
    • 在父类中的某个方法使用了synchronized修饰,而在子类中覆盖了这个方法,在子类中的这个方法默认情况下并不是同步的,子类必须显式地在这个方法上加synchronized。
    • 在父类中的某个方法使用了synchronized修饰,子类使用super.xxx()调用了父类的该方法,子类的方法也就相当于同步的。
    • 定义接口方法时不能使用synchronized修饰。
    • 构造方法不能使用synchronized修饰。
  • 修改一个静态的方法,其作用的范围是整个静态方法,作用的对象是这个类的所有对象。
  • 修改一个类,其作用的范围是synchronized后面括号括起来的部分,作用主的对象是这个类的所有对象。

2.3.优化锁

JDK1.6以后,加入了大量优化。锁的四种状态级别:无锁<偏向锁<轻量级锁<重量级锁,根据竞争状态的激烈程度,锁只能自动升级,不能降级。

2.3.1.偏向锁

大多数情况下,锁不仅不存在多线程竞争,而且总是由同一个线程多次获得。为了让线程获取锁的开销降低而引入偏向锁。偏向锁针对的是锁仅会被同一线程持有的情况。

2.3.2.轻量级锁

多个线程在不同的时间段请求同一把锁,也就是不存在锁竞争的情况。在无锁竞争的情况下,只需要依靠一条 CAS 原子指令就可以完成锁的获取及释放。当存在锁竞争的情况下,执行 CAS 指令失败的线程将调用重量级锁进入到阻塞状态,当锁被释放的时候被唤醒。针对的是多个线程在不同时间段申请同一把锁的情况。

2.3.3.自旋锁

让一个线程在请求一个共享数据的锁时执行忙循环(自旋)一段时间,如果在这段时间内能获得锁,就可以避免进入阻塞状态 。

2.3.4.适应性自旋锁

自旋的次数不再是固定的,它是由前一次在同一个锁上的自旋时间及锁的拥有者的状态来决定。

2.3.5.锁消除

JVM 检测到不可能存在共享数据竞争,会对这些同步锁进行锁消除。锁消除的依据是JIT逃逸分析的数据支持。

2.3.6.锁粗化

减少不必要的紧连在一起的 lock、unlock 操作,将多个连续的锁扩展成一个范围更大的锁。

3.volatile

volatile是Java的关键字,可以满足JMM中的可见性,禁止指令重排序而保障“顺序性”。
Memory Barrier(内存屏障,内存栅栏)是一种CPU指令,用于控制特定条件下的重排序和内存可见性问题。
通过对JMM的认识,由于编译器和处理器都能执行指令重排,如果在指令之间插入一条Memory Barrier指令则会告诉编译器和CPU,不管什么指令都不能和这条Memory Barrier指令重排序,也就是说,通过插入Memory Barrier指令,禁止对Memory Barrier指令前后的指令执行重排。这就是volatile的原理。

4.final

我们知道,final修饰的变量不可被修改;修饰的类不可被继承;修饰的方法不可被覆写。这说明被final修饰的共享资源是不可被改变的,所以能保证线程安全。

5.Lock

Lock接口功能与synchronized关键字类似。在JDK 1.5 之后,java.util.concurrent中新增了Lock接口以及相关实现类用来实现锁功能,使用时需显式地获取和释放锁,因而比synchronized灵活。

5.1.ReentrantLock

ReentrantLock可重入锁,有公平锁和非公平锁。默认为非公平锁。
synchronized可以配合wait()和notify()实现线程在条件不满足时等待,条件满足时唤醒;ReentrantLock使用Condition对象实现相同的功能。
为了避免拿到锁的线程在运行期间出现异常,导致程序终止没有释放锁,配合try-finally来保证无论是否出现异常,最终必须释放锁。
ReentrantLock 为重入锁,如果必须递归,必须正确控制退出条件。

5.2.ReentrantReadWriteLock

ReentrantReadWriteLock 可重入读写锁,支持一写多读的同步锁。在读操作远远高于写操作的环境下,可在保证线程安全的情况下,提高运行效率。

5.3.StampedLock

StampedLock读写锁,JDK 8新引入的锁,比 ReentrantReadWriteLock 更快的一种锁,支持乐观读、悲观读锁和写锁。

6.ThreadLocal

ThreadLocal作用是用于线程间数据隔离,不将数据共享到主内存,从而避免线程安全问题(是避免而不是解决)。ThreadLocal是线程内部数据存储类,通过ThreadLocal将数据存储在指定线程中,存储后只能通过指定线程获取数据,其它线程是获取不到数据的。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值