Java并发编程-8.锁

重入锁

概念

  • 重入锁,也叫做递归锁(避免在传递中产生死锁现象),指的是同一线程外层函数获得锁之后 ,内层递归函数仍然有获取该锁的代码,但不受影响
  • 重入锁有多种实现(如 synchronized(重量级) 和 ReentrantLock(轻量级)等等 ) ,这些已经写好提供的锁为我们开发提供了便利

样例

  • synchronized
    public class SynchronizedDemo implements Runnable {
    
        @Override
        public void run() {
            set();
        }
    
        public synchronized void get() {
            System.out.println("get方法");
        }
    
        public synchronized void set() {
            System.out.println("set方法");
            get();
        }
    
        public static void main(String[] args) {
            new Thread(new SynchronizedDemo()).start();
        }
    }
    
  • ReentrantLock
    public class ReentrantLockDemo implements Runnable {
        private ReentrantLock lock = new ReentrantLock();
    
        @Override
        public void run() {
            set();
        }
    
        public void get() {
            try {
                lock.lock();
                System.out.println("get方法");
    
            } catch (Exception ex) {
    
            } finally {
                lock.unlock();
            }
        }
    
        public void set() {
            try {
                lock.lock();
                System.out.println("set方法");
            } catch (Exception ex) {
    
            } finally {
                lock.unlock();
            }
            get();
        }
    
        public static void main(String[] args) {
            new Thread(new ReentrantLockDemo()).start();
        }
    }
    
    由上面的synchronized和ReentrantLock 锁的demo中,get和set方法都加锁了,执行set方法的时候,当执行到get方法时get方法也加了锁,get与set都属用的同一把锁,执行get方法时,set方法并没有释放锁,但是get方法并没有去重新获取锁资源,由于可以看出set方法将锁资源传递给了get方法,才可以正常执行。如果synchronized和ReentrantLock锁不具备重入性的话,则执行get方法时会重新获取锁资源,但是set方法并没有释放锁资源,则get与set方法会产生死锁情况,代码不能够正常执行。

读写锁

概念

ReadWriteLock同Lock一样也是一个接口,提供了readLock和writeLock两种锁的操作机制,一个是只读的锁,一个是写锁

  • 两个线程同时读一个资源没有任何问题,所以应该允许多个线程能在同时读取共享资源。但是如果有一个线程想去写这些共享资源,就不应该再有其它线程对该资源进行读或写

互斥原则

  • 读-读能共存
  • 读-写不能共存
  • 写-写不能共存

样例

模拟jvm内置缓存(无锁)

  • 代码样例
    public class ReadWriteLockDemo {
    
        private volatile Map<String, String> cache = new HashMap<>();
    
        public void put(String key, String value) {
            System.out.println("put开始,key=" + key);
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
            }
            cache.put(key, value);
            System.out.println("put结束,key=" + key);
        }
    
        public String get(String key) {
            System.out.println("get开始,key=" + key);
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
            }
            System.out.println("get结束,key=" + key);
            return cache.get(key);
        }
    
    
        public static void main(String[] args) {
            ReadWriteLockDemo demo = new ReadWriteLockDemo();
            //写线程
            new Thread(() -> {
                for (int i = 0; i < 5; i++) {
                    demo.put(i + "", i + "");
                }
            }).start();
            //读线程
            new Thread(() -> {
                for (int i = 0; i < 5; i++) {
                    demo.get(i+"");
                }
    
            }).start();
        }
    }
    
  • 执行结果
    put开始,key=0
    get开始,key=0
    get结束,key=0
    put结束,key=0
    get开始,key=1
    put开始,key=1
    get结束,key=1
    put结束,key=1
    
    执行结果可以看出,当正在在写入数据的时候,同时也发生了读操作,这样读操作的时候很容易读取到脏数据

