首页 > 编程知识 正文

简要说明事物的原子性,消息队列怎么测试

时间:2023-05-03 05:58:04 阅读:160901 作者:2508

目录

写在前面

原子性

锁定机构

当上一个进程尝试执行任务时,它必须依赖于线程。 换句话说,进程的最小执行单位是线程,并且每个进程至少有一个线程。 说到多线程,这里说两个概念,就是串行和并行。 澄清这一点就能更好地理解多线程。 串行实际上是对单个线程执行多个任务。 以下载文件为例。 下载多个文件时,串行下载顺序是一定的。 也就是说,你必须先下载a,然后再下载b。 这些在时间上不会重叠。

要编写线程安全代码,管理对状态的访问操作非常重要,特别是对共享状态和可变状态的访问。 如果的对象是线程安全的,则必须采用同步机制来协调对象对可变状态的访问,否则可能会导致数据损坏或其他不希望的结果。 java同步机制是关键字synchronized,提供独占锁定方法。 这还包括volatile类型变量、显式锁定和原子变量。

如果将单线程近似定义为“所见即知”,则定义线程安全时,如果该类在访问包含多个线程的类时始终能够表现出正确的行为,则该类称为线程安全。 如果一个类在单线程上工作不正常,它肯定不是线程安全的。 如果正确安装了对象,则任何操作都不会违反不变性条件和事后条件,包括调用对象的公共方法,以及对该公共域进行读写操作等。 在对象实例上对联机安全类执行的串行或并行操作不会被禁用。

无状态对象一定是安全的,无状态是什么? 此类不包含域,也不包含对其他类的域的引用。 例如,大多数servlet都是无状态的,这大大降低了实现servlet线程安全的复杂性,只有当servlet处理请求时需要存储信息时,线程安全才是一个问题。

原子性其实是不可分割的,怎么理解呢? 例如,如果向无状态对象添加状态,会发生什么情况? 计算类被调用了多少次。 这个类可以是servlet。 定义一个专用的长整型域(命名为count ),该域在方法中不调用一次,而是将此值加1。 这意味着:

Integer count=0; 公共语音获取() { count; system.out.println(count ); }因为我们没有对这个类执行任何同步机制操作,所以这个类是非线程的,安全的,在单线程环境中可以正常工作,但是在多线程的情况下会出现问题。 count这个操作看起来像是操作,但实际上不是原子性的。 因为它并不是作为不可分割的操作来执行的。 实际上,count包含三个独立的操作:读取count的值,将值加1,然后将计算结构写入count。 而且,这是“读取-修改-写入”的操作序列。同时执行会出现问题。 在多线程中执行上面的代码,结果如下

如你所见,这里有两个26。 为什么会发生这种情况? 出现这种情况显然表明我们的方法不是线程安全的。 发生这样问题的理由有很多。 最常见的情况是,我们a线程进入方法后,获取count的值,在刚读取该值且没有更改count的值时,结果线程b也会进来,线程a和线程b就到手了

在并发程序中,这种不正确的执行时机导致不正确的结果是非常重要的情况,因此被正式命名为“竞争状态条件”。 竞争条件是什么? 如果某个计算的准确性取决于多个线程的交替执行定时,则会发生冲突状态条件。 最常见的竞争条件类型是“检查后运行”操作,它根据可能无效的观察结果确定下一步的行为。 例如,观察到某个条件为真(例如,文件x不存在),根据观察结果执行适当的动作(文件x的制作),但实际上,在从观察到结果到开始文件x的制作的期间,观察结果是无效的

与许多并发错误一样,争用条件不一定会导致错误,而是需要某些不正确的执行时机。 原子操作是对访问相同状态的所有操作(包括操作本身)原子执行的操作。 类似上述count的操作称为复合操作,它包含一组为了确保线程安全而必须原子执行的操作。 实际上,应该尽可能使用现有的线程安全对象来管理类的状态,与不线程安全的对象相比,判断线程安全对象的可能状态及其状态迁移更容易、更容易

