Java并发编程 锁

1.Lock接口

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

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

1.3 Lock主要方法介绍
在Lock中声明了四个方法来获取锁

lock()

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

tryLock()
  • tryLock()用来尝试获取锁,如果当前锁没有被其他线程占用。则获取成功,则返回true,否则返回false,代表获取锁失败
  • 相比于lock,这样的方式显然功能更强大了,我们可以根据是否能获取到锁决定后续程序的行为
tryLock(long time,TimeUnit unit)

超时就放弃

  • 该方法会立即返回,即便在拿不到锁时不会一直在那等
public class TryLockDeadLock implements Runnable {
    static Lock lock1 = new ReentrantLock();
    static Lock lock2 = new ReentrantLock();
    int flag = 1;

    public static void main(String[] args) {
        TryLockDeadLock tryLockDeadLock1 = new TryLockDeadLock();
        TryLockDeadLock tryLockDeadLock2 = new TryLockDeadLock();
        tryLockDeadLock1.flag = 1;
        tryLockDeadLock2.flag = 2;
        new Thread(tryLockDeadLock1).start();
        new Thread(tryLockDeadLock2).start();
    }

    @Override
    public void run() {
        for (int i = 0; i < 10; 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成功获取到了2把锁");
                                    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 == 2) {
                try {
                    if (lock2.tryLock(800, 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成功获取到了2把锁");
                                    break;
                                } finally {
                                    lock1.unlock();
                                }
                            } else {
                                System.out.println("线程2获取锁2失败,已重试");
                            }
                        } finally {
                            lock2.unlock();
                            Thread.sleep(new Random().nextInt(1000));
                        }
                    } else {
                        System.out.println("线程2获取锁2失败,已重试");
                    }

                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}
线程1获取到了锁1
线程2获取到了锁2
线程1获取锁2失败,已重试
线程2获取到了锁1
线程2成功获取到了2把锁
线程1获取到了锁1
线程1获取到了锁2
线程1成功获取到了2把锁
lockInterruptibly()

相当于tryLock(long time,TimeUnit unit)把超时时间设置为无限。在等待锁的过程中,线程可以被中断

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();
        }
        thread1.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

Thread-1尝试获取锁
Thread-0尝试获取锁
Thread-1获取到了锁
Thread-1睡眠期间被中断了
Thread-1释放了锁
Thread-0获取到了锁
Thread-0释放了锁

输出结果2

Thread-0尝试获取锁
Thread-1尝试获取锁
Thread-0获取到了锁
Thread-1获得锁期间被中断了
Thread-0释放了锁
unlock 解锁
1.4 可见性保证

可见性
happens-before
因为jvm会对代码进行编译优化,指令会出现重排序的情况,为了避免编译优化对并发编程安全性的影响,需要happens-before规则定义一些禁止编译优化的场景,保证并发编程的正确性。

public class VolatileExample {
    int x = 0 ;
    volatile boolean v = false;
    public void writer(){
        x = 42;
        v = true;
    }

    public void reader(){
        if (v == true){
            // 这里x会是多少呢
        }
    }
}

抛出问题:假设有两个线程A和B,A执行了writer方法,B执行reader方法,那么B线程中独到的变量x的值会是多少呢?

jdk1.5之前,线程B读到的变量x的值可能是0,也可能是42,jdk1.5之后,变量x的值就是42了。原因是jdk1.5中,对volatile的语义进行了增强。来看一下happens-before规则在这段代码中的体现。
jdk1.5之前,线程B读到的变量x的值可能是0,也可能是42,jdk1.5之后,变量x的值就是42了。原因是jdk1.5中,对volatile的语义进行了增强。来看一下happens-before规则在这段代码中的体现。

1. 规则一:程序的顺序性规则

一个线程中,按照程序的顺序,前面的操作happens-before后续的任何操作。

对于这一点,可能会有疑问。顺序性是指,我们可以按照顺序推演程序的执行结果,但是编译器未必一定会按照这个顺序编译,但是编译器保证结果一定==顺序推演的结果。

2. 规则二:volatile规则

对一个volatile变量的写操作,happens-before后续对这个变量的读操作。

3. 规则三:传递性规则

如果A happens-before B,B happens-before C,那么A happens-before C。

