多线程学习总结(七)

目录

一.前言:

二.CAS介绍:

(1)什么是CAS:

(2)CAS的应用:

(3)CAS的ABA问题:

三.JUC常见类:

(1)Callable接口:

(2)ReentrantLock——可重入锁:

(3)信号量Semaphore:

(4)CountDownLatch:

(5)线程安全的集合类常用利器之CopyOnWrite容器:

(6)ConcurrentHashMap——线程安全的哈希表:

四.总结:


一.前言:

搁置了很久的多线程,终于也是在别人的死亡问题下想起来了,有始有终,本篇也是多线程学习总结的最后一篇了,下面进入正题。

二.CAS介绍:

(1)什么是CAS:

CAS全称“Compare and swap”,译为“比较并交换”,CAS也是一种类似锁的实现机制,CAS本质上是由一个原子的硬件指令完成的,它的工作大致可以表述为“对比寄存器中的值跟内存地址中的值是否相等,如果相等则将内存地址中的值跟另一个寄存器中的值进行交换”,它的工作流程大致可以表示如下:

boolean CAS(address, expectvalue, swapvalue) {
    if(*address == expectvalue) {
        //相等就将address中存储的值跟swapvalue中的值进行交换

        //并返回true
        return true;
    }
    return false;
}

此处只是以伪代码的形式进行举例,包括上述伪代码,也并非是原子的,而真正的CAS操作则是原子的。由于CAS是一条原子的指令,这也就让线程安全有了新的思路出现——基于CAS衍生出的无锁编程,当然,无锁编程并非真的无锁,而是通过CAS来实现类似加锁的效果。(上述伪代码中,address是内存地址,expectvalue跟swapvalue则是两个寄存器,address中寄存着要修改的值,expectvalue寄存器中存储着改变前的值,swapvalue寄存器中存储着改变后的值)

(2)CAS的应用:

1.实现原子类:

class AtomicInteger {
    private int value;
    public int getAndIncrement() {
        int oldvalue = value;
        while(CAS(value, olevalue, olevalue+1) != true) {
            oldvalue = value;
        }
        return oldvalue;
    }
}

以上伪代码就是基于CAS实现原子类的大致流程,其中的value就是要修改的值,oldvalue为修改前的值,而oldvalue+1就是修改后的值,CAS方法则对应的是CAS指令,因为此处是伪代码,所以参数直接传递的变量本身,而真正的CAS指令中,value位置指代的应该是要修改的变量的内存地址,oldvalue和oldvalue+1则是存储了改变前后值的寄存器,通过CAS指令来判断其他线程是否有在修改value,如果有的话,则会因为CAS指令返回false而进入循环,并将其他线程修改后的值交给oldvalue中,使得本线程可以第一时间获取到其他线程的修改情况,避免出现多线程安全的问题。在上述伪代码中,并没有加锁操作,但是却通过CAS指令实现了一种类似于加锁的效果,这也就是前面所说的基于CAS实现的“无锁编程”。

2.实现自旋锁:

public class SpinLock {
    private Thread owner = null;
    public void lock() {
        while(!CAS(this.owner, null, Thread.currentThread)) {
        }
    }
    
    public void unlock() {
        this.owner = null;
    }
}

上述伪代码就是通过CAS来实现的自旋锁,大概原理如下:owner表示持有当前锁的线程,通过CAS判断,当前锁是否被某个线程所持有了,如果已经被其他线程持有了,则进入循环等待,如果未被其他线程所持有,则会尝试让当前线程持有该锁(就是把owner设置成当前准备加锁的线程)。

基于CAS的应用还有很多,包括各种锁的仿照实现等等,掌握CAS应用的相关实现并不是最重要的,理解掌握它的原理,才是最重要的。

(3)CAS的ABA问题:

