线程安全的实现方法

1. 互斥同步

synchronized 和 ReentrantLock。

2. 非阻塞同步

互斥同步最主要的问题就是线程阻塞和唤醒所带来的性能问题,因此这种同步也称为阻塞同步。

互斥同步属于一种悲观的并发策略,总是认为只要不去做正确的同步措施,那就肯定会出现问题。

无论共享数据是否真的会出现竞争,它都要进行加锁(这里讨论的是概念模型,实际上虚拟机会优化掉很大一部分不必要的加锁) 、用户态核心态转换、维护锁计数器和检查是否有被阻塞的线程需要唤醒等操作。

2.1 CAS

随着硬件指令集的发展,我们可以使用基于冲突检测的乐观并发策略:先进行操作,如果没有其它线程争用共享数据,那操作就成功了,否则采取补偿措施(不断地重试,直到成功为止) 。

这种乐观的并发策略的许多实现都不需要将线程阻塞,因此这种同步操作称为非阻塞同步。

乐观锁需要操作和冲突检测这两个步骤具备原子性,这里就不能再使用互斥同步来保证了,只能靠硬件来完成。硬件支持的原子性操作最典型的是:比较并交换(Compare-and-Swap,CAS)。

CAS 指令需要有 3 个操作数,分别是内存地址V、旧的预期值 A 和新值 B。当执行操作时,只有当 V 的值等于 A,才将 V 的值更新为 B。

2.2 AtomicInteger

J.U.C 包里面的整数原子类 AtomicInteger,其中的 compareAndSet() 和getAndIncrement() 等方法都使用了 Unsafe 类的 CAS 操作。

以下代码使用了 AtomicInteger 执行了自增的操作。

private AtomicInteger cnt = new AtomicInteger();
    public void add() {
    cnt.incrementAndGet();
}

以下代码是 incrementAndGet() 的源码,它调用了 unsafe 的 getAndAddInt() 。

public final int incrementAndGet() {
    return unsafe.getAndAddInt(this, valueOffset, 1) + 1;
}

以下代码是 getAndAddInt() 源码,var1 指示对象内存地址,var2 指示该字段相对对象内存地址的偏移,var4 指示操作需要加的数值,这里为 1。

public final int getAndAddInt(Object var1, long var2, int var4){
    int var5;
    do {
        var5 = this.getIntVolatile(var1, var2);
    } while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));
    return var5;
}

通过getIntVolatile(var1, var2) 得到旧的预期值,通过调用 compareAndSwapInt() 来进行 CAS 比较,如果该字段内存地址中的值等于 var5,那么就更新内存地址为var1+var2 的变量为 var5+var4。

可以看到 getAndAddInt() 在一个循环中进行,发生冲突的做法是不断的进行重试。

3. 无同步方案

要保证线程安全,并不是一定就要进行同步。

如果一个方法本来就不涉及共享数据,那它自然就无须任何同步措施去保证正确性。

3.1 栈封闭

多个线程访问同一个方法的局部变量时,不会出现线程安全问题,因为局部变量存储在虚拟机栈中,属于线程私有的。

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class StackClosedExample {
	public void add100() {
		int cnt = 0;
		for (int i = 0; i < 100; i++) {
			cnt++;
		}
		System.out.println(cnt);
	}
}

public static void main(String[] args) {
	StackClosedExample example = new StackClosedExample();
	ExecutorService executorService = Executors.newCachedThreadPool();
	executorService.execute(() -> example.add100());
	executorService.execute(() -> example.add100());
	executorService.shutdown();
}

运行结果:

3.2 线程本地存储(Thread Local Storage)

如果一段代码中所需要的数据必须与其他代码共享,那就看看这些共享数据的代码是否能保证在同一个线程中执行

如果能保证,我们就可以把共享数据的可见范围限制在同一个线程之内,这样,无须同步也能保证线程之间不出现数据争用的问题。

符合这种特点的应用并不少见:

  • 大部分使用消费队列的架构模式(如“生产者-消费者”模式) 都会将产品的消费过程尽量在一个线程中消费完。
  • 其中最重要的一个应用实例就是经典 Web 交互模型中的“一个请求对应一个服务器线程”(Thread-perRequest) 的处理方式,这种理方式的广泛应用使得很多 Web 服务端应用都可以使用线程本地存储来解决线程安全问题。

可以使用 java.lang.ThreadLocal 类来实现线程本地存储功能。

对于以下代码,thread1 中设置 threadLocal 为 1,而 thread2 设置 threadLocal 为2。过了一段时间之后,thread1 读取 threadLocal 依然是 1,不受 thread2 的影响。

public class ThreadLocalExample {
	public static void main(String[] args) {
		ThreadLocal threadLocal = new ThreadLocal();
		Thread thread1 = new Thread(() -> {
			threadLocal.set(1);
			try {
				Thread.sleep(1000);
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
			System.out.println(threadLocal.get());
			threadLocal.remove();
		});
		Thread thread2 = new Thread(() -> {
			threadLocal.set(2);
			threadLocal.remove();
		});
		thread1.start();
		thread2.start();
	}
}

运行结果:

为了理解 ThreadLocal,先看以下代码:

public class ThreadLocalExample1 {
	public static void main(String[] args) {
		ThreadLocal threadLocal1 = new ThreadLocal();
		ThreadLocal threadLocal2 = new ThreadLocal();
		Thread thread1 = new Thread(() -> {
			threadLocal1.set(1);
			threadLocal2.set(1);
		});
		Thread thread2 = new Thread(() -> {
			threadLocal1.set(2);
			threadLocal2.set(2);
		});
		thread1.start();
		thread2.start();
	}
}

它所对应的底层结构图为:

每个 Thread 都有一个 ThreadLocal.ThreadLocalMap 对象,Thread 类中就定义了ThreadLocal.ThreadLocalMap 成员。

/* ThreadLocal values pertaining to this thread. This map is maintained
* by the ThreadLocal class. */
ThreadLocal.ThreadLocalMap threadLocals = null;

当调用一个 ThreadLocal 的 set(T value) 方法时,先得到当前线程的ThreadLocalMap 对象,然后将 ThreadLocal->value 键值对插入到该 Map 中。

public void set(T value) {
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);
    if (map != null)
        map.set(this, value);
    else
        createMap(t, value);
}

get() 方法类似。

public T get() {
	Thread t = Thread.currentThread();
	ThreadLocalMap map = getMap(t);
	if (map != null) {
		ThreadLocalMap.Entry e = map.getEntry(this);
		if (e != null) {
			@SuppressWarnings("unchecked")
			T result = (T)e.value;return result;
		}
	}
	return setInitialValue();
}

ThreadLocal 从理论上讲并不是用来解决多线程并发问题的,因为根本不存在多线程竞争。

在一些场景 (尤其是使用线程池) 下,由于 ThreadLocal.ThreadLocalMap 的底层数据结构导致 ThreadLocal 有内存泄漏的情况,应该尽可能在每次使用 ThreadLocal后手动调用 remove(),以避免出现 ThreadLocal 经典的内存泄漏甚至是造成自身业务混乱的风险。

3.3 可重入代码(Reentrant Code)

这种代码也叫做纯代码(Pure Code) ,可以在代码执行的任何时刻中断它,转而去执行另外一段代码(包括递归调用它本身) ,而在控制权返回后,原来的程序不会出现任何错误。

可重入代码有一些共同的特征,例如不依赖存储在堆上的数据和公用的系统资源、用到的状态量都由参数中传入、不调用非可重入的方法等。

 

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

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值