jdk1.5的增强就体现在这里。回到上面例子中,线程A中,根据规则一,对变量x的写操作是happens-before对变量v的写操作的,根据规则二,对变量v的写操作是happens-before对变量v的读操作的,最后根据规则三,也就是说,线程A对变量x的写操作,一定happens-before线程B对v的读操作,那么线程B在注释处读到的变量x的值,一定是42.

4.规则四:管程中的锁规则

对一个锁的解锁操作,happens-before后续对这个锁的加锁操作。

这一点不难理解。

5.规则五:线程start()规则

主线程A启动线程B,线程B中可以看到主线程启动B之前的操作。也就是start() happens before 线程B中的操作。

6.规则六:线程join()规则

主线程A等待子线程B完成,当子线程B执行完毕后,主线程A可以看到线程B的所有操作。也就是说,子线程B中的任意操作,happens-before join()的返回。

2.锁的分类

image.png

3.乐观锁和悲观锁

3.1 为什么会诞生非互斥同步锁(乐观锁)
互斥同步锁(悲观锁)的劣势
  • 阻塞和唤醒带来的性能劣势
  • 永久阻塞:如果持有锁的线程被永久阻塞,比如遇到了无限循环、死锁等活跃性问题,那么等待该线程释放锁的线程,将永远也得不到执行。
  • 优先级反转 被阻塞的优先级高的线程等待优先级低的线程
3.2 什么是乐观锁和悲观锁

从是否锁住资源的角度分类

  • 悲观锁

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

  • 乐观锁

认为自己在处理操作的时候不会有其它线程的来干扰,所以并不会锁住被操作对象
在更新的时候,去对比在我修改的期间数据有没有被其他人改变过:
如果没有改变过,就去正常修改数据。
如果数据和我一开始拿到的不一样,就不去修改数据,会选择放弃、报错、重试等策略。
乐观锁的实现一般都是利用CAS算法来实现的。
乐观锁Java的典型例子就是原子类、并发容器等。
乐观锁的其它例子:
1.Git
2.update set num = 2,version=version+1where version=1 and id = 5;

3.3 开销对比
  • 悲观锁的原始开销要高于乐观锁,但是特点是一劳永逸,临界区持锁时间就算越来越差,也不会对互斥锁的开销造成影响
  • 乐观锁一开始的开销比悲观锁小,但是如果自旋时间很长或者不停重试,那么消耗的资源也会越来越多。
临界区表示一种公共资源或共享数据,可以被多个线程使用。但是每一次只能有一个线程使用它。一旦临界区资源被占用,想使用该资源的其他线程必须等待。在java中通常使用下面的方式来实现
synchronized(syncObject) { 
    //critical section
}
3.4 使用场景

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

  1. 临界区有IO操作
  2. 临界区代码复杂或者循环量大
  3. 临界区竞争非常激烈
    乐观锁:适合并发写入少,大部分是读取的场景,不加锁能让读取性能大幅提高。

4.可重入锁和非可重入锁

非可重入锁

所谓不可重入锁,即若当前线程执行某个方法已经获取了该锁,那么在方法中尝试再次获取锁时,就会获取不到被阻塞。
我们尝试设计一个不可重入锁:

public class Lock{
    private boolean isLocked = false;
    public synchronized void lock() throws InterruptedException{
        while(isLocked){
            wait();
        }
        isLocked = true;
    }
    public synchronized void unlock(){
        isLocked = false;
        notify();
    }
}
public class LockTest {
    Lock lock = new Lock();
    public void methodA() {
        try {
            lock.lock();
            methodB();
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }

        System.out.println("methodA 执行完了");
    }

    public void methodB() throws InterruptedException {

        try {
            lock.lock();
            System.out.println("methodB 正在执行...");
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }

    }

    public static void main(String[] args)  {
        LockTest lockTest = new LockTest();
        lockTest.methodA();
    }

}

发生死锁,结果无输出

可重入锁

可重入锁,指的是以线程为单位,当一个线程获取对象锁之后,这个线程可以再次获取本对象上的锁,而其他的线程是不可以的。
synchronized 和 ReentrantLock 都是可重入锁。
将前面

