zoukankan      html  css  js  c++  java
  • Java深入学习(5):锁

    可重入锁:

    简单来说,支持重复加锁,有可重用性

    特征:锁可以传递,方法递归传递

    目的:避免了死锁现象

    代码:

    public class Test implements Runnable {
    
        @Override
        public void run() {
            method1();
        }
    
        public synchronized void method1() {
            System.out.println("method1");
            method2();
        }
    
        public synchronized void method2() {
            System.out.println("method2");
        }
    
        public static void main(String[] args) {
            new Thread(new Test()).start();
        }
    
    }

    打印:

    method1
    method2

    分析:如果锁不能重用,那么这里将会出现死锁问题

    使用ReentrantLock锁:

    public class TestLock implements Runnable {
        //重入锁
        private Lock reentrantLock = new ReentrantLock();
    
        @Override
        public void run() {
            method1();
        }
    
        public void method1() {
            try {
                reentrantLock.lock();
                System.out.println("method1");
                method2();
            } catch (Exception e) {
                e.printStackTrace();
            } finally {
                reentrantLock.unlock();
            }
        }
    
        public void method2() {
            try {
                reentrantLock.lock();
                System.out.println("method2");
            } catch (Exception e) {
                e.printStackTrace();
            } finally {
                reentrantLock.unlock();
            }
        }
    
        public static void main(String[] args) {
            new Thread(new TestLock()).start();
        }
    
    }

    读写锁:

    高并发的时候,写操作的同时应当不允许读操作

    (多线程中:读-读共存;读-写、写-写都不可以共存)

    代码:制造读写操作的线程安全问题

    public class TestWriteLock {
    
        Map<String, String> cache = new HashMap<>();
    
        //写入元素
        public void put(String key, String value) {
            try {
                System.out.println("开始写入key : " + key + " value : " + value);
                Thread.sleep(50);
                cache.put(key, value);
                System.out.println("完成写入key : " + key + " value : " + value);
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    
        //读取元素
        public String get(String key) {
            System.out.println("开始读取key : " + key);
            String value = cache.get(key);
            System.out.println("读取成功key : " + key + " value : " + value);
            return value;
        }
    
        public static void main(String[] args) {
            TestWriteLock test = new TestWriteLock();
            Thread readThread = new Thread(new Runnable() {
                @Override
                public void run() {
                    for (int i = 0; i < 10; i++) {
                        test.put("i", i + "");
                    }
                }
            });
    
            Thread writeThread = new Thread(new Runnable() {
                @Override
                public void run() {
                    for (int i = 0; i < 10; i++) {
                        test.get("i");
                    }
                }
            });
    
            readThread.start();
            writeThread.start();
        }
    }

    观察打印:发现不合理

    开始写入key : i value : 0
    开始读取key : i
    读取成功key : i value : null
    .................................

    分析:在没有写入完成的时候,就开始了读取,得到的结果为空

    解决:

    1.使用synchronized,虽然可以解决,但是效率低下,写操作同时不能读,产生阻塞

    2.使用读写锁

    public class TestWriteLock {
    
        Map<String, String> cache = new HashMap<>();
    
        ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
        ReentrantReadWriteLock.WriteLock writeLock = lock.writeLock();
        ReentrantReadWriteLock.ReadLock readLock = lock.readLock();
    
        //写入元素
        public void put(String key, String value) {
            try {
                writeLock.lock();
                System.out.println("开始写入key : " + key + " value : " + value);
                Thread.sleep(50);
                cache.put(key, value);
                System.out.println("完成写入key : " + key + " value : " + value);
            } catch (Exception e) {
                e.printStackTrace();
            } finally {
                writeLock.unlock();
            }
        }
    
        //读取元素
        public String get(String key) {
            String value = "";
            try {
                readLock.lock();
                System.out.println("开始读取key : " + key);
                value = cache.get(key);
                System.out.println("读取成功key : " + key + " value : " + value);
            } catch (Exception e) {
                e.printStackTrace();
            } finally {
                readLock.unlock();
            }
            return value;
        }
    
        public static void main(String[] args) {
            TestWriteLock test = new TestWriteLock();
            Thread readThread = new Thread(new Runnable() {
                @Override
                public void run() {
                    for (int i = 0; i < 10; i++) {
                        test.put("i", i + "");
                    }
                }
            });
    
            Thread writeThread = new Thread(new Runnable() {
                @Override
                public void run() {
                    for (int i = 0; i < 10; i++) {
                        test.get("i");
                    }
                }
            });
            readThread.start();
            writeThread.start();
        }
    }

    观察打印:完美解决

    乐观锁:

    简单来讲,乐观锁就是没有锁,无阻塞无等待

    一条SQL语句做示范:

    UPDATE TABLE SET X=X+1,VERSION=VERSION+1 WHERE ID=#{id} AND VERSION=#{version}

     在高并发地情况下,假设初始version是1,请求1到来,根据id和version能查到,所以允许更新

     请求2同时做操作,但是根据id和version已经查不到了(被请求1修改了),所以不允许更新

    悲观锁:

    简单来讲,重量级锁, 会阻塞,会进行等待

    可以理解为上锁之后只允许一个线程来操作,也就是Java中的synchronized

    原子类:

    一段模拟线程安全问题的代码:

    public class ThreadTest implements Runnable {
    
        private static int count = 1;
    
        @Override
        public void run() {
            while (true) {
                Integer count = getCount();
                if (count >= 100) {
                    break;
                }
                System.out.println(count);
            }
        }
    
        public synchronized Integer getCount() {
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            return count++;
        }
    
        public static void main(String[] args) {
            ThreadTest t = new ThreadTest();
            new Thread(t).start();
            new Thread(t).start();
        }
    
    }

    观察打印后发现果然出现了线程安全问题

    一种修改方式:效率较低

        public synchronized Integer getCount() {

    使用原子类:乐观锁,底层没有加锁,使用CAS无锁技术

    public class ThreadTest implements Runnable {
    
        // 线程安全
        private AtomicInteger atomicInteger = new AtomicInteger();
    
        @Override
        public void run() {
            while (true) {
                Integer count = getCount();
                if (count >= 100) {
                    break;
                }
                System.out.println(count);
            }
        }
    
        public Integer getCount() {
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            return atomicInteger.incrementAndGet();
        }
    
        public static void main(String[] args) {
            ThreadTest t = new ThreadTest(); 
            new Thread(t).start();
            new Thread(t).start();
        }
    
    }

    CAS无锁技术(Compare And Swap)

    翻译过来为:比较再交换

    本地内存中存放共享内存的副本

    比如主内存中有i=0,复制到两个线程的本地内存中

    两个线程执行了i++,本地内存都变成i=1,然后刷新入主内存

    CAS算法:

    它包含三个参数CAS(V,E,N):

    V表示要更新的变量(主内存)

    E表示预期值(本地内存)

    N表示新值(新值)

    仅当V值等于E值时(主内存=本地内存),才会将V的值设为N

    如果V值和E值不同(主内存!=本地内存),则说明已经有其他线程做了更新,则当前线程什么都不做

    最后,CAS返回当前V的真实值。

    观察原子类的源码:

        /** 
         * Atomically increments by one the current value. 
         * 
         * @return the updated value 
         */  
        public final int incrementAndGet() {  
            for (;;) {  
                //获取当前值  
                int current = get();  
                //设置期望值  
                int next = current + 1;  
                //调用Native方法compareAndSet,执行CAS操作  
                if (compareAndSet(current, next))  
                    //成功后才会返回期望值,否则无线循环  
                    return next;  
            }  
        }  

    CAS无锁机制的缺点:

    1.死循环

    2.ABA问题:如果变量V初次读取的时候是A,并且在准备赋值的时候检查到它仍然是A,那能说明它的值没有被其他线程修改过了吗

    (如果在这段期间曾经被改成B,然后又改回A,那CAS操作就会误认为它从来没有被修改过。针对这种情况,java并发包中提供了一个带有标记的原子引用类AtomicStampedReference,它可以通过控制变量值的版本来保证CAS的正确性。)

    JVM数据同步:采用分布式锁

    自旋锁和互斥锁的区别:

    悲观和乐观锁的区别,自旋锁是死循环不会阻塞,互斥锁是同一时间只有一个线程访问数据

  • 相关阅读:
    ASP.NET MVC中多种ActionResult用法总结
    jQuery中异步操作对象Deferred
    jQuery中bind方法和live方法区别解析
    深入理解Javascript中this, prototype, constructor
    SQL及常见的三种类型注释
    SQLServer的两个日期相减(间隔)datediff函数
    SQLServer查询进程与死锁语句
    SqlServer获取当前日期的详细写法
    SQL中 Decode 和 Sign 语法的简单用法
    数据仓库模型之CDM、LDM与PDM的区别
  • 原文地址:https://www.cnblogs.com/xuyiqing/p/11635786.html
Copyright © 2011-2022 走看看