JAVA线程进阶

目录

一、锁策略

1、乐观锁和悲观锁

2、轻量级锁和重量级锁

3、自旋锁和挂起等待锁

4、普通互斥锁和读写锁

5、公平锁和非公平锁

6、可重入锁和不可重入锁

二、synchronized锁和系统原生锁特点

三、synchronize锁内部原理

1、升级过程

2、锁消除

3、锁粗化

四、CAS(compare and swap)

1、概念

2、应用

3、CAS实现原理

4、CAS的ABA问题

一、锁策略

1、乐观锁和悲观锁

乐观锁:在加锁之前预估当前出现锁冲突的概率不大,加锁过程做的事情比较少,加锁速度快,但也会引起其他问题,例如:消耗更多的cpu资源。

悲观锁:在加锁之前预估当前出现锁冲突的概率较大,加锁过程做的事情就会较多,加锁速度慢,但是整个过程不容易出现其他问题。

2、轻量级锁和重量级锁

轻量级锁:加锁的开销小,加锁速度快。一般就是乐观锁。

重量级锁:加锁的开销大,加锁速度慢。一般就是悲观锁。

3、自旋锁和挂起等待锁

自旋锁:轻量级锁的一种典型实现。加锁时搭配while循环使用,当加锁成功时,自然结束循环;当加锁不成功时,也不会阻塞放弃cpu,而是进行新一轮的尝试加锁。这个反复快速执行的过程称为“自旋”,其他线程一旦释放锁,使用自旋锁的线程能第一时间拿到锁。自旋锁也是乐观锁,出现锁冲突的概率不大,循环执行次数少,cpu消耗少。如果锁冲突概率大,就会一直自旋,浪费cpu.

挂起等待锁:重量级锁的一种典型实现。当某个线程没有申请到锁的时候,此时该线程会被挂起,即加入到等待队列等待。当锁被释放的时候,就会被唤醒,重新竞争锁。所以该线程真正拿到锁的时间就会长一些。可用于锁冲突概率较大的情况下。

4、普通互斥锁和读写锁

普通互斥锁:同一时刻同一个对象,只能有一个线程进行访问,当其他线程想获取锁访问时,该线程获取锁失败,线程进入睡眠,等待其他线程释放锁后被唤醒。

读写锁:将加锁分成两种情况:加读锁和加写锁。读锁和读锁之间不会出现锁冲突,允许多个线程同时获得读操作;写锁和写锁之间会出现锁冲突,同一时刻同一对象只能有一个线程获取到写操作,其他线程获取失败只能进入睡眠状态,当锁被释放时就会被唤醒;读锁和写锁之间会出现锁冲突,写锁会阻塞其他读锁,当线程获取到写锁后,其他线程想获取读锁也会被阻塞。

为什么要引入读写锁???synchronized锁不是读写锁,对于一些只涉及到读的操作,本身就是线程安全的,加synchronized,读操作之间互斥,会降低效率。但是对读操作不加锁也不行,对于有些线程,既有读的操作,也有写的操作,会存在线程不安全问题,需要加锁。读写锁就很好的解决了上述问题。

5、公平锁和非公平锁

公平的定义:先来后到的顺序。

要想实现公平锁,就需要额外引入数据结构(队列,存放锁的先后顺序)。使用公平锁,可以解决线程饿死问题。

synchronized锁是非公平锁,线程的调度是无序的。

6、可重入锁和不可重入锁

可重入锁:对于同一个线程,加锁两次,不会出现死锁问题,只会锁计数器+-1;

不可重入锁:对于同一个线程,加锁两次,会出现死锁问题。

二、synchronized锁和系统原生锁特点

1、synchronized锁

乐观锁/悲观锁自适应;轻量级锁/重量级锁自适应;自旋锁/挂起等待锁自适应;是非公平锁;可重入锁;普通互斥锁(不是读写锁)

2、系统原生锁(Linux提供的mutex锁)

悲观锁;重量级锁;挂起等待锁;非公平锁;不可重入锁;普通互斥锁(不是读写锁)

三、synchronize锁内部原理

1、升级过程

①偏向锁阶段

该阶段,并没有加锁。由于没有其他线程竞争,只是做了一个非常轻量的标记。如果一直没有锁竞争,整个过程就把加锁的操作省略了,也不会有互斥现象。

②轻量级锁阶段

一旦有其他线程来竞争锁,该线程就会在另一个线程之前获取到锁,将锁升级为轻量级锁。synchronized内部也会统计当前锁对象上有多少个线程在竞争锁,有竞争但数目不多,就是轻量级锁,其他未获取到锁对象的线程自旋获取锁。

③重量级锁阶段

synchronized内部统计发现当前锁对象上有多个线程在竞争锁,大量线程自旋获取锁的话cpu消耗就多了,此时拿不到锁的线程就转为重量级锁,不再自旋获取锁,而是挂起等待,让出cpu。当线程释放锁后,再随机唤醒一个阻塞等待的线程。

