• Java面试——锁


    优质博文IT-BLOG-CN

    公平锁: 是指多个线程按照申请锁的顺序来获取锁,有点先来后到的意思。在并发环境中,每个线程在获取锁时会先查看此锁维护的队列,如果为空,或者当前线程是等待队列的第一个,就占有锁,否则就会加入到等待队列中,以后会按照 FIFO 的规则从队列中取到自己。

    非公平锁: 指多个线程获取锁的顺序并不是按照申请锁的顺序,上来就尝试占有锁,如果尝试失败,就再采用类似公平锁的方式获取锁。有可能后申请的线程比先申请的线程优先获取锁,在高并发的情况下,有可能会造成优先级反转或者饥饿现象。

    ReentrantLock:并发包中ReentrantLock的创建可以指定构造函数的boolean类型来得到公平锁或非公平锁,默认是false(非公平锁)。非公平的优点在于吞吐量比公平锁大。对于Synchronized锁也是一种非公平锁。

    可重入锁(又名递归锁): 指同一线程外层函数获得锁之后,内层递归函数仍然能获取该锁的代码。也就是说,线程可以进入任何一个它已经拥有的锁,所同步的代码块。synchronizedunlock都是可重入锁。

    //简单理解,就是方法1 是一个同步方法,里面包含了一个方法2 也是同步方法,但是当进入方法1后,也就获得了方法2的锁,即可重入锁
    public synchronized void method1(){
    	System.out.println("方法1 synchronized");
    	method2();
    }
    
    public synchronized  void method2(){
    	System.out.printf("方法2 synchronized");
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    自旋锁: 是指尝试获取锁的线程不会立即阻塞,而是采用循环的方式尝试获取锁,这样的好处是减少了上下文切换的消耗,确定是循环会消耗CPU。循环比较直到成功为止。

    public final int getAndAddInt(Object var1, long var2, int var4){
        int var5;
        do{
            //根据对象和地址偏移量获取内存中的值
            var5 = this.getIntVolatile(var1, var2);
        //将获取到的值 var5 传入,此方法内部会先比较var2地址的值是否等于 var5,相等则修改var5值并返回,否则重新进入循环。
        }while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));
            return var5;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    手写一个自旋锁: 思想就是通过while中的循环条件来充当锁,当条件成立时,表示未获得锁,进行死循环,直到while条件不成立,也就是获得锁。就退出死循环,执行业务逻辑。具体查看如下代码:

    public class Test {
    	public static void main(String[] args) throws Exception {
    		/*执行结果展示:  AA   myLock
    						BB   myLock
    						AA    unLock
    						BB    unLock
    		 *  分析:我们 AA 线程休眠了 5秒足以让 BB 线程执行结束,那为什么 BB 执行到 myLock 之后就没有继续执行呢。
    		 *  其实,BB 一直执行着,只不过陷入了 while 死循环中,因为 AA 将线程置为非空。
    		 *  等到 5 秒后,AA unlock 重新将线程=null时,BB 获取线程并执行任务。over
    		 */
    		Test test = new Test();
    		new Thread(()->{
    				test.myLock();
    				try {
    					TimeUnit.SECONDS.sleep(5);
    				} catch (InterruptedException e) {
    					e.printStackTrace();
    				}
    				test.unLock();
    			},"AA").start();
    
    			TimeUnit.SECONDS.sleep(1);
    
    			new Thread(()->{
    				test.myLock();
    				try {
    					TimeUnit.SECONDS.sleep(1);
    				} catch (InterruptedException e) {
    					e.printStackTrace();
    				}
    				test.unLock();
    			},"BB").start();
    	}
    
    	//对线程保证原子性
    	AtomicReference<Thread> atomicReference = new AtomicReference<>();
    	//获取锁,其实质,将锁看做一个条件判断,只要这个判断能够保证线程安全即可。
    	//如下:我们将线程是否为空作为条件,如果是空的就没锁,自己可以对其加锁,将其值设为自己。
    	//如果使用完,使用unlock 将线程设置为 null,其他线程通过判断来获得锁,其实就像一种约定而已。
    	public void myLock(){
    		Thread thread = Thread.currentThread();
    		System.out.println(thread.getName()+"   myLock");
    		while (!atomicReference.compareAndSet(null,thread)){
    		   
    		}
    	}
    	
    	//释放锁
    	public void unLock(){
    		Thread thread = Thread.currentThread();
    		atomicReference.compareAndSet(thread,null);
    		System.out.println(thread.getName()+"    unLock");
    	}
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54

    自旋锁的优点主要包括:
    【1】减少线程阻塞:对于锁竞争不激烈且锁占用时间短暂的情况,自旋锁能够显著提高性能,因为它减少了线程因阻塞而产生的上下文切换开销。
    【2】避免内核态切换:与非自旋锁相比,自旋锁在尝试获取锁失败时会继续执行循环而不立即陷入内核态,这样可以避免线程在用户态和内核态之间的频繁切换,这在一定程度上提高了系统的整体性能。

    然而,自旋锁也存在一些缺点:
    【1】高负载下效率低下:如果锁竞争激烈或持有锁的线程需要长时间执行同步块,自旋锁会因为不断重复无效的旋转操作而导致性能下降。在这种情况下,自旋锁的消耗可能会超过线程阻塞后的恢复成本,因此应该关闭自旋锁以避免不必要的性能损失。1234
    【2】可能存在不公平性:某些自旋锁实现(如Java中的)不是完全公平的,这意味着它们可能无法为等待时间最长线程提供优先权,这可能导致所谓的“线程饥饿”问题。
    【3】单核处理器上的限制:在单核处理器上,自旋锁实际上没有真正的并行性,因为即使当前线程不阻塞其他线程,锁仍然不会被释放,导致资源的浪费。此外,如果处理器数量少于线程数量,自旋锁也可能造成不必要的资源浪费。4
    【4】不适合计算密集型任务:如果任务主要是计算密集型的,使用自旋锁可能会导致性能下降,因为自旋锁会占用CPU资源,而在计算密集型任务中,减少锁的使用可能是更优的选择。

    综上所述,自旋锁适用于锁竞争不太激烈且锁占用时间较短的场景,但在竞争激烈或锁占用时间较长的情况下,其性能优势不明显,甚至可能导致性能下降。

    【独占锁】(写锁): 指该锁只能被一个线程所持有。对ReentrantLockSynchronized而言都是独占锁。
    【共享锁】(读锁): 指该锁可被多个线程持有。

    【1】不加读写锁时,代码及出现的问题如下:创建5个线程进行写入,5个线程进行读取。

    public class ReadWriteLock {
        private volatile Map map = new HashMap();
    
        //写入方法
        public void put(String k,Object v){
            System.out.println(Thread.currentThread().getName()+"   开始写入:"+k );
            try {
                TimeUnit.MICROSECONDS.sleep(30);
            }catch (Exception e){
                e.printStackTrace();
            }
            map.put(k,v);
            System.out.println(Thread.currentThread().getName()+"   写入完成");
        }
    
        //读方法
        public void get(String k){
            System.out.println(Thread.currentThread().getName()+"   读数据开始:"+k );
            try {
                TimeUnit.MICROSECONDS.sleep(10);
            }catch (Exception e){
                e.printStackTrace();
            }
            Object v = map.get(k);
            System.out.println(Thread.currentThread().getName()+"   读数据完场完成"+v);
        }
    
        public static void main(String[] args) {
            ReadWriteLock readWriteLock = new ReadWriteLock();
    
            //写入数据
            for (int i=1;i<6;i++){
                final int tempInt = i;
                new Thread(()->{
                    readWriteLock.put(tempInt+"",tempInt+"");
                },String.valueOf(i)).start();
            }
    
            //读取数据
            for(int i=1;i<6;i++){
                final int tempInt = i;
                new Thread(()->{
                    readWriteLock.get(tempInt+"");
                },String.valueOf(i)).start();
            }
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47

    【2】上述代码输出如下:第一个线程未写入完成时,其他线程就进入了该方法,进行了写操作。不符合多线程安全特性。

    在这里插入图片描述

    【3】加入读写锁:ReentrantReadWriteLock(读写锁)位于JUC包下

    public class ReadWriteLock{
        private volatile Map map = new HashMap();
        private ReentrantReadWriteLock rwLock = new ReentrantReadWriteLock();
        //写入方法
        public void put(String k,Object v){
            rwLock.writeLock().lock();
            try {
                System.out.println(Thread.currentThread().getName()+"   开始写入:"+k );
                try {
                    TimeUnit.MICROSECONDS.sleep(30);
                }catch (Exception e){
                    e.printStackTrace();
                }
                map.put(k,v);
                System.out.println(Thread.currentThread().getName()+"   写入完成");
            }catch (Exception e){
                e.printStackTrace();
            }finally {
                rwLock.writeLock().unlock();
            }
        }
    
        //读方法
        public void get(String k){
            rwLock.readLock().lock();
            try {
                System.out.println(Thread.currentThread().getName()+"   读数据开始:"+k );
                try {
                    TimeUnit.MICROSECONDS.sleep(10);
                }catch (Exception e){
                    e.printStackTrace();
                }
                Object v = map.get(k);
                System.out.println(Thread.currentThread().getName()+"   读数据完场完成"+v);
            } catch (Exception e) {
                e.printStackTrace();
            } finally {
                rwLock.readLock().unlock();
            }
        }
    
        public static void main(String[] args) {
            ReadWriteLock readWriteLock = new ReadWriteLock();
            //写入数据
            for (int i=1;i<6;i++){
                final int tempInt = i;
                new Thread(()->{
                    readWriteLock.put(tempInt+"",tempInt+"");
                },String.valueOf(i)).start();
            }
    
            //读取数据
            for(int i=1;i<6;i++){
                final int tempInt = i;
                new Thread(()->{
                    readWriteLock.get(tempInt+"");
                },String.valueOf(i)).start();
            }
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60

    【4】加入读写锁后,输出如下:
    在这里插入图片描述

  • 相关阅读:
    logback通过EvaluatorFilter实现同时记录多个level级别的日志
    MySQL (2) DQL
    VMWare16的安装及VMware配置Ubuntu虚拟机
    el-form重置后input无法输入问题
    TikTok与媒体素养:如何辨别虚假信息?
    鸿蒙打包hvigorw clean报错No npmrc file is matched in the current user folder解决
    No module named ‘PyQt5.QtWebEngineWidgets‘kn-----已解决
    远程服务器配置 Anaconda 并安装 PyTorch 详细教程
    六大核心原则,引领自动化正确实施
    一键让你使用录屏
  • 原文地址:https://blog.csdn.net/zhengzhaoyang122/article/details/136151332