多线程进阶(上)

目录

一.常见的锁策略

二.CAS

三.synchronized的优化


一.常见的锁策略

1.乐观锁和悲观锁

乐观锁:从名字上来看就可以看出来,这个很乐观,这个会预期锁冲突的概率很低,就会认为锁即将要被解除了,不需要等待很久,因此上,乐观锁消耗的成本很少,因为做出一些准备去应对所竞争,所以做的工作就很少,而且效率会高一些

悲观锁:反之这个会预期锁冲突的概率会很高,会认为锁很长时间都不会被解除,需要等待很久很久,因此就会做出很多的准备,做更多的工作来应对锁竞争,从而消耗的成本就会多,效率也就更低一些

2.读写锁vs普通的互斥锁

普通的互斥锁,一般只有两种操作:加锁和解锁,因此只要两个线程对同一个对象进行加锁的话,就会产生互斥

而读写锁分成了三个操作加读锁,加写锁,解锁,这里和读和写分开了,如果只是进行了读操作就加读锁,如果进行了修改操作就加写锁,这样分有什么好处呢?我们知道读只是一步指令,是一个原子性的,因此读锁和读锁之间是没有互斥关系的,读锁和写锁,写锁和写锁之间才需要互斥,因为写操作不是原子性的,根据线程的随机调度,很可能出现问题,因此只要是写锁在多线程里面就会出现问题,就需要让他们互斥,这里把读操作分离出来,就是让在读和读之间不再加锁,提高效率的方法

3.重量级锁vs轻量级锁(和上面的乐观锁和悲观锁有一定重叠)

重量级锁:做的工作量大,开销更大

轻量级锁:做的工作量小,开销更小

如果锁是基于内核的一些功能进行实现的话(比如调用了操作系统提供的mutex接口),一般认为这是重量级锁(操作系统的锁会在内核中做很多的事情,比如让线程阻塞等待)

如果锁是纯用户态实现的,一般认为是轻量级锁(用户态的代码更可控更高效)

4.挂机等待锁vs自旋锁

挂起等待锁:往往是通过内核的一些机制来实现的,往往比较重,也就是上面的重量级锁(重量级锁的典型实现)

自旋锁:往往是通过用户态代码来实现的,往往较轻,(轻量级锁的典型实现)

5.公平锁vs非公平锁

公平锁:多个线程在等待一把锁的时候,遵守先来后到的规则,谁先来谁就能先获得这个锁

非公平锁:多个线程在等待一把锁的时候,不遵守先来后到的规则,后面的每个线程获得锁的概率都是均等的,不需要看谁是先来的

对于操作系统来说,本身线程的调度就是随机的(概率均等的),操作系统提供的mutex这个锁就是非公平锁,而要实现一个公平锁的话,就要使用一个队列,来在保证线程的先来后到,反而要付出更多的代价

6.可重入锁vs不可重入锁

一个线程针对一把锁,连续的加两次锁,如果出现死锁,就是不可重入锁,没有出现死锁的话,就是可重入锁,具体可以再看一下我前面对于可重入锁的解释:线程加锁关键字_栋zzzz的博客-CSDN博客

二.CAS

全称:compare and swap(比较和交换),那这个是做什么的呢?简单介绍一下:

简单来说就是拿着寄存器/某个内存中的值和另一个内存的值进行比较,如果值相同了,就把另一个寄存器/内存的值和当前的值进行交换,

举个例子:内存中的原数据V,旧的预期值A,需要修改的新值B

1.比较A和V是否相等(比较)

2.如果比较相等,将B写入到V(交换)

3.返回操作是否成功

此处的CAS指的是CPU提供的一个单独的CAS指令,通过这一条指令,就可以完成上述的过程!(本来这些不是一条指令的话,就是有可能存在线程安全问题的,而将这些操作融合为一条指令,就是原子性的了,指令就已经是最小的分割单位了,因此就不再有线程安全问题了)

CAS最大意义就是在编写多线程安全的代码的时候,有一条新的思路和方向(和锁不一样),就像刚才的比较交换逻辑,相当于是硬件直接实现出来了,通过一条指令封装号,直接就可以使用了