public class LockTest {
    ReentrantLock lock = new ReentrantLock ();
    public void methodA() {
        try {
            lock.lock();
            methodB();
        } finally {
            lock.unlock();
        }

        System.out.println("methodA 执行完了");
    }

    public void methodB()  {

        try {
            lock.lock();
            System.out.println("methodB 正在执行...");
        }  finally {
            lock.unlock();
        }

    }

    public static void main(String[] args)  {
        LockTest lockTest = new LockTest();
        lockTest.methodA();
    }

}
methodB 正在执行...
methodA 执行完了

查看线程得到锁的个数

public class GetHoldCount {
    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.unlock();
        System.out.println(lock.getHoldCount());
        lock.unlock();
        System.out.println(lock.getHoldCount());
        lock.unlock();
        System.out.println(lock.getHoldCount());
    }
}
0
1
2
3
2
1
0
public class RecursionDemo {

    private static ReentrantLock lock = new ReentrantLock();

    private static void accessResource() {
        lock.lock();
        try {
            System.out.println("已经对资源进行了处理");
            if (lock.getHoldCount()<5) {
                System.out.println(lock.getHoldCount());
                accessResource();
                System.out.println(lock.getHoldCount());
            }
        } finally {
            lock.unlock();
        }
    }
    public static void main(String[] args) {
        accessResource();
    }
}
可重入锁优点
  • 避免死锁
  • 提升代码封装性
源码实现

可重入锁ReentrantLock


image.png

非可重入锁ThreadPoolExecutor得Worker类


image.png
ReentrantLock的其他方法介绍
  • isheldbycurrentthread():查询当前线程是否保持此锁。
  • getQueueLength():返回正等待获取此锁定线程数,如果一共开启了5个线程,一个线程执行了await()方法,那么在调用此方法是,返回4,说明此时正有4个线程在等待锁的释放。

5.公平锁和非公平锁

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

    private ReentrantLock lock;

    public Service(boolean isFair) {
        super();
        lock = new ReentrantLock(isFair);
    }

    public void serviceMethod() {
        try {
            lock.lock();
            System.out.println(Thread.currentThread().getName()
                    + "获取锁定");
        } finally {
            lock.unlock();
        }
    }
    public static void main(String[] args) {
        final Service service = new Service(true);
        Runnable runnable = new Runnable() {

            @Override
            public void run() {
                System.out.println("线程" + Thread.currentThread().getName()
                        + "运行了");
                service.serviceMethod();
            }
        };
        Thread[] threadArray = new Thread[10];
        for (int i = 0; i < 10; i++) {
            threadArray[i] = new Thread(runnable);
        }
        for (int i = 0; i < 10; i++) {
            threadArray[i].start();
        }
    }
}
线程Thread-0运行了
线程Thread-1运行了
Thread-0获取锁定
线程Thread-4运行了
Thread-1获取锁定
Thread-4获取锁定
线程Thread-5运行了
Thread-5获取锁定
线程Thread-8运行了
Thread-8获取锁定
线程Thread-2运行了
Thread-2获取锁定
线程Thread-3运行了
Thread-3获取锁定
线程Thread-6运行了
Thread-6获取锁定
线程Thread-9运行了
Thread-9获取锁定
线程Thread-7运行了
Thread-7获取锁定
非公平

上述代码true 修改为false

final Service service = new Service(false);
线程Thread-0运行了
线程Thread-4运行了
线程Thread-5运行了
线程Thread-1运行了
Thread-0获取锁定
Thread-4获取锁定
线程Thread-2运行了
线程Thread-3运行了
Thread-5获取锁定
线程Thread-8运行了
Thread-8获取锁定
Thread-1获取锁定
Thread-2获取锁定
线程Thread-7运行了
Thread-3获取锁定
Thread-7获取锁定
线程Thread-9运行了
线程Thread-6运行了
Thread-9获取锁定
Thread-6获取锁定
5.2 什么要有非公平锁
  • 为了提高效率,避免唤醒带来的空档期
5.3公平和非公平的优缺点
类型 优势 劣势
公平锁 各线程公平平等,每个线程在等待一段时间后,总有执行的机会 更慢,吞吐量更小
不公平锁 更快,吞吐量更大 有可能产生线程饥饿,也就是某些线程在长时间内,始终得不到执行
image.png
image.png