模拟jvm内置缓存(读写锁)

  • 代码样例
    public class ReadWriteLockDemo {
    
        private volatile Map<String, String> cache = new HashMap<>();
        //读写锁
        private ReentrantReadWriteLock rwl = new ReentrantReadWriteLock();
        private ReentrantReadWriteLock.ReadLock readLock = rwl.readLock();
        private ReentrantReadWriteLock.WriteLock writeLock = rwl.writeLock();
    
        public void put(String key, String value) {
            try {
                writeLock.lock();
                System.out.println("put开始,key=" + key);
                Thread.sleep(1000);
                cache.put(key, value);
                System.out.println("put结束,key=" + key);
            } catch (Exception e) {
            } finally {
                writeLock.unlock();
            }
        }
    
        public String get(String key) {
            try {
                readLock.lock();
                System.out.println("get开始,key=" + key);
                Thread.sleep(1000);
                System.out.println("get结束,key=" + key);
                return cache.get(key);
            } catch (Exception e) {
                return null;
            } finally {
                readLock.unlock();
            }
        }
        
        public static void main(String[] args) {
            ReadWriteLockDemo demo = new ReadWriteLockDemo();
            //写线程
            new Thread(() -> {
                for (int i = 0; i < 5; i++) {
                    demo.put(i + "", i + "");
                }
            }).start();
            //读线程
            new Thread(() -> {
                for (int i = 0; i < 5; i++) {
                    demo.get(i + "");
                }
    
            }).start();
        }
    }
    
  • 执行结果
    put开始,key=0
    put结束,key=0
    get开始,key=0
    get结束,key=0
    put开始,key=1
    put结束,key=1
    put开始,key=2
    put结束,key=2
    
    执行结果看出,读写操作的开始和结束都是成对存在,由此可见:当写的时候,读操作并没有发生,读的时候,写操作也并没有发生,保证了读写操作时,线程安全问题。

乐观锁

概念

  • 总是认为不会产生并发问题,每次去取数据的时候总认为不会有其他线程对数据进行修改,因此不会上锁,但是在更新时会判断其他线程在这之前有没有对数据进行修改,一般会使用版本号机制或CAS操作实现
  • 乐观锁本质无锁,效率比较高,无阻塞,无等待,但是需要重试机制

实现方式

  • version方式
    一般是在数据表中加上一个数据版本号version字段,表示数据被修改的次数,当数据被修改时,version值会加一。当线程A要更新数据值时,在读取数据的同时也会读取version值,在提交更新时,若刚才读取到的version值为当前数据库中的version值相等时才更新,否则重试更新操作,直到更新成功
    核心SQL语句:
    update table set x=x+1, version=version+1 where id=#{id} and version=#{version}; 
    
  • CAS操作方式
    即compare and swap 或者 compare and set,涉及到三个操作数,数据所在的内存值,预期值,新值。当需要更新时,判断当前内存值与之前取到的值是否相等,若相等,则用新值更新,若失败则重试,一般情况下是一个自旋操作,即不断的重试

悲观锁

概念

  • 总是假设最坏的情况,每次取数据时都认为其他线程会修改,所以都会加锁(读锁、写锁、行锁等),当其他线程想要访问数据时,都需要阻塞挂起
  • 悲观锁属于重量级锁,会阻塞,会等待其他线程执行完成才执行
  • 可以依靠数据库实现,如行锁、读锁和写锁等,都是在操作之前加锁
  • 在Java中,synchronized的思想也是悲观锁

CAS无锁模式

什么是CAS

  • CAS:Compare and Swap,即比较再交换
  • jdk5增加了并发包java.util.concurrent.*,其下面的类使用CAS算法实现了区别于synchronouse同步锁的一种乐观锁
  • JDK 5之前Java语言是靠synchronized关键字保证同步的,这是一种独占锁,也是是悲观锁

CAS算法理解

(1)与锁相比,使用比较交换(下文简称CAS)会使程序看起来更加复杂一些。但由于其非阻塞性,它对死锁问题天生免疫,并且,线程间的相互影响也远远比基于锁的方式要小。更为重要的是,使用无锁的方式完全没有锁竞争带来的系统开销,也没有线程间频繁调度带来的开销,因此,它要比基于锁的方式拥有更优越的性能。

