【Java多线程】总结(五)策略锁篇

1 乐观锁

1 概念:它认为一般情况下不会发生冲突,所以只会在更新数据 的时候才对冲突进行检测,如果没有发生冲突直接进行修改,如果发生了冲突不做任何修改,然后把结果返回给⽤户,让⽤户⾃⾏决定处理。

2 实现:CAS(Compare and Swap)比较并替换

  1. V :内存值

  2. A :旧值

  3. B :新值

    1 流程 V==A?true(没有发生并发冲突)->V=B:false(发生并发冲突)
    (解释一下 时间长确实会忘 :CAS 中包含了三个操作单位:V(内存值)、A(预期旧值)、B(新值),比较 V 值和 A 是否相等,如果相等的话则将 V 的值更换成 B,否则就提示用户修改失败,从而实现了 CAS 的机制。

3 CAS 底层实现:CAS 实现是借助 Unsafe 类,Unsafe 类调⽤操作系统的 Atomic::cmpxchg(原⼦性汇编指令)来实现CAS

4 乐观锁(CAS)在java的应用:unsafe类和AtomicXXX

  1. AtomicInteger:
    乐观锁机制保证线程安全
注释掉的为原本非线程安全代码
public class CASDemo {
    private static int number=0;
    private static AtomicInteger atomicInteger=new AtomicInteger(0);
    private final static int MAX_COUNT=100000;
    public static void main(String[] args) throws InterruptedException {
        Thread t1=new Thread(()->{
            for (int i = 0; i < MAX_COUNT; i++) {
                atomicInteger.getAndIncrement();//i++
//                number++;
            }
        });t1.start();
        Thread t2=new Thread(()->{
            for (int i = 0; i < MAX_COUNT; i++) {
                atomicInteger.getAndDecrement();
//                number++;
            }
        });
        t2.start();
        t1.join();
        t2.join();
        System.out.println("最终结果:"+atomicInteger.get());
//        System.out.println("最终结果:"+number);

    }

}

5 CAS缺陷:性能比较高 但是存在ABA问题

  1. 演示:
  2. V :内存值
    A :旧值
    B :新值
    

张三给某人转账 +自己刚好发工资
张三原本有100块,第一次转账系统卡顿,又点了一次成功转账50元给对方,这时张三又收到自己的工资50元,张三又有了100块,这是系统恢复,CAS判断(V=100,A=100,B=50),于是又扣了50。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-e1dVZIgE-1650678467212)(C:\Users\ainoyygg\AppData\Roaming\Typora\typora-user-images\image-20220422154231001.png)]

  1. 代码演示:
import java.util.concurrent.atomic.AtomicInteger;

/**
 * ABA问题
 */
public class ABADemo {
    private static AtomicInteger money=new AtomicInteger(100);

