Java——多线程高并发系列之Lock、ReentrantLock

简介: Java——多线程高并发系列之Lock、ReentrantLock

文章目录:


写在前面

说说synchronizedLock的区别?

Demo1(先演示一下锁的可重入性)

Demo2ReentrantLock的基本使用)

Demo3(使用Lock锁同步不同方法中的代码块)

Demo4ReentrantLock锁的可重入性)

Demo5ReentrantLocklockInterruptibly()方法)

Demo6lockInterruptibly()方法可以避免死锁)

Demo7ReentrantLocktryLock(long time, TimeUnit unit)方法)

Demo8ReentrantLocktryLock()方法)

Demo9tryLock()方法可以避免死锁)

写在前面


JDK5 Lock 口,有 ReentrantLock 现类,ReentrantLock 锁称为可重入锁,它功能比 synchronized 多。

一般来说,我们称 synchronized 为内部锁,称 Lock 为显示锁。


说说synchronizedLock的区别?

·       synchronized是关键字,Lock是接口。

·       synchronized无法获取锁的状态,Lock可以。

·       synchronized会自动释放锁,Lock需要手动。

·       synchronized没有Lock锁灵活(Lock锁可以自己定制)。

Demo1(先演示一下锁的可重入性)


package com.szh.lock;
/**
 * 演示锁的可重入性
 */
public class Test01 {
    public synchronized void metthod1() {
        System.out.println("同步方法1");
        //线程执行 metthod1() 方法,默认 this 作为锁对象,
        //在 metthod1() 方法中调用了 method2() 方法,注意当前线程还是持有 this 锁对象的
        //method2() 同步方法默认的锁对象也是 this 对象, 要执行 method2() 必须先获得 this 锁对象,
        //当前 this 对象被当前线程持有,可以 再次获得 this 对象, 这就是锁的可重入性.
        //假设锁不可重入的话,可能会造成死锁
        method2();
    }
    public synchronized void method2() {
        System.out.println("同步方法2");
        method3();
    }
    public synchronized void method3() {
        System.out.println("同步方法3");
    }
    public static void main(String[] args) {
        Test01 obj=new Test01();
        new Thread(new Runnable() {
            @Override
            public void run() {
                obj.metthod1();
            }
        }).start();
    }
}

Demo2(ReentrantLock的基本使用)


首先呢,LockJDK5中新增的锁接口,它的实现类中有一个常用的就是ReentrantLock,在这其中,先介绍一下 lock()unlock() 方法,第一个是先获得锁,第二个是释放锁,而在这两个方法之间的代码就可以看作同步代码块。就像之前的synchronized

package com.szh.lock;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
/**
 * ReentrantLock 的基本使用
 */
public class Test02 {
    //定义一个显示锁
    static Lock lock=new ReentrantLock();
    public static void method() {
        //先获得锁
        lock.lock();
        //for循环此时就是同步代码块
        for (int i = 0; i < 3; i++) {
            System.out.println(Thread.currentThread().getName() + " ---> " + i);
        }
        //释放锁
        lock.unlock();
    }
    public static void main(String[] args) {
        Runnable r=new Runnable() {
            @Override
            public void run() {
                method();
            }
        };
        //启动三个线程
        new Thread(r).start();
        new Thread(r).start();
        new Thread(r).start();
    }
}

可以看到,这里Thread-0线程先执行,经过lock.lock()获得了锁对象之后,执行for循环(此处相当于同步代码块),而在这个时候,Thread-1Thread-2是无法进来执行for循环的,因为当前的锁对象是被Thread-0占有的,如果后两个子线程想要进来执行了话,必须等到Thread-0通过lock.unlock()方法释放锁对象之后,才可以执行。

Demo3(使用Lock锁同步不同方法中的代码块)


package com.szh.lock;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
/**
 * 使用 Lock 锁同步不同方法中的同步代码块
 */
