java

JUC并发编程(三)——锁

Nick · 6月5日 · 2025年 · 本文16142字 · 阅读41分钟182

千变万化的锁

  1. Lock接口
  2. 锁的分类
  3. 乐观锁和悲观锁
  4. 可重入锁和非可重入锁,已ReentrantLock为例(重点)
  5. 公平锁和非公平锁
  6. 共享锁和排它锁:以ReentrantReadWriteLock读写锁为(重点)
  7. 自旋锁和阻塞锁
  8. 可中断锁:顾名思义,就是可以响应中断的锁
  9. 锁优化

Lock简介、地位、作用

  • 锁是一种工具,用于控制对共享资源的访问。
  • Lock和synchronized,这两个是最常见的锁,它们都可以达到线
    程安全的目的,但是在使用上和功能上又有较大的不同。
  • Lock并不是用来代替synchronized的,而是当使用
    synchronized不合适或不足以满足要求的时候,来提供高级功能
    的。
  • Lock接口最常见的实现类是ReentrantLock
  • 通常情况下,Lock只允许一个线程来访问这个共享资源。不过
    有的时候,一些特殊的实现也可允许并发访问,比如
    ReadWriteLock里面的ReadLock。

为什么需要Lock?

为什么synchronized不够用?

1) 效率低:锁的释放情况少、试图获得锁时不能设定超时、不能
中断一个正在试图获得锁的线程
2) 不够灵活(读写锁更灵活):加锁和释放的时机单一,每个锁
仅有单一的条件(某个对象),可能是不够的
3) 无法知道是否成功获取到锁

Lock主要方法介绍

在Lock中声明了四个方法来获取锁:
lock()、 tryLock()、 tryLock(long time, TimeUnit unit)和
lockinterruptibly()

  1. lock()方法
  • lock()就是最普通的获取锁。如果锁已被其他线程获取,则进行等待
  • Lock不会像synchronized一样在异常时自动释放锁
  • 因此最佳实践是,在finally中释放锁,以保证发生异常时锁一定被释放
  • lock()方法不能被中断,这会带来很大的隐患:一旦陷入死锁,
    lock()就会陷入永久等待

使用案例:

import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

/**
 * 描述:     Lock不会像synchronized一样,异常的时候自动释放锁,所以最佳实践是,finally中释放锁,以便保证发生异常的时候锁一定被释放
 */
public class MustUnlock {

    private static Lock lock = new ReentrantLock();

    public static void main(String[] args) {
        lock.lock();
        try{
            //获取本锁保护的资源
            System.out.println(Thread.currentThread().getName()+"开始执行任务");
        }finally {
            lock.unlock();
        }
    }
}
  1. tryLock()
  • tryLock()用来尝试获取锁,如果当前锁没有被其他线程占用
    则获取成功,则返回true,否则返回false,代表获取锁失败
  • 相比于lock,这样的方法显然功能更强大了,我们可以根据
    是否能获取到锁来决定后续程序的行为
  • 该方法会立即返回,即便在拿不到锁时不会一直在那等

tryLock() 避免死锁案例:

package lock.lock;

import java.util.Random;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

/**
 * 描述:     用tryLock来避免死锁
 */
public class TryLockDeadlock implements Runnable {

    int flag = 1;
    static Lock lock1 = new ReentrantLock();
    static Lock lock2 = new ReentrantLock();

    public static void main(String[] args) {
        TryLockDeadlock r1 = new TryLockDeadlock();
        TryLockDeadlock r2 = new TryLockDeadlock();
        r1.flag = 1;
        r1.flag = 0;
        new Thread(r1).start();
        new Thread(r2).start();

    }