    public static void main(String[] args) throws InterruptedException {
        //第一次点击转账按钮
        Thread t1=new Thread(()->{
            int old_money=money.get();
            //执行花费两秒
            try {
                Thread.sleep(2000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            money.compareAndSet(old_money,old_money-50);
        });
        t1.start();
        //第二次点击转账按钮(第一次点了没反应 又点了一次
        Thread t2=new Thread(()->{
            int old_money=money.get();
            money.compareAndSet(old_money,old_money-50);
        });
        t2.start();
        //给账户加50元
        Thread t3=new Thread(()->{
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            int old_money = money.get();
            money.compareAndSet(old_money, old_money + 50);
        });
        t3.start();
        t1.join();
        t2.join();
        t3.join();
        System.out.println("最终账号余额:"+money.get());
    }
}

6 ABA问题解决方案:

  1. 引入版本号,每次操作后版本号+1,执行的时候判断版本号和值,就可以解决ABA问题
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicStampedReference;

/**
 * ABA问题演示
 */
public class ABADemo2 {
    private static AtomicStampedReference<Integer> money=new AtomicStampedReference<>(100,0);

    public static void main(String[] args) throws InterruptedException {
        //第一次点击转账按钮
        Thread t1=new Thread(()->{
            int old_money=money.getReference();//先得到余额
            int version=money.getStamp();//得到版本号
            try {
                Thread.sleep(2000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            money.compareAndSet(old_money,old_money-50,version,version+1);
        });
        t1.start();
        //第二次点击转账按钮(第一次点了没反应 又点了一次
        Thread t2=new Thread(()->{
            int old_money=money.getReference();//先得到余额
            int version=money.getStamp();//得到版本号
            money.compareAndSet(old_money,old_money-50,version,version+1);
        });
        t2.start();
        //给账户加50元
        Thread t3=new Thread(()->{
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            int old_money=money.getReference();//先得到余额
            int version=money.getStamp();//得到版本号
            money.compareAndSet(old_money,old_money+50,version,version+1);
        });
        t3.start();
        t1.join();
        t2.join();
        t3.join();
        System.out.println("最终账号余额:"+money.getReference());
    }
}
2 悲观锁

1 定义:它认为通常情况下会发生并发冲突,所以在进入方法后就会进行检测(总是假设最坏的情况,每次去拿数据的时候都认为别⼈会修改,所以每次在拿数据的时候都会上锁,这样别⼈想拿这个数据就会阻塞直到别人拿到锁。 )
2 举例:synchronized、Lock

3 公平锁与非公平锁

1 公平锁:所有任务来了之后先排队,线程空闲之后去任务队列按顺序执⾏最早任务

ReentrantLock lock= new ReentrantLock(true)

2 非公平锁:抢占式执行,有⼀些先来的任务还在排队,刚好释放锁的时候新来了⼀个任务,此时并不会通知任务队列来执⾏任务,⽽是执⾏新来的任务

ReentrantLock lock = new ReentrantLock(false)如果构造函数不传递参数,则默认 是⾮公平锁

4 读写锁

1 定义:将一把锁分为两部分:读锁和写锁,读锁可以被多个线程同时拥有,写锁只能被一个线程拥有,读写锁的特征是:读读不互斥,读写互斥(防止同时读写脏读),写写互斥

2 适用场景:多读少写的业务操作

3 代码演示

import java.time.LocalDateTime;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.ReentrantReadWriteLock;

/**
 * 读写锁演示
 */
public class ReadWriteLockDemo {
    public static void main(String[] args) {
        //创建读写锁(不修改锁 只是调用)(默认非公平锁)
        final ReentrantReadWriteLock readWriteLock=new ReentrantReadWriteLock();
        //创建读锁
        final ReentrantReadWriteLock.ReadLock readLock=readWriteLock.readLock();
        //创建写锁
        final ReentrantReadWriteLock.WriteLock writeLock=readWriteLock.writeLock();
        ThreadPoolExecutor executor=new ThreadPoolExecutor(5,5,0, TimeUnit.SECONDS,new LinkedBlockingQueue<>(100));
        executor.submit(()->{
            //加锁操作
            readLock.lock();
            try {
                //执行业务逻辑
                System.out.println("执行读锁1:"+ LocalDateTime.now());
                TimeUnit.SECONDS.sleep(3);
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally{
                readLock.unlock();
            }
        });
        executor.submit(()->{
            readLock.lock();
            try {
                System.out.println("执行读锁2:"+LocalDateTime.now());
                TimeUnit.SECONDS.sleep(3);
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally{
                readLock.unlock();
            }
        });
        executor.submit(()->{
            writeLock.lock();
            try {
                System.out.println("执行写锁1:"+LocalDateTime.now());
                TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally{
                writeLock.unlock();
            }
        });
        executor.submit(()->{
            writeLock.lock();
            try {
                System.out.println("执行写锁2:"+LocalDateTime.now());
                TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally{
                writeLock.unlock();
            }
        });
    }
}
/*
执行读锁1:2022-04-22T17:13:39.477
执行读锁2:2022-04-22T17:13:39.477
执行写锁1:2022-04-22T17:13:42.492
执行写锁2:2022-04-22T17:13:43.497
 */

4 优点:锁粒度更小,性能更高

5 独占锁

1 定义:是指任何时候都只有⼀个线程能执行资源操作。
2 举例:synchronized、Lock

6 共享锁

1 定义:共享锁指定是可以同时被多个线程读取,但只能被⼀个线程修改

2 举例:java 中的ReentrantReadWriteLock 就是共享锁的实现⽅式,它允许⼀个线程进⾏写操作,允许多个线程读操作。

7 可重入锁&自旋锁

1 可重⼊锁:指的是该线程获取了该锁之后,可以⽆限次的进⼊该锁锁住的代码。即允许同一个线程多次获取同一把锁。比如一个递归函数里有加锁操作,递归过程中这个锁会阻塞自己吗?如果不会,那么这个锁就是可重入锁(因为这个原因可重入锁也叫做递归锁)。

注意 :Java里只要以Reentrant开头命名的锁都是可重入锁,而且JDK提供的所有现成的Lock实现类,包括synchronized关键字锁都是可重入的。

2 自旋锁:是指尝试获取锁的线程不会立即阻塞,而是采用循环的⽅式去尝试获取锁,这样的好处是减少线程上下⽂切换的消耗,缺点是循环会消耗 CPU。

  1. synchronized:自适应自旋锁(自旋次数不确定)
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值