万字讲清 synchronized 和 ReentrantLock 实现并发中的锁

synchronized 和 ReentrantLock 是用来实现并发操作中的锁机制。

synchronized

synchronized关键字用于为 Java 对象、方法、代码块提供线程安全的操作。
synchronized属于独占式的悲观锁,同时属于可重入锁。在使用 synchronized修饰对象的时候,同一时刻只能有一个线程对该对象进行访问;在synchronized 修饰的方法、代码块时,同一个时刻只能有一个线程执行该方法体或方法块,其他线程只有等待当前线程执行完毕并释放资源后才能访问该对象或执行同步代码块。

synchronized提供的是一种原子性内置锁,在Java 中每个对象都是可以把它当做是监视器锁,线程代码执行在进入 synchronized 代码块的时候会自动获取内部所,这个时候其他线程访问的时候就会被阻塞,直到 synchronized中代码执行完毕或者抛出异常,或者调用了wait 方法,才会释放锁资源。
在进入 synchronized 会从主内存把变量读取到自己的工作内存,在退出的时候会把工作内存的值写入到主内存,保证了原子性。

在Java当中,每个对象都有一个 monitor 对象,加锁就是在竞争 monitor 对象。对代码块加锁是通过咱前后分别加上 monitorenter 和 monitorexit 指令实现的,对方法是否加锁是通过一个标记位来判断的。

synchronized 的作用范围

synchronized 作用于成员变量和非静态方法的时候,锁住的是对象的实例,即 this 对象。

synchronized 作用于静态方法的时候,锁住的是 Class 实例,因为静态方法是属于 Class 而不属于对象的。

synchronized 作用于一个代码块的时候,锁住的是所有代码块中配置的对象。

作用于成员方法和非静态方法:

public class SynchronizedDemo01 {
    
    
    public static void main(String[] args) {
    
    
        final SynchronizedDemo01 synDemo = new SynchronizedDemo01();
        new Thread(new Runnable() {
    
    
            @Override
            public void run() {
    
    
                synDemo.getSynchronized1();
            }
        }).start();

        new Thread(new Runnable() {
    
    
            @Override
            public void run() {
    
    
                synDemo.getSynchronized1();
            }
        }).start();

    }

    //synchronized 修饰的成员方法,锁住了对象
    public synchronized void getSynchronized1() {
    
    
        try {
    
    
            for (int i = 0; i < 3; i++) {
    
    
                System.out.println(Thread.currentThread().getName() + ":" + i);
                Thread.sleep(3000);
            }
        } catch (InterruptedException e) {
    
    
            e.printStackTrace();
        }
    }
}

在上面的程序当中我们定义的是一个 synchronized 所修饰的普通方法,然后在main函数当中定义了对象的实例并发的执行各个方法。我们在这在看到 线程2会等待线程1执行完毕才能执行。这就是synchronized 锁住了当前的对象实例 synchronizedDemo01 导致的。

结果:
在这里插入图片描述

而去掉synchronized 就是并行的执行了:
在这里插入图片描述

或者定义两个实例去调用这一个方法,也是可以并发的执行:
在这里插入图片描述

synchronized所修饰的静态方法
synchronized在修饰静态方法的时候,它锁住的是当前类的 Class 对象,具体的代码如下:
还是上面定义的两个实例的代码当中,给方法加上static 修饰为静态方法:

public class SynchronizedDemo01 {
    
    
    public static void main(String[] args) {
    
    
        final SynchronizedDemo01 synDemo = new SynchronizedDemo01();
        final SynchronizedDemo01 synDemo01 = new SynchronizedDemo01();
        new Thread(new Runnable() {
    
    
            @Override
            public void run() {
    
    
                synDemo.getSynchronized1();
            }
        }).start();

        new Thread(new Runnable() {
    
    
            @Override
            public void run() {
    
    
                synDemo01.getSynchronized1();
            }
        }).start();

    }

