欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

Java 线程安全相关

程序员文章站 2022-05-22 12:41:02
...

线程不安全的原因

正是有了多线程,才会出现线程不安全。线程不安全的原因就是多个线程某段代码不具备原子性、可见性或有序性导致的。

  1. 原子性是指一个操作是不可被其它线程打断的,加锁就是保证了原子性。
  2. 可见性是一个线程修改了某个共享值,其它线程能立即可知,不会出现一个线程修改了某个值,另一个线程读到旧值的情况。使用 volatile 就可以保证可见性。
  3. 有序性是由于编译器会进行指令重排序,当不同线程间可进行指令重排的存在先后依赖时,就会破坏了其有序性,使用 volatile 可保证有序性。

线程安全的实现方法

阻塞同步(互斥同步)

顾名思义,就是通过阻塞线程来达到同步的目的,保证同一时间只有一个线程对其访问。实现形式就是加锁,通过 synchronized 关键字或 ReenTrantLock 对象,比较两者的特点:

synchronized:
synchronized 关键字进行编译后,会在代码块前后加上 monitorenter 和 monitorexit 指令,线程的阻塞和唤醒表现在原生系统上,涉及到线程状态的转换,这会牺牲掉一些性能。
可重入;不会产生死锁,如遇异常则会释放锁;

ReenTrantLock:
与 synchronized 一样都具有可重入性,不一样的是它是在 api 层面的互斥,且在使用时需要加 try-finally 块以防止发生异常时产生死锁,因为 ReenTrantLock 在遇到异常是不会释放锁的。另外还增加了三个新特性:等待中断,公平锁,一个锁可绑定多个条件

JDK 1.6 之后, synchronized 在性能方面已经和 ReenTrantLock 没有什么区别了,所以在选择锁的时候,除非要用到 ReenTrantLock 的三个新特性,否则就优先选择 synchronized.
synchronized 锁优化的点:自旋锁(自适应自旋),消除锁,粗化锁,轻量级锁,偏向锁

非阻塞同步

非阻塞同步,就是不用通过阻塞线程来实现同步的目的。阻塞同步,就是悲观锁的概念,先进行加载,其它线程获取不到锁就阻塞等待。这样就有可能会出现一个问题,假如一个线程获取到锁之后执行时间很短,其它竞争该锁的线程刚被挂起就有可用资源了,就需要再抢占,这样频繁的挂起恢复存在很大的开销。
而非阻塞同步采用的是乐观锁,先操作,操作若是成功的,则执行完毕,操作若是不成功的则放弃本次操作,再重新开始操作。
如 CAS(Compare And Swap), 通过 sun.misc.Unsafe 中的 compareAndSwapInt, compareAndSwapLong 等方法来实现,这几个方法是原子操作。
但 CAS 中会有一个经典的 ABA 问题,如:先从线程 1 取 value(值为 A) 赋值给 current, 然后从线程 2 取 value(值为 B) 赋值给 current, 再在线程 2 中把 value 修改为 A, 进行 CAS 操作时看上去是成功的,但实际上中间有发生过变化,具体案例可看这里面一个链表的例子, https://www.cnblogs.com/549294286/p/3766717.html, 为解决这类问题一般会给对象加上 version 之类的标记。

非同步方案

在 Java 中就是使用 ThreadLocal, 它实际是每个线程都有一个对象,不涉及到多个线程操作同一对象,通过这样的方法来保证线程安全。

Java 内存模型与线程

分析一下 Java 的内存模型,从这里面可以看到导致线程不安全的根本原因。仅用于自己的学习和理解。
在主内存中有一份数据,这块内存所有的线程都可以访问,每个线程访问的时候,会在自己的工作内存保存自己的一份数据,这块内存是仅该线程可访问的。对一个数据的操作过程:从主内存读数据到工作内存,然后对其进行操作,操作完成后再写回主内存。
Java 内存模型中定义了以下 8 中操作:

lock(加锁), 作用于主内存,在主内存中给数据加标记线程独占
unlock(解锁),作用于主内存,在主内存中给数据去掉线程独占的标记
read(读取),作用于主内存,从主内存中读取数据到工作内存
load(载入),作用于工作内存,将 read 的数据保存到工作内存中
use(使用),作用于工作内存,在工作内存中使用数据
assign(赋值),作用于工作内存,在工作内存中给数据赋值
store(存储),作用于工作内存,从工作内存保存到主内存
write(写入),作用主内存,把工作内存传过来的值写入到主内存

其中 read 和 load 需要一起使用,store 和 write 需要一起使用。
线程不安全的场景分析如下图:
Java 线程安全相关

死锁问题

死锁是由于循环等待引起的,A 线程请求的资源 obj1 被 B 线程加锁没有释放,同时 B 线程请求的资源 obj2 被 A 线程加锁没有释放,因此造成了死锁,程序无法运行。死锁的代码如下:

private static String obj1 = "abc";
private static String obj2 = "def";

public static void main(String[] args) {
	new Thread() {
		@Override
		public void run() {
			synchronized (obj1) {
				System.out.println("线程1获取到 obj1 锁");
				System.out.println("线程1开始sleep");
				try {
					Thread.sleep(1000);
				} catch (InterruptedException e) {
					e.printStackTrace();
				}
				System.out.println("线程1结束sleep");
				synchronized (obj2) {
					System.out.println("线程1获取到 obj2 锁");
				}
			}
		}
	}.start();
	new Thread() {
		@Override
		public void run() {
			synchronized (obj2) {
				System.out.println("线程2获取到 obj2 锁");
				System.out.println("线程2开始sleep");
				try {
					Thread.sleep(1000);
				} catch (InterruptedException e) {
					e.printStackTrace();
				}
				System.out.println("线程2结束sleep");
				synchronized (obj1) {
					System.out.println("线程2获取到 obj1 锁");	
				}
			}
		}
	}.start();
	
}

运行结果如下:
线程1获取到 obj1 锁
线程1开始sleep
线程2获取到 obj2 锁
线程2开始sleep
线程1结束sleep
线程2结束sleep

可以看到 “线程1获取到 obj2 锁” 和 “线程2获取到 obj1 锁” 这两句话没有输出,这是因为两者请求的资源分别被对方占有着。