java基础之多线程总结二(CAS和各种常用锁)

Posted 程序员涂小哥

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了java基础之多线程总结二(CAS和各种常用锁)相关的知识,希望对你有一定的参考价值。

CAS

在java中,对很多常见的需要加锁的操作进行了封装,例如Atomic开头的一些类,这些类是线程安全的,但是内部却不是用synchronized加锁实现,而是CAS。
例如AtomicInteger的’incrementAndGet()'方法最终调用的实际是下边这个方法

@HotSpotIntrinsicCandidate
    public final native boolean compareAndSetInt(Object o, long offset,
                                                 int expected,
                                                 int x);

这个本地方法在一个比较特殊的类中,即Unsafe,这个类可以直接进行内存管理等操作,且源码可以看出来这是一个典型的单例模式。
CAS是compare and set的缩写,即比较并设值的意思,CAS之所以能保证线程安全,是因为这类操作是cpu原语支持的,执行过程中不能被打断。
CAS操作会有ABA问题,意思就是一个线程在操作的时候,另一个线程把数据修改了,然后又改了回去,单纯比较值似乎没有变化。
ABA问题对于基础数据类型的数据其实没有太大影响,如果不是基础类型,并且必须处理ABA问题,可以考虑增加版本号管理,在compare的时候连版本号一起比较。
就像AtomicInteger类,如果要处理ABA问题可以考虑使用AtomicStampedReference类。

Atomic这一类操作的CAS是无锁的,所以有的时候比synchronized的效率要高,而针对数据自增这种操作,jdk还自带了一些其他的类,例如LongAdder,LongAdder采用的也是CAS,但是是分段的,所以一些应用场景下可能性能更好,例如有这样一段测试代码:

/**
 * @Author tuzongxun
 * @Date 2022/2/23
 */
public class LongAddrDemo 
	private static Long count1 = 0L;

	private static AtomicLong count2 = new AtomicLong(0);

	private static LongAdder count3 = new LongAdder();

	public static void main(String[] args) 
		Thread[] threads = new Thread[1000];

		Object o = new Object();
		for (int i = 0; i < threads.length; i++) 
			threads[i] = new Thread(() -> 
				for (int j = 0; j < 100000; j++) 
					synchronized (o) 
						count1++;
					
				
			);
		

		Long t1 = System.currentTimeMillis();
		for (int i = 0; i < threads.length; i++) 
			threads[i].start();
		
		for (int i = 0; i < threads.length; i++) 
			try 
				threads[i].join();
			
			catch (InterruptedException e) 
				e.printStackTrace();
			
		
		Long t2 = System.currentTimeMillis();
		System.out.println("count1-synchronized:" + count1 + ":" + (t2 - t1));
		//############################################
		for (int i = 0; i < threads.length; i++) 
			threads[i] = new Thread(() -> 
				for (int j = 0; j < 100000; j++) 
					count2.incrementAndGet();
				
			);
		

		Long t11 = System.currentTimeMillis();
		for (int i = 0; i < threads.length; i++) 
			threads[i].start();
		
		for (int i = 0; i < threads.length; i++) 
			try 
				threads[i].join();
			
			catch (InterruptedException e) 
				e.printStackTrace();
			
		
		Long t12 = System.currentTimeMillis();
		System.out.println("count2-Atomic:"+count2 + ":" + (t12 - t11));

		//############################################
		for (int i = 0; i < threads.length; i++) 
			threads[i] = new Thread(() -> 
				for (int j = 0; j < 100000; j++) 
					count3.increment();
				
			);
		

		Long t21 = System.currentTimeMillis();
		for (int i = 0; i < threads.length; i++) 
			threads[i].start();
		
		for (int i = 0; i < threads.length; i++) 
			try 
				threads[i].join();
			
			catch (InterruptedException e) 
				e.printStackTrace();
			
		
		Long t22 = System.currentTimeMillis();
		System.out.println("count3-LongAdder:"+count3 + ":" + (t22 - t21));
	

上述代码运行后结果如下:

count1-synchronized:100000000:6060
count2-Atomic:100000000:2149
count3-LongAdder:100000000:499

很明显,针对当前的测试,LongAdder的性能高于AtomicLong,AtomicLong的性能要高于synchronized。
但是需要注意的是,上边的结论只是针对于当前场景,并不是说什么时候都是这样,具体应用的时候还需要进行实际分析和测试确定。