    //synchronized 修饰的静态方法,锁住的是Class对象
    public static synchronized void getSynchronized1() {
    
    
        try {
    
    
            for (int i = 0; i < 3; i++) {
    
    
                System.out.println(Thread.currentThread().getName() + ":" + i);
                Thread.sleep(3000);
            }
        } catch (InterruptedException e) {
    
    
            e.printStackTrace();
        }
    }

执行结果如下:
在这里插入图片描述

通过控制台的打印输出,我们可以清晰的看出 在static 方法是食欲Class 的,并且Class 的相关数据在 JVM 中是全局共享的,因此静态方法锁相当于类的全局锁,会锁住所有调用该方法的线程。

synchronized作用于代码块
synchronized作用于代码块的时候 ,锁住的往往是在代码块中的配置。
代码:

public class SynchronizedDemo02 {
    
    
    String lock = "lock"; //需要锁的内容

    public static void main(String[] args) {
    
    
        final SynchronizedDemo02 synDemo = new SynchronizedDemo02();
        new Thread(new Runnable() {
    
    
            @Override
            public void run() {
    
    
                synDemo.myThread1();
            }
        }).start();

        new Thread(new Runnable() {
    
    
            @Override
            public void run() {
    
    
                synDemo.myThread2();
            }
        }).start();
    }

    public void myThread1() {
    
    
        try {
    
    
            //在这就占用了叫lock的锁,在这使用的时候,其他地方就不能使用这个锁
            synchronized (lock) {
    
    
                for (int i = 1; i < 3; i++) {
    
    
                    System.out.println(Thread.currentThread().getName() + ":" + i);
                    Thread.sleep(3000);
                }
            }
        } catch (InterruptedException e) {
    
    
            e.printStackTrace();
        }
    }

    public void myThread2() {
    
    
        try {
    
    
            synchronized (lock) {
    
    
                for (int i = 1; i < 3; i++) {
    
    
                    System.out.println(Thread.currentThread().getName() + ":" + i);
                    Thread.sleep(3000);
                }
            }
        } catch (InterruptedException e) {
    
    
            e.printStackTrace();
        }
    }
}

在上面的这个代码里面,就是两个方法都需要这个名字叫lock的锁,所以第二个线程只能等第一个线程忙完才能使用这个锁。
在这里插入图片描述

死锁的现状

我们在编写多线程的时候往往就可能会出现 A 线程依赖 B 线程中的资源,而 B 线程又依赖 A 线程的资源的情况,这是就可能出现死锁的现状。
而这种情况是我们在开发的时候要独杜绝的情况。

代码演示一下死锁的现状

public class SynchronizedDemo02 {
    
    
    String lockA = "lockA"; //需要锁的内容
    String lockB = "lockB";

    public static void main(String[] args) {
    
    
        final SynchronizedDemo02 synDemo = new SynchronizedDemo02();
        new Thread(new Runnable() {
    
    
            @Override
            public void run() {
    
    
                synDemo.myThread1();
            }
        }).start();

        new Thread(new Runnable() {
    
    
            @Override
            public void run() {
    
    
                synDemo.myThread2();
            }
        }).start();
    }

    public void myThread1() {
    
    
        try {
    
    
            //在这就占用了叫lock的锁,在这使用的时候,其他地方就不能使用这个锁
            synchronized (lockA) {
    
    
                for (int i = 1; i < 3; i++) {
    
    
                    System.out.println(Thread.currentThread().getName() + ":" + i);
                    Thread.sleep(3000);
                    synchronized (lockB) {
    
    
                    }
                }
            }
        } catch (InterruptedException e) {
    
    
            e.printStackTrace();
        }
    }

