1、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;
}
}
public static void main(String[] args) {
FutureTask futureTask = new FutureTask<Integer>(()->{
return 1024;
});
}
发现Runnable接口有实现类FutureTask(中间对象)
FutureTask的构造函数有Callable参数,通过FutureTask创建线程对象
2、JUC强大的辅助类
2.1、CountDownLatch类
该类的构造方法为
CountDownLatch(int count)
构造一个用给定计数初始化的CountDownLatch
两个常用的主要方法
await()
使当前线程在锁存器倒计数至零之前一直在等待,除非线程被中断
countDown()
递减锁存器的计数,如果计数达到零,将释放所有等待的线程
CountDownLatch 类可以设置一个计数器,然后通过 countDown 方法来进行减 1 的操作,使用 await 方法等待计数器不大于 0,然后继续执行 await 方法之后的语句
具体步骤可以演化为定义一个类,减1操作,并等待到0,为0执行结果
通过具体的案例进行加深代码
6个同学陆续离开教室之后,班长才能锁门
如果不加 CountDownLatch类,会出现线程混乱执行,同学还未离开教室班长就已经锁门了
public class CountDownLatchDemo {
public static void main(String[] args) throws InterruptedException {
CountDownLatch countDownLatch = new CountDownLatch(6);
for (int i = 0; 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() + " 班长锁门");
}
}
打印结果:
2.2 CyclicBarrier
该类是一个同步辅助类,允许一组线程互相等到,直到到达某个公共屏障点,在设计一组固定大小的线程的程序中,这些线程必须互相等待,这个类很有用,因为barrier在释放等待线程后可以重用,所以称为循环barrier
常用的构造方法有:
CyclicBarrier(int parties,Runnable barrierAction)创建一个新的CyclicBarrier,它将在给定数量的参与者(线程)处于等待状态时启动,并在启动barrier时执行给定的屏障操作,该操作由最后一个进入barrier的线程操作
常用的方法有:
await()在所有的参与者都已经在此barrier上调用await方法之前一直等待
通过具体案例
集齐7颗龙珠就可以召唤神龙
完整代码
//集齐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()+" 星龙被收集到了");
//等待
cyclicBarrier.await();
} catch (Exception e) {
e.printStackTrace();
}
},String.valueOf(i)).start();
}
}
}
2.3 Semaphore类
一个计数信号量,从概念上将,信号量维护了一个许可集,如有必要,在许可可用前会阻塞每一个acquire(),然后在获取该许可。每个release()添加一个许可,从而可能释放一个正在阻塞的获取者。但是,不使用实际的许可对象,Semaphore只对可用许可的号码进行计数,并采取相应的行动
具体常用的构造方法有:
Semaphore(int permits)创建具有给定的许可数和非公平的公平设置的Semapore
具体常用的方法有:
acquire()从此信号量获取一个许可,在提供一个许可前一直将线程阻塞,否则线程被中断
release()释放一个许可,将其返回给信号量
设置许可数量Semaphore semaphore = new Semaphore(3);
一般acquire()都会抛出异常,release在finally中执行
通过具体案例
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();
}
}
}
3、读写锁
3.1 悲观锁
使用悲观锁的过程,目前有10000元钱,张三先操作这个数字,首先就是先上锁,张三减了8000后,再释放锁,李四再来操作这个数字,也是先上锁,操作完数字后再释放锁。每个人操作都是这个流程,缺点是不支持并发操作,效率低,只能一个一个操作。
3.2 乐观锁
乐观锁的执行流程:
在10000数字中还包含一个版本号,现在有张三和李四两个人同时去操作这个数字并得到这个版本号。张三减去8000,李四减去5000,但是张三先提交了事务,除了修改账户外,还要把版本号加1变为V1.1。这时李四再去提交事务之前会先去比较当前版本号和数据库中的版本是否一致,由于数据库中的版本号和当前李四手中的版本号不一致,所以李四提交事务失败。
3.3 新概念
- 表锁:整个表操作,不会发生死锁
- 行锁:每个表中的单独一行进行加锁,会发生死锁
- 读锁:共享锁(可以有多个人读),会发生死锁
- 写锁:独占锁(只能有一个人写),会发生死锁
读锁和写锁为什么会发生死锁的现象?
(1)读锁发生死锁的情况
线程1和线程2都可以读取同一条数据。当线程1在读取后想对该数据进行修改操作时,需要等待线程2读完之后才能操作,同样线程2修改时,也需要等待线程1读完后才能执行修改操作。
(2)写锁发生死锁的情况
线程1在对第一条数据进行写操作,线程2对第二条数据同样进行写操作。由于线程1在写操作的时候,也可以对其他的数据进行操作,当线程1要对第二条数据进行操作时,由于线程2在对第二条数据进行写操作,所有其他线程不可以操作。同样线程2也不能去操作第一条数据,这个时候就出现了死锁的现象。
读写锁:一个资源可以被多个读线程访问,也可以被一个写线程访问,但不能同时存在读写线程,读写互斥,读读共享
读写锁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中取数据和读数据
完整代码如图
public class ReadWriteLocakDemo {
public static void main(String[] args) {
MyCache myCache = new MyCache();
for (int i = 1; i <= 5 ; i++) {
final int num = i;
new Thread(()->{
try {
myCache.put(num+"",num+"");
} catch (Exception e) {
e.printStackTrace();
}
},String.valueOf(i)).start();
}
for (int i = 1; i <= 5; i++) {
final int num = i;
new Thread(() -> {
myCache.get(num + "");
}, String.valueOf(i)).start();
}
}
}
// 资源类
class MyCache{
private volatile Map<String,Object> map = new HashMap<>();
private ReadWriteLock rwLock = new ReentrantReadWriteLock();
public void put(String key,Object obj) {
rwLock.writeLock().lock();
try {
System.out.println(Thread.currentThread().getName() + " 正在写操作" + key);
TimeUnit.MICROSECONDS.sleep(100);
map.put(key,obj);
System.out.println(Thread.currentThread().getName() + " 写完了" + key);
} catch (InterruptedException e) {
e.printStackTrace();
}finally {
rwLock.writeLock().unlock();
}
}
public Object get(String key){
Object obj = null;
rwLock.readLock().lock();
try {
System.out.println(Thread.currentThread().getName() + " 正在读操作" + key);
TimeUnit.MICROSECONDS.sleep(100);
obj = map.get(key);
System.out.println(Thread.currentThread().getName() + " 读完了" + key);
} catch (InterruptedException e) {
e.printStackTrace();
}finally {
rwLock.readLock().unlock();
}
return obj;
}
}
打印结果:
4、阻塞队列
阻塞队列是共享队列(多线程操作),一端输入,一端输出
不能无限放队列,满了之后就会进入阻塞,取出也同理
- 当队列是空的,从队列中获取元素的操作将会被阻塞
- 当队列是满的,从队列中添加元素的操作将会被阻塞
- 试图从空的队列中获取元素的线程将会被阻塞,直到其他线程往空的队列插入新的元素
- 试图向已满的队列中添加新元素的线程将会被阻塞,直到其他线程从队列中移除一个或多个元素或者完全清空,使队列变得空闲起来并后续新增
4.1阻塞队列的种类
1 ArrayBlockingQueue
基于数组的阻塞队列
由数组结构组成的有界阻塞队列
-ArrayBlockingQueue 在生产者放入数据和消费者获取数据,都是共用同一个锁对象,无法并行
2、LinkedBlockingQueue
基于链表的阻塞队列
由链表结构组成的有界(但大小默认值为integer.MAX_VALUE)阻塞队列
- 之所以能够高效的处理并发数据,还因为其对于生产者端和消费者端分别采用了独立的锁来控制数据同步,这也意味着在高并发的情况下生产者和消费者可以并行地操作队列中的数据,以此来提高整个队列的并发性能
3、DelayQueue
使用优先级队列实现的延迟无界阻塞队列
- DelayQueue 中的元素只有当其指定的延迟时间到了,才能够从队列中获取到该元素。DelayQueue 是一个没有大小限制的队列,因此往队列中插入数据的操作(生产者)永远不会被阻塞,而只有获取数据的操作(消费者)才会被阻塞
4、PriorityBlockingQueue
基于优先级的阻塞队列
支持优先级排序的无界阻塞队列
不会阻塞数据生产者,而只会在没有可消费的数据时,阻塞数据的消费者
5、LinkedBlockingDeque
由链表结构组成的双向阻塞队列
阻塞有两种情况
- 插入元素时: 如果当前队列已满将会进入阻塞状态,一直等到队列有空的位置时再该元素插入,该操作可以通过设置超时参数,超时后返回 false 表示操作失败,也可以不设置超时参数一直阻塞,中断后抛出 InterruptedException异常。
- 读取元素时: 如果当前队列为空会阻塞住直到队列不为空然后返回元素,同样可以通过设置超时参数
6、方法
创建阻塞队列 BlockingQueue<String> blockingQueue = new ArrayBlockingQueue<>(3);
第一种方法:
- 加入元素System.out.println(blockingQueue.add(“a”));,成功为true,失败为false
- 检查元素System.out.println(blockingQueue.element());
- 取出元素System.out.println(blockingQueue.remove());,先进先出
第二种方法:
-
加入元素System.out.println(blockingQueue.offer(“a”));
-
取出元素System.out.println(blockingQueue.poll());
第三种方法:
- 加入元素blockingQueue.put(“a”);
- 取出元素System.out.println(blockingQueue.take());
该方法加入元素或者取出元素,如果满了或者空了,还进行下一步加入或者取出操作,会出现阻塞的状态,而第一二种方法是直接抛出异常
第四种方法:
加入元素System.out.println(blockingQueue.offer(“a”));
该方法满了或者空了在进行会有阻塞,但可以加入参数,超时退出System.out.println(blockingQueue.offer(“w”,3L, TimeUnit.SECONDS));
代码实现:
public static void main(String[] args) {
ArrayBlockingQueue<Object> queue = new ArrayBlockingQueue<>(3);
// 添加元素
boolean flag = queue.add("bbb");
System.out.println(flag); // true
// 检查元素
System.out.println(queue.element()); // bbb
// 取出元素
System.out.println(queue.remove()); // bbb
System.out.println("----------------------------");
// 第二种方式:加入
System.out.println(queue.offer("sss")); // true
// 取出元素
System.out.println(queue.poll()); // sss
System.out.println("----------------------------");
}