各种锁

jdk中有很多使用CAS实现的锁,例如ReentrantLock,ReenTrantLock相比synchronized更加灵活,不过从实现上来说可能稍微麻烦些,其中有一点就是需要手动解锁,例如如下代码:

/**
 * @Author tuzongxun
 * @Date 2022/2/23
 */
public class LockDemo 
	private static int count=0;

	public static void main(String[] args) 
		ReentrantLock lock=new ReentrantLock();
		Thread t1=new Thread(()->
			lock.lock();
			for (int i = 0; i < 1000000; i++) 
				count++;
			
			lock.unlock();
		);
		Thread t2=new Thread(()->
			lock.lock();
			for (int i = 0; i < 1000000; i++) 
				count++;
			
			lock.unlock();
		);
		try 
			t1.start();
			t2.start();
			t1.join();
			t2.join();
		
		catch (InterruptedException e) 
			e.printStackTrace();
		
		System.out.println(count);
	

上边的代码总能输出2000000,是线程安全的。需要注意的是,这里的unlock最后必须手动调用,上边的代码比较简单,所以没有过多处理,正常来说应该加入到finally代码块中以保证一定被调用。
之所以说ReentrantLock灵活,是因为它可以被打断,使用lockInterruptibly(),在创建lock对象的时候,还可以选择使用公平锁还是非公平锁,默认是非公平的,如果要公平,则可以在后边参数中传true,例如:

ReentrantLock lock=new ReentrantLock(true);

所谓的公平锁可以简单地理解为先来后到,而不是来了就直接·抢。

上边的代码,在main线程中使用了join方法等待两个线程结束,然后输出最终结果,实际上还可以用CountDownLatch替换这种写法,例如:

/**
 * @Author tuzongxun
 * @Date 2022/2/24
 */
public class CountDownLatchDemo 
	private static int count=0;

	public static void main(String[] args) 
		ReentrantLock lock=new ReentrantLock(true);
		CountDownLatch cd=new CountDownLatch(2);
		new Thread(()->
			lock.lock();
			for (int i = 0; i < 1000000; i++) 
				count++;
			
			lock.unlock();
			cd.countDown();
		).start();
		new Thread(()->
			lock.lock();
			for (int i = 0; i < 1000000; i++) 
				count++;
			
			lock.unlock();
			cd.countDown();
		).start();
		try 
			cd.await();
		
		catch (InterruptedException e) 
			e.printStackTrace();
		
		System.out.println(count);
	

需要注意的是,CountDownLatch关键在于创建对象的时候定义的数量以及调用countDown方法,所以实际上可以在一个线程里多次调用countDown把数量减到零,这是需要写程序的时候自己控制的。

上边用到的ReentrantLock和synchronized比较类似,都是排他锁,这种锁在读多写少需要读写分离的场景中就有些不够用,相对来说效率也不够高,例如如下代码:

/**
 * @Author tuzongxun
 * @Date 2022/2/24
 */
public class ReentrantLockDemo2 
	private static int count=0;
	private static ReentrantLock reentrantLock=new ReentrantLock();
	public static void readCount(Lock lock)
		try
			lock.lock();
			Thread.sleep(1000);
			System.out.println(new Date() +"----"+count);
		catch (Exception e)
			e.printStackTrace();
		finally 
			lock.unlock();
		
	

	private static void writeCount(Lock lock)
		try
			lock.lock();
			Thread.sleep(1000);
			count++;
			System.out.println(new Date() +"----"+count);
		catch (Exception e)
			e.printStackTrace();
		finally 
			lock.unlock();
		
	

	public static void main(String[] args) 
		for (int i = 0; i < 5; i++) 
			new Thread(()->
				readCount(reentrantLock);
			).start();
		
		for (int i = 0; i < 5; i++) 
			new Thread(()->
				writeCount(reentrantLock);
			).start();
		
	

上述代码运行结果如下:

Thu Feb 24 20:32:38 CST 2022----0
Thu Feb 24 20:32:39 CST 2022----0
Thu Feb 24 20:32:40 CST 2022----0
Thu Feb 24 20:32:41 CST 2022----0
Thu Feb 24 20:32:42 CST 2022----0
Thu Feb 24 20:32:43 CST 2022----1
Thu Feb 24 20:32:44 CST 2022----2
Thu Feb 24 20:32:45 CST 2022----3
Thu Feb 24 20:32:46 CST 2022----4
Thu Feb 24 20:32:47 CST 2022----5

