网站推广的电子邮件推广,谷歌浏览器下载安卓版,技能培训机构,祭祀网站建设方案1. Callable的用法
之前已经接触过了Runnable接口#xff0c;即我们可以使用实现Runnable接口的方式创建一个线程#xff0c;而Callable也是一个interface#xff0c;我们也可以用Callable来创建一个线程。
Callable是一个带有泛型的interface实现Callable接口必须重写cal…1. Callable的用法
之前已经接触过了Runnable接口即我们可以使用实现Runnable接口的方式创建一个线程而Callable也是一个interface我们也可以用Callable来创建一个线程。
Callable是一个带有泛型的interface实现Callable接口必须重写call方法call方法带有返回值且返回值类型就是泛型类型可以借助FutureTask类接收返回值更方便的帮助程序员完成计算任务并获取结果
下面我们来举一个案例要求分别使用实现Runnable接口和Callable接口完成计算123…1000并在主线程中打印结果的任务体会区别。
1.1 使用Runnable接口
public class ThreadDemo01 {private static int result 0;public static void main(String[] args) throws InterruptedException {Thread t new Thread(new Runnable() {Overridepublic void run() {for (int i 1; i 1000; i) {result i;}}});// 启动线程t.start();t.join();// 打印结果System.out.println(result: result); // 500500}
}上述代码实现Runnable接口来启动一个线程这段代码可以实现累加1-1000的任务但是不够优雅因为需要额外借助一个成员变量result来保存结果当业务量繁多时如果有多个线程完成各自的计算任务那么就需要更多的成员变量保存结果因此现在想想带有返回值的线程也许是有必要的下面我们来看看如何使用Callable接口完成
1.2 使用Callable接口
public class ThreadDemo02 {public static void main(String[] args) throws ExecutionException, InterruptedException {CallableInteger callable new CallableInteger() {Overridepublic Integer call() throws Exception {int result 0;for (int i 1; i 1000; i) {result i;}return result;}};// 创建FutureTask实例FutureTaskInteger futureTask new FutureTask(callable);// 创建线程并启动Thread t new Thread(futureTask);t.start();// 阻塞并获取返回结果int result futureTask.get();System.out.println(result: result); // 500500}
}上述代码中我们使用了实现Callable的方式完成任务值得一提的是Callable实现类对象不能直接作为Thread构造方法的参数我们需要借助一个中间人即 FutureTask 类该类实现了Runnable接口因此可以直接作为Thread构造方法的参数。 注futureTask.get()方法带有阻塞功能直到子线程完成返回结果才会继续运行 1.3 如何理解FutureTask和Callable
理解Callable1、Callable与Runnable是相对的都是描述一个任务Callable描述带有返回值的任务Runnable描述不带返回值的任务。2、Callable往往需要搭配FutureTask一起使用FutureTask用于保存Callable的返回结果因为Callable中的任务往往在另一个线程执行具体什么时候执行并不确定 理解FutureTaskFutureTask顾名思义就是未来任务即Callable中的任务是不确定何时执行完毕的我们可以形象描述为去杨国福吃麻辣烫时等待叫号但是什么时候叫号是不确定的通常点餐完毕后服务员会给一个取号凭证我们可以凭借这个取号凭证查看自己的麻辣烫有没有做好。
1.4 相关面试题
介绍下Callable是什么
2. 信号量Semaphore
相信大学期间修过操作系统课的小伙伴对semaphore这个单词并不陌生这不就是OS的信号量机制可以实现PV操作的么而JVM对OS提供的semaphore又进行了封装形成了Java标准库中的Semaphore类 semaphore信号量用来表示可用资源的数目本质上是一个计数器。 我们可以拿停车场的展示牌进行举例当前留有空闲车位100个时展示牌上就会显示100表示可用资源的数目当有汽车开进停车位时相当于申请了一个可用资源此时展示牌数目就会-1称为信号量的P操作当有汽车驶出停车位相当于释放了一个可用资源此时展示牌数目1称为信号量V操作 Semaphore的信号量PV操作都是原子性的可以直接在多线程环境中使用。 锁其实本质上就是特殊的信号量加锁可以看作时信号量为0解锁可以看作信号量为1所以也有说法称锁其实是一个二元信号量那么既然锁机制能够实现线程安全信号量也可以用来保证线程安全 2.1 Semaphore代码案例
Semaphore相关API
acquire相当于P操作申请一个可用资源release相当于V操作释放一个可用资源
代码举例
创建Semaphore实例并初始化为4表示有4个可用资源创建20个线程每个线程都尝试申请资源sleep1秒后释放资源观察程序运行状况
public class SemaphoreExample {public static void main(String[] args) {// 初始化信号量为4Semaphore semaphore new Semaphore(4);// 创建20个线程for (int i 0; i 20; i) {int id i;Thread t new Thread(() - {try {semaphore.acquire(); // 申请资源System.out.println(线程 id 申请到了资源);Thread.sleep(1000); // 睡眠1ssemaphore.release(); // 释放资源System.out.println(线程 id 释放资源);} catch (InterruptedException e) {throw new RuntimeException(e);}});// 启动线程t.start();}}
}执行结果 其中可以发现当前四个线程申请资源后此时第五个线程尝试申请资源后信号量变为-1就会阻塞等待直到其他线程释放资源后才可以继续申请
3. CountDownLatch的使用
CountDownLatch可以等待N个任务全部完成。类似于N个人进行跑步比赛直到最后一个人跃过终点才会宣布比赛结束公布最后成绩。
3.1 CountDownLatch相关API
CountDownLatch提供API
new CountDownLatch(int n)构造方法初始化n表示有n个任务需要完成countDown()任务执行完毕后调用内部计数器进行自减await()阻塞等待所有任务全部完成后继续执行相当于等待内部计数器为0
3.2 CountDownLatch代码案例
CountDownLatch代码案例
创建10个线程并初始化CountDownLatch为10每个线程随机休眠1-5秒模拟比赛结束主线程中使用await阻塞等待全部执行完毕
public class CountDownLatchExample {public static void main(String[] args) throws InterruptedException {Random random new Random();// 初始化CountDownLatch为10CountDownLatch latch new CountDownLatch(10);// 创建10个线程for (int i 0; i 10; i) {int curId i;Thread t new Thread(() - {System.out.println(线程 curId 开始执行...);try {Thread.sleep(random.nextInt(6) * 1000);System.out.println(线程 curId 结束执行...);latch.countDown(); // 计数器自减} catch (InterruptedException e) {throw new RuntimeException(e);}});// 启动线程t.start();}// 阻塞等待全部执行完毕latch.await();System.out.println(全部任务执行完毕);}
}执行结果 此时我们可以看到调用countDownLatch.await()方法时只有当所有的线程都执行完毕即调用10次countDown方法后即内部计数器为0时才会停止阻塞
4. ReentrantLock类
ReentrantLock位于java.util.concurrent包下被称为可重入互斥锁和synchronized定位类似都是用来实现互斥效果的可重入锁保证线程安全。
4.1 ReentrantLock的用法
ReentrantLock相关API
lock()尝试加锁如果加锁失败就阻塞等待tryLock(超时时间)尝试加锁获取不到就阻塞等待一定时间超时则放弃加锁unlock()解锁
注意由于ReentrantLock需要手动释放锁因此常常把unlock方法放在finally块中
ReentrantLock lock new ReentrantLock();
-------------- 相关代码 --------------
lock.lock();
try {// do something...
} finally {lock.unlock();
}4.2 ReentrantLock与synchronized的区别经典面试题
synchronized是一个关键字是JVM内部实现的大概率使用C语言实现而ReentrantLock是一个标准库中提供的类是在JVM外部实现的基于Java实现ReentrantLock使用lock和unlock一对方法进行手动加锁、解锁相较于synchronized更加灵活但是也容易遗漏释放锁的步骤。synchronized是非公平锁而ReentrantLock默认是非公平锁但是可以变成公平锁只需要在构造方法中传入参数为true即可。ReentrantLock比synchronized具有更加精准的唤醒机制synchronized使用Object类的wait/notify进行唤醒随机唤醒一个等待线程而ReentrantLock借助类Condition实现等待唤醒可以精准控制唤醒某一个指定线程。
5. CAS机制
5.1 CAS的基本概念
CAS全程为compare and swap字面意思就是比较并且交换一个CAS涉及以下操作 我们假设内存中的原数据为V旧的预期值为A需要修改的新值为B 比较A与V是否相等比较如果比较相等则将B写入内存替换V交换返回操作是否成功。 CAS伪代码
public boolean CAS(address, expectValue, swapValue) {if (address expectValue) {address swapValue;return true;}return false;
}注意CAS是一个硬件指令其操作是原子性的这个伪代码只是辅助理解CAS的工作流程 对CAS原子性的理解CAS可以看做是乐观锁的一种实现方式当多个线程同时对某个资源进行CAS操作时只要一个线程操作成功返回true其余线程全部返回false但是不会阻塞。 5.2 CAS的常见应用
5.2.1 实现原子类
标准库中提供了java.util.concurrent.atomic包里面的类都是基于CAS的方式实现的最典型的就是AtomicInteger类其中的getAndIncrement方法相当于i操作
AtomicInteger atomicInteger new AtomicInteger(0);
// 相当于i操作
atomicInteger.getAndIncrement();下面是AtomicInteger类基于CAS方式的伪代码实现
class AtomicInteger {private int value;public int getAndIncrement() {int oldValue value;while (CAS(value, oldValue, oldvalue 1) ! true) {oldValue value;}return oldValue;}
}5.2.2 实现自旋锁
基于CAS也可以实现更加灵活的锁获取到更多的控制权 下面是基于CAS的自旋锁伪代码实现
class SpinLock {private Thread owner;public void lock() {while (!CAS(owner, null, Thread.currentThread())) {}}public void unlock() {this.owner null;}
}5.3 CAS的ABA问题
5.3.1 ABA问题概述
什么是ABA问题假设现在有两个线程共用一个共享变量num初始值为A此时线程t1期望使用CAS机制将num值修改为Z线程t1的CAS判定流程如下
如果此时num A那么就将内存中的num值修改为Z如果此时num ! A那么就不进行修改重新判定
但是此时中间穿插了线程t2执行将num值修改为了B但是又重新修改为了A所以尽管t1线程比较的预期结果是一致的但是很可能已经是别人的形状了
ABA导致的问题
ABA问题有可能会导致严重的后果比如说我去银行ATM机取钱考虑采用的时CAS机制目前我的账户余额为1500我按下确定按钮想要取出500元钱但是ATM机卡住了因此我又按下一次确定按钮此时正常情况如下 正常情况 此时结果是符合预期的扣款线程t1进行扣款操作此时余额变为1000元扣款线程t2判断当前余额已经不为1500了说明已经扣款成功于是不进行后续扣款操作但是如果中间出现其他线程汇款操作就会出现ABA问题导致严重后果 极端情况 此时扣款线程t1完成扣款操作后余额变为1000但是中间穿插了汇款线程t3刚好往账户中存入金额500此时扣款线程t3判断余额仍然为1500因此又进行了多余的一次扣款操作这是相当不合理的
5.3.2 ABA问题解决思路
为了解决ABA带来的问题我们可以考虑使用 版本号 的思想解决
CAS读取数据的时候同时也要读取版本号如果当前版本号与读取版本号一致修改数据同时将版本号1如果当前版本号高于读取版本号则说明数据已经被修改当前操作非法
6. 线程安全的集合类
Java提供的集合类大部分都是线程不安全的 VectorStackHashTable是线程安全的但是官方不推荐使用 6.1 多线程环境使用ArrayList
自己加锁使用synchronized或者ReentrantLock实现使用Collections.synchronizedList这是标准库提供的基于synchronized实现的线程安全的类本质上是在关键方法上加了synchronized修饰使用CopyOnWriteArrayList这是一个借助写时拷贝机制实现的容器常用于配置文件等不经常修改占用内存较小等场景 写时拷贝机制的核心就是可以对原容器进行并发的读涉及写操作则先对原容器进行拷贝然后向新容器中添加元素最后修改引用实现了读写分离 6.2 多线程环境使用队列
自己加锁实现synchronized或者ReentrantLock实现使用标准库提供的BlockingQueue接口及其实现类
6.3 多线程环境使用哈希表 使用HashTable 只是在关键方法上加上synchronized进行修饰 ConcurrentHashMap常考面试题 相比于Hashtable做出了一系列优化以JDK1.8为例 优化方式一读操作没有加锁只有写操作才会加锁加锁的方式仍然使用synchronized但是并不是锁整个对象而是锁桶使用链表的头结点作为锁对象大大降低了锁冲突的概率优化方式二对于一些变量例如哈希表元素个数size使用CAS机制避免重量级锁出现优化方式三优化了扩容方式采用化整为零蚂蚁搬家的方式扩容期间新老数组同时存在一次搬运只搬运少量元素因此新增操作只需要在新数组中插入即可查询操作需要在新老数组同时查询删除操作也需要新老数组同时删除