一,Lock
Lock锁实现了比使用同步锁可以获得更广泛的锁条件。他们允许更灵活的结构,可能具有非常不同的属性,并且可能支持多个关联条件对象。Lock提供了比synchronized更多的功能。
1.1,Lock与Synchronize区别
- Lock不是java语言内置的(也就是使用的时候需要导包),而synchronized则是Java语言的关键字,因此是内置特性。Lock是一个类,通过这个类可以是实现同步访问:
- Lock和synchronized最大的不同是,采用synchronized不需要用户去手动释放锁,当synchronized方法或者代码块执行完后自动让线程释放对锁的占用;而Lock不同,必须要求用户手动的去释放锁,如果没有手动的释放锁,就会导致死锁的情况。
- Lock可以让等待锁的线程响应中断,而synchronized却不行,使用synchronized时,等待的线程会一致等待下去,不能够响应中断。
- 通过Lock可以知道有没有成功获取锁,而synchronized却无法办到。
- Lock可以提高多个线程进行读写操作的效率。
1.2,Lock接口的使用
-
创建一个可重入锁(自己可以上锁也可以解锁)
//括号里的参数表示是否采用公平的排序策略 private final ReentrantLock lock = new ReentrantLock(true);
-
在我们需要的时候进行加锁
lock.lock();
-
操作完成后进行释放锁(最好放在try catch代码块的finally中,这样子他就肯定可以解锁,防止出现死锁)
lock.unlock();
package day01.sync;
import java.util.concurrent.locks.ReentrantLock;
/**
* @version v1.0
* @ProjectName: JUC
* @ClassName: LSaleTicket
* @Description: 使用Lock实现卖票
* @Author: 赵先生
* @Date: 2022/1/22 15:59
* lock使用方式
* 1,创建一个可重入锁(自己可以上锁也可以解锁)ReentrantLock
*/
/**
* 第一步 创建资源类,定义属性和和操作方法
*/
class LTicket {
/**
* 票数量
*/
private int number = 30;
/**
* 创建可重入锁
*/
private final ReentrantLock lock = new ReentrantLock(true);
/**
* 卖票方法
*/
public void sale() {
//上锁
lock.lock();
try {
//判断是否有票
if(number > 0) {
System.out.println(Thread.currentThread().getName()+" :卖出"+(number--)+" 剩余:"+number);
}
} finally {
//解锁
lock.unlock();
}
}
}
public class LSaleTicket {
public static void main(String[] args) {
/**
* 第二步穿点多个线程,调用资源类的操作
*/
LTicket ticket = new LTicket();
new Thread(()-> {
for (int i = 0; i < 40; i++) {
ticket.sale();
}
},"AA").start();
new Thread(()-> {
for (int i = 0; i < 40; i++) {
ticket.sale();
}
},"BB").start();
new Thread(()-> {
for (int i = 0; i < 40; i++) {
ticket.sale();
}
},"CC").start();
}
}
1.3,Lock方法
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()
方法用来获取锁
- 如果锁已被其他线程获取,则进行等待
- 发生异常不会自动解锁,需用在 try{}catch{}块中进行
lock.lock();
try{
//处理任务
}catch(Exception ex){
}finally{
lock.unlock(); //释放锁
}
Condition 类
也可以实现等待/通知模式
- 关键字 synchronized 与 wait()/notify()这两个方法一起使用可以实现等待/通知模式,Lock 锁的 newContition()方法返回 Condition 对象,Condition 类也可以实现等待/通知模式
- 用 notify()通知时,JVM 会随机唤醒某个等待的线程, 使用 Condition 类可以进行选择性通知, Condition 比较常用的两个方法:
- await()会使当前线程等待,同时会释放锁,当其他线程调用 signal()时,线程会重新获得锁并继续执行
- signal()用于唤醒一个等待的线程
ReentrantLock 是唯一实现了 Lock 接口的类,并且 ReentrantLock 提供了更多的方法
ReentrantLock
可重入锁
- ReentrantReadWriteLock 里面提供了很多丰富的方法,不过最主要的有两个方法:readLock()和 writeLock()用来获取读锁和写
writeLock();
来获取读锁readLock();
获取写锁- 注意读写锁的关系:读锁时候共享型的,写锁是独占型的。读锁可以在没有写锁的时候被多个线程同时持有,写锁是独占的。同时当有读锁时,写锁就不能获得;而当有写锁时,除了获得写锁的这个线程可以获得读锁外,其他线程不能获得读锁
二,线程间通信
2.1,概述
- 线程间通信的模型有两种:共享内存和消息传递
- 线程间的通信具体步骤:(涉及上中下部)
- 创建资源类,在资源类中船舰属性和操作方法
- 在资源类操作方法:判断、操作、通知
- 创建多个线程,调用资源类的操作方法
- 防止虚拟唤醒问题(在判断中使用while)
2.2,synchronized案例
操作线程的时候,等待线程使用wait()
通知另外的线程操作用notify()
、notifyAll()
假设有两个线程,该线程在执行过程中,判断值(不是该值等待,让其他线程抢),操作值,通知另外一个线程的调度
通过使用两个线程对0这个值操作,一个线程加1,一个线程减1,交替实现多次
//第一步 创建资源类,定义属性和操作方法
class Share {
//初始值
private int number = 0;
//+1的方法
public synchronized void incr() throws InterruptedException {
//第二步 判断 干活 通知
if(number != 0) { //判断number值是否是0,如果不是0,等待
this.wait(); //在哪里睡,就在哪里醒
}
//如果number值是0,就+1操作
number++;
System.out.println(Thread.currentThread().getName()+" :: "+number);
//通知其他线程
this.notifyAll();
}
//-1的方法
public synchronized void decr() throws InterruptedException {
//判断
if(number != 1) {
this.wait();
}
//干活
number--;
System.out.println(Thread.currentThread().getName()+" :: "+number);
//通知其他线程
this.notifyAll();
}
}
public class ThreadDemo1 {
//第三步 创建多个线程,调用资源类的操作方法
public static void main(String[] args) {
Share share = new Share();
//创建线程
new Thread(()->{
for (int i = 1; i <=10; i++) {
try {
share.incr(); //+1
} catch (InterruptedException e) {
e.printStackTrace();
}
}
},"AA").start();
new Thread(()->{
for (int i = 1; i <=10; i++) {
try {
share.decr(); //-1
} catch (InterruptedException e) {
e.printStackTrace();
}
}
},"BB").start();
}
}
但是当我们再加两个线程的时候就会发现问题:
package day01;
/**
* @version v1.0
* @ProjectName: JUC
* @ClassName: ThreadDemo2
* @Description: 线程的通信(使用synchronized实现)
* @Author: 赵先生
* @Date: 2022/1/22 16:36
*/
class Share {
/**
* 初始值
*/
private int number = 0;
/**
* +1的方法
* @throws InterruptedException
*/
public synchronized void incr() throws InterruptedException {
//第二步 判断 干活 通知
//判断number值是否是0,如果不是0,等待
while(number != 0) {
//在哪里睡,就在哪里醒
this.wait();
}
//如果number值是0,就+1操作
number++;
System.out.println(Thread.currentThread().getName()+" :: "+number);
//通知其他线程
this.notifyAll();
}
/**
* -1的方法
* @throws InterruptedException
*/
public synchronized void decr() throws InterruptedException {
//判断
while(number != 1) {
this.wait();
}
//干活
number--;
System.out.println(Thread.currentThread().getName()+" :: "+number);
//通知其他线程
this.notifyAll();
}
}
public class ThreadDemo2 {
//第三步 创建多个线程,调用资源类的操作方法
public static void main(String[] args) {
Share share = new Share();
//创建线程
new Thread(()->{
for (int i = 1; i <=10; i++) {
try {
share.incr(); //+1
} catch (InterruptedException e) {
e.printStackTrace();
}
}
},"AA").start();
new Thread(()->{
for (int i = 1; i <=10; i++) {
try {
share.decr(); //-1
} catch (InterruptedException e) {
e.printStackTrace();
}
}
},"BB").start();
new Thread(()->{
for (int i = 1; i <=10; i++) {
try {
share.incr(); //+1
} catch (InterruptedException e) {
e.printStackTrace();
}
}
},"CC").start();
new Thread(()->{
for (int i = 1; i <=10; i++) {
try {
share.decr(); //-1
} catch (InterruptedException e) {
e.printStackTrace();
}
}
},"DD").start();
}
}
这个就是由于虚假唤醒的原因。因为我们wait的特性,就是在哪里等待,下回就在哪里苏醒。这样就会产生问题,就是当一个线程多次被调用的时候就会跳过判断的过程,从而进行了错误的执行。所以解决办法就是将if
变成while
,通过循环的方式就不会发生之前的情况。
2.3,Lock案例
使用lock先要创建锁的对象以及通知的对象
放置在资源类中
//创建Lock
private Lock lock = new ReentrantLock();
private Condition condition = lock.newCondition();
上锁lock.lock();
解锁lock.unlock();
以下都为condition类:
唤醒所有等待的线程signalAll()
,带上类名condition.signalAll();
唤醒一个等待线程signal()
,带上类名,condition.signal();
造成当前线程在接到信号或者被中断之前一直处于等待状态await()
,带上类名,condition.await();
package day01.sync;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
/**
* @version v1.0
* @ProjectName: JUC
* @ClassName: ThreadDemo2
* @Description: 请描述该类的功能
* @Author: 赵先生
* @Date: 2022/1/22 16:55
*/
//第一步 创建资源类,定义属性和操作方法
class Share {
private int number = 0;
//创建Lock
private Lock lock = new ReentrantLock();
private Condition condition = lock.newCondition();
//+1
public void incr() throws InterruptedException {
//上锁
lock.lock();
try {
//判断
while (number != 0) {
condition.await();
}
//干活
number++;
System.out.println(Thread.currentThread().getName()+" :: "+number);
//通知
condition.signalAll();
}finally {
//解锁
lock.unlock();
}
}
//-1
public void decr() throws InterruptedException {
lock.lock();
try {
while(number != 1) {
condition.await();
}
number--;
System.out.println(Thread.currentThread().getName()+" :: "+number);
condition.signalAll();
}finally {
lock.unlock();
}
}
}
public class ThreadDemo2 {
public static void main(String[] args) {
Share share = new Share();
new Thread(()->{
for (int i = 1; i <=10; i++) {
try {
share.incr();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
},"AA").start();
new Thread(()->{
for (int i = 1; i <=10; i++) {
try {
share.decr();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
},"BB").start();
new Thread(()->{
for (int i = 1; i <=10; i++) {
try {
share.incr();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
},"CC").start();
new Thread(()->{
for (int i = 1; i <=10; i++) {
try {
share.decr();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
},"DD").start();
}
}
三,线程间定制化通信
所谓定制化通信,需要让线程进行一定的顺序操作
实现的思路:创建不同的开锁信息,让每个线程完成它们自己的任务,并将执行的顺序存放到资源类的方法中。
案列:启动三个线程,按照如下要求:
AA打印5此,BB打印10次,CC打印15次,一共进行10轮
具体思路:
每个线程添加一个标志位,是该标志位则执行操作,并且修改为下一个标志位,通知下一个标志位的线程
创建一个可重入锁private Lock lock = new ReentrantLock();
分别创建三个开锁通知private Condition c1 = lock.newCondition();
具体资源类中的A线程代码操作
上锁,(执行具体操作(判断、操作、通知),解锁)放于try、finally
public void print5(int loop) throws InterruptedException {
//上锁
lock.lock();
try {
//判断
while(flag != 1) {
//等待
c1.await();
}
//干活
for (int i = 1; i <=5; i++) {
System.out.println(Thread.currentThread().getName()+" :: "+i+" :轮数:"+loop);
}
//通知
flag = 2; //修改标志位 2
c2.signal(); //通知BB线程
}finally {
//释放锁
lock.unlock();
}
}
总代码如下:
package day02;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
/**
* @version v1.0
* @ProjectName: JUC
* @ClassName: ThreadDemo3
* @Description: 线程间定制化通信
* @Author: 赵先生
* @Date: 2022/1/23 15:36
*/
class ShareResource {
/**
* 定义标志位,当flag为1时是AA线程进行操作,2时是BB线程操作,3时是CC线程进行操作
*/
private int flag = 1;
/**
* 创建一个可重入锁
*/
private Lock lock = new ReentrantLock();
/**
* 创建三个condition
*/
private Condition c1 = lock.newCondition();
private Condition c2 = lock.newCondition();
private Condition c3 = lock.newCondition();
public void print5(int loop) {
lock.lock();
try {
//判断
while(flag != 1) {
//等待
c1.await();
}
//干活
for (int i = 1; i <=5; i++) {
System.out.println(Thread.currentThread().getName()+" :: "+i+" :轮数:"+loop);
}
//通知
//修改标志位 2
flag = 2;
//通知BB线程
c2.signal();
} catch (InterruptedException e) {
e.printStackTrace();
} finally{
lock.unlock();
}
}
public void print10(int loop) throws InterruptedException {
lock.lock();
try {
while(flag != 2) {
c2.await();
}
for (int i = 1; i <=10; i++) {
System.out.println(Thread.currentThread().getName()+" :: "+i+" :轮数:"+loop);
}
//修改标志位
flag = 3;
//通知CC线程
c3.signal();
}finally {
lock.unlock();
}
}
/**
* 打印15次,参数第几轮
* @param loop
* @throws InterruptedException
*/
public void print15(int loop) throws InterruptedException {
lock.lock();
try {
while(flag != 3) {
c3.await();
}
for (int i = 1; i <=15; i++) {
System.out.println(Thread.currentThread().getName()+" :: "+i+" :轮数:"+loop);
}
//修改标志位
flag = 1;
//通知AA线程
c1.signal();
}finally {
lock.unlock();
}
}
}
public class ThreadDemo3 {
public static void main(String[] args) {
ShareResource shareResource = new ShareResource();
new Thread(()->{
for (int i = 1; i <=10; i++) {
try {
shareResource.print5(i);
} catch (Exception e) {
e.printStackTrace();
}
}
},"AA").start();
new Thread(()->{
for (int i = 1; i <=10; i++) {
try {
shareResource.print10(i);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
},"BB").start();
new Thread(()->{
for (int i = 1; i <=10; i++) {
try {
shareResource.print15(i);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
},"CC").start();
}
}
四,集合的线程安全
以下是通过list集合去演示并发修改异常。
代码如下:
package day02;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;
/**
* @version v1.0
* @ProjectName: JUC
* @ClassName: ThreadDemo4
* @Description: 线程不安全(并发修改异常)
* @Author: 赵先生
* @Date: 2022/1/23 16:12
*/
public class ThreadDemo4 {
public static void main(String[] args) {
//创建ArrayList集合
List<String> list = new ArrayList<>();
for (int i = 0; i <30; i++) {
new Thread(()->{
//向集合添加内容
list.add(UUID.randomUUID().toString().substring(0,8));
//从集合获取内容
System.out.println(list);
},String.valueOf(i)).start();
}
}
}
java.util.ConcurrentModificationException
为并发修改问题
产生的原因:
当点开list
的代码时,我们发现他的add方法并没有加锁,这就意味着别的线程可以随时加入这个方法,导致多个线程一起修改值
下面提供解决办法:
4.1 Vector
通过list下的实现类Vector
因为在Vector下的add普遍都是线程安全
查看源代码:
public synchronized boolean add(E e) {
modCount++;
add(e, elementData, elementCount);
return true;
}
在改变代码时,只需要将其修改为List<String> list = new Vector<>();
但此方法用的比较少,因为在jdk 1.0的版本适用,效率低。
4.2 Collections
Collections类中的很多方法都是static静态
其中有一个方法是返回指定列表支持的同步(线程安全的)列表为synchronizedList(List <T> list)
具体代码为:
List<String> list = Collections.synchronizedList(new ArrayList<>());
4.3 CopyOnWriteArrayList
将其代码修改为:
List<String> list = new CopyOnWriteArrayList<>();
涉及的底层原理为写时复制技术:
- 读的时候是并发的,但是写的时候是独立的
- 具体过程就是,当我们有新的内容的时候就需要将原本的list集合复制一份,在新的集合中进行写的操作,等写完之后在将其合并
代码如下:
public boolean add(E e) {
final ReentrantLock lock = this.lock;
//加锁
lock.lock();
try {
//获取之前的数组
Object[] elements = getArray();
//计算出他的长度
int len = elements.length;
//复制一份新的数组,其长度比之前长一位
Object[] newElements = Arrays.copyOf(elements, len + 1);
//将最后一个位置放入需要添加的元素e
newElements[len] = e;
//以后获取数组就用新的数组
setArray(newElements);
return true;
} finally {
//解锁
lock.unlock();
}
}
4.4 CopyOnWriteArraySet
该类是HashSet的实现类
同样使用HashSet类,也会出现线程不安全
java Set<String> set = new HashSet<>();
需要将上面的代码改为
Set<String> set = new CopyOnWriteArraySet<>();
具体操作代码为
//演示HashSet
//HashSet<String> set = new HashSet<>();
Set<String> set = new CopyOnWriteArraySet<>();
for (int i = 0; i <30; i++) {
new Thread(()->{
//向集合添加内容
set.add(UUID.randomUUID().toString().substring(0,8));
//从集合获取内容
System.out.println(set);
},String.valueOf(i)).start();
}
4.5 ConcurrentHashMap
ConcurrentHashMap
类是HashMap的实现类
先讲述其线程不安全实列
HashMap不安全线程也同理Map<String,String> map = new HashMap<>();
具体实现代码是
for (int i = 0; i <30; i++) {
String key = String.valueOf(i);
new Thread(()->{
//向集合添加内容
map.put(key,UUID.randomUUID().toString().substring(0,8));
//从集合获取内容
System.out.println(map);
},String.valueOf(i)).start();
}
将其代码修改为
Map<String,String> map = new ConcurrentHashMap<>();
通过这行代码可以编程线程安全
五,多线程锁
某一个时刻内,只能有唯一一个线程去访问这些synchronized 方法
所有的静态同步方法用的也是同一把锁——类对象本身,这两把锁是两个不同的对象,所以静态同步方法与非静态同步方法之间是不会有竞态条件的但是一旦一个静态同步方法获取锁后,其他的静态同步方法都必须等待该方法释放锁后才能获取锁,而不管是同一个实例对象的静态同步方法之间,还是不同的实例对象的静态同步方法之间,只要它们同一个类的实例对象。换句话将,静态方法的的锁就好比一顿楼大门上的锁,而普通方法上的锁就不一样了,相当于这顿楼内部房间的锁,大门上锁并不影响里面的锁的状态。
- synchronized锁的是方法,则是对象锁
- 同个对象锁的机制要等待,不同对象锁的机制调用同一个不用等待
- 加了static则为class锁而不是对象锁
通过具体的实例进行分析
两个线程安全方法一个普通方法
class Phone {
public synchronized void sendSMS() throws Exception {
//停留4秒
TimeUnit.SECONDS.sleep(4);
System.out.println("------sendSMS");
}
public synchronized void sendEmail() throws Exception {
System.out.println("------sendEmail");
}
public void getHello() {
System.out.println("------getHello");
}
}
public class Lock_8 {
public static void main(String[] args) throws Exception {
Phone phone = new Phone();
Phone phone2 = new Phone();
new Thread(() -> {
try {
phone.sendSMS();
} catch (Exception e) {
e.printStackTrace();
}
}, "AA").start();
Thread.sleep(100);
new Thread(() -> {
try {
// phone.sendEmail();
// phone.getHello();
phone2.sendEmail();
} catch (Exception e) {
e.printStackTrace();
}
}, "BB").start();
}
}
具体八种情况为:
1 标准访问,先打印短信还是邮件
------sendSMS
------sendEmail
2 停4秒在短信方法内,先打印短信还是邮件
------sendSMS
------sendEmail
3 新增普通的hello方法,是先打短信还是hello
------getHello
------sendSMS
4 现在有两部手机,先打印短信还是邮件
------sendEmail
------sendSMS
5 两个静态同步方法,1部手机,先打印短信还是邮件
------sendSMS
------sendEmail
6 两个静态同步方法,2部手机,先打印短信还是邮件
------sendSMS
------sendEmail
7 1个静态同步方法,1个普通同步方法,1部手机,先打印短信还是邮件
------sendEmail
------sendSMS
8 1个静态同步方法,1个普通同步方法,2部手机,先打印短信还是邮件
------sendEmail
------sendSMS
总结:
- 同样的对象访问不同的同步锁,是按照顺序执行
- 同样的对象访问同步锁与不同步锁,是先不同步锁执行,即就是先没有锁的方法先执行
- 不同对象访问不同步锁,按照顺序执行
- 同一对象访问不同静态同步锁,按照顺序执行
- 不同对象访问不同静态同步锁,也要按照顺序执行(这是因为他锁的对象是这个类)
- 同一对象访问一个静态同步锁,一个同步锁,也是按照顺序执行的,因为他们不是一把锁
- 不同对象访问一个静态同步锁,一个同步锁,也是按照顺序执行
5.2,公平锁和非公平锁
- 公平锁:效率相对低,但是雨露均沾
- 非公平锁:效率高,但是线程容易饿死
通过查看源码
带有参数的ReentrantLock(true)
为公平锁
ReentrantLock(false)
为非公平锁
主要是调用NonfairSync()
与FairSync()
public ReentrantLock() {
sync = new NonfairSync();
}
/**
* Creates an instance of {@code ReentrantLock} with the
* given fairness policy.
*
* @param fair {@code true} if this lock should use a fair ordering policy
*/
public ReentrantLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
}
static final class FairSync extends Sync {
private static final long serialVersionUID = -3000897897090466540L;
final void lock() {
acquire(1);
}
/**
tryAcquire 的公平版本。除非递归调用或没有服务员或者是第一个,否则不要授予访问权限。
*/
protected final boolean tryAcquire(int acquires) {
//获取当前的线程
final Thread current = Thread.currentThread();
//获取到当前的状态
int c = getState();
//如果当前线程状态为同步状态
if (c == 0) {
//判断他是否有排队的线程
if (!hasQueuedPredecessors() &&
//比较并且设置的状态
compareAndSetState(0, acquires)) {
//如果状态满足,就将当前线程设置为独占所有者线程
setExclusiveOwnerThread(current);
return true;
}
}
else if (current == getExclusiveOwnerThread()) {
int nextc = c + acquires;
if (nextc < 0)
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}
}
static final class NonfairSync extends Sync {
private static final long serialVersionUID = 7316153563782823691L;
/**
* 执行锁定。尝试立即驳船,在失败时恢复正常获取。
*/
final void lock() {
//与公平锁相比较,他无序去判断是否有排队的线程
if (compareAndSetState(0, 1))
setExclusiveOwnerThread(Thread.currentThread());
else
acquire(1);
}
protected final boolean tryAcquire(int acquires) {
return nonfairTryAcquire(acquires);
}
}
5.2,可重入锁
synchronized和lock都是可重入锁
- sychronized是隐式锁,不用手工上锁与解锁,而lock为显示锁,需要手工上锁与解锁
- 可重入锁也叫递归锁
而且有了可重入锁之后,破解第一把之后就可以一直进入到内层结构
可重入锁就是说我们只要能进入外部锁,那么就可以进入同一个锁的内部锁。换言之就是钥匙可以重复使用
package day02
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
/**
* @version v1.0
* @ProjectName: JUC
* @ClassName: SyncLockDemo
* @Description: 可重入锁
* @Author: 赵先生
* @Date: 2022/1/24 9:59
* 可重入锁就是说我们只要能进入外部锁,那么就可以进入同一个锁的内部锁。换言之就是钥匙可以重复使用
*/
public class SyncLockDemo {
public synchronized void add() {
add();
}
public static void main(String[] args) {
//Lock演示可重入锁
Lock lock = new ReentrantLock();
//创建线程
new Thread(()->{
try {
//上锁
lock.lock();
System.out.println(Thread.currentThread().getName()+" 外层");
try {
//上锁
lock.lock();
System.out.println(Thread.currentThread().getName()+" 内层");
}finally {
//释放锁
lock.unlock();
}
}finally {
//释放做
lock.unlock();
}
},"t1").start();
//创建新线程
new Thread(()->{
lock.lock();
System.out.println("aaaa");
lock.unlock();
},"aa").start();
}
}
在同一把锁中的嵌套锁,内部嵌套锁没解锁还是可以输出,但是如果跳出该线程,执行另外一个线程就会造成死锁
要把握上锁与解锁的概念,都要写上
5.3,死锁
两个或以上的进程因为争夺资源而造成互相等待资源的现象称为死锁
产生死锁的原因:
- 系统资源不足
- 系统资源分配不当
- 进程运行顺序不当
验证是否是死锁
- jps 类似于linux中的
ps -ef
查看进程号 - jstack 自带的堆栈跟踪工具
通过用idea自带的命令行输入 jps -l
查看其编译代码的进程号后jstack 进程号
死锁验证截图
具体死锁的操作代码实列
可理解背下来,大厂面试可考,死锁的简单案例
package day02;
import java.util.concurrent.TimeUnit;
/**
* @version v1.0
* @ProjectName: JUC
* @ClassName: DeadLock
* @Description: 死锁
* @Author: 赵先生
* @Date: 2022/1/24 10:16
*/
public class DeadLock {
//创建两个对象
static Object a = new Object();
static Object b = new Object();
public static void main(String[] args) {
new Thread(()->{
synchronized (a) {
System.out.println(Thread.currentThread().getName()+" 持有锁a,试图获取锁b");
//休眠一秒
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (b) {
System.out.println(Thread.currentThread().getName()+" 获取锁b");
}
}
},"A").start();
new Thread(()->{
synchronized (b) {
System.out.println(Thread.currentThread().getName()+" 持有锁b,试图获取锁a");
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (a) {
System.out.println(Thread.currentThread().getName()+" 获取锁a");
}
}
},"B").start();
}
}
六,Callable接口
创建线程的多种方式:
- 继承Thread类
- 实现Runnable接口
- Callable接口
- 线程池
目前学习了有两种创建线程的方法,一种是通过创建 Thread 类,另一种是通过使用 Runnable 创建线程,但是,Runnable 缺少的一项功能是,当线程终止时(即 run()完成时),我们无法使线程返回结果。为了支持此功能,Java 中提供了 Callable 接口。
比较Runnable接口和Callable接口
- Callable中的call()计算结果,如果无法计算结果,会抛出异常
- Runnable中的run()使用实现接口Runnable的对象创建一个线程时,启动该线程将导致在独立执行的线程中调用该对象的run方法
- 总的来说:run()没有返回值,不会抛出异常。而call()有返回值,会抛出异常
两个接口的代码
//实现Runnable接口
class MyThread1 implements Runnable {
@Override
public void run() {
}
}
//实现Callable接口
class MyThread2 implements Callable {
@Override
public Integer call() throws Exception {
return 200;
}
}
具体在主函数中
通过Thread线程创建接口
只有这个可以new Thread(new MyThread1(),"AA").start();
这个不可以new Thread(new MyThread2(),"BB").start();
因为Thread的构造函数中没有Callable接口的参数设置
直接替换不可以,只能用下面这种线程创建方法
(找一个类,即和Runnable接口有关系,又和Callable接口有关系)
发现Runnable接口有实现类FutureTask(中间对象)
FutureTask的构造函数有Callable参数,通过FutureTask创建线程对象
6.1 FutureTask
FutureTask的构造方法有
FutureTask(Callable<> callable)
创建一个FutureTask,一旦运行就执行给定的CallableFutureTask(Runnable runnable,V result)
创建一个FutureTask,一旦运行就执行给定的Ru你那边了,并安排成功完成时get返回给定的结果
其他常用的代码:
get()
获取结果
isDone()
判断是否计算结束
FutureTask 具体实现方法可以有两种方式(此处用的泛型参数)
第一种是
//实现Callable接口
class MyThread2 implements Callable {
@Override
public Integer call() throws Exception {
return 200;
}
}
FutureTask<Integer> futureTask1 = new FutureTask<>(new MyThread2());
第二种是
//lam表达式
FutureTask<Integer> futureTask2 = new FutureTask<>(()->{
System.out.println(Thread.currentThread().getName()+" come in callable");
return 1024;
});
创建线程 new Thread(futureTask2,"lucy").start();
获取结果System.out.println(futureTask2.get());
所谓的FutureTask是在不影响主任务的同时,开启单线程完成某个特别的任务,之后主线程续上单线程的结果即可(该单线程汇总给主线程只需要一次即可)
如果之后主线程在开启该单线程,可以直接获得结果,因为之前已经执行过一次了
完整的代码
//比较两个接口
//实现Runnable接口
class MyThread1 implements Runnable {
@Override
public void run() {
}
}
//实现Callable接口
class MyThread2 implements Callable {
@Override
public Integer call() throws Exception {
System.out.println(Thread.currentThread().getName()+" come in callable");
return 200;
}
}
public class Demo1 {
public static void main(String[] args) throws ExecutionException, InterruptedException {
//Runnable接口创建线程
new Thread(new MyThread1(),"AA").start();
//Callable接口,报错
// new Thread(new MyThread2(),"BB").start();
//FutureTask
FutureTask<Integer> futureTask1 = new FutureTask<>(new MyThread2());
//lam表达式
FutureTask<Integer> futureTask2 = new FutureTask<>(()->{
System.out.println(Thread.currentThread().getName()+" come in callable");
return 1024;
});
//创建一个线程
new Thread(futureTask2,"lucy").start();
new Thread(futureTask1,"mary").start();
// while(!futureTask2.isDone()) {
// System.out.println("wait.....");
// }
//调用FutureTask的get方法
System.out.println(futureTask2.get());
System.out.println(futureTask1.get());
System.out.println(Thread.currentThread().getName()+" come over");
}
}
七,JUC强大辅助类
该辅助类主要讲述三个
分别为:
减少计数CountDownLatch
循环栅栏CyclicBarrier
信号灯Semaphore
7.1 CountDownLatch(减少计数)
该类的构造方法为
CountDownLatch(int count)
构造一个用给定计数初始化的CountDownLatch在这里插入代码片
两个常用的主要方法
await()
使当前线程在锁存器倒计数至零之前一直在等待,除非线程被中断
countDown()
递减锁存器的计数,如果计数达到零,将释放所有等待的线程
CountDownLatch 类可以设置一个计数器,然后通过 countDown 方法来进行减 1 的操作,使用 await 方法等待计数器不大于 0,然后继续执行 await 方法之后的语句
实例
6个同学陆续离开教室之后,班长才能锁门
如果不加 CountDownLatch类,会出现线程混乱执行,同学还未离开教室班长就已经锁门了
public class CountDownLatchDemo {
//6个同学陆续离开教室之后,班长锁门
public static void main(String[] args) throws InterruptedException {
//6个同学陆续离开教室之后
for (int i = 1; i <=6; i++) {
new Thread(()->{
System.out.println(Thread.currentThread().getName()+" 号同学离开了教室");
},String.valueOf(i)).start();
}
System.out.println(Thread.currentThread().getName()+" 班长锁门走人了");
}
}
这个的话会出现学生还没有走完就已经锁门的结果。
解决后的代码
package day02;
import java.util.concurrent.CountDownLatch;
/**
* @version v1.0
* @ProjectName: JUC
* @ClassName: CountDownLatchDemo
* @Description: CountDownLatchDemo演示
* @Author: 赵先生
* @Date: 2022/1/24 16:27
*/
public class CountDownLatchDemo {
//6个同学陆续离开教室之后,班长锁门
public static void main(String[] args) throws InterruptedException {
//创建CountDownLatch对象
CountDownLatch countDownLatch = new CountDownLatch(6);
//6个同学陆续离开教室之后
for (int i = 1; i <=6; i++) {
new Thread(()->{
System.out.println(Thread.currentThread().getName()+" 号同学离开了教室");
//每次走一个同学就-1
countDownLatch.countDown();
},String.valueOf(i)).start();
}
countDownLatch.await();
System.out.println(Thread.currentThread().getName()+" 班长锁门走人了");
}
}
7.2 CyclicBarrier(循环栅栏)
该类是一个同步辅助类,允许一组线程互相等到,直到到达某个公共屏障点,在设计一组固定大小的线程的程序中,这些线程必须互相等待,这个类很有用,因为barrier在释放等待线程后可以重用,所以称为循环barrier。
简言之就是说,只有一组线程达到了一个预先设定的值的时候才会执行一个固定的方法。
常用的构造方法有:
CyclicBarrier(int parties,Runnable barrierAction)
创建一个新的CyclicBarrier,它将在给定数量的参与者(线程)处于等待状态时启动,并在启动barrier时执行给定的屏障操作,该操作由最后一个进入barrier的线程操作
常用的方法有:
await()
在所有的参与者都已经在此barrier上调用await方法之前一直等待
通过具体案例
集齐7颗龙珠就可以召唤神龙
完整代码
package day02;
import java.util.concurrent.CyclicBarrier;
/**
* @version v1.0
* @ProjectName: JUC
* @ClassName: CyclicBarrierDemo
* @Description: 只有一组线程达到了一个预先设定的值的时候才会执行一个固定的方法。
* @Author: 赵先生
* @Date: 2022/1/24 16:40
*/
//集齐7颗龙珠就可以召唤神龙
public class CyclicBarrierDemo {
//创建固定值
private static final int NUMBER = 7;
public static void main(String[] args) {
//创建CyclicBarrier
CyclicBarrier cyclicBarrier =
new CyclicBarrier(NUMBER,()->{
System.out.println("*****集齐7颗龙珠就可以召唤神龙");
});
//集齐七颗龙珠过程
for (int i = 1; i <=7; i++) {
new Thread(()->{
try {
System.out.println(Thread.currentThread().getName()+" 星龙被收集到了");
//等待 await操作是通过不断的去减从而实现的。也就是说我从初始值为开始减,减到0的时候就执行我们预先设定的方法
cyclicBarrier.await();
} catch (Exception e) {
e.printStackTrace();
}
},String.valueOf(i)).start();
}
}
}
7.3 Semaphore(信号灯)
一个计数信号量,从概念上将,信号量维护了一个许可集,如有必要,在许可可用前会阻塞每一个acquire(),然后在获取该许可。每个release()添加一个许可,从而可能释放一个正在阻塞的获取者。但是,不使用实际的许可对象,Semaphore只对可用许可的号码进行计数,并采取相应的行动。
具体常用的构造方法有:
Semaphore(int permits)
创建具有给定的许可数和非公平的公平设置的Semapore。
具体常用的方法有:
acquire()
从此信号量获取一个许可,在提供一个许可前一直将线程阻塞,否则线程被中断
release()
释放一个许可,将其返回给信号量
设置许可数量Semaphore semaphore = new Semaphore(3);
一般acquire()
都会抛出异常,release
在finally
中执行
通过具体案例
6辆汽车,停3个车位
完整代码:
package day02;
import java.util.Random;
import java.util.concurrent.Semaphore;
import java.util.concurrent.TimeUnit;
/**
* @version v1.0
* @ProjectName: JUC
* @ClassName: SemaphoreDemo
* @Description: 信号灯(抢占的问题)
* @Author: 赵先生
* @Date: 2022/1/24 16:51
*/
//6辆汽车,停3个车位
public class SemaphoreDemo {
public static void main(String[] args) {
//创建Semaphore,设置许可数量
Semaphore semaphore = new Semaphore(3);
//模拟6辆汽车
for (int i = 1; i <=6; i++) {
new Thread(()->{
try {
//抢占
semaphore.acquire();
System.out.println(Thread.currentThread().getName()+" 抢到了车位");
//设置随机停车时间
TimeUnit.SECONDS.sleep(new Random().nextInt(5));
System.out.println(Thread.currentThread().getName()+" ------离开了车位");
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
//释放
semaphore.release();
}
},String.valueOf(i)).start();
}
}
}
运行结果
八,读写锁
回顾悲观锁和乐观锁的概念
悲观锁:单独每个人完成事情的时候,执行上锁解锁。解决并发中的问题,不支持并发操作,只能一个一个操作,效率低
乐观锁:每执行一件事情,都会比较数据版本号,谁先提交,谁先提交版本号
简单描述一下,悲观锁就是一个很悲观的锁,在他的世界里一切的操作都会发生问题。所以在每个行为发生的前都要去加一把锁,行为结束之后在去释放锁,所以他不支持并发操作。
乐观锁就是一个很乐观的锁,在他的世界里一切的操作都是不会有问题的,所以我们在进行每一步操作的时候不需要再操作前加锁,只要在操作完成后进行比较即可。
新概念:
表锁:整个表操作,不会发生死锁
行锁:每个表中的单独一行进行加锁,会发生死锁
读锁:共享锁(可以有多个人读),会发生死锁
写锁:独占锁(只能有一个人写),会发生死锁
读写锁:一个资源可以被多个读线程访问,也可以被一个写线程访问,但不能同时存在读写线程,读写互斥,读读共享
读写锁ReentrantReadWriteLock
读锁为ReentrantReadWriteLock.ReadLock
,readLock()
方法
写锁为ReentrantReadWriteLock.WriteLock
,writeLock()
方法
创建读写锁对象private ReadWriteLock rwLock = new ReentrantReadWriteLock();
写锁 加锁 rwLock.writeLock().lock();
,解锁为rwLock.writeLock().unlock();
读锁 加锁rwLock.readLock().lock();
,解锁为rwLock.readLock().unlock();
案例分析:
模拟多线程在map中取数据和读数据
完整代码如图
package day02;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
/**
* @version v1.0
* @ProjectName: JUC
* @ClassName: ReadWriteLockDemo
* @Description: 读写锁
* @Author: 赵先生
* @Date: 2022/1/24 17:33
*/
//资源类
class MyCache {
//创建map集合
private volatile Map<String,Object> map = new HashMap<>();
//创建读写锁对象
private ReadWriteLock rwLock = new ReentrantReadWriteLock();
//放数据
public void put(String key,Object value) {
//添加写锁
rwLock.writeLock().lock();
try {
System.out.println(Thread.currentThread().getName()+" 正在写操作"+key);
//暂停一会
TimeUnit.MICROSECONDS.sleep(300);
//放数据
map.put(key,value);
System.out.println(Thread.currentThread().getName()+" 写完了"+key);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
//释放写锁
rwLock.writeLock().unlock();
}
}
//取数据
public Object get(String key) {
//添加读锁
rwLock.readLock().lock();
Object result = null;
try {
System.out.println(Thread.currentThread().getName()+" 正在读取操作"+key);
//暂停一会
TimeUnit.MICROSECONDS.sleep(300);
result = map.get(key);
System.out.println(Thread.currentThread().getName()+" 取完了"+key);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
//释放读锁
rwLock.readLock().unlock();
}
return result;
}
}
public class ReadWriteLockDemo {
public static void main(String[] args) throws InterruptedException {
MyCache myCache = new MyCache();
//创建线程放数据
for (int i = 1; i <=5; i++) {
final int num = i;
new Thread(()->{
myCache.put(num+"",num+"");
},String.valueOf(i)).start();
}
TimeUnit.MICROSECONDS.sleep(300);
//创建线程取数据
for (int i = 1; i <=5; i++) {
final int num = i;
new Thread(()->{
myCache.get(num+"");
},String.valueOf(i)).start();
}
}
}
通过这一章节和以上文章
总结锁的演变
- 无锁:多线程抢夺资源
- synchronized和ReentrantLock,都是独占,每次只可以一个操作,不能共享
- ReentrantReadWriteLock,读读可以共享,提升性能,但是不能多人写。缺点:造成死锁(一直读,不能写),读进程不能写,写进程可以读。
- 写锁降级为读锁(一般等级写锁高于读锁)
具体第四步演练的代码
具体降级步骤
获取写锁->获取读锁->释放写锁->释放读锁
//演示读写锁降级
public class Demo1 {
public static void main(String[] args) {
//可重入读写锁对象
ReentrantReadWriteLock rwLock = new ReentrantReadWriteLock();
ReentrantReadWriteLock.ReadLock readLock = rwLock.readLock();//读锁
ReentrantReadWriteLock.WriteLock writeLock = rwLock.writeLock();//写锁
//锁降级
//1 获取写锁
writeLock.lock();
System.out.println("manongyanjiuseng");
//2 获取读锁
readLock.lock();
System.out.println("---read");
//3 释放写锁
writeLock.unlock();
//4 释放读锁
readLock.unlock();
}
}
运行截图:
如果是读之后再写,执行不了
因为读锁权限小于写锁
需要读完之后释放读锁,在进行写锁
//2 获取读锁
readLock.lock();
System.out.println("---read");
//1 获取写锁
writeLock.lock();
System.out.println("manongyanjiuseng");
九,阻塞队列
而阻塞队列是共享队列(多线程操作),一端输入,一端输出
不能无限放队列,满了之后就会进入阻塞,取出也同理
- 当队列是空的,从队列中获取元素的操作将会被阻塞
- 当队列是满的,从队列中添加元素的操作将会被阻塞
- 试图从空的队列中获取元素的线程将会被阻塞,直到其他线程往空的队列插入新的元素
- 试图向已满的队列中添加新元素的线程将会被阻塞,直到其他线程从队列中移除一个或多个元素或者完全清空,使队列变得空闲起来并后续新增
9.1 种类
1.ArrayBlockingQueue
- 基于数组的阻塞队列
- 由数组结构组成的有界阻塞队列
- ArrayBlockingQueue 在生产者放入数据和消费者获取数据,都是共用同一个锁对象,无法并行
2. LinkedBlockingQueue
- 基于链表的阻塞队列
- 由链表结构组成的有界(但大小默认值为integer.MAX_VALUE)阻塞队列
- 之所以能够高效的处理并发数据,还因为其对于生产者端和消费者端分别采用了独立的锁来控制数据同步,这也意味着在高并发的情况下生产者和消费者可以并行地操作队列中的数据,以此来提高整个队列的并发性能
3.DelayQueue
- 使用优先级队列实现的延迟无界阻塞队列
- DelayQueue 中的元素只有当其指定的延迟时间到了,才能够从队列中获取到该元素。DelayQueue 是一个没有大小限制的队列,因此往队列中插入数据的操作(生产者)永远不会被阻塞,而只有获取数据的操作(消费者)才会被阻塞
4.PriorityBlockingQueue
- 基于优先级的阻塞队列
- 支持优先级排序的无界阻塞队列
- 不会阻塞数据生产者,而只会在没有可消费的数据时,阻塞数据的消费者
5.SynchronousQueue
- 一种无缓冲的等待队列
- 相对于有缓冲的 BlockingQueue 来说,少了一个中间经销商的环节(缓冲区)
- 不存储元素的阻塞队列,也即单个元素的队列
- 声明一个 SynchronousQueue 有两种不同的方式,它们之间有着不太一样的行为。
- 公平模式:SynchronousQueue 会采用公平锁,并配合一个 FIFO 队列来阻塞多余的生产者和消费者,从而体系整体的公平策略;
- 非公平模式(SynchronousQueue 默认):SynchronousQueue 采用非公平锁,同时配合一个 LIFO 队列来管理多余的生产者和消费者
- 而后一种模式,如果生产者和消费者的处理速度有差距,则很容易出现饥渴的情况,即可能有某些生产者或者是消费者的数据永远都得不到处理
6.LinkedTransferQueue
- 由链表结构组成的无界阻塞 TransferQueue 队列
- 由链表组成的无界阻塞队列
- 预占模式。意思就是消费者线程取元素时,如果队列不为空,则直接取走数据,若队列为空,生成一个节点(节点元素为 null)入队,消费者线程被等待在这个节点上,生产者线程入队时发现有一个元素为 null 的节点,生产者线程就不入队了,直接就将元素填充到该节点,并唤醒该节点等待的线程,被唤醒的消费者线程取走元素,从调用的方法返回
7.LinkedBlockingDeque
- 由链表结构组成的双向阻塞队列
- 阻塞有两种情况
- 插入元素时: 如果当前队列已满将会进入阻塞状态,一直等到队列有空的位置时再该元素插入,该操作可以通过设置超时参数,超时后返回 false 表示操作失败,也可以不设置超时参数一直阻塞,中断后抛出 InterruptedException异常
- 读取元素时: 如果当前队列为空会阻塞住直到队列不为空然后返回元素,同样可以通过设置超时参数
9.2 方法
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-lrARASGX-1643102477023)(JUC.assets/image-20220125094904706.png)]
方法演示:
package queue;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.TimeUnit;
/**
* @version v1.0
* @ProjectName: JUC
* @ClassName: BlockingQueueDemo
* @Description: 阻塞队列
* @Author: 赵先生
* @Date: 2022/1/25 9:51
*/
public class BlockingQueueDemo {
public static void main(String[] args) throws InterruptedException {
//创建一个阻塞队列,需要设置一个初始的容量
BlockingQueue<String> blockingQueue = new ArrayBlockingQueue<>(3);
// //第一组
// System.out.println(blockingQueue.add("a"));
// System.out.println(blockingQueue.add("b"));
// System.out.println(blockingQueue.add("c"));
// //检索但不删除此队列的头部。如果队列为空会产生异常。
// System.out.println(blockingQueue.element());
// //当队列满后,会Exception in thread "main" java.lang.IllegalStateException: Queue full异常
// //System.out.println(blockingQueue.add("d"));
//
// //remove方法就是我们会移除元素并返回
// System.out.println(blockingQueue.remove());
// System.out.println(blockingQueue.remove());
// System.out.println(blockingQueue.remove());
// //移除时如果阻塞队列为空,Exception in thread "main" java.util.NoSuchElementException异常
// System.out.println(blockingQueue.remove());
//第二组
// System.out.println(blockingQueue.offer("a"));
// System.out.println(blockingQueue.offer("a"));
// System.out.println(blockingQueue.offer("a"));
// //这个方法,如果队列满了,那么就返回false而不是返回异常
// //System.out.println(blockingQueue.offer("a"));
//
// System.out.println(blockingQueue.poll());
// System.out.println(blockingQueue.poll());
// System.out.println(blockingQueue.poll());
// //取元素的时候如果队列为空那么就返回null
// System.out.println(blockingQueue.poll());
//第三组
// blockingQueue.put("a");
blockingQueue.put("a");
blockingQueue.put("a");
//如果队列满了就会处于阻塞状态,什么时候有空间什么时候恢复
//blockingQueue.put("a");
System.out.println(blockingQueue.take());
System.out.println(blockingQueue.take());
System.out.println(blockingQueue.take());
//去元素,同理
System.out.println(blockingQueue.take());
//第四组
System.out.println(blockingQueue.offer("a"));
System.out.println(blockingQueue.offer("a"));
System.out.println(blockingQueue.offer("a"));
//如果队列满了就会发生阻塞,三秒后没办法解决就会结束
System.out.println(blockingQueue.offer("b",3L, TimeUnit.SECONDS));
//同理
System.out.println(blockingQueue.offer("w",3L, TimeUnit.SECONDS));
}
}
十,线程池
回顾以前的连接池概念
连接池是创建和管理一个连接的缓冲池的技术,这些连接准备好被任何需要它们的线程使用
线程池(英语:thread pool):一种线程使用模式。线程过多会带来调度开销,进而影响缓存局部性和整体性能。而线程池维护着多个线程,等待着监督管理者分配可并发执行的任务。这避免了在处理短时间任务时创建与销毁线程的代价。线程池不仅能够保证内核的充分利用,还能防止过分调度。
特点:
- 降低资源消耗: 通过重复利用已创建的线程降低线程创建和销毁造成的销耗。
- 提高响应速度: 当任务到达时,任务可以不需要等待线程创建就能立即执行。
- 提高线程的可管理性: 线程是稀缺资源,如果无限制的创建,不仅会销耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一的分配,调优和监控。
具体架构:
Java 中的线程池是通过 Executor 框架实现的,该框架中用到了 Executor,Executors,ExecutorService,ThreadPoolExecutor 这几个类
说明:Executors为工具类,I为接口类,C为实现类
10.1 种类与创建
-
Executors.newFixedThreadPool(int)
一池N线程ExecutorService threadPool1 = Executors.newFixedThreadPool(5); //5个窗口
-
Executors.newSingleThreadExecutor()
一池一线程ExecutorService threadPool2 = Executors.newSingleThreadExecutor(); //一个窗口
-
Executors.newCachedThreadPool()
一池可扩容根据需求创建线程ExecutorService threadPool3 = Executors.newCachedThreadPool();
-
执行线程
execute()
-
关闭线程
shutdown()
线程池中的执行方法execute源代码为
public interface Executor {
/**
* Executes the given command at some time in the future. The command
* may execute in a new thread, in a pooled thread, or in the calling
* thread, at the discretion of the {@code Executor} implementation.
*
* @param command the runnable task
* @throws RejectedExecutionException if this task cannot be
* accepted for execution
* @throws NullPointerException if command is null
*/
void execute(Runnable command);
}
void execute(Runnable command);
参数为Runnable接口类,可以通过设置lambda
具体案例代码案例
//演示线程池三种常用分类
public class ThreadPoolDemo1 {
public static void main(String[] args) {
//一池五线程
ExecutorService threadPool1 = Executors.newFixedThreadPool(5); //5个窗口
//一池一线程
ExecutorService threadPool2 = Executors.newSingleThreadExecutor(); //一个窗口
//一池可扩容线程
ExecutorService threadPool3 = Executors.newCachedThreadPool();
//10个顾客请求
try {
for (int i = 1; i <=10; i++) {
//执行
threadPool3.execute(()->{
System.out.println(Thread.currentThread().getName()+" 办理业务");
});
}
}catch (Exception e) {
e.printStackTrace();
}finally {
//关闭
threadPool3.shutdown();
}
}
}
10.2 底层原理
通过查看上面三种方式创建对象的类源代码
都有new ThreadPoolExecutor
具体查看该类的源代码,涉及七个参数
public ThreadPoolExecutor(int corePoolSize,//常驻的线程数量(核心线程数量)
int maximumPoolSize,//线程池最大支持的数量
long keepAliveTime,//保持存辉的时间
TimeUnit unit,
BlockingQueue<Runnable> workQueue,//工作的阻塞队列
ThreadFactory threadFactory,//线程工厂,用于创建线程
RejectedExecutionHandler handler//拒绝策略) {
if (corePoolSize < 0 ||
maximumPoolSize <= 0 ||
maximumPoolSize < corePoolSize ||
keepAliveTime < 0)
throw new IllegalArgumentException();
if (workQueue == null || threadFactory == null || handler == null)
throw new NullPointerException();
this.acc = System.getSecurityManager() == null ?
null :
AccessController.getContext();
this.corePoolSize = corePoolSize;
this.maximumPoolSize = maximumPoolSize;
this.workQueue = workQueue;
this.keepAliveTime = unit.toNanos(keepAliveTime);
this.threadFactory = threadFactory;
this.handler = handler;
}
具体工作流程是:
- 在执行创建对象的时候不会创建线程
- 创建线程的时候execute()才会创建
- 先到常驻线程,满了之后再到阻塞队列进行等待,阻塞队列满了之后,在往外扩容线程,扩容线程不能大于最大线程数。大于最大线程数和阻塞队列之和后,会执行拒绝策略。
具体的拒绝策略有:
- 抛异常
- 谁调用找谁
- 抛弃最久执行当前
- 不理不问
10.3 自定义线程池
实际在开发中不允许使用Executors创建,而是通过ThreadPoolExecutor的方式,规避资源耗尽风险。
ExecutorService threadPool = new ThreadPoolExecutor(
2,
5,
2L,
TimeUnit.SECONDS,
new ArrayBlockingQueue<>(3),
Executors.defaultThreadFactory(),
new ThreadPoolExecutor.AbortPolicy()
);
其他都同理,只是调用ThreadPoolExecutor类,自定义参数.
完整代码:
package ThreadPoo;
import java.util.concurrent.*;
/**
* @version v1.0
* @ProjectName: JUC
* @ClassName: ThreadPoolDemo2
* @Description: 自定义线程池
* @Author: 赵先生
* @Date: 2022/1/25 10:56
*/
//自定义线程池创建
public class ThreadPoolDemo2 {
public static void main(String[] args) {
ExecutorService threadPool = new ThreadPoolExecutor(
2,
5,
2L,
TimeUnit.SECONDS,
new ArrayBlockingQueue<>(3),
Executors.defaultThreadFactory(),
new ThreadPoolExecutor.AbortPolicy()
);
//10个顾客请求
try {
for (int i = 1; i <=10; i++) {
//执行
threadPool.execute(()->{
System.out.println(Thread.currentThread().getName()+" 办理业务");
});
}
}catch (Exception e) {
e.printStackTrace();
}finally {
//关闭
threadPool.shutdown();
}
}
}
十一,Fork/Join分支合并框架
将一个大的任务拆分成多个子任务进行并行处理,最后将子任务结果合并成最后的计算结果。(类似于分治的思想)
- ForkJoinTask:我们要使用 Fork/Join 框架,首先需要创建一个 ForkJoin 任务。该类提供了在任务中执行 fork(分) 和 join(合) 的机制。通常情况下我们不需要直接集成 ForkJoinTask 类,只需要继承它的子类,Fork/Join 框架提供了两个子类:
- RecursiveAction:用于没有返回结果的任务
- RecursiveTask:用于有返回结果的任务
- ForkJoinPool:ForkJoinTask 需要通过 ForkJoinPool 来执行
具体案例:1加到100,相加两个数值不能大于10
package forkjion;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ForkJoinPool;
import java.util.concurrent.ForkJoinTask;
import java.util.concurrent.RecursiveTask;
/**
* @version v1.0
* @ProjectName: JUC
* @ClassName: ForkJoinDemo
* @Description: Fork与Join分支
* @Author: 赵先生
* @Date: 2022/1/25 15:23
*/
/**
* 我们要使用 Fork/Join 框架,首先需要创建一个 ForkJoin 任务.但是我们一般不直接使用 ForkJoinTask 类而是用其子类
* RecursiveAction:用于没有返回结果的任务
* RecursiveTask:用于有返回结果的任务,继承后可以实现递归(自己调自己)调用的任务
*/
class MyTask extends RecursiveTask<Integer> {
/**
* 拆分差值不能超过10,计算10以内运算
*/
private static final Integer VALUE = 10;
private int begin;
private int end;
private int result;
public MyTask(int begin,int end) {
this.begin = begin;
this.end = end;
}
/**
* 拆分合并
* @return
*/
@Override
protected Integer compute() {
//判断相加两个数值是否大于10
if ((end - begin) <= VALUE) {
for (int i = begin;i <= end; i++) {
result = result + i;
}
} else {
int middle = (begin + end)/2;
//拆分左边
MyTask myTask1 = new MyTask(begin, middle);
//拆分右边
MyTask myTask2 = new MyTask(middle+1, end);
//调用拆分方法
myTask1.fork();
myTask2.fork();
//合并结果
result = myTask1.join() + myTask2.join();
}
return result;
}
}
/**
* @author 赵先生
*/
public class ForkJoinDemo {
public static void main(String[] args) throws ExecutionException, InterruptedException {
//创建MyTask对象
MyTask myTask = new MyTask(0,100);
//创建分支合并池
ForkJoinPool forkJoinPool = new ForkJoinPool();
//通过分支合并池获取到这个任务
ForkJoinTask<Integer> forkJoinTask = forkJoinPool.submit(myTask);
//获取最终合并之后的结果
Integer integer = forkJoinTask.get();
System.out.println(integer);
forkJoinPool.shutdown();
}
}
十二,异步回调
CompletableFuture 在 Java 里面被用于异步编程,异步通常意味着非阻塞,可以使得我们的任务单独运行在与主线程分离的其他线程中,并且通过回调可以在主线程中得到异步任务的执行状态,是否完成,和是否异常等信息
类中的具体引用类以及接口:
CompletableFuture 实现了 Future, CompletionStage 接口,实现了 Future接口就可以兼容现在有线程池框架,而 CompletionStage 接口才是异步编程的接口抽象,里面定义多种异步方法,通过这两者集合,从而打造出了强大的CompletableFuture 类
异步调用没有返回值方法runAsync
异步调用有返回值方法supplyAsync
主线程调用 get 方法会阻塞
代码演示:
package day02;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;
/**
* @version v1.0
* @ProjectName: JUC
* @ClassName: CompletableFutureDemo
* @Description: 异步调用
* @Author: 赵先生
* @Date: 2022/1/25 15:53
*/
public class CompletableFutureDemo {
public static void main(String[] args) throws ExecutionException, InterruptedException {
//异步调用 没有返回值
CompletableFuture<Void> completableFuture = CompletableFuture.runAsync(() -> {
System.out.println(Thread.currentThread().getName()+"completableFuture1");
});
//异步调用 有返回值
CompletableFuture<Integer> completableFuture2 = CompletableFuture.supplyAsync(()->{
System.out.println(Thread.currentThread().getName()+"completableFuture2");
return 1024;
});
completableFuture2.whenComplete((t,u)->{
//方法中的返回值
System.out.println("----t"+t);
//u返回的是异常信息
System.out.println("----u"+u);
}).get();
}
}
具体whenComplete的源代码为:
t为返回结果,u为异常信息
public CompletableFuture<T> whenComplete(
BiConsumer<? super T, ? super Throwable> action) {
return uniWhenCompleteStage(null, action);
}
12.1 Future 与 CompletableFuture
对比这两种方法,一个为同步一个为异步
Futrue 在 Java 里面,通常用来表示一个异步任务的引用,比如我们将任务提交到线程池里面,然后我们会得到一个 Futrue,在 Future 里面有 isDone 方法来 判断任务是否处理结束,还有 get 方法可以一直阻塞直到任务结束然后获取结果,但整体来说这种方式,还是同步的,因为需要客户端不断阻塞等待或者不断轮询才能知道任务是否完成
1)不支持手动完成
我提交了一个任务,但是执行太慢了,我通过其他路径已经获取到了任务结果,现在没法把这个任务结果通知到正在执行的线程,所以必须主动取消或者一直等待它执行完成
(2)不支持进一步的非阻塞调用
通过 Future 的 get 方法会一直阻塞到任务完成,但是想在获取任务之后执行额外的任务,因为 Future 不支持回调函数,所以无法实现这个功能
(3)不支持链式调用
对于 Future 的执行结果,我们想继续传到下一个 Future 处理使用,从而形成一个链式的 pipline 调用,这在 Future 中是没法实现的。
4)不支持多个 Future 合并
比如我们有 10 个 Future 并行执行,我们想在所有的 Future 运行完毕之后,执行某些函数,是没法通过 Future 实现的。
(5)不支持异常处理
Future 的 API 没有任何的异常处理的 api,所以在异步运行时,如果出了问题是不好定位的