维护和验证线程安全性。这里值得一提的是,java中给我们弄好了很多原子操作的类型,在这个包下java.util.concurrent.atomic。

加锁机制

上面我们说了,我们可以对单个类进行原子性操作,这样可以保证我们程序的安全性,但是,我们想一想,如果当多个原子性的操作同时进行时,而且各个原子性操作之间都存在相互依赖的关系,这种情况下,我们怎么保证程序运行正确(线程安全)?如果还是使用原子性操作的方法,那么我们要保持状态的一致性,就需要在单个原子操作中更新所有相关的状态变量。

说到这里,我们就不得不提一下java提供的一种内置的锁机制来支持原子性,即同步代码块(synchronized block),同步代码快包括两部分,一个作为锁的对象引用,一个作为由这个锁保护的代码块。以关键字synchronized来修饰的方法就是一种横跨整个方法体的同步代码块,其中该同步代码块的锁就是方法调用所在的对象。每个Java对象都可以用作一个实现同步的锁,这些所被称为内置锁或监视器锁。线程在进入同步代码块之前会自动获得锁,并且在退出同步代码块时自动释放锁,这里要说,获得内置锁的唯一途径就是进入由这个锁保护的同步代码块或方法。值得一提的是,内置锁相当于一种互斥锁,意味着最多只有一个线程能持有这种锁。要注意了,任何一个执行同步代码块的线程,都不能看到有其他线程正在执行由同一锁保护的同步代码块(这里涉及到可见性的问题)。synchronized直接加在方法上,虽然能很方便的解决线程安全的问题,但是也会带来性能低下的问题,所以synchronized怎么使用,也是值得学习的。

可重入函数可以由多于一个任务并发使用,而不必担心数据错误。相反,不可重入函数不能由超过一个任务所共享,除非能确保函数的互斥(或者使用信号量,或者在代码的关键部分禁用中断)。可重入函数可以在任意时刻被中断,稍后再继续运行,不会丢失数据。可重入函数要么使用本地变量,要么在使用全局变量时保护自己的数据。

当某个线程请求一个由其他线程持有的锁时,发出请求的线程就会被阻塞,然而,由于内置锁是可重入的,因此如果某个线程试图获得一个已经由它自己持有的锁,那么这个请求就会成功。“重入”意味着获取锁的操作的粒度是线程,而不是调用。重入的一种实现方式是,为每一个锁关联一个获取计数值和一个所有者线程,当计数值为0时,这个锁就被认为是没有被任何线程持有。当线程请求一个未被持有的锁时,JVM将记下锁的持有者,并且将获取计数值置为1,如果同一个线程再次请求获取这个锁,计数值将递增,而当线程退出同步代码块时,计数器会相应的递减,当计数值为0时,这个锁将被释放。

到此我们就可以将多个复合操作封装到一个同步代码块中,但是这样是不够的,如果用同步代码块来协调对某个变量的访问,那么在访问这个变量的所有位置上都需要使用同步。而且,当使用锁来协调对某个变量的访问是,在访问变量的所有位置上都要使用同一个锁。而且,并不是只有在写入共享变量时才需要使用同步,对于可能被多个线程同时访问的可变状态变量,在访问它时都需要有同一个锁,这种情况下,我们成状态变量时由这个锁保护的。

在这里值得一提的是,当使用锁时,你应该清楚代码块中实现的功能,以及在执行该代码块时是否需要很长的时间。无论是执行计算密集的操作,还是在执行某个可能阻塞的操作,如果持有锁的时间过长,那么都会带来活跃性或性能的问题。当执行时间较长的计算或者可能无法快速完成的操作时(例如网络IO或控制台IO),一定不要持有锁。

版权声明:该文观点仅代表作者本人。处理文章:请发送邮件至 三1五14八八95#扣扣.com 举报,一经查实,本站将立刻删除。