JUC并发编程之锁与CAS

225 阅读9分钟

java 中提供了丰富的锁,各种锁的应用场景也是不一样,在适当场景下锁可以使业务处理具有非常高的效率。

锁分类
  • 公平锁/非公平锁

公平锁是按照先来后到的原则进行处理业务,多个线程在处理业务时,按照申请锁的顺序来获取锁。

非公平锁多个线程在处理业务时获取锁的顺序不是按照申请的顺序来获取,允许请求加塞,一上来就占用锁,如果尝试失败,采用公平锁方式获取锁

非公平锁的设计直接导致了非公平锁的吞吐量要比公平锁大

synchronized就是非公平锁

对于java ReentrantLock而言,通过构造函数指定该锁是否是公平锁,默认是非公平锁

Lock lock = new ReentrantLock(true);//公平锁 Lock unlock = new ReentrantLock();//非公平锁

  • 可重入锁

可重入锁又称为递归锁,在同一个线程在外层访问获取锁的时候,在内层中是自动获取锁。

最大的作用就是防止死锁。

线程可以进入任何一个它已经拥有的锁所同步着的代码块

synchronized即为可重入锁。

  • 自旋锁

尝试获取锁的线程不会立即阻塞,而是采用循环的方式来尝试解锁,从而减少了线程上下文切换的消耗,但不断尝试也增加了CPU的消耗。

典型案例就是CAS思想

  • 独占(写)锁 / 共享(读)锁 / 互斥锁

独占锁是指线程锁一次之后就被一个线程所持有

共享锁是指锁可以被多个线程所持有

互斥锁是指线程在访问资源签进行加锁操作,在访问完成之后进行解锁操作。

注:java中的锁还有分段锁,偏向锁,轻量锁,重量锁

synchronized与lock
  • synchronized

    • JVM层面是java的关键字,底层通过monitor锁监控进行操作
    • 不需要用户进行手动释放资源,执行完成后会自动释放资源
    • 不可中断,除非抛出异常或执行完成
    • 默认为非公平锁
    • 没有绑定多个条件(condition),要么随机一个,要么全部
  • LOCK

    • java中的具体类,属于API层面,java util包下的类
    • ReentrantLock 需要用户手动释放锁,如果没有释放锁,就会导致死锁出现
    • ReentrantLock 可以中间中断,比如通过设置超时时间

举例:比如购票,现在我有三个窗口我要各自卖出40张票,如果票不够会出现什么情况

synchronized:实现

//定义一个单独的资源类
class Ticket{

    private int number = 30;

    // synchronized 这是一个关键字
    public synchronized void saleTicket(){
        if (number>0){
            System.out.println(Thread.currentThread().getName() + "卖出第"+(number--)+"票,还剩:"+number);
        }
    }

    
 //启动三个独立线程进行卖票操作
    public static void main(String[] args) throws InterruptedException {
     //1:创建资源类
         Ticket ticket = new Ticket();
        //2:线程操作资源类
        new Thread(new Runnable() {
            public void run() {
                for (int i = 1; i <=40; i++) {
                    ticket.saleTicket();
                }
            }
        },"A").start();
        
        new Thread(new Runnable() {
            public void run() {
                for (int i = 1; i <=40; i++) {
                    ticket.saleTicket();
                }
            }
        },"B").start();
        
        new Thread(new Runnable() {
            public void run() {
                for (int i = 1; i <=40; i++) {
                    ticket.saleTicket();
                }
            }
        },"C").start();
    }

注:这个地方写法,在线程操作是:线程调用资源类。

Lock:实现

//依旧是定义一个资源类,注一定要进行加锁,解锁
class Ticket2{
    // 使用Lock,它是一个对象
    // ReentrantLock 可重入锁:回家:大门 (卧室门,厕所门...)
    // ReentrantLock 默认是非公平锁!
    // 非公平锁: 不公平 (插队,后面的线程可以插队)
    // 公平锁: 公平(只能排队,后面的线程无法插队)
    private Lock lock = new ReentrantLock();

    private int number = 30;