CAS如何帮我们解决线程安全问题?

1.基于CAS能提供实现"原子类"

java标准库提供了一组原子类,针对常用的一些int,long,int array进行了封装,可以基于CAS的方式进行修改,让其线程安全

import java.util.concurrent.atomic.AtomicInteger;
public class Demo27 {
    public static void main(String[] args) throws InterruptedException {
        AtomicInteger count = new AtomicInteger(0);//原子类,初始化一个0
        Thread t1 = new Thread(()->{
            for (int i = 0; i < 500; i++) {
                count.getAndIncrement();//这个操作相当于num++

            }
        });
        Thread t2 = new Thread(()->{
            for (int i = 0; i < 500; i++) {
                count.getAndIncrement();//这个操作相当于num++

            }
        });
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        System.out.println(count.get());//通过get方法获得到原子类内部的值
    }
}

 这个如果我们不使用这样的原子类的话出来的结果一定是在500-1000之间的,是存在问题的,而我们使用了这样的原子类,就不存在线程安全了这是基于CAS实现的++操作,这里就可以既保证线程安全,有比synchronized操作更高效,因为synchronized操作是会导致线程相互等待的,而此处的CAS是不涉及到线程阻塞等待的,因而更高效

2.基于CAS能实现自旋锁

3.这里最为重要的还是CAS里面的ABA问题 

CAS中的关键,是先比较,再交换,比较其实在比较当前值和旧值是否相同,这两个值相同的话就视为这个值中间没有发生过变化,但是真的一定没发生过变化吗?

这是存在一些问题的,有可能当前值和旧值相同中间确实没有发生过变化,但是也是有可能中间发生变化然后又变回来了呢!其实这样的问题在大多数问题下都是没有影响的,但是极端情况下也是会引起bug的!

此时我们发现结果是正确的,但是在我们引入ABA问题之后就可以结果会出现差错了 

 

此时就会发现出现差错了,本来要取款一次的结果取了两次,这就是ABA问题,那么该如何解决呢? 

 可以引入一个"版本号",这个版本号只能变大,不能变小,比较的时候就不是比较变量本身了,而是比较版本号了

利用"版本号",我们就可以很好的规避ABA问题了,这种基于版本号的方式进行多线程数据的控制,也是一种乐观锁的典型实现

三.synchronized的优化

对于前面我们学习过的synchronized锁是一个什么样的锁呢:

1.既是一个乐观锁,也是一个悲观锁(根据锁竞争的激烈程度,自适应)

2.是一个普通的互斥锁,不是读写锁

3.既是一个轻量级锁也是一个重量级锁(根据锁竞争的激烈程度,自适应)

4.轻量级锁部分基于自旋锁实现,重量级锁部分基于挂起等待锁来实现

5.非公平锁(通过竞争获得锁)

6.可重入锁

那么对于synchronized的优化有哪些呢?

synchronized几个典型的优化手段:

1.锁膨胀/锁升级

 这个就体现了synchronized的"自适应"的能力

 根据锁的竞争,就可以自适应的进行锁状态,加锁解锁的开销也更小,这样来保证高效性

2.锁粗化/细化

这里的粗细指的是"锁的粒度"(粒度表示加锁代码涉及到的代码范围,加锁代码的范围越大,认为锁的粒度越粗,加锁代码范围越小,锁的粒度越细)

因此锁的粗化也是会在一定程度上帮助程序员来优化代码效率的! 

3.锁消除

这里表示再有些地方,可能并不需要加锁,但是你不小心加上了锁,编译器发现加上这个锁好像没啥必要,编译器就会直接把锁给去掉了

比如你在单线程里面使用了StringBuffer,Vector(这些都在标准库里面进行了加锁操作),就相当于是你在单线程里面进行了加锁,但是这里编译器会自主的做出判定,从而取消掉锁的加锁和解锁,从而提高代码的效率!

猜你喜欢

转载自blog.csdn.net/qq_58266033/article/details/123859258