关于线程安全的思考

线程安全是什么?

​   维基百科:线程安全是程序设计中的术语,指某个函数函数库多线程环境中被调用时,能够正确地处理多个线程之间的公用变量,使程序功能正确完成。

​   《Java并发编程实战(Java Concurrency In Practice)》的作者Brian Goetz:当多个线程同时访问一个对象时,如果不用考虑这些线程在运行时环境下的调度和交替执行,也不需要进行额外的同步,或者在调用方进行任何其他的协调操作,调用这个对象的行为都可以获得正确的结果,那就称这个对象是线程安全的。”

实现线程安全的几种方式

  同步的定义:同步是指在多线程环境中,控制多个线程有序地访问共享数据,即保证同一时刻只有一个线程访问。

阻塞同步(悲观)

​   互斥是方法是手段,同步是目的。

​   使用synchronized关键字或者JUC并发包下的接口Lock的实现类ReentrantLock。

​   synchronized和ReentrantLock对比:

  • Java语法层面的同步,足够清晰,也足够简单;

  • 对开发者要求高,必须要在finally块中释放锁;

  • JVM可以在线程和对象的元数据中记录synchronized中锁的相关信息来进行优化,而Lock不行。

    如果synchronized能满足使用需求那就使用synchronized。​

    ReentrantLock的高级功能:

    • 线程阻塞等待时可以中断;

    • 可以实现公平锁(在锁被释放时,等待锁的所有线程按照时间顺序来依次获得锁,性能差,吞吐量下降);

    • 锁可以绑定多个条件。​

    ​这是一种悲观的策略。不管共享数据有没有出现竞争,都会默认加上锁,主要问题是进行线程阻塞和唤醒所带来的性能开销
    (不过JDK6下的synchronized除外,JVM会通过线程和对象的元数据信息优化掉很大一部分不必要的加锁。
    synchronized的升级过程:偏向锁 --自旋+CAS的方式两个线程来抢锁--> 轻量级锁 --自旋等待的过程中,锁仍未释放,就会升级为重量级锁--> 重量级锁:OS级别的上下文用户态到内核态的转换)。

     

非阻塞同步(乐观无锁)

​   硬件层面提供的基于冲突检测的乐观并发策略,通俗地说就是不管共享数据有没竞争,直接操作;如果共享的数据的确出现竞争了,产生了冲突,那再进行其他的补偿措施,最常用的补偿措施是不断地重试(自旋锁,固定重试次数),直到出现没有竞争的共享数据为止。

​   JDK下的Unsafe类中的native方法compareAndSwapInt,其中JUC下的AutomicInteger的incrementAndGet方法就使用到了。

 

无需同步

可重入代码

  定义:一个方法的返回结果对于相同的输入始终相同。

​   可重入代码的确具备上述特征,例如不依赖全局变量、存储在堆上的数据和公用的系统资源,用到的状态量都由参数中传入,不调用非可重入的方法等。因此,如果一个方法的返回结果是可以预测的,只要输入了相同的数据就都能返回相同的结果,那么它就满足可重入性的要求,即使这个方法被多个线程同时调用也不会出现线程安全问题。

​   可重入代码的另一个重要特征是,当一个线程在执行可重入代码时,它可以被另一个线程中断,并在中断后恢复执行,而不会影响程序的正确性。这是因为可重入代码只依赖于传入的参数和局部变量,它不会修改全局变量和其他线程共享的资源,因此不会对其他线程的执行产生影响。

  ​ 总之,可重入代码具有预测性和可中断性两个特征,这些特征保证了可重入代码的线程安全性,使得它可以被多个线程同时调用而不会出现数据竞争、死锁等线程安全问题。

线程本地存储

​   把共享数据的代码保证在同一个线程中执行。

​   Java中的实现就是ThreadLocal。一般使用在存储用户数据上下文。

 

参考:《深入理解Java虚拟机》

posted @ 2023-05-02 21:26  road2master  阅读(10)  评论(0编辑  收藏  举报