浅谈一下锁

1.Lock接口

1.1简介、地位、作用

◆锁是一种工具,用于控制对共享资源的访问。

◆Lock和 synchronized,这两个是最常见的锁,它们都可以达到线程安全的目的,但是在使用上和功能上又有较大的不同

◆Lock并不是用来代替 synchronized的,而是当使用synchronized不合适或不足以满足要求的时候,来提供高级功能的

◆Lock接口最常见的实现类是 Reentrantlock

◆通常情况下,Lock只允许—个线程来访问这个共享资源。不过有的时候,一些特殊的实现也可允许并发访问,比如ReadWritelock里面的 Readlock。

1.2为什么 synchronzed不够用?为什么需要Lock?

1)效率低:锁的释放情况少、试图获得锁时不能没定超时、不能中断一个正在试图获得锁的线程

2)不够灵活(读写锁更灵活):加锁和释放的时机单一,每个锁仅有单一的条件(某个对象),可能是不够的

3)无法知道是否成功获取到锁

1.3方法介绍

在Lock中声明了四个方法来获取锁

  • lock()

◆lock就是最普通的获取锁。如果锁已被其他线程获取,则进行等待

◆Lock不会像 synchronized-样在异常时自动释放锁

◆因此最佳实践是,在 finally中释放锁,以保证发生异常时锁一定被释放

◆lock()方法不能被中断,这会带来很大的隐患:一旦陷入死锁lock()就会陷入永久等待

代码:

private static Lock lock = new ReentrantLock();
public static void main(String[] args) {
    lock.lock();
    try {
        //业务代码
    } finally {
        lock.unlock();
    }
}
  • tryLock(): 尝试获得锁,如果锁空闲就能够获得,否则直接返回FALSE

    /**
     * Acquires the lock only if it is free at the time of invocation.
     *
     * <p>Acquires the lock if it is available and returns immediately
     * with the value {@code true}.
     * If the lock is not available then this method will return
     * immediately with the value {@code false}.
     *
     * <p>A typical usage idiom for this method would be:
     * <pre> {@code
     * Lock lock = ...;
     * if (lock.tryLock()) {
     *   try {
     *     // manipulate protected state
     *   } finally {
     *     lock.unlock();
     *   }
     * } else {
     *   // perform alternative actions
     * }}</pre>
     *
     * This usage ensures that the lock is unlocked if it was acquired, and
     * doesn't try to unlock if the lock was not acquired.
     *
     * @return {@code true} if the lock was acquired and
     *         {@code false} otherwise
     */
    boolean tryLock();
    
  • tryLock( long time,TimeUnit unit):超时就放弃。

public class TryLockDeadLock implements Runnable{

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

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

    public static void main(String[] args) {
        TryLockDeadLock tryLockDeadLock1 = new TryLockDeadLock();
        TryLockDeadLock tryLockDeadLock2 = new TryLockDeadLock();
        tryLockDeadLock1.flag = 1;
        tryLockDeadLock2.flag = 0;
        new Thread(tryLockDeadLock1).start();
        new Thread(tryLockDeadLock2).start();
    }
}
  • lockinterruptibly(): 相当于 tryLock(long time, TimeUnit unit)把超时时间设置为无限。在等待锁的过程中,线程可以被中断。

    public class LockInterruptibly implements Runnable{
    
        private Lock lock = new ReentrantLock();
        @Override
        public void run() {
            System.out.println(Thread.currentThread().getName() + "尝试获取锁");
            try {
                lock.lockInterruptibly();
                try {
                    System.out.println(Thread.currentThread().getName() + "拿到了锁");
                    TimeUnit.SECONDS.sleep(5);
                } 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() + "获得锁期间被中断");
            }
        }
    
        public static void main(String[] args) throws InterruptedException {
            LockInterruptibly lockInterruptibly = new LockInterruptibly();
            Thread thread1 = new Thread(lockInterruptibly);
            Thread thread2 = new Thread(lockInterruptibly);
            thread1.start();
            thread2.start();
            TimeUnit.SECONDS.sleep(2);
    //        thread1.interrupt();
            thread2.interrupt();
        }
    }
    
  • unlock(): 释放锁,在使用的时候先try{}finally{lock.unlock();}确保释放锁

1.4可见性保证

2.锁的分类

线性要不要锁住同步资源
锁住
悲观锁
不锁住
乐观锁

多线程能否共享一把锁
可以
共享锁
不可以
独占锁

