JUC是java.util.concurrent包,由jdk1.5引进。
1.ReentrantLock
可重入互斥锁,和Sychronized定位类似,都是用来实现互斥效果,保证线程安全的。
ReentrantLock的方法:
1.lock():加锁操作,如果获取不到锁就死等。
2.trylock(超时时间):加锁操作,如果等待一段时间,获取不到锁之后就直接放弃加锁。
3.unlock():解锁操作。
1.ReentrantLock的休眠唤醒
和Sychronized休眠和唤醒不一样,需要创建Condition对象来进行休眠与唤醒。ReentrantLock是有条件的休眠和唤醒,对应的对象唤醒对应的线程。
// 创建一个休眠和唤醒的条件
Condition condition = reentrantLock.newCondition();
// 等待,类似于locker.wait();
condition.await();
// 唤醒,类似于locker.notify();
condition.signal();
// 唤醒所有,类似于locker.notifyAll();
condition.signalAll();
// 锁对象
Object locker = new Object();
// 等待,并释放锁资源
locker.wait();
// 唤醒之前等待的线程
locker.notify();
// 唤醒所有
locker.notifyAll();
2.ReentrantLock与Sychronized的区别
Sychronized退出代码块就自动释放锁,ReentrantLock退出代码块需要手动释放锁,注意需要用try catch来处理加锁的代码(防止忘记释放锁形成死锁)。
Sychronized只支持非公平锁,ReentrantLock自持公平锁也支持非公平锁,可以通过构造方法传入true来开启公平锁模式
ReentrantLock可以根据不同条件来唤醒和休眠
Sychronized在申请失败时,会一直等待锁资源,ReentrantLock在申请失败时,会等待一段时间,超时会放弃申请
Sychronized是一个关键字,是JVM内部实现的,ReentrantLock是标准库中的一个类,由java层面实现
3.如何选择哪个锁
锁竞争不激烈的时候, 使用 synchronized, 效率更高, 自动释放更方便.
锁竞争激烈的时候, 使用 ReentrantLock, 搭配 trylock 更灵活控制加锁的行为, 而不是死等.
如果需要使用公平锁, 使用 ReentrantLock.
2.Callable接口
Callable是一个interface,相当于把线程封装了一个返回值,方便借助多线程的方式进行计算。
其中Callable是搭配FutureTask一起使用,FutureTask是用来获取Callable 的执行结果的
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;
import java.util.concurrent.TimeUnit;
public class Demo_1101_Callable {
public static void main(String[] args) {
// 通过Callable定义线程的任务
Callable<Integer> callable = new Callable<Integer>() {
@Override
public Integer call() throws Exception {
// 这里是具体要处理的任务
System.out.println("执行运算..");
int a = 10 + 20;
// 等待3秒返回
TimeUnit.SECONDS.sleep(3);
// throw new Exception("任务执行过程中出现异常");
return a;
}
};
// Callable要配合FutureTask一起使用,FutureTask是用来获取Callable的执行结果的
FutureTask<Integer> futureTask = new FutureTask<>(callable);
// FutureTask当做构造参数传入Thread的构造方法里
Thread thread = new Thread(futureTask);
// 启动线程
thread.start();
try {
// 阻塞等待Callable任务的执行结果
Integer result = futureTask.get();
System.out.println("线程的执行结果:" + result);
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
System.out.println("捕获到任务异常:" + e.getMessage());
}
}
}
同样是描述一个任务,Runnable与Callable 的区别:
1.Callable要实现call()方法,是有返回值,而Runnable要实现run()方法, 没有返回值。
2.Callable的call()方法可以抛出异常,Runnable的run()方法不能抛出异常。
3.Callable 要搭配FutureTask来使用,获取结果的时候需要使用get()方法。
4.两者都是描述线程任务的接口
3.原子类
原子类内部用的是 CAS 实现,所以性能要比加锁实现 i++ 高很多。原子类有以下几个:
AtomicBoolean
AtomicInteger
AtomicIntegerArray
AtomicLong
AtomicReference
AtomicStampedReference
以 AtomicInteger 举例,常见方法有:
addAndGet(int delta); i += delta;
decrementAndGet(); --i;
getAndDecrement(); i--;
incrementAndGet(); ++i;
getAndIncrement(); i++;
4.信号量——Semaphore
计数信号量。 从概念上讲,信号量保持一组许可。 如果有必要,每个 acquire()都会阻止,直到有许可证,然后接受。 每个 release()都添加了许可证,可能会释放阻止收购者。 但是,没有使用实际的许可对象; Semaphore只保留可用数量并相应地采取行动。信号量通常用于限制线程数,而不是访问某些(物理或逻辑)资源
通俗的来讲,信号量用来表示资源的数量,本质上就是一个计数器。acquire()申请资源,release()释放资源。例如创建多个线程去申请资源,就会不断地申请资源和时释放资源:
import java.util.concurrent.Semaphore;
import java.util.concurrent.TimeUnit;
public class Demo_1202_Semaphore {
public static void main(String[] args) {
// 创建信号量的对象, 并指定可用资源数量
Semaphore semaphore = new Semaphore(5);
Runnable runnable = new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + "开始申请资源...");
try {
// 申请资源
semaphore.acquire();
System.out.println(Thread.currentThread().getName() + "=====申请资源成功.");
// 持有一会
TimeUnit.SECONDS.sleep(1);
// 释放资源
semaphore.release();
System.out.println(Thread.currentThread().getName() + "释放资源.");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
};
// 创建多个线程去申请资源
for (int i = 0; i < 20; i++) {
Thread thread = new Thread(runnable);
// 启动线程
thread.start();
}
}
}
5.CountDonwLatch
同时等待N个任务执行结束。就例如跑步比赛,等所有人撞线之后,才开始公布成绩。
构造 CountDownLatch 实例, 初始化 10 表示有 10 个任务需要完成.
每个任务执行完毕, 都调用 latch.countDown() . 在 CountDownLatch 内部的计数器同时自减.
主线程中使用 latch.await(); 阻塞等待所有任务执行完毕. 相当于计数器为 0 了
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
public class Demo_1203_CountDownLatch {
public static void main(String[] args) throws InterruptedException {
// 多少个选手参赛
CountDownLatch countDownLatch = new CountDownLatch(10);
System.out.println("========= 所有选手各就各位 =============");
// 创建线程表示参赛选手
for (int i = 0; i < 10; i++) {
Thread thread = new Thread(() -> {
System.out.println(Thread.currentThread().getName() + "出发...");
try {
// 休眠一段时间,模拟从起点到终点的过程
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "到达终点.");
// 用这个方法来表示选手到达终点
countDownLatch.countDown();
});
// 启动线程,相当于开始比赛
thread.start();
}
// 等待比赛结束
countDownLatch.await();
// 当计数变为 0 的时候,继续执行后续操作
System.out.println("========== 比赛结束 ===========");
}
}
6.线程安全的集合类
在多线程的情况下,多个线程对同一个变量进行写操作的时候,就会出现线程不安全。
1.多线程环境使用ArrayList
使用一个普通的ArrayList在多线程环境下的操作:
package com.lihui;
import java.util.ArrayList;
import java.util.List;
public class Demo_1204 {
public static void main(String[] args) {
// 创建一个普通集合类
List<Integer> array = new ArrayList<>();
// 创建10个线程
for (int i = 0; i < 10; i++) {
int j = i;
// 在每个线程中分别对这个集合进行写入和读取
Thread thread = new Thread(() -> {
// 写
array.add(j);
// 读
System.out.println(array);
});
thread.start();
}
}
}
由于线程不安全,无法预知结果会是怎么样。
使用一个线程安全的Collections.synchronizedList(new ArrayList)来实现同样操作:
synchronizedList 是标准库提供的一个基于 synchronized 进行线程同步的 List.
synchronizedList 的关键操作上都带有 synchronized
package com.lihui;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
public class Demo_1205 {
public static void main(String[] args) {
// 创建一个普通集合类
List<Integer> array = new ArrayList<>();
// 把一个普通的集合类转换成一个线程安全的集合类
List<Integer> list = Collections.synchronizedList(array);
// 创建10个线程
for (int i = 0; i < 10; i++) {
int j = i;
// 在每个线程中分别对这个集合进行写入和读取
Thread thread = new Thread(() -> {
// 写
list.add(j);
// 读
System.out.println(list);
});
thread.start();
}
}
}
这就是我们预期的结果。
CopyOnWriteArrayList(写时复制技术)
其中原理为:如果有一个集合,在此时需要修改其中的数据,但它不会在原始集合修改,而是将原始集合复制一份,在新的集合修改,在此期间若有读操作,还是在原始集合进行读取,等待新集合写操作完成之后将数据集更新,此后使用新的数据集。
优点:对于读多写少的环境下效率非常之高,不需要加锁竞争
缺点:会消耗很多的资源,也不能保证第一时间读到最新的数据
2.多线程环境使用队列
ArrayBlockingQueue 基于数组实现的阻塞队列
LinkedBlockingQueue基于链表实现的阻塞队列
PriorityBlockingQueue基于堆实现的带优先级的阻塞队列
TransferQueue 最多只包含一个元素的阻塞队列
3.多线程环境使用哈希表
1.HashTable
HashTable是线程安全的,
但是这相当于直接给HashTable对象本身加锁
如果多个线程访问一个HashTable就会造成冲突
size属性也是通过Sychronized控制的,比较慢
一旦触发扩容, 就由该线程完成整个扩容过程. 这个过程会涉及到大量的元素拷贝, 效率会非常低
2.ConcurrentHashMap
相比于 Hashtable 做出了一系列的改进和优化:
读操作没有加锁(但是使用了 volatile 保证从内存读取结果), 只对写操作进行加锁. 加锁的方式仍然
是用 synchronized, 但是不是锁整个对象, 而是 "锁桶" (用每个链表的头结点作为锁对象), 大大降
低了锁冲突的概率.
充分利用 CAS 特性. 比如 size 属性通过 CAS 来更新. 避免出现重量级锁的情况.
优化了扩容方式: 化整为零
发现需要扩容的线程, 只需要创建一个新的数组, 同时只搬几个元素过去.
扩容期间, 新老数组同时存在.
后续每个来操作 ConcurrentHashMap 的线程, 都会参与搬家的过程. 每个操作负责搬运一小
部分元素.
搬完最后一个元素再把老数组删掉.
这个期间, 插入只往新数组加.
这个期间, 查找需要同时查新数组和老数组
7.死锁
在多线程中最严重的问题之一,死锁是这样一种情形:多个线程同时被阻塞,它们中的一个或者全部都在等待某个资源被释放。由于线程被无限期地阻塞,因此程序不可能正常终止。
典型案例——哲学家就餐问题。
假如五个哲学家吃一盘面,每两个人之间有一根筷子,规定:必须有两根筷子才可以吃面。此时五个人立马拿起筷子,但是每个人只有一根筷子,谁也不让谁,然后就形成僵局。这就是死锁。
造成死锁的原因
互斥使用:当资源被一个线程占用之后,其他线程就不能使用这个资源。
不可抢占:资源请求者不能从资源拥有者的手中抢占当前资源,只能等待拥有这释放之后再使用
请求和保持:当线程已经获取到锁A,还要继续获取锁B
循环等待:线程1等待线程2释放锁,线程2等待线程3释放锁,线程3等待线程1释放锁,此时形成一个回路。
以上四条是形成死锁的四个原因,必须同时满足,才可以形成死锁,也就是说只要打破一条,就不可能形成死锁
解决死锁问题
由于互斥访问与不可抢占是锁的基本特性,所以不能打破。
请求和保持:这个在于代码的设计与实现,可以改变
循环等待:这个是最容打破的条件而改变死锁
打破循环等待来解锁死锁
规定锁的编号,让使用锁时按照编号,保证不会出现回路,造成死锁。
可能产生回路的代码:
Object locker1 = new Object();
Object locker2 = new Object();
Thread t1 = new Thread(() ->{
synchronized (locker1){
synchronized (locker2){
}
}
});
t1.start();
Thread t2 = new Thread(() ->{
synchronized (locker2){
synchronized (locker1){
}
}
});
t2.start();
不会产生回路的代码:
Object locker1 = new Object();
Object locker2 = new Object();
Thread t1 = new Thread(() ->{
synchronized (locker1){
synchronized (locker2){
}
}
});
t1.start();
Thread t2 = new Thread(() ->{
synchronized (locker1){
synchronized (locker2){
}
}
});
t2.start();