注:该过程只能升级,不能降级!!!

2、锁消除

synchronized内置的一种优化策略。在编译代码时,发现代码不需要加锁,就会自动把锁干掉。比如:只有一个线程或者线程里没有涉及到成员变量的修改等等。

3、锁粗化

针对同一个锁对象,把多个细粒度的锁合并成一个粗粒度的锁。一般情况下,synchronized{}大括号里代码越少,就认为锁的粒度越细,代码越多,就认为锁的粒度越粗。粗化是为了提高效率,细化有利于多个线程并发执行。

四、CAS(compare and swap)

1、概念

是一个特殊的cpu指令,完成的工作就是比较和交换。

伪代码:

address是内存地址,expectValue是expect寄存器的值,swapValue是swap寄存器的值。将内存地址的值与expect寄存器的值比较,如相同则将内存地址的值与swapValue的值交换,并且返回true;如不相同,则返回false。

以上cpu指令是原子的。

2、应用

考虑到CAS指令是原子的,基于CAS指令,可以考虑实现多线程的线程安全问题。

public class test {
    private static int count=0;
    public static void main(String[] args) throws InterruptedException {
        Object lock=new Object();
        Thread t1=new Thread(()->{
            for (int i = 0; i < 5000; i++) {
                synchronized (lock){
                    count++;
                }
            }
        });
        Thread t2=new Thread(()->{
            for (int i = 0; i < 5000; i++) {
                synchronized (lock){
                    count++;
                }
            }
        });
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        System.out.println(count);
    }
}

以上对于线程不安全问题采用的是加锁,将count++变成原子的,基于CAS,不加锁实现上述代码.

java标准库中对于CAS进行了封装,放在了unsafe包里,提供了一些工具类可以直接使用。最主要的工具是原子类。

针对以上对象进行多线程修改时,就是线程安全的。

import java.util.concurrent.atomic.AtomicInteger;

public class test1 {
    private static AtomicInteger count=new AtomicInteger(0);

    public static void main(String[] args) throws InterruptedException {
        Thread t1=new Thread(()->{
            for (int i = 0; i < 5000; i++) {
                count.getAndIncrement(); //相当于count++
            }
        });
        Thread t2=new Thread(()->{
            for (int i = 0; i < 5000; i++) {
                count.getAndIncrement(); //相当于count++
            }
        });
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        System.out.println(count.get());
    }
}

以上count是原子类整型,多个线程对他进行修改时是线程安全的。

针对count对象还有其他操作:

3、CAS实现原理

上述过程并未加锁,CAS是怎么实现线程安全的???

针对上述实现count++,伪代码:

value是内存中的值,oldValue是寄存器中的值,若发现内存中的值和寄存器中的值一样,就交换,将oldValue+1的值与value值交换,返回True,结束循环;若发现内存中的值和寄存器中的值不一样(其他线程改过),进入循环,重新获取内存值赋值于寄存器,再次比较交换。

之前线程不安全是因为内存中的值变了,但是寄存器中的值未改变。CAS指令可以识别出内存中的值是否改变,未改变就交换,改变了就重新获取内存值,然后再比较交换。

以上比较和交换是自旋式的,会消耗更多的cpu资源,但不会引起线程阻塞。

4、CAS的ABA问题

(1)什么是ABA问题

假设存在两个线程t1、t2,他们拥有共享变量value,value的初始值为A,线程t1想使用CAS把value修改为C,此时就需要先读取value的值,并赋值给oldValue寄存器中;判断当前value的值是否等于oldValue,若等于,将value值交换变为C。但是在进行这两个操作之间,线程t2可能把value的值修改为B,又从B修改为A。线程t1在进行判断时,无法判断value始终是这个值还是经历了变化。

(2)ABA问题导致的BUG

假设现在余额是100,取款50元,有两个线程t1、t2来执行这个任务。

正常情况:线程t1执行CAS(value,oldvalue,oldvalue-50),value==oldvalue,value与oldvalue-50交换,变为50,返回true;线程t2执行CAS(value,oldvalue,oldvalue-50),value!=oldvalue,执行失败,返回false。即:总体就取款了50

异常情况:线程t1执行CAS(value,oldvalue,oldvalue-50),value==oldvalue,value与oldvalue-50交换,变为50,返回true;此时又来了个线程t3执行CAS(value,oldvalue,oldvalue+50),value与oldvalue+50交换,变为100,返回true;然后线程t2执行CAS(value,oldvalue,oldvalue-50),value==oldvalue,value与oldvalue-50交换,变为50,返回true;即:总体就取款了两个50。

(3)如何解决ABA问题

给要修改的值引入版本号,在比较当前value的值和oldvalue值是否相同的同时,也要比较版本号是否符合预期。在value的值和oldvalue值相同时,如当前版本号和之前读到的版本号一样,则修改数据,并将版本号+1,如当前版本号高于之前读到的版本号,认为数据已被修改过,操作失败。

  • 18
    点赞
  • 26
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

ambition…

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值