Java线程安全

createh53周前 (04-11)技术教程18

当多个线程处理相同的数据,数据值发生变化时,会得到不一致的结果,这种情况不是线程安全的。 当一个线程已经在一个对象上工作并阻止另一个线程在同一个对象上工作时,这个过程称为线程安全。

线程安全体现

  1. 原子性提供互斥访问,同一时刻只能有一个线程对数据进行操作(atomic,synchronized,lock);
  2. 可见性一个线程对主内存的修改可以及时地被其他线程看到(volatile,synchronized);
  3. 有序性: 一个线程观察其他线程中的指令执行顺序,由于指令重排序,该观察结果一般杂乱无序(happens-before原则)

原子性

  • Atomic

它们是通过CAS完成原子性。

CAS定义

CAS(compare and swap),比较并交换。可以解决多线程并行情况下使用锁造成性能损耗的一种机制.CAS 操作包含三个操作数—内存位置(V)、预期原值(A)和新值(B)。如果内存位置的值与预期原值相匹配,那么处理器会自动将该位置值更新为新值。否则,处理器不做任何操作。一个线程从主内存中得到num值,并对num进行操作,写入值的时候,线程会把第一次取到的num值和主内存中num值进行比较,如果相等,就会将改变后的num写入主内存,如果不相等,则一直循环对比,知道成功为止。

CAS使用的时机

  1. 线程数较少、等待时间短可以采用自旋锁进行CAS尝试拿锁,较于synchronized高效。
  2. 线程数较大、等待时间长,不建议使用自旋锁,占用CPU较高
  • Synchronized

synchronized是一种同步锁,通过锁实现原子操作。

JDK提供锁分两种:一种是synchronized,依赖JVM实现锁,因此在这个关键字作用对象的作用范围内是同一时刻只能有一个线程进行操作;另一种是LOCK,是JDK提供的代码层面的锁,依赖CPU指令,代表性的是ReentrantLock。

synchronized修饰的对象有四种:

  1. 修饰代码块,作用于调用的对象;
  2. 修饰方法,作用于调用的对象;
  3. 修饰静态方法,作用于所有对象;
  4. 修饰类,作用于所有对象。
  • Lock

Lock和syncronized的区别:

  1. synchronized是Java语言的关键字, Lock是一个类。
  2. synchronized不需要用户去手动释放锁,发生异常或者线程结束时自动释放锁;Lock则必须要用户去手动释放锁,如果没有主动释放锁,就有可能导致出现死锁现象。
  3. lock可以配置公平策略,实现线程按照先后顺序获取锁。
  4. 提供了trylock方法 可以试图获取锁,获取到或获取不到时,返回不同的返回值 让程序可以灵活处理。
  5. lock()和unlock()可以在不同的方法中执行,可以实现同一个线程在上一个方法中lock()在后续的其他方法中unlock(),比syncronized灵活的多。

公平锁与非公平锁

公平锁:就是很公平,在并发环境中,每个线程在获取锁时会先查看此锁维护的等待队列,如果为空,或者当前线程线程是等待队列的第一个,就占有锁,否则就会加入到等待队列中,以后会按照FIFO的规则从队列中取到自己。

非公平锁:比较粗鲁,上来就直接尝试占有锁,如果尝试失败,就再采用类似公平锁那种方式。

  • ReentrantLock--重入锁

ReentrantLock是Lock接口的实现类,是非公平锁。

可见性

volatile

这种方式可以保证每次取数直接从主存取,它只能保证内存的可见性,无法保证原子性。它不需要加锁,比 synchronized 更轻量级,不会阻塞线程,不会被编译器优化。然而要求对这个变量做原子操作,否则还是会有问题。虽然 volatile 是轻量级,但是它也需要保证 读写的顺序不乱序,所以可以有优化点,比如在单例实现方式中的双重校验中,使用 临时变量 降低 volatile 变量的访问。

synchronized

Synchronized 能够实现原子性和可见性;在 Java 内存模型中,synchronized规 定,线程在加锁时,先清空工作内存 → 在主内存中拷贝最新变量的副本到工作内存 → 执行完代码 → 将更改后的共享变量的值刷新到主内存中 → 释放互斥锁。

所以如果无法用 volatile 做可见性,则可以考虑用 synchronized 可以做可见性的保证。

有序性

在程序执行时,为了提高性能,编译器和处理器常常会对指令做重排序。重排序分3中:

  • 编译器优化的重排序
  • 指令级并行的重排序
  • 内存系统的重排序

java程序的有序性可以归结为一句话:在单线程内,所有的操作都是有序的;如果在一个线程中看另一个线程,所有的操作都是无序的。前半句是指as-if-serial,意思是:不管编译器和处理器怎么重排序,单线程的执行结果不会改变。后半句是指指令重排序现象和主内存和工作内存有延迟的现象。

Java中保证有序性方法:

  1. 使用volatile关键字修饰变量flag就可以解决重排序问题,禁止重排序的规则如下:
    第一:当第二个操作是volatile变量写时,不论第一个操作是什么,都不允许重排序
    第二:当的第一个操作是volatile读时,不论第二个操作是什么,都不能重排序
    第三:当第一个操作是volatile写,而二个操作是volatile读时,不能重排序。
  2. Synchronized以及锁操作,保证只有一个线程访问。

相关文章

轻松掌握Java多线程 - 第四章:线程安全问题

学习目标1. 什么是线程安全1.1 线程安全的定义1.2 线程安全的重要性2. 共享资源访问的竞态条件2.1 什么是竞态条件2.2 竞态条件示例2.3 竞态条件的类型3. 线程安全问题的表现形式3.1...

揭秘Java局部变量线程安全的真相:为什么它天生免疫并发问题

··在Java并发编程中,线程安全是一个永恒的话题。你是否曾疑惑:为什么局部变量不需要加锁就能避免并发问题?本文将深入剖析其底层原理,结合实战案例,带你彻底理解这一设计精髓。(点击收藏,解锁高薪面试必...

从双重检查锁到枚举单例:Java线程安全实践中的道与术

2022年双十一前夕,笔者所在团队的全局配置管理类突然出现诡异现象:在2000+QPS的配置刷新场景下,日志中频繁出现多个ConfigManager实例的哈希码。这直接导致部分服务器读取到过期配置,险...

Java中实现线程安全的单例模式

Java中实现线程安全的单例模式在Java编程中,单例模式是一种非常常见的设计模式。它确保一个类只有一个实例,并且提供一个全局访问点来获取这个实例。然而,当涉及到多线程环境时,确保单例模式的线程安全性...

Java多线程锁机制:让多线程编程更安全

Java多线程锁机制:让多线程编程更安全在Java的世界里,多线程就像是一场热闹非凡的派对,每个线程都想在这个派对上尽情展现自己的才华。然而,当多个线程同时访问共享资源时,如果没有妥善管理,就可能引发...