JavaEE_CAS_Synchronized原理_线程安全集合类


一、CAS

1.什么是CAS

CAS:全称Compare and swap ,字面意思,“比较并交换”。

比较交换的是内存和寄存器。
如果一个内存:M
现在还有两个寄存器:A,B
CAS(M,A,B):
如果M和A的值相同,就把M和B里的值进行交换,同时整个操作返回true.
如果M和A的值不相同,无事发生,同时整个操作返回false

交换的本质,是为了把B赋值给M.(寄存器B里的值是啥,我们不太关心,更关心的是M里的情况)
M=B

CAS其实是一个cpu指令。一个cpu指令,就能完成上述比较交换的逻辑。单个cpu指令,是原子的!!!就可以使用CAS完成一些操作,进一步替代“加锁”。————给编写线程安全的代码引入新的思路,基于CAS实现线程安全的方式,也称为“无锁编程”

优点:保证线程安全,同时避免阻塞(效率)
缺点:
1.代码会更复杂,不好理解
2,只能够适合一些特定场景,不如加锁方式更普通

CAS本质上是cpu提供的指令=》又被操作系统封装,提供api=》又被JVM封装,也提供api=》程序员使用。

2.CAS有哪些应用

1.实现原子类 - AtomicInteger

标准库中提供了 java.util.concurrent.atomic 包, 里面的类都是基于这种方式来实现的.
典型的就是 AtomicInteger 类. 其中的 getAndIncrement 相当于 i++ 操作.

AtomicInteger atomicInteger = new AtomicInteger(0);
// 相当于 i++
atomicInteger.getAndIncrement();

样例:

    public static AtomicInteger count= new AtomicInteger(0);
    public static void main(String[] args) throws InterruptedException {
        Thread t1 =new Thread(()->{
            for(int  i =0;i<50000;i++)
            {
                //count++;
                count.getAndIncrement();
//                //++ count;
//                count.incrementAndGet();
//                //count --
//                count.getAndDecrement();
//                //-- count
//                count.decrementAndGet();
            }
        });
        Thread t2 =new Thread(()->{
            for(int  i =0;i<50000;i++)
            {
//                count++;
                count.getAndIncrement();
            }
        });
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        System.out.println(count);
    }

在这里插入图片描述
在Java中,有些操作的偏底层的操作,偏底层的操作在使用的时候有更多的注意事项。稍有不慎就容易写出问题,这些操作,就放到unsafe中进行归类。
在这里插入图片描述
原子类内部没有使用synchronized加锁的。
在这里插入图片描述
native修饰的方法称为“本地方法”
也就是在JVM源码中,使用C++实现的逻辑。=》涉及到一些底层操作。

结论:原子类里面是基于CAS来实现的。

前面说“线程不安全”本质上是进行自增的过程中,穿插执行了。
CAS也是让这里的自增,不要穿插执行,核心思路和加锁是类似的。
CAS则是会通过重试的方式,避免穿插。
在这里插入图片描述

2.基于CAS实现的自旋锁

public class SpinLock {
	//
    private Thread owner = null;
    public void lock(){
        // 通过 CAS 看当前锁是否被某个线程持有. 
        // 如果这个锁已经被别的线程持有, 那么就自旋等待. 
        // 如果这个锁没有被别的线程持有, 那么就把 owner 设为当前尝试加锁的线程. 
        while(!CAS(this.owner, null, Thread.currentThread())){
       }
   }
    public void unlock (){
        this.owner = null;
   }
}

3.CAS的ABA问题

CAS也是多线程中的一种重要技巧,虽然开发中直接使用CAS的概率不大,但是经常会用到一些内部封装了CAS的操作。

还会伴随ABA问题,什么是ABA问题?
CAS进行操作的关键,是通过值“没有发生变化”来作为“没有其他线程穿插执行”判断依据。
但是,这种判定方式不够严谨,更极端的的情况下:可能有另一个线程穿插进来,把值从A->B->B,针对第一个线程来说,看起来好像把这个值,没变,但实际上已经被穿插执行了。