    public void saleTicket(){
        lock.lock(); // 加锁
        try {
            // 业务代码
            if (number>0){
                System.out.println(Thread.currentThread().getName() + "卖出第"+(number--)+"票,还剩:"+number);
            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            lock.unlock(); // 解锁
        }
    }
   
    
    public static void main(String[] args) {
        // 1、新建资源类
        Ticket2 ticket = new Ticket2();
        // 2、线程操作资源类 , 所有的函数式接口都可以用 lambda表达式简化!
        // lambda表达式 (参数)->{具体的代码}
        new Thread(()->{for (int i = 1; i <= 40 ; i++)                          ticket.saleTicket();},"A").start();
        
        new Thread(()->{for (int i = 1; i <= 40 ; i++) ticket.saleTicket();},"B").start();
        
        new Thread(()->{for (int i = 1; i <= 40 ; i++) ticket.saleTicket();},"C").start();
    }
读写锁举例

读写锁,如果多个线程去取一个共享资源是没有问题,但是如果一个线程想去写入共享资源,就不应该有其他线程对资源进行写的操作。比如数据库、缓存。

举例,多线程环境下对缓存读写操作

//首先定义了一个缓存资源类,在这里进行写入缓存和读取缓存两个操作,
class MyCache{
    private volatile Map<String,Object> map = new HashMap<>();
    private ReentrantReadWriteLock rwlock = new ReentrantReadWriteLock();

    /**
     * 进行写的操作
     * @param key
     * @param value
     */
    public void put(String key,Object value){
        //在写入缓存前利用写锁进行加锁
        rwlock.writeLock().lock();
        try{
            System.out.println(Thread.currentThread().getName()+"开始写入 "+ key);
            try {
                //睡一秒用意在于多线程环境下是否排队进行写操作
                TimeUnit.SECONDS.sleep(1);
                map.put(key,value);
            }catch (Exception e){
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName()+"写入完成");
        }catch (Exception e){
            e.printStackTrace();
        }finally {
            //最后一定要进行解锁
            rwlock.writeLock().unlock();
        }

    }

    /**
     * 读的操作
     * @param key
     */
    public  void get(String key){
        //读取前进行加锁
        rwlock.readLock().lock();
        try{
            System.out.println(Thread.currentThread().getName()+"开始读取" +  key);
            try {
                TimeUnit.SECONDS.sleep(1);
                Object resutl = map.get(key);
                System.out.println(Thread.currentThread().getName()+"读取完成"+ resutl);
            }catch (Exception e){
                e.printStackTrace();
            }
        }catch (Exception e){
            e.printStackTrace();
        }finally {
            //读取后进行解锁
            rwlock.readLock().unlock();
        }
    }
}

//调用实践
//在这我起5个线程进行写入操作,5个线程进行读取操作
public static void main(String[] args) {
        MyCache cache = new MyCache();
        //执行写的操作
        for(int i=0;i<5;i++){
            final  int tempInt = i;
            new Thread(()->{
                cache.put(tempInt+"",tempInt+"");
            },String.valueOf(i)+"w").start();
        }
        for(int i=0;i<5;i++){
            final  int tempInt = i;
            new Thread(()->{
                cache.get(tempInt+"");
            },String.valueOf(i)+"r").start();
        }

    }

正确的执行结果如下

0w开始写入
0w写入完成
//。。。。这个地方省去中间三个写入操作
4w开始写入
4w写入完成
//多线程读取则是无序的,执行结果仅供参考
 0r开始读取0
 1r开始读取1
 3r开始读取3
 //...一样中间省略
 0r读取完成0
 1r读取完成1
 2r读取完成2
自旋锁

在多线程环境下,尝试获取锁的线程不会立即阻塞,而是以循环的方式不断尝试获取锁。

public class SpinLockDemo {

 //原子引用线程
 AtomicReference<Thread> atomicReference = new AtomicReference<>();

 public void myLock(){
     Thread thread = Thread.currentThread();
     System.out.println(Thread.currentThread().getName()+"线程进来了");
     //当线程   当A 进来的时候主内存中存的是 null
     //当B 进来的时候主内存为  a  不断的获取锁
     //当A 5秒后解锁后进行解锁
     //B 等到A解锁后进入,解锁
     while(!atomicReference.compareAndSet(null,thread)){

     }
 }

 public void myunLock(){
     Thread thread = Thread.currentThread();
     System.out.println(Thread.currentThread().getName()+"线程开始解锁");
     atomicReference.compareAndSet(thread,null);//解锁
 }

 public static void main(String[] args) {
     SpinLockDemo obj = new SpinLockDemo();
     new Thread(()->{
         obj.myLock();
         try {
             TimeUnit.SECONDS.sleep(5);
         }catch (Exception e){

         }
         obj.myunLock();
     },"AAA").start();
     try{
         TimeUnit.SECONDS.sleep(1);
     }catch (Exception e){
         e.printStackTrace();
     }
     new Thread(()->{
         obj.myLock();
         obj.myunLock();
     },"BBB").start();
 }
}
可重入锁

重入锁就是递归,在获取了外层锁之后,内层的锁是自动获取的。

举例:我们发短信和发邮件的业务实现,在发送短信之后给用户发送邮件

class Phone implements Runnable{
 public synchronized  void sendSms() throws  Exception{
     System.out.println(Thread.currentThread().getName()+"调用段短信方法");
     sendEmail();
 }

 public synchronized  void sendEmail() throws Exception{
     System.out.println(Thread.currentThread().getName()+"调用发送邮件方法");
 }

 Lock lock = new ReentrantLock();

 @Override
 public void run() {
     get();
 }
 public void get() {
     //锁如果锁几次,解锁几次一样可以运行正常
     lock.lock();
     lock.lock();
     try{
         System.out.println(Thread.currentThread().getName()+"调用get");
         set();
     }catch (Exception e){
         e.printStackTrace();
     }finally {
         lock.unlock();
         lock.unlock();//如果锁了两把锁,只解锁一次会程序卡死,加锁几次就要解锁几次
     }
 }

 public void set() {
     lock.lock();
     try{
         System.out.println(Thread.currentThread().getName()+"调用set");
     }catch (Exception e){
         e.printStackTrace();
     }finally {
         lock.unlock();
     }
 }
}
public class T1 {
 public static void main(String[] args) {
     //可重入锁也叫递归锁,
     // 同一个线程在获取到外层锁后,
     // 内层锁同样可以获取到改锁
     Phone p = new Phone();
     new Thread(()->{
         try {
             p.sendSms();
         } catch (Exception e) {
             e.printStackTrace();
         }
     },"t1").start();

     new Thread(()->{
         try {
             p.sendSms();
         } catch (Exception e) {
             e.printStackTrace();
         }
     },"t2").start();
     /** 输出内容
         * t1调用段短信方法
         * t1调用发送邮件方法
         * t2调用段短信方法
         * t2调用发送邮件方法
         */

        try {
            TimeUnit.SECONDS.sleep(4);
        }catch (Exception e){
            e.printStackTrace();
        }

        Thread t3 = new Thread(p);
        Thread t4 = new Thread(p);
        t3.start();
        t4.start();
        /**
         * Thread-0调用get
         * Thread-0调用set
         * Thread-1调用get
         * Thread-1调用set
         */
    }
}

注:锁的概念比较抽象,如果您感兴趣可给小编留言,提供对应的java代码,建议从头到尾敲打一遍。

CAS

cas 是compare and swap 就是比较和交换,用于实现多线程条件下,子线程往主线程同步的原子质量,子线程将内存中的值与给定的值进行比较,如果值相同那么将会更新内存中的值。这样作为单个原子操作完成。如果值在同一时间被另一个线程更新,则会写入失败。

举个简单例子

public class CasDemo {

 /**
     * CAS 比较并交换  compare and set
     * @param args
     */
    public static void main(String[] args) {
        AtomicInteger atomicInteger = new AtomicInteger(5);

        // 保证主物理内存中的期望值是 5  并 更改换为10
        boolean flag = atomicInteger.compareAndSet(5,10);
        System.out.println(flag+"确认值"+atomicInteger.get());
        //期望值如果是5  并更换成   1022
        boolean aflg = atomicInteger.compareAndSet(5,1022);
        System.out.println(aflg+"确认值"+atomicInteger.get());
    }
}

上面代码的执行情况如下

true  确认值  10
false 确认值  10

所谓CAS就是比较期望值和实际值,如果一样则进行交换,如果不一致则不进行交换

cas底层实现

以atomicinteger为例进行说明

public class AtomicInteger extends Number implements Serializable {
    private static final long serialVersionUID = 6214790243416807050L;
    private static final Unsafe unsafe = Unsafe.getUnsafe();

unsafe是CAS的核心类,由于java方法无法直接访问底层系统,需要通过本地native方法进行访问,unsafe相当于一个后门,基于该类是java可以直接操作特定内存的数据,unsafe类存在在sun.misc包中,其内部方法操作,可以像C的指针一样直接操作内存,注意unsafe中所有的方法都是用native进行修饰了,也就是unsafe类中的方法都可以直接调用操作系统底层资源执行相应任务。

 public native int getInt(Object var1, long var2);

    public native void putInt(Object var1, long var2, int var4);

    public native Object getObject(Object var1, long var2);

    public native void putObject(Object var1, long var2, Object var4);

    public native boolean getBoolean(Object var1, long var2);

    public native void putBoolean(Object var1, long var2, boolean var4);

    public native byte getByte(Object var1, long var2);

CAS是一条CPU并发原语,通过调用unsafe类中方法,JVM帮我们实现出CAS汇编指令,这是一种完全依赖于硬件的功能,通过它实现了原子操作,由于CAS是一种系统原语,原语是属于操作系统用语范畴,是由若干指令构成的,用于完成某个功能的一个过程,原语的执行必须是连续的,在执行过程中不允许被终端,是一条CPU原子指令,不会造成所谓的数据不一致问题。

问题:

​ 线程A执行一次操作需要5秒钟,线程B执行一次操作需要15秒钟,线程A与B同时操作内存数据,

​ A 5----> 10 , 10---> 5

​ B 5---->15

请问,当A执行两条指令将内存数据由 5变成10 再有10 变成5 用时10秒,等到B执行由5变成15是否会成功,如果成功B手中的5和内存中的5是否一致?

如果您对今天的分享感兴趣,请关注公众号,多谢

本文使用 mdnice 排版