目录
Collections.synchronizedList (new ArrayList);
JUC (java.util.concurrent)常见类
这个包里面的内容主要是一些多线程相关的组件
Callable 接口
创建线程的一种方式, 想让某个线程执行一个逻辑并返回一个结果的时候使用
相比之下, Runnable 不关注结果
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;
public class Demo4 {
public static void main(String[] args) throws ExecutionException, InterruptedException {
Callable<Integer> callable = new Callable<Integer>() {
// Callable 中的和新方法, 返回值就是Intrger, 期望返回一个整数
@Override
public Integer call() throws Exception {
int sum = 0;
for(int i = 0; i < 1000; i++) {
sum += i;
}
return sum;
}
};
// 把任务放到线程中执行
FutureTask<Integer> futureTask = new FutureTask<>(callable);
Thread t = new Thread(futureTask);
t.start();
// 此处 get 就能获得到 callable 里面的返回结果
// 由于线程是并发执行的, 执行到主线程的 get 的时候, t 线程可能还没执行完
// 没执行玩的话 get 就会阻塞
System.out.println(futureTask.get());
}
}
futureTask 是干嘛的?
例子:
我们去吃饭点菜之后拿到小票, 点餐完成之后, 后厨相当于一个线程, 开始执行, 这个过程等着, 等才做好之后凭小票取餐, futureTask 就相当于 小票, 拿小票换执行结果.
总结:
线程创建方式
1> 继承Thread 重写run (创建单独的类, 也可以匿名内部类)
2> 实现Runnable 重写run (创建单独的类, 也可以匿名内部类)
3> 实现 Callale 重写 call (创建单独的类, 也可以匿名内部类)
4> 使用 lambda 表达式
5> ThreadFactory 线程工厂
6> 线程池
.......
ReentrantLock
可重入锁, 使用效果和 synchronized 类似
优势
1> ReentrantLock 在加锁的时候有两种方式, lock tryLock
使用了 lock 之后遇到锁冲突之后就会阻塞等待
使用 tryLock 遇到锁冲突就会放弃
tryLock 给了更多可操作空间.
2> ReentrantLock 提供了公平锁的实现 (默认情况下是非公平锁)
3> ReentrantLock 提供了更强大的等待通知机制, 搭配了 Condition 类实现等待通知
虽然有上述优势, 但在加锁时首选 synchronized, 因为其使用更复杂, 尤其是忘记解锁
信号量 Semaphore
操作系统中重要概念
例子:
找地方停车, 停车场的门口挂个牌子, 剩余 xxx 个车位, 车子进来数字 - 1, 车子出去 + 1
信号量就是一个计数器, 描述了 " 可用资源 " 的个数, 申请一个可用资源, 计数器 -1 (P 操作), 释放一个可用资源 计数器 +1 (V 操作)
信号量初始情况时 10, 进行10次 P 操作, 数值就是 0, 如果继续 P 操作就会 阻塞等待(类似锁)
锁就是一种特殊的信号量, 锁是可用资源 为 1的信号量, 加锁操作, P操作, 1 -> 0, 解锁操作 V操作 0 -> 1. 锁就叫做二元信号量
操作系统提供了 信号量 实现, 提供了 api, JVM 封装了这样的 api, 可以在 Java 代码中使用了.
代码实现
import java.util.concurrent.Semaphore;
public class Demo5 {
public static void main(String[] args) throws InterruptedException {
Semaphore semaphore = new Semaphore(4);
semaphore.acquire();
System.out.println("P 操作");
semaphore.acquire();
System.out.println("P 操作");
semaphore.acquire();
System.out.println("P 操作");
semaphore.acquire();
System.out.println("P 操作");
}
}
开发中遇到了需要申请资源的场景就可以使用 semaphore来实现了
CountDownLatch
适用于 多个线程 来完成一系列任务的时候, 用来用来衡量任务的进度是否完成.
比如 : 需要把一个大的任务拆分成多个小任务, 并让这些任务并发执行
使用 countDownLatch 来判定这些任务是否全部完成了
下载一个文件就可以使用多线程下载
CountDownLatch 主要有两个方法:
1> await 调用的时候就会阻塞, 等待其他线程任务完成之后 await 才会返回继续往下走
2> countDown 告诉 countDownLatch 当前这一个字任务已经完成了
代码实现:
import java.util.concurrent.CountDownLatch;
public class Demo6 {
public static void main(String[] args) throws InterruptedException {
// 10 个选手参赛, await 就会在 10 次调用完, countDown 之后才能继续执行
CountDownLatch countDownLatch = new CountDownLatch(10);
for (int i = 0; i < 10; i++) {
int id = i;
Thread t = new Thread(() -> {
System.out.println("thread " + id);
try {
Thread.sleep(500);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
// 通知当前任务执行完毕了
countDownLatch.countDown();
});
t.start();
}
// a => all
countDownLatch.await();
System.out.println("所有的任务都完成了.");
}
}
线程安全的集合类
数据结构中大部分的集合类都是线程不安全. Vector, Stack, Hashtable 线程安全 => synchronized(不建议使用, 上古时期的东西)
针对线程不安全的集合类, 想要在多线程环境下使用需要考虑 线程安全问题
标准库提供了一些搭配的组件, 保证线程安全
Collections.synchronizedList (new ArrayList);
这个东西会返回一个新的对象, 这个对象相当于给 ArrayList 套了一层壳, 这层可就是在方法上直接使用 synchronized 的
CopyOnWriteArrayList 写时拷贝
举个例子:
如果两个线程同时使用一个 ArrayList, 可能会读也可能会修改, 如果两个线程读, 直接读就好了, 如果某个线程需要修改, 就把 ArrayList 复制出一份副本, 修改线程就修改这个副本, 与此同时另一个线程仍然可以读取数据 (从原来的数据进行读取), 修改完毕后就会使用修改好的数据代替原来的数据, 这样就不用加锁了.
1> 当前操作 ArrayList 不能太大(不然的话比加锁成本还高了)
2> 更适合一个线程去修改, 不能是多个线程同时修改(多个线程都, 一个线程修改)
适合于 服务器的配置更新, 通过配置文件来描述配置的详细内容(本身不大)
配置的内容被读到内存中, 再由其他的线程读取这里的内容, 但是修改这个配置内容往往只有一个线程来修改, 使用某个命令让服务器重新加载配置就可以使用写时拷贝了.
多线程环境使用队列
多线程环境使用哈希表(重)
HashMap 本身不是线程安全的
在多线程环境下使用哈希表可以使用:
1> Hashtable
2> ConcurrentHashMap
Hashtable 保证线程安全主要是给关键方法加上 synchronized , 直接加到 方法上的, 所以只要有两个线程操作同一个 Hashtable 就会出现 锁冲突
但是, 实际上对于哈希表来说, 所不一定非得这么加, 有些情况其实不涉及到线程安全问题的.
如果两个线程操作同一个链表比较容易出现问题, 如果两个线程操作的是不同的链表根本不用加锁, 只有操作的是同一个链表才需要加锁.
ConcurrentHashMap 最核心的改进
1> 把一个全局的大锁改进成了 每个链表独立地一把小锁, 这样大幅度降低了锁冲突的概率. (一个 hash 表中有很多这样的链表, 两个线程恰好同时访问一个链表情况, 本身情况少)
怎么完成的: 把每个链表的头节点作为锁对象, synchronized 可以使用任意对象作为锁对象
2> 充分利用了 CAS 特性, 把一些不必要加锁环节给省略加锁了. 比如: 使用变量记录 hash 表中的元素个数(使用原子操作(CAS) 修改元素个数)
3> ConcurrentHashMap, 还有一个激进的操作, 针对读操作没有加锁. 读和读之间, 读和写之间不会有锁竞争(ConcurrentHashMap编码过程中 避免使用 ++之类的非原子的操作)写和写之间加锁
4> ConcurrentHashMap 针对扩容操作做出了单独的优化.
本身 Hashtable 或者 HashMap 在扩容的时候需要把所有的元素都拷贝了一遍. 如果元素很多, 拷贝就比较耗时
使用了 化整为零的方法. 需要扩容时, 需要搬运, 但是分多次完成.
小结
理解即可.