针对ABA问题如果真的出现,其实大部分情况下也不会产生bug,虽然中间穿插一个线程执行了,由于值又改回去了,此时逻辑上不一定会产生bug.

ABA 问题引来的 BUG
假设 滑稽老哥 有 100 存款. 滑稽想从 ATM 取 50 块钱. 取款机创建了两个线程, 并发的来执行 -50 操作.

  1. 存款 100. 线程1 获取到当前存款值为 100, 期望更新为 50; 线程2 获取到当前存款值为 100, 期
    望更新为 50.
  2. 线程1 执行扣款成功, 存款被改成 50. 线程2 阻塞等待中.
  3. 在线程2 执行之前, 滑稽的朋友正好给滑稽转账 50, 账户余额变成 100 !!
  4. 轮到线程2 执行了, 发现当前存款为 100, 和之前读到的 100 相同, 再次执行扣款操作
    这个时候, 扣款操作被执行了两次!!! 都是 ABA 问题搞的鬼

解决方案
给要修改的值, 引入版本号. 在 CAS 比较数据当前值和旧值的同时, 也要比较版本号是否符合预期

CAS 操作在读取旧值的同时, 也要读取版本号.
真正修改的时候,

  • 如果当前版本号和读到的版本号相同, 则修改数据, 并把版本号 + 1.
  • 如果当前版本号高于读到的版本号. 就操作失败(认为数据已经被修改过了)

小结:在实际开发中,一般不会直接使用CAS,都是用的封装好的,但是面试中比较容易考到CAS,一旦考察到CAS,一定会涉及到ABA问题。

二、Synchronized原理

1.基本特点

结合之前的锁策略,我们就可以总结出,Synchronized具有以下特性(只考虑JDK1.8):
1.开始时是乐观锁,如果锁冲突频繁,就转换为悲观锁。
2.开始是轻量级锁实现,如果锁如果持有时间较长,就转换为重量级锁。
3.实现轻量级锁的时候大概率用到了自旋锁策略
4.是一种不公平锁
5.是一种可重入锁
6.不是读写锁

synchronized几个重要机制:
1.锁升级
2.锁消除
3.锁粗化

在这里插入图片描述

锁升级的过程是单向的,不能再降级了。

2.偏向锁

第一个尝试加锁的线程,优先进入偏向锁状态。
偏向锁不是真的 “加锁”, 只是给对象头中做一个 “偏向锁的标记”, 记录这个锁属于哪个线程. 如果后续没有其他线程来竞争该锁, 那么就不用进行其他同步操作了(避免了加锁解锁的开销)如果后续有其他线程来竞争该锁(刚才已经在锁对象中记录了当前锁属于哪个线程了, 很容易识别当前申请锁的线程是不是之前记录的线程), 那就取消原来的偏向锁状态, 进入一般的轻量级锁状态.
偏向锁本质上相当于 “延迟加锁” . 能不加锁就不加锁, 尽量来避免不必要的加锁开销. 但是该做的标记还是得做的, 否则无法区分何时需要真正加锁.
偏向锁既保证效率,也保证了线程安全。

3.锁消除

锁消除:也是一种编译器优化的手段。
编译器会自动针对你当前写的加锁的代码,做出判断,如果编译器觉得这个场景,不需要加锁,此时就会把你写的synchronized给优化掉。

StringBulider不带synchronized
StringBuffer带synchronized
如果是在单线程中使用StringBuffer,此时编译器就会自动把synchronized给优化掉。
编译器只会在自己非常有把握的时候,才会进行锁消除

偏向锁,这是运行的事情,运行过程中多线程的调度情况不同。
这个线程的锁肯有人竞争,可能没人竞争。

4.锁粗化

锁的粒度。
synchronized里头,代码越多,就认为锁的粒度越粗。代码越少,锁的粒度越细。

粒度细的时候,能够并发执行的逻辑更多,更有利于充分利用多核CPU资源。
但是,如果粒度细的锁,被反复进行加锁解锁,可能实际效果还不如粒度粗的锁(涉及到反复的锁竞争)

