Callable接口
Callable接口是Java中的一个函数式接口,它允许在另一个线程中执行某个任务并返回结果。
Callable也是一种创建线程的方式。但是与Runnable不同的是,Callable的call方法可以有一个返回值,Runnable的run方法没有返回值。即:Runnable关心过程(比如定时器,线程池中都是使用Runnable创建新线程),Callable关心结果(比如要让线程计算一个公式)。
Callable<Integer> callable = new Callable<Integer>() {
@Override
public Integer call() throws Exception {
return 1 + 2;
}
};
FutureTask<Integer> futureTask = new FutureTask<>(callable);
Thread t = new Thread(futureTask);
t.start();
int a = futureTask.get();
System.out.println(a);
需要注意的是,Callable不能直接作为Thread的参数,通常需要搭配FutureTask使用。
因为Callable线程什么时候执行完并产生返回值我们并不确定,什么时候去接收就不确定。FutureTask的get方法解决了这个问题,它可以阻塞等待直到Callable产生返回值。
ReentrantLock类
ReentrantLock也是Java中的一种锁机制,与Synchronized类似,它们都是可重入互斥锁,但是ReentrantLock还提供了一些其它的功能:
1:ReentrantLock需要手动的去获取和释放锁:
ReentrantLock reentrantLock = new ReentrantLock();
reentrantLock.lock();//调用lock方法加锁
//需要加锁的操作
//...
reentrantLock.unlock();//调用unlock方法解锁
这个时候就会产生一个问题:如果中间进行return 或者 抛出异常 ,就会执行不到unlock方法,这个锁也就无法被释放了。这也是ReentrantLock一个比较致命的劣势。
针对这个问题,我们可以把解锁操作写在finally模块当中:
ReentrantLock reentrantLock = new ReentrantLock();
try{
reentrantLock.lock();//调用lock方法加锁
//需要加锁的操作
//...
} finally {
reentrantLock.unlock();//调用unlock方法解锁
}
2.提供了超时锁机制:即允许线程在等待一定时间后自动放弃获取锁;
ReentrantLock提供了一个unlock方法,它有俩个重载版本,一是在尝试加锁后直接返回加锁结果,二是设置一个等待时间,在等待时间内尝试获取锁,成功就返回true,失败就返回false。这给我们提供了很大的操作性。
3.ReentrantLock支持公平锁和非公平锁俩种模式
ReentrantLock默认是不公平锁,但是可以设置为公平锁。
4.ReentrantLock有更强大的等待通知机制
ReentrantLock可以搭配Condition实现更加灵活和精细的操作,具有更加强大的功能。
另外,Synchronized和ReentrantLock相比:
Synchronized是jvm内部实现的,而ReentrantLock是库中的一个类。
Synchronized加锁对象可以是任何对象,而ReentrantLock只能是自己本身
信号量 Semaphore
Semaphore本质上是一个计数器,用来描述可用资源的个数,是并发编程的一个常用组件。
具体来说:Semaphore提供了俩个方法:acquire()方法 和 release()方法,我们创建Semaphore对象时给它可用资源的个数。
acquire()方法获取资源,使得可用资源的个数减一
release()方法释放线程获得的资源,使得可用资源的个数加1
当可用资源的个数为0时,如果继续获取,就会产生阻塞等待。
锁本质上就是一个二元信号量,加锁相当于获取资源,解锁就相当于释放资源,资源用完再获取就会产生阻塞。
CountDownLatch
CountDownLatch是Java中的一个同步工具类,用于控制线程的执行顺序。它可以让某些线程一直等待,直到其他线程完成一系列操作后再执行。CountDownLatch维护了一个计数器,表示需要等待的操作的数量,线程可以通过调用countDown()方法来减少计数器的值,也可以通过调用await()方法来等待计数器的值变为0。
使用CountDownLatch可以实现一些并发控制的场景,例如等待多个线程完成某个操作后再进行下一步操作、等待多个服务启动完成后再启动主服务等
举例:
CountDownLatch countDownLatch = new CountDownLatch(5);//维护一个计数器值为5
for(int i = 0;i < 5;i ++){
Thread thread = new Thread(() -> {
try {
Thread.sleep(500);
countDownLatch.countDown();//执行完一个任务,让计数器的值减为0
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
});
thread.start();
}
countDownLatch.await();//等待计数器的值为0
System.out.println("5个任务全部执行完");
除了这些类以外,JUC中还包括之前已经写过的原子类和线程池。
线程安全的使用集合类
Vector Stack HashTable集合类
Vector,Stack,HashTable是很久之前人们实现的线程安全的集合类,但是它们为了实现线程安全,用类似给整个方法进行加锁的方式,大大影响到了程序的并发性。而且这种加锁方式也不能保证完全线程安全(方法之间配合使用时还是有可能引起线程安全问题),而且由于时间过早,它们有很多不太合理的机制,后面我们又引入了很多更加灵活高效的集合类,所以这几种集合类不建议被使用。
多线程使用ArrayList
1.自己根据实际情况合适的使用锁机制
2.Collections.synchronizedList(new ArrayList);
相当于让ArrayList像Vector一样工作,使得它关键步骤都加上锁。不建议使用。
3.使用 CopyOnWriteArrayList
本质上也是一种读写分离的思想,读数据时不会产生线程安全问题,所以直接正常返回值就好。
当要修改数据时,会涉及到线程安全问题,数组先去复制一份,在这个复制的数组上进行修改,同时对整个修改过程加锁,避免线程安全问题。修改完成以后,将修改后的ArrayList替换旧的ArrayList(本质上就是一个引用的重新赋值,是一个原子操作)。在这个过程中,可以正常在原数组上进行读操作,互不影响。
CopyOnWriteArrayList的set方法源码:
缺点:占用内存太多(要复制)
修改的值不能立刻被读到
优点:在读多写少的场景下性能优秀,锁竞争很少。
HashTable HashMap ConcurrentHashMap 比较
HashTable 和 ConcurrentHashMap 是线程安全的哈希表类
HashMap线程不安全
HashTable 和 ConcurrentHashMap的比较:
HashTable的缺点:
1:枷锁方式
HashTable实现线程安全的方式是给关键方法加锁相当于针对对象加锁,当这个对象多次重复调用加锁方法时就会产生激烈的锁竞争,这时,程序就像是串行执行,极大影响了并发性。
2:扩容机制
HashTable计算出新的容量并进行初始化后,一股脑将所有元素逐一重新哈希,这时候我们就不能正常进行操作了,相当于突然就卡顿了一会儿。
ConcurrentHashMap做出的改进:
1:枷锁方式
ConcurrentHashMap缩小了锁的粒度,修改数据时,ConcurrentHashMap对哈希桶的每个链表分别加锁。
当我们修改不同链表上的数据时,根本不会产生线程安全问题,也就不会发生锁冲突。当修改同一个链表上的数据时,由于我们针对每个链表加锁,这时就会产生锁冲突,避免了线程安全问题。
本身,多线程修改到同一个链表上的数据的概率是很小的,所以我们这样的加锁方式大大减少了锁冲突,提高了并发性。
2:扩容机制:
ConcurrentHshMap采用了渐进式扩容方法:
在创建出新的数组后,ConcurrentHashMap逐渐的将旧的表上的数据往新的哈希表上挪,这个过程可能需要一段时间,在这个时间内,我们仍然可以进行操作,这个期间, 插入只往新数组加.这个期间, 查找需要同时查新数组和老数组
并且,我们每次进行操作也会连带着搬运一定量的数组,这样积少成多,逐步完成扩容过程。
3:只给写操作加锁,读操作不加锁了
读操作和写操作冲突的问题开发者通过其他手段解决掉了
4:引入CAS机制,使size++(对哈希表元素个数计数的变量)这样的操作不会引发线程安全问题
最无关紧要的区别:HashMap允许键和值为空,HashTable和ConcurrentHashMap不允许键和值为空。