6.共享锁和排它锁:以ReentrantReadWriteLock读写锁

排它锁,又称为独占锁,独享锁
共享锁,又称为读锁,获得共享锁之后,可以查看但无法修改和删除数据,其他线程此时也可以获取到共享锁,也可以查看单无法修改和删除数据

共享锁和排它锁的典型是读写锁ReentrantReadWriteLock,其中读锁是共享锁,写锁是独享锁。

6.1读写锁的作用
  • 在没有读写锁之前,我们假设使用ReentrantLock,那么虽然我们保证了线程安全,但是浪费了一定资源,多个读操作同时进行,并没有线程安全问题
  • 在读的地方使用读锁,在写的地方使用写锁,灵活控制,如果没有写锁的情况下,读是阻塞的,提高了程序的执行效率。
6.2读写锁的规则
  • 多个线程只申请读锁,都可以申请到
  • 如果有一个线程已经占有了读锁,则此时其它线程如果要申请写锁,则申请写锁的线程会一直等待释放读锁
  • 如果有一个线程已经占有了写锁,则此时其它线程如果要申请写锁或者读锁,则申请写锁的线程会一直等待释放写锁
    总结:要么一个或多个线程同时有读锁,要么是一个线程有写锁,但是两者不会同时出现。

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

ReentrantReadWriteLock用法
public class ReadWrite {

    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();
    }
}
Thread1得到了读锁,正在读取
Thread2得到了读锁,正在读取
Thread1释放读锁
Thread2释放读锁
Thread3得到了写锁,正在写入
Thread3释放写锁
Thread4得到了写锁,正在写入
Thread4释放写锁

可以同时读,不能同时写

6.3读锁和写锁的交互方式

读锁插队策略
公平锁:不允许插队
非公平锁下:假设线程2和线程4正在同时读取,线程3想要写入,拿不到锁,于是进入等待队列,线程5不在队列里,现在过来想要读取。此时有2中策略:
策略1:读可以插队

  • 效率高
  • 容易造成饥饿

策略2:避免饥饿
1.写锁可以随时插队
2.读锁仅在等待队列头结不是想获取写锁的线程的时候可以插队。

ReentrantReadWriteLock 选择了策略2
6.4ReentrantReadWriteLock源码
公平锁的情况
image.png
非公平锁的情况
image.png
public class ReadWriteQueue {

    private static ReentrantReadWriteLock reentrantReadWriteLock = new ReentrantReadWriteLock(false);
    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(()->write(),"Thread1").start();
        new Thread(()->read(),"Thread2").start();
        new Thread(()->read(),"Thread3").start();
        new Thread(()->write(),"Thread4").start();
        new Thread(()->read(),"Thread5").start();
    }
}
Thread1得到了写锁,正在写入
Thread1释放写锁
Thread2得到了读锁,正在读取
Thread3得到了读锁,正在读取
Thread2释放读锁
Thread3释放读锁
Thread4得到了写锁,正在写入
Thread4释放写锁
Thread5得到了读锁,正在读取
Thread5释放读锁

非公平锁:读锁仅在等待队列头结不是想获取写锁的线程的时候可以插队
代码验证如下

public class NonfairBargeDemo {

    private static ReentrantReadWriteLock reentrantReadWriteLock = new ReentrantReadWriteLock(
            false);

    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();
    }
}
image.png
6.4锁的升降级

同一个线程中,在没有释放读锁的情况下,就去申请写锁,这属于锁升级,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();
    }
}
先演示降级是可以的
Thread1得到了写锁,正在写入
在不释放写锁的情况下,直接获取读锁,成功降级
Thread1释放写锁
------------------
演示升级是不行的
Thread2得到了读锁,正在读取
升级会带来阻塞
ReentrantReadWriteLock 能降级不能升级
锁降级中读锁的获取是否必要呢?

答案是必要的。主要是为了保证数据的可见性,如果当前线程不获取读锁而是直接释放写锁, 假设此刻另一个线程(记作线程T)获取了写锁并修改了数据,那么当前线程无法感知线程T的数据更新。如果当前线程获取读锁,即遵循锁降级的步骤,则线程T将会被阻塞,直到当前线程使用数据并释放读锁之后,线程T才能获取写锁进行数据更新。