    @Override
    public void run() {
        for (int i = 0; i < 100; i++) {
            if (flag == 1) {
                try {
                    if (lock1.tryLock(800, TimeUnit.MILLISECONDS)) {
                        try {
                            System.out.println("线程1获取到了锁1");
                            Thread.sleep(new Random().nextInt(1000));
                            if (lock2.tryLock(800, TimeUnit.MILLISECONDS)) {
                                try {
                                    System.out.println("线程1获取到了锁2");
                                    System.out.println("线程1成功获取到了两把锁");
                                    break;
                                } finally {
                                    lock2.unlock();
                                }
                            } else {
                                System.out.println("线程1获取锁2失败,已重试");
                            }
                        } finally {
                            lock1.unlock();
                            Thread.sleep(new Random().nextInt(1000));
                        }
                    } else {
                        System.out.println("线程1获取锁1失败,已重试");
                    }
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }

            if (flag == 0) {
                try {
                    if (lock2.tryLock(3000, TimeUnit.MILLISECONDS)) {
                        try {
                            System.out.println("线程2获取到了锁2");
                            Thread.sleep(new Random().nextInt(1000));
                            if (lock1.tryLock(800, TimeUnit.MILLISECONDS)) {
                                try {
                                    System.out.println("线程2获取到了锁1");
                                    System.out.println("线程2成功获取到了两把锁");
                                    break;
                                } finally {
                                    lock1.unlock();
                                }
                            } else {
                                System.out.println("线程2获取锁1失败,已重试");
                            }
                        } finally {
                            lock2.unlock();
                            Thread.sleep(new Random().nextInt(1000));
                        }
                    } else {
                        System.out.println("线程2获取锁2失败,已重试");
                    }
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}
  1. tryLock(long time, TimeUnit unit)
  • 获取锁,超时就放弃
  • lockInterruptibly():相当于tryLock(long time, TimeUnit unit)把超时时间设置为无限。在等待锁的过程中,线程可以被中断

线程中断,获取锁和释放锁案例:

package lock.lock;

import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

/**
 * 描述:     TODO
 */
public class LockInterruptibly implements Runnable {

    private Lock lock = new ReentrantLock();
public static void main(String[] args) {
    LockInterruptibly lockInterruptibly = new LockInterruptibly();
    Thread thread0 = new Thread(lockInterruptibly);
    Thread thread1 = new Thread(lockInterruptibly);
    thread0.start();
    thread1.start();

    try {
        Thread.sleep(2000);
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
    thread0.interrupt(); // 中断线程执行
}
    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName() + "尝试获取锁");
        try {
            lock.lockInterruptibly();
            try {
                System.out.println(Thread.currentThread().getName() + "获取到了锁");
                Thread.sleep(5000);
            } catch (InterruptedException e) {
                System.out.println(Thread.currentThread().getName() + "睡眠期间被中断了");
            } finally {
                lock.unlock();
                System.out.println(Thread.currentThread().getName() + "释放了锁");
            }
        } catch (InterruptedException e) {
            System.out.println(Thread.currentThread().getName() + "获得锁期间被中断了");
        }
    }
}
  1. 可见性保证

Lock的加解锁和synchronized有同样的内存语义,也就是说
下一个线程加锁后可以看到所有前一个线程解锁前发生的所有
操作

synchronized可见性示例:
JUC并发编程(三)——锁-Nickの博客

Lock可见性示例:
JUC并发编程(三)——锁-Nickの博客

锁的分类

注意:锁的分类,是从各种不同角度出发去看的,这些分类并不是互斥的。也就是多个类型可以并存:有可能一个锁,同时属于两种类型。比如ReentrantLock既是互斥锁,又是可重入锁

JUC并发编程(三)——锁-Nickの博客

乐观锁和悲观锁

乐观锁:非互斥同步锁
悲观锁:互斥同步锁

为什么会诞生非互斥同步锁?
互斥同步锁的劣势

  1. 阻塞和唤醒带来的性能劣势
  2. 永久阻塞:如果持有锁的线程被永久阻塞,比如遇到了无
    限循环、死锁等活跃性问题,那么等待该线程释放锁的
    那几个悲催的线程,将永远也得不到执行
  3. 优先级反转

什么是悲观锁?

  • 如果我不锁住这个资源,别人就会来争抢,就会造成数据结果
    错误,所以每次悲观锁为了确保结果的正确性,会在每次获取
    并修改数据时,把数据锁住,让别人无法访问该数据,这样就
    可以确保数据内容万无一失
  • Java中悲观锁的实现就是synchronized和Lock相关类

什么是乐观锁?

  • 认为自己在处理操作的时候不会有其他线程来干扰,所以并不会锁
    住被操作对象
  • 在更新的时候,去对比在我修改的期间数据有没有被其他人改变过:
    如果没被改变过,就说明真的是只有我自己在操作,那我就正常去
    修改数据
  • 如果数据和我一开始拿到的不一样了,说明其他人在这段时间内改
    过数据,那我就不能继续刚才的更新数据过程了,我会选择放弃、
    报错、重试等策略
  • 乐观锁的实现一般都是利用CAS算法来实现的
  • 乐观锁的典型例子就是原子类,并发容器等

使用场景:

悲观锁:适合并发写入多的情况,适用于临界区持锁时间比较长的情况,悲观锁可以避免大量的无用自旋等消耗,典型情况:
1 临界区有IO操作
2 临界区代码复杂或者循环量大
3 临界区竞争非常激烈
乐观锁:适合并发写入少,大部分是读取的场景,不加锁的能让读取性能大幅提高。

开销对比:

  • 悲观锁的原始开销要高于乐观锁,但是特点是一劳永逸,临
    界区持锁时间就算越来越差,也不会对互斥锁的开销造成影
  • 相反,虽然乐观锁一开始的开销比悲观锁小,但是如果自旋
    时间很长或者不停重试,那么消耗的资源也会越来越多。

典型例子:
乐观锁:
原子类,并发容器等;
Git:Git就是乐观锁的典型例子,当我们往远端仓库push的时候,git会检查远端仓库的版本是不是领先于我们现在的版本,如果远程仓库的版本号和本地的不一样,就表示有其他人修改了远端代码了,我们的这次提交就失败;如果远端和本地版本号一致,我们就可以顺利提交版本到远端仓库

悲观锁:synchronized和Lock相关类

可重入锁和非可重入锁,ReentrantLock

什么是可重入和非可重入?
可重入:一把锁可以多次被拿到
非可重入:一把锁不能同时被两个线程拿到,必须等第一个拿到锁的线程释放锁了才能被第二个线程拿到。

  1. 案例一:预定电影票座位

目标效果:两个线程无法同时预定座位

<code class="language-Java">package lock.reentrantlock;

import java.util.concurrent.locks.ReentrantLock;

/**
 * 描述:     演示多线程预定电影院座位
 */
public class CinemaBookSeat {

    private static ReentrantLock lock = new ReentrantLock();

    private static void bookSeat() {
        lock.lock();
        try {
            System.out.println(Thread.currentThread().getName() + "开始预定座位");
            Thread.sleep(1000);
            System.out.println(Thread.currentThread().getName() + "完成预定座位");
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }

    public static void main(String[] args) {
        new Thread(() -> bookSeat()).start();
        new Thread(() -> bookSeat()).start();
        new Thread(() -> bookSeat()).start();
        new Thread(() -> bookSeat()).start();
    }
}

ReentrantLock的方法介绍

  1. getHoldCount()锁已经被重入的数量
  2. isHeldByCurrentThread()可以看出锁是否被当前线程持有
  3. getQueueLength()可以返回当前正在等待这把锁的队列有多长,一般这两个方法是开发和调试时候使用,上线后用到的不多

可重入锁和不可重入锁源码对比:
JUC并发编程(三)——锁-Nickの博客

公平锁和非公平锁

  • 公平指的是按照线程请求的顺序,来分配锁;非公平指的是,
    不完全按照请求的顺序,在一定情况下,可以插队。
  • 注意:非公平也同样不提倡“插队〞行为,这里的非公平,指
    的是"在合适的时机〞插队,而不是盲目插队。

什么是合适的时机呢?
同一个线程重入的时候!!!
是为了提高效率,避免唤醒带来的空档期

优缺点对比:
JUC并发编程(三)——锁-Nickの博客

代码案例:
获取公平锁:
Lock queueLock = new ReentrantLock(true);
获取不公平锁:
Lock queueLock = new ReentrantLock(false);

new ReentrantLock()默认是非公平锁

演示公平和不公平两种情况

package lock.reentrantlock;

import java.util.Random;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

/**
 * 描述:     演示公平和不公平两种情况
 */
public class FairLock {

    public static void main(String[] args) {
        PrintQueue printQueue = new PrintQueue();
        Thread thread[] = new Thread[10];
        for (int i = 0; i < 10; i++) {
            thread[i] = new Thread(new Job(printQueue));
        }
        for (int i = 0; i < 10; i++) {
            thread[i].start();
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

class Job implements Runnable {

    PrintQueue printQueue;

    public Job(PrintQueue printQueue) {
        this.printQueue = printQueue;
    }

    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName() + "开始打印");
        printQueue.printJob(new Object());
        System.out.println(Thread.currentThread().getName() + "打印完毕");
    }
}

class PrintQueue {

    private Lock queueLock = new ReentrantLock(true);

    public void printJob(Object document) {
        queueLock.lock();
        try {
            int duration = new Random().nextInt(10) + 1;
            System.out.println(Thread.currentThread().getName() + "正在打印,需要" + duration);
            Thread.sleep(duration * 1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            queueLock.unlock();
        }

        queueLock.lock();
        try {
            int duration = new Random().nextInt(10) + 1;
            System.out.println(Thread.currentThread().getName() + "正在打印,需要" + duration+"秒");
            Thread.sleep(duration * 1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            queueLock.unlock();
        }
    }
}

执行结果:
公平锁的时候:所有线程顺序拿到锁,顺序执行。
JUC并发编程(三)——锁-Nickの博客

非公平锁的时候:第二次打印的时候,并没有释放锁,两次打印一起执行了,不需要重新获取锁
JUC并发编程(三)——锁-Nickの博客

特例

  • 针对tryLock()方法,它是很猛的,它不遵守设定的公平的规则
  • 例如,当有线程执行tryLock()的时候,一旦有线程释放了锁,那
    么这个正在tryLock的线程就能获取到锁,即使在它之前已经有其
    他现在在等待队列里了

共享锁和排它锁

  • 排他锁,又称为独占锁、独享锁
  • 共享锁,又称为读锁,获得共享锁之后,可以查看但无法修改和删
    除数据,其他线程此时也可以获取到共享锁,也可以查看但无法修
    改和删除数据
  • 共享锁和排它锁的典型是读写锁ReentrantReadWriteLock,其中
    读锁是共享锁,写锁是独享锁

读写锁的作用

  • 在没有读写锁之前,我们假设使用ReentrantLock,那么虽然我
    们保证了线程安全,但是也浪费了一定的资源:多个读操作同时
    进行,并没有线程安全问题
  • 在读的地方使用读锁,在写的地方使用写锁,灵活控制,如果没
    有写锁的情况下,读是无阻塞的,提高了程序的执行效率

读写锁的规则

a) 多个线程只申请读锁,都可以申请到

b) 如果有一个线程已经占用了读锁,则此时其他线程如果要申请写
锁,则申请写锁的线程会一直等待释放读锁。

c) 如果有一个线程已经占用了写锁,则此时其他线程如果申请写锁或者读锁,则申请的线程会一直等待释放写锁。

d) 一句话总结:要么是一个或多个线程同时有读锁,要么是一个线
程有写锁,但是两者不会同时出现(要么多读,要多一写)

1. 换一种思路更容易理解:读写锁只是一把锁,可以通过两种方式锁定读锁定和写锁定。读写锁可以同时被一个或多个线程读锁定,也可以被单一线程写锁定。但是永远不能同时对这把锁进行读锁定和写锁定。
2. 这里是把"获取写锁〞理解为“把读写锁进行写锁定〞,相当于是换了一种思路,不过原则是不变的,就是要么是一个或多个线程同时有读锁(同时读锁定),要么是一个线程有写锁(进行写锁定),但是两者不会同时出现

代码案例:
线程1和2为读锁,3和4为写锁

package lock.readwrite;

import java.util.concurrent.locks.ReentrantReadWriteLock;

/**
 * 描述:     TODO
 */
public class CinemaReadWrite {

    private static ReentrantReadWriteLock reentrantReadWriteLock = new ReentrantReadWriteLock();
    private static ReentrantReadWriteLock.ReadLock readLock = reentrantReadWriteLock.readLock();
    private static ReentrantReadWriteLock.WriteLock writeLock = reentrantReadWriteLock.writeLock();

    private static void read() {
        readLock.lock();
        try {
            System.out.println(Thread.currentThread().getName() + "得到了读锁,正在读取");
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            System.out.println(Thread.currentThread().getName() + "释放读锁");
            readLock.unlock();
        }
    }

    private static void write() {
        writeLock.lock();
        try {
            System.out.println(Thread.currentThread().getName() + "得到了写锁,正在写入");
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            System.out.println(Thread.currentThread().getName() + "释放写锁");
            writeLock.unlock();
        }
    }

    public static void main(String[] args) {
        new Thread(()->read(),"Thread1").start();
        new Thread(()->read(),"Thread2").start();
        new Thread(()->write(),"Thread3").start();
        new Thread(()->write(),"Thread4").start();
    }
}

执行结果:
JUC并发编程(三)——锁-Nickの博客

读锁共享,可以同时进行; 写锁排他,独立进行。

读锁插队策略
公平锁:不允许插队

非公平锁

  1. 写锁可以随时插队(和非可重入锁类似,上来就插队,拿不到锁再排队)
  2. 读锁仅在等待队列头结点不是想获取写锁的线程的时候
    可以插队

验证非公平和公平的ReentrantReadWriteLock的策略

package lock.readwrite;

import java.util.concurrent.locks.ReentrantReadWriteLock;

/**
 * 描述:     演示非公平和公平的ReentrantReadWriteLock的策略
 */
public class NonfairBargeDemo {

    private static ReentrantReadWriteLock reentrantReadWriteLock = new ReentrantReadWriteLock(
            );

    private static ReentrantReadWriteLock.ReadLock readLock = reentrantReadWriteLock.readLock();
    private static ReentrantReadWriteLock.WriteLock writeLock = reentrantReadWriteLock.writeLock();

    private static void read() {
        System.out.println(Thread.currentThread().getName() + "开始尝试获取读锁");
        readLock.lock();
        try {
            System.out.println(Thread.currentThread().getName() + "得到读锁,正在读取");
            try {
                Thread.sleep(20);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        } finally {
            System.out.println(Thread.currentThread().getName() + "释放读锁");
            readLock.unlock();
        }
    }

    private static void write() {
        System.out.println(Thread.currentThread().getName() + "开始尝试获取写锁");
        writeLock.lock();
        try {
            System.out.println(Thread.currentThread().getName() + "得到写锁,正在写入");
            try {
                Thread.sleep(40);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        } finally {
            System.out.println(Thread.currentThread().getName() + "释放写锁");
            writeLock.unlock();
        }
    }

    public static void main(String[] args) {
        new Thread(()->write(),"Thread1").start();
        new Thread(()->read(),"Thread2").start();
        new Thread(()->read(),"Thread3").start();
        new Thread(()->write(),"Thread4").start();
        new Thread(()->read(),"Thread5").start();
        new Thread(new Runnable() {
            @Override
            public void run() {
                Thread thread[] = new Thread[1000];
                for (int i = 0; i < 1000; i++) {
                    thread[i] = new Thread(() -> read(), "子线程创建的Thread" + i);
                }
                for (int i = 0; i < 1000; i++) {
                    thread[i].start();
                }
            }
        }).start();
    }
}

非公平锁时,子线程的读锁尝试获取锁时,会在第一个写锁释放后插队JUC并发编程(三)——锁-Nickの博客

而Thread2,Thread3释放读锁后,队列轮到Thread4得到写锁,后续子线程的写锁无法再插队
JUC并发编程(三)——锁-Nickの博客

适用场合:相比于ReentrantLock适用于一般场合,
ReentrantReadWriteLock适用于读多写少的情况,合理使用可以进
一步提高并发效率。

锁的升降级

为什么需要升降级?
(在拿到写锁写入后,需要读,但是不需要写了的场景,避免浪费资源和重新排队拿读锁,从写锁降级到读锁)

支持锁的降级,不支持升级

代码演示

package lock.readwrite;

import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.locks.ReentrantReadWriteLock;

/**
 * 描述:     演示ReentrantReadWriteLock可以降级,不能升级
 */
public class Upgrading {

    private static ReentrantReadWriteLock reentrantReadWriteLock = new ReentrantReadWriteLock(
            false);
    private static ReentrantReadWriteLock.ReadLock readLock = reentrantReadWriteLock.readLock();
    private static ReentrantReadWriteLock.WriteLock writeLock = reentrantReadWriteLock.writeLock();

    private static void readUpgrading() {
        readLock.lock();
        try {
            System.out.println(Thread.currentThread().getName() + "得到了读锁,正在读取");
            Thread.sleep(1000);
            System.out.println("升级会带来阻塞");
            writeLock.lock();
            System.out.println(Thread.currentThread().getName() + "获取到了写锁,升级成功");
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            System.out.println(Thread.currentThread().getName() + "释放读锁");
            readLock.unlock();
        }
    }

    private static void writeDowngrading() {
        writeLock.lock();
        try {
            System.out.println(Thread.currentThread().getName() + "得到了写锁,正在写入");
            Thread.sleep(1000);
            readLock.lock();
            System.out.println("在不释放写锁的情况下,直接获取读锁,成功降级");
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            readLock.unlock();
            System.out.println(Thread.currentThread().getName() + "释放写锁");
            writeLock.unlock();
        }
    }

    public static void main(String[] args) throws InterruptedException {
        System.out.println("先演示降级是可以的");
        Thread thread1 = new Thread(() -> writeDowngrading(), "Thread1");
        thread1.start();
        thread1.join();
        System.out.println("------------------");
        System.out.println("演示升级是不行的");
        Thread thread2 = new Thread(() -> readUpgrading(), "Thread2");
        thread2.start();
    }
}

运行结果:
JUC并发编程(三)——锁-Nickの博客

没有打印:获取到了写锁,升级成功
说明readLock.lock()后没有释放,直接writeLock.lock()被阻塞了。
JUC并发编程(三)——锁-Nickの博客

为什么不支持锁的升级?
如果同时存在两个读锁要升级写锁,则容易死锁。

自旋锁和阻塞锁

自旋锁的来源与概述:
阻塞或唤醒一个Java线程需要操作系统切换CPU状态来完成,这种状态转换需要耗费处理器时间

如果同步代码块中的内容过于简单,状态转换消耗的时间有可能比用户代码执行的时间还要长

在许多场景中,同步资源的锁定时间很短,为了这一小段时间去切换线程,线程挂起和恢复现场的花费可能会让系统得不偿失

如果物理机器有多个处理器,能够让两个或以上的线程同时并行执行,我们就可以让后面那个请求锁的线程不放弃CPU的执行时间,看看持有锁的线程是否很快就会释放锁而为了让当前线程“稍等一下〞,我们需让当前线程进行自旋。

如果在自旋完成后前面锁定同步资源的线程己经释放了锁,那么当前线程就可以不必阻塞而是直接获取同步资源而避免切换线程的开销。这就是自旋锁。

阻塞锁和自旋锁相反,阻塞锁如果遇到没拿到锁的情况,会直接
把线程阻塞,直到被唤醒

自旋锁的缺点;
如果锁被占用的时间很长,那么自旋的线程只会白浪费处理器资
源,在自旋的过程中,一直消耗cpu,所以虽然自旋锁的起始开销低
于悲观锁,但是随着自旋时间的增长,开销也是线性增长的

原理和源码分析;
在Java1.5版本及以上的并发框架java.util.concurrent 的atmoic
包下的类基本都是自旋锁的实现
AtomicInteger的实现:自旋锁的实现原理是CAS
Atomicinteger中调用unsafe进行自增操作的源码中的do-while
循环就是一个自旋操作,如果修改过程中遇到其他线程竞争导致
没修改成功,就在while里死循环,直至修改成功
源码如图:
JUC并发编程(三)——锁-Nickの博客

自己实现简单的自旋锁:

package lock.spinlock;

import java.util.concurrent.atomic.AtomicReference;

/**
 * 描述:     自旋锁
 */
public class SpinLock {

    private AtomicReference<Thread> sign = new AtomicReference<>();

    public void lock() {
        Thread current = Thread.currentThread();
        while (!sign.compareAndSet(null, current)) {
            System.out.println("自旋获取失败,再次尝试");
        }
    }

    public void unlock() {
        Thread current = Thread.currentThread();
        sign.compareAndSet(current, null);
    }

    public static void main(String[] args) {
        SpinLock spinLock = new SpinLock();
        Runnable runnable = new Runnable() {
            @Override
            public void run() {
                System.out.println(Thread.currentThread().getName() + "开始尝试获取自旋锁");
                spinLock.lock();
                System.out.println(Thread.currentThread().getName() + "获取到了自旋锁");
                try {
                    Thread.sleep(300);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } finally {
                    spinLock.unlock();
                    System.out.println(Thread.currentThread().getName() + "释放了自旋锁");
                }
            }
        };
        Thread thread1 = new Thread(runnable);
        Thread thread2 = new Thread(runnable);
        thread1.start();
        thread2.start();
    }
}

执行结果:
thread0获取锁后,thread1不断自旋尝试,直到thread0锁释放
JUC并发编程(三)——锁-Nickの博客

自旋锁的适用场景:
自旋锁一般用于多核的服务器,在并发度不是特别高的情况下,比阻塞锁的效率高
另外,自旋锁适用于临界区比较短小的情况,否则如果临界区很大(线程一旦拿到锁,很久以后才会释放),那也是不合适的

可中断锁

在Java中,synchronized就不是可中断锁,而Lock是可中断锁,
因为tryLock(time)和locklnterruptibly都能响应中断。

如果某一线程A正在执行锁中的代码,另一线程B正在等待获取该锁,
可能由于等待时间过长,线程B不想等待了,想先处理其他事情,
我们可以中断它,这种就是可中断锁

代码演示:

package lock.lock;

import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

/**
 * 描述:     TODO
 */
public class LockInterruptibly implements Runnable {

    private Lock lock = new ReentrantLock();
public static void main(String[] args) {
    LockInterruptibly lockInterruptibly = new LockInterruptibly();
    Thread thread0 = new Thread(lockInterruptibly);
    Thread thread1 = new Thread(lockInterruptibly);
    thread0.start();
    thread1.start();

    try {
        Thread.sleep(2000);
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
    thread0.interrupt(); // 中断线程执行
}
    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName() + "尝试获取锁");
        try {
            lock.lockInterruptibly();
            try {
                System.out.println(Thread.currentThread().getName() + "获取到了锁");
                Thread.sleep(5000);
            } catch (InterruptedException e) {
                System.out.println(Thread.currentThread().getName() + "睡眠期间被中断了");
            } finally {
                lock.unlock();
                System.out.println(Thread.currentThread().getName() + "释放了锁");
            }
        } catch (InterruptedException e) {
            System.out.println(Thread.currentThread().getName() + "获得锁期间被中断了");
        }
    }
}

运行结果
JUC并发编程(三)——锁-Nickの博客

锁优化

Java虛拟机对锁的优化

自旋锁和自适应
锁消除
锁粗化

在写代码时如何优化锁和提高并发性能

  1. 缩小同步代码块
  2. 尽量不要锁住方法
  3. 减少请求锁的次数
  4. 避免人为制造"热点"(Map全局遍历等)
  5. 锁中尽量不要再包含锁
  6. 选择合适的锁类型或合适的工具类
0 条回应
在线人数:1人 来访统计
新时代
Ado
隐藏