javaEE-多线程进阶-JUC的常见类
juc:指的是java.util.concurrent包,该包中加载了一些有关的多线程有关的类。
目录
一、Callable接口
FutureTask类
参考代码:
二、ReentrantLock 可重入锁
ReentrantLock和synchronized的区别:
1.ReentantLock还有一个方法:tryLock:尝试上锁。
2.ReentantLock可以实现公平锁
3,ReentrantLock和synchronized的等待通知机制不同
三、semaphore 信号量
四、CountDownLatch 同时等待多个任务执行结束
五、实现线程安全的方式总结
六、创建线程的方式总结
七.线程安全的集合类
1.多线程环境使用集合:ArrayList
1>.用synchronized加锁,保证线程安全.
2>.使用Collectios.synchronizedList(new ArrayList)
3>.使用CopyOnWriteArrayList 写时拷贝
2.多线程环境使⽤队列
1>.自己加锁保证线程安全。
2>.使用阻塞队列BlockingQueue(线程安全的).
3.多线程环境使用哈希表
1>.HashTable
2>.ConcurrentHashMap:并发哈希表
一、Callable接口
Callable是一个接口,将线程封装在方法中,带有一个返回值,用于实现计算,并获取结果类型的任务。相当于把线程封装了⼀个"返回值".⽅便借助多线程的⽅式计算结果.它的作用与Runnable类似.
计算1+2+3+...+1000:
按照之前的方法,要先定义一个成员变量,在一个线程中进行累加,并返回累加的结果,还要用join等该线程执行结束,之后才能得到结果:
这种方式感觉不太美观,Callable是一个接口,就是处理这种带有返回值的多线程任务的;
Callable是一个泛型接口,指定的类型就是要返回结果的类型。
通过 匿名内部类 实现该接口的call方法,来完成要实现的任务:
FutureTask类
在Thread类中,并没有提供构造函数来传入Callable。但JVM提供了另一个类,FutureTask类:未来任务,作为Thread类和Callable接口的 “粘合剂”,
FutureTask提供了可以传Callable的构造方法,且Thread提供了可以传FutureTask类的构造方法,这就可以将两者结合起来;FutureTask类也是泛型类,指定的类型就是传入的Callable的返回类型:
对FutureTask类的理解:
Callable完成任务是需要时间的,该任务是在未来完成的,最后取结果的时候 需要一个凭据,FutureTask 就是这个凭据。
通过调用FutureTask的get方法,可以得到Callable的返回值;get方法是带有阻塞的,当传入的Callable没有完成任务,返回数据时,get方法就处于阻塞等待状态。
参考代码:
/*** Callable接口 带有返回值* 通过匿名内部类 ,重写它的 call方法完成任务 * 通过 FutureTask类 将Thread与Callable连接*/public static void main(String[] args) throws ExecutionException, InterruptedException {Callable<Integer> callable = new Callable<Integer>(){@Overridepublic Integer call() throws Exception {int sum=0;for(int i = 1; i <= 1000; i++ ){sum +=i;}return sum;}};
// Thread thread = new Thread(callable);FutureTask<Integer> futureTask = new FutureTask<>(callable);Thread thread = new Thread(futureTask);thread.start();System.out.println("sum= "+ futureTask.get());}//普通方法:private static int sum=0;//定义一个 静态成员变量public static void main2(String[] args) throws InterruptedException {Thread t=new Thread(()->{for(int i = 1;i <= 1000;i++){sum += i;}});t.start();t.join();//要等t线程执行结束System.out.println("sum= " + sum);}
Callable和Runnable相比,Callable在实现计算,并有返回值的任务时,代码看上去更美观,别的和Runnable功能是一样的。
二、ReentrantLock 可重入锁
可重入锁,和synchronized锁类似。
这个锁提供了两个方法:lock(上锁) unlocker(解锁)
使用这个锁的时候要注意解锁代码的放置位置;上锁后,在代码执行过程中,遇到return 或者异常终止了,就可能引起 unlock没有被执行,锁没有释放。因此,正确使用ReentrantLock锁 是把unlock放在 finall代码块中,这样就能防止 锁未被释放了。
ReentrantLock和synchronized的区别:
1.ReentantLock还有一个方法:tryLock:尝试上锁。
lock:直接加锁,加锁不成功,就进行阻塞;tryLock:尝试上锁,上锁不成功,返回false,不会进入阻塞状态。提供了更多的可操作空间。
创建两个线程t1,t2;对两个线程都用tryLock上锁,t1线程先获取到了锁,并返回true;t2未获取到锁,返回了false,但并未阻塞,而是继续执行代码,打印了"Thread 02",因为未加锁成功,也就无法解锁,t2的unlock就抛出了异常;之后,t1的休眠时间到,打印“Thread 01",解锁。
2.ReentantLock可以实现公平锁
ReentantLock提供的实现公平锁的方法,通过一个队列,按照先来后到的方式,将线程放到队列中,按照队列中的方式执行。
3,ReentrantLock和synchronized的等待通知机制不同
synchronized通过 wait/notify 来实现;
ReentrantLock通过Condition方法来实现,
三、semaphore 信号量
信号量,⽤来表⽰"可⽤资源的个数".本质上就是⼀个计数器.
就类似停车场:用N记录当前可停车位,
有车进来停车,N-1;有车开走,N+1。这个N就表示可用资源的个数。
设“可⽤资源的个数"用 N来表示:
申请一个资源,会使N-1,称为“P操作”;释放一个资源,会使N+1,称为“V操作”。如果N为0了,继续P操作,则会进行阻塞。
信号量是操作系统内部提供的一种机制,操作系统对应的api被JVM封装下,就能通过java代码来调用其相关的操作了。
在java中,用 acquire方法,表示申请;release方法,表示释放。
锁就是一种特殊的信号量,可以认为是计数值为1的信号量:释放锁状态,就是1;加锁状态,就是0。对于这种非0即1的信号量,称为二元信号量。
Semaphore( n ):传参数的构造方法,参数表示可用资源的个数. 当n=1时(也就相当于一个锁了),通过acquire申请资源,打印“信号量1”,再申请资源,就会进入阻塞状态。
四、CountDownLatch 同时等待多个任务执行结束
CountDownLatch是多线程在特定场景中使用的一个小工具:功能与join类似,join是等待一个进程结束;CountDownLatch 可同时等待 N个任务执行结束。
代码案例:同时等待10个线程结束, 创建10个线程,同时执行下载任务,main线程 等待结束.
/*** CountDownLatch:可同时等待多个线程结束* 设同时等待10个线程结束* 创建10个线程,同时下载,等待结束*/
public static void main(String[] args) throws InterruptedException {CountDownLatch countDownLatch = new CountDownLatch(10);//10表示:同时等待10个线程结束for(int i=0;i<10;i++){ //创建10个线程 负责下载int n=i;new Thread(()->{System.out.println("开始下载: "+n);Random random = new Random();long s=(random.nextInt(5)+1)*1000;try {Thread.sleep(s);//每个线程设置随机休眠时间} catch (InterruptedException e) {throw new RuntimeException(e);}System.out.println("结束下载: "+n);countDownLatch.countDown();//执行完一个线程,就记录一下}).start();}countDownLatch.await();System.out.println("main ");}
执行结果:
五、实现线程安全的方式总结
1.synchronized锁
2.ReentrantLock可重入锁
3.CAS原子类
4.Semaphore信号量
六、创建线程的方式总结
1.通过继承Thread类及其匿名内部类方式
2.通过实现Runnable接口及匿名内部类方式
3.基于lambda方式
4.基于线程池
5.通过实现Callable接口方式创建
七.线程安全的集合类
Vector,Stack,HashTable,是线程安全的(不建议⽤),其他的集合类不是线程安全的.
1.多线程环境使用集合:ArrayList
保证在多线程安全下的使用方式:
1>.用synchronized加锁,保证线程安全.
2>.使用Collectios.synchronizedList(new ArrayList)
通过Collections类,关键位置都加上synchronized锁,保证线程安全. synchronizedList是标准库提供的⼀个基于synchronized进⾏线程同步的List.synchronizedList 的关键操作上都带有synchronized
相当于给ArrayList加了个壳,加壳后新的集合 list 就是线程安全的了.
3>.使用CopyOnWriteArrayList 写时拷贝
CopyOnWrite容器即写时复制的容器。
当多个线程只进行读操作的时候,不会产生线程安全问题;当要对数组修改时,会先将顺序表复制一benneg份,修改新的表中的内容,再将引用指向新的数组.(这里的操作是原子的,不用加锁)
使用CopyOnWriteArrayList 的利弊:
优点:在多读少写的情况下,无需加锁就解决了ArrayList的线程安全问题,提高了性能。
缺点:对数组的修改不能太频繁;数组不能太长,这些可能会导致复制操作成本太高。
2.多线程环境使⽤队列
1>.自己加锁保证线程安全。
2>.使用阻塞队列BlockingQueue(线程安全的).
3.多线程环境使用哈希表
HashMap是线程不安全的,HashTable是线程安全的duoxianceh
多线程环境使用哈希表可以使用:
1>.HashTable
2>.ConcurrentHashMap:并发哈希表
HashTable是把关键方法上都加了synchronized锁,也就是一个线程对数组中某条链表操作时,别的线程都不能对该数组操作,HashTable在多线程下的执行效率是很慢的。
ConcurrentHashMap: 对HashTable进行了改进和优化:
1>.优化了加锁方式
对读操作没有加锁,(只是使用Volatile修饰,确保从内存读数据,读到的是刚修改过的数据)只对写操作进行加锁,且缩小了锁的粒度,不再将整个数组都加锁,对每个链表都分配了一把锁(将每个链表的头节点对象设为锁),只有当多个线程访问同一个链表时,才会产生锁冲突。这样就降低了锁冲突,提高了效率。
2>.充分利用CAS原子操作特性
⽐如size属性通过CAS来更新.避免出现重量级锁的情况.
3>.优化了扩容方式
HashTable通过计算负载因子,判断是否需要扩容,达到要扩容的值,就直接扩容:创建新数组,将原来的数据全复制到新数组中。当数据量非常的时,扩容操作会进行的比较慢,表现出来的就是在运行的某一时刻比较慢,不具有稳定性。
ConcurrentHashMap对此进行了优化,通过“化整为零”方式进行扩容,不是一下将全部数据进行拷贝,而是进行分批拷贝
当需要扩容时,先创建一个新的数组,每次将一部分数据拷贝到新数组中,后续每个来操作ConcurrentHashMap的线程,都会参与搬家的过程.每个操作负责搬运⼀⼩部 分元素.这个过程中新老哈希表都存在,扩容结束,删除旧表;
扩容期间,进行插入操作:直接向新数组中进行插入;
删除操作:对新老数组都进行删除操作;
查找操作:对新老数组都进行查找操作;