首先来了解一下,什么是ABA问题。ABA问题指的是“从A改变为B又改成A”这样的类似情况。而CAS的原理,又恰好是通过判断修改情况来进行操作的,那么,如果此时有某个线程对变量进行了修改,但很快又修改回去了,此时,对于其他线程的CAS操作,就有可能会产生影响了。不过,虽然会产生影响,但是在大多数场景下,是不会造成什么恶略的后果的,不过,凡事都有例外,一旦遇上了某些极端情况,那就不好说了,既然有可能造成不好的后果,就需要应对其找出解决办法,所以ABA问题也有他自己的解决方法,具体如下:通过前面介绍CAS的作用原理可以知道,CAS的本质并不是看值的数值是否改变,而是看某一个变量是否发生过变化,如果有,就会触发CAS指令,那么,既然关注的是变量是否发生过改变,那我们就可以尝试引入一个只会单向改变的变量,并以这个变量为标志,判断当前要修改的变量是否发生过改变,但是,有什么变量是只会单向改变的呢?那就是“版本号”,版本号是永远只加不减的,所以,我们就可以通过在CAS操作中引入版本号的方法来解决ABA问题。

三.JUC常见类:

(1)Callable接口:

Callable接口也是创建线程的一种方式,其是通过内部的call方法来进行创建的,与run方法不同的是,call方法有一个具体的返回值,返回值类型可以通过泛型参数来指定。不过在前面初学多线程的时候就已经提到了多种创建线程的方法了(前面其实也提到了用Callable接口),它们各有不同的特点,那么,Callable接口创建线程的方式又有什么特点呢?

当多线程操作只关注线程运行的过程时,直接重写run方法即可,因为并不关注线程最终运行成什么样,也并不关注线程的最终结果,但是,当多线程操作开始关注线程的结果时,使用run方法就不太稳妥了,此时,使用call方法就更为合适了,因为call方法是具有返回值的,可以更加便利的使主线程或其他线程获取到本线程的返回值,这也就是Callable接口创建线程的主要特点。演示代码如下:

public class Main {
    public static void main() {
        Callable<Integer> callable = new Callable<>() {
            @Override
            public Integer call() throws Exception {
                int sum = 0;
                for(int i = 1; i <= 1000; i++) {
                    sum += i;
                }
                return sum;
            }
        };
        FutureTask<Integer> task = new FutureTask<>(callable);
        Thread t = new Thread(task);
        t.start();
        Integer re = task.get();
        System.out.println(re);
    }
}

上述代码中,不要关注线程内干了什么,重点在于,Callable接口本身无法启动线程,还需要将它交给Thread类,但是,在这中间,却又引入了一个FutureTask类,这是因为Callable接口是无法直接最为参数传递给Thread构造方法的,它需要使用FutureTask类最为中间人将Callable再封装后再传递给Thread的构造方法。除此以外,FutureTask类还有一个至关重要的作用,那就是,通过FutureTask类中的get方法,才可以获取到call方法的返回值(因为Callable接口无法直接启动线程,自然也就不能直接通过call方法赋值的形式来获取返回值)。(这里再补充一下get方法,可能会有人有疑惑,get方法什么时候获取返回值呢?会不会获取出问题呢?其实get方法跟join方法是类似的,当call方法未执行完毕时,get方法就会进入阻塞等待的状态,并不会出现提前获取,获取不到等状况)

(2)ReentrantLock——可重入锁:

ReentrantLock也是一种锁,也可以在需要加锁时使用,不过它的使用频率并不像synchronized那么高,但是这边并不意味着它的使用就没有必要,因为,ReentrantLock拥有一些只有它自己才有的特点。在介绍它的特点之前,先来看一下ReentrantLock的加锁解锁方式:

public class Main{
    public static void main() {
        ReentrantLock locker = new ReentrantLock();
        locker.lock();//加锁
        locker.unlock();//解锁
    }
}

介绍完了它的加锁解锁方法后,接下来就是它的特点了,如下:

特点一,tryLock方法——对比原本的加锁操作,例如lock方法或者synchronized方法等,它们的加锁方式非常的简单直接,同时,对于无法成功加锁的处理也很直接,就是一直阻塞等待到能加锁了为止,这就使得一个线程的效率大大降低了,而tryLock方法,则是译为尝试加锁,如果加锁不成功,就会放弃掉当前加锁,而不是一直阻塞等待,不仅如此,tryLock方法是支持传参的,它的参数就是等待时间,我们可以手动传递一个等待时间给tryLock方法,这样就可以大大提高线程的效率了。

特点二,ReentrantLock的两种模式——ReentrantLock是支持两种锁的模式的,一种是公平锁,另一种是非公平锁,跟常用的synchronized不同,synchronized只有非公平锁这一种模式,如果想要使用公平锁,它是做不到的。ReentrantLock设置公平锁或非公平锁的方式也非常简单,通过在构造方法中传入不同的参数即可完成设置。

特点三,ReentrantLock的等待通知机制——ReentrantLock也是有等待通知机制的,就像wait跟notify一样,不过不同的是,ReentrantLock的等待通知需要搭配Condition类来实现,虽然相较wait和notify麻烦了一些,但是ReentrantLock的等待通知机制也是要比wait和notify强的,其中最明显的一点就是,如果使用wait和notify,那么,当多个线程被wait时,notify只能随机的唤醒一个线程,而无法明确的指定唤醒某个特定的线程,但如果使用的是ReentrantLock的等待通知,那么就可以进行指定,从而唤醒某个特定的线程。

说了那么多ReentrantLock的优势,接下来也来说说它的略势吧,首先就是它的解锁操作,由于ReentrantLock的解锁和加锁操作是分开的,这就导致,很容易把解锁操作给遗漏掉,或者程序执行到一般出现了异常,没有执行到解锁操作,这都是有可能的,那么这时候就需要我们手动写一个finally来进行强制运行到解锁操作了,这是ReentrantLock的第一个略势,也是最关键的一个,其次,ReentrantLock它不像synchronized可以给任意一个对象进行加锁,ReentrantLock它就只能给自己加锁,一旦多个线程调用了自己的ReentrantLock对象,这个时候是无法产生锁竞争的,也就很容易产生线程安全问题,最后,ReentrantLock也不像synchronized,被进行了各种优化,这些优化在ReentrantLock身上基本是没有的。

(3)信号量Semaphore:

信号量其实是一个计数器,描述了当前线程的“临界资源数量(当多个线程共同修改一个变量时,这个变量就可以称为临界资源)”,再简单一点描述就是,判断当前线程是否还有可用资源,并记录一下有多少。

介绍完信号量是什么后,再来介绍一下信号量中的两个重要操作,“P操作(accquire)”和“V操作(release)”。那么,什么是“P操作”,什么又是“V操作”呢?“P操作”就是为线程申请一个“临界资源”,而,“V操作”则是为线程释放一个“临界资源”。这里的所谓的“P操作”和“V操作”实际上对应代码中的两个库方法,“accquire”和“release”,至于为什么叫“P操作”、“V操作”,这里的原因就跟内容无关了,不做讨论。接下来演示一下,信号量的使用:

public class Main {
    public static void main(String[] args) throws InterruptedException {
        //创建信号量为4的计数器
        Semaphore semaphore = new Semaphore(4);
        //申请使用一个临界资源
        semaphore.acquire();
        //释放一个正在被使用的临界资源
        semaphore.release();
    }
}

在上述代码中,我们创建了一个信号量为4的计数器(Semaphore构造方法中传递的参数指的是信号量为多少,类似大小),并进行了一次使用跟释放,这里要注意,如果修改一下上述代码,进行多次申请但是却没有释放的话,一旦申请的数量超过了信号量的大小,就会进入阻塞等待,直到执行了释放方法,这个过程就类似于锁竞争,这是因为,锁,其实是一个特殊的信号量,它的大小就只有0和1,这种信号量称之为“二元信号量” 。

(4)CountDownLatch:

CountDownLatch用于判断多个线程的执行结果(就是结没结束),通过在每个线程中调用countDown方法并在主线程中使用await方法,即可使主线程得知每个线程的执行状态(这里的执行执行状态仅指是否执行结束),具体的用法如下:

public class Main {
    public static void main(String[] args) throws InterruptedException {
        CountDownLatch latch = new CountDownLatch(10);
        for(int i = 1; i <= 10; i++) {
            int n = i;
            Thread t = new Thread(() -> {
                System.out.println(n);
                latch.countDown();
            });
            t.start();
        }
        latch.await();
        System.out.println("全部线程已执行完毕");
    }
}

上述代码进行了一个简单的演示,那么它的逻辑是怎样的呢?首先,在创建CountDownLatch类对象时,会在构造方法中传入一个数字,代表CountDownLatch对象要统计几个线程的执行状态,然后,由countDown方法来进行计数,最后,由await方法进行判断,判断当前结束的线程总量是否达到了设置量,如果没有就会进入阻塞等待,直到符合要求就证明当前线程都已经执行完毕了。

(5)线程安全的集合类常用利器之CopyOnWrite容器:

看容器的名字就大概能猜到,这个容器的本质时通过复制再修改的方式来保证线程的安全性的,那么具体的过程是什么呢?

首先,这个容器的名字叫做“写时复制容器”,当我们向一个容器中添加元素时,不会直接添加到容器中,而是先将容器复制下来,并将要添加的元素先添加到复制好的容器中,再让原容器的引用指向新复制的容器,这样一来,我们直接对原容器进行读操作即可,而并发的读操作是不会有线程安全问题的,所以也不需要加锁。其实这里的原理跟CAS指令也有一点相似之处,CAS的原理也是将待修改的值先进行复制,以确保即便有其他线程修改了当前变量,本线程也可以准确的找出最初拿到的变量的值。上述过程使CountDownLatch极大地提高了线程的执行效率,不过,它也有一个很突出的缺点,就是占用的内存比较多,并且,新写入的数据无法第一时间读取到。

(6)ConcurrentHashMap——线程安全的哈希表:

再说ConcurrentHashMap之前,先来讨论一下另一个同样线程安全的哈希表——HashTable,HashTable也是一个线程安全的哈希表,它的各种操作内部都是进行了加锁操作的,这就使得它无论在那种情况下,基本都不会有线程安全问题,但同时,再来思考一个问题,什么都要加锁,而多线程又很容易产生锁竞争,那么,这时候就会使效率大大降低,包括在单线程中,即便有编译器的锁消除优化,但,这依旧是不可靠的,因为我们不可控,所以,这时候,就需要一种新的解决方案了,那就是ConcurrentHashMap,同样是线程安全的哈希表,但,ConcurrentHashMap却不是像HashTable那样,什么都要进行加锁,在ConcurrentHashMap内部,是有对加锁操作进行优化的,下面就来介绍一下它的优化之处:

1.ConcurrentHashMap采用了降低锁的粒度的方式来进行了第一个优化,它不再像HashTable那样,直接就给整体进行加锁操作了,而是选择给每条链表加上一个锁,这样一来就只有针对每个链操作的时候才会产生锁竞争了,大部分的时候,都不会有锁竞争出现。(核心优化)

2.ConcurrentHashMap的方法内部频繁地使用了CAS指令,很好的规避了加锁操作,减少了锁竞争。

3.只针对写操作进行了加锁(链表级的加锁),而HashTable不论读写都有加锁。这里再来考虑一个问题,假如,一个线程读,一个线程写,那会不会出现问题呢?其实还是不会的,因为在其内部还有一些其它的操作(使用了volatile关键字来保证可以及时读到新修改的数据),来帮我们规避了这一问题。(同时,即便是自己解决也不是做不到的,使用读写锁就可以了)

4.针对扩容操作进行了优化,采用了渐进式扩容的方法。HashTable的扩容我们是知道的,它是直接将整个原哈希表搬到一个更大的空间中,这个操作是相当重量级的过程,而渐进式扩容,则是当需要进行扩容时,系统会先创建出一个更大的数组,然后依照我们的访问请求,逐步的将原哈希表的内容搬运到新的空间中,这也会使得有一段时间内,会出现新旧数组同时存在的现象。