(2)无锁的好处:

  • 在高并发的情况下,它比有锁的程序拥有更好的性能
  • 天生就是死锁免疫的

(3)CAS算法的过程是这样:它包含三个参数CAS(V,E,N): V表示要更新的变量(主内存),E表示预期值(本地内存),N表示新值。仅当V值等于E值时,才会将V的值设为N,如果V值和E值不同,则说明已经有其他线程做了更新,则当前线程什么都不做。最后,CAS返回当前V的真实值

  • 如果V=E(主内存值与本地内存值一致),说明:没有被修改过,将V的值设置为新值N
  • 如果V!=E(主内存值与本地内存值不一致),说明:已经被修改过,重新刷新主内存值,然后循环进行比较

(4)CAS操作是抱着乐观的态度进行的,它总是认为自己可以成功完成操作。当多个线程同时使用CAS操作一个变量时,只有一个会胜出,并成功更新,其余均会失败。失败的线程不会被挂起,仅是被告知失败,并且允许再次尝试,当然也允许失败的线程放弃操作。基于这样的原理,CAS操作即使没有锁,也可以发现其他线程对当前线程的干扰,并进行恰当的处理

(5)简单地说,CAS需要你额外给出一个期望值,也就是你认为这个变量现在应该是什么样子的。如果变量不是你想象的那样,那说明它已经被别人修改过了。你就重新读取,再次尝试修改就好了

(6)在硬件层面,大部分的现代处理器都已经支持原子化的CAS指令。在JDK 5.0以后,虚拟机便可以使用这个指令来实现并发操作和并发数据结构,并且,这种操作在虚拟机中可以说是无处不在

CAS缺点

CAS存在一个很明显的问题,即ABA问题。

问题:如果变量V初次读取的时候是A,并且在准备赋值的时候检查到它仍然是A,那能说明它的值没有被其他线程修改过了吗?
如果在这段期间曾经被改成B,然后又改回A,那CAS操作就会误认为它从来没有被修改过。针对这种情况,java并发包中提供了一个带有标记的原子引用类AtomicStampedReference,它可以通过控制变量值的版本来保证CAS的正确性

CAS典型用法-Atomic原子类

  • jdk 1.5实现(AtomicInteger.incrementAndGet())
    public final int getAndAddInt(Object o, long offset, int delta) {
        int v;
        do {
            v = getIntVolatile(o, offset);
        } while (!compareAndSwapInt(o, offset, v, v + delta));
        return 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;  
        }  
    }  
    
  • jdk 1.8实现(AtomicInteger .incrementAndGet())
    /**
     * Atomically increments by one the current value.
     *
     * @return the updated value
     */
    public final int incrementAndGet() {
        return unsafe.getAndAddInt(this, valueOffset, 1) + 1;
    }
    public final int getAndAddInt(Object var1, long var2, int var4) {
        int var5;
        do {
            var5 = this.getIntVolatile(var1, var2);
        } while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));
        return var5;
    }
    
    public native int getIntVolatile(Object var1, long var2);
    
    public final native boolean compareAndSwapInt(Object var1, long var2, int var4, int var5);
    

自旋锁

概念

  • 是为实现保护共享资源而提出一种锁机制。其实,自旋锁与互斥锁比较类似,它们都是为了解决对某项资源的互斥使用。无论是互斥锁,还是自旋锁,在任何时刻,最多只能有一个保持者,也就说,在任何时刻最多只能有一个执行单元获得锁。但是两者在调度机制上略有不同。对于互斥锁,如果资源已经被占用,资源申请者只能进入睡眠状态。但是自旋锁不会引起调用者睡眠,如果自旋锁已经被别的执行单元保持,调用者就一直循环在那里看是否该自旋锁的保持者已经释放了锁,"自旋"一词就是因此而得名
  • CAS模式采用自旋锁