可以看到这里不论是读还是写,都会独自占用一秒时间,总共花费10秒。
实际上,有一种读写分离的锁可以使的读锁共享,写锁排他,从而在适当的应用场景下提升效率,例如上边代码可以改成这样:

/**
 * @Author tuzongxun
 * @Date 2022/2/24
 */
public class ReentrantLockDemo2 
	private static int count=0;
	private static ReentrantReadWriteLock reentrantReadWriteLock=new ReentrantReadWriteLock();
	private static Lock readLock=reentrantReadWriteLock.readLock();
	private static Lock writeLock=reentrantReadWriteLock.writeLock();

	public static void readCount(Lock lock)
		try
			lock.lock();
			Thread.sleep(1000);
			System.out.println(new Date() +"----"+count);
		catch (Exception e)
			e.printStackTrace();
		finally 
			lock.unlock();
		
	

	private static void writeCount(Lock lock)
		try
			lock.lock();
			Thread.sleep(1000);
			count++;
			System.out.println(new Date() +"----"+count);
		catch (Exception e)
			e.printStackTrace();
		finally 
			lock.unlock();
		
	

	public static void main(String[] args) 
		for (int i = 0; i < 5; i++) 
			new Thread(()->
				readCount(readLock);
			).start();
		
		for (int i = 0; i < 5; i++) 
			new Thread(()->
				writeCount(writeLock);
			).start();
		
	

上述代码运行结果如下:

Thu Feb 24 20:36:18 CST 2022----0
Thu Feb 24 20:36:18 CST 2022----0
Thu Feb 24 20:36:18 CST 2022----0
Thu Feb 24 20:36:18 CST 2022----0
Thu Feb 24 20:36:18 CST 2022----0
Thu Feb 24 20:36:19 CST 2022----1
Thu Feb 24 20:36:20 CST 2022----2
Thu Feb 24 20:36:21 CST 2022----3
Thu Feb 24 20:36:22 CST 2022----4
Thu Feb 24 20:36:23 CST 2022----5

可以看到这里实际上只花费了5秒,写的操作每个占用了1秒,所有读的操作都在同一秒内完成了。这里的代码和上边的相比,只是用了不同的锁。

jdk中还有一个类,可以实现指定数量的线程都到齐之后再开始运行,这个类就是CyclicBarrier,示例代码如下:

/**
 * @Author tuzongxun
 * @Date 2022/2/24
 */
public class CyclicBarrierDemo 

	public static void main(String[] args) 
		CyclicBarrier cb=new CyclicBarrier(20,()->
			System.out.println("数量凑齐,开始运行-------------------:"+new Date());
		);

		for (int i = 0; i <41 ; i++) 
			System.out.println("创建线程:"+(i+1)+new Date());
			new Thread(()->
				try 
					cb.await();
					System.out.println("开始运行:"+new Date());
				
				catch (InterruptedException e) 
					e.printStackTrace();
				
				catch (BrokenBarrierException e) 
					e.printStackTrace();
				
			).start();
			try 
				Thread.sleep(1000);
			
			catch (InterruptedException e) 
				e.printStackTrace();
			
		
	

上述代码输出结果如下:

