日升时奋斗,日落时自省
目录
1、Callable接口
1.1、Callable方式
1.2、非Callable方式
2、JUC(java.util.concurrent)的常见类
2.1、ReentrantLock
2.2、信号量Semaphore
2.3、CountDownLatch
3、线程安全的集合类
3.1、多线程使用ArrayList
3.2、多线程使用哈希表
3.2.1、Hashtable
3.2.2、ConcurrentHashMap
1、Callable接口
Callable接口类似于Runnable一样(稍有区别)
Runable 用来描述一个任务,描述的任务没有返回值
Callable 也是用来描述一个任务, 描述的任务有返回值
Callable适用条件也就一目了然了,如果是一个线程单独计算某个值出来的话,Callable就比较合适。
1.1、Callable方式
那我们简单看一下 Callable接口使用
Callable<Integer> callable=new Callable<Integer>() {
@Override
public Integer call() throws Exception {
//这里就是简单写一个加加的例子
int sum=0;
for(int i=0; i<1000;i++){
sum+=i;
}
//主要是说明Callable可以有返回值
return sum; //这里哈
}
};
在创建一个类的时候会带有call方法,call方法就相当于是Runnable的run方法,run方法返回值是void,此时的call返回值是Integer,这里泛型的参数是Integer,所以返回值由 泛型参数决定。
既然类似Runable就 在创建线程那也就是不能直接调用的了。
所以走特殊途径这里就涉及到一个辅助类(FutureTask)
FutureTask刚刚出现,不容易记住与接受
举例理解:在一家饭店里,创建的callable对象就是一碗饭, 但是一家店里不是一个人来吃饭,所以这碗饭是谁的就需要证明了,FutureTask就是单子(小票)证明它是那为客人点的,有单子了,这不就顺理成章了,直接开锅做饭(放到线程里就知道对象是谁了,可以运行)
那FutureTask创建的辅助类已经接收了返回值,如何拿出来???
FutureTask内部方法get()就可以取出来刚刚的sum
这里的get是要抛两个异常的ExecutionException, InterruptedException一个是执行异常,另一个是阻塞异常,所以get是获取结果的,get也会发生阻塞,直到callable执行完毕,get才阻塞完成,才会获取结果
//java标准库中提供了专门的Callable接口来解决返回值的问题
/*
* 代码写起来也比Thread更加理解
* */
public static void main(String[] args) throws ExecutionException, InterruptedException {
Callable<Integer> callable=new Callable<Integer>() {
@Override
public Integer call() throws Exception {
int sum=0;
for(int i=0; i<1000;i++){
sum+=i;
}
return sum;
}
};
//但是这里的Callable 是不能直接装在Thread里面的
/*
* 需要一点点的东西 , 通过一个媒介就行
* */
FutureTask<Integer> futureTask=new FutureTask<>(callable);
Thread t=new Thread(futureTask);
t.start();
int result=futureTask.get(); //这里的get需要抛出两个异常
System.out.println(result);
}
这里把代码附上,这样写出来是不是比较好看,也很清晰,本身代码量少。就是接触两个新的东西一个Callable和FutureTask辅助类 理解后就不会看着很难了;相比Thread,来实现返回值也是可以的但会比现在的麻烦一点,下面就介绍这种方法,多一种方法,多一种理解。
1.2、非Callable方式
思路:
(1)创建一个自定义类 定义一个计算和的变量, 一个Object对象
(2)main方法中创建一个线程然后run方法中写计算和
(3)计算和写完后,仍在线程内写一个赋值与唤醒(唤醒谁呢,看(4))
(4)线程开始执行后,此时写wait等待,等待run方法执行完了,唤醒我,再打印当前计算和值
class Result{
public int sum=0; // 防止局部作用域的影响
public Object lock=new Object(); //进行判定的
}
public class NoCallable {
//Thread是没有返回值的,但是线程也可能会需要用到,在Thread中也是可以做到的
public static void main(String[] args) throws InterruptedException {
Result result=new Result();
// 放置对象的
Thread t=new Thread(){
@Override
public void run() {
int sum=0;
for(int i=0;i<100;i++){
sum+=i;
}
synchronized (result.lock){ //如果当前对象没有锁 这里就加锁当前部分
result.sum=sum; //这里是要采取赋值的,为什么因为当前sum是在run方法内定义的,拿不出去,只能赋值给自定义类的变量
result.lock.notify(); //唤醒是为了,让主线程在结束位置等等,要不还没有执行,就结束,看不到任何结果
}
}
};
t.start();
synchronized (result.lock){
//结果值不能为0 ,为0了等于没有加 出问题了,这里谨慎一点
while (result.sum==0){
//在线程开始执行后,等待run方法执行结束 唤醒我才能往后走,进行打印
result.lock.wait();
}
//如果不在最后这是wait就会导致主线程提前结束,可能得不到任何结果
System.out.println(result.sum);
}
}
}
附代码上有注释,这里就不做过多解释了
2、JUC(java.util.concurrent)的常见类
2.1、ReentrantLock
我们现在以提到锁基本就是synchronized,ReentrantLock同样是一种锁;
reentrant(翻译:可重入)感觉这个新单词不好记,那英语的办法记 entry是这个词的词根,加了一个re的前缀,和一个后缀ant就成了,类就是在该单词后面加了Lock(翻译:锁)
那既然都是锁,就有了可比性
区别:
相同点:synchronized和ReentrantLock两把锁都是可重入的(相同点比较少)
不同点:
(1)synchronized是直接基于代码块方式来加锁解锁的,ReentrantLock比较传统,使用lock方法和unlock方法加锁解锁
public static void main(String[] args) {
ReentrantLock now=new ReentrantLock(); //创建类
//传统 方法加锁
now.lock();
//传统 方法解锁
now.unlock();
}
(2)synchronized是一个关键字是有JVM内部实现,ReentrantLock是java标准库里的一个类,是JVM外部实现的。
(3)synchronized使用时不需要手动释放锁,ReentrantLock是手动加锁和释放锁,使用更灵活,但是也容易遗漏unlock
为什么这么说??? 因为不是所有情况都是代码结束了才返回
public static void main(String[] args) {
ReentrantLock now=new ReentrantLock();
//传统 方法加锁
now.lock();
/*
* 如果 在if中返回了不是就没有经过 unlock操作
* */
if(true){
return ;
}
//传统 方法解锁
now.unlock();
}
问题出现了,如何解决? 在if中return前面加一个解锁,但是if操作多了怎么办一个一个加嘛,总会有漏的时候;此时使用另一个办法就是直接在最后加一个finally程序执行结束必定会执行解锁
public static void main(String[] args) {
//ReentrantLock 是一个可重入锁 reentrant就是可重入的意思
ReentrantLock lock=new ReentrantLock();
lock.lock(); //这就是简单的上锁
try {
/*
* 但是有一个缺陷就是 解锁可能会被忘记 如果添加一个 if 语句就会导致当前 程序提前结束
* */
if (true) {
return;
}
/*
* 为了避免这种情况 直接用finally 来处理 因为 finally 是一定会执行的
* */
}finally {
lock.unlock(); //解锁 是比较灵活的
}
}
(4)synchronized在申请锁失败的时候选择方式死等,ReentrantLock可以通过trylock方法进行一段时间等待后如果没有解锁,就放弃锁;没有设置等待时间就直接放弃锁。(trylock会返回一个布尔类型的值)
问题:那放弃锁,不就是没有加锁,那还要解锁吗?回答:当然不用
处理方式:在finally里面加一个if语句进行判定就行 如果加锁 就解锁, 没有加锁就不管
public static void main(String[] args) {
ReentrantLock lock=new ReentrantLock();
boolean flag=lock.tryLock(); //如果没有设定时间的话就是立刻放弃
/*
* 如果设置时间 时间到了,就会放弃这个锁 提高效率
* */
/*
* 疑惑就是 不知道是否上锁, 该方法返回值是 boolean 用一个变量来接收if判断
* */
try{
} finally {
if(flag){
//加锁 就可以解锁
}else{
//没有加锁 不用解锁
}
}
}
(5)synchronized是非公平锁,ReentrantLock默认是非公平锁,可以通过构造方法传入一个true变成公平锁(可以在Idea中自信点开看 一个参数的构造方法)
(6)唤醒机制:synchronized是通过Object的wait/notify实现等待-唤醒,每次唤醒的是一个随机线程,ReentrantLock搭配Condition类实现等待唤醒,可以唤醒某个指定线程
使用选择
注:上面的区别就是使用的偏向 (这里只是大体总结一下)
<1> 锁竞争不激烈的时候,使用synchronized效率更高,自动是释放更方便
<2>锁竞争激烈的时候,使用ReentrantLock可以控制时间,不需要一直死等,看自己的检测情况
<3>想用公平锁就是ReentrantLock
2.2、信号量Semaphore
友情提醒:操作系统信号量和java中的信号量不是一个东西
此处的信号量用来表示“可用资源的个数”。本质上就是一个计数器
什么是可用资源个数???
举例解释:就以酒店来说,有人住店房间数就会-1, 住店者离开就会空房数+1 ,空房数就是可用资源个数。
PV操作:
P操作:申请(acquire)一个可用资源 ,计数器就要 - 1
V操作:释放(release)一个可用资源 , 计数器就要 + 1
P操作如果造成计数器等于0了,继续P操作就会出现阻塞(就像顾客来住店没有房间了,只能等就是阻塞)
信号量可以延伸
计数初始值为1 的信号量 那就只有1和0两种结果(信号量没有负值)
执行一次P操作 1->0
执行一个V操作 0->1
如果已经进行一次P操作了,继续进行P操作就会阻塞:这里就像是一个线程加一把锁,另一个线程还想加这把锁就阻塞了
锁是信号量的一种特殊情况 ,信号量就是是锁的一般表达
这里就不难看出Semaphore能实现类似于锁的效果,来保证线程安全
这里附一个代码演示使用刚刚申请(acquire)方法和 释放(release)这两个方法
// Semaphore 就像是一个裁判一样 能够限制任务做几个 信号量 java中的表示
public static void main(String[] args) {
//限制了当前只能有四个信号量 所以即便是多线程执行 一次也只能执行四个任务,其他线程得等着
Semaphore semaphore=new Semaphore(4);
Runnable runnable=new Runnable() {
@Override
public void run() {
try {
synchronized (this){
semaphore.acquire(); //该方法就是 申请一个位置
System.out.println( "申请资源");
}
Thread.sleep(500);
semaphore.release();
System.out.println("释放资源"); //释放一个位置
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
};
for(int i=0; i<20;i++){
//这里创建了20个线程 不会一次全部执行完的
Thread t=new Thread(runnable);
t.start();
}
}
2.3、CountDownLatch
作用同时等待N个任务结束
举例解释:以比赛为例:一个田径比赛 一个比赛10个人,裁判会等每个人会来才会结束比赛(就是这个意思)
(1)CountDownLatch实例,初识10个值
(2)每个线程(人)执行任务完成,latch.countDown()。该方法内部计数减减
(3)使用latch.await();阻塞等待所有任务执行完毕,阻塞等待才会结束(这里在详细一点,等第10个人执行了latch.countDown()方法 await就结束了)
//能同时计时多个任务
public static void main(String[] args) throws InterruptedException {
CountDownLatch lacth=new CountDownLatch(10);
Runnable runnable=new Runnable() {
@Override
public void run() {
try {
Thread.sleep((long) Math.random()*1000);
lacth.countDown(); //内部计数器减减
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
};
for (int i=0;i<10;i++){
//是个线程都执行
new Thread(runnable).start();
}
//十个人都会来才算是结束
lacth.await();
System.out.println("比赛结束");
}
虽然这里说是所有线程都执行结束,当然也可以不是所有的,根据自己的情况设计的等待个数。
在实际中有很多地方可以用得到,例如视频文件下载,多个线程分块下载提升速度,多线程下载不是充分利用了多核CPU,而是充分李利用了IO(下载是IO操作),所以现在使用CountDownLatch多个线程执行结束也就下载完成,
3、线程安全的集合类
java标准库大部分集合类都是“线程不安全”的,多线程使用同一个集合类对象,很可能不安全
安全的有哪些:Vector,Stack ,HashTable这几个类线程安全(内部带有synchronized),但是在线程上不常用
3.1、多线程使用ArrayList
如何安全使用 ,ArrayList本身是不安全的
(1)自己加锁,使用synchronized或者 ReentrantLock 加锁
(2)使用集合类套一层锁,Collects.synchronizedList 这里会提供一些ArrayList相关的方法,同时是关键操作带锁的(加的是synchronized锁),使用方式大同小异。
List<Integer> list=Collections.synchronizedList(new ArrayList<>());
(3)CopyOnWriteArrayList(拷贝用的)简称COW 也叫做“写时拷贝” 如果针对这个ArrayLIst进行操作,则拷贝一份新的ArrayList,针对新的容器进行修改,修改过程中如果有读操作,就继续读旧的容器的数据,当修改完毕了,使用新容器替换旧容器(这样比较保险,单方面只拷贝的话,中途有数据写入时就不好处理了(脏读问题))
优点:不需要加锁,执行速度快,在多读少写的场景下
缺点:占内存多,开辟数组不大。
总述:只是适用于数组比较小的情况下
方法不一定常用,可以了解了解思维:
举例解释:以服务器为例子,服务器程序的配置 、维护(配置文件很常听),服务器中也存在有想要的功能和不想要的功能(针对不同人),所以就会有服务器修改配置文件,修改后一般服务器都是要关了再重开的。(数据库mysql为例 配置文件 my.ini)
但是重启操作可以能成本比较高,这里就不拿本地电脑mysql举例了,因为mysql自己电脑上就算是服务器关了再开也不会对你产生什么影响,此处假设一个服务区重启需要花5分钟时间(大型服务器),现在服务器接收多了一般也不会采用一台,分布式解决问题,10台这样的服务器,重启就会消耗50分钟时间,(不能同时重启这10个服务器,如果同时重启,就没有办法在接收响应了,带来的损失也会很大)。
这里其实就明白服务器重启是比较不好用的,服务器也有提供“热加载”这样的功能,通过这样的功能就可以不重启,实现配置更新,就代表服务器也不需要担心服务器拥挤,“热加载”就可以“写时拷贝”的思路。
写时拷贝总述:新的配置放在新的对象中,加载过程中,请求仍然基于旧的配置进行工作,当新的对象加载完毕,新的配置可以代替旧的了,旧的也就释放了
3.2、多线程使用哈希表
HashMap本身是不具有线程安全的
哈希表很也好用,便捷,在多线程环境下也可以使用,需要其他类Hashtable 、ConcurrentHashMap这两个类来调用哈希表,下面分开说这两个类
3.2.1、Hashtable
Hashtable是线程安全的 ,给关键方法 ,加了synchronized
这里就看两个比较常见的方法,当然Hashtable是java标准库的方法,出来这两个以外其他的方法也有加锁的,有想法的友友们可以去看看这里就不详细说还有那些了
以上get和put方法来看就是直接对Hashtable对象本身加锁
(1)就是相当于多个线程访问Hashtable就会造成锁冲突。
(2)size也是加锁了的,现在就比较慢了
但是同样解决了线程安全问题,这里画一个图来接解释可能的线程安全问题
上图有有从两个方面看,只有一条链的时候有问题,但是是整个对象, 但凡来一个线程想要调度都会导致锁冲突,冲突概率太大了,及时是修改两条不同的链表也会产生冲突就很不划算
所以我们就开始说说ConcurrentHashMap针对这一情况的好处
3.2.2、ConcurrentHashMap
其实ConcurrentHashMap是很相近的,就是锁上的区别,锁实现于每个链表各自加锁(不是整个对象加锁,大家也就不公用一把锁)
这里还是花图解释
(1) ConcurrentHashMap的锁粒度变细了相比Hashtable中的锁粒度,但是早JDK1.8之前ConcurrentHashMap是:“分段锁”,顾名思义就是将数组分为2或者3个一组加锁,加锁的数量有所减少,但是仍然有可能多线程执行的时候会造成线程安全问题,不能排除多个线程呢访问的是同一个锁下的不同链表。(也就是问题二的情况)
(2)ConcurrentHashMap针对读操作不加锁,只针对写操作加锁(回顾锁相关)
<1>读操作和读操作之间是没有冲突的
<2>读操作和写操作之间是存在冲突的
<3>写操作和写操作之间是存在冲突的
其实读操作不加,只加锁写操作(synchronized加锁),会导致读了一个写了一半的结果(脏读),那是因为写操作不是“原子”但是ConcurrentHashMap使用了volatile+原子的写操作,保证从内存读取结果。
(3)ConcurrentHashMap内部充分的使用了CAS,CAS是特殊方式保证线程安全,减少了加减锁的操作次数
(4)针对扩容,采取了“化整为零”的方式
前面没有说Hashtable的扩容怎么样,因为他和HashMap是一样的。
创建一个更大的数组空间,把旧的数组上的链表上的每个元素都搬运到新的数组上(旧链表上产删除+新链表上插入),那既然有插入就会使用到put方法,那10个容量的哈希表很好快,但是如果个数多了呢,以亿做单位一次性put完够呛的,用户不会所有人都卡,但是总会有那么些幸运人员是比较卡的(想一下你打游戏的时候卡,感觉不那么舒服哈)。
这里是为了对比ConcurrentHashMap在扩容上的优越性
ConcurrentHashMap中,扩容采取的时候分成小部分进行扩容,进行多次完成全部扩容,创建新数组,也保留旧数组,每次put都会在新数组上添加,同时也进行小部分搬运,那现在get,也不会出现问题,get数据来源于旧数组和新数组,都会进行查询经过一段时间之后所有元素都搬运好了,最后释放旧数组