juc中的组件:
引言:
在之前我们学习了java的多线程,我们知道java的线程是被封装在java.util.current包中,当然这个包中不仅有Thread 类,还有一些其他的类,下面我们就是来学习一下这些其他的组件。
1.callable( ):
什么是callable?
callable是java中的一个执行并发任务的一个类,他和Runnable是类似的但是又有所不同。
- callable里面的是call()方法。
- call()方法是带有泛型的返回值。
- callable(),任务不能直接给线程执行需要封装成FutureTask任务,返回的结果也是通过FutureTask获取。
- callable(),能直接通过submit提交给线程池(submit在提交的过程中,会自己封装成FutureTask()),返回Future对象。
- 可以直接传入多个callable对象,但是接收结果的时候可以使用一个数组或接收。
代码1:FutureTask
java
package Thread.JUC;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;
import java.util.concurrent.atomic.AtomicInteger;
public class callable1 {
public static void main(String[] args) throws ExecutionException, InterruptedException {
AtomicInteger atomicInteger = new AtomicInteger();
Callable<Integer> callable = new Callable<Integer>() {
@Override
public Integer call() throws Exception {
for(int i = 0;i<1000;i++){
atomicInteger.getAndIncrement();
}
return Integer.valueOf(atomicInteger.get());
}
};
FutureTask<Integer> futureTask = new FutureTask<>(callable);
Thread t1 = new Thread(futureTask);
t1.start();
System.out.println(futureTask.get());
}
}
注意:
- 线程我们不能忘记start否则会没有结果
- 此外call( ) 方法的返回值要和他的泛型类型是一样的。
- 获取结果的get方法,是带有堵塞的效果的,如果执行到这里且任务没有执行完的话,他会进行堵塞等待任务结束。
代码2:Future
java
package Thread.JUC;
import java.util.concurrent.*;
import java.util.concurrent.atomic.AtomicInteger;
public class callable2 {
public static void main(String[] args) throws ExecutionException, InterruptedException {
AtomicInteger atomicInteger = new AtomicInteger();
Callable<Integer> callable = new Callable<Integer>() {
@Override
public Integer call() throws Exception {
for(int i = 0;i<1000;i++){
atomicInteger.getAndIncrement();
}
return Integer.valueOf(atomicInteger.get());
}
};
ExecutorService service = Executors.newFixedThreadPool(1);
Future<Integer> future = service.submit(callable);
System.out.println(future.get());
}
}
总结:
- future或者futurefask他们相当于一个号码牌,有了这个东西我们才能找到他的返回结果。
- 此外get方法,还有一个超时等待版本,如果,超过等待时间还没有结束,就不在堵塞等待了。
代码:
java
package Thread.JUC;
import java.util.concurrent.*;
import java.util.concurrent.atomic.AtomicInteger;
public class callable1 {
public static void main(String[] args) throws ExecutionException, InterruptedException {
AtomicInteger atomicInteger = new AtomicInteger();
Callable<Integer> callable = new Callable<Integer>() {
@Override
public Integer call() throws Exception {
for(int i = 0;i<1000;i++){
atomicInteger.getAndIncrement();
Thread.sleep(1000);
}
return Integer.valueOf(atomicInteger.get());
}
};
FutureTask<Integer> futureTask = new FutureTask<>(callable);
Thread t1 = new Thread(futureTask);
t1.start();
try {
Integer result = futureTask.get(500, TimeUnit.MILLISECONDS);
System.out.println("执行成功");
} catch (TimeoutException e) {
System.out.println("超时等待");
throw new RuntimeException(e);
}
}
}
2.ReentrantLock :
ReentrantLock是什么?
ReentrantLock是一个可重入锁,他和synchronized并列的,他是java当中的一个类,通过lock unlock进行加锁,解锁。
代码案例:
java
package Thread.JUC;
import java.util.concurrent.locks.ReentrantLock;
public class ReentrantLock1 {
private static int count = 0;
public static void main(String[] args) throws InterruptedException {
ReentrantLock locker = new ReentrantLock();
Thread t1 = new Thread(()->{
for(int i = 0;i<50000;i++){
locker.lock();
try{
count++;
}finally{
locker.unlock();
}
}
});
Thread t2 = new Thread(()->{
for(int i = 0;i<50000;i++){
locker.lock();
try{
count++;
}finally{
locker.unlock();
}
}
});
t1.start();
t2.start();
t1.join();t2.join();
System.out.println(count);
}
}
代码分析:
- 这是一个会出现线程安全的问题,如果但是我们使用了ReentrantLock进行加锁,和解锁。避免了线程安全问题。
ReentrantLock和 synchronized的区别
-
ReentrantLock是java的一个类 ,内部是由java实现的。 synchronized是一个关键字,主要实现是由JVM通过c++实现的。
-
ReentrantLock 是通过方法lock和 unlock进行加锁和解锁的时候,我们要注意对unlock进行解锁,通常我们会用try finally代码包裹避免忘记解锁。synchronized是通过代码块进行加锁和解锁的,不需要考虑忘记解锁等。
-
ReentrantLock 除了lock和 unlock方法以外还有trylock方法
- 加锁成功饭返回true,加锁失败返回false,调用者根据返回结果做出下一步操作,不像synchronized关键字一样,加锁失败就一直堵塞,具有更多的操作空间。
- 此外trylock还提供了带时间参数的版本,他不会立即返回结果,而是等待,超出时间之后才会返回结果。
-
ReentrantLock 提供了公平锁的选项,默认是非公平的(即不分先来后到,概率均等)
java
ReentrantLock reentrantLock1 = new ReentrantLock(false);//非公平
ReentrantLock reentrantLock2 = new ReentrantLock(true);//公平
- ReentrantLock搭配的等待机制是condition相比synchroized的wait和 notify功能更强大。
总结:
关于ReentrantLock,我们主要了解他是如何使用的以及他和synchroized的区别
信号量 3.semaphore
什么是信号量?
在多线程当中,信号量是用来控制线程对共享资源的访问权限的,本质是由一个计数器+堵塞队列控制的。
- 计数器:记录当前还有多少个可用的资源
- 取资源计数器--;
- 放资源:计数器++;
- 堵塞队列:当计数器为0的时候,会堵塞线程,等待资源的释放。
- 类比 :信号量可类比为带固定车位的停车场:车位总数是计数器初始值(共享资源总量) ,车主进停车场 对应线程执行 P 操作**(申请资源)------ 有空位则计数器减 1 后放行,无空位则进入等待区排队(线程阻塞);车主离开对应线程执行 V 操作(释放资源)------ 计数器加 1,若有排队车主则唤醒一位进入(线程唤醒)**;其中 1 个车位的充电桩对应二进制信号量(互斥锁,仅允许 1 个线程独占),10 个车位的普通停车场对应计数信号量(允许 N 个线程同时访问),入口管理员则保证进出场操作的原子性(避免计数错乱),完美匹配信号量 "通过计数器 + 等待队列控制共享资源并发访问" 的核心逻辑。
代码:资源足够的
java
package Thread.JUC;
import java.util.concurrent.Semaphore;
public class semaphore {
public static void main(String[] args) throws InterruptedException {
Semaphore semaphore = new Semaphore(4);
for(int i = 0;i<4;i++){
semaphore.acquire();
System.out.println("获得资源"+(i+1));
}
}
}
代码:资源不够,发生堵塞
java
public static void main(String[] args) throws InterruptedException {
Semaphore semaphore = new Semaphore(3);
for(int i = 0;i<4;i++){
semaphore.acquire();
System.out.println("获得资源"+(i+1));
}
}
- 运行代码,在四次循环的时候,我们尝试获取,却发现资源不够,因此就进行堵塞。
代码:释放资源
java
public static void main(String[] args) throws InterruptedException {
Semaphore semaphore = new Semaphore(3);
for(int i = 0;i<4;i++){
semaphore.acquire();
System.out.println("获得资源"+(i+1));
semaphore.release();
}
}
- java的v操作对应于relase操作,释放资源。,我们使用之后就进行释放,就不会发生堵塞了。
二元信号量
如果一个信号量的初始值是1 那么他的取值不是1就是0 ,这个时候这个二元信号量有锁的作用了。
代码演示:
java
private static int count = 0;
public static void main(String[] args) throws InterruptedException {
Semaphore semaphore = new Semaphore(1);
Thread t1 = new Thread(()->{
for(int i = 0;i<50000;i++){
try {
semaphore.acquire();
count++;
semaphore.release();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
});
Thread t2 = new Thread(()->{
for(int i = 0;i<50000;i++){
try {
semaphore.acquire();
count++;
semaphore.release();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
});
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println(count);
}
- 上述获得资源释放资源 的过程中,起到了一个锁的作用
4.CountDownLatch
在多线程中,我们通常是把一个大任务分成多个小任务去执行,但是在线程并发执行的过程中,我们是如何知道,当前线程执行完毕了,当前的子任务已经全部执行完了呢?这个时候我们就需要使用CountDownLatch来记录当前的子任务执行的数量。
使用方法:
- 构造CountDownLatch实例 latch,注意这个时候CountDownLatch里面的参数要和将执行的任务的数量相等。
- 每完成一次任务,我们都调用一次latch.countDownLatch,此时就会在CountDownLatch里面完成自减操作。
- 在主线程调用latch.await( )等待所有任务执行完毕。
代码:
java
package Thread.JUC;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class myCountDownLatch {
public static void main(String[] args) throws InterruptedException {
CountDownLatch latch = new CountDownLatch(10);
ExecutorService service = Executors.newFixedThreadPool(4);
for(int i = 0;i<10;i++){
int id = i+1;
service.submit(()->{
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println("执行到任务"+id);
latch.countDown();
});
}
latch.await();
System.out.println("任务全部执行完毕");
}
}
- CountDownLatch的参数列表和任务数目相同
- 每次执行完成一个任务我们都需要执行一次countDownLatch方法。
- await方法的话,会堵塞调用线程,只有CountDownLatch里面的任务数目执行完之后才继续执行调用线程。
- CountDownLatch只针对个别场景下才有用。
5.多线程下ArrayList的使用
ArrayList是一个线程不安全的类,在多线程中使用会出现线程安全问题,那么我们应该如何做,才能在多线程中安全的使用ArrayList呢?
1.自主加锁:
我们针对我们的代码逻辑,把可能出现线程安全问题的代码打包加锁,组装成原子类
2.使用synchronized进行套壳
collection.synchronizedList( new ArrayList) ,返回一个list对象,这个list对象的所有方法都会被synchronized修饰。
3.CopyOnWriteArrayList:
赋值修改思想:
多线程执行到修改的时候,会首先复制一份数据,在修改的过程中,进行读取,引用指向旧的数据,等到修改之后,引用就执行修改之后的数据。
- 这样写避免了读取到修改一般的数据
- 但是这样写存在一些弊端:
*- 如果数组非常大的话,就会导致资源的大量浪费
- 2.此外,如果同时多个线程同时修改数据,那么最终结果的引用指向哪个,这个我们也没办法确定。
总结:
上述虽然讲了三种方法,但是实际当中,我们更倾向于使用第一种,第二种对于所有方法都加了锁,会造成性能的浪费,第三种在多线程同时修改的时候会出现问题,此外如果数组非常大,会造成空间极大的浪费。
6.多线程下哈希表的使用
HashMap和HashTable
在之前的学习当中,我们知道HashMap是一个线程不安全的类,HashTable是一个线程安全的类。
- HashTable;
- 他本身被synchroized修饰,相当于对this加锁,因此他的冲突概率比较大。
- 哈希表产生概率的情况比较小,对不同的链表进行修改,且同一链表上不同修改的两个数据的位置不相邻的时候,都不会产生线程安全问题。
- 因此,哈希表产生冲突的情况很低,但是this针对任意操作都进行加锁,因此就会影响性能。
- CurrentHashTable:
- CurrentHashTable相对于HashTable,他并不是针对this加锁,而是针对每一个的链表的头节点,就避免了无效冲突的发生,大大降低了冲突的概率,提升了性能。
- 这个时候我们又有了一个新的问题:
- 我们针对每个链表进行加锁,但是如果我们在多线程条件下在两个链表里面都添加一个元素,他没有线程冲突,但是他的size存在线程安全的情况,这就会让代码出现问题。
- 如何处理呢?我们对size使用一个原子类,这样就避免了size的线程安全问题。
总结:
- 1.我们说了callable的类的使用
- 2.ReentrantLock的使用;
- 3.信号量
- 4.CountDownLatch
- 5.多线程下对ArrayList以及哈希表的使用。