浅谈Java线程安全

 在多线程编程中我们需要同时关注可见性、顺序性和原子性。

  • 可见性。对于共享数据,一个地方如果改变了该数据,其它地方要立马知道。
  • 原子性。类似于数据库事务的原子性,一次操作要全部执行,要么全部不执行。
  • 顺序性。程序在执行的时候,程序的代码执行顺序和语句的顺序是一致的。

一、原子性

  • 使用锁
  • 使用同步

锁:保证同一时间只有一个线程拿到,也保证了同一时间只有一个线程执行申请锁和锁释放的代码。

同步:与锁类似的是同步方法或者同步代码块。使用非静态同步方法时,锁住的是当前实例, 使用静态同步方法时,锁住的是该类的class对象。

volatile:对volatile单次的读写可保证原子性。

二、可见性

计算机存储分为主内存和高级缓存。CPU写速度要远比从主内存读数据再写回主内存的速度快,直接对主内存操作的话,会引发各种问题。因此设计了交互更快的高级缓存。CPU与高级缓存进行交互,然后高级缓存再与主内存进行交互。由于CPU是多核的,一个核上A的线程共享变量进行写操作的时候,另一个核B的变量可能正在读,但是读取的是与之对应的缓存中的数据,因此读到的不是最新A写入的变量,这就是可见性问题的大概描述。解决办法是使用volatile变量。

  • 修改volatile变量时会强制将修改后的值刷新到内存中
  • 修改volatile变量后其它线程内存的变量立即失效,需从主内存读取

三、有序性

程序执行的顺序按照代码的先后顺序执行。如果在本线程内观察,所有操作都是有序的;如果在一个线程中观察另一个线程,所有操作都是无序的。上述内容为指令重排序。

1.内存屏障

保证有序性的方法为内存屏障。

:针对跨处理器的读写操作,它被插入到两个指令之间。作用是禁止对编译器和处理器进行重排序。

  • MonitorEnter
  • Load Barrier
  • Acquire Barrier
  • Critical Area
  • Release Barrier
  • MonitorExit
  • Store Barrier

把屏障分为两类,加载屏障和存储屏障

  • 加载屏障:刷新处理器缓存
  • 存储屏障:冲刷处理器缓存

保证有序性,还有个happens-before原则。

  • 如果一个操作happens-before另一个操作,那么第一个操作的执行结果将对第二个操作可见,而且第一个操作的执行顺序排在第二个操作之前。
  • 两个操作之间存在happens-before关系,并不意味着一定要按照happens-before原则制定的顺序来执行。如果重排序之后的执行结果与按照happens-before原则执行的结果一致,那么这种重排序合法。

四、题外话,Java中的各种锁

1、乐观锁/悲观锁

  • 悲观锁:对数据的并发操作一定采取加锁行为
  • 乐观锁:对数据的并发性操作不加锁。几种方法。一种是CAS(compare and swap)即比较替换,早期的做法是比较内容值是否和预期值一样,如果一样则写入新值,不一样就放弃。这存在ABA的问题,ABA问题就是当内容值和预期值一样的时候,可能内容值已经被修改过了,可能修改了两次又改回为了预期值。解决:CompareAndSet方法作用首先检查当前引用是否等于预期引用,并且当前标志是否等于预期标志,若都相等则更新最新引用和最新值。

2、分段锁

ConcurrentHashMap中分段锁称为Segment,它即类似于HashMap,当Put时并不是对整个HashMap进行加锁,而是先通过HashCode来知道他要放在哪一个分段中,然后对这个分段进行加锁,只要不是放在一个分段,则就可实现分段加锁。分段锁是ReentrantLock。

3、偏向锁/轻量级锁/重量级锁

  • 偏向锁,一段同步代码一直被一个线程访问,那么该线程会自动获取锁,降低获取锁的代价。
  • 轻量级锁是指当锁是偏向锁的时候,被另一个线程访问,偏向锁会被升级为轻量级锁。其他线程会通过自旋形式获锁,不会阻塞,提高性能。(锁的自旋,与普通锁不同,一个线程A在获得普通锁后,如果再有线程B试图获取锁,那么这个线程B将会挂起。如果两个线程资源竞争不是特别激烈,而处理器阻塞一个线程引起的线程上下文切换的代价高于等待资源的代价,那么可以不放弃CPU的时间片,而是原地等,直到锁的持有者释放该锁)
  • 重量级锁是指当锁为轻量级锁的时候,另一个线程虽然是自旋的,单自旋不会一直持续下去,当自旋一定次数,还没有获取锁,就会阻塞,该锁变为重量级锁,其他申请的线程进入阻塞。

4、公平锁/非公平锁

  • 公平锁:过个线程按照申请锁的顺序来获取锁,队列实现
  • 非公平锁:多个线程获取锁的顺序并不是按照申请锁的顺序

5、独享锁/共享锁

  • 独享锁:该锁一次只能被1个线程所持有:ReentrantLock,Synchronized
  • 共享锁:该锁可被多个线程所持有:ReentrantLock,Reed共享,Write独享

猜你喜欢

转载自www.cnblogs.com/ylxn/p/10354631.html