多线程竞争时,是否排队
排队
公平锁
先尝试插队,插队失败再排队
非公平锁

同一个线程是否可以重复获取同一把锁
可以
可重入
不可以
不可重入锁

是否可以中断
可以
可中断锁
不可以
非可重入锁

等待锁的过程
自旋
自旋锁
阻塞
非自旋锁

3.乐观锁和悲观锁

1.为什么会诞生非互斥同步锁: 互斥同步锁的劣势

​ ◆阻塞和唤醒带来的性能劣势

​ ◆永久阻塞:如果持有锁的线程被永久阻塞,比如遇到了无限循环、死锁等活跃性问题,那么等待该线程释放锁的那几个悲催的线程,将 永远也得不到执行

​ ◆优先级反转😥 低优先级的拿到了锁,如果一直不释放,那么当等待优先级比较高的想拿的时候却无法拿到

2.什么是乐观锁和悲观锁

乐观锁:

​ ◆认为自己在处理操作的时候不会有其他线程来干扰,所以并不会锁住被操作对象

​ ◆在更新的时候,去对比在我修改的期间数据有没有被其他人改变过,如果没被改变过,就说明真的是只有我自己在操作,那我就正常去修改数据

​ ◆如果数据和我开始拿到的不一样了,说明其他人在这段时间内改过数据,那我就不能继续刚才的更新数据过程了,我会选择放弃、报错、重试等策略

​ ◆乐观锁的实现一般都是利用CAS算法来实现的

悲观锁:

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

​ ◆Java中悲观锁的实现就是 synchronizedLock相关类

3.典型例子

​ ◆悲观锁: synchronized和lock接口

​ ◆乐观锁的典型例子就是原子类、并发容器等

​ ◆代码演示

public class PessimismOptimismLock {
    public static void main(String[] args) {
        //乐观锁
        AtomicInteger a = new AtomicInteger();
        a.incrementAndGet();
    }
    //悲观锁
    int a;
    public synchronized void test() {
        a++;
    }

}

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

​ ◆数据库

​ ◆ select for update就是悲观锁

​ ◆用 version控制数据库就是乐观锁

4.开销对比

​ ◆悲观锁的原始开销要高于乐观锁,但是特点是一劳永逸,临界区持锁时间就算越来越差,也不会对互斥锁的开销造成影响

​ ◆相反,虽然乐观锁—开始的开销比悲观锁小,但是如果自旋时间很长或者不停重试,那么消耗的资源也会越来越多

5.两种锁各自的使用场景

​ ◆悲观锁:适合并发写入多的情况,适用于临界区持锁时间比较长的情况,悲观锁可以避免大量的无用自旋等消耗,典型情况

​ 1.临界区有IO操作

​ 2.临界区代码复杂或者循环量大

​ 3.临界区竞争非常激烈

​ ◆乐观锁:适合并发写入少,大部分是读取的场景,不加锁的能让读取性能大幅提高。

4.可重入锁和非重入锁,以ReentrantLock为例

什么是可重入锁呢?

当一个线程获取了某个对象锁以后,还可以再次获得该对象锁。

public class Demo1 {
    public synchronized void functionA(){
        System.out.println("iAmFunctionA");
        functionB();
    }
    public synchronized void functionB(){
        System.out.println("iAmFunctionB");
    }
    private static ReentrantLock lock = new ReentrantLock();
    public void f1() {
        lock.lock();
        try {
            f2();
        } finally {
            lock.unlock();
        }
    }
    public void f2() {
        lock.lock();
        try {
        } finally {
            lock.unlock();
        }
    }
}

functionA()functionB()都是同步方法,当线程进入funcitonA()会获得该类的对象锁,这个锁"new Demo1()",在functionA()对方法functionB()做了调用,但是functionB()也是同步的,因此该线程需要再次获得该对象锁(new Demo1()),其他线程是无法获该对象锁的。

这就是可重入锁。可重入锁的作用就是为了避免死锁,java中synchronizedReentrantLock都是可重入锁。

重入锁的实现原理

通过为每个锁关联一个请求计数器和一个获得该锁的线程。当计数器为0时,认为锁是未被占用的。线程请求一个未被占用的锁时,JVM将记录该线程并将请求计数器设置为1,此时该线程就获得了锁,当该线程再次请求这个锁,计数器将递增,当线程退出同步方法或者同步代码块时,计数器将递减,当计数器为0时,线程就释放了该对象,其他线程才能获取该锁

ReentrantLock源码:

final boolean nonfairTryAcquire(int acquires) {
    final Thread current = Thread.currentThread();
    int c = getState();
    if (c == 0) {
        if (compareAndSetState(0, acquires)) {
            setExclusiveOwnerThread(current); 
            return true;
        }
    }
    else if (current == getExclusiveOwnerThread()) {
        int nextc = c + acquires;
        if (nextc < 0) // overflow
            throw new Error("Maximum lock count exceeded");
        setState(nextc);
        return true;
    }
    return false;
}

5.公平锁和非公平锁

公平指的是按照线程请求的顺序,来分配锁;非公平指的是不完全按照请求的顺序,在一定情况下,可以插队。

​ 注意:非公平也同样不提倡“插队”行为,这里的非公平,指的是“在合适的时机”插队,而不是盲目插队。

优势劣势
公平锁各线程公平平等,每个线程在等待一段时间后,总有执行的机会更慢,吞吐量更小
非公平锁更快,吞吐量更大有可能产生线程饥饿,也就是某些线程在长时间内始终得不到执行

代码演示公平锁和非公平锁的效果:

下面的代码使用的是ReentrantLock,默认是非公平锁,效果是 printJob(Object document)中的会连续打印两次,因为非公平锁,当第一次锁释放后,然后第二次会直接插队,因为其他线程还在等待,唤醒需要时间,为了减少空档期,提高速度,会直接插队拿到锁,连续打印;设置new ReentrantLock(true), 这时是公平锁,那么会按照排队顺序抢锁,所以printJob(Object document)中打印第一次后,不会接着打印第二次,因为这个时候再去抢锁会发现,前面还有很多人在排队。

public class FairLock {
    public static void main(String[] args) throws InterruptedException {
        PrintQueue printQueue = new PrintQueue();
        Thread[] threads = new Thread[10];
        for (int i = 0; i < 10; i++) {
            threads[i] = new Thread(new Job(printQueue));
        }
        for (int i = 0; i < 10; i++) {
            threads[i].start();
            Thread.sleep(100L);
        }
    }
}

class Job implements Runnable {

    private 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();

    public void printJob(Object document) {
        queueLock.lock();
        try {
            int duration = new Random().nextInt(10) + 1;
            System.out.println(Thread.currentThread().getName() + " 正在打印, 需要 " + duration);
            TimeUnit.SECONDS.sleep(duration);
        } 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);
            TimeUnit.SECONDS.sleep(duration);
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            queueLock.unlock();
        }
    }
}

公平锁源码:

@ReservedStackAccess
protected final boolean tryAcquire(int acquires) {
    final Thread current = Thread.currentThread();
    int c = getState();
    if (c == 0) {
        if (!hasQueuedPredecessors() &&
            compareAndSetState(0, acquires)) {
            setExclusiveOwnerThread(current);
            return true;
        }
    }
    else if (current == getExclusiveOwnerThread()) {
        int nextc = c + acquires;
        if (nextc < 0)
            throw new Error("Maximum lock count exceeded");
        setState(nextc);
        return true;
    }
    return false;
}

非公平锁:

@ReservedStackAccess
final boolean nonfairTryAcquire(int acquires) {
    final Thread current = Thread.currentThread();
    int c = getState();
    if (c == 0) {
        if (compareAndSetState(0, acquires)) {
            setExclusiveOwnerThread(current);
            return true;
        }
    }
    else if (current == getExclusiveOwnerThread()) {
        int nextc = c + acquires;
        if (nextc < 0) // overflow
            throw new Error("Maximum lock count exceeded");
        setState(nextc);
        return true;
    }
    return false;
}

可以看到,公平锁中会通过hasQueuedPredecessors()判断是否有人在队列等待。

6.共享锁和排它锁:以ReentranReadWriteLock读写锁为例

◆什么是共享锁和排它锁

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

◆读写锁的作用

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

◆读写锁的规则

  • ​ 多个线程只申请读锁,都可以申请到
  • ​ 如果有一个线程已经占用了读锁,则此时其他线程如果要申请写锁,则申请写锁的线程会一直等待释放读锁。
  • ​ 如果有一个线程已经占用了写锁,则此时其他线程如果申请写锁或者读锁,则申请的线程会一直等待释放写锁。
  • ​ —句话总结:要么是一个或多个线程同时有读锁,要么是一个线程有写锁,但是两者不会同时出现(要么多读,要多一写)

