前段时间有事情,已经好久没有更新过了,现在我们继续来看看多线程的最后一部分
ReentrantLock
前面我们讲了synchronized关键字
来加锁,但这种锁一是很重,二是获取时必须一直等待,没有额外的尝试机制
java.util.concurrent.locks包提供的ReentrantLock
用于替代synchronized加锁,我们来看一下传统的synchronized代码:
public class Test{
private int count;
public void add(int n) {
synchronized(this) {
count += n;
}
}
}
如果用ReentrantLock替代,可以把代码改造为:
public class Test{
private final Lock lock = new ReentrantLock();
private int count;
public void add(int n) {
lock.lock();
try {
count += n;
} finally {
lock.unlock();
}
}
}
ReentrantLock
实现的锁,我们必须先获取锁,然后在finally
中正确释放锁。
顾名思义,ReentrantLock是可重入锁
,它和synchronized一样,一个线程可以多次获取同一个锁
。
和synchronized不同的是
,ReentrantLock可以尝试获取锁
:
if (lock.tryLock(1, TimeUnit.SECONDS)) {
try {
...
} finally {
lock.unlock();
}
}
上述代码在尝试获取锁的时候,最多等待1秒。如果1秒后仍未获取到锁,
tryLock()
返回false
,程序就可以做一些额外处理,而不是无限等待下去。
所以,使用ReentrantLock比直接使用synchronized更安全,线程在tryLock()失败的时候不会导致死锁。
小结:
- ReentrantLock可以替代synchronized进行同步;
- ReentrantLock获取锁更安全;
- 必须先获取到锁,再进入try {…}代码块,最后使用finally保证释放锁;
- 可以使用tryLock()尝试获取锁。
Condition
synchronized
可以配合wait和notify
实现线程在条件不满足时等待,条件满足时唤醒,用ReentrantLock
我们怎么编写wait和notify
的功能呢?
答案是使用Condition对象
来实现wait和notify的功能。
class TaskQueue {
private final Lock lock = new ReentrantLock();
private final Condition condition = lock.newCondition();
private Queue<String> queue = new LinkedList<>();
public void addTask(String s) {
lock.lock();
try {
queue.add(s);
condition.signalAll();
} finally {
lock.unlock();
}
}
public String getTask() {
lock.lock();
try {
while (queue.isEmpty()) {
condition.await();
}
return queue.remove();
} finally {
lock.unlock();
}
}
}
使用Condition
时,引用的Condition对象必须从Lock实例
的newCondition()
返回,这样才能获得一个绑定了Lock实例的Condition实例。
Condition
提供的await()、signal()、signalAll()
原理和synchronized
锁对象的wait()、notify()、notifyAll()
是一致的,并且其行为也是一样的:
- await()会释放当前锁,进入等待状态;
- signal()会唤醒某个等待线程;
- signalAll()会唤醒所有等待线程;
- 唤醒线程从await()返回后需要重新获得锁。
对于锁,还有悲观锁和乐观锁,大家可以去看看这位大佬写的博客,很详细很有用。传送门
线程池
当一个程序中若创建大量线程,并在任务结束之后销毁,这样会给系统带来过度消耗资源,以及过度切换线程的危险,从而可能导致系统崩溃。那我们要怎么解决呢?这时我们便可以使用线程池
来解决这个问题。
那什么是线程池呢?线程池的作用是什么呢?我们要怎么创造线程池呢?
什么是线程池
首先创建一些线程,他们的集合成为线程池,当服务器接受到一个客户的请求之后,就从线程池中取出一个空闲的线程为之服务,服务完后不关闭该线程,而是将该线程还回到线程池中
。
在线程池的编程模式下,任务是提交给整个线程池
,而不是直接交给某个线程,线程池拿到任务之后,他就在内部找有无空闲的线程
,再把任务交给内部某个空闲的线程,任务是提交给整个线程池,一个线程同时只能执行一个任务,但可以同时向一个线程池提交多个任务
。
线程池大家可以将它当做为美团平台,其中的线程为骑手。当平台接到客户的订单之后(
任务是提交给整个线程池
),他就会找看看有哪个骑手是空闲的(在内部找有无空闲的线程
),找到之后,就把订单分给这个骑手。骑手接完一单之后肯定不会下班的吧,人家还要继续赚钱(这就是服务完之后不关闭该线程
),一个骑手一次只能接一次单,接完一单才能接下一单,骑手在配送中,客户可能会叫骑手顺路买点东西的吧,这时骑手为了五星好评,只要不是太难,基本都会答应(一个线程同时只能执行一个任务,但可以同时向一个线程池提交多个任务
)。
线程池的作用
线程池有两个主要的作用:
- 控制线程数量
- 重用线程
控制线程数量呢,就是控制一下骑手的数量,不可能所有人都成为骑手吧,这样就乱套了,所以得控制一下数量。重用线程呢,每个骑手不可能上了一天的班就辞职是吧,他们得赚钱,得养家。
线程池的实现
Java标准库提供了ExecutorService
接口表示线程池
线程池有以下几个实现方法:
- 创建一个可重用的固定线程集合的线程池,以共享的无界队列方式来运行这些线程(
线程数固定的线程池
):
Executors.newFixedThreadPool(int nThreads);
我们来看一个小栗子:
import java.util.concurrent.*;
public class Main {
public static void main(String[] args) {
// 创建一个固定大小为4的线程池:
ExecutorService es = Executors.newFixedThreadPool(4);
for (int i = 0; i < 6; i++) {
es.submit(new Task("" + i));
}
// 关闭线程池:
es.shutdown();
}
}
class Task implements Runnable {
private final String name;
public Task(String name) {
this.name = name;
}
@Override
public void run() {
System.out.println("start task " + name);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
}
System.out.println("end task " + name);
}
}
我们一次性放入6个任务,由于线程池只有固定的4个线程,因此,前4个任务会同时执行,等到有线程空闲后,才会执行后面的两个任务
这里说一下:
线程池在程序结束的时候要关闭。使用shutdown()
方法关闭线程池的时候,它会等待正在执行的任务先完成,然后再关闭。shutdownNow()会立刻停止正在执行的任务
,awaitTermination()则会等待指定的时间让线程池关闭
。
- 创建一个可根据需要创建新线程的线程池,但是在以前构造的线程可用时将重用他们(
线程数根据任务动态调整的线程池
):
Executors.newCashedThreadPool();
如果我们把线程池改为CachedThreadPool
,由于这个线程池的实现会根据任务数量动态调整线程池的大小,所以6个任务可一次性全部同时执行。
如果我们想把线程池的大小限制在4~10个之间动态调整怎么办?我们查看Executors.newCachedThreadPool()方法的源码:
public static ExecutorService newCachedThreadPool() {
return new ThreadPoolExecutor(0, Integer.MAX_VALUE,60L, TimeUnit.SECONDS,new SynchronousQueue<Runnable>());
}
因此,想创建指定动态范围的线程池,可以这么写:
int min = 4;
int max = 10;
ExecutorService es = new ThreadPoolExecutor(min, max,
60L, TimeUnit.SECONDS, new SynchronousQueue<Runnable>());
- 创建一个线程池,他可安排给定延迟后运行命令或者定期地执行:
Executors.newScheduledThreadPool(int corePoolSize);
ScheduledThreadPool需要定期反复执行,例如,每秒刷新证券价格。这种任务本身固定,需要反复执行的
创建一个ScheduledThreadPool仍然是通过Executors类:
ScheduledExecutorService ses = Executors.newScheduledThreadPool(4);
对于ScheduledThreadPool的理解,大家可以看看这篇博客传送门
- 创建一个使用单个worker线程的Executor,以无界队列方式来运行该线程:
Executor.newSingleThreadExecutor();
使用BlockingQueue
对于BlockingQueue双缓冲队列,因为知识点有点多,我就不写了,以后有时间的话我会写一下。这里给大家推荐几篇关于BlockingQueue的博客,
传送门1
传送门2
对于多线程的其他小知识,大家可以去我的主页看看,算上这篇总共写了四篇。