【并发编程】锁机制

简介: 【并发编程】锁机制

1.锁的分类

  • 自旋锁:线程状态及上下文切换消耗系统资源,当访问共享资源的时间短,频繁上下文切换不值得。jvm实现,使线程在没有获得锁的时候,不被挂起,转而执行空循环,循环几次之后,如果还没能获得锁,则被挂起。
  • 阻塞锁:阻塞锁改变了线程的运行状态,让线程进入阻塞状态进行等待,当获得相应的信号(唤醒或者时间)时,才可以进入线程的准备就绪状态,转为就绪状态的所有线程,通过竞争,进入运行状态。

     重入锁:支持线程再次进入的锁,就像我们有房间的钥匙,可以多次进入房间类似。

读写锁:两把锁,读锁跟写锁,写写互斥、读写互斥、读读共享。

互斥锁:当线程抢占到资源,其他线程进不来。

悲观锁: 总是假设最坏的情况,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会阻塞直到它拿到锁。

乐观锁:每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,可以使用版本号等机制。

公平锁:所有线程老老实实排队,对大家而言都很公平。

非公平锁:一部分线程排着队,但是新来的线程可能插队。

偏向锁:偏向锁使用了一种等到竞争出现才释放锁的机制,所以当其他线程尝试竞争偏向锁时,持有偏向锁的线程才会释放锁。

独占锁:独占锁模式下,每次只能有一个线程能持有锁。

共享锁:允许多个线程同时获取锁,并发访问共享资源。

2.深入理解Lock接口

(1)lock于synchronized的区别

  • synchronized是java内置的关键字,在jvm层面,Lock是个java类。
  • synchronized无法判断是否获取锁的状态,Lock可以判断是否获取到锁。
  • synchronized会自动释放锁(线程在执行代码块的过程中发生异常会自动释放锁),Lock需要在finally中手动释放锁(unlock方法),否则会造成死锁。
  • 用synchronized关键字的两个线程1和线程2,如果当线程1获取锁,线程2等待。如果线程1阻塞,线程2则会一直等待下去,而Lock锁就不会一直等待下去,如果尝试获取不到锁,线程可以不用一直等待就结束了。
  • synchronized的锁可重入、不可中断、非公平,而Lock锁可重入、可中断、可公平(两者皆可)
  • Lock锁适合大量同步的代码的同步问题,而synchronized锁适合少量代码同步问题。

(2)Lock继承体系图

f3eff356d88443af98e3fa398e643ccc.jpg

(3)Lock常用的API

  • lock()、unlock()加锁、解锁
public class LockTest {
    private Lock lock = new ReentrantLock();
    /**
     * 当前线程释放锁后,其他线程才可以获取到锁
     */
    public void lock(){
        lock.lock();
        try {
            System.out.println("线程"+Thread.currentThread().getName()+":获取到锁资源");
            Thread.sleep(2000L);
        }catch (Exception e){
            System.out.println("线程"+Thread.currentThread().getName()+":释放锁发生异常");
        }finally {
            lock.unlock();
            System.out.println("线程"+Thread.currentThread().getName()+":释放锁完毕");
        }
    }
    public static void main(String[] args) {
        LockTest lockTest = new LockTest();
        new Thread(()->{
            lockTest.lock();
        }).start();
        new Thread(()->{
            lockTest.lock();
        }).start();
    }
}

a2999e74a8ab40d3b03f011998d04508.jpg

  • 尝试获取锁tryLock(),表示用来尝试获取锁,如果获取成功返回true,失败返回false。
public class LockTest {
    private Lock lock = new ReentrantLock();
    /**
     * 线程尝试获取线程锁,如果有其他线程占用,就返回false,无法拿到锁
     */
    public void lock(){
        if(lock.tryLock()){
            try {
                System.out.println("线程"+Thread.currentThread().getName()+":获取到锁资源");
                Thread.sleep(2000L);
            }catch (Exception e){
                System.out.println("线程"+Thread.currentThread().getName()+":释放锁发生异常");
            }finally {
                lock.unlock();
                System.out.println("线程"+Thread.currentThread().getName()+":释放锁完毕");
            }
        }else{
            System.out.println("线程"+Thread.currentThread().getName()+":尝试获取锁失败,其他线程持有锁");
        }
    }
    public static void main(String[] args) {
        LockTest lockTest = new LockTest();
        new Thread(()->{
            lockTest.lock();
        }).start();
        new Thread(()->{
            lockTest.lock();
        }).start();
    }
}