    public void myThread2() {
    
    
        try {
    
    
            synchronized (lockB) {
    
    
                for (int i = 1; i < 3; i++) {
    
    
                    System.out.println(Thread.currentThread().getName() + ":" + i);
                    Thread.sleep(3000);
                    synchronized (lockA) {
    
    
                    }
                }
            }
        } catch (InterruptedException e) {
    
    
            e.printStackTrace();
        }
    }
}

在这里插入图片描述

通过上面的代码可以看到,在执行myThread1方法的时候,synchronized(lockA) 在第一次循环执行后就执行了synchronized(lockB) 要求使用了lockB 锁,而在执行 myThread2方法的时候 synchronized(lockB) 要求使用了 lockB 锁,接着往下进行要求使用了 lockA锁,在这样的情况下出现了死锁,执行结果就是两个线程都挂起,就是等待对方才放手,才可以继续向下。

synchronized的实现原理

在 synchronized 的内部是包括 ContentionList、 EntryList、 WaitSet、 OnDeck、 Owner、 !Owner 这 6 个区域,每个区域的数据都代表锁的不同形态。

ContentionList:锁竞争队列。在所有请求锁的线程都被放在这个竞争队列当中。

EntryList:竞争候选列表。在竞争队列里面有资格成为候选者的移动到这个进制候选列表当中。

WaitSet:等待集合。调用wait 方法后被阻塞的线程就放在这个等待集合当中。

OnDeck:机制候选者。在同一时刻最多只有一个线程在竞争锁资源,该线程的状态被称为OnDeck.

Owner:竞争到锁资源的线程被称为 Owner 状态的线程。

!Owner:在Owner 线程释放锁后,会在 Owner 的状态变为 Owner。

synchronized的执行过程

synchronized 在收到新的锁请求时首先自旋,如果是通过自旋也没有获取锁资源,而将被放入锁的竞争队列 ContentionList中。

为了防止锁竞争时 ContentionList 尾部的元素被大量的并发线程进行 CAS 访问而影响性能。 Owner 线程会在释放锁资源的时候将 ContentionList 中的部分线程移动到 EntryList 中,并指定 EntryList 中的某个线程(一般是最先进入的线程)为 OnDeck 线程。Owner 线程并没有直接把锁传递给 OnDeck 线程,而是把锁竞争的权利交给 OnDeck,让OnDeck 线程重新竞争锁。 在Java当中把这个行为称为“竞争切换”,该行为是牺牲了公平性,但是它提高了性能。

获取到锁的 OnDeck 线程会变为 Owner 线程,而未获得锁资源的线程仍然停留在 EntryList 中。

Owner 线程在被 wait 方法阻塞后,会被转移到 WaitSet 队列当中,直到某个时刻被 notify 方法或者 notifyAll 方法唤醒,会再次进入到 EntryList 中。 ContentionList、EntryList、 WaitSet 中等线程均为阻塞状态,该阻塞是由操作系统来完成的(在Linux 内核下是采用 pthread_mutex_lock 内核函数实现的)

Owner 线程在执行完毕后会被释放锁的资源并变为 !Owner 状态。
在这里插入图片描述

在 synchronized 中,在线程进入 ContentionList 之前,等待的线程会先尝试以自旋的方式来获取锁,如果获取不到就进入 ContentionList ,该做法对于已经进入队列的线程是不公平的,因此 synchronized 是一个非公平锁。
另外,自旋锁获取锁的线程也可以直接抢占 OnDeck 线程的锁资源。

synchronized 它是一个重量级的操作,需要调用操作系统的相关接口,性能较低,给线程加锁的时间有可能超过获取锁后具体的逻辑代码操作时间。

在 JDK 1.6 对 synchronized 做了很多的优化,引入了适应自旋、锁消除、锁粗化、轻量级锁及偏向锁的效率。
锁可以从偏向锁升级到轻量级锁,再升级到重量级锁。这种升级过程就叫做锁膨胀。

在 JDK 1.6 中是默认开启了偏向锁和轻量级锁,可以通过 -XX:UseBiasedLocking 来禁用偏向锁。

ReentrantLock

ReentrantLock 继承了 Lock 接口并实现了在接口中定义的方法,是一个可重入的独占式锁。 ReentrantLock 可以通过自定义队列同步锁(Abstract Queued Synchronized,AQS)来实现锁的获取与释放。

独占锁指该锁在同一时刻只能被一个线程获取,而获取锁的其他线程只能在同步队列中等待;可重入锁指能够支持一个线程对同一个资源执行多次加锁的操作。

ReentrantLock 支持公平锁和非公平锁的实现。 公平指线程竞争锁的机制是公平的,而非公平指不同的线程获取锁的机制是不公平的。

RenntrantLock 不但提供了 synchronized 对锁的操作功能,还提供了诸如可响应中断锁、可轮询锁请求、定时锁等避免多线程死锁的方法。

ReentrantLock 的用法

ReentrantLock 是 java.util.concurrent.locks 包 下 的 类,实 现 Lock 接口,Lock 的意义在于提供区别于 synchronized 的另一种具有更多广泛操作的同步方式,它能支持更多灵活的结构。

ReentrantLock 是有显示的操作过程,何时加锁、何时释放锁都是在程序员的控制下完成的。具体的实验流程是定义一个ReentrantLock ,在需要加锁的地方通过 lock 方法加锁,等资源使用完成后再通过 unlock 方法释放锁。

具体代码如下:

public class ReentrantLockDemo1 {
    
    
    public static void main(String[] args) throws ExecutionException, InterruptedException {
    
    
        ReenterLockDemo reentrantLock = new ReenterLockDemo();
        FutureTask<Integer> futureTask = new FutureTask<>(reentrantLock);
        Thread thread = new Thread(futureTask);
        thread.start();//启动线程
        System.out.println(futureTask.get());//获得线程里面返回的结果
    }
}

class ReenterLockDemo implements Callable<Integer> {
    
    

