空空叶博客 学习与开发博客

Java锁的研究

2019-03-05
kongkongye

Java锁的研究,包括相关概念,锁的分类,锁优化,死锁等.

概念

  • 原子性(atomicity): 指一个操作不可中断,要么全部成功要么全部失败
  • 可见性(visibility): 指当一个线程修改了共享变量后,其他线程能立即得知这个修改(它要对付内存缓存和编译器优化的各种反常行为)
  • 同步/异步: 同步指一个任务需要依赖另一个任务时,必须等待依赖的任务完成;异步指不需要等待依赖的任务完成.
  • 阻塞/非阻塞: 从CPU消耗上说,阻塞就是CPU等待慢操作完成;非阻塞就是慢操作执行时CPU去干其他事.

锁分类

公平锁/非公平锁

公平锁是指线程按照申请锁的顺序来获取锁.

非公平锁的吞吐量更大.

ReentrantLock可以通过构造函数指定是否是公平锁,默认是非公平锁.

synchronized是非公平锁,并且没任何办法变成公平锁.

可重入锁

又名递归锁,指在外层获取锁后,在内层会自动获取锁.

可一定程度避免死锁.

ReentrantLocksynchronized都是可重入锁.

以下代码描述了可重入锁:

synchronized void setA() {
  setB();
}
synchronized void setB() {
}

独享锁/共享锁

独享锁指该锁只能被一个线程持有,共享锁指该锁可以同时被多个线程持有.

ReentrantLocksynchronized是独享锁,ReadWriteLock的读锁是共享锁,写锁是独享锁

互斥锁/读写锁

ReentrantLock是互斥锁,ReadWriteLock是读写锁

读锁是共享锁,写锁是排他锁.

乐观锁/悲观锁

指看待并发同步的角度,悲观锁假定会发生冲突,因此会加锁(在mysql角度,依靠数据库的加锁机制). 乐观锁假定不会发生冲突,只在提交时检测是否冲突,CAS(compare and swap)是最常见的乐观锁(在mysql角度,一般采用版本号方式实现,但乐观锁不能解决脏读问题)

乐观锁适合大量读操作的场景,会带来大量性能提升.

分段锁

分段锁是一种锁的设计.

ConcurrentHashMap使用的就是分段锁.

偏向锁/轻量级锁/重量级锁

这三种指的是synchronized锁的状态(jdk1.6后引入的).

偏向锁指一段代码一直被一个线程所访问,那么该线程会自动获取锁,降低获取锁的代价.

轻量级锁指锁是偏向锁的时候,被另一个线程所访问,偏向锁就会升级为轻量级锁,其它线程会通过自旋的形式尝试获取锁,不会阻塞,提高性能.

重量级锁指锁为轻量级锁的时候,另一个线程在自旋一定次数后,如果还没有获取到锁,就会进入阻塞,该锁升级为重量级锁.重量级锁会让其它申请的线程进入阻塞,性能降低.

自旋锁

自旋锁指尝试获取锁的进程不会立即阻塞,而是采用循环的方式去尝试获取锁,好处是减少线程上下文切换的消耗,坏处是会消耗CPU.

锁的优化

  1. 减少锁持有时间
  2. 减小锁粒度
  3. 读写分离锁代替独占锁: 适合读多写少的情况
  4. 锁分离: 像LinkedBlockingQueue, 把take()与put()分离
  5. 锁粗化: 主要避免循环内反复申请锁

死锁

死锁是指两个或两个以上的进程在执行过程中,由于竞争资源或者由于彼此通信而造成的一种阻塞的现象,若无外力作用,它们都将无法推进下去。

预防

  1. 以确定顺序获得锁
  2. 超时放弃

其他死锁类型

  • 线程池死锁,比如任务A获得线程后需要等待任务B的执行结果,任务B需要获得线程才能执行,但线程池没有多余线程了(比如线程池是单线程的)
  • 连接池死锁,比如数据库连接池大小都为1的N1与N2,线程A以顺序N1,N2来获得数据库连接;线程B以顺序N2,N1来获得数据库连接,就可能发生死锁

synchronized

根据修饰对象分类

  • 修饰代码块
    • synchronized(this|object) {}
    • synchronized(类.class) {}
  • 修饰方法
    • 修饰非静态方法
    • 修饰静态方法

根据获取的锁分类

  • 获取对象锁
    • synchronized(this|object) {}
    • 修饰非静态方法
  • 获取类锁
    • synchronized(类.class) {}
    • 修饰静态方法

注意事项

  • synchronized关键字不能继承: 父类方法有synchronized关键字,之类覆盖时不会继承,必须手动指定
  • 定义接口方法时不能使用synchronized
  • 构造方法不能使用synchronized,但可以使用synchronized代码块

与ReentrantLock比较

共同点

  • 都是协调多线程对变量的访问
  • 都是可重入锁
  • 都保证了可见性与互斥性

不同点

  • ReentrantLock显示获得、释放锁,synchronized隐式获得释放锁
  • ReentrantLock可响应中断、可轮回,synchronized不可响应中断
  • ReentrantLock是API级别的,synchronized是JVM级别的
  • ReentrantLock可以实现公平锁
  • ReentrantLock通过Condition可以绑定多个条件
  • 底层实现不一样, synchronized是同步阻塞,使用的是悲观并发策略,lock是同步非阻塞,采用的是乐观并发策略
  • synchronized不支持分段加锁

性能

从jdk1.5后,jvm对synchronized进行了优化(偏向锁/轻量级锁…),性能就差不多了.

推荐只在synchronized无法满足的情况下使用ReentrantLock.

wait & notify & notifyAll

先说两个概念:

  • 锁池: 假设线程A拥有某个对象的锁,其他线程要获得锁,就会进入此对象的锁池
  • 等待池: 假设线程A调用某个对象的wait()方法,就会释放锁,进入此对象的等待池

然后就很好理解:

  • wait: 释放锁,并进入对象的锁池
  • notify: (不释放锁)随机唤醒一个等待池中线程进入锁池
  • notifyAll: (不释放锁)唤醒全部等待池中线程进入锁池

sleep

线程sleep方法不会释放锁

与wait的区别

  • wait释放锁,sleep不释放
  • wait用来进行线程间通信,sleep用来在执行时暂停

并发工具类

Semaphore 信号量

控制同时执行某个操作/访问某个资源的数量

Semaphore管理着一组许可(permit),操作要先获得许可才能进行

比如厕所只有5个位置,有人离开了其他人才能进入.

CountDownLatch 闭锁

可以延迟线程的进度直到达到终止状态,它强调的是一个/多个线程需要等待另外的n个线程干完某事后才继续执行.

比如裁判等待运动员都跑到终点后再宣布结果.

CyclicBarrier 栅栏

它强调的是多个线程全部都到达栅栏位置后再继续执行.

比如运动员等待所有人都跑到终点后再一起去喝酒.


上一篇 IOS内存管理

下一篇 mysql分库分表

目录