【java】乐观锁与悲观锁

一般来说,对于并发的场景,我们通常使用锁来保证线程安全:

  • 悲观锁是一种悲观的策略。它总是假设每一次的临界区操作会产生冲突,因此,必须对每次操作都小心翼翼。如果有多个线程同时需要访问临界区资源,就宁可牺牲性能让线程进行等待,所以说锁会阻塞线程执行;
  • 乐观锁是一种乐观的策略,它会假设对资源的访问是没有冲突的。既然没有冲突,自然不需要等待,所以所有的线程都可以在不停顿的状态下持续执行。乐观锁的策略使用一种叫做比较交换的技术(CAS Compare And Swap)来鉴别线程冲突,一旦检测到冲突产生,就重试当前操作直到没有冲突为止。

很多时候,对共享资源的访问主要是对某一数据结构的读写操作,如果数据结构本身就带有排他性访问的特性,也就相当于该数据结构自带一个细粒度的锁,对该数据结构的并发访问就能更加简单高效。

使用乐观锁的好处:

  1. 在高并发的情况下,它比有锁的程序拥有更好的性能;
  2. 它天生就是死锁免疫的,即不会出现死锁。

就凭借这两个优势,就值得我们冒险尝试使用乐观锁的并发。

那么,java中有哪些乐观锁技术呢?

1.原子操作

原子操作:顾名思义就是不可分割的操作,该操作只存在未开始和已完成两种状态,不存在中间状态,在JDK 1.8 中,java.util.concurrent.atomic 包下类都是原子类,原子类都是基于 sun.misc.Unsafe 实现的,用于实现原子操作。

上文提到的比较交换技术是通过名为 CAS 的CPU指令实现原子操作,由 CPU 硬件级别上保证原子性:

 CAS(变量, 比较值, 新值)

当变量的当前值与比较值相等时,才把变量更新为新值。

java.util.concurrent.atomic 包中的原子分为:原子性基本数据类型、原子性对象引用类型、原子性数组、原子性对象属性更新器和原子性累加器。具体实现类如下:

  • 原子性基本数据类型:AtomicBooleanAtomicIntegerAtomicLong
  • 原子性对象引用类型:AtomicReferenceAtomicStampedReferenceAtomicMarkableReference
  • 原子性数组:AtomicIntegerArrayAtomicLongArrayAtomicReferenceArray
  • 原子性对象属性更新:AtomicIntegerFieldUpdaterAtomicLongFieldUpdaterAtomicReferenceFieldUpdater
  • 原子性累加器:DoubleAccumulatorDoubleAdderLongAccumulatorLongAdder

当多个线程需要操作同一个变量时,我们可以放心地假设是线程安全的。

2. 线程本地存储

java.lang.ThreadLocal 类用于线程本地化存储。

扫描二维码关注公众号,回复: 10757951 查看本文章

线程本地化存储,就是为每一个线程创建一个变量,只有本线程可以在该变量中查看和修改值。需要明确的是,这种情况根本不存在资源共享,各个线程各用操作各自的变量,因此,也就不存在死锁。

典型的使用例子就是,spring 在处理数据库事务问题的时候,就用了 ThreadLocal 为每个线程存储了各自的数据库连接 Connection。

使用 ThreadLocal 要注意,在不使用该变量的时候,一定要调用 remove() 方法移除变量,否则可能造成内存泄漏的问题。

/**
 * 描述 Java中的ThreadLocal类允许我们创建只能被同一个线程读写的变量。
 * 因此,如果一段代码含有一个ThreadLocal变量的引用,即使两个线程同时执行这段代码,
 * 它们也无法访问到对方的ThreadLocal变量。
 */
public class ThreadLocalExsample {
 /**
 * 创建了一个MyRunnable实例,并将该实例作为参数传递给两个线程。两个线程分别执行
 * run()方法,并且都在ThreadLocal实例上保存了不同的值。
 * 如果它们访问的不是ThreadLocal对象,则第二个线程会覆盖掉第一个线程设置的值。
 */
public static class MyRunnable implements Runnable {
    private ThreadLocal threadLocal = new ThreadLocal();
    @Override
    public void run() {
        //一旦创建了一个ThreadLocal变量,你可以通过如下代码设置某个需要保存的值
        threadLocal.set((int) (Math.random() * 100D));
        try {
        	Thread.sleep(2000);
        } catch (InterruptedException e) {
        }
        //可以通过下面方法读取保存在ThreadLocal变量中的值
        System.out.println("-------threadLocal value-------"+threadLocal.get());
    }
}

	public static void main(String[] args) {
        MyRunnable sharedRunnableInstance = new MyRunnable();
        Thread thread1 = new Thread(sharedRunnableInstance);
        Thread thread2 = new Thread(sharedRunnableInstance);
        thread1.start();
        thread2.start();
    }
}

运行结果:

-------threadLocal value-------38
-------threadLocal value-------88

3. copy-on-write

copy-on-write,即写时复制,是指:在并发访问的情景下,当需要修改JAVA中容器的元素时,不直接修改该容器,而是先复制一份副本,在副本上进行修改。修改完成之后,将指向原来容器的引用指向新的容器(副本容器)。

集合类上的常用操作是:向集合中添加元素、删除元素、遍历集合中的元素然后进行某种操作。当多个线程并发地对一个集合对象执行这些操作时就会引发ConcurrentModificationException,比如:

线程A在for-each中遍历ArrayList,而线程B同时又在删除ArrayList中的元素,就可能会抛出ConcurrentModificationException,可以在线程A遍历ArrayList时加锁避免这一异常;