了解了ConcurrentHashMap的优化之后,再来讨论一下,ConcurrentHashMap和HashTable还有HashMap的区别。首先是三者最本质的区别,在这三者中,只有HashMap是线程不安全的,并且它还允许key为null;再来是HashTable,它虽然是线程安全的,但是它是通过synchronized对HashTable对象进行加锁的方式来实现的,效率低下,同时,它的key是不允许为null的;最后就是ConcurrentHashMap了,相较于前两者,它不仅是线程安全的,同时还进行了各种优化(降低锁的粒度,大量使用CAS操作和渐进式扩容),极大地提高了运行效率,并且,它的key也是不允许为null的。

四.总结:

遗忘了很久的多线程总结终于也是圆满结束了,内容还是很多的,大概也有很多不足之处,欢迎各位大佬前来指点,感谢感谢!

  • 12
    点赞
  • 22
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
【优质项目推荐】 1、项目代码均经过严格本地测试,运行OK,确保功能稳定后才上传平台。可放心下载并立即投入使用,若遇到任何使用问题,随时欢迎私信反馈与沟通,博主会第一时间回复。 2、项目适用于计算机相关专业(如计科、信息安全、数据科学、人工智能、通信、物联网、自动化、电子信息等)的在校学生、专业教师,或企业员工,小白入门等都适用。 3、该项目不仅具有很高的学习借鉴价值,对于初学者来说,也是入门进阶的绝佳选择;当然也可以直接用于 毕设、课设、期末大作业或项目初期立项演示等。 3、开放创新:如果您有一定基础,且热爱探索钻研,可以在此代码基础上二次开发,进行修改、扩展,创造出属于自己的独特应用。 欢迎下载使用优质资源!欢迎借鉴使用,并欢迎学习交流,共同探索编程的无穷魅力! 基于业务逻辑生成特征变量python实现源码+数据集+超详细注释.zip基于业务逻辑生成特征变量python实现源码+数据集+超详细注释.zip基于业务逻辑生成特征变量python实现源码+数据集+超详细注释.zip基于业务逻辑生成特征变量python实现源码+数据集+超详细注释.zip基于业务逻辑生成特征变量python实现源码+数据集+超详细注释.zip基于业务逻辑生成特征变量python实现源码+数据集+超详细注释.zip基于业务逻辑生成特征变量python实现源码+数据集+超详细注释.zip 基于业务逻辑生成特征变量python实现源码+数据集+超详细注释.zip 基于业务逻辑生成特征变量python实现源码+数据集+超详细注释.zip
提供的源码资源涵盖了安卓应用、小程序、Python应用和Java应用等多个领域,每个领域都包含了丰富的实例和项目。这些源码都是基于各自平台的最新技术和标准编写,确保了在对应环境下能够无缝运行。同时,源码中配备了详细的注释和文档,帮助用户快速理解代码结构和实现逻辑。 适用人群: 这些源码资源特别适合大学生群体。无论你是计算机相关专业的学生,还是对其他领域编程感兴趣的学生,这些资源都能为你提供宝贵的学习和实践机会。通过学习和运行这些源码,你可以掌握各平台开发的基础知识,提升编程能力和项目实战经验。 使用场景及目标: 在学习阶段,你可以利用这些源码资源进行课程实践、课外项目或毕业设计。通过分析和运行源码,你将深入了解各平台开发的技术细节和最佳实践,逐步培养起自己的项目开发和问题解决能力。此外,在求职或创业过程中,具备跨平台开发能力的大学生将更具竞争力。 其他说明: 为了确保源码资源的可运行性和易用性,特别注意了以下几点:首先,每份源码都提供了详细的运行环境和依赖说明,确保用户能够轻松搭建起开发环境;其次,源码中的注释和文档都非常完善,方便用户快速上手和理解代码;最后,我会定期更新这些源码资源,以适应各平台技术的最新发展和市场需求。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值