zoukankan      html  css  js  c++  java
  • 线程锁相关概念学习,锁消除,锁粗化

    线程锁

    1. 自旋锁:为了不放弃CPU执行事件,循环的使用CAS技术对数据尝试进行更新,直到成功。
    2. 悲观锁:假定会发生并发冲突,同步所有对数据的相关操作,从数据就开始上锁。
    3. 乐观锁:假定没有冲突,在修改数据时如果数据发现和之前获取的不一致,则读最新数据,修改后重新修改。
    4. 独享锁(写):给资源加上写锁,线程可以修改资源,其他线程不能再锁;(单写)。
    5. 共享锁(读):给资源加上读锁只能读不能改,其他线程也只能加读锁,不能加写锁;(多读)。 
    6. 可重入锁、不可重入锁:线程拿到一把锁之后,可以自由进入同一把锁同步的其他代码。
    7. 公平锁、非公平锁:争抢锁的顺序,如果是按先来后到,则为公平。
    8. synchronized同步锁

    偏向锁、轻量级锁、重量级锁

    代码实现


     可重入锁

    线程拿到一把锁之后,可以自由进入同一把锁同步的其他代码。

    // 可重入
    public class ObjectSyncDemo2 {
    
        public synchronized void test1(Object arg) {
            System.out.println(Thread.currentThread() + " 我开始执行 " + arg);
            if (arg == null) {
                test1(new Object());//这里再调用方法,使用同步锁
            }
            System.out.println(Thread.currentThread() + " 我执行结束" + arg);
        }
    
        public static void main(String[] args) throws InterruptedException {
            new ObjectSyncDemo2().test1(null);
        }
    }
    

     锁粗化

    在编译器编译优化过程中,多个相同的同步锁,可以粗化为一个同步锁

     锁消除

    在jit编译器编译优化过程中,判断没有线程安全的问题,会消除没必要使用的锁

    // 锁消除(jit)
    public class ObjectSyncDemo4 {
        public void test3(Object arg) {
            StringBuilder builder = new StringBuilder();
            builder.append("a");
            builder.append(arg);
            builder.append("c");
            System.out.println(arg.toString());
        }
    
        public void test2(Object arg) {
            String a = "a";
            String c = "c";
            System.out.println(a + arg + c);
        }
    
    
        public void test1(Object arg) {
            // jit 优化, 消除了锁
            StringBuffer stringBuffer = new StringBuffer();
            stringBuffer.append("a");
            stringBuffer.append(arg);
            stringBuffer.append("c");
            // System.out.println(stringBuffer.toString());
        }
    
        public static void main(String[] args) throws InterruptedException {
            for (int i = 0; i < 1000000; i++) {
                new ObjectSyncDemo4().test1("123");
            }
        }
    }
    

        stringBuffer.append(arg)方法是线程安全的方法。

     

    使用jitwatch可视化工具可以看到优化前的

    优化后  

    偏向锁、轻量级锁、重量级锁

       对象会有一个对象头的信息,其中一个名叫mark work,用于记录锁的信息

    轻量级加锁

    如果线程加锁成功,对象记录线程地址,状态转换为锁定状态

    线程记录锁的信息

    后续线程来拿锁的时候,需要CAS比对替换来判断

    偏向锁

    偏向锁,本质为无锁,如果没有发生过多线程争抢的情况,JVM认为就是单线程,无需做同步。

    对象创建时,对象头信息中专门状态位0和1来判断是否开启偏向锁。默认是0,当有线程来拿到锁的时候,mark word记录线程ID,偏向锁状态改为0,此时线程拿到锁。

    后续线程来拿锁的时候,只需要进行线程id的比对就判断是否是原先拿到偏向锁的线程,如果是无需再次申请,如果不是,存在争抢锁的情况,升级为轻量级锁。

    偏向锁升级到轻量级锁   

     

     重量级锁-监视器(monitor)

    修改mark word如果失败,会自选CAS一定次数,改次数可以通过参数配置,超过一定次数,仍未抢到锁,则锁升级为重量级锁,进入阻塞。

    monitor也叫管程

     锁使用相关代码

    lock的代码相关api

    reentrantLock(可重入锁)相关api

            lock.lock();  // block until condition holds
            try {
                System.out.println("第一次获取锁");
                System.out.println("当前线程获取锁的次数" + lock.getHoldCount());
                lock.lock();
                System.out.println("第二次获取锁了");
                System.out.println("当前线程获取锁的次数" + lock.getHoldCount());
            } finally {
                lock.unlock();
                lock.unlock();
            }
            System.out.println("当前线程获取锁的次数" + lock.getHoldCount());
    
            // 如果不释放,此时其他线程是拿不到锁的
            new Thread(() -> {
                System.out.println(Thread.currentThread() + " 期望抢到锁");
                lock.lock();
                System.out.println(Thread.currentThread() + " 线程拿到了锁");
            }).start();

    ReadWriteLock(读写锁)

      维护一对读锁和写锁,可以多个线程同时去读,只有单个线程去写,两者不能同时进行,即,写锁是线程独占,读锁是共享

    适合读取线程比写入线程多的场景,示例场景:缓存组件。集合并发线程安全性改造

    锁降级指的是写锁降级成为读锁。把持住当前拥有的写锁的同时,再获取到读锁,随后释放锁的过程。

    缓存示例

    // 缓存示例
    public class CacheDataDemo {
        // 创建一个map用于缓存
        private Map<String, Object> map = new HashMap<>();
        private static ReadWriteLock rwl = new ReentrantReadWriteLock();
    
        public static void main(String[] args) {
            // 1 读取缓存里面的数据
            // cache.query()
            // 2 如果换成没数据,则取数据库里面查询  database.query()
            // 3 查询完成之后,数据塞到塞到缓存里面 cache.put(data)
        }
    
        public Object get(String id) {
            Object value = null;
            // 首先开启读锁,从缓存中去取
            rwl.readLock().lock();
            try {
                if (map.get(id) == null) {
                    // TODO database.query();  全部查询数据库 ,缓存雪崩
                    // 必须释放读锁
                    rwl.readLock().unlock();
                    // 如果缓存中没有释放读锁,上写锁。如果不加锁,所有请求全部去查询数据库,就崩溃了
                    rwl.writeLock().lock(); // 所有线程在此处等待  1000  1  999 (在同步代码里面再次检查是否缓存)
                    try {
                        // 双重检查,防止已经有线程改变了当前的值,从而出现重复处理的情况
                        if (map.get(id) == null) {
                            // TODO value = ...如果缓存没有,就去数据库里面读取
                        }
                        rwl.readLock().lock(); // 加读锁降级写锁,这样就不会有其他线程能够改这个值,保证了数据一致性
                    } finally {
                        rwl.writeLock().unlock(); // 释放写锁@
                    }
                }
            } finally {
                rwl.readLock().unlock();
            }
            return value;
        }
    }

    Condition

    用来替代wait/notify

    Object中wait(),notify(),notifyAll()方法是和synchronized配合使用,可以唤醒一个或者全部(单个等待集)

    Condition是需要与Lock配合使用,提供多个等待集合,更精确的控制(底层是park/unpark机制)

    代码理解

    // condition 实现队列线程安全。
    public class QueueDemo {
        final Lock lock = new ReentrantLock();
        // 指定条件的等待 - 等待有空位
        final Condition notFull = lock.newCondition();
        // 指定条件的等待 - 等待不为空
        final Condition notEmpty = lock.newCondition();
    
        // 定义数组存储数据
        final Object[] items = new Object[100];
        int putptr, takeptr, count;
    
        // 写入数据的线程,写入进来
        public void put(Object x) throws InterruptedException {
            lock.lock();
            try {
                while (count == items.length) // 数据写满了
                    notFull.await(); // 写入数据的线程,进入阻塞
                items[putptr] = x;
                if (++putptr == items.length) putptr = 0;
                ++count;
                notEmpty.signal(); // 唤醒指定的读取线程
            } finally {
                lock.unlock();
            }
        }
        // 读取数据的线程,调用take
        public Object take() throws InterruptedException {
            lock.lock();
            try {
                while (count == 0)
                    notEmpty.await(); // 线程阻塞在这里,等待被唤醒
                Object x = items[takeptr];
                if (++takeptr == items.length) takeptr = 0;
                --count;
                notFull.signal(); // 通知写入数据的线程,告诉他们取走了数据,继续写入
                return x;
            } finally {
                lock.unlock();
            }
        }
    }
    

      

     以上部分内容图片摘自网易云课堂

  • 相关阅读:
    min25筛学习笔记
    【模板】回滚莫队&不删除莫队
    UOJ#188. 【UR #13】Sanrd
    LOJ#572. 「LibreOJ Round #11」Misaka Network 与求和
    Product
    Lcm
    点是否在三角形内的判断
    今天学会了 在Linux下,用GCC编译C语言程序,mark下
    让你沉迷的五种设计
    搞清楚了自发光特效的制作原理,3张图,3个步骤
  • 原文地址:https://www.cnblogs.com/shuzhixia/p/13358850.html
Copyright © 2011-2022 走看看