下面代码中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();
}
}
}
可重入互斥锁. 和 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 开启
公平锁模式
是一个更广义的锁.锁是信号量里第一种特殊情况,叫做"二元信号量"
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();
}
}
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("比赛结束!");
}
}
写时拷贝,在修改的时候,会创建一份副本出来
这样做的好处,就是修改的同时对于读操作,是没有任何影响的,读的时候优先读旧的版本
不会说出现读到一个"修改了一半"的中间状态,等到修改完毕,再将副本转正.
HashMap本身线程不安全
HashTable对象只有一把锁
如果元素多了,链表就会长,就影响hash表的效率~~就需要扩容(增加数组的长度)
扩容就需要创建一个更大的数组,然后把之前旧的元素都给搬运过去~(非常耗时)
ConcurrentHashmap是针对这个元素所在的链表的头结点来加锁的.
如果你两个线程操作是针对两个不同的链表上的元素,没有线程安全问题,其实不必加锁~~
由于hash 表中,链表的数目非常多,每个链表的长度是相对短的,因此就可以保证锁冲突的概率就非常小了
对于HashTable 来说只要你这次put触发了扩容就一口气搬运完.会导致这次put非常卡顿.
ConcurrentHashMap,每次操作只搬运一点点.通过多次操作完成整个搬运的过程.同时维护一个新的HashMap和一个旧的.查找的时候既需要查旧的也要查新的.插入的时候只插入新的直到搬运完毕再销毁旧的