public class Test03 {
    //定义锁对象
    static Lock lock=new ReentrantLock();
    public static void method1() {
        //经常在 try 代码块中获得 Lock 锁, 在 finally 子句中释放锁
        try {
            lock.lock(); //获得锁
            System.out.println(Thread.currentThread().getName() + " ---method1--- " + System.currentTimeMillis());
            Thread.sleep(1000);
            System.out.println(Thread.currentThread().getName() + " ---method1--- " + System.currentTimeMillis());
        } catch (InterruptedException e) {
            e.printStackTrace();
        }finally {
            lock.unlock(); //释放锁
        }
    }
    public static void method2() {
        //经常在 try 代码块中获得 Lock 锁, 在 finally 子句中释放锁
        try {
            lock.lock(); //获得锁
            System.out.println(Thread.currentThread().getName() + " ---method2--- " + System.currentTimeMillis());
            Thread.sleep(1000);
            System.out.println(Thread.currentThread().getName() + " ---method2--- " + System.currentTimeMillis());
        } catch (InterruptedException e) {
            e.printStackTrace();
        }finally {
            lock.unlock(); //释放锁
        }
    }
    public static void main(String[] args) {
        Runnable r1=new Runnable() {
            @Override
            public void run() {
                method1();
            }
        };
        Runnable r2=new Runnable() {
            @Override
            public void run() {
                method2();
            }
        };
        new Thread(r1).start();
        new Thread(r1).start();
        new Thread(r2).start();
        new Thread(r2).start();
    }
}

这里的执行结果:首先创建了4个子线程,Thread-0先抢到CPU执行权,进来获得lock锁对象,中间有一个sleep方法,这个的意思是让当前执行它的线程进入睡眠状态,但是当前线程不会释放它占有的锁,所以其他三个子线程是进不来的,因为当前lock锁对象被Thread-0占有了,在Thread-0执行完毕后。其他三个子线程都是同样的道理,只是说每次运行的结果可能会不相同,是因为线程的执行是抢着执行的。

Demo4(ReentrantLock锁的可重入性)


package com.szh.lock;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
/**
 * ReentrantLock 锁的可重入性
 */
public class Test04 {
    static class SubThread extends Thread {
        //定义锁对象
        private static Lock lock=new ReentrantLock();
        //定义变量
        private static int num=0;
        @Override
        public void run() {
            for (int i = 0; i < 10000; i++) {
                try {
                    //可重入锁指可以反复获得该锁
                    lock.lock();
                    lock.lock();
                    num++;
                }finally {
                    lock.unlock();
                    lock.unlock();
                }
            }
        }
    }
    public static void main(String[] args) throws InterruptedException {
        SubThread t1=new SubThread();
        SubThread t2=new SubThread();
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        System.out.println(SubThread.num);
    }
}

这个代码案例的意思:创建两个子线程,第一个子线程抢到了CPU执行权,开始执行,第一个lock.lock()获得了锁对象,之后还可以再次lock.lock()第二次获得该锁对象,这是完全可以的,也就是说Lock接口的实现类ReentrantLock是支持锁的可重入性的。但是释放锁的时候,你线程获得了几个锁对象,这里就需要释放几次锁对象。

如果说第一个子线程连续执行两次lock.lock()获得了两次锁对象,但是只执行一次lock.unlock()了话,就会出现一种结果:程序卡在这里了,因为第一个子线程申请了两个当前锁对象,只释放了一次当前锁对象,那么还有一个锁对象被它占有着,这就导致第二个子线程没法执行了,因为第一个子线程没有释放完锁对象,那么第二个子线程获得不到当前lock锁对象,就没法继续执行下去了,所以,程序卡在这里了。

Demo5(ReentrantLock的lockInterruptibly()方法)


package com.szh.lock;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
/**
 * lockInterruptibly()方法
 *  如果当前线程未被中断则获得锁,
 *  如果当前线程被中断则出现异常.
 */
