JUC(java.util.concurrent)的常见类
Callable(这是一个interface接口)
这个也是创建线程的一种方式
Runnable能表示一个任务(run方法)
返回:void
Callable也能表示一个任务(call方法)
返回:一个具体的值,类型可以通过泛型参数来指定(Object)
如果进行多线程操作,只是关心多线程执行的过程,使用Runnable即可.(像,线程池,定时器 )
如果是关心多线程的计算结果,使用Callable更合适这个是使用Runnable来实现从1加到1000,最后返回值的代码.下面我们用Callable的方式写一个一模一样功能的代码
public class Demo {
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中的,但是Runnable可以,
// 原因就是Runnable不在意返回值,但是Callable在意返回值.
//这里要用到FutureTask.
FutureTask<Integer> futureTask=new FutureTask<>(callable);
Thread t1=new Thread(futureTask);
t1.start();
//这里需要用到FutureTask的get方法来获取Callable最后的结果.
Integer result=futureTask.get();
System.out.println(result);
}
}
注意看注释!!!
FutureTask.get();这个是用来获取call方法的返回结果的,get类似于join一样,如果call方法没执行完,就会阻塞等待.
ReentrantLock(可重入锁)
这个锁,没有synchronized那么常用,但是也是一个可以选择的加锁的组件,这个锁在使用上更接近于C++.这个里面有lock()方法以及unlock()方法,但是我们这样用的时候就容易出现,unlock调用不到的情况,比如,中间return/抛出异常.
ReentrantLock具有一些特点是synchronized不具备的功能
优势
1.提供了一个tryLock方法进行加锁
对于lock这样的一个操作,如果枷锁不成功,就会阻塞等待(死等).对于tryLock而言,如果加锁失败,直接返回false/也可以设定等待时间.
2.ReentrantLock有两种模式,可以工作在公平锁状态下,也可以工作在非公平锁的状态下,构造方法中通过参数设定的 公平/非公平模式
3.ReentrantLock也有等待通知机制,搭配Condition着花样的类来完成,这里的等待通知要比wait notify功能更强.
劣势
1.synchronized锁对象是任意对象,RenntrantLock锁对象就是自己本身,如果你多个线程针对不同的ReentrantLock调用Lock方法,此时是不会产生锁竞争的.
信号量 Semaphore
这个在操作系统中也经常出现,Semaphore是并发编程中的一个重要的概念/组件.
准确来说,Semaphore是一个计数器(变量),描述了"可用资源(别称:临界资源)的个数",临界资源其实就是多个线程/进程等并发执行的实体可以公共使用到的资源(多个线程修改同一个变量,这个变量就可以认为是临界资源)
例子:假设我们现在要去一个停车场去停车,停车场的入口,上面有一个显示屏,显示了一行字:还有XX个空余车位(这里的XX就是信号量).如果我开车进入停车场,就相当于释放了一个车位(申请了一个可用资源),此时计数器就要-1,称为P操作accquire,如果我开车出了停车场,就相当于,释放了一个车位(释放了一个可用资源),此时计数器就要+1,称为V操作release.当计数器为0的时候,继续进行P操作,就会阻塞等待,一直等到其他线程执行了V操作,释放了一个空闲资源为止.锁,蹦智商是一个特殊的信号量(里面的数值,非0即1,二元信号量)信号量要比锁更广义,不仅仅可以描述一个资源,还可以描述N个资源.
public class Demo {
public static void main(String[] args) throws InterruptedException {
//这里可以通过构造方法来指定计数器的初始值
Semaphore semaphore=new Semaphore(3);
semaphore.acquire();
System.out.println("执行了一个P操作");
semaphore.acquire();
System.out.println("执行了一个P操作");
semaphore.acquire();
System.out.println("执行了一个P操作");
semaphore.acquire();
System.out.println("执行了一个P操作");
}
}
由于我们的初始值只有三个,但是我们这里写了四个P操作,所以在执行第四个P操作的时候会进入阻塞等待.
CountDownLatch
这个是针对特定场景的一个组件,场景:下载某个东西,有的时候下载文件并不是你的网速慢,更多的时候是因为人家服务器的限制,有一些"多线程下载器",就可以把一个大的文件,拆分成多个小的部分,是哟几个多个线程分别下载,每个线程负责下载一部分,每个线程分别是一个网络连接,这样就可以大幅提高下载速度.假设,我们这里分成了10个线程,10个部分来下载,啥时候算是下载完了??10个部分都下载完了,整体才算完成,那我们怎么来判定10个部分都下载完了呢?
CountDownLatch:当需要把一个任务拆分成多个任务,如何衡量现在是把多个任务都搞定了呢?
public class Demo2 {
public static void main(String[] args) throws InterruptedException {
//CountDownLatch的构造方法中所写的数字就是我们这里分成了几个任务
CountDownLatch countDownLatch=new CountDownLatch(10);
for (int i = 0; i < 10; i++) {
int id=i;//这个就可以修正下面因为变量捕获所带来的一些不必要的麻烦
Thread t=new Thread(()->{
//这里直接写: +i+ 会报错,为什么?
//1.涉及到lambda表达式的变量捕获,前面说过了
System.out.println("任务"+id+"开始执行");
//这里用sleep来代替下载所需要的时间
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("任务"+id+"结束");
//每个任务执行到这里都调用一下方法,就好比接力赛,每个运动员到这里都撞线了.
countDownLatch.countDown();
});
t.start();
}
//主线程如何知道任务结束了呢?
//总不能用join吧?假设我们有1万个线程,难道你要写1万个join吗?
//而且有的时候任务结束并不代表线程结束,万一任务需要结束,但是由任务所创建出来的线程不需要结束,
// 那么join不是也就不行了吗?
//在这里,主线程中可以使用 CountDownLatch 来负责等待任务结束.
//如果上方countDown这里的次数<设定的初始值
//await就会阻塞
countDownLatch.await();
System.out.println("多个线程的所有任务都执行结束了");
}
}
运行结果如下
集合类
在Java集合类中,哪些是线程安全的?也就是多个线程同时操作这个集合类,是否会产生问题.
Vector,Stack,HashTable是线程安全的,其中Vector与HashTable是不建议使用的,其他的集合类是线程不安全的.Vector和HashTable属于是Java上古时期搞出来的集合类.那个时候人们对多线程编程的认识还不够深刻,所以在这里Vector与HashTable都过分的加了锁,有的时候,加了锁,不一定就线程安全,不加锁也不一定就线程不安全=>务必要具体问题具体分析.
所以,Vector/HashTable这样的集合类,虽然加了synchronized也不能保证一定是线程安全的.同时,在单线程的情况下,又可能因为synchronized影响到执行效率,所以,后续设计的集合类,就不再是这种加锁方式了.
多环境使用ArrayList
1.自己使用同步机制(ReentrantLock或者synchronized)(说人话就是自己手动加锁)
2.Collections.synchronizedList(new ArrayList)(很少会真用这个)
ArrayList本身没有使用synchronized,但你又不想自己加锁,就可以使用这个,相当于让ArrayList像Vector一样工作.
CopyOnWriteArrayList(写时复制)
多个线程同时修改同一个变量,会出现线程安全问题,那么如果多个线程修改不同变量,是不是就安全了呢?
如果多线程去读取,本身就不会有任何线程安全问题,一旦有线程修改,就会把自身复制一份,尤其是修改如果比较耗时的话,其他线程还是从旧的数据上读取,一旦修改完成,就使用新的ArrayList替换旧的ArrayList(本质上就是一个引用的重新赋值,速度极快,并且又是原子的)这个过程中,没有引入任何加锁操作,使用了创建副本=>修改副本=>使用副本替换的方式
ConcurrentHashMap
线程安全的hash表
HashTable是在方法上直接加上synchronized,就相当于针对this加锁.
任意的针对ht对象的操作,都会涉及到针对this的加锁.此时,如果有很多线程都想操作ht,就一定会触发激烈的锁竞争,这些线程最后只能一个一个排着队,依次进行(这种情况,并发程度很低)
如果两个修改操作是针对两个不同的链表进行修改,是否会存在线程安全问题呢?? 显然是不会的,所以原来的HashTable给每一个插入以及其他操作都加锁,这种情况是没有必要的,因为加锁这个操作本身的开销就是比较大的,而且也会这样也会造成不必要的阻塞等待.还有一种情况,针对同一个链表修改时,如果两个线程修改的是同一个链表中的同一个位置,那么会造成线程安全问题,针对同一个链表的不同位置修改时,就不会造成线程安全问题.
具体的做法就是给每个链表都去安排一把锁
这样的话,我们的锁冲突的概率就打打降低了,那么怎么给每一个链表都上锁呢?由于Java的synchronized随便拿个对象就可以加锁,所以我们就拿每个链表的头结点来进行加锁
ConcurrentHashMap改进:
1.[核心]减小了锁的粒度,每个链表都有一把锁,大部分情况下都不会涉及到锁冲突.
2.广泛使用了CAS操作,减小了锁冲突的概率
3.写操作进行了加锁(链表级),读操作就不加锁了
4.针对扩容操作进行了优化,渐进式扩容
HashTable一旦触发扩容,就会立即的一口气完成所有元素的搬运,这个过程相当耗时.就会出现大部分请求都很顺畅,突然某个请求就卡了比较久这样的情况,要解决这个问题也非常简单:化整为零,当需要进行扩容的时候,会创建出另一个更大的数组,然后把旧的数组上的数据逐渐的往新的数组上搬运.
1.新增元素,往新数组上插入
2.删除元素,把旧数组的元素给删掉即可
3.查找元素,新数组旧数组都得查找
4.修改元素,统一把这个元素给搞到新数组上
与此同时,每个操作都会触发一定程度搬运,每次搬运一点,就可以保证整体的时间不是很长,积少成多之后,逐渐完成搬运了,也就可以把之前的旧数组彻底销毁了.
那么话说回来,HashMap和ConcurrentHashMap有什么区别吗?
线程安全和线程不安全之间的区别.
关于ConcurrentHashMap的分段技术?
Java 8 之前,ConcurrentHashMap是使用分段锁,从Java 8 开始,就是每个链表自己一把锁了.
就像这样,这样虽然能提高效率,但是不如每个链表一把锁,代码实现起来也更复杂
标签:JUC,加锁,java,synchronized,链表,util,线程,Callable,操作 From: https://blog.csdn.net/X_HJJ/article/details/140176592