并发编程系列教程(10) - 深入Java锁机制

代码已上传到Github,有兴趣的同学可以下载看看:https://github.com/ylw-github/Java-ThreadDemo

1. 重入锁

锁作为并发共享数据,保证一致性的工具,在JAVA平台有多种实现(如 synchronized 和 ReentrantLock等等 ) 。这些已经写好提供的锁为我们开发提供了便利。

重入锁,也叫做递归锁,指的是同一线程 外层函数获得锁之后 ,内层递归函数仍然有获取该锁的代码,但不受影响。

在JAVA环境下 ReentrantLock(显式锁、轻量级锁)和Synchronized (内置锁、重量级锁)都是 可重入锁。

Synchronized(内置锁、重量级锁):

public class RepeatLockDemo {
    public static class RepeatLockRunnable 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) {
        RepeatLockRunnable repeatLockRunnable = new RepeatLockRunnable();
        Thread thread = new Thread(repeatLockRunnable);
        Thread thread1 = new Thread(repeatLockRunnable);
        Thread thread2 = new Thread(repeatLockRunnable);
        thread.start();
        thread1.start();
        thread2.start();
    }
}

运行结果:
在这里插入图片描述

ReentrantLock(显式锁、轻量级锁)

package com.ylw.thread;

import java.util.concurrent.locks.ReentrantLock;

public class ReentrantLockDemo {

    public static class ReentrantLockRunnable implements Runnable{

        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) {
        ReentrantLockRunnable reentrantLockRunnable = new ReentrantLockRunnable();
        Thread thread = new Thread(reentrantLockRunnable);
        Thread thread1 = new Thread(reentrantLockRunnable);
        Thread thread2 = new Thread(reentrantLockRunnable);
        thread.start();
        thread1.start();
        thread2.start();
    }

}

运行结果:
在这里插入图片描述

2. 读写锁

相比Java中的锁(Locks in Java)里Lock实现,读写锁更复杂一些。假设你的程序中涉及到对一些共享资源的读和写操作,且写操作没有读操作那么频繁。在没有写操作的时候,两个线程同时读一个资源没有任何问题,所以应该允许多个线程能在同时读取共享资源。但是如果有一个线程想去写这些共享资源,就不应该再有其它线程对该资源进行读或写,也就是说:读-读能共存,读-写不能共存,写-写不能共存。这就需要一个读/写锁来解决这个问题。Java5在java.util.concurrent包中已经包含了读写锁。尽管如此,我们还是应该了解其实现背后的原理。

import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantReadWriteLock;

public class ReadWriteLockDemo {

    static Map<String, Object> map = new HashMap<String, Object>();
    static ReentrantReadWriteLock rwl = new ReentrantReadWriteLock();
    static Lock r = rwl.readLock();
    static Lock w = rwl.writeLock();

    // 获取一个key对应的value
    public static final Object get(String key) {
        r.lock();
        try {
            System.out.println("正在做读的操作,key:" + key + " 开始");
            Thread.sleep(100);
            Object object = map.get(key);
            System.out.println("正在做读的操作,key:" + key + " 结束");
            System.out.println();
            return object;
        } catch (InterruptedException e) {

        } finally {
            r.unlock();
        }
        return key;
    }

    // 设置key对应的value,并返回旧有的value
    public static final Object put(String key, Object value) {
        w.lock();
        try {

            System.out.println("正在做写的操作,key:" + key + ",value:" + value + "开始.");
            Thread.sleep(100);
            Object object = map.put(key, value);
            System.out.println("正在做写的操作,key:" + key + ",value:" + value + "结束.");
            System.out.println();
            return object;
        } catch (InterruptedException e) {

        } finally {
            w.unlock();
        }
        return value;
    }

    // 清空所有的内容
    public static final void clear() {
        w.lock();
        try {
            map.clear();
        } finally {
            w.unlock();
        }
    }

    public static void main(String[] args) {
        new Thread(new Runnable() {

            @Override
            public void run() {
                for (int i = 0; i < 5; i++) {
                    ReadWriteLockDemo.put(i + "", i + "");
                }

            }
        }).start();
        new Thread(new Runnable() {

            @Override
            public void run() {
                for (int i = 0; i < 5; i++) {
                    ReadWriteLockDemo.get(i + "");
                }

            }
        }).start();
    }
}

运行结果:

正在做写的操作,key:0,value:0开始.
正在做写的操作,key:0,value:0结束.

正在做写的操作,key:1,value:1开始.
正在做写的操作,key:1,value:1结束.

正在做写的操作,key:2,value:2开始.
正在做写的操作,key:2,value:2结束.

正在做写的操作,key:3,value:3开始.
正在做写的操作,key:3,value:3结束.

正在做读的操作,key:0 开始
正在做读的操作,key:0 结束

正在做写的操作,key:4,value:4开始.
正在做写的操作,key:4,value:4结束.

正在做读的操作,key:1 开始
正在做读的操作,key:1 结束