三、JUC(java.util.concurrent)的常见类

并发(这个包里的内容,主要就是一些多线程相关的组件)

1.Callable接口

也是创建线程的方式,适合于,想让某个线程执行一个逻辑,并且返回结果的时候,相比之下,Runable不关注结果。

    public static void main(String[] args) throws ExecutionException, InterruptedException {
        //定义了任务
        Callable<Integer> callable = new Callable<Integer>() {
            @Override
            public Integer call() throws Exception {
                int sum =0;
                for(int i = 0;i<=1000;i++)
                {
                    sum+=i;
                }
                return sum;
            }
        };
        //把任务放到线程中进行执行。
        FutureTask<Integer> futureTask =new FutureTask<>(callable);
        Thread t = new Thread(futureTask);
        t.start();
        //此时的get就能获取到callable里面的返回结果
        //由于线程是并发执行的,执行到主线程的get的时候,t线程可能还没执行完。
        //没执行完的话,get就会阻塞。
        System.out.println(futureTask.get());
    }

futureTask是给我们凭借谁来取结果。

小结:线程的创建方式
1.继承Thread,重写run(创建单独的类,也可以匿名内部类)
2.实现Runable重写run(创建单独的类,也可以匿名内部类)
3.实现Callable,重写call(创建单独的类,也可以匿名内部类)
4.使用lambda表达式
5.ThreadFactory线程工厂
6.线程池

2.ReentrantLock

可重入互斥锁. 和 synchronized 定位类似, 都是用来实现互斥效果, 保证线程安全.
ReentrantLock 的用法:

  • lock(): 加锁, 如果获取不到锁就死等.
  • trylock(超时时间): 加锁, 如果获取不到锁, 等待一定的时间之后就放弃加锁.
  • unlock(): 解锁
    public static void main(String[] args) {
        ReentrantLock lock =new ReentrantLock();
        lock.lock();
        try{
            //woking
        }finally
        {
            lock.unlock();
        }
    }

优势:
1.ReentrantLock,在加锁的时候,有两种方式,lock,tryLock.——tryLock给了更多操作空间。
2.ReentrantLock,提供了公平锁的实现(默认情况下是非公平锁)
3.ReentranLock提供了更强大的等待通知机制。搭配了Condition类实现等待通知的。
虽然ReentrantLock有上述优势,但是咱们在加锁的时候,还是首选synchronized,但是很明显,ReentrantLock使用更复杂,尤其是容易忘记解锁。

3.信号量Semaphore

信号量,用来表示“可用资源的个数”,本质上就是一个计数器。描述了“可用资源”的个数
每次申请一个可用资源,就需要让计数器-1 P操作acquire
每次释放一个可用资源,就需要让计数器+1 V操作 release
(这里的+1和-1都是原子的)

信号量,假设初始情况下数值是10
每次进行P操作,数值就-1
当我已经进行了10次P操作之后,数值就是0了。
如果我继续进行P操作,会咋样?=>阻塞等待!!
锁,本质上就属于是一种特殊的信号量。锁就是可用资源为1的信号量。锁就是可用资源为1的信号量。加锁操作,P操作1-》0,解锁操作,v操作0->1——二元信号量
操作系统,提供了信号量实现,提供了api.JVM封装了这样的api,就可以在java代码中使用了。

    public static void main(String[] args) throws InterruptedException {
        Semaphore semaphore=new Semaphore(4);
        semaphore.acquire();
        System.out.println("p操作");
        semaphore.acquire();
        System.out.println("p操作");
        semaphore.acquire();
        System.out.println("p操作");
        semaphore.acquire();
        System.out.println("p操作");
        semaphore.acquire();
        System.out.println("p操作");
        semaphore.release();
    }

在这里插入图片描述

4.CountDownLatch

这个东西,主要是适用于,多个线程来完成一些列任务的时候,来衡量任务的进度是否完成。
比如需要把一个大的任务,拆分成多个小的任务,让这些任务并发的去执行。
就可以使用countDownLatch来判定说当前这些任务是否全都完成了。

