JUC
- Callable:接口:解决Runnable不方面返回结果的问题
- ReentrantLock
- 信号量:Semaphore
- CountDownLatch:等待所有线程执行完毕后,await返回
- CopyonwriteArrayList
- HashTable(不推荐)和ConcurrentHashmap(推荐)
Callable:接口:解决Runnable不方面返回结果的问题
下面代码中Callable是在描述一个任务,且有一个返回值
将Callable对象callable传入FutureTask的构造方法中所创建出来的对象task,就是Callable中描述的任务的维护标识对象.将task传入Thread的构造方法中,等待该线程运行结束后,可以通过task.get()得到任务的返回值.
public class Demo28 { public static void main(String[] args) { // 通过 callable 来描述一个这样的任务~~ Callable<Integer> callable = new Callable<Integer>() { @Override public Integer call() throws Exception { int sum = 0; for (int i = 1; i <= 1000; i++) { sum += i; } return sum; } }; // 为了让线程执行 callable 中的任务, 光使用构造方法还不够, 还需要一个辅助的类. FutureTask<Integer> task = new FutureTask<>(callable); // 创建线程, 来完成这里的计算工作~~ Thread t = new Thread(task); t.start(); // 凭小票来端你自己的麻辣烫. // 如果线程的任务没有执行完呢, get 就会阻塞. // 一直阻塞到, 任务完成了, 结果算出来了~~ try { System.out.println(task.get()); } catch (InterruptedException e) { e.printStackTrace(); } catch (ExecutionException e) { e.printStackTrace(); } }}
ReentrantLock
可重入互斥锁. 和 synchronized 定位类似, 都是用来实现互斥效果, 保证线程安全.
lock(): 加锁, 如果获取不到锁就死等.
trylock(超时时间): 加锁, 如果获取不到锁, 等待一定的时间之后就放弃加锁.
unlock(): 解锁
public class Demo29 { public static void main(String[] args) { ReentrantLock locker = new ReentrantLock(); // 加锁 locker.lock(); // 抛出异常了. 就容易导致 unlock 执行不到~~ // 解锁 locker.unlock(); }}
把加锁和解锁两个操作分开了,很容易遗漏unlock(容易出现死锁)当多个线程竞争同一个锁的时候就会阻塞…
ReentrantLock 和 synchronized 的区别:
1.synchronized 是一个关键字, 是 JVM 内部实现的(大概率是基于 C++ 实现). ReentrantLock 是标准
库的一个类, 在 JVM 外实现的(基于 Java 实现).
2.synchronized 使用时不需要手动释放锁. ReentrantLock 使用时需要手动释放. 使用起来更灵活,
但是也容易遗漏 unlock.
3.synchronized 在申请锁失败时, 会死等. ReentrantLock 可以通过 trylock 的方式等待一段时间就
放弃.
4.synchronized 是非公平锁, ReentrantLock 默认是非公平锁. 可以通过构造方法传入一个 true 开启
公平锁模式
信号量:Semaphore
是一个更广义的锁.锁是信号量里第一种特殊情况,叫做”二元信号量”
P操作 acquire申请
V操作 release释放
锁就可以视为”二元信号量”,可用资源就一个,计数器的取值,非О即1
public class Demo30 { public static void main(String[] args) throws InterruptedException { // 初始化的值表示可用资源有 4 个. Semaphore semaphore = new Semaphore(4); // 申请资源, P 操作 semaphore.acquire(); System.out.println("申请成功"); semaphore.acquire(); System.out.println("申请成功"); semaphore.acquire(); System.out.println("申请成功"); semaphore.acquire(); System.out.println("申请成功"); semaphore.acquire(); System.out.println("申请成功"); // 释放资源, V 操作 // semaphore.release(); }}
CountDownLatch:等待所有线程执行完毕后,await返回
public class Demo31 { public static void main(String[] args) throws InterruptedException { // 构造方法的参数表示有几个选手参赛. CountDownLatch latch = new CountDownLatch(10); for (int i = 0; i < 10; i++) { Thread t = new Thread(() -> { try { Thread.sleep(3000); System.out.println(Thread.currentThread().getName() + " 到达终点!"); latch.countDown(); } catch (InterruptedException e) { e.printStackTrace(); } }); t.start(); } // 裁判就要等待所有的线程到达 // 当这些线程没有执行完的时候, await 就阻塞, 所有的线程都执行完了, await 才返回. latch.await(); System.out.println("比赛结束!"); }}
CopyonwriteArrayList
写时拷贝,在修改的时候,会创建一份副本出来
这样做的好处,就是修改的同时对于读操作,是没有任何影响的,读的时候优先读旧的版本
不会说出现读到一个”修改了一半”的中间状态,等到修改完毕,再将副本转正.
HashTable(不推荐)和ConcurrentHashmap(推荐)
HashMap本身线程不安全
HashTable对象只有一把锁
如果元素多了,链表就会长,就影响hash表的效率~~就需要扩容(增加数组的长度)
扩容就需要创建一个更大的数组,然后把之前旧的元素都给搬运过去~(非常耗时)
ConcurrentHashmap是针对这个元素所在的链表的头结点来加锁的.
如果你两个线程操作是针对两个不同的链表上的元素,没有线程安全问题,其实不必加锁~~
由于hash 表中,链表的数目非常多,每个链表的长度是相对短的,因此就可以保证锁冲突的概率就非常小了
对于HashTable 来说只要你这次put触发了扩容就一口气搬运完.会导致这次put非常卡顿.
ConcurrentHashMap,每次操作只搬运一点点.通过多次操作完成整个搬运的过程.同时维护一个新的HashMap和一个旧的.查找的时候既需要查旧的也要查新的.插入的时候只插入新的直到搬运完毕再销毁旧的