但由于遍历操作是一种常见的操作,加锁之后会影响程序的性能,因此for-each遍历选择了不对ArrayList加锁,而是当有多个线程修改ArrayList时抛出ConcurrentModificationException,因此,这是一种设计上的权衡。

为了应对这种多线程并发修改集合的这种情况,通常有下面的两种策略:

  • “写时复制” 机制;
  • 线程安全的容器类,例如利用CopyOnWriteArrayList代替ArrayList,利用ConcurrentHashMap代替HashMap。它们并不是从“复制”这个角度来应对多线程并发修改,而是引入了分段锁和CAS锁解决多线程并发修改的问题。

这里,主要讨论"写时复制"机制。由于不会其修改原始容器,只修改副本容器。因此,可以对原始容器进行并发地读。其次,实现了读操作与写操作的分离,读操作发生在原始容器上,写操作发生在副本容器上;数据一致性问题:读操作的线程可能不会立即读取到新修改的数据,因为修改操作发生在副本上。但最终修改操作会完成并更新容器,因此这是最终一致性。

CopyOnWrite容器适用于读多写少的场景。因为写操作时,需要复制一个容器,造成内存开销很大,也需要根据实际应用把握初始容器的大小。而不适合于数据的强一致性场合,即当数据修改之后要求能够立即被读到,则不能用写时复制技术。因为它是最终一致性

Java 中的 copy-on-write 容器包括:CopyOnWriteArrayListCopyOnWriteArraySet
这里通过如下实例说明一下CopyOnWriteArrayList的用法:

import java.util.ArrayList;
import java.util.List;
import java.util.Random;
import java.util.concurrent.CopyOnWriteArrayList;

public class TestCopyOnWrite {
 
	private static final Random R = new Random();
	
	private static CopyOnWriteArrayList<Integer> cowList = new CopyOnWriteArrayList<Integer>();
	//private static ArrayList<Integer> cowList = new ArrayList<Integer>();
	
	public static void main(String[] args) throws InterruptedException {
		List<Thread> threadList = new ArrayList<Thread>();
		//启动 10 个线程,向 cowList 添加 5 个随机整数
		for (int i = 0; i < 10; i++) {
			Thread t = new Thread(() -> {
				for (int j = 0; j < 5; j++) {
					//休眠 10 毫秒,让线程同时向 cowList 添加整数,引出并发问题
					try {
						Thread.sleep(100);
					} catch (InterruptedException e) {
						e.printStackTrace();
					}
					int tmp = R.nextInt(100);
					cowList.add(tmp);
					System.out.println(Thread.currentThread().getName()+" :  "+tmp);
				}
			}) ;
			t.start();
			threadList.add(t);
		}
		
		//主线程等待所有的线程运行结束
		for (Thread t : threadList) {
			t.join();
		}
		System.out.println(cowList.size());
	}
}

使用CopyOnWriteArrayList可以保证列表中数据个数始终未50个,结果如下:

Thread-2 :  30
Thread-1 :  90
Thread-9 :  88
Thread-0 :  67
Thread-8 :  29
Thread-7 :  30
Thread-6 :  28
Thread-5 :  18
Thread-4 :  23
Thread-3 :  30
Thread-2 :  10
Thread-0 :  1
Thread-8 :  79
Thread-7 :  21
Thread-9 :  22
Thread-1 :  10
Thread-4 :  78
Thread-5 :  81
Thread-3 :  44
Thread-6 :  47
Thread-2 :  24
Thread-7 :  90
Thread-0 :  83
Thread-8 :  93
Thread-1 :  10
Thread-5 :  43
Thread-3 :  67
Thread-6 :  23
Thread-9 :  40
Thread-4 :  36
Thread-2 :  29
Thread-0 :  3
Thread-7 :  6
Thread-1 :  43
Thread-8 :  43
Thread-3 :  90
Thread-9 :  26
Thread-5 :  65
Thread-6 :  58
Thread-4 :  76
Thread-2 :  85
Thread-8 :  95
Thread-1 :  10
Thread-7 :  65
Thread-0 :  95
Thread-3 :  40
Thread-6 :  89
Thread-4 :  53
Thread-9 :  86
Thread-5 :  95
50

而如果使用ArrayList,则偶尔会出现列表中数据个数少于50个的情况:

Thread-2 :  30
Thread-8 :  38
Thread-7 :  42
Thread-6 :  96
Thread-3 :  59
Thread-5 :  46
Thread-1 :  14
Thread-4 :  18
Thread-0 :  54
Thread-9 :  17
Thread-8 :  26
Thread-6 :  37
Thread-1 :  87
Thread-5 :  26
Thread-9 :  7
Thread-3 :  66
Thread-7 :  9
Thread-2 :  56
Thread-0 :  81
Thread-4 :  30
Thread-8 :  19
Thread-5 :  59
Thread-6 :  10
Thread-7 :  23
Thread-3 :  17
Thread-4 :  4
Thread-0 :  19
Thread-2 :  45
Thread-9 :  81
Thread-1 :  8
Thread-8 :  22
Thread-6 :  9
Thread-3 :  51
Thread-0 :  23
Thread-5 :  45
Thread-4 :  8
Thread-7 :  24
Thread-1 :  44
Thread-2 :  35
Thread-9 :  21
Thread-8 :  67
Thread-3 :  53
Thread-5 :  46
Thread-4 :  96
Thread-1 :  68
Thread-0 :  82
Thread-6 :  12
Thread-7 :  63
Thread-9 :  80
Thread-2 :  11
49

本文内容参考资料:Java 中有哪些无锁技术来解决并发问题?如何使用?

发布了192 篇原创文章 · 获赞 318 · 访问量 28万+

猜你喜欢

转载自blog.csdn.net/zyxhangiian123456789/article/details/102229117
今日推荐