互斥锁(悲观锁)与自旋锁区别

  • 互斥锁需要等待与阻塞,属于悲观锁,一次只能一个线程拥有互斥锁,其他线程只有等待
  • 自旋锁属于乐观锁,是一种特殊的互斥锁,当资源被枷锁后,其他线程想要再次加锁,此时该线程不会被阻塞睡眠而是陷入循环等待状态(CPU不能做其它事情),循环检查资源持有者是否已经释放了资源,这样做的好处是减少了线程从睡眠到唤醒的资源消耗,但会一直占用CPU的资源。适用于资源的锁被持有的时间短,而又不希望在线程的唤醒上花费太多资源的情况
  • 互斥锁: 线程会从sleep(加锁)—》running(解锁),过程中有上下文的切换,cpu的抢占,信号的发送等开销
  • 自旋锁: 线程一直都是running(加锁—》解锁),死循环检测锁的标志位,机制不复杂

公平锁与非公平锁

实现原理

在公平的锁中,如果有另一个线程持有锁或者有其他线程在等待队列中等待这个所,那么新发出的请求的线程将被放入到队列中。而非公平锁上,只有当锁被某个线程持有时,新发出请求的线程才会被放入队列中(此时和公平锁是一样的)。所以,它们的差别在于非公平锁会有更多的机会去抢占锁

公平锁

  • 公平锁在于每次都是从等待执行的链表以此从队首取值
    final Thread current = Thread.currentThread();
    int c = getState();
    if (c == 0) {
    	if (!hasQueuedPredecessors() &&
        	compareAndSetState(0, acquires)) {
            setExclusiveOwnerThread(current);
        	return true;
            }
    }
    
    #hasQueuedPredecessors的实现
    public final boolean hasQueuedPredecessors() {
        Node t = tail; // Read fields in reverse initialization order
        Node h = head;
        Node s;
        return h != t &&
            ((s = h.next) == null || s.thread != Thread.currentThread());
    }
    
  • 公平锁样例
    public class FairLock implements Runnable {
        /**
         * true 表示 ReentrantLock 的公平锁
         */
        private ReentrantLock lock = new ReentrantLock(true);
    
        @Override
        public void run() {
            try {
                lock.lock();
                Thread.sleep(50);
                System.out.println(Thread.currentThread().getName() + "获得锁");
            } catch (Exception ex) {
            } finally {
                lock.unlock();
            }
        }
    
        public static void main(String[] args) {
            FairLock fairLock = new FairLock();
    
            for (int i = 0; i < 5; i++) {
                new Thread(fairLock).start();
            }
        }
    }
    
  • 执行结果
    Thread-0获得锁
    Thread-1获得锁
    Thread-2获得锁
    Thread-3获得锁
    Thread-4获得锁
    
    可以看到,获取锁的线程顺序正是线程启动的顺序

非公平锁

  • 在等待锁的过程中,如果有任意新的线程妄图获取锁,都是有很大几率直接获取到锁
    final Thread current = Thread.currentThread();
    int c = getState();
    if (c == 0) {
    	if (compareAndSetState(0, acquires)) {
        	setExclusiveOwnerThread(current);
            return true;
       }
    }
    
  • 非公平锁样例
    public class NotFairLock implements Runnable {
        /**
         * false 表示 ReentrantLock 的非公平锁
         */
        private ReentrantLock lock = new ReentrantLock(false);
    
        @Override
        public void run() {
            try {
                lock.lock();
                Thread.sleep(50);
                System.out.println(Thread.currentThread().getName() + "获得锁");
            } catch (Exception ex) {
            } finally {
                lock.unlock();
            }
        }
    
        public static void main(String[] args) {
            NotFairLock fairLock = new NotFairLock();
    
            for (int i = 0; i < 5; i++) {
                new Thread(fairLock).start();
            }
        }
    }
    
  • 执行结果
    Thread-0获得锁
    Thread-2获得锁
    Thread-1获得锁
    Thread-3获得锁
    Thread-4获得锁
    
    可以看出非公平锁对锁的获取是乱序的,即有一个抢占锁的过程

分布式锁

如果在不同的jvm中保证数据同步,需要使用分布式锁技术
实现方式:有数据库实现、缓存实现、Zookeeper分布式锁

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值