换—种思路更容易理解:读写锁只是一把锁,可以通过两种方式锁定读锁定和写锁定。读写锁可以同时被一个或多个线程读锁定,也可以被单—线程写锁定。但是永远不能同时对这把锁进行读锁定和写锁定。

这里是把“获取写锁”理解为“把读写锁进行写锁定”,相当于是换了一种思路,不过原则是不变的,就是要么是一个或多个线程同时有读锁(同时读锁定),要么是一个线程有写锁(进行写锁定),但是两者不会同时出现

ReentrantReadwritelock具体用法

public class CinemaReadWrite {
    private static ReentrantReadWriteLock reentrantReadWriteLock = new ReentrantReadWriteLock();
    private static ReentrantReadWriteLock.ReadLock readLock = reentrantReadWriteLock.readLock();
    private static ReentrantReadWriteLock.WriteLock writeLock = reentrantReadWriteLock.writeLock();

    public static void read() {
        readLock.lock();
        try {
            System.out.println(Thread.currentThread().getName() + "得到了读锁,正在读取");
            Thread.sleep(1000L);
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            System.out.println(Thread.currentThread().getName() + "释放了读锁");
            readLock.unlock();
        }
    }
    public static void write() {
        writeLock.lock();
        try {
            System.out.println(Thread.currentThread().getName() + "得到了写锁,正在写入");
            Thread.sleep(1000L);
        } 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();
    }
}

◆读锁和写锁的交互方式

选择规则
读线程插队(比喻:男女共用厕所,男生可以插队吗?

​ **策略一:**线程4正在同时读取,线程3想要写入,拿不到锁,于是进入等待队列,线程6不在队列里,现在过来想要读取。

  • ​ 读可以插队,效率高
  • 容易造成饥饿,就是等待的写线程一直等待,因为一直有读线程不断插队,读锁被一直持有

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-6C0nhYbl-1636097380233)(https://i.loli.net/2021/10/24/O4E1sPm9kpwFlNL.png)]

策略二:避免饥饿。策略的选择取决于具体锁的实现,ReentrantReadWriteLock实现是选择了策略二。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-mpQbm5P2-1636097380235)(https://i.loli.net/2021/10/24/jTv6hLfRgGcnlNs.png)]

◆公平和非公平源码

  1. ​ 公平锁:不允许插队

    static final class FairSync extends Sync {
        private static final long serialVersionUID = -2274990926593161451L;
        final boolean writerShouldBlock() {
            return hasQueuedPredecessors();
        }
        final boolean readerShouldBlock() {
            return hasQueuedPredecessors();
        }
    }
    
  2. ​ 非公平锁:

​ 1)写锁可以随时插队

​ 2)读锁仅在等待队列头不是想获取写锁的线程的时候可以插队,写锁线程在头不能插

static final class NonfairSync extends Sync {
    private static final long serialVersionUID = -8159625535654395037L;
    final boolean writerShouldBlock() {
        return false; // writers can always barge
    }
    final boolean readerShouldBlock() {
        /* As a heuristic to avoid indefinite writer starvation,
         * block if the thread that momentarily appears to be head
         * of queue, if one exists, is a waiting writer.  This is
         * only a probabilistic effect since a new reader will not
         * block if there is a waiting writer behind other enabled
         * readers that have not yet drained from the queue.
         */
        return apparentlyFirstQueuedIsExclusive();
    }
}

◆锁的升降级

​ 为什么需要升级

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

public class Upgrading {
    private static ReentrantReadWriteLock reentrantReadWriteLock = new ReentrantReadWriteLock(false);
    private static ReentrantReadWriteLock.ReadLock readLock = reentrantReadWriteLock.readLock();
    private static ReentrantReadWriteLock.WriteLock writeLock = reentrantReadWriteLock.writeLock();
    public static void read() {
        readLock.lock();
        try {
            System.out.println(Thread.currentThread().getName() + "得到了读锁,正在读取");
            writeLock.lock();
            System.out.println(Thread.currentThread().getName() + "得到了写锁,正在写入");
            Thread.sleep(1000L);
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            System.out.println(Thread.currentThread().getName() + "释放了读锁");
            System.out.println(Thread.currentThread().getName() + "释放了写锁");
            writeLock.unlock();
            readLock.unlock();
        }
    }
    public static void write() {
        writeLock.lock();
        try {
            System.out.println(Thread.currentThread().getName() + "得到了写锁,正在写入");
            readLock.lock();
            System.out.println(Thread.currentThread().getName() + "得到了读锁,正在读取");
            Thread.sleep(1000L);
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            System.out.println(Thread.currentThread().getName() + "释放了写锁");
            System.out.println(Thread.currentThread().getName() + "释放了读锁");
            readLock.unlock();
            writeLock.unlock();
        }
    }
    public static void main(String[] args) {
        new Thread(()->write()).start();
        new Thread(()->read()).start();
    }
}