    // 第一步定义一个ReentrantLock
    static ReentrantLock lock = new ReentrantLock();
    static int i = 0;

    @Override
    public Integer call() {
    
    
        for (int j = 0; j < 10; j++) {
    
    
            lock.lock();//第二步:加锁
            lock.lock();//加入重入锁
            try {
    
    
                i++;
            } finally {
    
    
                lock.unlock();//第三步:释放锁
                lock.unlock();
            }
        }
        return i;
    }
}

也都是正常能执行的:
在这里插入图片描述

ReentrantLock 如何避免死锁

在ReentrantLock当中可以避免死锁等问题,具体的避免可以:响应中断、可轮询锁、定时锁

响应中断

在 synchronized 中如果一个线程尝试获取一把锁,其结果就是要么成功获得这个锁继续执行,要么保持等待。
在ReentrantLock 还提供了可响应中断的可能,即在等待锁的过程中,线程可以根据需要取消对锁的请求。

import java.util.concurrent.locks.ReentrantLock;

public class ReentrantLockDemo2 {
    
    
    public static void main(String[] args) {
    
    
        //获得时间
        long time = System.currentTimeMillis();

        ThreadDemo1 demo1 = new ThreadDemo1();
        ThreadDemo2 demo2 = new ThreadDemo2();
        Thread thread1 = new Thread(demo1, "Thread1");
        Thread thread2 = new Thread(demo2, "Thread2");
        thread1.start();
        thread2.start();

        while (true) {
    
    
            //如果等待世界超多3秒
            if (System.currentTimeMillis() - time > 3000) {
    
    
                thread2.interrupt();//中断线程demo2
                break;
            }
        }

    }
}

class ThreadDemo1 implements Runnable {
    
    

    //锁lock1
    static ReentrantLock lock1 = new ReentrantLock();
    //获得另一个线程里面的锁lock2
    ReentrantLock lock2 = ThreadDemo2.lock2;

    @Override
    public void run() {
    
    
        try {
    
    
            lock1.lockInterruptibly();//如果当前线程未被中断的话,就获取锁
            System.out.println(Thread.currentThread().getName() + ",开始执行,获得lock1锁");
            Thread.sleep(1000);//线程睡眠一下
            lock2.lockInterruptibly();//获得另一个锁
            System.out.println(Thread.currentThread().getName() + ",成功获得lock2锁,执行完毕");
        } catch (InterruptedException e) {
    
    
            e.printStackTrace();
        } finally {
    
    
            if (lock1.isHeldByCurrentThread()) {
    
    //关闭锁
                lock1.unlock();
            }
            if (lock2.isHeldByCurrentThread()) {
    
    
                lock2.unlock();
            }
            System.out.println(Thread.currentThread().getName() + ",退出了");
        }
    }
}

class ThreadDemo2 implements Runnable {
    
    