创建线程:1Thu Feb 24 19:52:27 CST 2022
创建线程:2Thu Feb 24 19:52:28 CST 2022
创建线程:3Thu Feb 24 19:52:29 CST 2022
创建线程:4Thu Feb 24 19:52:30 CST 2022
创建线程:5Thu Feb 24 19:52:31 CST 2022
创建线程:6Thu Feb 24 19:52:32 CST 2022
创建线程:7Thu Feb 24 19:52:33 CST 2022
创建线程:8Thu Feb 24 19:52:34 CST 2022
创建线程:9Thu Feb 24 19:52:35 CST 2022
创建线程:10Thu Feb 24 19:52:36 CST 2022
创建线程:11Thu Feb 24 19:52:37 CST 2022
创建线程:12Thu Feb 24 19:52:38 CST 2022
创建线程:13Thu Feb 24 19:52:39 CST 2022
创建线程:14Thu Feb 24 19:52:40 CST 2022
创建线程:15Thu Feb 24 19:52:41 CST 2022
创建线程:16Thu Feb 24 19:52:42 CST 2022
创建线程:17Thu Feb 24 19:52:43 CST 2022
创建线程:18Thu Feb 24 19:52:44 CST 2022
创建线程:19Thu Feb 24 19:52:45 CST 2022
创建线程:20Thu Feb 24 19:52:46 CST 2022
数量凑齐,开始运行-------------------:Thu Feb 24 19:52:46 CST 2022
开始运行:Thu Feb 24 19:52:46 CST 2022
开始运行:Thu Feb 24 19:52:46 CST 2022
开始运行:Thu Feb 24 19:52:46 CST 2022
开始运行:Thu Feb 24 19:52:46 CST 2022
开始运行:Thu Feb 24 19:52:46 CST 2022
开始运行:Thu Feb 24 19:52:46 CST 2022
开始运行:Thu Feb 24 19:52:46 CST 2022
开始运行:Thu Feb 24 19:52:46 CST 2022
开始运行:Thu Feb 24 19:52:46 CST 2022
开始运行:Thu Feb 24 19:52:46 CST 2022
开始运行:Thu Feb 24 19:52:46 CST 2022
开始运行:Thu Feb 24 19:52:46 CST 2022
开始运行:Thu Feb 24 19:52:46 CST 2022
开始运行:Thu Feb 24 19:52:46 CST 2022
开始运行:Thu Feb 24 19:52:46 CST 2022
开始运行:Thu Feb 24 19:52:46 CST 2022
开始运行:Thu Feb 24 19:52:46 CST 2022
开始运行:Thu Feb 24 19:52:46 CST 2022
开始运行:Thu Feb 24 19:52:46 CST 2022
开始运行:Thu Feb 24 19:52:46 CST 2022
创建线程:21Thu Feb 24 19:52:47 CST 2022
创建线程:22Thu Feb 24 19:52:48 CST 2022
创建线程:23Thu Feb 24 19:52:49 CST 2022
创建线程:24Thu Feb 24 19:52:50 CST 2022
创建线程:25Thu Feb 24 19:52:51 CST 2022
创建线程:26Thu Feb 24 19:52:52 CST 2022
创建线程:27Thu Feb 24 19:52:53 CST 2022
创建线程:28Thu Feb 24 19:52:54 CST 2022
创建线程:29Thu Feb 24 19:52:55 CST 2022
创建线程:30Thu Feb 24 19:52:56 CST 2022
创建线程:31Thu Feb 24 19:52:57 CST 2022
创建线程:32Thu Feb 24 19:52:58 CST 2022
创建线程:33Thu Feb 24 19:52:59 CST 2022
创建线程:34Thu Feb 24 19:53:00 CST 2022
创建线程:35Thu Feb 24 19:53:01 CST 2022
创建线程:36Thu Feb 24 19:53:02 CST 2022
创建线程:37Thu Feb 24 19:53:03 CST 2022
创建线程:38Thu Feb 24 19:53:04 CST 2022
创建线程:39Thu Feb 24 19:53:05 CST 2022
创建线程:40Thu Feb 24 19:53:06 CST 2022
数量凑齐,开始运行-------------------:Thu Feb 24 19:53:06 CST 2022
开始运行:Thu Feb 24 19:53:06 CST 2022
开始运行:Thu Feb 24 19:53:06 CST 2022
开始运行:Thu Feb 24 19:53:06 CST 2022
开始运行:Thu Feb 24 19:53:06 CST 2022
开始运行:Thu Feb 24 19:53:06 CST 2022
开始运行:Thu Feb 24 19:53:06 CST 2022
开始运行:Thu Feb 24 19:53:06 CST 2022
开始运行:Thu Feb 24 19:53:06 CST 2022
开始运行:Thu Feb 24 19:53:06 CST 2022
开始运行:Thu Feb 24 19:53:06 CST 2022
开始运行:Thu Feb 24 19:53:06 CST 2022
开始运行:Thu Feb 24 19:53:06 CST 2022
开始运行:Thu Feb 24 19:53:06 CST 2022
开始运行:Thu Feb 24 19:53:06 CST 2022
开始运行:Thu Feb 24 19:53:06 CST 2022
开始运行:Thu Feb 24 19:53:06 CST 2022
开始运行:Thu Feb 24 19:53:06 CST 2022
开始运行:Thu Feb 24 19:53:06 CST 2022
开始运行:Thu Feb 24 19:53:06 CST 2022
开始运行:Thu Feb 24 19:53:06 CST 2022
创建线程:41Thu Feb 24 19:53:07 CST 2022