为什么不支持锁的升级?死锁,假设存在AB线程,A线程升级,那么B线程必须释放读锁,同理,B升级,那么A线程必须释放读锁。

总结

  1. ReentrantReadWriteLock实现了ReadWriteLock接口,最主要的有两个方法:readLock()writeLock()用来获取读锁和写锁

  2. 锁申请和释放策略

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

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

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

      要么多读,要么一写

  3. 插队策略:为了防止饥饿,读锁不能插队(等待队列头是写线程时)

  4. 升降级策略:只能降级,不能升级

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

7.自旋锁和阻塞锁

◆阻塞或唤醒一个Java线程需要操作系统切换CPU状态来完成,这种状态转换需要耗费处理器时间

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

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

◆如果物理机器有多个处理器,能够让两个或以上的线程同时并行执行,我们就可以让后面那个请求锁的线程不放弃CPU的执行时间,看看持有锁的线程是否很快就会释放锁

◆而为了让当前线程“稍等一下”,我们需让当前线程进行自旋, 如果在自旋完成后前面锁定同步资源的线程已经释放了锁,那么当前线程就可以不必阻塞而是直接获取同步资源,从而避免切换线程的开销。这就是自旋锁

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

自旋锁的缺点

◆如果锁被占用的时间很长,那么自旋的线程只会白浪费处理器资源

◆在自旋的过程中,一直消耗cρu,所以虽然自旋锁的起始开销低于悲观锁,但是随着自旋时间的增长,开销也是线性增长的

源码分析

◆在java1.5版本及以上的并发框架java.util.concurrentatmoic包下的类基本都是自旋锁的实现

AtomicInteger的实现:自旋锁的实现原理是CASAtomicInteger中调用 unsafe进行自增操作的源码中的 do-while循环就是一个自旋操作,如果修改过程中遇到其他线程竞争导致没修改成功,就在 while里死循环,直至修改成功

public final int getAndIncrement() {
    return U.getAndAddInt(this, VALUE, 1);
}
@HotSpotIntrinsicCandidate
public final int getAndAddInt(Object o, long offset, int delta) {
    int v;
    do {
        v = getIntVolatile(o, offset);
    } while (!weakCompareAndSetInt(o, offset, v, v + delta));
    return v;
}

自定义自旋锁

public class SpinLock {
    private AtomicReference<Thread> sign = new AtomicReference<>();
    public void lock() {
        Thread current = Thread.currentThread();
        while (!sign.compareAndSet(null, current)) {

        }
    }
    public void unlock() {
        Thread current = Thread.currentThread();
        sign.compareAndSet(current, null);
    }
    public static void main(String[] args) {
        SpinLock spinLock = new SpinLock();
        Runnable runnable = () -> {
            System.out.println(Thread.currentThread().getName() + "开始尝试获取自旋锁");
            spinLock.lock();
            System.out.println(Thread.currentThread().getName() + "获取到了自旋锁");
            try {
                Thread.sleep(1000L);
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                System.out.println(Thread.currentThread().getName() + "释放自旋锁");
                spinLock.unlock();
            }
        };
        Thread t1 = new Thread(runnable);
        Thread t2 = new Thread(runnable);
        t1.start();
        t2.start();
    }
}

自旋锁的适用场景

◆自旋锁一般用于多核的服务器,在并发度不是特别高的情况下,比阻塞锁的效率高

◆另外,自旋锁适用于临界区比较短小的情况,否则如果临界区很大(线程一旦拿到锁,很久以后才会释放),那也是不合适的

8.可中断锁:顾名思义,就是可以响应中断的锁

◆在java中, synchronized就不是可中断锁,而Lock是可中断锁,因为 tryLock(time)lockInterruptibly都能响应中断。

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

9.锁优化

Java虚拟机对锁的优化

  1. 自旋锁和自适应
  2. 锁消除
  3. 锁粗化

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

  1. 缩小同步代码块
  2. 尽量不要锁住方法
  3. 减少请求锁的次数
  4. 避免人为制造“热点”
  5. 锁中尽量不要再包含锁
  6. 选择合适的锁类型或合适的工具类
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值