public class Test05 {
    static class Service {
        private Lock lock=new ReentrantLock(); //定义锁对象
        public void serviceMethod() {
            try {
                //lock.lock();  获得锁, 即使调用了线程的 interrupt() 方法, 也没有真正的中断线程
                //如果线程被中断了, 不会获得锁, 会产生异常
                lock.lockInterruptibly();
                System.out.println(Thread.currentThread().getName() + " --- begin lock");
                //执行一段耗时的操作
                for (int i = 0; i < Integer.MAX_VALUE; i++) {
                    new StringBuilder();
                }
                System.out.println(Thread.currentThread().getName() + " --- end lock");
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                System.out.println(Thread.currentThread().getName() + " === 释放锁");
                lock.unlock(); //释放锁
            }
        }
    }
    public static void main(String[] args) throws InterruptedException {
        Service s=new Service();
        Runnable r=new Runnable() {
            @Override
            public void run() {
                s.serviceMethod();
            }
        };
        Thread t1=new Thread(r);
        t1.start();
        Thread.sleep(50);
        Thread t2=new Thread(r);
        t2.start();
        Thread.sleep(50);
        t2.interrupt(); //中断 t2 线程
    }
}

lockInterruptibly()方法的作用:如果当前线程未被中断则获得锁,如果当前线程被中断则出现异常。


当你使用lock()来获得锁的时候,即使你让一个线程执行interrupt方法中断它也没有用。如果想让线程能够被中断了话,需要使用 lockInterruptibly() 方法。


上述代码的执行过程:创建了两个子线程,第一个子线程lock.lockInterruptibly()获得了锁对象,去执行同步代码块(for循环)了,因为start方法只要一给当前线程开辟出栈空间,那么它就结束了,开辟成功的线程就去执行自己的run方法了,而程序代码就可以正常向下执行了。所以这个时候第二个子线程也start了,它也会去执行run方法对应的要求,上来就是获得锁对象,而此时锁对象暂时被第一个子线程占有,而第一个子线程还在for循环那里呢(这个for循环很耗时的),这个时候main主线程中,已经走到了最后一行,中断第二个子线程,根据lock.lockInterruptibly()方法,如果执行它的线程被中断,则直接出现异常,所以第二个子线程会走catch子句,最后执行finally释放锁(当然了,它压根也就没获得锁,这里打印这句话是为了更好的理解),最后第一个子线程是可以顺利执行完、释放锁的。


Demo6(lockInterruptibly()方法可以避免死锁)


package com.szh.lock;
import java.util.concurrent.locks.ReentrantLock;
/**
 * 通过 ReentrantLock 锁的 lockInterruptibly() 方法避免死锁的产生
 */
public class Test06 {
    static class MyLock implements Runnable {
        //创建两个ReentrantLock等锁对象
        private static ReentrantLock lock1=new ReentrantLock();
        private static ReentrantLock lock2=new ReentrantLock();
        int lockNum; //定义整数变量,决定使用哪个锁,偶数用lock1,奇数用lock2
        public MyLock(int lockNum) {
            this.lockNum=lockNum;
        }
        @Override
        public void run() {
            try {
                if (lockNum % 2 == 1) { //奇数, 先锁 1, 再锁 2
                    lock1.lockInterruptibly();
                    System.out.println(Thread.currentThread().getName() + "获得锁1,还需要获得锁2");
                    Thread.sleep(1000);
                    lock2.lockInterruptibly();
                    System.out.println(Thread.currentThread().getName() + "同时获得了锁1与锁2");
                }else { //偶数, 先锁 2, 再锁 1
                    lock2.lockInterruptibly();
                    System.out.println(Thread.currentThread().getName() + "获得了锁2,还需要获得锁1");
                    Thread.sleep(1000);
                    lock1.lockInterruptibly();
                    System.out.println(Thread.currentThread().getName() + "同时获得了锁1与锁2");
                }
            }catch (InterruptedException e) {
                e.printStackTrace();
            }finally {
                if (lock1.isHeldByCurrentThread()) { //判断当前线程是否持有该锁
                    lock1.unlock();
                }
                if (lock2.isHeldByCurrentThread()) {
                    lock2.unlock();
                }
                System.out.println(Thread.currentThread().getName() + "线程退出");
            }
        }
    }
    public static void main(String[] args) throws InterruptedException {
        MyLock myLock1=new MyLock(11);
        MyLock myLock2=new MyLock(22);
        Thread t1=new Thread(myLock1);
        Thread t2=new Thread(myLock2);
        t1.start();
        t2.start();
        //在 main 线程, 等待 3000 ms, 如果还有线程没有结束就中断该线程
        Thread.sleep(1000 * 3);
        //可以中断任何一个线程来解决死锁, t2 线程会放弃对锁 1 的申请, 同时释放锁 2, t1 线程会完成它的任务
        if (t2.isAlive()) {
            t2.interrupt();
        }
    }
}

