一文总结 JUC 并发编程
Posted 小毕超
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了一文总结 JUC 并发编程相关的知识,希望对你有一定的参考价值。
文章目录
一、JUC 并发编程
在 Java
编程中,线程并发是一个十分重要的模块,而 JUC
其实就是java.util .concurrent
工具包的简称 ,该工具包整合了 Java
中对于并发处理、优化等工具。其中包括了例如线程的创建与协调、原子类、Lock 锁、线程安全集合、辅助类等。
二、协调锁
在 JUC
中锁可以分为两块,一个是传统的 synchronized
锁,另一个则是实现 Lock
接口的锁。
1. Synchronized
synchronized
是 Java
中的关键字,是一种常见的同步锁。它可以修饰一个代码块,被修饰的代码块称为同步代码块,作用的对象是调用这个代码块的对象。如果后面括号括起来的部分是一个类,其作用锁的对象是这个类的所有对象。
synchronized (this)
...
synchronized (A.class)
...
还可以修饰一个方法,被修饰的方法称为同步方法,其作用的范围是整个方法,作用
的对象是调用这个方法的对象;
public synchronized void test()
...
还可以修饰一个静态的方法,其作用的范围是整个静态方法,作用的对象是这个类的
所有对象;
public synchronized static void test()
...
注意点:虽然可以使用 synchronized
来定义方法,但 synchronized
并不属于方法定
义的一部分,因此,synchronized
关键字不能被继承。如果在父类中的某个方法使用了synchronized
关键字,而在子类中覆盖了这个方法,在子类中的这个方法默认情况下并不是同步的,而必须显式地在子类的这个方法中加上synchronized
关键字才可以。当然,还可以在子类方法中调用父类中相应的方法,这样虽然子类中的方法不是同步的,但子类调用了父类的同步方法,因此,子类的方法也就相当于同步了。
2. Synchronized 锁下线程通信
如果一个代码块被 synchronized
修饰了,当一个线程获取了对应的锁,并执行该代码块时,其他线程便只能一直等待,其中要么获得锁的线程执行完了所有逻辑,正常释放锁,要么线程执行发生异常,由 JVM
自动释放锁,但是如果获得锁的线程需要等待某个时机,但一直不释放锁,其他线程能等待,肯定影响程序执行效率,因此线程间通信是必须的。在 Synchronized 锁的情况下,可以使用 wait()
、notify()
、notifyAll()
进行通信,其中 wait()
表示挂起当前线程并释放锁,notify()
、notifyAll()
则唤醒被挂起的线程,这些都是Object
内置的方法
例如实现一个生产消费的场景,消费的前提是有产品,如果没有产品则释放锁等待,生产者则生产,如果有产品则等待消费者消费后再生产:
public class Test1
private volatile int productNum;
private synchronized void producer()
try
while (productNum != 0)
this.wait();
productNum++;
System.out.println("生产增加!");
notifyAll();
catch (Exception e)
e.printStackTrace();
private synchronized void consumer()
try
while (productNum == 0)
this.wait();
productNum--;
System.out.println("消费减少!");
notifyAll();
catch (Exception e)
e.printStackTrace();
public static void main(String[] args)
Test1 test1 = new Test1();
new Thread(() ->
for (int i = 0; i < 5; i++)
test1.producer();
, "线程 A").start();
new Thread(() ->
for (int i = 0; i < 5; i++)
test1.consumer();
, "线程 B").start();
这里补充下 Join、Wait、Sleep
之间的区别:
sleep(long)
:在睡眠时不释放对象锁。
join(long)
:先执行另外的一个线程,在等待的过程中释放对象锁 底层是基于wait
封装的。
wait(long)
:在等待的过程中释放对象锁需要在我们synchronized
中使用。
3. Lock 锁
Lock
锁实现提供了比使用 Synchronized
更广泛的锁操作。它们允许更灵活的结构,可能具有不同的属性,并且可能支持多个关联的条件对象。Lock
是一个Java
接口,并非 Java
的关键字,使用 Lock
锁 需要用户手动去上锁和释放锁,如果没有主动释放锁,就有可能导致出现死锁现象。
public interface Lock
// 获取锁
void lock();
// 可中断加锁,即在锁获取过程中不处理中断状态,而是直接抛出中断异常,由上层调用者处理中断。
void lockInterruptibly() throws InterruptedException;
// 尝试获取锁,并立即返回结果
boolean tryLock();
// 尝试获取锁,在一定时间范围内
boolean tryLock(long time, TimeUnit unit) throws InterruptedException;
// 释放锁
void unlock();
// 线程协调,通过它挂起、唤醒线程
Condition newCondition();
注意:如果采用 Lock
锁,必须主动去释放锁,并且在发生异常的情况下,是不会自动释放锁。所以使用 Lock
时,建议在 try
块中进行,并且在 finally
块中释放锁,以保证锁一定会被释放,防止死锁 。
其中在 Lock
接口中,提供了 newCondition()
方法获取一个 Condition
接口对象,该对象提供了 await()
、signal()
方法,与 synchronized
的 wait()
、notify()
方法类似,可以挂起、唤醒线程,不过 Condition
可以声明出多个,以分别应对不同的线程,进行针对性的挂起和唤醒。
Lock
对比 synchronized
在性能上来说,如果竞争资源不激烈,两者的性能差不多,当竞争资源非常激烈时, Lock
的性能要远远优于 synchronized
。
4. Lock 锁实例 - ReentrantLock
和 Synchronized
相同 ReentrantLock
为可重入锁,可通过 fair
参数决定当前是公平锁还是非公平锁,例如生产消费的场景实现:
public class Test1
//可重入锁
private Lock lock = new ReentrantLock(true);
// 生产者的选择器
private Condition producerCondition = lock.newCondition();
// 消费者的选择器
private Condition consumerCondition = lock.newCondition();
private volatile int productNum;
private void producer()
lock.lock();
try
while (productNum != 0)
producerCondition.await();
productNum++;
System.out.println("生产增加!");
// 唤起消费者
consumerCondition.signalAll();
catch (Exception e)
e.printStackTrace();
finally
lock.unlock();
private void consumer()
lock.lock();
try
while (productNum == 0)
consumerCondition.await();
productNum--;
System.out.println("消费减少!");
// 唤起生产者
producerCondition.signalAll();
catch (Exception e)
e.printStackTrace();
finally
lock.unlock();
public static void main(String[] args)
Test1 test1 = new Test1();
new Thread(() ->
for (int i = 0; i < 5; i++)
test1.producer();
, "线程 A").start();
new Thread(() ->
for (int i = 0; i < 5; i++)
test1.consumer();
, "线程 B").start();
5. 读写锁 - ReadWriteLock
读写锁将操作读写分开成 2 个锁来分配给线程,在多个线程读的情况下,不受锁的控制,但是在写的情况下,只允许一个获得锁的线程来写,并且写的过程中读锁也在等待。
例如:
public class Test
private ReadWriteLock readWriteLock = new ReentrantReadWriteLock();
private Lock readLock = readWriteLock.readLock();
private Lock writeLock = readWriteLock.writeLock();
public void read()
readLock.lock();
try
Thread.sleep(1000);
System.out.println(Thread.currentThread().getName() + "读操作,进行中!");
catch (Exception e)
e.printStackTrace();
finally
readLock.unlock();
public void write()
writeLock.lock();
try
System.out.println(Thread.currentThread().getName() + "写操作,进行中!");
Thread.sleep(1000);
catch (Exception e)
e.printStackTrace();
finally
writeLock.unlock();
public static void main(String[] args)
Test test = new Test();
new Thread(() ->
for (int i = 0; i < 5; i++)
test.read();
, "线程 A").start();
new Thread(() ->
for (int i = 0; i < 5; i++)
test.read();
, "线程 B").start();
new Thread(() ->
for (int i = 0; i < 5; i++)
test.write();
, "线程 C").start();
可以看出,当写锁执行时,读锁也会等待,写锁释放后,读锁不会阻塞。
三、CAS & 原子类
上面情况下的锁都属于悲观锁,当多个线程对争夺同一个锁时,最后只有一个线程才能获取成功,其余线程只能是阻塞等待,比如:mysql
中在更新一条数据时,事务未提交时,通过 for update
查询最新的数据,只能等待事务提交后方可查询。
而乐观锁比较乐观,通过预值或者版本号比较,进行 CAS
,如果不一致性的情况则通过循环控制修改,当前线程不会被阻塞,数据也没有加锁,因此性能比悲观锁要好。
比如下面的 SQL 语句:
update table set value='newValue', version=version+1 where id = 1 and version = 1;
1. CAS
Compare and Swap
的缩写,译为比较并交换,其中有三个主要的参数 CAS(V,E, N)
,内存值V
,旧的预期值E
,要修改的新值N
, 在将值由 V
更新为 N
时,先进行 E == V
判断,如果成立则更新 V
的值为 N
,否则什么都不做。一般通过自旋操作控制不成立的情况,则重新计算 N
值,并将当前的 V
值赋予 E
值 ,重新判断。
但这种情况有可能会遇到 ABA
的问题,就是原来的值为 A
,此时有线程改为了 B
,B
又改为了A
,此时如果某个线程中的旧值还是 A
,会感知不到中间发生的变化,解决该问题,可以通过增加版本号,每次修改对版本号加一,后面进行版本号的对比。
CAS
也不是没有缺点,如果长时间不成功,一直处于自旋状态,会给CPU
带来非常大的执行开销。而悲观锁就不会给 CPU
带来很大的开销,但会带来比较多的上下文切换。
CAS
的过程需要保证原子性,在 Java
中通过 unsafe jni
技术使用硬件指令来实现。
在 Java
中通过该技术实现了一批类,可以让我们无需关注底层的CAS
过程。
2. 原子类
原子类是 java.util.concurrent.atomic
包下的类,在多线程情况下通过 CAS
无锁机制进行数据的更新。
2.1 基础原子类
原子类中提供了基础类型的类:AtomicInteger、AtomicBoolean、AtomicLong
,可以直接开箱即用,以 AtomicInteger
为例,其中常用的 API
如下:
// 获取当前值。
public final int get()
// 获取当前值,并将当前值设置为新值
public final int getAndSet(int newValue)
// 获取当前值,并将当前值 +1
public final int getAndIncrement()
// 将当前值 +1 ,获取当前值
public final int incrementAndGet()
// 获取当前值,并将当前值 -1
public final int getAndDecrement()
// 将当前值 -1 ,获取当前值
public final int decrementAndGet()
// 获取当前值,并将当前值 + delta
public final int getAndAdd(int delta)
// 将当前值 + delta , 获取当前值
public final int addAndGet(int delta)
// 如果当前值==为预期值,则将该值设置为给定的新值。
public boolean comapreAndSet(int expect,int update)
例如当多个线程对一个变量进行操作时,会出现线程安全问题,现在换成原子类,进行测试下,当两个线程对一个变量进行 +1
:
public class ATest1
public static void main(String[] args)
AtomicInteger productNum = new AtomicInteger(0);
new Thread(() ->
for (int i = 0; i < 5; i++)
System.out.println(productNum.incrementAndGet());
, "线程 A").start();
new Thread(() ->
for (int i = 0; i < 5; i++)
System.out.println(productNum.incrementAndGet());
, "线程 B").start();
2.2 数组类型原子类
原子类中数组类型的原子类有:AtomicIntegerArray、AtomicLongArray、AtomicRreferenceArray
,相比于基础类型,在数据操作时,需要传入数据的下标。
AtomicIntegerArray atomicIntegerArray = new AtomicIntegerArray(10);
// 第 0 个位置的数据,+ 10
atomicIntegerArray.getAndAdd(0,10);
2.3 引用型原子类
上面的类型都是指定类型的, 在实际开发中会遇到各种各样的类型,为此为我们提供了引用型原子类,AtomicReference<V>
,例如实现一个 String
类型的原子类:
AtomicReference<String> atomicReference = new AtomicReference<>("");
//设置数据
atomicReference.set("abc");
//获取数据
System.out.println(atomicReference.get());
四、线程
创建线程上面使用了,直接创建 Thread
类的方式,在刚开始学习线程时,还有一种通过使用 Runnable
创建线程,这些基本的方式这里就不做介绍了,这里主要介绍两个,Callable & Future
、线程池。
1. Callable 和 Future
上面创建线程的方式都拿不到线程返回的结果,为了支持此功能,Java
中提供了 Callable
接口 和 Future
接口。
对于 Callable
,需要通过 call()
方法返回线程的结果,如:
Callable<String> callable = () ->
System.out.println(Thread.currentThread().getName() + " 执行了");
return "success";
;
不过 call()
返回的结果需要存储在主线程已知的对象中,以便主线程可以知道该线程返回的结果。此时就需要用到 Future
对象,Future
可以视为保存结果的对象,它可能暂时不保存结果,但将来会保存(一旦Callable
返回),Future
基本上是主线程可以跟踪进度以及其他线程的结果的一种方式。
public class Test2
public static void main(String[] args) throws Exception
Callable<String> callable = () ->
System.out.println(Thread.currentThread().getName() + " 执行了");
return "success";
;
FutureTask<String> future = new FutureTask<>(callable);
new Thread(future<以上是关于一文总结 JUC 并发编程的主要内容,如果未能解决你的问题,请参考以下文章
Java基础学习总结(193)—— JUC 常用并发工具类总结