多线程 (下) - 学习笔记1:https://developer.aliyun.com/article/1518498
一些对于锁的优化操作
锁消除
编译器 + JVM 判断锁是否可消除, 如果判定通过, 则直接消除
eg: StringBuffer 是线程安全, 原因在于有很多操作 (eg: append) 是加锁的, 但是如果是单线程环境下, 就没有必要进行上锁, 此时 编译器 和 JVM 就会进行判断, 并且消除锁
锁粗化
一段逻辑中如果出现多次锁解锁, 编译器 + JVM 会自动进行锁的粗化
锁的粒度: 粗和细
比如上面的一段过程反复加锁解锁, 中间如果没有其他线程的介入, JVM 就会自动把这一段的锁操作合并为一个, 减少了资源消耗
Callable 接口
Callable 是什么?
Callable 是一个接口, 和 Runnable 相对, 但是 Callable 会有一个返回值 (Runnable 没有)
Callable 和 Runnable 都是描述一个 “任务”, 二者的区别在于有没有返回值 .
示例代码
- 泛型参数表示返回值的类型
futureTask.get()
可以阻塞等待线程计算完毕, 并获取 futureTask 中的结果
JUC (java.util.concurrent) 的常见类
ReentrantLock 类
可重入互斥锁, 和 synchronized 定位相似, 都是用来实现互斥效果, 保证线程安全的
“Reentrant” 该单词原意就是 “可重入”
ReentrantLock 的用法 :
- lock () : 加锁, 死等
- trylock (超时时间) : 加锁, 超时就放弃加锁
- unlock () : 解锁
ReentrantLock 和 synchronized 的区别
- synchronized 是一个关键字, 是 JVM 内部实现的. ReentrantLock 是标准库的一个类, 在 JVM 外实现 (基于 Java 实现) .
- synchronized 使用时不需要收到那个释放锁, ReentrantLock 需要手动释放, 使用更灵活.
- synchronized 在申请锁失败时, 会死等, ReentrantLock 可以设置超时时间
- synchronized 是非公平锁, ReentrantLock 默认是非公平锁, 可通过构造方法, 设置为公平锁 .
- ReentrantLock 有更强大的唤醒机制(这个了解就好, 有点深了), synchronized 通过 Object 的 wait / notify 实现 等待-唤醒, 随机唤醒(非公平锁). ReentrantLock 搭配 Condition 类实现 等待-唤醒, 可以精确控制唤醒哪个线程 (可设置为公平锁).
如何选择使用二者?
- 锁竞争不激烈的时候, 选 synchronized, 更方便高效
- 需要公平锁, 用 ReentrantLock 并设置构造方法
原子类
原子类内部使用 CAS 实现, 所以性能要优于加锁, 有以下几个
- AtomicBoolean
- AtomicInteger
- AtomicIntegerArray
- AtomicLong
- AtomicReference
- AtomicStampedReference
原子类内部有许多方法, 具有原子性
线程池
解决频繁创建线程的问题
ExecutorService & Executors
- ExecutorService 表示一个线程池的实例
- Executor 是一个工厂类, 能够创建出集中不同风格的线程池
- ExecutorService 的 submit 方法能够向线程池中提交若干个任务
代码示例
private static void test01() { ExecutorService pool = Executors.newFixedThreadPool(10); pool.submit(new Runnable() { @Override public void run() { System.out.println("hello_zrj!"); } }); }
Executors 创建线程池的几种方法
- newFixedThreadPool : 创建固定线程数的线程池
- newCachedThreadPool : 创建线程数数目动态增长的线程池
- newSingleThreadExecutor : 创建只含单个线程的线程池
- newScheduledThreadPool : 设置延迟时间后执行命令, 或者定期执行命令, 进阶版的 Timer
Executors 本质上是 ThreadPoolExecutor 类的封装
ThreadPoolExecutor 提供了更多的可选参数, 可以进一步细化线程池行为的设定
信号量
表示 “可用资源的数量”, 本质上就是一个计数器
Semaphore 的 PV 操作中, 加减计数器操作都是原子的, 可在 多线程 下直接使用
代码示例
private static void test02() { Semaphore semaphore = new Semaphore(4); Runnable runnable = new Runnable() { @Override public void run() { try { System.out.println("申请资源"); semaphore.acquire(); System.out.println("获取到资源"); Thread.sleep(1000); System.out.println("释放资源"); semaphore.release(); } catch (InterruptedException e) { e.printStackTrace(); } } }; for (int i = 0; i < 20; i++) { Thread t = new Thread(runnable); t.start(); } }
上述代码因为设置的信号量为4, 因此最多只能有四个线程在同时正常进行, 其余的线程, 需要阻塞队列等待信号量空余才能调用 .
CountDownLatch
同时等待 N 个任务结束
示例代码
private static void test03() throws InterruptedException { CountDownLatch latch = new CountDownLatch(10); Runnable runnable = new Runnable() { @Override public void run() { try { Thread.sleep(1000); latch.countDown(); } catch (InterruptedException e) { e.printStackTrace(); } } }; for (int i = 0; i < 10; i++) { new Thread(runnable).start(); } // 必须等到 10 个线程都结束 latch.await(); System.out.println("阻塞结束, 运行后续代码"); }
线程安全的集合类
Vector , Stack , HashTable
多线程环境下使用 ArrayList
- 加锁, synchronized & ReentrantLock
- Collections.synchronizedList(new ArrayList) ;
synchronizedList 是标准库提供的一个基于 synchronized 进行线程同步的 List
synchronizedList 的关键操作上都带有 synchronized - 使用 CopyOnWriteArrayList
CopyOnWrite 容器即写时复制的容器, 采用读写分离思想
往该容器添加元素的时候, 不会直接添加, 而是先将该容器进行拷贝到新容器, 往新容器添加元素, 再将原容器的引用指向新的容器
好处是可以进行并发的读, 而不用加锁 (读也是拷出来读, 所以对原容器的修改不会涉及到这个读的容器)
优点 :
在读多写少的情况下, 性能高 (不涉及锁竞争)
缺点 :
占用内存多 (容器拷贝)
新写的数据, 不能第一时间获取到 (写完后, 还得把原容器的引用指向新容器, 中间涉及时间消耗)
多线程环境使用队列
ArrayBLockQueue
基于数组实现的阻塞队列
LinkedBlockQueue
基于链表实现的阻塞队列
PriorityBlockQueue
基于堆实现的优先级阻塞队列
TransferQueue
最多只包含一个元素的阻塞队列
多线程环境使用哈希表
HashMap 不是线程安全的
多线程下可以使用 :
- HashTable
- ConcurrentHashMap
HashTable
只是简单的把关键方法加上了 synchronized 关键字
相当于直接针对 Hashtable 对象本身加锁
- 多线程访问同一个 HashTable 对象, 会造成锁冲突
- size 属性也通过 synchronized 来控制同步, 比较慢
- 一旦触发扩容, 由当前占用线程完成整个扩容过程, 该过程设计大量元素拷贝, 效率相当低
ConcurrentHashMap
相当于对 HashTable 进行一定的优化
- 读操作没有加锁 (但是使用 volatile 保证内存可见性), 写操作加锁 (仍使用 synchronized), 锁的不是整个对象, 而是 “锁桶 (每个链表的头结点)”, 这样只有两个线程访问的恰好是一个哈希桶上的数据才会出现锁冲突
- 充分利用 CAS 特性, 如 size 属性通过 CAS 更新, 避免出现重量级锁
- 优化扩容方式: 化整为零
发现需要扩容的线程, 只需要创建一个新的数组, 同时搬几个元素过去
扩容期间, 新老数组同时存在
- 后续每个操作 ConcurrentHashMao 的线程, 都会参与搬元素的过程 (每个线程只操作一部分)
搬完最后一个元素后, 删除老数组
在搬元素的过程中, 插入数据只操作新数组, 查找数据只操作老数组
死锁
什么是死锁
多个线程同时阻塞, 并且互相占有彼此所需的资源, 并且资源之间不可抢夺, 导致程序被无限期阻塞, 因此程序不可能正常终止
死锁的特性
- 互斥使用
- 不可抢占
- 请求和保持
- 循环等待
如何避免死锁
从特性入手
具体的话, emm … (
翻课本吧 BUSHI)(写累了, 下次再说, 有缘再补上 …)