自学内容网 自学内容网

Java多线程进阶(锁策略)

常见的锁策略:

1)悲观锁和乐观锁

加锁的时候,预测当前锁冲突的概率是大还是小。

预测当前锁冲突概率大,后续要做的工作往往就会更多,加锁开销就更大(时间,系统资源)=》悲观锁

预测当前锁冲突概率不大,后续要做的工作往往就更少,加锁开销就更小(时间,系统资源)=》乐观锁

synchronzied既是乐观锁,也是悲观锁

synchronzied支持自适应,能够自动的统计出当前的锁冲突的次数,进行判定当前锁冲突概率低还是概率高。

当冲突概率低的时候,就是按照乐观锁的方式来执行的。(速度更快)

当锁冲突概率高的时候,就会升级悲观锁的方式来执行(做的工作更多)

2)重量级锁和轻量级锁

一般来说,悲观锁,往往就是重量级锁,乐观锁就是轻量级锁。

3)自旋锁和挂起等待锁

自旋锁,是轻量级锁的一种典型的实现方式

消耗了更多的cpu资源,但是一旦锁被释放,就能第一时间拿到锁。拿到锁的速度很快。消耗cpu

挂起等待锁,是重量级锁的一种典型实现方式。

借助系统中的线程,当尝试加锁,并且锁被占用了,出现锁冲突,就会让当前这个尝试加锁的线程,被挂起(阻塞状态)此时这个线程就不会参与调度了。

直到这个线程被释放了,然后系统才能唤醒这个线程,去尝试重新获取锁。

(拿到锁的速度更慢,节省cpu)

synchronzied 轻量级锁部分,基于自旋锁实现,重量级锁部分,基于挂起等待锁实现。(基于CAS机制来实现)

4)可重入锁和不可重入锁

synchronzied就是可重入锁,一个线程,针对这把锁,连续加锁两次,不会死锁。

比如c++de std::mutex就是不可重入锁,一个线程针对这把锁,连续加锁两次,会死锁。

5)公平锁和非公平锁

公平锁:严格按照先来后到的顺序来获取锁,哪个线程等待的时间长,哪个线程就拿到锁。

非公平锁:若干个线程,各凭本事,随机的获取到锁,和线程等待时间无关了。

synchronized属于非公平锁,多个线程,尝试获取这个锁,此时是按照概率均等的方式来进行获取的。

{系统本身线程调度的顺序就是随机的,如果需要实现公平锁,就需要引入额外的队列,按照加锁顺序把这些获取锁的线程的线程入队列,再一个一个的取}

6)互斥锁和读写锁

synchronized本事就是普通的互斥锁

读写锁,则是一个更特殊的锁(加读锁,加写锁,解锁)

Java的读写锁是这样设定的:

1)读锁和读锁之间,不会产生互斥

2)写锁和写锁之间,会产生互斥

3)读锁和写锁之间,会产生互斥

synchronized实现原理:

synchronized即使悲观锁也是乐观锁,既是轻量级锁也是重量级锁,轻量级锁是自旋转实现,重量级锁就是挂起等待锁实现,是可重入锁,不是读写锁,是非公平锁

synchronized的“自适应”

锁升级的过程:

1)未加锁的状态(无锁){代码中开始调用执行synchronized}

2)偏向锁

3)轻量级锁

4)重量级锁

偏向锁的过程:

首次使用synchronized对对象进行加锁的时候,不是真的加锁,而只是做一个“标记”(非常轻量,非常快,几乎没有开销)

如果没有别的线程尝试对这个对象加锁,就可以保持这个暧昧状态,一直到解锁。

但是,如果在偏向锁状态下,有某个线程也尝试来对这个对象加锁,立马把偏向锁升级成轻量级锁(真的加锁,真的有互斥了),相当于立即确认关系,也就可以保证锁能够正常生效。

本质上,偏向锁策略就是“懒”字具体体现,能不加锁,就不加锁,能晚加锁,就晚加锁。

针对一个锁对象来说,是不可逆的,只能升级,不能降级。

一旦升级到了重量级锁,不会回退到轻量级锁(当前jvm里面的做法)

7)锁消除,编译器优化策略

代码里面写了加锁操作,jvm会对当前的代码做出判定,看这个地方到底是不是真的需要加锁。

如果这个不需要加锁,就会自动的把加锁操作,给优化掉。

8)锁粗话,也是一种优化策略

有些逻辑中,需要频繁加锁频繁,编译器就会自动的把多次细粒度的锁,合并成一次粗粒度的锁。

三个简单的加锁操作,合并成一个加锁操作。

CAS compare and swap 比较和交换

这是一条cpu(原子的)指令,就可以完成比较和交换这样的一套操作下来。

CAS的流程==》想象成一个方法

  boolean cas(address,reg1,reg2){
            if(address ==reg1){}
            //把address内存地址的值和reg2寄存器的值进行交换
            return true;
        }
        return false;

这里说的交换,实际上更多的是来“赋值”,一般更关心内存中,交换后的数据。而不关心reg2寄存器里交换后的数据。

由于cpu提供了上述指令,因此操作系统内核,也就能够完成上述操作,就会提供出这样的CAS的api,jvm又对于系统的CASapi进一步的封装了,在Java代码中也就可以使用CAS操作了。

在Java中也有一些类,对cas进行了进一步的封装,典型的就是原子类。

Atomiclnteger相当于针对int进行了封装。可以保证此处的++--操作是原子的

package Thread;

import java.util.concurrent.atomic.AtomicInteger;

public class Demo25 {
    private static AtomicInteger count=new AtomicInteger(0);
    public static void main(String[] args) throws InterruptedException {
        Thread t=new Thread(() ->{
            for (int i = 0; i <10000 ; i++) {
                count.getAndIncrement();

            }

        });
        Thread t1=new Thread(() ->{
            for (int i = 0; i < 50000; i++) {
                count.getAndIncrement();
            }
        });
        t.start();
        t1.start();
        t.join();
        t1.join();
        System.out.println(count);
    }
}

CAS的ABA问题

CAS这里的核心是比较-发现相等-交换=》潜台词 发现相等=》数据中间没有发生过任何改变


原文地址:https://blog.csdn.net/2302_80113478/article/details/143644882

免责声明:本站文章内容转载自网络资源,如本站内容侵犯了原著者的合法权益,可联系本站删除。更多内容请关注自学内容网(zxcms.com)!