1.简介
锁是用来控制多个线程访问共享资源的方式,一般来说,一个锁能够防止多个线程同时访问共享资源。
在Lock接口出现之前,java程序主要是靠synchronized关键字实现锁功能的,而JDK5之后,并发包中增加了lock接口,它提供了与 synchronized一样的锁功能。虽然它失去了像synchronize关键字隐式加锁解锁的便捷性,但是却拥有了锁获取和释放的可操作性,可中断的获取锁以及超时获取锁等多种synchronized关键字所不具备的同步特性。
通常使用lock的形式如下:
Lock lock = new ReentrantLock();
try{
lock.lock(); //设置当前线程同步状态,并在队列中保存线程和线程的同步状态
....... //设置成功往下执行
}finally{
lock.unlock();
}
需要注意的是synchronized同步块执行完成或者遇到异常是锁会自动释放,而lock必须调用unlock()方法释放锁, 因此在finally块中释放锁。
2.Lock接口API
void lock()
;-----获取锁
void lockInterruptibly() throws InterruptedException
----获取锁的过程能够响应中断(lock独有)
Boolean tryLock();
----非阻塞式响应中断,能立即返回,获取锁返回true反之为false
boolean tryLock(long time,TimeUnit unit);
----增加了超时等待机制,规定时间内未获取到锁,线程直接返回(lock独有)
void unlock();
----解锁
Condition newCondition();
----获取与lock绑定的等待通知组件,当前线程必须先获得了锁才能等待,等待会释
放锁,再次获取到锁才能从等待中返回。
Lock锁的实现原理---->AQS
3.AQS
全称:AbstractQueuedSynchronizer抽象队列同步器(Lock体系最核心)
3.1 AQS实现原理:双端队列保存线程和线程的同步状态,并通过CAS提供设置线程同步状态的方法。
如ReentranLock实现时,调用lock.lock()操作,会不停的设置线程同步状态。
AQS提供的模板方法可以分为3类:
- 独占式获取与释放同步状态;
- 共享式获取与释放同步状态;
- 查询同步队列中等待线程情况;
同步组件通过AQS提供的模板方法实现自己的同步语义
3.2 AQS的应用
- CountDownLatch
例:多线程并发问题三种解决方法
public class CountDownLatchTest {
public static void main(String[] args) throws InterruptedException {
Thread[] threads = new Thread[10];
CountDownLatch countDownLatch = new CountDownLatch(10); //给定初始值
for (int i = 0; i < 10; i++) {
final int j = i; //(匿名)内部类只能访问final变量
threads[i] = new Thread(new Runnable() {
@Override
public void run() {
System.out.println(j);
countDownLatch.countDown(); //运行一次-1;
}
});
}
//要求所有线程运行结束,主线程再打印
for (Thread t:threads) {
t.start();
}
/**
* 方式1:活跃线程数>1,main线程让步
*
while (Thread.activeCount() > 1){
Thread.yield();
}
*/
/**
* 方式2:join()
*
for (int i = 0; i < 10; i++) {
threads[i].join();
}
*/
/**
* 方式3:CountDownLatch
*/
countDownLatch.await(); //当前线程阻塞等待,直到CountDownLatch为0
System.out.println("所有线程运行结束");
}
}
0
1
2
3
4
5
6
7
8
9
所有线程运行结束
- Semaphore
场景:
(1)和CountDownLatch一致。
(2)多线程有限资源的同时访问
使用:
1.new Semaphore(int a): 给定a数量的初始信号量
2.acquire(int b):获取Semaphore中b数量的信号量。获取到,信号量减少,继续执行。获取不到,当前线程阻塞等待
3.release(int c):释放信号量,计数器增加c数量的信号量
例:模拟餐厅有2个窗口,但是有20个人要吃饭。
public class SemaphoreTest {
public static int count = 20;
public static int threadTotal = 2;
public static void main(String[] args) throws InterruptedException {
Semaphore semaphore = new Semaphore(threadTotal); // 初始信号量为0
CountDownLatch countDownLatch = new CountDownLatch(count);
for (int i = 0; i < count; i++) {
Thread thread = new Thread(()-> {
try {
Thread.sleep(50);
semaphore.acquire(1);
System.out.println("当前次饭线程:"+Thread.currentThread().getName());
countDownLatch.countDown();
}catch (InterruptedException e){
e.printStackTrace();
}finally {
semaphore.release(1);
}
});
thread.start();
}
countDownLatch.await();
System.out.println(Thread.currentThread().getName()+"结束,所有人次饭完毕");
}
}
当前次饭线程:Thread-2
当前次饭线程:Thread-1
当前次饭线程:Thread-0
当前次饭线程:Thread-11
当前次饭线程:Thread-10
当前次饭线程:Thread-9
当前次饭线程:Thread-8
当前次饭线程:Thread-7
当前次饭线程:Thread-6
当前次饭线程:Thread-5
当前次饭线程:Thread-4
当前次饭线程:Thread-3
当前次饭线程:Thread-13
当前次饭线程:Thread-12
当前次饭线程:Thread-14
当前次饭线程:Thread-15
当前次饭线程:Thread-18
当前次饭线程:Thread-19
当前次饭线程:Thread-17
当前次饭线程:Thread-16
main结束,所有人次饭完毕
- 线程池ThreadPoolExecutor
线程池多看看源码,会用就OK
使用线程池的三个优点如下:
- 降低资源消耗:通过重复利用已创建的线程降低线程创建和销毁带来的消耗。
- 提高响应速度:当任务到达时,任务可以不需要等待线程创建就能立即执行。
- 提高线程的可管理性:使用线程池可以统一进行线程分配、调度和监控。
线程池参数
public class ThreadPool {
public static void main(String[] args) {
ExecutorService pool = new ThreadPoolExecutor(
1, //核心线程数
2, //最大线程数
3, //时间数量
TimeUnit.MILLISECONDS, //时间单位
//时间数量+时间单位决定了临时线程的存活时间
new ArrayBlockingQueue<>(100), //阻塞队列
new ThreadFactory() { //线程池创建Thread线程的工厂类,如果没有提供,就使用线程内部默认的创建线程的方法
@Override
public Thread newThread(Runnable r) {
return null;
}
}, new ThreadPoolExecutor.CallerRunsPolicy()); // 拒绝策略(4种)
/* RejectedExecutionHandler rejected = null;
rejected = new ThreadPoolExecutor.AbortPolicy();//默认,队列满了丢任务抛出异常
rejected = new ThreadPoolExecutor.DiscardPolicy();//队列满了丢任务不异常
rejected = new ThreadPoolExecutor.DiscardOldestPolicy();//将最早进入队列的任务删,之后再尝试加入队列
rejected = new ThreadPoolExecutor.CallerRunsPolicy();//如果添加到线程池失败,那么主线程会自己去执行该任务
*/
}
}
向线程池提交任务
可以使用两个方法向线程池提交任务,分别为execute()
和submit()
方法。
execute()
方法用于提交不需要返回值的任务,所以无法判断任务是否被线程池执行成功。
例:使用execute()方法(传入实现Runnable的对象)
package ThreadPool;
import java.util.concurrent.*;
class RunnableThread implements Runnable {
@Override
public void run() {
for (int i = 0; i < 20; i++) {
System.out.println(Thread.currentThread().getName() + "、" + i);
}
}
}
public class execute{
public static void main(String[] args){
RunnableThread runnableThread = new RunnableThread();
ThreadPoolExecutor threadPoolExecutor =
new ThreadPoolExecutor(3,5,2000,TimeUnit.MILLISECONDS,
new LinkedBlockingDeque<Runnable>());
for (int i = 0; i < 5; i++) {
threadPoolExecutor.execute(runnableThread);
}
}
}
submit()
方法用于提交需要返回值的任务。线程池会返回一个future类型的对象,通过这个future对象可以判断任务是否执行成功,并且可以通过future的get()方法来获取返回值
get()方法会阻塞当前线程直到任务完成,而get(long timeout,TimeUnit unit)方法则会阻塞当前线程一段时间后立即返回,这时候有可能任务没有执行完。
例:使用submit()方法(传入实现Callable的对象)
import java.util.concurrent.*;
class CallableThread implements Callable<String> {
@Override
public String call() throws Exception {
for (int i = 0; i < 20; i++) {
System.out.println(Thread.currentThread().getName() + "、" + i);
}
return Thread.currentThread().getName()+"任务执行完毕";
}
}
public class submit {
public static void main(String[] args){
CallableThread callableThread = new CallableThread();
ThreadPoolExecutor threadPoolExecutor =
new ThreadPoolExecutor(3,5,2000,TimeUnit.MILLISECONDS,
new LinkedBlockingDeque<Runnable>());
for (int i = 0; i < 5; i++) {
Future<String> future = threadPoolExecutor.submit(callableThread);
try {
String str = future.get();
System.out.println(str);
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}
}
}
}
4.Condition
任何一个java对象都继承于Object类,在线程间实现通信的往往会应用到Object的几个方法,比如 wait()
与notify(),notifyAll()
几个方法实现等待/通知机制。
同样的, 在 java Lock体系下依然会有同样的方法实现等待/通知机制。
区别:从整体上来看Object的wait和notify/notify是与对象监视器配合完成线程间的等待/通知机制,而Condition与Lock配合完成等待通知机制,前者是java底层级别的,后者是语言级别的,具有更高的可控制性和扩展性。
两者除了在使用方式上不同外,在功能特性上还是有很多的不同:
- Condition能够支持不响应中断,而通过使用Object方式不支持;
- Condition能够支持多个等待队列(new 多个Condition对象),而Object方式只能支持一个;
- Condition能够支持超时时间的设置,而Object不支持。
使用:
(1)通过锁对象.new Condition()获取Condition对象
(2)Condition对象.await(),让当前对象阻塞等待,并释放锁(=synchronized锁对象.wait())
(3)Condition对象.signal/signalAll(),通知之前await阻塞的线程(=synchronized锁对象.notify/notifyAll())
具体例子:
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class LockTest {
public static Lock LOCK = new ReentrantLock();
public static Condition CONDITION = LOCK.newCondition(); //线程通信
public static void t3(){
try {
LOCK.lock();
//执行业务
}finally {
LOCK.unlock();
}
}
public static void t4(){
try {
LOCK.lock();
t3(); //Reentrant关键字的Lock包下的API:可重入锁
//执行业务
}finally {
LOCK.unlock();
}
}
public static void t5() { //生产者消费者模型
try {
LOCK.lock(); //=synchronized()加锁的代码
// //执行业务
// while (库存达到上限){
// CONDITION.await(); //=synchronized锁对象.wait();
// }
// CONDITION.signal(); //=synchronized锁对象.notify();
// CONDITION.signalAll();//=synchronized锁对象.notifyAll();
}finally {
LOCK.unlock();
}
}
}
5.ReentrantReadWriteLock - 读写锁
实现Lock接口的 ReentrantReadWriteLock - 提供的读写锁API (WirteLock和ReadLock)
场景:读写锁允许同一时刻被多个读线程访问,但是在写线程访问时,所有的读线程和其他的写线程都会被阻塞
使用:
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
public class ReadWriteLockTest {
//ReadWriteLock为接口,ReentrantReadWriteLock为实现类
private static ReadWriteLock LOCK = new ReentrantReadWriteLock();
private static Lock READ_LOCK = LOCK.readLock();
private static Lock WRITE_LOCK = LOCK.writeLock();
public static void readFile(){ //读操作加锁
try {
READ_LOCK.lock();
//IO读取
}finally {
READ_LOCK.unlock();
}
}
public static void writeFile(){ //写操作加锁
try {
WRITE_LOCK.lock();
//IO写
}finally {
WRITE_LOCK.unlock();
}
}
}
读写锁总结:
- 公平性选择:支持非公平性(默认)和公平的锁获取方式,吞吐量还是非公平优于公平;
- 重入性:支持重入,读锁获取后能再次获取,写锁获取之后能够再次获取写锁,同时也能够获取读锁;
- 锁只能降级:遵循获取写锁,获取读锁再释放写锁的次序。写锁能够降级成为读锁 ,读锁不能升级为写锁
优点:针对读读并发执行,提高运行效率。
6.Lock锁特点
- 提供两种锁:公平锁和非公平锁。
如果一个锁是公平的,那么锁的获取顺序就应该符合请求上的绝对时间顺序,满足FIFO。
ReentrantLock的构造方法无参时是构造非公平锁。另外还提供了另外一种方式,可传入一个boolean值,true时为公平锁,false时为非公平锁
- AQS提供独占式获取同步状态,共享式获取同步状态(获取锁的本质是设置线程同步状态)
独占式:只允许一个线程获取到锁(独占锁)
共享式:一定数量的线程共享式获取锁(共享锁) - 带Reentrant关键字的Lock包下的API:可重入锁(允许多次获取同一个对象的锁)