下载一个文件,就可以使用多线程下载。
很多的下载工具,下载速度,很一般。
相比之下,有一些专业下载工具,就可以成倍的提升下载速度(IDM),多个线程下载,每个线程都建立一个连接,此时就需要把任务进行分割。

CountDownLatch主要有两个方法:

  1. await,调用的时候就会阻塞,就会等待其他的线程完成任务,所有的线程都完成了任务之后,此时这个await才会返回,才会继续往下走。
  2. countDown,会告诉countDownLatch,我当前这一个子任务已经完成了。
    public static void main(String[] args) throws InterruptedException {
        //10个选手参赛,await就会在10次调用完,countDown之后才能继续执行。
        CountDownLatch countDownLatch = new CountDownLatch(10);
        for(int i=0;i<10;i++)
        {
            int id =i;
            Thread t= new Thread(()->{
                System.out.println("thread"+id);
                try {
                    Thread.sleep(500);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
                //通知说当前任务执行完毕了。
                countDownLatch.countDown();
            });
            t.start();
        }
        countDownLatch.await();
        System.out.println("所有的任务都完成了");
    }

在这里插入图片描述

四、线程安全集合类

数据结构中大部分的集合类,都是线程不安全的。
Vector,Stack,Hashtable线程安全 用了synchronized
上古时期,Java引入的集合类,现在都不建议使用了,未来会被删除的内容。

1.多线程环境使用 ArrayList

1.针对这些线程不安全的集合类,要想在多线程环境下使用,就需要考虑好线程安全的问题了。

2.同时,标准库,也给我们提供了一些搭配的组件,保证线程安全。

Collections.synchronizedList(new ArrayList);

这个东西会返回一个新的对象,这个新对象,就相当于给ArrayList套上一层壳。这层壳就是在方法上直接使用synchronized的。

3.使用CopyOnWriteArrayList
CopyOnWrite容器即写时复制的容器

  • 当我们往一个容器添加元素的时候,不直接往当前容器添加,而是先将当前容器进行Copy,复制出一个新的容器,然后新的容器里添加元素,
  • 添加完元素之后,再将原容器的引用指向新的容器

2.多线程环境使用队列

  1. ArrayBlockingQueue
    基于数组实现的阻塞队列
  2. LinkedBlockingQueue
    基于链表实现的阻塞队列
  3. PriorityBlockingQueue
    基于堆实现的带优先级的阻塞队列
  4. TransferQueue
    最多只包含一个元素的阻塞队列

3.多线程环境使用哈希表

HashMap 本身不是线程安全的.
在多线程环境下使用哈希表可以使用:

  • Hashtable
  • ConcurrentHashMap
    1)Hashable
    只是简单的把关键方法加上了 synchronized 关键字
    只要两个线程,在操作同一个Hashtable就会出现锁冲突,但实际上,对于哈希表来说,锁不一定非得这么加,有些情况,其实是不涉及到线程安全问题的。
  1. ConcurrentHashMap
  • ConcurrentHashMap最核心的改进,就是把一个全局的大锁,改进成了每个链表独立的一把小锁。这样做,大幅降低了锁冲突的概率。
  • 充分利用到了CAS特性,把一些不必要加锁的环节给省略加锁了。比如需要使用变量记录hash表中的元素个数,此时,就可以使用原子操作(CAS)修改元素个数。
  • ConcurrentHashMap还有一个激进的操作,针对读操作没有加锁,读和读之间,读和写之间,都不会有锁竞争。
  • ConcurrentHashMap针对扩容操作,做出了单独的优化。本身hashtable或者HashMap在扩容的时候,都是需要把所有元素都拷贝一遍的(如果元素很多,拷贝就比较耗时)化整为零,一旦需要扩容,确实需要搬运,不是在一次操作中搬运完成,而是多分多次,来搬运,每次搬运一部分数据,避免这单次操作过于卡顿。
  • 17
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值