首先创建两个子线程,同时创建两个锁对象,这两个子线程一人一个锁,分别执行自己的start方法,main主线程此时睡眠3秒。


第一个子线程执行run方法,首先获得锁1,然后sleep1秒,让出CPU执行权,但不会释放锁;此时第二个子线程执行run方法,首先获得锁2,然后sleep1秒;第一个子线程醒了,它还想获得锁2,锁2已被第二个子线程占有,无法获得;第二个子线程醒了,它还想获得锁1,锁1已被第一个子线程占有,无法获得;这个时候差不多main主线程也该醒了,它会判断当前的t2子线程是否还活着,如果活着,则中断t2子线程;根据lockInterruptibly()方法,如果被中断,则产生异常,所以此时第二个子线程会走catch子句,最后finally子句判断第二个子线程是否占有锁1、锁2,有就释放,最后第二个子线程执行完毕退出;最后剩了第一个子线程,因为锁2已经被第二个子线程释放了,所以它此时可以获得锁2了,那就顺利执行呗,到最后的finally子句,正常释放锁对象就可以了。


Demo7(ReentrantLock的tryLock(long time, TimeUnit unit)方法)


package com.szh.lock;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.ReentrantLock;
/**
 * tryLock(long time, TimeUnit unit) 的作用在给定等待时长内,
 * 锁没有被另外的线程持有, 并且当前线程也没有被中断, 则获得该锁.
 * 通过该方法可以实现锁对象的限时等待.
 */
