// 也是一种创建线程的方式
// Runable 能表示一个任务 (run 方法), 返回值 void ; Callable 也能表示一个任务 (call 方法), 返回值是一个具体的值, 类型可以通过泛型参数来指定 (Object)
// 如果进行多线程操作, 且只关心多线程执行的过程 (像 线程池, 定时器等), 使用 Runable 即可; 如果是关心多线程的计算结果, 使用 Callable 更合适
// 使用 Callable 不能直接作为 Thread 的构造方法参数, 需要用 FutureTask 过渡一下
// 代码实现
public static void main(String[] args) throws ExecutionException, InterruptedException {
Callable callable = new Callable() {
@Override
public Integer call() throws Exception {
int sum = 0;
for (int i = 0; i < 1000; i++) {
sum += i;
}
return sum;
}
};
FutureTask futureTask = new FutureTask<>(callable);
Thread t = new Thread(futureTask);
t.start();
Integer result = futureTask.get();
System.out.println(result);
}
// get 类似于 join, 如果 call 方法没执行完, 会阻塞等待
// reentrant : 可重入的
// 这个锁没有 synchronized 那么常用, 但是也是一个可选的加锁组件, 这个锁在使用上更接近于 C++ 里的锁
// 因为是两步锁, 那么就容易出现 unlock 调用不到的情况, 比如 中间 return 了或者 抛出异常了
// unlock 容易遗漏, 所以用 finally 来执行 unlock
// synchronized 锁对象是任意对象, ReentrantLock 锁对象就是自己本身, 如果你对多个线程针对不同的 ReentrantLock 调用 lock 方法, 此时不会产生锁竞争的.
// 实际开发中, 进行多线程开发, 用到的锁依旧首选 synchronized
// 可以使线程既不需要加锁又可以保证线程安全
// 例如 : 视频播放中的一些数据 (播放量, 点赞量, 转发量等等)
// 统计出现错误的请求数目,请求总数(衡量服务器的压力)或每个请求的响应时间 -> 平均响应时间, (衡量服务器的运行效率), 使用原子类, 记录出错的请求数目, 另外写一个监控服务器, 获取到线上服务器的错误计数, 再通过曲线图绘制到页面上, 我们就可以及时发现 版本BUG
// 线上服务器通过这些统计内容, 进行简单计数 => 实现监控服务器, 获取/ 统计/ 展示/ 报警
// 在操作系统中, 也经常出现, Semaphore 是并发编程中的一个重要的概念/ 组件; 准确来说, Semaphore 是一个计数器 (变量), 描述了 "可用资源的个数", 描述的是当前线程是否"有临界资源可以用"
//临界资源: 多个线程/ 进程等并发执行的实体可以公共使用到的资源
// P / V 操作: P 操作是计数器减少(申请资源 acquire), V 操作是计数器增加(释放资源 release)
// 当计数器数值为 0 的时候, 继续 P 操作, 就会阻塞等待, 一直等待到其他线程执行了 V 操作, 释放了一个空闲资源为止
// 锁, 本质上是一种特殊的信号量 (里面的值, 非 0 即 1, 二元信号量)
// 信号量比锁更广义, 不仅仅可以描述一个资源, 还可以描述 N 个资源, 虽然概念上更广泛, 但是实际开发中还是锁更多一些 (二元信号量是最常用的)
// 伪代码 展示一下 P / V 操作
public static void main(String[] args) throws InterruptedException {
Semaphore semaphore = new Semaphore(4);
semaphore.acquire();
System.out.println("执行 P 操作");
semaphore.release();
System.out.println("执行 V 操作");
}
// 针对特定场景一个组件, 当我们需要将一个任务拆分成多个部分, 每个部分用线程分别执行的时候就可以使用这个组件, 来判定出当前是否所有的任务都执行完了
// 主要用于下载方面
// 伪代码展示一下
public static void main(String[] args) throws InterruptedException {
CountDownLatch countDownLatch = new CountDownLatch(10);
for (int i = 0; i < 10; i++) {
int id = i;
Thread t = new Thread(() -> {
System.out.println("线程" + id + "正在工作");
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println("线程" + id + "结束工作");
countDownLatch.countDown();
});
t.start();
}
countDownLatch.await();
System.out.println("这个线程的所有任务都执行完了");
}
// 集合类哪些是线程安全的
// Vector, Stack, HashTable 是线程安全的, 但不建议用, 其他的集合类不是线程安全的, 关键方法中, 使用 synchronized
// Vector / HashTable 这样的集合类, 虽然加了 synchronized 也不能保证一定是线程安全的, 同时, 在单线程的情况下, 又可能因为 synchronized 影响到执行效率
// ArrayList 本身没有使用 synchronized 但是你又不想自己加锁, 那么就可以使用上面这个
// 相当于让 ArrayList 像 Vector 一样工作
// 多线程同时修改同一个变量
// 如果多个线程去读取, 本身不会有任何线程安全问题, 一旦有线程修改, 就会把自身复制一份, 尤其修改如果比较耗时的话, 其他线程还是从旧的数据上读取, 一旦修改成功, 使用新的 ArrayList (本质上就是引用的重新赋值, 速度极快, 并且又是原子的), 这个过程中没有引入任何的加锁操作, 使用了 创建副本 => 修改副本 => 使用副本替换, 类似于显卡渲染画面
// HashMap 本身不是线程安全的
// HashTable 是在方法上直接加上 synchronized, 就相当于针对 this 加锁
2.2.1 [核心] 减小了锁的粒度, 每个链表有一把锁, 大部分情况下都不会涉及到锁冲突
2.2.2 广泛使用 CAS 操作, 像 size++ 这种操作就不会产生锁冲突
2.2.3 写操作进行了加锁(链表级), 读操作, 不加锁了
2.2.4 针对扩容操作进行了优化, 渐进式扩容
// 化整为零, 当需要扩容的时候, 会创建出另一个更大的数组, 然后把旧的数组上的数据逐渐的往新的数组上搬运. 会出现一段时间, 旧数组和新数组同时存在
2.2.4.1 新增元素, 往新数组上插入
2.2.4.2 删除元素, 把旧数组的元素给删掉即可
2.2.4.3 查找元素, 新旧数组都要查找
2.2.4.4 修改元素, 统一把这个元素给搞到新的数组上
// 与此同时, 每个操作都会触发一定程度搬运, 每次搬运一点, 就可以保证整体的时间不是很长, 积少成多之后, 逐渐完成搬运了, 也就可以把之前的旧数组彻底销毁了
// HashMap 和 ConcurrentHashMap 之间的区别在于: 线程不安全和线程安全之间的区别
// Java 8 之前, ConcurrentHashMap 是使用分段锁, 之后都是每个链表一把锁
// 线程池构造方法中的 ThreadFactory 也可以构造线程
// 其中 1, 2, 5 这三种方法又可以搭配匿名内部类来使用