从上述结果可以看出,只有20个线程都准备好了之后才会开始运行,并且这个程序如果不手动关闭,则会一直处理运行等待状态。

jdk中还有一个线程相关的类,可以实现类似限流的操作,可以设定允许同时运行的线程数量,这个类就是Semaphore,示例代码如下:

/**
 * @Author tuzongxun
 * @Date 2022/2/24
 */
public class SemaphoreDemo 

	public static void main(String[] args) 
		Semaphore sd=new Semaphore(2);
		for (int i = 0; i < 10; i++) 
			new Thread(()->
				try 
					sd.acquire();
					System.out.println(new Date());
					Thread.sleep(1000);
				
				catch (InterruptedException e) 
					e.printStackTrace();
				finally 
					sd.release();
				
			).start();
		
	

这里创建Semaphore对象时传入了参数2,意思就是同时运行有2个线程运行,上述代码执行结果如下:

Thu Feb 24 20:48:57 CST 2022
Thu Feb 24 20:48:57 CST 2022
Thu Feb 24 20:48:58 CST 2022
Thu Feb 24 20:48:58 CST 2022
Thu Feb 24 20:48:59 CST 2022
Thu Feb 24 20:48:59 CST 2022
Thu Feb 24 20:49:00 CST 2022
Thu Feb 24 20:49:00 CST 2022
Thu Feb 24 20:49:01 CST 2022
Thu Feb 24 20:49:01 CST 2022

可以看到,每秒只有两个结果是一样的。如果把上边对象的2改成5,则运行结果如下:

Thu Feb 24 20:53:21 CST 2022
Thu Feb 24 20:53:21 CST 2022
Thu Feb 24 20:53:21 CST 2022
Thu Feb 24 20:53:21 CST 2022
Thu Feb 24 20:53:21 CST 2022
Thu Feb 24 20:53:22 CST 2022
Thu Feb 24 20:53:22 CST 2022
Thu Feb 24 20:53:22 CST 2022
Thu Feb 24 20:53:22 CST 2022
Thu Feb 24 20:53:22 CST 2022

即同一秒有5个线程在运行。
需要注意的是,这个类也是支持公平锁和非公平锁的,默认是非公平,如果要使用公平锁,则可以这样增加第二个参数,设置为true,例如:

Semaphore sd=new Semaphore(5,true);

除了上述加锁用法,还有一个也比较常用的锁相关的类LockSupport,用法示例如下:

/**
 * @Author tuzongxun
 * @Date 2022/2/24
 */
public class LockSupportDemo 
	public static void main(String[] args) 
		Thread t = new Thread(() -> 
			for (int i = 0; i < 10; i++) 
				if (i == 3) 
					LockSupport.park();
				
				System.out.println(i + ":" + new Date());
			
		);
		t.start();
		try 
			Thread.sleep(3000);
			LockSupport.unpark(t);
		
		catch (InterruptedException e) 
			e.printStackTrace();
		
	

上述代码输出结果如下:

0:Thu Feb 24 23:58:50 CST 2022
1:Thu Feb 24 23:58:50 CST 2022
2:Thu Feb 24 23:58:50 CST 2022
3:Thu Feb 24 23:58:53 CST 2022
4:Thu Feb 24 23:58:53 CST 2022
5:Thu Feb 24 23:58:53 CST 2022
6:Thu Feb 24 23:58:53 CST 2022
7:Thu Feb 24 23:58:53 CST 2022
8:Thu Feb 24 23:58:53 CST 2022
9:Thu Feb 24 23:58:53 CST 2022

可以很明显的看到线程在park加锁后就进入了阻塞状态,在调用了unpark之后才开始继续运行。
线程锁相关的用法很多,各种锁都有自己的适用场景,没有绝对的哪个更好,甚至有的时候可能用哪个都差不多,这些都需要具体需要的时候分析以及测试。

以上是关于java基础之多线程总结二(CAS和各种常用锁)的主要内容,如果未能解决你的问题,请参考以下文章

java基础之多线程总结二

java基础之多线程总结三(AQSThreadLocal和线程池)

java基础之多线程总结三(AQSThreadLocal和线程池)

java基础之多线程总结三(AQSThreadLocal和线程池)

Java并发编程:并发编程基础各种锁详细介绍

Java技术栈各种风骚的分布式锁