01600727466f4b8ebebbea1e93f4989e.jpg

  • tryLock(3000,TimeUtil.MILLISECONDS)尝试获取锁,获取不到就等待3s,如果3s后还是获取不到就返回false
public class LockTest {
    private Lock lock = new ReentrantLock();
    /**
     * 线程尝试获取线程锁,如果有其他线程占用,就返回false,无法拿到锁
     */
    public void lock() throws InterruptedException {
        if(lock.tryLock(3000,  TimeUnit.MILLISECONDS)){
            try {
                System.out.println("线程"+Thread.currentThread().getName()+":获取到锁资源");
                Thread.sleep(5000L);
            }catch (Exception e){
                System.out.println("线程"+Thread.currentThread().getName()+":释放锁发生异常");
            }finally {
                lock.unlock();
                System.out.println("线程"+Thread.currentThread().getName()+":释放锁完毕");
            }
        }else{
            System.out.println("线程"+Thread.currentThread().getName()+":尝试获取锁失败,其他线程持有锁");
        }
    }
    public static void main(String[] args) {
        LockTest lockTest = new LockTest();
        new Thread(()->{
            try {
                lockTest.lock();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }).start();
        new Thread(()->{
            try {
                lockTest.lock();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }).start();
    }
}

5c1eebc922c84f559624bb190c89d09c.jpg

3.自定义实现可重入锁

(1)可重入锁简介

  • 可重入锁,指的是以线程为单位,当一个线程获取对象锁之后,这个线程可以再次获取本对象上的锁,而其他线程是不可以的。
  • synchronized和ReetrantLock都是可重入锁。
  • 可重入锁的意义就在于防止死锁。
  • 实现原理是通过为每个锁关联一个请求计数器和一个占有它的线程。当计数为0时,认为锁是未被占有的,线程请求一个未被占用的锁时,JVM将记录锁的占有者,并且将请求计数器置为1。
  • 如果同一个线程再次请求这个锁,计数器将递增。
  • 每次占用线程退出同步块,计数器将递减,直到计数器为0,锁被释放。
  • 关于父类和子类的锁的重入:子类覆盖了父类的synchonized方法,然后调用父类中的方法,此时如果没有可重入的锁,那么这段代码将会产生死锁。

(2)伪代码案例

class A
public synchronized methodA(){
  methodB();
}
public synchronized methodB(){
}
#当线程调用A类的对象methodA同步方法,如果其他线程没有获取A类的对象锁,那么当前线程就获得点钱A类对象的锁,然后执行methodB同步方法,当前线程能够在次获取A类对象的锁,其他线程是不可以的,这就是可重入锁。

(3)自定义锁发生死锁问题

  • 自定义锁继承ReentrantLock类重写lock和unlock方法
public class MyLock extends ReentrantLock {
    //定义锁的标志位
    private boolean isHoldLock = false;
    /**
     * 同一时刻,能且仅能有一个线程获取到锁
     * 其他线程只能等待该线程释放锁之后才能获取到锁
     */
    @Override
    public synchronized void lock() {
        //判断是否有当前线程持有锁,如果有就进入等待
        if (isHoldLock) {
            try {
                wait();
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
        //将锁的标志置成true,表示当前线程持有锁
        isHoldLock = true;
    }
    @Override
    public synchronized void unlock() {
        //唤醒等待的线程
        notify();
        //释放锁资源
        isHoldLock = false;
    }
}
  • 定义ReentryDemo创建两个方法A、B其中在A方法中调用B方法
public class ReentryDemo {
    private Lock lock = new MyLock();
    public void methodA(){
        lock.lock();
        System.out.println("进入方法A");
        methodB();
        lock.unlock();
    }
    public void methodB(){
        lock.lock();
        System.out.println("进入方法B");
        lock.unlock();
    }
    public static void main(String[] args) {
        ReentryDemo reentryDemo = new ReentryDemo();
        reentryDemo.methodA();
    }
}
  • 上面这段代码看似没有问题,但其实已经造成了死锁。
  • 运行结果:

61c6a628412a4313a97731d0fd222f59.jpg

  • jconsole调出java管控台,查看当前运行的线程,发现并没有出现死锁的现象。


d52d029961054c3eaeba365c4a4ed608.jpg

  • 回到代码分析原因



27d13738e6d5468e9a45f693dd1abebf.jpg

  • 解决办法:将自定义的锁改成可重入锁,当同一线程再次访问的时候,允许再次获取锁。

(4)自定义可重入锁

  • 自定义锁继承ReentrantLock类重写lock和unlock方法
public class MyLock extends ReentrantLock {
    private boolean isHoldLock = false;
    //当前线程实例
    private Thread holdLockThread = null;
    //重入的次数
    private int reentryCount = 0;
    /**
     * 同一时刻,能且仅能有一个线程获取到锁
     * 其他线程只能等待该线程释放锁之后才能获取到锁
     */
    @Override
    public synchronized void lock() {
        //判断当前线程是否持有锁,并且是不是同一个线程重复进入
        if(isHoldLock && Thread.currentThread() != holdLockThread){
            try{
                wait();
            }catch (Exception e){
                e.printStackTrace();
            }
        }
        holdLockThread = Thread.currentThread();
        isHoldLock = true;
        //记录加锁的次数
        reentryCount++;
    }
    @Override
    public synchronized void unlock() {
        //判断当前线程是否是持有锁的线程,是,重入次数减1,不是就不做处理
        if(Thread.currentThread() == holdLockThread){
            reentryCount--;
            if(reentryCount == 0){
                notify();
                isHoldLock = false;
            }
        }
    }
}
  • 测试代码和上面相同,此处省略
  • 执行结果:

4a510cb9b7304208991fee33734e217e.jpg

  • 分析原因:e582598fa2b040588e304cda314709f0.jpg


e15e117a71384d5bbd8573bc780817fd.jpg

(5) 一个线程执行同步代码时,再次重入该锁过程中,如果抛出异常,会释放锁吗?

  • 注意:如果有涉及锁的操作的业务方法的时候,都要加上try-catch-finally,在finally中释放锁资源。
  • 锁不释放的案例


6e793456210c4056b94d6353b1c7e60b.jpg

public class ReentryDemo {
    private Lock lock = new MyLock();
    private int num = 0;
    public void methodA() throws InterruptedException {
        lock.lock();
        System.out.println("进入方法A");
        methodB();
        Thread.sleep(3000L);
        if (num == 0) {
            num++;
            int a = 1 / 0;
        }
        lock.unlock();
    }
    public void methodB() {
        lock.lock();
        System.out.println("进入方法B");
        lock.unlock();
    }
    public static void main(String[] args) {
        ReentryDemo reentryDemo = new ReentryDemo();
        new Thread(() -> {
            try {
                reentryDemo.methodA();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }).start();
        new Thread(() -> {
            try {
                reentryDemo.methodA();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }).start();
    }

386c962b53874453bc257dc3d009892e.jpg

  • 修改代码优化
public class ReentryDemo {
    private Lock lock = new MyLock();
    private int num = 0;
    public void methodA() {
        try {
            lock.lock();
            System.out.println("进入方法A");
            methodB();
            Thread.sleep(3000L);
            if (num == 0) {
                num++;
                int a = 1 / 0;
            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }
    public void methodB() {
        try {
            lock.lock();
            System.out.println("进入方法B");
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }
    public static void main(String[] args) {
        ReentryDemo reentryDemo = new ReentryDemo();
        new Thread(() -> {
            reentryDemo.methodA();
        }).start();
        new Thread(() -> {
            reentryDemo.methodA();
        }).start();
    }
}

e4b69d5db9174b02a20a4362b3b65ff7.jpg


相关文章
|
6月前
|
Java
Java并发编程中的锁机制
【2月更文挑战第22天】 在Java并发编程中,锁机制是一种重要的同步手段,用于保证多个线程在访问共享资源时的安全性。本文将介绍Java锁机制的基本概念、种类以及使用方法,帮助读者深入理解并发编程中的锁机制。
|
6月前
|
Java 程序员 开发者
深入理解Java并发编程:线程同步与锁机制
【4月更文挑战第30天】 在多线程的世界中,确保数据的一致性和线程间的有效通信是至关重要的。本文将深入探讨Java并发编程中的核心概念——线程同步与锁机制。我们将从基本的synchronized关键字开始,逐步过渡到更复杂的ReentrantLock类,并探讨它们如何帮助我们在多线程环境中保持数据完整性和避免常见的并发问题。文章还将通过示例代码,展示这些同步工具在实际开发中的应用,帮助读者构建对Java并发编程深层次的理解。
|
5月前
|
Java
并发编程的艺术:Java线程与锁机制探索
【6月更文挑战第21天】**并发编程的艺术:Java线程与锁机制探索** 在多核时代,掌握并发编程至关重要。本文探讨Java中线程创建(`Thread`或`Runnable`)、线程同步(`synchronized`关键字与`Lock`接口)及线程池(`ExecutorService`)的使用。同时,警惕并发问题,如死锁和饥饿,遵循最佳实践以确保应用的高效和健壮。
46 2
|
6月前
|
存储 安全 Java
深入理解Java并发编程:线程安全与锁机制
【5月更文挑战第31天】在Java并发编程中,线程安全和锁机制是两个核心概念。本文将深入探讨这两个概念,包括它们的定义、实现方式以及在实际开发中的应用。通过对线程安全和锁机制的深入理解,可以帮助我们更好地解决并发编程中的问题,提高程序的性能和稳定性。
|
2月前
|
Java 数据库
JAVA并发编程-一文看懂全部锁机制
曾几何时,面试官问:java都有哪些锁?小白,一脸无辜:用过的有synchronized,其他不清楚。面试官:回去等通知! 今天我们庖丁解牛说说,各种锁有什么区别、什么场景可以用,通俗直白的分析,让小白再也不怕面试官八股文拷打。
|
6月前
|
缓存 安全 Java
Java并发编程中的锁机制及其应用
传统的锁机制在Java并发编程中扮演着重要角色,但随着技术的发展,新的锁机制和应用不断涌现。本文将深入探讨Java并发编程中常用的锁机制,包括synchronized关键字、ReentrantLock、ReadWriteLock等,并结合实际案例分析其应用场景和优劣势。通过本文的阐述,读者将对Java并发编程中的锁机制有更为深入的了解。
57 0
|
2月前
|
安全 Java 开发者
Java并发编程中的锁机制解析
本文深入探讨了Java中用于管理多线程同步的关键工具——锁机制。通过分析synchronized关键字和ReentrantLock类等核心概念,揭示了它们在构建线程安全应用中的重要性。同时,文章还讨论了锁机制的高级特性,如公平性、类锁和对象锁的区别,以及锁的优化技术如锁粗化和锁消除。此外,指出了在高并发环境下锁竞争可能导致的问题,并提出了减少锁持有时间和使用无锁编程等策略来优化性能的建议。最后,强调了理解和正确使用Java锁机制对于开发高效、可靠并发应用程序的重要性。
27 3
|
3月前
|
Java C语言 C++
并发编程进阶:线程同步与互斥
并发编程进阶:线程同步与互斥
36 0
|
缓存 算法 安全
06.一文看懂并发编程中的锁
大家好,我是王有志。相信你经常会听到读锁/写锁,公平锁/非公平锁,乐观锁/悲观锁等五花八门的锁,那么每种锁有什么用呢?它们又有什么区别呢?今天我们就一起聊聊并发编程中的各种锁。
188 1
06.一文看懂并发编程中的锁
|
安全 编译器 C++
C++并发编程中的锁的介绍(二)
C++并发编程中的锁的介绍(二)
161 0