美文网首页
java锁机制

java锁机制

作者: s_j_x | 来源:发表于2018-08-26 17:09 被阅读0次

    场景

    当多个请求同时操作数据库的时候,首先将订单状态改为已支付,在对应的金额上加上200,在同事并发场景查询条件会造成重复通知,也就是会出现数据“脏读”的问题。

    排它锁

    引用百度百科里面的一句话
    事务T对数据对象A加上X锁,则只允许T读取和修改A,其他任何事务都不能再对A加任何类型的锁,直到T释放A上的锁。这就保证了其他事务在T释放A上的锁之前不能再读取和修改A。

    悲观锁与乐观锁

    悲观锁:之所以称悲观锁,就是该锁认为每次操作数据都会造成数据更新丢失的问题,每此查询时加上排它锁。
    每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候会上锁,这样别人想拿这个数据就会block直到它拿到锁。传统的关系型数据库里边就用到了很多这种锁机制,比如行锁,表锁,读锁,写锁等,都是在做操作之前先上锁。
    select * from xxx for update
    乐观锁: 乐观锁会认为每次查询都不会造成更新丢失,利用版本控制实现。
    通常实现是这样的:在表中的数据进行操作时(更新),先给数据表加一个版本(version)字段,每操作一次,将那条记录的版本号加1。也就是先查询出那条记录,获取出version字段,如果要对那条记录进行操作(更新),则先判断此刻version的值是否与刚刚查询出来时的version的值相等,如果相等,则说明这段期间,没有其他程序对其进行操作,则可以执行更新,将version字段的值加1;如果更新时发现此刻的version值与刚刚获取出来的version的值不相等,则说明这段期间已经有其他程序对其进行操作了,则不进行更新操作。

    举例:
    下单操作包括3步骤:
    1.查询出商品信息
    select (status,status,version) from t_goods where id=#{id}
    2.根据商品信息生成订单
    3.修改商品status为2(已支付)
    update t_goods
    set status=2,version=version+1
    where id=#{id} and version=#{version};

    重入锁

    锁作为并发共享数据,保证一致性的工具,在JAVA平台有多种实现(如 synchronized 和 ReentrantLock等等 ) 。这些已经写好提供的锁为我们开发提供了便利。
    重入锁,也叫做递归锁,指的是同一线程 外层函数获得锁之后 ,内层递归函数仍然有获取该锁的代码,但不受影响。
    在JAVA环境下 ReentrantLock 和synchronized 都是 可重入锁

    public class Test implements Runnable {
        public  synchronized void get() {
            System.out.println("name:" + Thread.currentThread().getName() + " get();");
            set();
        }
    
        public synchronized  void set() {
            System.out.println("name:" + Thread.currentThread().getName() + " set();");
        }
    
        @Override
    
        public void run() {
            get();
        }
    
        public static void main(String[] args) {
            Test ss = new Test();
            new Thread(ss).start();
            new Thread(ss).start();
            new Thread(ss).start();
            new Thread(ss).start();
        }
    }
    
    public class Test02 extends Thread {
        ReentrantLock lock = new ReentrantLock();
        public void get() {
            lock.lock();
            System.out.println(Thread.currentThread().getId());
            set();
            lock.unlock();
        }
        public void set() {
            lock.lock();
            System.out.println(Thread.currentThread().getId());
            lock.unlock();
        }
        @Override
        public void run() {
            get();
        }
        public static void main(String[] args) {
            Test ss = new Test();
            new Thread(ss).start();
            new Thread(ss).start();
            new Thread(ss).start();
        }
    }
    

    读写锁

    两个线程同时读一个资源没有任何问题,所以应该允许多个线程能在同时读取共享资源。但是如果有一个线程想去写这些共享资源,就不应该再有其它线程对该资源进行读或写 。也就是说:读-读能共存,读-写不能共存,写-写不能共)。这就需要一个读/写锁来解决这个问题。
    下面看一个不加读写锁会出现的情况

    public class WriteAndReadLock {
        static volatile Map<String, Object> map = new HashMap<>();
        public static void put(String key, Object value) {
            System.out.println("正在做写的操作,key:" + key + ",value:" + value + "开始.");
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            map.put(key, value);
            System.out.println("正在做写的操作,key:" + key + ",value:" + value + "结束.");
            System.out.println();
        }
        public static final Object get(String key) {
            System.out.println("正在做读的操作,key:" + key + " 开始");
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            Object object = map.get(key);
            System.out.println("正在做读的操作,key:" + key +"value:"+object+ " 结束");
            System.out.println();
            return object;
        }
    
        public static void main(String[] args) {
            new Thread(() -> {
                for (int i = 0; i < 10; i++) {
                    WriteAndReadLock.put(i + "", i + "");
                }
            }).start();
            new Thread(() -> {
                for (int i = 0; i < 10; i++) {
                    WriteAndReadLock.get(i + "");
                }
            }).start();
        }
    }
    

    运行结果:

    image.png

    从运行结果中可以明显看出来当在写key=8 的时候,还没有写完,这个时候读的线程便开始读。写线程在对数据进行写入的操作时,读线程对其进行了干扰,导致数据出现问题。
    下面看看通过加读写锁来解决这一问题。

    public class WriteAndReadLock {
        static volatile Map<String, Object> map = new HashMap<>();
        //读写锁
        static ReentrantReadWriteLock reentrantReadWriteLock = new ReentrantReadWriteLock();
        static Lock readLock = reentrantReadWriteLock.readLock();
        static Lock writeLock = reentrantReadWriteLock.writeLock();
    
        public static void put(String key, Object value) {
            writeLock.lock();
            try {
                System.out.println("正在做写的操作,key:" + key + ",value:" + value + "开始.");
                Thread.sleep(100);
                map.put(key, value);
                System.out.println("正在做写的操作,key:" + key + ",value:" + value + "结束.");
                System.out.println();
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                writeLock.unlock();
            }
        }
        public static final void get(String key) {
            readLock.lock();
            try {
                System.out.println("正在做读的操作,key:" + key + " 开始");
                Thread.sleep(100);
                Object object = map.get(key);
                System.out.println("正在做读的操作,key:" + key + "value:" + object + " 结束");
                System.out.println();
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                readLock.unlock();
            }
        }
        public static void main(String[] args) {
            new Thread(() -> {
                for (int i = 0; i < 10; i++) {
                    WriteAndReadLock.put(i + "", i + "");
                }
    
            }).start();
            new Thread(() -> {
                for (int i = 0; i < 10; i++) {
                    WriteAndReadLock.get(i + "");
                }
    
            }).start();
        }
    }
    

    结果


    image.png

    在上述代码中也可以使用synchronized 同步函数也能保证读写分离互不干扰。

    synchronized和ReentrantLock的对比
    到现在,看到多线程中,锁定的方式有2种:synchronized和ReentrantLock。两种锁定方式各有优劣,下面简单对比一下:
    1、synchronized是关键字,就和if...else...一样,是语法层面的实现,因此synchronized获取锁以及释放锁都是Java虚拟机帮助用户完成的;ReentrantLock是类层面的实现,因此锁的获取以及锁的释放都需要用户自己去操作。特别再次提醒,ReentrantLock在lock()完了,一定要手动unlock()
    2、synchronized简单,简单意味着不灵活,而ReentrantLock的锁机制给用户的使用提供了极大的灵活性。这点在Hashtable,ConcurrentHashMap中体现得淋漓尽致。synchronized一锁就锁整个Hash表,而ConcurrentHashMap则利用ReentrantLock实现了锁分离,锁的只是segment而不是整个Hash表
    3、synchronized是不公平锁,而ReentrantLock可以指定锁是公平的还是非公平的
    4、synchronized实现等待/通知机制通知的线程是随机的,ReentrantLock实现等待/通知机制可以有选择性地通知
    5、和synchronized相比,ReentrantLock提供给用户多种方法用于锁信息的获取,比如可以知道lock是否被当前线程获取、lock被同一个线程调用了几次、lock是否被任意线程获取等等
    总结起来,我认为如果只需要锁定简单的方法、简单的代码块,那么考虑使用synchronized,复杂的多线程处理场景下可以考虑使用ReentrantLock。当然这只是建议性地,还是要具体场景具体分析的。

    CAS无锁机制

    1)与锁相比,使用比较交换(下文简称CAS)会使程序看起来更加复杂一些。但由于其非阻塞性,它对死锁问题天生免疫,并且,线程间的相互影响也远远比基于锁的方式要小。更为重要的是,使用无锁的方式完全没有锁竞争带来的系统开销,也没有线程间频繁调度带来的开销,因此,它要比基于锁的方式拥有更优越的性能。
    (2)无锁的好处:
    第一,在高并发的情况下,它比有锁的程序拥有更好的性能;
    第二,它天生就是死锁免疫的。
    就凭借这两个优势,就值得我们冒险尝试使用无锁的并发。
    (3)CAS算法的过程是这样:它包含三个参数CAS(V,E,N): V表示要更新的变量,E表示预期值,N表示新值。仅当V值等于E值时,才会将V的值设为N,如果V值和E值不同,则说明已经有其他线程做了更新,则当前线程什么都不做。最后,CAS返回当前V的真实值。
    (4)CAS操作是抱着乐观的态度进行的,它总是认为自己可以成功完成操作。当多个线程同时使用CAS操作一个变量时,只有一个会胜出,并成功更新,其余均会失败。失败的线程不会被挂起,仅是被告知失败,并且允许再次尝试,当然也允许失败的线程放弃操作。基于这样的原理,CAS操作即使没有锁,也可以发现其他线程对当前线程的干扰,并进行恰当的处理。
    (5)简单地说,CAS需要你额外给出一个期望值,也就是你认为这个变量现在应该是什么样子的。如果变量不是你想象的那样,那说明它已经被别人修改过了。你就重新读取,再次尝试修改就好了。
    (6)在硬件层面,大部分的现代处理器都已经支持原子化的CAS指令。在JDK 5.0以后,虚拟机便可以使用这个指令来实现并发操作和并发数据结构,并且,这种操作在虚拟机中可以说是无处不在。


    image.png

    在jdk1.5并发包中的AtomicInteger 这个类底层就是用的CAS无所机制来保证原子性的。

        /** 
         * 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;  
            }  
        }  
    

    自旋锁

    自旋锁是一种用于保护多线程共享资源的锁,与一般互斥锁(mutex)不同之处在于当自旋锁尝试获取锁时以忙等待(busy waiting)的形式不断地循环检查锁是否可用。
    在多CPU的环境中,对持有锁较短的程序来说,使用自旋锁代替一般的互斥锁往往能够提高程序的性能

    自旋锁和互斥锁的区别

    下该段内容引自http://blog.chinaunix.net/uid-28711483-id-4995776.html
    从实现原理上来讲,Mutex属于sleep-waiting类型的 锁。例如在一个双核的机器上有两个线程(线程A和线程B),它们分别运行在Core0和Core1上。假设线程A想要通过 pthread_mutex_lock操作去得到一个临界区的锁,而此时这个锁正被线程B所持有,那么线程A就会被阻塞(blocking),Core0 会在此时进行上下文切换(Context Switch)将线程A置于等待队列中,此时Core0就可以运行其他的任务(例如另一个线程C)而不必进行忙等待。而Spin lock则不然,它属于busy-waiting类型的锁,如果线程A是使用pthread_spin_lock操作去请求锁,那么线程A就会一直在 Core0上进行忙等待并不停的进行锁请求,直到得到这个锁为止。
    如果大家去查阅Linux glibc中对pthreads API的实现NPTL(Native POSIX Thread Library) 的源码的话(使用”getconf GNU_LIBPTHREAD_VERSION”命令可以得到我们系统中NPTL的版本号),就会发现pthread_mutex_lock()操作如果 没有锁成功的话就会调用system_wait()的系统调用并将当前线程加入该mutex的等待队列里。而spin lock则可以理解为在一个while(1)循环中用内嵌的汇编代码实现的锁操作(印象中看过一篇论文介绍说在linux内核中spin lock操作只需要两条CPU指令,解锁操作只用一条指令就可以完成)。有兴趣的朋友可以参考另一个名为sanos的微内核中pthreds API的实现:mutex.c spinlock.c,尽管与NPTL中的代码实现不尽相同,但是因为它的实现非常简单易懂,对我们理解spin lock和mutex的特性还是很有帮助的。
    对于自旋锁来说,它只需要消耗很少的资源来建立锁;随后当线程被阻塞时,它就会一直重复检查看锁是否可用了,也就是说当自旋锁处于等待状态时它会一直消耗CPU时间。
    对于互斥锁来说,与自旋锁相比它需要消耗大量的系统资源来建立锁;随后当线程被阻塞时,线程的调度状态被修改,并且线程被加入等待线程队列;最后当锁可用 时,在获取锁之前,线程会被从等待队列取出并更改其调度状态;但是在线程被阻塞期间,它不消耗CPU资源。
    因此自旋锁和互斥锁适用于不同的场景。自旋锁适用于那些仅需要阻塞很短时间的场景,而互斥锁适用于那些可能会阻塞很长时间的场景。

    分布式锁

    如果想在不同的jvm中保证数据同步,使用分布式锁技术。
    有数据库实现、缓存实现、Zookeeper分布式锁、redis分布式锁等

    相关文章

      网友评论

          本文标题:java锁机制

          本文链接:https://www.haomeiwen.com/subject/zvzniftx.html