    //锁 lock2
    static ReentrantLock lock2 = new ReentrantLock();
    //获得另一个线程里面的锁lock1
    ReentrantLock lock1 = ThreadDemo1.lock1;

    @Override
    public void run() {
    
    
        try {
    
    
            lock2.lockInterruptibly();//如果当前线程未被中断的话,就获取锁
            System.out.println(Thread.currentThread().getName() + ",开始执行,获得lock2锁");
            Thread.sleep(1000);//线程睡眠一下
            lock1.lockInterruptibly();
            System.out.println(Thread.currentThread().getName() + ",成功获得lock1锁,执行完毕");
        } catch (InterruptedException e) {
    
    
            e.printStackTrace();
        } finally {
    
    
            if (lock2.isHeldByCurrentThread()) {
    
    
                lock2.unlock();
            }
            if (lock1.isHeldByCurrentThread()) {
    
    
                lock1.unlock();
            }
            System.out.println(Thread.currentThread().getName() + ",退出了");
        }
    }
}

在上面的代码当中,线程thread1 和thread2 启动后:
thread1 先占用 lock1,再占用 lock2 锁;
thread2 先占用 lock2,在占用 lock1 锁。
这便形成了thread1 和 thread2 之间的相互等待,在两个线程都启动时便处于死锁状态。在main函数当中,while循环中,如果等待时间过长(这里我设置为3秒),就认为已经发生了死锁等问题,那么我就让thread2 主动中断(interrupt),释放对 lock1 锁的申请,同时释放掉在使用的 lock1 锁。让 thread1 线程顺利获得 lock2 锁,是程序继续执行下去

执行结果:
在这里插入图片描述

可轮询锁

就是通过 boolean tryLock() 获取锁,如果有可用的锁,则获取该锁并范湖 true,如果锁不可用的话,就不获得,并返回false。

定时锁

通过

boolean tryLock(long time, TimeUnit unit) throws InterruptedException

获取定时锁。

如果在给定的时间内获取到可用的锁,其当前线程未被中断,则获取该锁并返回true。 如果在给定的时间内获取不到可用锁,将禁用当前线程,并且在发生以下三种情况之前,该线程一直处于休眠状态。

  1. 当前线程获取到了可用锁并返回true。
  2. 当前线程在进入此方法的时候设置了该线程的中断状态,或者当前线程在获取锁时被中断,则将抛出 InterruptedException, 并清除当前线程的已中断状态。
  3. 当前线程获取锁的时间超过了指定的等待时间,则就返回 false。如果设定的时间小于等于0,则该方法玩去哪不等待(和可轮询锁一样)。

Lock接口的主要方法

