在 Java 中主要通过使用 synchronized , volatile 关键字, 及 Lock 接口的子类 ReentrantLock 和 ReadWriteLock 等来实现加锁.
synchronized
属性
synchronized 属于独占式的悲观锁, 同时属于可重入锁.
作用
synchronized 可以把任意一个非 NULL 的对象当作锁. 其在不同场景下的作用范围如下:
作用于方法时, 锁住的是对象的实例 (this);
作用于静态方法时, 锁住的是 Class 实例, 会锁住所有调用该方法的线程.(又因为 Class 的相关数据存储在永久代 PermGen[Jdk1.8 则是 metaspace] , 永久代是全局共享的, 因此静态方法锁相当于类的一个全局锁);
作用于一个对象实例时, 锁住的是所有以该对象为锁的代码块.
实现
它有多个队列, 当多个线程一起访问某个对象监视器的时候, 对象监视器会将这些线程存储在不同的容器中.
Wait Set: 存储调用 wait 方法被阻塞的线程;
Contention List(竞争队列): 所有请求锁的线程首先被放在这个竞争队列中;
Entry List:Contention List 中那些有资格成为候选资源的线程被移动到 Entry List 中;
OnDeck: 任意时刻, 最多只有一个线程正在竞争锁资源, 该线程成为 OnDeck;
Owner: 当前已经获取到所资源的线程被称为 Owner;
!Owner: 当前释放锁的线程.
参考资料
volatile
属性
比 sychronized 更轻量级的同步锁
适用场景
使用 volatile 必须同时满足下面两个条件才能保证在并发环境的线程安全:
对变量的写操作不依赖于当前值 (比如 i++), 或者说是单纯的变量赋值 (boolean flag = true);
不同的 volatile 变量之间, 不能互相依赖, 只有在状态真正独立于程序内其他内容时才能使用 volatile.
对 volatile 变量的单次读 / 写操作可以保证原子性的, 如 long 和 double 类型变量, 但是并不能保证 i++ 这种操作的原子性, 因为本质上 i++ 是读, 写两次操作.
Lock
Java 中的锁都实现于 Lock 接口, 主要方法有:
void lock(): 用于获取锁. 如果锁可用, 则获取锁. 若锁不可用, 将禁用当前线程, 直到取到锁;
boolean tryLock(): 尝试获取锁. 如果锁可用, 则获取锁, 并返回 true, 否则返回 false;
该方法和 lock() 的区别在于, 如果锁不可用, tryLock() 不会导致当前线程被禁用.
tryLock(long timeout TimeUnit unit): 如果锁在给定等待时间内没有被另一个线程保持, 则获取该锁.
void unlock(): 释放锁. 锁只能由持有者释放, 如果线程并不持有锁, 却执行该方法. 可能导致异常的发生;
Condition newCondition(): 条件对象, 获取等待通知组件. 该组件和当前的锁绑定, 当前线程只有获取了锁, 才能调用该组件的 await() 方法, 而调用后, 当前线程将缩放锁;
getHoldCount() : 查询当前线程保持此锁的次数.
getQueueLength(): 返回正等待获取此锁的线程估计数. 比如启动 10 个线程, 1 个线程获得锁, 此时返回的是 9;
getWaitQueueLength:(Condition condition) 返回等待与此锁相关的给定条件的线程估计数. 比如 10 个线程, 用同一个 condition 对象, 并且此时这 10 个线程都执行了 condition 对象的 await 方法, 那么此时执行此方法返回 10;
hasWaiters(Condition condition): 查询是否有线程等待与此锁有关的给定条件 (condition). 对于指定 contidion 对象, 有多少线程执行了 condition.await 方法;
hasQueuedThread(Thread thread): 查询给定线程是否等待获取此锁;
hasQueuedThreads(): 是否有线程等待此锁;
isFair(): 该锁是否公平锁;
isHeldByCurrentThread(): 当前线程是否保持锁锁定, 线程的执行 lock 方法的前后分别是 false 和 true;
isLock(): 此锁是否有任意线程占用;
lockInterruptibly(): 如果当前线程未被中断, 获取锁.
tryLock 和 lock 和 lockInterruptibly 的区别
tryLock 能获得锁就返回 true, 不能就立即返回 false,tryLock(long timeout,TimeUnit unit), 可以增加时间限制, 如果超过该时间段还没获得锁, 返回 false;
lock 能获得锁就返回 true, 不能的话一直等待获得锁;
lock 和 lockInterruptibly, 如果两个线程分别执行这两个方法, 但此时中断这两个线程, lock 不会抛出异常, 而 lockInterruptibly 会抛出异常.
Condition
作用
Condition 的作用是对锁进行更精确的控制. 对于同一个锁, 我们可以创建多个 Condition, 在不同的情况下使用不同的 Condition.
Condition 和 Object
相似之处:
Condition 类的 awiat 方法和 Object 类的 wait 方法等效;
Condition 类的 signal 方法和 Object 类的 notify 方法等效;
Condition 类的 signalAll 方法和 Object 类的 notifyAll 方法等效.
不同处:
ReentrantLock 类可以唤醒指定条件的线程, 而 object 的唤醒是随机的;
Object 中的 wait(),notify(),notifyAll() 方法是和 "同步锁"(synchronized 关键字) 捆绑使用的; 而 Condition 是需要与 "互斥锁"/"共享锁" 捆绑使用的.
ReentrantLock
属性
可重入锁.
特点
除了能完成 synchronized 所能完成的所有工作外, 还提供了诸如可响应中断锁, 可轮询锁请求, 定时锁等避免多线程死锁的方法.
与 synchronized 的区别
ReentrantLock 需要通过方法 lock() 与 unlock() 手动进行加锁与解锁操作, 而 synchronized 会 被 JVM 自动加锁, 解锁;
ReentrantLock 相比 synchronized 的优势是可中断, 公平锁, 多个锁.
- public class MyLock {
- private Lock lock = new ReentrantLock();
- // Lock lock = new ReentrantLock(true); // 公平锁
- // Lock lock = new ReentrantLock(false); // 非公平锁
- private Condition condition = lock.newCondition(); // 创建 Condition
- public void testMethod() {
- try {
- lock.lock(); //lock 加锁
- // 1:wait 方法等待:
- //System.out.println("开始 wait");
- condition.await();
- // 通过创建 Condition 对象来使线程 wait, 必须先执行 lock.lock 方法获得锁
- // 2:signal 方法唤醒
- condition.signal(); //condition 对象的 signal 方法可以唤醒 wait 线程
- for (int i = 0; i < 5; i++) {
- System.out.println("ThreadName=" + Thread.currentThread().getName() + (" " + (i + 1)));
- }
- } catch (InterruptedException e) {
- e.printStackTrace();
- } finally {
- lock.unlock();
- }
- }
- }
- ReadWriteLock
属性
共享锁 (读 - 写锁)
特点
如果没有写锁的情况下, 读是无阻塞的, 在一定程度上提高了程序的执行效率;
读写锁分为读锁和写锁, 多个读锁不互斥, 读锁与写锁互斥 (这是由 JVM 自己控制的, 代码只要上好相应的锁即可).
使用原则
如果你的代码只读数据, 可以很多人同时读, 但不能同时写, 那就上读锁;
如果你的代码修改数据, 只能有一个人在写, 且不能同时读取, 那就上写锁.
CountDownLatch(线程计数器 )
作用
CountDownLatch 是一个同步辅助类, 在完成一组正在其他线程中执行的操作之前, 它允许一个或多个线程一直等待.
- final CountDownLatch latch = new CountDownLatch(2);
- new Thread() {
- public void run() {
- System.out.println("子线程" + Thread.currentThread().getName() + "正在执行");
- Thread.sleep(3000);
- System.out.println("子线程" + Thread.currentThread().getName() + "执行完毕");
- latch.countDown();
- }
- ;
- }.start();
- new Thread() {
- public void run() {
- System.out.println("子线程" + Thread.currentThread().getName() + "正在执行");
- Thread.sleep(3000);
- System.out.println("子线程" + Thread.currentThread().getName() + "执行完毕");
- latch.countDown();
- }
- ;
- }.start();
- System.out.println("等待 2 个子线程执行完毕...");
- latch.await();
- System.out.println("2 个子线程已经执行完毕");
- System.out.println("继续执行主线程");
- CyclicBarrierr(回环栅栏 - 等待至 barrier 状态再全部同时执行)
作用
CyclicBarrier 是一个同步辅助类, 允许一组线程互相等待, 直到到达某个公共屏障点 (common barrier point). 因为该 barrier 在释放等待线程后可以重用, 所以称它为循环 的 barrier.
主要方法
CyclicBarrier 中最重要的方法就是 await 方法, 它有 2 个重载版本:
public int await(): 用来挂起当前线程, 直至所有线程都到达 barrier 状态再同时执行后续任务;
public int await(long timeout, TimeUnit unit): 让这些线程等待至一定的时间, 如果还有线程没有到达 barrier 状态就直接让到达 barrier 的线程执行后续任务.
- public static void main(String[] args) {
- int N = 4;
- CyclicBarrier barrier = new CyclicBarrier(N);
- for (int i = 0; i < N; i++)
- new Writer(barrier).start();
- }
- static class Writer extends Thread {
- private CyclicBarrier cyclicBarrier;
- public Writer(CyclicBarrier cyclicBarrier) {
- this.cyclicBarrier = cyclicBarrier;
- }
- @Override
- public void run() {
- try {
- Thread.sleep(5000); // 以睡眠来模拟线程需要预定写入数据操作
- System.out.println("线程" + Thread.currentThread().getName() + "写入数据完 毕, 等待其他线程写入完毕");
- cyclicBarrier.await();
- } catch (InterruptedException e) {
- e.printStackTrace();
- } catch (BrokenBarrierException e) {
- e.printStackTrace();
- }
- System.out.println("所有线程写入完毕, 继续处理其他任务, 比如数据操作");
- }
- }
CountDownLatch 和 CyclicBarrier 的区别
CountDownLatch 的作用是允许 1 或 N 个线程等待其他线程完成执行; 而 CyclicBarrier 则是允许 N 个线程相互等待;
CountDownLatch 的计数器无法被重置; CyclicBarrier 的计数器可以被重置后使用, 因此它被称为是循环的 barrier.
Semaphore
Semaphore 是一种基于计数的信号量. 它可以设定一个阈值, 基于此, 多个线程竞争获取许可信号, 做完自己的申请后归还, 超过阈值后, 线程申请许可信号将会被阻塞. Semaphore 可以用来构建一些对象池, 资源池之类的, 比如数据库连接池.
实现互斥锁 (计数器为 1)
我们也可以创建计数为 1 的 Semaphore, 将其作为一种类似互斥锁的机制, 这也叫二元信号量, 表示两种互斥状态.
代码实现
它的用法如下:
- // 创建一个计数阈值为 5 的信号量对象
- // 只能 5 个线程同时访问
- Semaphore semp = new Semaphore(5);
- try { // 申请许可
- semp.acquire();
- try {
- // 业务逻辑
- } catch (Exception e) {
- } finally {
- // 释放许可
- semp.release();
- }
- } catch (InterruptedException e) {
- }
Semaphore 与 ReentrantLock 相似处
Semaphore 基本能完成 ReentrantLock 的所有工作, 使用方法也有许多类似之处:
都需要手动加锁. 通过 acquire() 与 release() 方法来获得和释放资源;
Semaphone.acquire() 方法默认为可响应中断锁, 与 ReentrantLock.lockInterruptibly() 作用效果一致, 也就是说在等待临界资源的过程中可以被 Thread.interrupt() 方法中断;
Semaphore 也实现了可轮询的锁请求与定时锁的功能, 除了方法名 tryAcquire 与 tryLock 不同, 其使用方法与 ReentrantLock 几乎一致;
Semaphore 也提供了公平与非公平锁的机制, 也可在构造函数中进行设定;
锁释放方式相同. 与 ReentrantLock 一样 Semaphore 的锁释放操作也由手动进行. 同时, 为避免线程因抛出异常而无法正常释放锁的情况发生, 释放锁的操作也必须在 finally 代码块中完成.
多线程与并发系列推荐
Java 多线程并发 06--CAS 与 AQS
Java 多线程并发 05-- 那么多的锁你都了解了吗
Java 多线程并发 04-- 合理使用线程池
Java 多线程并发 03-- 什么是线程上下文, 线程是如何调度的
Java 多线程并发 02-- 线程的生命周期与常用方法, 你都掌握了吗
Java 多线程并发 01-- 线程的创建与终止, 你会几种方式
来源: https://www.cnblogs.com/weechang/p/12542843.html