这个是在学习工作中的一些总结,若有不对之处欢迎大家指出。侵删!
需要源码联系QQ:1352057131
得之在俄顷,积之在平日。
Lock的简介、地位和作用
锁是一种工具,用于控制对共享资源的访问
Lock和Synchronized这两个是最常见的锁,他们都可以达到线程安全的目的,但是 在使用上和功能上又有较大的不同。
Lock并不是用来代替Synchronized的,而是当使用Synchronized不合适或不足以满足要求的时候来提供高级功能。
Lock接口最常见的实现类是ReentrantLock。
通常情况下,lock只允许一个线程来访问这个共享资源,不过有的时候,一些特殊的实现也可以允许并发访问,比如ReadWritelock里面的readlock
Synchronized的缺陷
1、效率低:锁的释放情况少,试图获得锁市不能设定超时,不能中断一个正在试图获得锁的线程。
2、不够灵活(读写锁更灵活):加锁和释放锁的时机单一。
3、无法知道是否成功获取锁。
常用方法:
1、lock():这是最常见的获取锁的方式,如果锁已经被其他线程获取,则进行等待,lock不会像Synchronized那样在异常时释放锁,所以我们就必须在finally里面手动释放锁。
2、tryLock():用来尝试获取锁,获取成功就返回true,失败就返回false,该方法会立即返回,不会等待拿锁。
3、tryLock(long time,TimeUnit unit):与tryLock()区别在于设置获取锁的时间,超时就直接返回false
4、unlock():解锁
锁的可见性保证
Lock的加解锁和Synchronized有同样的内存语义即下一个加锁前可以看到前一个解锁后的所有语句
锁的分类
悲观锁(互斥同步锁)和乐观锁
悲观锁的实现
Synchronized和lock相关的类。
悲观锁的劣势
1、阻塞和唤醒带来的性能劣势。
2、永久阻塞:如果持有锁的线程被永久阻塞,比如遇到了无限循环、死锁等问题。
乐观锁的实现
一般都是利用CAS算法来实现的,典型例子有:Git 原子类 并发容器 数据库等。
ReentrantLock用法
public class LockTest {
static ExecutorService ExecutorService = Executors.newScheduledThreadPool(2);
//创建锁
private static ReentrantLock lock = new ReentrantLock();
public static class Run implements Runnable{
@Override
public void run() {
System.out.println(Thread.currentThread().getName()+"拿到锁");
}
}
public static void main(String[] args) throws InterruptedException {
for (int i = 0; i <10 ; i++) {
lock.lock();
try {
ExecutorService.submit(new Run());
Thread.sleep(1000);
}finally {
System.out.println("释放锁");
lock.unlock();
}
}
}
}
乐观锁与悲观锁的开销对比
悲观锁的原始开销高于乐观锁,但是特点是一劳永逸,临界区持锁时间就算越来越差也不会对互斥锁的开销造成影响;相反,虽然乐观锁一开始的开销比悲观锁小,如果自旋时间很长或者不停的重试,那么消耗资源越来越多。
悲观锁和乐观锁的使用场景
乐观锁:适合并发写入少,大部分是读取的场景。
悲观锁:适合并发写入多的情况,适用于临界区持锁时间比较长的情况,悲观锁可以避免大量的无用自旋等消耗,典型情况:
1、临界区有IO操作
2、临界区代码复杂或者循环量大
3、临界区竞争非常激烈
可重入锁与非可重入锁
可重入
同一个线程可以多次获得同一把锁。
好处
避免死锁,提高封装性
Synchronized和reentrantlock是可重入锁
源码对比
可重入锁示例(汽车上牌照)
public class LockTest {
private static ReentrantLock lock = new ReentrantLock();
public static void main(String[] args) {
System.out.println("该锁被拿到第"+lock.getHoldCount()+"次");
lock.lock();
System.out.println("该锁被拿到第"+lock.getHoldCount()+"次");
lock.lock();
System.out.println("该锁被拿到第"+lock.getHoldCount()+"次");
lock.lock();
System.out.println("该锁被拿到第"+lock.getHoldCount()+"次");
lock.lock();
}
}
输出结果:
该锁被拿到第0次
该锁被拿到第1次
该锁被拿到第2次
该锁被拿到第3次
公平锁与非公平锁
非公平锁的好处
提高效率
避免唤醒带来的空档期
示例
public class LockTest {
public static void main(String[] args) {
LockQueue lockQueue =new LockQueue();
ExecutorService executorService = Executors.newFixedThreadPool(2);
for (int i = 0; i <2 ; i++) {
executorService.submit(new Run(lockQueue));
}
executorService.shutdown();
}
}
class Run implements Runnable{
private LockQueue lockQueue;
public Run(LockQueue lockQueue) {
this.lockQueue = lockQueue;
}
@Override
public void run() {
System.out.println(Thread.currentThread().getName()+"开始打印");
lockQueue.printTest();
System.out.println(Thread.currentThread().getName()+"结束打印");
}
}
class LockQueue{
//创建锁 传入一个false则为非公平锁
private ReentrantLock lock = new ReentrantLock(false);
public void printTest(){
lock.lock();
try {
//设置打印时间
int time = new Random().nextInt(100);
System.out.println(Thread.currentThread().getName()+"正在打印需要"+time+"秒");
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}finally {
lock.unlock();
}
lock.lock();
try {
//设置打印时间
int time = new Random().nextInt(100);
System.out.println(Thread.currentThread().getName()+"打印需要"+time+"秒");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}finally {
lock.unlock();
}
}
}
特例
tryLock()方法不遵守设定的公平规则,当有线程执行tryLock()方法,一旦有线程释放了锁,那么这个正在执行tryLock()方法的线程就能立即获取到锁,即使在它之前已经有其他线程在队列里等待了。
公平锁与非公平锁优缺点
源码对比
共享锁和排它锁
理解
排它锁:又称为独占锁、独享锁(写锁就是排它锁)
共享锁:又称为读锁,获得共享锁后只可以读不可以修改或删除
读写锁的作用
多个读操作是可以同时进行的,并不会发生线程安全问题;在读的地方使用读锁,写的地方使用写锁,如果没有写锁的情况下,读是无阻塞的,这样就提高了程序的执行效率。
读写锁的规则
要么多个线程读,写操作阻塞;要么一个线程写,其余线程阻塞。
示例
public class LockTest {
public static void main(String[] args) {
ReadAndWrite readAndWrite = new ReadAndWrite();
Thread thread0 = new Thread(new RunRead(readAndWrite));
Thread thread1 = new Thread(new RunRead(readAndWrite));
Thread thread2 = new Thread(new RunWrite(readAndWrite));
Thread thread3 = new Thread(new RunWrite(readAndWrite));
thread0.start();
thread1.start();
thread2.start();
thread3.start();
}
}
//读
class RunRead implements Runnable{
private ReadAndWrite readAndWrite;
public RunRead(ReadAndWrite readAndWrite) {
this.readAndWrite = readAndWrite;
}
@Override
public void run() {
readAndWrite.read();
}
}
//写
class RunWrite implements Runnable{
private ReadAndWrite readAndWrite;
public RunWrite(ReadAndWrite readAndWrite) {
this.readAndWrite = readAndWrite;
}
@Override
public void run() {
readAndWrite.write();
}
}
class ReadAndWrite{
private static ReentrantReadWriteLock reentrantReadWriteLock = new ReentrantReadWriteLock();
//获得读锁
private static ReentrantReadWriteLock.ReadLock readLock = reentrantReadWriteLock.readLock();
//获得写锁
private static ReentrantReadWriteLock.WriteLock writeLock = reentrantReadWriteLock.writeLock();
//读的方法
public void read(){
readLock.lock();
try {
System.out.println(Thread.currentThread().getName()+"正在读取");
Thread.sleep(100);
System.out.println(Thread.currentThread().getName()+"读取完毕");
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
System.out.println(Thread.currentThread().getName()+"释放了读锁");
readLock.unlock();
}
}
//写的方法
public void write(){
writeLock.lock();
try {
System.out.println(Thread.currentThread().getName()+"正在写");
Thread.sleep(100);
System.out.println(Thread.currentThread().getName()+"写完毕");
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
System.out.println(Thread.currentThread().getName()+"释放了读锁");
writeLock.unlock();
}
}
}
读锁的插队策略
在非公平锁中,读锁插队的时候看头结点是否为写锁,若为写锁则排队,这样可以有效的防止线程饥饿;在公平锁中不存在插队。
锁的升降级
写锁可以降级,读锁不能升级,因为读锁升级可能会造成死锁。
注意
如果当前线程拥有写锁,然后将其释放,最后再获取到读锁,这种分段完成的过程不能称之为锁降级。
锁降级的应用场景
对于数据比较敏感, 需要在对数据修改以后, 获取到修改后的值, 并进行接下来的其它操作
示例:
public class LockTest {
public static void main(String[] args) {
ReadAndWrite readAndWrite = new ReadAndWrite();
Thread thread0 = new Thread(new RunRead(readAndWrite));
Thread thread1 = new Thread(new RunRead(readAndWrite));
Thread thread2 = new Thread(new RunWrite(readAndWrite));
Thread thread3 = new Thread(new RunRead(readAndWrite));
thread0.start();
thread1.start();
thread2.start();
thread3.start();
}
}
//读
class RunRead implements Runnable{
private ReadAndWrite readAndWrite;
public RunRead(ReadAndWrite readAndWrite) {
this.readAndWrite = readAndWrite;
}
@Override
public void run() {
readAndWrite.read();
}
}
//写
class RunWrite implements Runnable{
private ReadAndWrite readAndWrite;
public RunWrite(ReadAndWrite readAndWrite) {
this.readAndWrite = readAndWrite;
}
@Override
public void run() {
readAndWrite.write();
}
}
class ReadAndWrite{
private static ReentrantReadWriteLock reentrantReadWriteLock = new ReentrantReadWriteLock(false);
//获得读锁
private static ReentrantReadWriteLock.ReadLock readLock = reentrantReadWriteLock.readLock();
//获得写锁
private static ReentrantReadWriteLock.WriteLock writeLock = reentrantReadWriteLock.writeLock();
//读的方法
public void read(){
readLock.lock();
try {
System.out.println(Thread.currentThread().getName()+"正在读取");
Thread.sleep(100);
System.out.println(Thread.currentThread().getName()+"读取完毕");
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
System.out.println(Thread.currentThread().getName()+"释放了读锁");
readLock.unlock();
}
}
//写的方法
public void write(){
writeLock.lock();
try {
System.out.println(Thread.currentThread().getName()+"正在写");
Thread.sleep(100);
System.out.println(Thread.currentThread().getName()+"写完毕");
System.out.println(Thread.currentThread().getName()+"准备降级为写锁");
readLock.lock();
System.out.println(Thread.currentThread().getName()+"降级为写锁成功");
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
System.out.println(Thread.currentThread().getName()+"释放了读写锁");
writeLock.unlock();
readLock.unlock();
}
}
}
自旋锁与阻塞锁
自旋锁
是指当一个线程在获取锁的时候,如果锁已经被其它线程获取,那么该线程将循环等待,然后不断的判断锁是否能够被成功获取,直到获取到锁才会退出循环(atomic里面的都是基于自旋锁)。
阻塞锁
与自旋锁相反,阻塞锁如果遇到没拿到锁的情况,会直接把线程阻塞直到被唤醒。
自旋锁示例
public class LockTest {
private AtomicReference<Thread> atomicReference = new AtomicReference<>();
//加锁
public void lock(){
//拿到当前线程引用
Thread current = Thread.currentThread();
while (!atomicReference.compareAndSet(null,current)){
System.out.println(Thread.currentThread().getName()+"获取失败,正在尝试");
}
}
//解锁
public void unlock(){
Thread current = Thread.currentThread();
atomicReference.compareAndSet(current,null);
}
public static void main(String[] args) {
LockTest lockTest = new LockTest();
Runnable runnable = new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName()+"开始尝试获取自旋锁");
lockTest.lock();
System.out.println(Thread.currentThread().getName()+"获得了自旋锁");
try {
Thread.sleep(200);
} catch (InterruptedException e) {
e.printStackTrace();
}finally {
System.out.println(Thread.currentThread().getName()+"解锁成功");
lockTest.unlock();
}
}
};
Thread thread0 = new Thread(runnable);
Thread thread1 = new Thread(runnable);
thread0.start();
thread1.start();
}
}
当第一个线程thread0获取锁的时候,能够成功获取到,不会进入while循环,如果此时线程thread0没有释放锁,另一个线程thread1又来获取锁,此时由于不满足CAS,所以就会进入while循环,不断判断是否满足CAS,直到thread0线程调用unlock方法释放了该锁。
自旋锁的优缺点
优点:自旋锁不会使线程状态发生切换,一直处于用户态,即线程一直都是active的;不会使线程进入阻塞状态,减少了不必要的上下文切换,执行速度快
缺点:在自旋的过程中一直消耗cpu,如果锁被占用的时间很长,那么自旋的线程只会白白浪费处理器资源。
自旋锁适用场景
一般用于多核服务器,在并发不是很高的情况下,比阻塞效率高。
自旋锁适用于临界区比较短小的情况下。
锁的优化
Java虚拟机对锁的优化
自旋锁和自适应
锁消除
锁优化
写代码时优化
缩小同步代码块
尽量不要锁住方法,尽量使用代码块
减少锁的次数
锁中不要包含锁,容易造成死锁
选择合适的锁类型和工具类