  1. void lock():给对象加锁,如果锁未被其他线程使用,则当前线程将获取该锁,如果锁正在被其他线程使用,则进入阻塞状态。直到当前线程获取到锁。
  2. boolean tryLock():视图给对象加锁,如果锁未被其他线程使用,则将获取该锁并返回 true,否则就返回 false。 tryLock() 和 lock() 的区别就在于 tryLock() 只是“视图”获取锁,如果没有锁,就立即返回。lock() 在所不可用的时候就会一直等待。直到得到可用的锁。
  3. boolean tryLock(long time, TimeUnit unit):创建定时时间,如果在给定的时间内锁可用,就获得锁。
  4. void unlock():释放当先线程锁持有的锁。锁只能由持有至释放掉。如果当前线程并不持有该锁却执行该方法,就抛出异常。
  5. Condition newCondition():创建条件对象,获取等待通知的组件。该组件和当前锁绑定,当前线程只有获取了锁才能调用该组件的 await(),在调用后当前线程就释放锁。
  6. getHoldCount():查询当下线程保持此锁的次数,也就是此线程执行 lock 方法的次数。
  7. getQueueLength():返回等待获取此锁的线程估计数。比如:启动了5个线程,1个线程获得锁,此时另一个线程要获得此锁,就返回4。
  8. getWaitQueueLength(Condition condition):返回在Condition 条件下等待该锁的线程数量。比如:有5个线程要用同一个 condition 对象,并且这 5 个线程都执行了 condition 对象的 await() 方法,那么执行这个方法,就返回5。
  9. hasWaiters(Condition condition):查询是否有线程正在等待与给定条件有关的锁,即对于指定的 condition 对象,有多少个线程执行了 condition.await 方法
  10. hasQueueThread(Thread thread):查询给定的线程当前是否等待获取该锁。
  11. hasQueueThreads():查询当前是否有线程等待该锁
  12. isFair():查询当前该锁是否是公平锁
  13. isHeldByCurrentThread():查询当前线程是否持有该锁,线程执行 lock() 方法的前后状态分别是 false 和 true。
  14. isLock():判断此锁是否被线程所使用。
  15. lockInterruptibly():如果当前线程未被中断,则获取该锁。

tryLock、lock和lockInterruptibly 的区别

● tryLock 若没有可用锁的话,则就获得该锁并返回 true。否则就返回 false。不会有演示和等待;tryLock(long time, TimeUnit unit) 可以增加时间限制,如果超过了指定的时间还没有获得锁,就返回false。

● lock 若有可用的锁,就获取该锁并返回 true,否则就会一直等待直到获取到可用的锁。

● 在锁中断的时候,lockInterruptibly 会抛出异常,而lock 不会。

ReentrantLock中的公平锁和非公平锁

ReentrantLock 是支持公平锁和非公平锁两种方式的。

公平锁指锁的分配和竞争机制是公平的,即遵循先到先得的原则;
非公平锁指 JVM 遵循随机,就近原则分配锁的机制。

ReentrantLock 是通过在构造函数 ReentrantLock(boolean fair)中传递不同的参数来定义不同类型的锁。

ReentrantLock 中总共有三个内部类,并且三个内部类是紧密相关的。
在这里插入图片描述

● Sync 类继承 AbstractQueuedSynchronizer;

● FairSync 类也继承了 Sync 类,表示采用公平策略获取锁,其实现了 Sync 类中的抽象 lock 方法;
在这里插入图片描述

● NonfairSync 类继承了 Sync 类,表示采用非公平策略获取锁,其实现 了 Sync 类中抽象的 lock 方法。
在这里插入图片描述


在ReentrantLock中默认的实现方式是非公平锁。这是因为,非公平锁虽然放弃了锁的公平性,但在执行效率上是明显高于公平锁的。如果系统上没有特殊的要求话,一般情况下建议使用非公平锁。

Synchronized 和 ReentrantLock 的区别

共同点:

都用于控制多线程对共享对象的访问。
都是可重入锁
都保证了可见性和互斥性

不同点:

  1. ReentrantLock 是显示获取和释放锁; Synchronized 是隐式获取和释放锁。为了避免程序出现异常而无法正常释放锁,在使用ReentrantLock 时必须在 finally 语句块中执行释放锁操作。
  2. ReentrantLock 可以响应中断、可轮回,为了处理锁提供了更多的灵活性。
  3. ReentrantLock 是API 级别的,Synchronized 是JVM 级别的。
  4. ReentrantLock 可以定义出公平锁。Synchronized 更本就是非公平锁。
  5. ReentrantLock 可以通过 Condition 绑定多个条件。
  6. 这两个的底层实现是不一样的:Synchronized 是同步阻塞的,采用的是悲观的并发策略; Lock 是同步并发阻塞,采用的是乐观并发的策略。
  7. Lock 是一个接口,而Synchronized 是 Java当中的关键字,Synchronized 是由内置语言来实现的。
  8. 我们可以通过 Lock 直到有没有成功获取到锁,而Synchronized 就无法做到。
  9. Lock 可以通过分别定义读写操作来提高多个线程读操作的效率。


上一篇: ===》 并发中全部锁的概念

猜你喜欢

转载自blog.csdn.net/weixin_45970271/article/details/125226849
今日推荐