public class DegradeLock {
    private int value;
    private ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
    private Lock readLock = lock.readLock();
    private Lock writeLock = lock.writeLock();

    public void writeRead(){
        try{
            Thread.sleep(300);
        }  catch (InterruptedException e) {
            e.printStackTrace();
        }
        this.writeLock.lock();//1
        this.value++; //2
        //this.readLock.lock(); //3
        this.writeLock.unlock();//4

        System.out.printf("%s 读取到value的值为 %d \n",Thread.currentThread().getName(),this.value);
        // this.readLock.unlock();//5
    }

    public static void main(String[] args) {
        DegradeLock dl = new DegradeLock();
        new Thread(()->dl.writeRead()).start();
        new Thread(()->dl.writeRead()).start();
        new Thread(()->dl.writeRead()).start();
        new Thread(()->dl.writeRead()).start();
        new Thread(()->dl.writeRead()).start();
    }
}
image.png

可以看到,出现并发问题了。

把代码中的注释打开之后,问题解决。

原因是3处又用读锁锁了一次,4处虽然释放了锁,由于又加了3处的锁,读写锁互斥,其他线程仍然无法进入2处修改值,只有到5处释放了锁,其它线程才能竞争1处的锁。

6.5适合场景

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

7.自旋锁和阻塞锁

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

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

在许多场景中,同步资源的锁定时间很短,为了这一小段时间去切换线程,线程挂起和恢复现场的花费可能会让系统得不偿失。
如果物理机器有多个处理器,能够让两个或以上的线程同时并行执行,我们就可以让后面那个请求锁的线程不放弃CPU的执行时间,看看持有锁的线程能否很快就释放锁。
而为了让当前线程“稍等一下”,我们需让当前线程进行自旋,如果在自旋完成后前面锁定同步资源的线程已经释放了锁,那么当前线程就可以不必阻塞而是直接获取同步资源,从而避免切换线程的开销。
这就是自旋。

自旋的缺点:

不能代替阻塞。虽然避免线程切换的开销,但要占用处理器时间。如果锁被占用的时间很短,自旋等待的效果就会非常好。反之,如果锁被长时间占用,那么自旋的线程只会浪费处理器资源。
所以,自旋等待的时间必须有一定的限度,如果自旋超过了限度次数(默认是10次,可以使用-XX:PreBlockSpin来更改)没有成功获得锁,就应当挂起线程。
自旋锁的实现原理同样也是CAS,AtomInteger中调用unsafe进行自增操作的源码中do-while循环就是一个自旋操作,如果修改数值失败则通过循环来执行自旋,直至修改成功。

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

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

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

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

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

9.锁优化

9.1 Java虚拟机对锁的优化
  • 自旋锁和自适应
  • 锁消除
  • 锁粗化
9.2 如何优化锁和提高并发性能
  1. 缩小同步代码块
  2. 尽量不要锁住方法
  3. 减少请求锁的次数
  4. 避免人为制造热点
  5. 锁中尽量不要再包含锁
  6. 选择合适的锁类型或合适的工具类
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,242评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,769评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,484评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,133评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,007评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,080评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,496评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,190评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,464评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,549评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,330评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,205评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,567评论 3 298
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,889评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,160评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,475评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,650评论 2 335

推荐阅读更多精彩内容

  • layout: posttitle: 《Java并发编程的艺术》笔记categories: Javaexcerpt...
    xiaogmail阅读 5,766评论 1 19
  • 第2章 java并发机制的底层实现原理 Java中所使用的并发机制依赖于JVM的实现和CPU的指令。 2.1 vo...
    kennethan阅读 1,370评论 0 2
  • 锁 锁是用来控制多个线程访问共享资源的方式,一般来说,一个锁能够防止多个线程同时访问共享资源(但是有些锁可以允许多...
    黄俊彬阅读 1,477评论 1 15
  • 参考资料:《实战Java高并发程序设计》 1.锁优化的几个方面 1.减少锁持有时间 减少锁的持有时间有助于降低锁冲...
    agile4j阅读 272评论 0 2
  • 即使是一丝希望的光芒,也可以照亮整个宇宙的黑暗。 ——《蓝石头》 《向左走向右走》 1.在这个...
    月亮和六便士_阅读 9,860评论 7 134