正在做读的操作,key:2 开始
正在做读的操作,key:2 结束

正在做读的操作,key:3 开始
正在做读的操作,key:3 结束

正在做读的操作,key:4 开始
正在做读的操作,key:4 结束

5. 悲观锁、乐观锁、排他锁

5.1 悲观锁

悲观锁: 悲观锁悲观的认为每一次操作都会造成更新丢失问题,在每次查询时加上排他锁。每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会block直到它拿到锁。传统的关系型数据库里边就用到了很多这种锁机制,比如行锁,表锁等,读锁,写锁等,都是在做操作之前先上锁。

共享锁和排他锁是悲观锁的不同实现,但都属于悲观锁。

例如排它锁:

对某一资源加排他锁,自身可以进行增删改查,其他人无法进行任何操作。语法为:

java select * from table for update

5.2 乐观锁

本身很乐观,认为在操作数据时(更新操作),默认别的线程并不会操作数据发生冲突,所以在每次操作时并不会上锁,只是在更新时判断别的线程在此期间是否有做修改,如果有修改该操作就无效

通常实现是这样的:在表中的数据进行操作时(更新),先给数据表加一个版本(version)字段,每操作一次,将那条记录的版本号加1。也就是先查询出那条记录,获取出version字段,如果要对那条记录进行操作(更新),则先判断此刻version的值是否与刚刚查询出来时的version的值相等,如果相等,则说明这段期间,没有其他程序对其进行操作,则可以执行更新,将version字段的值加1;如果更新时发现此刻的version值与刚刚获取出来的version的值不相等,则说明这段期间已经有其他程序对其进行操作了,则不进行更新操作。

#1.查询出商品信息
select (status,status,version) from t_goods where id=#{id}

#2.修改商品status为2
set status=2,version=version+1 where id=#{id} and version=#{version};

4. 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以后,虚拟机便可以使用这个指令来实现并发操作和并发数据结构,并且,这种操作在虚拟机中可以说是无处不在。
在这里插入图片描述
流程如下示例代码:

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

5. 自旋锁

自旋锁是采用让当前线程不停地的在循环体内执行实现的,当循环的条件被其他线程改变时 才能进入临界区,如下:

import java.util.concurrent.atomic.AtomicReference;

public class WhileLockDemo {



    public static class SpinLock{
        private AtomicReference<Thread> sign =new AtomicReference<>();

        public void lock() {
            Thread current = Thread.currentThread();
            while (!sign.compareAndSet(null, current)) {
            }
        }

        public void unlock() {
            Thread current = Thread.currentThread();
            sign.compareAndSet(current, null);
        }
    }


    public static class Test implements Runnable {
        static int sum;
        private SpinLock lock;

        public Test(SpinLock lock) {
            this.lock = lock;
        }

        @Override
        public void run() {
            this.lock.lock();
            this.lock.lock();
            sum++;
            this.lock.unlock();
            this.lock.unlock();
        }

        /**
         * @param args
         * @throws InterruptedException
         */
        public static void main(String[] args) throws InterruptedException {
            SpinLock lock = new SpinLock();
            for (int i = 0; i < 100; i++) {
                Test test = new Test(lock);
                Thread t = new Thread(test);
                t.start();
            }

            Thread.currentThread().sleep(1000);
            System.out.println(sum);
        }

    }
}

当一个线程调用这个不可重入的自旋锁去加锁的时候没问题,当再次调用lock()的时候,因为自旋锁的持有引用已经不为空了,该线程对象会误认为是别人的线程持有了自旋锁
使用了CAS原子操作,lock函数将owner设置为当前线程,并且预测原来的值为空。unlock函数将owner设置为null,并且预测值为当前线程。

当有第二个线程调用lock操作时由于owner值不为空,导致循环一直被执行,直至第一个线程调用unlock函数将owner设置为null,第二个线程才能进入临界区。

由于自旋锁只是将当前线程不停地执行循环体,不进行线程状态的改变,所以响应速度更快。但当线程数不停增加时,性能下降明显,因为每个线程都需要执行,占用CPU时间。如果线程竞争不激烈,并且保持锁的时间段。适合使用自旋锁。

6. 分布式锁

如果想在不同的jvm中保证数据同步,使用分布式锁技术。

实现的方式:

1.数据库实现: 使用排它锁的技术(很少用到)

2.Memcached:利用 Memcached 的 add 命令。此命令是原子性操作,只有在 key 不存在的情况下,才能 add 成功,也就意味着线程得到了锁。

3.Redis:和 Memcached 的方式类似,利用 Redis 的 setnx 命令。此命令同样是原子性操作,只有在 key 不存在的情况下,才能 set 成功。

4.Zookeeper:利用 Zookeeper 的顺序临时节点,来实现分布式锁和等待队列。Zookeeper 设计的初衷,就是为了实现分布式锁服务的。

5.Chubby:Google 公司实现的粗粒度分布式锁服务,底层利用了 Paxos 一致性算法。

总结

在这里插入图片描述

  • 2
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值