目录
前言
JUC是java.util.concurrent包的简称,在Java5.0添加,目的就是为了更好的支持
高并发
任务。让开发者进行多线程编程时减少竞争条件和死锁的问题!
一、atomic包
方便程序员在多线程环境下,无锁的进行原子操作。Atomic包里的类基本都是使用Unsafe实现的包装类,核心操作是CAS原子操作。
AtomicInteger类
举个例子就是我们平时的 i++,不是原子的操作,在多线程环境下会发生错误,但是使用atomic提供的原子类,就可以很好的避免这个问题发生。
代码演示:
public class Test {
static AtomicInteger atomicInteger = new AtomicInteger(0);
static int count = 100000000;
public static void main(String[] args) throws InterruptedException {
Thread t = new Thread(()->{
for(int i = 0; i < count; i++){
//相当于i++
atomicInteger.getAndIncrement();
}
});
t.start();
Thread t2 = new Thread(() -> {
for(int i = 0; i < count; i++){
//相当于i--
atomicInteger.getAndDecrement();
}
});
t2.start();
t.join();
t2.join();
System.out.println(atomicInteger);
}
}
运行结果
可以看到在一亿这个数量级上都是0,足以说明了原子性得到了保持。
AtomicReference类
CAS(
关于CAS之前的博客有详细的介绍
) 保证的是对一个变量执行操作的原子性,如果对多个变量操作时,CAS 目前无法直接保证操作的原子性的。所以将多个变量封装成对象,通过AtomicReference来保证原子性。
AtomicReference类内部对变量的表示:
private volatile V value;
AtomicStampedReference类
由于使用CAS会遇到ABA问题(
关于ABA问题
),所以使用
AtomicStampedReference类实现了用版本号作比较的CAS机制。
AtomicStampedReference类内部对变量的表示
private static class Pair<T> {
final T reference;
final int stamp;
private Pair(T reference, int stamp) {
this.reference = reference;
this.stamp = stamp;
}
static <T> Pair<T> of(T reference, int stamp) {
return new Pair<T>(reference, stamp);
}
}
private volatile Pair<V> pair;
/**
* Creates a new {@code AtomicStampedReference} with the given
* initial values.
*
* @param initialRef the initial reference //变量的引用
* @param initialStamp the initial stamp //这个就是版本号
*/
public AtomicStampedReference(V initialRef, int initialStamp) {
pair = Pair.of(initialRef, initialStamp);
}
二、locks包
我们选出几个比较重要接口和实现类讲一下:
接口
上面可以看到一共有三个接口分别是 Condition、Lock、ReadWriteLock。
Condition
condition
主要是为了在JUC框架中提供和Java传统的监视器风格的wait,notify和notifyAll方法类似的功能。
condition一般和lock一起使用,就像synchronized与wait、notify使用一样
。
代码展示:
public class Test {
static Lock lock = new ReentrantLock();
static Condition condition = lock.newCondition();
public static void main(String[] args) throws InterruptedException {
Thread t = new Thread(() -> {
lock.lock();
try {
condition.await();
System.out.println("子线程醒了");
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
});
t.start();
System.out.println("两秒之后叫醒子线程");
Thread.sleep(2000);
lock.lock();
condition.signal();
lock.unlock();
}
}
运行结果
Lock
在 Lock 接口中,获取锁的方法有 4 个:lock()、tryLock()、tryLock(long,TimeUnit)、lockInterruptibly(),为什么需要这么多方法?这些方法都有什么区别?接下来我们一起来看。
lock()
lock 方法是 Lock 接口中最基础的获取锁的方法,
当有可用锁时会直接得到锁并立即返回,当没有可用锁时会一直等待,直到获取到锁为止
,它的基础用法如下:
Lock lock = new ReentrantLock();
// 获取锁
lock.lock();
try {
// 执行业务代码...
} finally {
//释放锁
lock.unlock();
}
lockInterruptibly()
lockInterruptibly 方法和 lock 方法类似,
当有可用锁时会直接得到锁并立即返回,如果没有可用锁会一直等待直到获取锁,但和 lock 方法不同,lockInterruptibly 方法在等待获取时,如果遇到线程中断会放弃获取锁。
它的基础用法如下:
Lock lock = new ReentrantLock();
try {
// 获取锁
lock.lockInterruptibly();
try {
// 执行业务方法...
} finally {
// 释放锁
lock.unlock();
}
} catch (InterruptedException e) {
e.printStackTrace();
}
使用 t.interrupt() 方法可以中断线程执行。
tryLock()
与前面的两个方法不同,使用无参的 tryLock 方法会尝试获取锁,并立即返回获取锁的结果(true 或 false),如果有可用锁返回 true,并得到此锁,如果没有可用锁会立即返回 false。它的基础用法如下:
Lock lock = new ReentrantLock();
// 获取锁
boolean result = lock.tryLock();
if (result) {
try {
// 获取锁成功,执行业务代码...
} finally {
// 释放锁
lock.unlock();
}
} else {
// 执行获取锁失败的业务代码...
}
tryLock(long,TimeUnit)
有参数的 tryLock(long,TimeUnit) 方法需要设置两个参数,第一个参数是 long 类型的超时时间,第二个参数是对参数一的时间类型描述(比如第一参数是 3,那么它究竟是 3 秒还是 3 分钟,是第二个参数说了算的)。在这段时间内如果获取到可用的锁了就返回 true,如果在定义的时间内,没有得到锁就会返回 false
。
它的基础用法如下:
Lock lock = new ReentrantLock();
try {
// 获取锁(最多等待 3s,如果获取不到锁就返回 false)
boolean result = lock.tryLock(3, TimeUnit.SECONDS);
if (result) {
try {
// 获取锁成功,执行业务代码...
} finally {
// 释放锁
lock.unlock();
}
} else {
// 执行获取锁失败的业务代码...
}
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized和Lock的区别
1.synchronized 可以给类、方法、代码块加锁;而 lock 只能给代码块加锁。
2.synchronized 不需要手动获取锁和释放锁,使用简单,发生异常会自动释放锁,不会造成死锁;而 lock 需要自己加锁和释放锁,如果使用不当没有 unLock()去释放锁就会造成死锁。
3.通过 Lock 可以知道有没有成功获取锁,而 synchronized 却无法办到。
ReadWriteLock
首先ReentrantLock(后面介绍)某些时候有局限,如果使用ReentrantLock,可能本身是为了防止线程A在写数据、线程B在读数据造成的数据不一致,但这样,如果线程C在读数据、线程D也在读数据,读数据是不会改变数据的,没有必要加锁,但是还是加锁了,降低了程序的性能。
因为这个,才诞生了读写锁ReadWriteLock。ReadWriteLock是一个读写锁接口,ReentrantReadWriteLock是ReadWriteLock接口的一个具体实现,实现了读写的分离,
读锁是共享的,写锁是独占的,读和读之间不会互斥,读和写、写和读、写和写之间才会互斥,提升了读写的性能。
两个线程都是读:
public class Main {
public static void main(String[] args) {
ReadWriteLock readWriteLock = new ReentrantReadWriteLock();
Lock readLock = readWriteLock.readLock();
Lock writeLock = readWriteLock.writeLock();
// “写”的角色,请求写锁
// "只读“角色,请求读锁
readLock.lock(); // 读锁已经有了
// writeLock.lock(); // 写锁锁上
Thread t = new Thread() {
@Override
public void run() {
readLock.lock();
// writeLock.lock();
System.out.println("子线程也可以加锁成功");
}
};
t.start();
}
}
运行结果
一个线程读一个线程写
public class Main {
public static void main(String[] args) {
ReadWriteLock readWriteLock = new ReentrantReadWriteLock();
Lock readLock = readWriteLock.readLock();
Lock writeLock = readWriteLock.writeLock();
// “写”的角色,请求写锁
// "只读“角色,请求读锁
readLock.lock(); // 读锁已经有了
// writeLock.lock(); // 写锁锁上
Thread t = new Thread() {
@Override
public void run() {
// readLock.lock();
writeLock.lock();
System.out.println("子线程也可以加锁成功");
}
};
t.start();
}
}
此时由于读锁还没有解锁,所以写锁会阻塞在哪里,不会有输出。
实现类
ReentrantLock类
这个类前面我们已经用到了。首先根据字面意思表示可
重入锁
。
什么是可重入锁:先看一下代码
public class Main {
public static void main(String[] args) {
// 我们是主线程
Lock lock = new ReentrantLock(); // 名字已经说明,这把锁是可重入锁
lock.lock(); // 锁已经被 main 线程锁了
lock.lock();
System.out.println("说明允许可重入");
}
}
也就是是否允许持有锁的线程成功请求到同一把锁,
这里得是同一个线程。
对与synchrnized来说,也是可重入锁:代码如下:
public class Main2 {
public static void main(String[] args) {
Object lock = new Object();
synchronized (lock) { // main 线程已经对 lock 加锁了
synchronized (lock) { // main 线程再次对 lock 请求锁(处于已经锁上状态)
System.out.println("这里打印了就说明了 sync 锁是可重入锁");
}
}
}
}
ReentrantLock
默认是不公平锁
,但是可以修改。synchronized是不公平锁。
public class Main {
public static void main(String[] args) {
Lock lock = new ReentrantLock(true); // fair = true:使用公平锁模式
Lock lock1 = new ReentrantLock(false); // fair = false:使用不公平锁模式
Lock lock2 = new ReentrantLock(); // 默认情况下是不公平的
}
}
Synchronized与ReentrantLock
1.两者都是可重入锁
可重入锁:重入锁,也叫做递归锁,可重入锁指的是在一个线程中可以多次获取同一把锁,比如: 一个线程在执行一个带锁的方法,该方法中又调用了另一个需要相同锁的方法,则该线程可以直接执行调用的方法,而无需重新获得锁, 两者都是同一个线程每进入一次,锁的计数器都自增1,所以要等到锁的计数器下降为0时才能释放锁。
2.synchronized 依赖于 JVM 而 ReentrantLock 依赖于 API
- synchronized 是依赖于 JVM 实现的,虚拟机团队在 JDK1.6 为 synchronized 关键字进行了很多优化,但是这些优化都是在虚拟机层面实现的。
- ReentrantLock 是 JDK 层面实现的(也就是 API 层面,需要 lock() 和 unlock() 方法配合 try/finally 语句块来完成)
3.ReentrantLock 比 synchronized 增加了一些高级功能
相比synchronized,ReentrantLock增加了一些高级功能。主要来说主要有三点:①等待可中断;②可实现公平锁;③可实现选择性通知(锁可以绑定多个条件)
- 等待可中断.通过lock.lockInterruptibly()来实现这个机制。也就是说正在等待的线程可以选择放弃等待,改为处理其他事情。
- ReentrantLock可以指定是公平锁还是非公平锁。而synchronized只能是非公平锁。所谓的公平锁就是先等待的线程先获得锁。 ReentrantLock默认情况是非公平的,可以通过 ReentrantLock类的ReentrantLock(boolean fair)构造方法来制定是否是公平的。
-
ReentrantLock类线程对象可以注册在指定的Condition中,从而可以有选择性的进行线程通知,在调度线程上更加灵活。 在使用notify()/notifyAll()方法进行通知时,被通知的线程是由 JVM 选择的,
用ReentrantLock类结合Condition实例可以实现“选择性通知”
4.使用选择
- 除非需要使用 ReentrantLock 的高级功能,否则优先使用 synchronized。
- synchronized 是 JVM 实现的一种锁机制,JVM 原生地支持它,而 ReentrantLock 不是所有的 JDK 版本都支持。并且使用 synchronized 不用担心没有释放锁而导致死锁问题,因为 JVM 会确保锁的释放。
ReentrantReadWriteLock类
首先ReentrantLock某些时候有局限,如果使用ReentrantLock,可能本身是为了防止线程A在写数据、线程B在读数据造成的数据不一致,但这样,如果线程C在读数据、线程D也在读数据,读数据是不会改变数据的,没有必要加锁,但是还是加锁了,降低了程序的性能。
因为这个,才诞生了读写锁ReadWriteLock。ReadWriteLock是一个读写锁接口,ReentrantReadWriteLock是ReadWriteLock接口的一个具体实现,实现了读写的分离,读锁是共享的,写锁是独占的,读和读之间不会互斥,读和写、写和读、写和写之间才会互斥,提升了读写的性能。其实都是前面说的~~~。
三、CountDownLatch
CountDownLatch是一个同步工具类,
用来协调多个线程之间的同步
,或者说起到线程之间的通信(而不是用作互斥的作用)。
CountDownLatch能够使一个线程在等待另外一些线程完成各自工作之后,再继续执行。使用一个计数器进行实现。计数器初始值为线程的数量。当每一个线程完成自己任务后,计数器的值就会减一(当然这里一个线程也可以返回多个)。当计数器的值为0时,表示所有的线程都已经完成一些任务,然后在CountDownLatch上等待的线程就可以恢复执行接下来的任务。
代码演示:
public class Main {
// count: 计数器为 3 个,只有 3 个全部报到了,门闩才会打开
static CountDownLatch countDownLatch = new CountDownLatch(3);
static class MyThread extends Thread {
@Override
public void run() {
countDownLatch.countDown();
countDownLatch.countDown();
countDownLatch.countDown();
}
}
public static void main(String[] args) throws InterruptedException {
MyThread t = new MyThread();
t.start();
countDownLatch.await();
System.out.println("门闩被打开了");
}
}
四、Semaphore(信号量)
Semaphore也叫信号量,在JDK1.5被引入,可以用来控制同时访问特定资源的线程数量,通过协调各个线程,以保证合理的使用资源。
Semaphore内部维护了一组虚拟的许可,许可的数量可以通过构造函数的参数指定。
- 访问特定资源前,必须使用acquire方法获得许可,如果许可数量为0,该线程则一直阻塞,直到有可用许可。
- 访问资源后,使用release释放许可。
Semaphore和ReentrantLock类似,获取许可有公平策略和非公平许可策略,默认情况下使用非公平策略。
Semaphore可以用来做流量分流,特别是对公共资源有限的场景,比如数据库连接。
假设有这个的需求,读取几万个文件的数据到数据库中,由于文件读取是IO密集型任务,可以启动几十个线程并发读取,但是数据库连接数只有10个,这时就必须控制最多只有10个线程能够拿到数据库连接进行操作。这个时候,就可以使用Semaphore做流量控制。
代码展示:
public class SemaphoreTest {
private static final int COUNT = 40;
private static Executor executor = Executors.newFixedThreadPool(COUNT);
private static Semaphore semaphore = new Semaphore(10);
public static void main(String[] args) {
for (int i=0; i< COUNT; i++) {
executor.execute(new ThreadTest.Task());
}
}
static class Task implements Runnable {
@Override
public void run() {
try {
//读取文件操作
semaphore.acquire();
// 存数据过程
semaphore.release();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
}
}
}
}
总结
加油哦~~