public class Test07 {
    static class TimeLock implements Runnable {
        private static ReentrantLock lock=new ReentrantLock(); //定义锁对象
        @Override
        public void run() {
            try {
                //假设 t1 线程先持有锁, 完成任务需要 4 秒钟,
                //这个时候 t2 线程尝试获得锁, t2 线程在 3 秒内还没有获得锁的话, 那么它就不再等了,直接放弃
                if (lock.tryLock(3, TimeUnit.SECONDS)) {
                    System.out.println(Thread.currentThread().getName() + "获得锁,执行耗时任务");
                    Thread.sleep(1000 * 4);
                    /*
                        假设 t1 线程先持有锁, 完成任务需要 2 秒钟
                        这个时候t2 线程尝试获得锁, t2 线程会一直尝试
                        在它约定尝试的 3 秒内可以获得锁对象
                     */
                    //Thread.sleep(1000 * 2);
                }else {
                    System.out.println(Thread.currentThread().getName() + "没有获得锁");
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                if (lock.isHeldByCurrentThread()) {
                    lock.unlock();
                }
            }
        }
    }
    public static void main(String[] args) {
        TimeLock timeLock=new TimeLock();
        Thread t1=new Thread(timeLock);
        Thread t2=new Thread(timeLock);
        t1.setName("t1");
        t2.setName("t2");
        t1.start();
        t2.start();
    }
}

ReentrantLock类中的tryLock(long time, TimeUnit unit)方法有两个参数,第一个参数是指定时间,第二个参数是时间的级别(它是一个枚举)。代码中的意思是时间是3、级别是SECONDS秒,所以就是3秒。


这里创建两个子线程,设置了它们各自的名称,之后分别start启动,当t1线程走到 lock.tryLock(3, TimeUnit.SECONDS) 时,会判断在3秒之内,如果这个锁没有被其他线程持有,而且t1线程也没有被中断,则获得该锁。那显然,此时lock锁对象无人持有,所以t1线程获得该锁,之后它睡眠了4秒;这个时候t2线程来了,它走到lock.tryLock(3, TimeUnit.SECONDS) 时,也会像t1线程那样去判断3秒之内,如果这个锁没有被其他线程持有,而且t2线程也没有被中断,则获得该锁;显然该锁对象已经被t1线程持有了,而且t1线程还睡眠了4秒,也就是说t2线程等待3秒之后,t1线程还没有释放该锁对象,所以t2线程是获取不到这个锁对象的,所以t2线程就不会再等了,直接放弃,走else

Demo8(ReentrantLock的tryLock()方法)


package com.szh.lock;
import java.util.concurrent.locks.ReentrantLock;
/**
 * tryLock() 当锁对象没有被其他线程持有的情况下, 才会获得该锁定
 */
public class Test08 {
    static class Service {
        private ReentrantLock lock=new ReentrantLock();
        public void serviceMethod() {
            try {
                if (lock.tryLock()) {
                    System.out.println(Thread.currentThread().getName() + "获得锁定");
                    Thread.sleep(1000 * 3); //模拟执行任务的时长
                }else {
                    System.out.println(Thread.currentThread().getName() + "没有获得锁定");
                }
            }catch (InterruptedException e) {
                e.printStackTrace();
            }finally {
                if (lock.isHeldByCurrentThread()) {
                    lock.unlock();
                }
            }
        }
    }
    public static void main(String[] args) throws InterruptedException {
        Service service=new Service();
        Runnable r=new Runnable() {
            @Override
            public void run() {
                service.serviceMethod();
            }
        };
        Thread t1=new Thread(r);
        t1.start();
        Thread.sleep(100);
        Thread t2=new Thread(r);
        t2.start();
    }
}

ReentrantLock类中的tryLock()方法,是对上面那个案例的一个方法重载,这个方法中没有参数。意思是说:当前线程申请该锁对象,如果该锁对象没有被其他线程持有,则获得该锁,同时返回true;否则返回false,不会进行等待。这个执行结果我就不再讲解了,很好理解的。

Demo9(tryLock()方法可以避免死锁)


package com.szh.lock;
import java.util.concurrent.locks.ReentrantLock;
/**
 * 使用 tryLock() 可以避免死锁
 */
public class Test09 {
    static class MyLock implements Runnable {
        private static ReentrantLock lock1=new ReentrantLock();
        private static ReentrantLock lock2=new ReentrantLock();
        private int lockNum;
        public MyLock(int lockNum) {
            this.lockNum=lockNum;
        }
        @Override
        public void run() {
            if (lockNum % 2 == 0) { //偶数先锁 1, 再锁 2
                while (true) {
                    try {
                        if (lock1.tryLock()) {
                            System.out.println(Thread.currentThread().getName() + "获得了锁1,还想获得锁2");
                            Thread.sleep(50);
                            try {
                                if (lock2.tryLock()) {
                                    System.out.println(Thread.currentThread().getName() + "同时获得了锁1与锁2,完成任务了");
                                    return;
                                }
                            } finally {
                                if (lock2.isHeldByCurrentThread()) {
                                    lock2.unlock();
                                }
                            }
                        }
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    } finally {
                        if (lock1.isHeldByCurrentThread()) {
                            lock1.unlock();
                        }
                    }
                }
            }else { //奇数就先锁 2, 再锁 1
                while (true) {
                    try {
                        if (lock2.tryLock()) {
                            System.out.println(Thread.currentThread().getName() + "获得了锁2,还想获得锁1");
                            Thread.sleep(50);
                            try {
                                if (lock1.tryLock()) {
                                    System.out.println(Thread.currentThread().getName() + "同时获得了锁1与锁2,完成任务了");
                                    return;
                                }
                            } finally {
                                if (lock1.isHeldByCurrentThread()) {
                                    lock1.unlock();
                                }
                            }
                        }
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    } finally {
                        if (lock2.isHeldByCurrentThread()) {
                            lock2.unlock();
                        }
                    }
                }
            }
        }
    }
    public static void main(String[] args) {
        MyLock lock1=new MyLock(11);
        MyLock lock2=new MyLock(22);
        Thread t1=new Thread(lock1);
        Thread t2=new Thread(lock2);
        t1.start();
        t2.start();
        //运行后, 使用 tryLock() 尝试获得锁, 不会傻傻的等待, 通过循环不停的再次尝试, 如果等待的时间足够长, 线程总是会获得想要的资源
    }
}

相关文章
|
1天前
|
Java
"揭秘Java IO三大模式:BIO、NIO、AIO背后的秘密!为何AIO成为高并发时代的宠儿,你的选择对了吗?"
【8月更文挑战第19天】在Java的IO编程中,BIO、NIO与AIO代表了三种不同的IO处理机制。BIO采用同步阻塞模型,每个连接需单独线程处理,适用于连接少且稳定的场景。NIO引入了非阻塞性质,利用Channel、Buffer与Selector实现多路复用,提升了效率与吞吐量。AIO则是真正的异步IO,在JDK 7中引入,通过回调或Future机制在IO操作完成后通知应用,适合高并发场景。选择合适的模型对构建高效网络应用至关重要。
|
1天前
|
Java
java开启线程的四种方法
这篇文章介绍了Java中开启线程的四种方法,包括继承Thread类、实现Runnable接口、实现Callable接口和创建线程池,每种方法都提供了代码实现和测试结果。
java开启线程的四种方法
|
4天前
|
存储 缓存 安全
深度剖析Java HashMap:源码分析、线程安全与最佳实践
深度剖析Java HashMap:源码分析、线程安全与最佳实践
|
5天前
|
缓存 前端开发 JavaScript
一篇文章助你搞懂java中的线程概念!纯干货,快收藏!
【8月更文挑战第11天】一篇文章助你搞懂java中的线程概念!纯干货,快收藏!
13 0
一篇文章助你搞懂java中的线程概念!纯干货,快收藏!
|
4天前
|
算法 安全 Java
深入解析Java多线程:源码级别的分析与实践
深入解析Java多线程:源码级别的分析与实践
|
5天前
|
Java 程序员 调度
深入浅出Java多线程编程
Java作为一门成熟的编程语言,在多线程编程方面提供了丰富的支持。本文将通过浅显易懂的语言和实例,带领读者了解Java多线程的基本概念、创建方法以及常见同步工具的使用,旨在帮助初学者快速入门并掌握Java多线程编程的基础知识。
4 0
|
Java
java源码 - ReentrantLock之FairSync
开篇  这篇文章主要是讲解FairSync公平锁的源码分析,整个内容分为加锁过程、解锁过程,CLH队列等概念。  首先一直困扰我的CLH队列的CLH的缩写我终于明白,看似三个人的人名的首字符缩写"CLH" (Craig, Landin, andHagersten)。
1013 0
|
Java
java源码 - ReentrantLock之NonfairSync
开篇  NonfairSync和FairSync相比而言,多了一次抢占机会,其他处理逻辑几乎是一模一样。 NonfairSync的tryAcquire的操作流程中如果发现当前锁未被占用那么立即抢占锁。
1065 0
|
8天前
|
安全 Java 数据处理
Java并发编程:解锁多线程的潜力
在数字化时代的浪潮中,Java作为一门广泛使用的编程语言,其并发编程能力是提升应用性能和响应速度的关键。本文将带你深入理解Java并发编程的核心概念,探索如何通过多线程技术有效利用计算资源,并实现高效的数据处理。我们将从基础出发,逐步揭开高效并发编程的面纱,让你的程序运行得更快、更稳、更强。
|
7天前
|
Java 开发者
奇迹时刻!探索 Java 多线程的奇幻之旅:Thread 类和 Runnable 接口的惊人对决
【8月更文挑战第13天】Java的多线程特性能显著提升程序性能与响应性。本文通过示例代码详细解析了两种核心实现方式:Thread类与Runnable接口。Thread类适用于简单场景,直接定义线程行为;Runnable接口则更适合复杂的项目结构,尤其在需要继承其他类时,能保持代码的清晰与模块化。理解两者差异有助于开发者在实际应用中做出合理选择,构建高效稳定的多线程程序。
28 7