精华内容
下载资源
问答
  • 并发源于
    2021-11-28 19:35:53

    16 无畏并发

    安全高效的处理并发编程是Rust的另一个主要的目标

    内存安全和高效编程一直都是很多语言追求的目的,Rust采用所有权和类型系统来平衡处理这一点

    本章我们将会了解

    1.如何创建线程来同时运行多端代码

    2.消息传递并发,其中channel 被用来在线程之间传递消息

    3.共享状态并发,其中多个线程可以访问同一片数据

    4.Sync和Send trait,将Rust的并发保证扩展到用户定义的以及标准库提供的类型中

    16.3 共享状态并发

    消息传递虽然是一个很好的处理并发的方式,但是并不是唯一一个

    在某种程度上,任何编程语言中的通道都类似单所有权,因为一旦将一个值传入到通道中,就无法再使用这个值了

    共享内存则类似于多所有权,多个线程可以同时访问相同的内存位置。我们之前学过智能指针,这使得多所有权成为可能但是这会增加程序的复杂性,因此需要某种管理方式管理这些不同的所有者。Rust的所有权规则和类型系统能够给我们很大的协助

    互斥器一次只允许一个线程访问数据

    互斥器使用的时候必须记住:

    1.使用数据之前获取锁

    2.处理完被互斥器所保护的的数据之后,必须解锁数据,这样其他线程才能够获取锁

    Mutex的API

    我们来看通过一个例子看看互斥锁如何使用

    use std::sync::Mutex;
    
    fn main(){
        let m = Mutex::new(5);
        {
            let mut num = m.lock().unwrap();
            *num = 6;
        }
        println!("m = {:?}",m);
    }
    

    出于简单考虑,在一个单线程上下文中探索MutexAPI

    我们使用关联函数new来创建了一个Mutex.使用lock方法获取锁,以访问互斥器中的数据。这个调用会阻塞当前的线程,直到我们有锁为止

    如果另一个线程拥有锁,并且没有释放就Panic了,那lock调用就会失败。在这种情况下,此线程也无法调用lock,unwrap就用来处理这种情况

    Running `target/debug/smartPoint`
    m = Mutex { data: 6, poisoned: false, .. }
    

    一旦获取了锁,就可以将返回值看作一个可变引用了,我们可以访问并且修改这个锁里的值

    没错!Mutex是一个智能指针,更准确的说,lock调用返回一个叫做MutexGuard的智能指针。这个指针实现了Deref来指向其内部数据,其也提供了一个Drop实现当MutexGuard离开作用域时自动释放锁

    在线程间共享Mutex

    use std::sync::Mutex;
    use std::thread;
    
    fn main(){
        let counter = Mutex::new(0);
        let mut handles = vec![];
    
        for _ in 0..10 {
            let handle = thread::spawn(move||{
                let mut num = counter.lock().unwrap();
    
                *num +=1;
            });
            handles.push(handle);
        }
    
        for handle in handles {
            handle.join().unwrap();
        }
       println!("Result:{}",*counter.lock().unwrap())
    }
    

    程序启动了10个线程,每个线程都通过Mutex来增加计数器的值

    error[E0382]: use of moved value: `counter`
      --> src/main.rs:9:36
       |
    5  |     let counter = Mutex::new(0);
       |         ------- move occurs because `counter` has type `Mutex<i32>`, which does not implement the `Copy` trait
    ...
    9  |         let handle = thread::spawn(move||{
       |                                    ^^^^^^ value moved into closure here, in previous iteration of loop
    10 |             let mut num = counter.lock().unwrap();
       |                           ------- use occurs due to use in closure
    

    运行结果告诉我们出错了,counter值在上一次循环中被移动了,所以我们不能将counter锁的所有权移动到多个线程中,我们尝试用多所有权系统来修复一下这个问题

    多线程和多所有权

    智能指针Rc可以让值拥有多个所有者,我们现在就来改写一下程序

    use std::sync::Mutex;
    use std::thread;
    use std::rc::Rc;
    
    fn main(){
        let counter = Rc::new(Mutex::new(0));
        let mut handles = vec![];
    
        for _ in 0..10 {
            let counter = Rc::clone(&counter);
            let handle = thread::spawn(move||{
                let mut num = counter.lock().unwrap();
    
                *num +=1;
            });
            handles.push(handle);
        }
    
        for handle in handles {
            handle.join().unwrap();
        }
       println!("Result:{}",*counter.lock().unwrap())
    }
    
    error[E0277]: `Rc<Mutex<i32>>` cannot be sent between threads safely
       --> src/main.rs:11:22
        |
    11  |           let handle = thread::spawn(move||{
        |  ______________________^^^^^^^^^^^^^_-
        | |                      |
        | |                      `Rc<Mutex<i32>>` cannot be sent between threads safely
    12  | |             let mut num = counter.lock().unwrap();
    13  | |
    14  | |             *num +=1;
    15  | |         });
        | |_________- within this `[closure@src/main.rs:11:36: 15:10]`
        | 
       ::: /Users/xxx/.rustup/toolchains/stable-x86_64-apple-darwin/lib/rustlib/src/rust/library/std/src/thread/mod.rs:619:8
        |
    619 |       F: Send + 'static,
        |          ---- required by this bound in `spawn`
        |
        = help: within `[closure@src/main.rs:11:36: 15:10]`, the trait `Send` is not implemented for `Rc<Mutex<i32>>`
        = note: required because it appears within the type `[closure@src/main.rs:11:36: 15:10]`
    

    运行结果非常长,并且告诉我们Rc行不通,Rc<Mutex<i32>> cannot be sent between threads safely和required by this bound in spawn告诉我们了原因

    Rc会在每个clone被调用时增加计数,并在每一个clone被丢弃时减少计数。但是Rc没有使用任何并发源于原语来确保改变计数的的操作不会被其他线程打断

    原子引用计数Arc

    Arc和Rc类似可以安全的用于并发环境。但是原始类型都是原子性会带有性能惩罚

    我们再来修改一下代码

    use std::sync::{Mutex,Arc};
    use std::thread;
    
    fn main(){
        let counter = Arc::new(Mutex::new(0));
        let mut handles = vec![];
    
        for _ in 0..10 {
            let counter = Arc::clone(&counter);
            let handle = thread::spawn(move||{
                let mut num = counter.lock().unwrap();
    
                *num +=1;
            });
            handles.push(handle);
        }
    
        for handle in handles {
            handle.join().unwrap();
        }
       println!("Result:{}",*counter.lock().unwrap())
    }
    
         Running `target/debug/smartPoint`
    Result:10
    

    运行一下,太完美了!在这里我们使用Arc包装一个Mutex能够实现多线程之间共享所有权

    上面这个计数虽然简单,但它是一种非常好的策略,我们可以将计算分成独立的部分,分散到多个线程中,最后使用Mutex加总各线程的数据从而得到最终结果

    RefCell/Rc与Mutex/Arc的相似性能

    在上文中,虽然counter是不可变的,但是可以获取其内部值的可变引用;这意味着Mutex提供了内部可变性,就像cell系列类型那样。如使用RefCell可以改变Rc中的内容那样,同样可以使用Mutex来改变Arc中的内容

    另外,注意:Rust不能避免使用Mutex的全部错误,Rc可能会造成循环引用,Mutex也可能造成死锁:当一个操作需要锁住两个资源,而两个线程各持一个锁,这就会导致他们永远等待

    更多相关内容
  • 投票分级非双向忙碌类别无偏差轮训,Linux内核提供了epoll这样更高级的形式把需要处理的IO事件添加到epoll内核列表中,epoll_wait来进行监控并提醒用户程序当IO事件发生时此聊天室客户端代码叉两个进程,子进程把...
  • 源于“疯狂创客圈”的博客,以及疯狂迭代的趁热爱自由IM项目,重点讲解了Netty、Redis、Zookeeper的使用方法,为大家打下Java高并发技术开发的基础。
  • 在计算机编程领域,并发编程是一个很常见的名词和功能了,其实并发这个理念,最初是源于铁路和电报的早期工作。比如在同一个铁路系统上如何安排多列火车,保证每列火车的运行都不会发生冲突。 后来在20世纪60年代,...
  • Go并发编程研讨课.pdf

    2019-06-09 09:58:59
    了解一些扩展的同步源于,对于标准库sync包的补充 对于规模很大的项目,分布式同步原语是必不可少的,带你了解便利的分布式同步原语 atomic可以保证对数据操作的一致性,利用CAS可以设计lock-free的数据结构 channel...
  • 同时,如果系统初期就设计一个千万级并发的流量架构,很难有公司可以支撑这个成本。【编者按】对很多创业公司而言,随着业务增长,网站的流量也会经历不同的阶段。从十万流量到一百万流量,再从一百万流量跨越到一千...
  • 多进程并发提高数字运算在计算机编程领域,并发编程是一个很常见的名词和功能了,其实并发这个理念,最初是源于铁路和电报的早期工作。比如在同一个铁路系统上如何安排多列火车,保证每列火车的运行都不会发生冲突。...
  • Java并发编程进阶——并发

    千次阅读 2022-03-14 16:55:16
    其实乐观锁就是:每次不加锁而是假设没有并发冲突而去完成某项操作,如果因为并发冲突失败就重试,直到成功为止。 1.4 公平锁与非公平锁 根据线程获取锁的抢占机制,锁可以分为公平锁和非公平锁。Java中的...

    1 JAVA 多线程锁介绍

    1.1 悲观锁

    定义悲观锁指对数据被外界修改持保守态度,认为数据很容易就会被其他线程修改(很悲观),所以在数据被处理前先对数据进行加锁,并在整个数据处理过程中,使数据处于锁定状态。

    悲观锁的实现:开发中常见的悲观锁实现往往依靠数据库提供的锁机制,即在数据库中,在对数据记录操作前给记录加排它锁。如果获取锁失败,则说明数据正在被其他线程修改,当前线程则等待或者抛出异常。如果获取锁成功,则对记录进行操作,然后提交事务后释放排它锁。

    实例:Java 中的 synchronized 关键字就是一种悲观锁,一个线程在操作时,其他的线程必须等待,直到锁被释放才可进入方法进行执行,保证了线程和数据的安全性,同一时间,只能有一条线程进入执行。

    public class Student {
        private String name;
    
        public synchronized String getName() {
            return name;
        }
    
        public synchronized void setName(String name) {
            this.name = name;
        }
    }
    

    代码分析 :假设有 3 条线程,如下图,线程 3 正在操作 Student 类,此时线程 1 和线程 2 必须要等待线程 3 执行完毕方可进入,这就是悲观锁。
    在这里插入图片描述

    1.2 乐观锁

    定义:乐观锁是相对悲观锁来说的,它认为数据在一般情况下不会造成冲突,所以在访问记录前不会加排它锁,而是在进行数据提交更新的时候,才会正式对数据冲突与否进行检测。

    乐观锁的实现:依旧拿数据库的锁进行比较介绍,乐观锁并不会使用数据库提供的锁机制, 一般在表中添加 version 宇段或者使用业务状态来实现。 乐观锁直到提交时才锁定,所以不会产生任何死锁。

    Java 中的乐观锁:Java中的 CAS采用的就是乐观锁技术,当多个线程尝试使用 CAS 同时更新同一个变量时,只有其中一个线程能更新变量的值,而其它线程都失败,失败的线程并不会被挂起,而是被告知这次竞争中失败,并可以再次尝试。
    在这里插入图片描述

    Tips:当多个线程尝试使用 CAS 同时更新同一个变量时,只有其中一个线程能更新变量的值,而其它线程都失败。注意失败两字,失败意味着有操作,而悲观锁是等待,意味着不能同时操作。

    1.3 悲观锁机制存在的问题

    • 在多线程竞争下,加锁、释放锁会导致比较多的上下文切换和调度延时,引起性能问题;
    • 一个线程持有锁会导致其它所有需要此锁的线程挂起;
    • 如果一个优先级高的线程等待一个优先级低的线程释放锁会导致优先级倒置,引起性能风险。

    相比于悲观锁的这些问题,另一个更加有效的锁就是乐观锁。其实乐观锁就是:每次不加锁而是假设没有并发冲突而去完成某项操作,如果因为并发冲突失败就重试,直到成功为止。

    1.4 公平锁与非公平锁

    根据线程获取锁的抢占机制,锁可以分为公平锁和非公平锁。Java中的ReentrantLock 提供了公平和非公平锁的实现。

    公平锁:表示线程获取锁的顺序是按照线程请求锁的时间早晚来决定的,也就是最早请求锁的线程将最早获取到锁。

    非公平锁:非公平锁则在运行时闯入,不遵循先到先执行的规则。

    1.5 独占锁与共享锁

    根据锁只能被单个线程持有还是能被多个线程共同持有,锁可以分为独占锁和共享锁。

    独占锁:保证任何时候都只有一个线程能得到锁,ReentrantLock 就是以独占锁方式实现的。

    共享锁:则可以同时由多个线程持有,例如 ReadWriteLock 读写锁,它允许一个资源可以被多线程同时进行读操作。

    独占锁是一种悲观锁,由于每次访问资源都先加上互斥锁,这限制了并发性,因为读操作并不会影响数据的一致性,而独占锁只允许在同一时间由一个线程读取数据,其他线程必须等待当前线程释放锁才能进行读取。

    共享锁则是一种乐观锁,它放宽了加锁的条件,允许多个线程同时进行读操作。

    1.6 自旋锁

    由于 Java 中的线程是与操作系统中的线程相互对应的,所以当一个线程在获取锁(比如独占锁)失败后,会被切换到内核状态而被挂起。而当该线程获取到锁后又需要将其切换回用户态执行。用户状态与内核状态的切换开销是比较大的,在一定程度上会影响并发性能。

    自旋锁:自旋锁是当前线程在获取锁时,如果发现锁已经被其他线程占有,它不马上阻塞自己,在不放弃 CPU 使用权的情况下,多次尝试获取(默认次数是 10,可以使用-XX:PreBlockSpinsh 参数设置该值)。

    很有可能在后面几次尝试中其他线程己经释放了锁。如果尝试指定的次数后仍没有获取到锁则当前线程才会被阻塞挂起。由此看来自旋锁是使用 CPU 时间换取线程阻塞与调度的开销,但是很有可能这些 CPU 时间白白浪费了。

    2 并发锁之 Lock 接口

    2.1 Lock 接口的介绍

    Lock 接口的诞生:在 Java 中锁的实现可以由 synchronized 关键字来完成,但在 Java5 之后,出现了一种新的方式来实现,即 Lock 接口。

    诞生的意义:Lock 接口支持那些语义不同(重入、公平等)的锁规则,可以在非阻塞式结构的上下文(包括 hand-over-hand 和锁重排算法)中使用这些规则。主要的实现是 ReentrantLock。

    JDK 1.5 前的 synchronized:在多线程的情况下,当一段代码被 synchronized 修饰之后,同一时刻只能被一个线程访问,其他线程都必须等到该线程释放锁之后才能有机会获取锁访问这段代码。

    Lock 接口: 实现提供了比使用 synchronized 方法和语句可获得的更广泛的锁定操作。此实现允许更灵活的结构,可以具有差别很大的属性,可以支持多个相关的 Condition 对象。

    Lock 相对于 synchronized 关键字而言更加灵活,你可以自由得选择你想要加锁的地方。当然更高的自由度也带来更多的责任。

    使用示例:通常在 try catch 模块中使用 Lock 关键字,在 finally 模块中释放锁。

    Lock lock = new ReentrantLock(); //通过子类进行创建,此处以ReentrantLock进行举例
         lock.lock(); //加锁
         try {
             // 对上锁的逻辑进行操作
         } finally {
             lock.unlock(); //释放锁
         }
    

    2.2 Lock 接口与 synchronized 关键字的区别

    • 实现:synchronized 关键字基于 JVM 层面实现,JVM 控制锁的获取和释放。Lock 接口基于 JDK 层面,手动进行锁的获取和释放;
    • 使用:synchronized 关键字不用手动释放锁,Lock 接口需要手动释放锁,在 finally 模块中调用 unlock 方法;
    • 锁获取超时机制:synchronized 关键字不支持,Lock 接口支持;
    • 获取锁中断机制:synchronized 关键字不支持,Lock 接口支持;
    • 释放锁的条件:synchronized 关键字在满足占有锁的线程执行完毕,或占有锁的线程异常退出,或占有锁的线程进入 waiting 状态才会释放锁。Lock 接口调用 unlock 方法释放锁;
    • 公平性:synchronized 关键字为非公平锁。Lock 接口可以通过入参自行设置锁的公平性。

    2.3 Lock 接口相比 synchronized 关键字的优势

    下面通过两个案例分析来了解 Lock 接口的优势所在。
    案例 1 :在使用 synchronized 关键字的情形下,假如占有锁的线程由于要等待 IO 或者其他原因(比如调用 sleep 方法)被阻塞了,但是又没有释放锁,那么其他线程就只能一直等待,这会极大影响程序执行效率。

    解决方案:Lock 接口中的 tryLock (long time, TimeUnit unit) 方法或者响应中断 lockInterruptibly () 方法,能够解决这种长期等待的情况。

    案例 2 :我们知道,当多个线程读写文件时,读操作和写操作会发生冲突现象,写操作和写操作也会发生冲突现象,但是读操作和读操作不会发生冲突现象。
    如果采用 synchronized 关键字实现同步的话,当多个线程都只是进行读操作时,也只有一个线程可以进行读操作,其他线程只能等待锁的释放而无法进行读操作。

    解决方案:Lock 接口家族的ReentrantReadWriteLock也可以解决这种情况。

    总结:Lock 接口实现提供了比使用 synchronized 方法和语句可获得的更广泛的锁定操作,能够解决 synchronized 不能够避免的问题。

    2.4 Lock 接口的常用方法

    下面列出了JDK 中 Lock 接口的源码中所包含的方法:

    public interface Lock {
        void lock();
        void lockInterruptibly() throws InterruptedException;
        boolean tryLock();
        boolean tryLock(long time, TimeUnit unit) throws InterruptedException;
        void unlock();
        Condition newCondition();
    }
    

    方法介绍

    • void lock():获取锁。如果锁不可用,出于线程调度目的,将禁用当前线程,并且在获得锁之前,该线程将一直处于休眠状态;
    • void lockInterruptibly():如果当前线程未被中断,则获取锁;
    • boolean tryLock():仅在调用时锁为空闲状态才获取该锁。如果锁可用,则获取锁,并立即返回值 true。如果锁不可用,则此方法将立即返回值 false;
    • boolean tryLock(long time, TimeUnit unit):如果锁在给定的等待时间内空闲,并且当前线程未被中断,则获取锁;
    • void unlock():释放锁。调用前锁必须由当前线程保持;
    • Condition newCondition():返回绑定到此 Lock 实例的新 Condition 实例。

    3 乐观锁与悲观锁

    3.1 乐观锁与悲观锁的概念

    悲观锁:总是假设最坏的情况,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样其他线程想拿这个数据就会阻塞直到它拿到锁(共享资源每次只给一个线程使用,其它线程阻塞,用完后再把资源转让给其它线程)。

    乐观锁:总是假设最好的情况,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,可以使用版本号机制和 CAS 算法实现。

    乐观锁适用于多读的应用场景,这样可以提高吞吐量,像数据库提供的类似于 write_condition 机制,其实都是提供的乐观锁。

    3.2 乐观锁与悲观锁的使用场景

    简单的来说 CAS 适用于写比较少的情况下(多读场景,冲突一般较少),synchronized 适用于写比较多的情况下(多写场景,冲突一般较多)。

    • 对于资源竞争较少(线程冲突较轻)的情况,使用 synchronized 同步锁进行线程阻塞和唤醒切换以及用户态内核态间的切换操作额外浪费消耗 CPU 资源;而 CAS 基于硬件实现,不需要进入内核,不需要切换线程,操作自旋几率较少,因此可以获得更高的性能;
    • 对于资源竞争严重(线程冲突严重)的情况,CAS 自旋(多次对比)的概率会比较大,从而浪费更多的 CPU 资源,效率低于 synchronized。

    总结:乐观锁适用于写比较少的情况下(多读场景),即冲突真的很少发生的时候,这样可以省去了锁的开销,加大了系统的整个吞吐量。

    但如果是多写的情况,一般会经常产生冲突,这就会导致上层应用会不断地进行 retry,这样反倒是降低了性能,所以一般多写的场景下用悲观锁就比较合适。

    3.3 乐观锁的缺点

    ABA 问题:
    如果一个变量 V 初次读取的时候是 A 值,并且在准备赋值的时候检查到它仍然是 A 值,那我们就能说明它的值没有被其他线程修改过了吗?

    很明显是不能的,因为在这段时间它的值可能被改为其他值,然后又改回 A,那 CAS 操作就会误认为它从来没有被修改过。这个问题被称为 CAS 操作的 “ABA” 问题。

    循环时间长开销大:
    在特定场景下会有效率问题。

    自旋 CAS(也就是不成功就一直循环执行直到成功)如果长时间不成功,会给 CPU 带来非常大的执行开销。

    总结:这里主要关注 ABA 问题。循环时间长开销大的问题,在特定场景下很难避免的,因为所有的操作都需要在合适自己的场景下才能发挥出自己特有的优势。

    3.4 ABA 问题解决之版本号机制

    讲解 CAS 原理时,对于解决办法进行了简要的介绍,仅仅是一笔带过。这里进行较详细的阐释。其实 ABA 问题的解决,我们通常通过如下方式进行解决:版本号机制。我们一起来看下版本号机制:

    版本号机制:一般是在数据中加上一个数据版本号 version 字段,表示数据被修改的次数,当数据被修改时,version 值会加 1。当线程 A 要更新数据值时,在读取数据的同时也会读取 version 值,在提交更新时,若刚才读取到的 version 值为当前数据中的 version 值相等时才更新,否则重试更新操作,直到更新成功。这里看不懂没关系,看下面的示例。

    场景示例:假设商店类 Shop 中有一个 version 字段,当前值为 1 ;而当前商品数量为 50。

    • 店员 A将其读出( version=1 ),并将商品数量扣除 10,更新为 50 - 10 = 40;
    • 在店员 A 操作的过程中,店员 B 也读入此信息( version=1 ),并将商品数量扣除 20,更新为 50 - 20 = 30;
    • 店员 A 完成了修改工作,将数据版本号加 1( version=2 ),商品数量为 40,提交更新,此时由于提交数据版本大于记录当前版本,数据被更新,数据记录 version 更新为 2 ;
    • 店员 B 完成了操作,也将版本号加 1( version=2 ),试图更新商品数量为 30。但此时比对数据记录版本时发现,店员 B 提交的数据版本号为 2 ,数据记录当前版本也为 2 ,不满足 “ 提交版本必须大于记录当前版本才能执行更新 “ 的乐观锁策略,因此,店员 B 的提交被驳回;
    • 店员 B 再次重新获取数据,version = 2,商品数量 40。在这个基础上继续执行自己扣除 20 的操作,商品数量更新为 40 - 20 = 20;
    • 店员 B 将版本号加 1 ,version = 3,将之前的记录 version 2 更新为 3 ,将之前的数量 40 更新 为 20。

    从如上描述来看,所有的操作都不会出现脏数据,关键在于版本号的控制。

    Tips:Java 对于乐观锁的使用进行了良好的封装,我们可以直接使用并发编程包来进行乐观锁的使用。下面所使用的 Atomic 操作即为封装好的操作。

    3.5 Atomic 操作实现乐观锁

    为了更好地理解悲观锁与乐观锁,我们通过设置一个简单的示例场景来进行分析。并且我们采用悲观锁 synchronized 和乐观锁 Atomic 操作进行分别实现。

    Atomic 操作类,指的是 java.util.concurrent.atomic 包下,一系列以 Atomic 开头的包装类。例如 AtomicBoolean,AtomicInteger,AtomicLong。它们分别用于 Boolean,Integer,Long 类型的原子性操作。

    Atomic 操作的底层实现正是利用的 CAS 机制,而 CAS 机制即乐观锁。

    场景设计

    • 创建两个线程,创建方式可自选;
    • 定义一个全局共享的 static int 变量 count,初始值为 0;
    • 两个线程同时操作 count,每次操作 count 加 1;
    • 每个线程做 100 次 count 的增加操作。

    结果预期:最终 count 的值应该为 200。

    悲观锁 synchronized 实现

    public class DemoTest extends Thread{
        private static int count = 0; //定义count = 0
        public static void main(String[] args) {
            for (int i = 0; i < 2; i++) { //通过for循环创建两个线程
                new Thread(new Runnable() {
                    @Override
                    public void run() {
                        try {
                            Thread.sleep(10);
                        } catch (Exception e) {
                            e.printStackTrace();
                        }
                        //每个线程让count自增100次
                        for (int i = 0; i < 100; i++) {
                            synchronized (DemoTest.class){
                                count++;
                            }
                        }
                    }
                }). start();
            }
            try{
                Thread.sleep(2000);
            }catch (Exception e){
                e.printStackTrace();
            }
            System.out.println(count);
        }
    }
    

    结果验证

    200
    

    乐观锁 Atomic 操作实现

    public class DemoTest extends Thread{
        //Atomic 操作,引入AtomicInteger。这是实现乐观锁的关键所在。
        private static AtomicInteger count = new AtomicInteger(0);
        public static void main(String[] args) {
            for (int i = 0; i < 2; i++) {
                new Thread(new Runnable() {
                    @Override
                    public void run() {
                        try {
                            Thread.sleep(10);
                        } catch (Exception e) {
                            e.printStackTrace();
                        }
                        //每个线程让count自增100次
                        for (int i = 0; i < 100; i++) {
                            count.incrementAndGet();
                        }
                    }
                }). start();
            }
            try{
                Thread.sleep(2000);
            }catch (Exception e){
                e.printStackTrace();
            }
            System.out.println(count);
        }
    }
    

    结果验证:

    200
    

    代码解读
    此处主要关注两个点,第一个是 count 的创建,是通过 AtomicInteger 进行的实例化,这是使用 Atomic 的操作的入口,也是使用 CAS 乐观锁的一个标志。

    第二个是需要关注 count 的增加 1 调用是 AtomicInteger 中 的 incrementAndGet 方法,该方法是原子性操作,遵循 CAS 原理。

    4 AQS 原理

    4.1 什么是 AQS

    定义:AbstarctQueuedSynchronizer 简称 AQS,是一个用于构建锁和同步容器的框架。

    事实上 concurrent 包内许多类都是基于 AQS 构建的,例如 ReentrantLock,ReentrantReadWriteLock,FutureTask 等。AQS 解决了在实现同步容器时大量的细节问题。
    在这里插入图片描述

    AQS 使用一个 FIFO 队列表示排队等待锁的线程,队列头结点称作 “哨兵节点” 或者 “哑结点”,它不与任何线程关联。其他的节点与等待线程关联,每个阶段维护一个等待状态 waitStatus。

    4.2 AQS 提供的两种功能

    从使用层面来说,AQS 的锁功能分为两种:独占锁和共享锁。

    独占锁:每次只能有一个线程持有锁,比如前面演示的 ReentrantLock 就是以独占方式实现的互斥锁;

    共享锁:允许多个线程同时获取锁,并发访问共享资源,比如 ReentrantReadWriteLock。

    4.3 AQS 的内部实现

    AQS 的实现依赖内部的同步队列,也就是 FIFO 的双向队列,如果当前线程竞争锁失败,那么 AQS 会把当前线程以及等待状态信息构造成一个 Node 加入到同步队列中,同时再阻塞该线程。当获取锁的线程释放锁以后,会从队列中唤醒一个阻塞的节点 (线程)。

    如下图所示,一个节点表示一个线程,它保存着线程的引用(thread)、状态(waitStatus)、前驱节点(prev)、后继节点(next),其实就是个双端双向链表,其数据结构如下:
    在这里插入图片描述

    Tips:AQS 队列内部维护的是一个 FIFO 的双向链表,这种结构的特点是每个数据结构都有两个指针,分别指向直接的后继节点和直接前驱节点。所以双向链表可以从任意一个节点开始,很方便的访问前驱和后继。每个 Node 其实是由线程封装,当线程争抢锁失败后会封装成 Node 加入到 ASQ 队列中去。

    4.4 添加线程对于 AQS 队列的变化

    当出现锁竞争以及释放锁的时候,AQS 同步队列中的节点会发生变化,首先看一下添加线程的场景。
    在这里插入图片描述

    这里会涉及到两个变化:

    • 队列操作的变化:新的线程封装成 Node 节点追加到同步队列中,设置 prev 节点以及修改当前节点的前置节点的 next 节点指向自己;
    • tail 指向变化:通过同步器将 tail 重新指向新的尾部节点。

    4.5 释放锁移除节点对于 AQS 队列的变化

    第一个 head 节点表示获取锁成功的节点,当头结点在释放同步状态时,会唤醒后继节点,如果后继节点获得锁成功,会把自己设置为头结点,节点的变化过程如下:
    在这里插入图片描述

    这个过程也是涉及到两个变化:

    • head 节点指向:修改 head 节点指向下一个获得锁的节点;
    • 新的获得锁的节点:如图所示,第二个节点被 head 指向了,此时将 prev 的指针指向 null,因为它自己本身就是第一个首节点,所以 pre 指向 null。

    4.6 AQS 与 ReentrantLock 的联系

    ReentrantLock 实现:ReentrantLock 是根据 AQS 实现的独占锁,提供了两个构造方法如下:

    public ReentrantLock() {
            sync = new NonfairSync();
        }
    public ReentrantLock(boolean fair) {
            sync = fair ? new FairSync() : new NonfairSync();
        }
    

    ReentrantLock 有三个内部类:Sync,NonfairSync,FairSync,继承关系如下:
    在这里插入图片描述
    总结:这三个内部类都是基于 AQS 进行的实现,由此可见,ReentrantLock 是基于 AQS 进行的实现。

    ReentrantLock 提供两种类型的锁:公平锁,非公平锁。分别对应 FairSync,NonfairSync。默认实现是 NonFairSync。

    5 ReentrantLock 使用

    5.1 ReentrantLock 介绍

    ReentrantLock 在 Java 中是一个基础的锁, 实现了 Lock 接口提供的一系列基础函数,开发人员可以灵活使用函数满足各种应用场景。

    定义:ReentrantLock 是一个可重入且独占式的锁,它具有与使用 synchronized 监视器锁相同的基本行为和语义,但与 synchronized 关键字相比,它更灵活、更强大,增加了轮询、超时、中断等高级功能。

    ReentrantLock,顾名思义,它是支持可重入锁的锁,是一种递归无阻塞的同步机制。除此之外,该锁还支持获取锁时的公平和非公平选择。

    公平性:ReentrantLock 的内部类 Sync 继承了 AQS,分为公平锁 FairSync 和非公平锁 NonfairSync。

    如果在绝对时间上,先对锁进行获取的请求一定先被满足,那么这个锁是公平的,反之,是不公平的。公平锁的获取,也就是等待时间最长的线程最优先获取锁,也可以说锁获取是顺序的。

    ReentrantLock 的公平与否,可以通过它的构造函数来决定。

    5.2 ReentrantLock 基本方法 lock 与 unlock 的使用

    我们使用一个之前涉及到的 synchronized 的场景,通过 lock 接口进行实现。

    场景回顾

    • 创建两个线程,创建方式可自选;
    • 定义一个全局共享的 static int 变量 count,初始值为 0;
    • 两个线程同时操作 count,每次操作 count 加 1;
    • 每个线程做 100 次 count 的增加操作。

    结果预期:获取到的结果为 200。之前我们使用了 synchronized 关键字和乐观锁 Amotic 操作进行了实现,那么此处我们进行 ReentrantLock 的实现方式。

    实现步骤

    • step 1 :创建 ReentrantLock 实例,以便于调用 lock 方法和 unlock 方法;
    • step 2:在 synchronized 的同步代码块处,将 synchronized 实现替换为 lock 实现。

    实例

    public class DemoTest{
        private static int count = 0; //定义count = 0
        private static ReentrantLock lock = new ReentrantLock();//创建 lock 实例
        public static void main(String[] args) {
            for (int i = 0; i < 2; i++) { //通过for循环创建两个线程
                new Thread(new Runnable() {
                    @Override
                    public void run() {
                        //每个线程让count自增100次
                        for (int i = 0; i < 100; i++) {
                            try {
                                lock.lock(); //调用 lock 方法
                                count++;
                            } finally {
                                lock.unlock(); //调用unlock方法释放锁
                            }
                        }
                    }
                }). start();
            }
            try{
                Thread.sleep(2000);
            }catch (Exception e){
                e.printStackTrace();
            }
            System.out.println(count);
        }
    }
    

    代码分析
    我们通过 try finally 模块,替代了之前的 synchronized 代码块,顺利的实现了多线程下的并发。

    5.3 tryLock 方法

    Lock 接口包含了两种 tryLock 方法,一种无参数,一种带参数。

    • boolean tryLock():仅在调用时锁为空闲状态才获取该锁。如果锁可用,则获取锁,并立即返回值 true。如果锁不可用,则此方法将立即返回值 false;
    • boolean tryLock(long time, TimeUnit unit):如果锁在给定的等待时间内空闲,并且当前线程未被中断,则获取锁;

    为了了解两种方法的使用,我们先来设置一个简单的使用场景。

    场景设置

    • 创建两个线程,创建方式自选;
    • 两个线程同时执行代码逻辑;
    • 代码逻辑使用 boolean tryLock () 方法,如果获取到锁,执行打印当前线程名称,并沉睡 5000 毫秒;如果未获取锁,则打印 timeout,并处理异常信息;
    • 观察结果并进行分析;
    • 修改代码,使用 boolean tryLock (long time, TimeUnit unit) 方法,设置时间为 4000 毫秒;
    • 观察结果并进行分析;
    • 再次修改代码,使用 boolean tryLock (long time, TimeUnit unit) 方法,设置时间为 6000 毫秒;
    • 观察结果并进行分析。

    实例:使用 boolean tryLock () 方法

    public class DemoTest implements Runnable{
        private static Lock locks = new ReentrantLock();
        @Override
        public void run() {
            try {
                if(locks.tryLock()){ //尝试获取锁,获取成功则进入执行,不成功则执行finally模块
                    System.out.println(Thread.currentThread().getName()+"-->");
                    Thread.sleep(5000);
                }else{
                    System.out.println(Thread.currentThread().getName()+" time out ");
                }
            } catch (InterruptedException e) {
                 e.printStackTrace();
            }finally {
                try {
                    locks.unlock();
                } catch (Exception e) {
                    System.out.println(Thread.currentThread().getName() + "未获取到锁,释放锁抛出异常");
                }
            }
        }
        public static void main(String[] args) throws InterruptedException {
            DemoTest test =new DemoTest();
            Thread t1 = new Thread(test);
            Thread t2 = new Thread(test);
            t1. start();
            t2. start();
            t1.join();
            t2.join();
            System.out.println("over");
        }
    }
    

    结果验证:

    Thread-1-->
    Thread-0 time out 
    Thread-0 未获取到锁,释放锁抛出异常
    Over
    

    结果分析:从打印的结果来看, Thread-1 获取了锁权限,而 Thread-0 没有获取锁权限,这就是 tryLock,没有获取到锁资源则放弃执行,直接调用 finally。

    实例:使用 boolean tryLock (4000 ms) 方法
    将 if 判断进行修改如下:

    if(locks.tryLock(4000,TimeUnit.MILLISECONDS)){ //尝试获取锁,获取成功则进入执行,不成功则执行finally模块
          System.out.println(Thread.currentThread().getName()+"-->");
          Thread.sleep(5000);
      }
    

    结果验证:

    Thread-1-->
    Thread-0 time out 
    Thread-0 未获取到锁,释放锁抛出异常
    Over
    

    结果分析:tryLock 方法,虽然等待 4000 毫秒,但是这段时间不足以等待 Thread-1 释放资源锁,所以还是超时。 我们换成 6000 毫秒试试。

    实例:使用 boolean tryLock (6000 ms) 方法
    将 if 判断进行修改如下:

    if(locks.tryLock(6000,TimeUnit.MILLISECONDS)){ //尝试获取锁,获取成功则进入执行,不成功则执行finally模块
          System.out.println(Thread.currentThread().getName()+"-->");
          Thread.sleep(5000);
      }
    

    结果验证:

    Thread-1-->
    Thread-0-->
    Over
    

    结果分析:tryLock 方法,等待 6000 毫秒,Thread-1 先进入执行,5000 毫秒后 Thread-0 进入执行,都能够有机会获取锁。

    总结:以上就是 tryLock 方法的使用,可以指定最长的获取锁的时间,如果获取则执行,未获取则放弃执行。

    5.4 公平锁与非公平锁

    根据线程获取锁的抢占机制,锁可以分为公平锁和非公平锁。

    公平锁: 表示线程获取锁的顺序是按照线程请求锁的时间早晚来决定的,也就是最早请求锁的线程将最早获取到锁。

    非公平锁:非公平锁则在运行时闯入,不遵循先到先执行的规则。

    ReentrantLock:ReentrantLock 提供了公平和非公平锁的实现。

    ReentrantLock 实例:

    //公平锁
    ReentrantLock pairLock = new ReentrantLock(true);
    //非公平锁
    ReentrantLock pairLock1 = new ReentrantLock(false);
    //如果构造函数不传递参数,则默认是非公平锁。
    ReentrantLock pairLock2 = new ReentrantLock();
    

    场景介绍: 通过模拟一个场景假设,来了解公平锁与非公平锁。

    • 假设线程 A 已经持有了锁,这时候线程 B 请求该锁将会被挂起;
    • 当线程 A 释放锁后,假如当前有线程 C 也需要获取该锁,如果采用非公平锁方式,则根据线程调度策略,线程 B 和线程 C 两者之一可能获取锁,这时候不需要任何其他干涉;
    • 而如果使用公平锁则需要把 C 挂起,让 B 获取当前锁,因为 B 先到所以先执行。

    Tips:在没有公平性需求的前提下尽量使用非公平锁,因为公平锁会带来性能开销。

    5.5 lockInterruptibly 方法

    lockInterruptibly () 方法:能够中断等待获取锁的线程。当两个线程同时通过 lock.lockInterruptibly () 获取某个锁时,假若此时线程 A 获取到了锁,而线程 B 只有等待,那么对线程 B 调用 threadB.interrupt () 方法能够中断线程 B 的等待过程。

    场景设计

    • 创建两个线程,创建方式可自选实现;
    • 第一个线程先调用 start 方法,沉睡 20 毫秒后调用第二个线程的 start 方法,确保第一个线程先获取锁,第二个线程进入等待;
    • 最后调用第二个线程的 interrupt 方法,终止线程;
    • run 方法的逻辑为打印 0,1,2,3,4,每打印一个数字前,先沉睡 1000 毫秒;
    • 观察结果,看是否第二个线程被终止。

    实例:

    public class DemoTest{
        private Lock lock = new ReentrantLock();
    
        public void doBussiness() {
            String name = Thread.currentThread().getName();
            try {
                System.out.println(name + " 开始获取锁");
                lock.lockInterruptibly(); //调用lockInterruptibly方法,表示可中断等待
                System.out.println(name + " 得到锁,开工干活");
                for (int i=0; i<5; i++) {
                    Thread.sleep(1000);
                    System.out.println(name + " : " + i);
                }
            } catch (InterruptedException e) {
                System.out.println(name + " 被中断");
            } finally {
                try {
                    lock.unlock();
                    System.out.println(name + " 释放锁");
                } catch (Exception e) {
                    System.out.println(name + " : 没有得到锁的线程运行结束");
                }
            }
        }
    
        public static void main(String[] args) throws InterruptedException {
            final DemoTest lockTest = new DemoTest();
            Thread t0 = new Thread(new Runnable() {
                        public void run() {
                            lockTest.doBussiness();
                        }});
            Thread t1 = new Thread(new Runnable() {
                        public void run() {
                            lockTest.doBussiness();
                        }});
    
            t0. start();
            Thread.sleep(20);
            t1. start();
            t1.interrupt();
        }
    }
    

    结果验证:可以看到,thread -1 被中断了。

    Thread-0 开始获取锁
    Thread-0 得到锁,开工干活
    Thread-1 开始获取锁
    Thread-1 被中断
    Thread-1 : 没有得到锁的线程运行结束
    Thread-0 : 0
    Thread-0 : 1
    Thread-0 : 2
    Thread-0 : 3
    Thread-0 : 4
    Thread-0 释放锁
    

    5.6 ReentrantLock 其他方法介绍

    对 ReentrantLock 来说,方法很多样,如下介绍 ReentrantLock 其他的方法。

    • getHoldCount():当前线程调用 lock () 方法的次数;
    • getQueueLength():当前正在等待获取 Lock 锁的线程的估计数;
    • getWaitQueueLength(Condition condition):当前正在等待状态的线程的估计数,需要传入 Condition 对象;
    • hasWaiters(Condition condition):查询是否有线程正在等待与 Lock 锁有关的 Condition 条件;
    • hasQueuedThread(Thread thread):查询指定的线程是否正在等待获取 Lock 锁;
    • hasQueuedThreads():查询是否有线程正在等待获取此锁定;
    • isFair():判断当前 Lock 锁是不是公平锁;
    • isHeldByCurrentThread():查询当前线程是否保持此锁定;
    • isLocked():查询此锁定是否由任意线程保持。

    6 锁的可重入性验证

    6.1 什么是锁的可重入性

    定义:可重入锁又名递归锁,是指在同一个线程在外层方法获取锁的时候,再进入该线程的内层方法会自动获取锁(前提锁对象得是同一个对象或者 class),不会因为之前已经获取过还没释放而阻塞。

    Java 中 ReentrantLock 和 synchronized 都是可重入锁,可重入锁的一个优点是可一定程度避免死锁。

    可重入锁原理:可重入锁的原理是在锁内部维护一个线程标示,用来标示该锁目前被哪个线程占用,然后关联一个计数器。一开始计数器值为 0,说明该锁没有被任何线程占用。当一个线程获取了该锁时,计数器的值会变成 1,这时其他线程再来获取该锁时会发现锁的所有者不是自己而被阻塞挂起。

    但是当获取了该锁的线程再次获取锁时发现锁拥有者是自己,就会把计数器值加+1, 当释放锁后计数器值-1。当计数器值为 0 时,锁里面的线程标示被重置为 null,这时候阻塞的线程会被唤醒来竞争获取该锁。

    6.2 可重入锁与非可重入性锁

    为了解释可重入锁与非可重入性锁的区别与联系,我们拿可重入锁 ReentrantLock 和 非重入锁 NonReentrantLock 进行简单的分析对比。

    相同点: ReentrantLock 和 NonReentrantLock 都继承父类 AQS,其父类 AQS 中维护了一个同步状态 status 来统计重入次数,status 初始值为 0。

    不同点:当线程尝试获取锁时,可重入锁先尝试获取并更新 status 值,如果 status == 0 表示没有其他线程在执行同步代码,则把 status 置为 1,当前线程开始执行。

    如果 status != 0,则判断当前线程是否是获取到这个锁的线程,如果是的话执行 status+1,且当前线程可以再次获取锁。

    而非可重入锁是直接去获取并尝试更新当前 status 的值,如果 status != 0 的话会导致其获取锁失败,当前线程阻塞,导致死锁发生。

    Tips:看的云里雾里?没关系,看下面的示例就懂了。

    6.3 什么情况下使用可重入锁

    我们先来看看如下代码:同步方法 helloB 方法调用了同步方法 helloA。

    public class DemoTest{
        public synchronized void helloA(){
            System.out.println("helloA");
        }
        public synchronized void helloB(){
            System.out.println("helloB");
            helloA();
        }
    }
    

    在如上代码中,调用 helloB 方法前会先获取内置锁,然后打印输出。之后调用 helloA 方法,在调用前会先去获取内置锁,如果内置锁不是可重入的,那么调用线程将会一直被阻塞。

    因此,对于同步方法内部调用另外一个同步方法的情况下,一定要使用可重入锁,不然会导致死锁的发生。

    6.4 synchronized 验证锁的可重入性

    为了更好的理解 synchronized 验证锁的可重入性,我们来设计一个简单的场景。

    场景设计:

    • 创建一个类,该类中有两个方法,helloA 方法和 helloB 方法;
    • 将两个方法内部的逻辑进行 synchronized 同步;
    • helloA 方法内部调用 helloB 方法,营造可重入锁的场景;
    • main 方法创建线程,调用 helloA 方法;
    • 观察结果,看是否可以成功进行调用。

    实例:

    public class DemoTest {
        public static void main(String[] args) {
            new Thread(new SynchronizedTest()). start();
        }
    }
    class SynchronizedTest implements Runnable {
        private final Object obj = new Object();
        public void helloA() { //方法1,调用方法2
            synchronized (obj) {
                System.out.println(Thread.currentThread().getName() + " helloA()");
                helloB();
            }
        }
        public void helloB() {
            synchronized (obj) {
                System.out.println(Thread.currentThread().getName() + " helloB()");
            }
        }
        @Override
        public void run() {
            helloA(); //调用helloA方法
        }
    }
    

    结果验证:

    Thread-0 helloA()
    Thread-0 helloB()
    

    结果解析:如果同一线程,锁不可重入的话,helloB 需要等待 helloA 释放 obj 锁,如此一来,helloB 无法进行锁的获取,最终造成无限等待,无法正常执行。此处说明了 synchronized 关键字的可重入性,因此能够正常进行两个方法的执行。

    6.5 ReentrantLock 验证锁的可重入性

    相同的场景,对代码进行如下改造,将 synchronized 同步代码块修改成 lock 接口同步,我们看代码实例如下:

    public class DemoTest {
        public static void main(String[] args) {
            new Thread(new SynchronizedTest()). start();
        }
    }
    class SynchronizedTest implements Runnable {
        private final Lock lock = new ReentrantLock();
        public void helloA() { //方法1,调用方法2
            lock.lock();
            try {
                System.out.println(Thread.currentThread().getName() + " helloA()");
                helloB();
            } finally {
                lock.unlock();
            }
        }
        public void helloB() {
            lock.lock();
            try {
                System.out.println(Thread.currentThread().getName() + " helloB()");
            } finally {
                lock.unlock();
            }
        }
        @Override
        public void run() {
            helloA();
        }
    }
    

    结果验证:

    Thread-0 helloA()
    Thread-0 helloB()
    

    结果解析:ReentrantLock 一样是可重入锁,试验成功。

    7 读写锁 ReentrantReadWriteLock

    7.1 ReentrantReadWriteLock 介绍

    JDK 提供了 ReentrantReadWriteLock 读写锁,使用它可以加快效率,在某些不需要操作实例变量的方法中,完全可以使用读写锁 ReemtrantReadWriteLock 来提升该方法的运行速度。

    定义:读写锁表示有两个锁,一个是读操作相关的锁,也称为共享锁;另一个是写操作相关的锁,也叫排他锁。

    定义解读:也就是多个读锁之间不互斥,读锁与写锁互斥、写锁与写锁互斥。在没有线程 Thread 进行写入操作时,进行读取操作的多个 Thread 都可以获取读锁,而进行写入操作的 Thread 只有在获取写锁后才能进行写入操作。即多个 Thread 可以同时进行读取操作,但是同一时刻只允许一个 Thread 进行写入操作。

    7.2 ReentrantReadWriteLock 的类结构

    ReentrantReadWriteLock 是接口 ReadWriteLock 的子类实现,通过 JDK 的代码可以看出这一实现关系。

    public class ReentrantReadWriteLock
            implements ReadWriteLock, java.io.Serializable{}
    

    我们再来看下接口 ReadWriteLock,该接口只定义了两个方法:

    public interface ReadWriteLock {
        Lock readLock();
        Lock writeLock();
    }
    

    通过调用相应方法获取读锁或写锁,可以如同使用 Lock 接口一样使用。

    7.3 ReentrantReadWriteLock 的特点

    性质 1 :可重入性。

    ReentrantReadWriteLock 与 ReentrantLock 以及 synchronized 一样,都是可重入性锁。

    性质 2 :读写分离。

    我们知道,对于一个数据,不管是几个线程同时读都不会出现任何问题,但是写就不一样了,几个线程对同一个数据进行更改就可能会出现数据不一致的问题,因此想出了一个方法就是对数据加锁,这时候出现了一个问题:

    线程写数据的时候加锁是为了确保数据的准确性,但是线程读数据的时候再加锁就会大大降低效率,这时候怎么办呢?那就对写数据和读数据分开,加上两把不同的锁,不仅保证了正确性,还能提高效率。

    性质 3 :可以锁降级,写锁降级为读锁。

    线程获取写入锁后可以获取读取锁,然后释放写入锁,这样就从写入锁变成了读取锁,从而实现锁降级的特性。
    在这里插入图片描述

    性质 4 :不可锁升级。

    线程获取读锁是不能直接升级为写入锁的。需要释放所有读取锁,才可获取写锁。
    在这里插入图片描述

    7.4 ReentrantReadWriteLock 读锁共享

    ReentrantReadWriteLock 之所以优秀,是因为读锁与写锁是分离的,当所有的线程都为读操作时,不会造成线程之间的互相阻塞,提升了效率,那么接下来,我们通过代码实例进行学习。

    场景设计

    • 创建三个线程,线程名称分别为 t1,t2,t3,线程实现方式自行选择;
    • 三个线程同时运行获取读锁,读锁成功后打印线程名和获取结果,并沉睡 2000 毫秒,便于观察其他线程是否可共享读锁;
    • finally 模块中释放锁并打印线程名和释放结果;
    • 运行程序,观察结果。

    结果预期:三条线程能同时获取锁,因为读锁共享。

    实例:

    public class DemoTest {
        private ReentrantReadWriteLock lock = new ReentrantReadWriteLock();// 读写锁
        private int i;
        public String readI() {
            try {
                lock.readLock().lock();// 占用读锁
                System.out.println("threadName -> " + Thread.currentThread().getName() + " 占用读锁,i->" + i);
                Thread.sleep(2000);
            } catch (InterruptedException e) {
    
            } finally {
                System.out.println("threadName -> " + Thread.currentThread().getName() + " 释放读锁,i->" + i);
                lock.readLock().unlock();// 释放读锁
            }
            return i + "";
        }
    
        public static void main(String[] args) {
            final DemoTest demo1 = new DemoTest();
            Runnable runnable = new Runnable() {
                @Override
                public void run() {
                    demo1.readI();
                }
            };
            new Thread(runnable, "t1"). start();
            new Thread(runnable, "t2"). start();
            new Thread(runnable, "t3"). start();
        }
    }
    

    结果验证:

    threadName -> t1 占用读锁,i->0
    threadName -> t2 占用读锁,i->0
    threadName -> t3 占用读锁,i->0
    threadName -> t1 释放读锁,i->0
    threadName -> t3 释放读锁,i->0
    threadName -> t2 释放读锁,i->0
    

    结果分析:从结果来看,t1,t2,t3 均在同一时间获取了锁,证明了读锁共享的性质。

    7.5 ReentrantReadWriteLock 读写互斥

    当共享变量有写操作时,必须要对资源进行加锁,此时如果一个线程正在进行读操作,那么写操作的线程需要等待。同理,如果一个线程正在写操作,读操作的线程需要等待。

    场景设计:

    • 创建两个线程,线程名称分别为 t1,t2;
    • 线程 t1 进行读操作,获取到读锁之后,沉睡 5000 毫秒;
    • 线程 t2 进行写操作;
    • 开启 t1,1000 毫秒后开启 t2 线程;
    • 运行程序,观察结果。

    结果预期:线程 t1 获取了读锁,在沉睡的 5000 毫秒中,线程 t2 只能等待,不能获取到锁,因为读写互斥。

    实例:

    public class DemoTest {
        private ReentrantReadWriteLock lock = new ReentrantReadWriteLock();// 读写锁
        private int i;
        public String readI() {
            try {
                lock.readLock().lock();// 占用读锁
                System.out.println("threadName -> " + Thread.currentThread().getName() + " 占用读锁,i->" + i);
                Thread.sleep(5000);
            } catch (InterruptedException e) {
            } finally {
                System.out.println("threadName -> " + Thread.currentThread().getName() + " 释放读锁,i->" + i);
                lock.readLock().unlock();// 释放读锁
            }
            return i + "";
        }
    
        public void addI() {
            try {
                lock.writeLock().lock();// 占用写锁
                System.out.println("threadName -> " + Thread.currentThread().getName() + " 占用写锁,i->" + i);
                i++;
            } finally {
                System.out.println("threadName -> " + Thread.currentThread().getName() + " 释放写锁,i->" + i);
                lock.writeLock().unlock();// 释放写锁
            }
        }
    
        public static void main(String[] args) throws InterruptedException {
            final DemoTest demo1 = new DemoTest();
            new Thread(new Runnable() {
                @Override
                public void run() {
                    demo1.readI();
                }
            }, "t1"). start();
            Thread.sleep(1000);
            new Thread(new Runnable() {
                @Override
                public void run() {
                    demo1.addI();
                }
            }, "t2"). start();
        }
    }
    

    结果验证:

    threadName -> t1 占用读锁,i->0
    threadName -> t1 释放读锁,i->0
    threadName -> t2 占用写锁,i->0
    threadName -> t2 释放写锁,i->1
    

    结果解析:验证成功,在线程 t1 沉睡的过程中,写锁 t2 线程无法获取锁,因为锁已经被读操作 t1 线程占据了。

    8 锁机制之 Condition 接口

    8.1 Condition 接口简介

    任意一个 Java 对象,都拥有一组监视器方法(定义在 java.lang.Object 上),主要包括 wait ()、wait (long timeout)、notify () 以及 notifyAll () 方法。这些方法与 synchronized 同步关键字配合,可以实现等待 / 通知模式。

    定义:Condition 接口也提供了类似 Object 的监视器方法,与 Lock 配合可以实现等待 / 通知模式。Condition 可以看做是 Obejct 类的 wait ()、notify ()、notifyAll () 方法的替代品,与 Lock 配合使用。

    8.2 Condition 接口定义

    我们看到,从 JDK 的源码中可以获悉,Condition 接口包含了如下的方法,对于其中常用的方法,我们在后续的内容中会有比较详细的讲解。

    public interface Condition {
        void await() throws InterruptedException;
        long awaitNanos(long nanosTimeout) throws InterruptedException; 
        boolean await(long time, TimeUnit unit) throws InterruptedException;
        boolean awaitUntil(Date deadline) throws InterruptedException;
        void signal();
        void signalAll();
    }
    

    8.3 Condition 方法与 Object 方法的联系与区别

    联系 1:都有一组类似的方法.

    • Object 对象监视器: Object.wait()、Object.wait(long timeout)、Object.notify()、Object.notifyAll()。
    • Condition 对象: Condition.await()、Condition.awaitNanos(long nanosTimeout)、Condition.signal()、Condition.signalAll()。

    联系 2:都需要和锁进行关联。

    • Object 对象监视器: 需要进入 synchronized 语句块(进入对象监视器)才能调用对象监视器的方法。
    • Condition 对象: 需要和一个 Lock 绑定。

    区别:

    • Condition 拓展的语义方法,如 awaitUninterruptibly () 等待时忽略中断方法;
    • 在使用方法时,Object 对象监视器是进入 synchronized 语句块(进入对象监视器)后调用 Object.wait ()。而 Condition 对象需要和一个 Lock 绑定,并显示的调用 lock () 获取锁,然后调用 Condition.await ();
    • 从等待队列数量看,Object 对象监视器是 1 个。而 Condition 对象是多个。一般有几个锁对象就创建几个Condition对象,可以通过多次调用 lock.newCondition () 返回多个等待队列。

    8.4 Condition 对象的创建

    Condition 对象是由 Lock 对象创建出来的 (Lock.newCondition),换句话说,Condition 是依赖 Lock 对象的。那么我们来看看如何创建 Condition 对象。

    Lock lock = new ReentrantLock();
    Condition condition1 = lock.newCondition();
    Condition condition2 = lock.newCondition();
    

    8.5 Condition 方法介绍

    等待机制方法简介:

    • void await() throws InterruptedException:当前线程进入等待状态,直到被其它线程的唤醒继续执行或被中断;
    • void awaitUninterruptibly():当前线程进入等待状态,直到被其它线程被唤醒;
    • long awaitNanos(long nanosTimeout) throws InterruptedException:当前线程进入等待状态,直到被其他线程唤醒或被中断,或者指定的等待时间结束;nanosTimeout 为超时时间,返回值 = 超时时间 - 实际消耗时间;
    • boolean await(long time, TimeUnit unit) throws InterruptedException:当前线程进入等待状态,直到被其他线程唤醒或被中断,或者指定的等待时间结束;与上个方法区别:可以自己设置时间单位,未超时被唤醒返回 true,超时则返回 false;
    • boolean awaitUntil(Date deadline) throws InterruptedException:当前线程等待状态,直到被其他线程唤醒或被中断,或者指定的截止时间结束,截止时间结束前被唤醒,返回 true,否则返回 false。

    通知机制方法简介:

    • void signal():唤醒一个线程;
    • void signalAll():唤醒所有线程。

    8.6 ReentrantLock 与 Condition 实现生产者与消费者

    非常熟悉的场景设计,这是在讲解生产者与消费者模型时使用的案例设计,此处有细微的修改如下。

    场景修改

    • 创建一个工厂类 ProductFactory,该类包含两个方法,produce 生产方法和 consume 消费方法(未改变);
    • 对于 produce 方法,当没有库存或者库存达到 10 时,停止生产。为了更便于观察结果,每生产一个产品,sleep 3000 毫秒(5000 变 3000,调用地址也改变了,具体看代码);
    • 对于 consume 方法,只要有库存就进行消费。为了更便于观察结果,每消费一个产品,sleep 5000 毫秒(sleep 调用地址改变了,具体看代码);
    • 库存使用 LinkedList 进行实现,此时 LinkedList 即共享数据内存(未改变);
    • 创建一个 Producer 生产者类,用于调用 ProductFactory 的 produce 方法。生产过程中,要对每个产品从 0 开始进行编号 (新增 sleep 3000ms);
    • 创建一个 Consumer 消费者类,用于调用 ProductFactory 的 consume 方法 (新增 sleep 5000ms);
    • 创建一个测试类,main 函数中创建 2 个生产者和 3 个消费者,运行程序进行结果观察(未改变)。

    实例:

    public class DemoTest {
            public static void main(String[] args) {
                ProductFactory productFactory = new ProductFactory();
                new Thread(new Producer(productFactory),"1号生产者"). start();
                new Thread(new Producer(productFactory),"2号生产者"). start();
                new Thread(new Consumer(productFactory),"1号消费者"). start();
                new Thread(new Consumer(productFactory),"2号消费者"). start();
                new Thread(new Consumer(productFactory),"3号消费者"). start();
            }
    }
    
    class ProductFactory {
        private LinkedList<String> products; //根据需求定义库存,用 LinkedList 实现
        private int capacity = 10; // 根据需求:定义最大库存 10
        private Lock lock = new ReentrantLock(false);
        private Condition p = lock.newCondition();
        private Condition c = lock.newCondition();
        public ProductFactory() {
            products = new LinkedList<String>();
        }
        // 根据需求:produce 方法创建
        public void produce(String product) {
            try {
                lock.lock();
                while (capacity == products.size()) { //根据需求:如果达到 10 库存,停止生产
                    try {
                        System.out.println("警告:线程("+Thread.currentThread().getName() + ")准备生产产品,但产品池已满");
                        p.await(); // 库存达到 10 ,生产线程进入 wait 状态
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                products.add(product); //如果没有到 10 库存,进行产品添加
                System.out.println("线程("+Thread.currentThread().getName() + ")生产了一件产品:" + product+";当前剩余商品"+products.size()+"个");
                c.signalAll(); //生产了产品,通知消费者线程从 wait 状态唤醒,进行消费
            } finally {
                lock.unlock();
            }
        }
    
        // 根据需求:consume 方法创建
        public String consume() {
            try {
                lock.lock();
                while (products.size()==0) { //根据需求:没有库存消费者进入wait状态
                    try {
                        System.out.println("警告:线程("+Thread.currentThread().getName() + ")准备消费产品,但当前没有产品");
                        c.await(); //库存为 0 ,无法消费,进入 wait ,等待生产者线程唤醒
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                String product = products.remove(0) ; //如果有库存则消费,并移除消费掉的产品
                System.out.println("线程("+Thread.currentThread().getName() + ")消费了一件产品:" + product+";当前剩余商品"+products.size()+"个");
                p.signalAll();// 通知生产者继续生产
                return product;
            } finally {
                lock.unlock();
            }
        }
    }
    
    class Producer implements Runnable {
        private ProductFactory productFactory; //关联工厂类,调用 produce 方法
        public Producer(ProductFactory productFactory) {
            this.productFactory = productFactory;
        }
        public void run() {
            int i = 0 ; // 根据需求,对产品进行编号
            while (true) {
                productFactory.produce(String.valueOf(i)); //根据需求 ,调用 productFactory 的 produce 方法
                try {
                    Thread.sleep(3000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                i++;
            }
        }
    }
    class Consumer implements Runnable {
        private ProductFactory productFactory;
        public Consumer(ProductFactory productFactory) {
            this.productFactory = productFactory;
        }
        public void run() {
            while (true) {
                productFactory.consume();
                try {
                    Thread.sleep(5000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }
    

    结果验证:

    线程(1号生产者)生产了一件产品:0;当前剩余商品1个
    线程(2号生产者)生产了一件产品:0;当前剩余商品2个
    线程(1号消费者)消费了一件产品:0;当前剩余商品1个
    线程(2号消费者)消费了一件产品:0;当前剩余商品0个
    警告:线程(3号消费者)准备消费产品,但当前没有产品
    线程(2号生产者)生产了一件产品:1;当前剩余商品1个
    线程(1号生产者)生产了一件产品:1;当前剩余商品2个
    线程(3号消费者)消费了一件产品:1;当前剩余商品1个
    线程(2号消费者)消费了一件产品:1;当前剩余商品0个
    警告:线程(1号消费者)准备消费产品,但当前没有产品
    

    9 多线程售票案例

    9.1 售票机制模型

    售票机制模型是源于现实生活中的售票场景,从开始的单窗口售票到多窗口售票,从开始的人工统计票数到后续的系统智能在线售票。多并发编程能够实现这一售票场景,多窗口售票情况下保证线程的安全性和票数的正确性。
    在这里插入图片描述
    如上图所示,有两个售票窗口进行售票,有一个窗口处理退票,这就是现实生活中一个简单的售票机制。

    9.2 售票机制实现

    场景设计:

    • 创建一个工厂类 TicketCenter,该类包含两个方法,saleRollback 退票方法和 sale 售票方法;
    • 定义一个车票总数等于 10 ,为了方便观察结果,设置为 10。学习者也可自行选择数量;
    • 对于 saleRollback 方法,当发生退票时,通知售票窗口继续售卖车票;
    • 对 saleRollback 进行特别设置,每隔 5000 毫秒退回一张车票;
    • 对于 sale 方法,只要有车票就进行售卖。为了更便于观察结果,每卖出一张车票,sleep 2000 毫秒;
    • 创建一个测试类,main 函数中创建 2 个售票窗口和 1 个退票窗口,运行程序进行结果观察。
    • 修改 saleRollback 退票时间,每隔 25 秒退回一张车票;
      再次运行程序并观察结果。

    实现要求: 本实验要求使用 ReentrantLock 与 Condition 接口实现同步机制。

    实例:

    public class DemoTest {
            public static void main(String[] args) {
                TicketCenter ticketCenter = new TicketCenter();
                new Thread(new saleRollback(ticketCenter),"退票窗口"). start();
                new Thread(new Consumer(ticketCenter),"1号售票窗口"). start();
                new Thread(new Consumer(ticketCenter),"2号售票窗口"). start();
            }
    }
    
    class TicketCenter {
        private int capacity = 10; // 根据需求:定义10涨车票
        private Lock lock = new ReentrantLock(false);
        private Condition saleLock = lock.newCondition();
        // 根据需求:saleRollback 方法创建,为退票使用
        public void saleRollback() {
            try {
                lock.lock();
                capacity++;
                System.out.println("线程("+Thread.currentThread().getName() + ")发生退票。" + "当前剩余票数"+capacity+"个");
                saleLock.signalAll(); //发生退票,通知售票窗口进行售票
            } finally {
                lock.unlock();
            }
        }
    
        // 根据需求:sale 方法创建
        public void sale() {
            try {
                lock.lock();
                while (capacity==0) { //没有票的情况下,停止售票
                    try {
                        System.out.println("警告:线程("+Thread.currentThread().getName() + ")准备售票,但当前没有剩余车票");
                        saleLock.await(); //剩余票数为 0 ,无法售卖,进入 wait
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                capacity-- ; //如果有票,则售卖 -1
                System.out.println("线程("+Thread.currentThread().getName() + ")售出一张票。" + "当前剩余票数"+capacity+"个");
            } finally {
                lock.unlock();
            }
        }
    }
    
    class saleRollback implements Runnable {
        private TicketCenter TicketCenter; //关联工厂类,调用 saleRollback 方法
        public saleRollback(TicketCenter TicketCenter) {
            this.TicketCenter = TicketCenter;
        }
        public void run() {
            while (true) {
                try {
                    Thread.sleep(5000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                TicketCenter.saleRollback(); //根据需求 ,调用 TicketCenter 的 saleRollback 方法
    
            }
        }
    }
    class Consumer implements Runnable {
        private TicketCenter TicketCenter;
        public Consumer(TicketCenter TicketCenter) {
            this.TicketCenter = TicketCenter;
        }
        public void run() {
            while (true) {
                TicketCenter.sale(); //调用sale 方法
                try {
                    Thread.sleep(2000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }
    

    结果验证:

    线程(1号售票窗口)售出一张票。当前剩余票数9个
    线程(2号售票窗口)售出一张票。当前剩余票数8个
    线程(2号售票窗口)售出一张票。当前剩余票数7个
    线程(1号售票窗口)售出一张票。当前剩余票数6个
    线程(1号售票窗口)售出一张票。当前剩余票数5个
    线程(2号售票窗口)售出一张票。当前剩余票数4个
    线程(退票窗口)发生退票。当前剩余票数5个
    线程(1号售票窗口)售出一张票。当前剩余票数4个
    线程(2号售票窗口)售出一张票。当前剩余票数3个
    线程(2号售票窗口)售出一张票。当前剩余票数2个
    线程(1号售票窗口)售出一张票。当前剩余票数1个
    线程(退票窗口)发生退票。当前剩余票数2个
    线程(1号售票窗口)售出一张票。当前剩余票数1个
    线程(2号售票窗口)售出一张票。当前剩余票数0个
    警告:线程(1号售票窗口)准备售票,但当前没有剩余车票
    警告:线程(2号售票窗口)准备售票,但当前没有剩余车票
    线程(退票窗口)发生退票。当前剩余票数1个
    线程(1号售票窗口)售出一张票。当前剩余票数0个
    警告:线程(2号售票窗口)准备售票,但当前没有剩余车票
    警告:线程(1号售票窗口)准备售票,但当前没有剩余车票
    

    结果分析:从结果来看,我们正确的完成了售票和退票的机制,并且使用了 ReentrantLock 与 Condition 接口。

    代码片段分析 1:看售票方法代码。

    public void sale() {
            try {
                lock.lock();
                while (capacity==0) { //没有票的情况下,停止售票
                    try {
                        System.out.println("警告:线程("+Thread.currentThread().getName() + ")准备售票,但当前没有剩余车票");
                        saleLock.await(); //剩余票数为 0 ,无法售卖,进入 wait
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                capacity-- ; //如果有票,则售卖 -1
                System.out.println("线程("+Thread.currentThread().getName() + ")售出一张票。" + "当前剩余票数"+capacity+"个");
            } finally {
                lock.unlock();
            }
        }
    

    方法中仅仅使用了 await 方法,因为退票是场景触发的,售票窗口无需唤醒退票窗口,因为真实的场景下,可能没有退票的发生,所以无需唤醒。这与生产者与消费者模式存在着比较明显的区别。

    代码片段分析 2:看退票方法代码。

    public void saleRollback() {
            try {
                lock.lock();
                capacity++;
                System.out.println("线程("+Thread.currentThread().getName() + ")发生退票。" + "当前剩余票数"+capacity+"个");
                saleLock.signalAll(); //发生退票,通知售票窗口进行售票
            } finally {
                lock.unlock();
            }
        }
    

    退票方法只有 signalAll 方法,通知售票窗口进行售票,无需调用 await 方法,因为只要有退票的发生,就能够继续售票,没有库存上限的定义,这也是与生产者与消费者模式的一个主要区别。

    总结:售票机制与生产者 - 消费者模式存在着细微的区别,需要学习者通过代码的实现慢慢体会。由于售票方法只需要进入 await 状态,退票方法需要唤醒售票的 await 状态,因此只需要创建一个售票窗口的 Condition 对象。

    ps:以上内容来自对慕课教程的学习与总结

    展开全文
  • geventhttpclient使用一个用C编写的快速,该源于nginx,由Joyent提取和修改。 geventhttpclient专为高并发,流传输和支持HTTP 1.1持久连接而设计。 更一般而言,它旨在有效地从REST API和流式API(例如Twitter的...
  • shell 实现并发,并控制并发数量

    千次阅读 2021-08-04 10:21:18
    任务就算完成 该并发是可用的可行的 把里面的循环范围和命令改一下就可批量并发执行了 最开始产生的需求源于自己的工作,因为工作中需要对数据库的700多张表同时truncate,然是我们的工具很不好使用,而且工具里面...

    为了方便理解,一步步的来
    首先先看一下串行的:

    #! /bin/bash
    
    ST=$(date +%s)
    for i in $(seq 1 10)
    do
            echo $i
            sleep 1 # 模拟程序、命令
    done
    
    ET=$(date +%s)
    TIME=$(( ${ET} - ${ST} ))
    echo "time: ${TIME}"
    

    输出结果:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    time: 10
    

    这就最原始的进程运行模拟,串行方式,无法有效利用计算机的资源,很浪费切耗时。

    我们可以把进程放入后台运行,这样的可以达到并发执行的效果:

    #! /bin/bash
    
    ST=$(date +%s)
    for i in $(seq 1 10)
    do
    
      {
        echo $i
        sleep 1 # 模拟程序、命令
      }& # 把循环体放入后台运行,相当于是起一个独立的线程,在此处的作用就是相当于起来10个并发
    done
    wait # wait命令的意思是,等待(wait命令)上面的命令(放入后台的)都执行完毕了再往下执行,通常可以和&搭配使用
    
    ET=$(date +%s)
    TIME=$(( ${ET} - ${ST} ))
    echo "time: ${TIME}"
    

    执行的结果:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    time: 1
    

    有次可见,执行后时间由原来的10缩短到了1秒,这是很大的改进
    但是有个问题就是,这里我的总任务才是10个,那如果我的任务是1000个,那么计算机资源就不足了,就容易造成宕机。
    所以需要控制并发数据量,之前我想过很多方法,比如切割任务,和控制循环的时间等等,都不太优雅,查阅资料,找到一个可行方案:使用管道和文件描述符。

    管道:
    因为我对这方面也是新知识,所以一开始就看代码,有点懵,先看点理论知识。
    首先说是管道,有无名管道和有名管道
    无名管道,在日常使用频率超高,比如:

    ps -ef | grep java
    ls | wc -l
    

    这里面的“|”就是管道,它将前一个命令的结果输出到后一个进程中,作为两个进程的数据通道,不过他是无名的。

    使用mkfifo命令创建的管道即为有名管道,例如,mkfifo pipefile, pipefile即为有名管道
    有名管道有一个显著的特点:
    如果管道里没有数据,那么去取管道数据时,程序会阻塞住,直到管道内进入数据,然后读取才会终止这个操作,反之,管道在执行写入操作时,如果没有读取操作,同样会阻塞

    由此可以得到的有用内容就是:利用有名管道的特性就可以实现一个队列的控制。

    验证就不做了,网上验证资料很多。

    文件描述符:
    这个就不过多说了,网上资料也很多。
    但是值得注意的是,文件描述符的值不能乱取,取值范围是3-(ulimit -n)-1
    ulinit -n是 系统的open files
    默认一般就是1024, 直接输入ulimit -n就可以查看

    所以代码

    #! /bin/bash
    
    ST=$(date +%s)
    [ -e /tmp/fd1 ] || mkfifo /tmp/fd1 # 创建有名管道
    exec 5<>/tmp/fd1 # 创建文件描述符,以可读(<)可写(>)的方式关联管道文件,文件描述符5拥有有名管道文件的所有特性
    rm -rf /tmp/fd1 # 文件描述符关联后拥有管道的所有特性,所有删除管道
    NUM=$1 # 获取输入的并发数
    
    for (( i=1;i<=${NUM};i++ ))
    do
      echo >&5 # &5表示引用文件描述符5,往里面放置一个令牌
    done
    
    for i in $(seq 1 100)
    do
      read -u5
      {
        echo $i
        sleep 1 # 模拟程序、命令
        echo >&5 # 执行完把令牌放回管道
      }& # 把循环体放入后台运行,相当于是起一个独立的线程,在此处的作用就是相当于起来10个并发
    done
    wait # wait命令的意思是,等待(wait命令)上面的命令(放入后台的)都执行完毕了再往下执行,通常可以和&搭配使用
    
    ET=$(date +%s)
    TIME=$(( ${ET} - ${ST} ))
    echo "time: ${TIME}"
    

    运行:

    mshing@remtor:~/code$ ./Concurrent.sh 20
    1
    2
    3
    4
    5
    6
    ......
    95
    96
    97
    98
    99
    100
    time: 5
    

    代码分析
    举个例子吧,就拿以前我们学车来说,我们学车的时候就是车少人多,假如有20人练车,但是只有5辆教练车,车钥匙放在教练那里,每个人去练车要去和教练取钥匙练完放回教练那里。那么运行就是每次只能5人同时练习,等有人把车钥匙放回教练那里,下一个才能得到钥匙去练车。只到20个人练完。任务就算完成

    该并发是可用的可行的
    把里面的循环范围和命令改一下就可批量并发执行了

    最开始产生的需求源于自己的工作,因为工作中需要对数据库的700多张表同时truncate,然是我们的工具很不好使用,而且工具里面操作的也是串行的,有时候truncate表重建表速度更快一些,但是这样是不规范的操作,我们不应该去动表结构。所以产生了使用shell去操作数据库的想法,所以就来做个小脚本。

    展开全文
  • 术语竞态条件源于线程正在通过临界区竞争的比喻,并且该竞争的结果影响执行临界区的结果。 活力 并发应用程序及时执行的能力被称为活跃度。 最常见的一种活性问题,死锁,以及另外两种活性问题,饥饿和活锁。 僵局 ...
  • 并发测试的方法并发有4种测试方法。主要的差异点就是在并发测试的时候,是否会有连接的拆除。简单总结如下:方法1:用新建来测试并发——只有在拆除阶段的时候,会有连接的拆除。方法2:用并发的方式来测试并发,边...

    并发测试的方法

    并发有4种测试方法。主要的差异点就是在并发测试的时候,是否会有连接的拆除。简单总结如下:

    方法1:用新建来测试并发——只有在拆除阶段的时候,会有连接的拆除。方法2:用并发的方式来测试并发,边建立边拆除——在达到期望的并发值后,开始有连接的拆除和新建。方法3:用并发的方式来测试并发,从一开始就边建立边拆除——在达到期望并发值的过程中,就有连接的拆除和建立。方法4:用并发的方式来测试并发,只建立不拆除——测试至始至终都不拆除

    并发测试方法2:用并发的方式来测试并发,边拆边建立

    测试场景(该方法适合于测试最好值)

    场景1:验证系统是否可以达到指定数值的的并发值。场景2:在不知道系统并发的情况下,测试系统最大的并发值。

    在这种测试模型下,会不断的拆除会话和拆除会话,但是在每一秒来看,系统依然可以保持到一个设置的并发值。因此,在这种情况下,仪表上显示的,累积的并发值(attempted值)会远远超过我们设置的并发值。此时对DUT来说,会测试到会话表的新建和拆除。

    要点

    如何配置load?Load的单位选择conections或者simusers

    测试用例3:验证系统是否可以达到100w的并发

    假设系统的新建为3000,需要验证系统的并发是否可以达到100w。Load的设置如下图所示:

    特别注意,系统此时的新建值要低于系统的最大新建值(一定要“低于”,如果“等于”,也会在T2这段时间,也就是边建边拆的时候,出现失败,详细的说明可以参考下面的测试结果分析部分)

    假设此时系统的新建为 3000 connections/sec

    T1 = 100w/3000 = 333.3(写334)

    T2 希望的并发可以保持的时间(假设为120s)

    本例设置T3 = T1

    在aciton中,设置think time = T1,即 334s

    687b3d236894087152d481fa2d0ab20f.png

    特别说明:

    设置think time = T1,效果是使得系统在达到希望测试的并发目标的时候(100w),没有连接拆除。达到并发后,在T2这个时间段里,是有连接的拆除和新建的

    扩展说明:如果此时,设置think time = T1+T2,这时和本文描述的第一种测试方法,即“用新建的方式来测试并发”,效果是一样的。

    测试结果分析:

    83d2aa7d3f93bbcbdb98c968265becc2.png

    扩展说明

    从方法1和方法2的结果对比中,已经可以看出,不同的性能测试方法,对系统的压力的差异了。这点在性能测试设计中,和外测中需要特别注意。

    例如本例的例子。假设系统的新建能力就只有3000,那么在测试的过程中,特别在T2这一阶段就很容易出现大量失败。

    测试用例4:测试系统可以达到多大的并发

    测试系统可以达到的最大并发,和用例1的差异就是在最大负载的差异。

    第一轮测试

    一般会测试两轮。第一轮的测试目的是找到一个性能的“预定值”:

    先测试新建

    根据系统的内存(可以知道一个流表占用的内存大小,也可以知道用于流表的内存块有多大),预估并发的最大值。

    创建一个新的load。此时需要使用到stair阶段:

    5afad1c543cac5ee3e1ad95d5bc843b2.png

    总高度H= H1+H2 =预估系统最大的并发值 x ( 1 + 20%) 其中20%也是为经验值。

    H1= H x 80%

    H2 = H x 40%

    爬坡时间 T1 = H1 / 新建值 (这个新建值需要低于最大新建值,为了不让新建影响并发,建议低于1/10)

    每个阶梯(h1)的爬坡斜率和H1部分的爬坡斜率一致.

    设置think time = T1的时间

    运行这个配置,注意观察出现失败的点(此时可以重点关注http transactions per second)。然后将出现失败前的点,作为系统的最大并发值。

    5c8e451857da627803b86d6a58e81c83.png

    第二轮测试

    以第一轮测试得到的最大并发值,进行测试,确认系统能够稳定在该值。此时的测试方法和并发测试用例1的测试方法一致,不再赘述。

    a7095cd9cebbfc4adc27c0626bb4c1f3.png

    文章源于网络,不做任何商业用途~

    举报/反馈

    展开全文
  • 了解并发的基础概念、优缺点以及并发编程的 BUG 源头是什么。
  • PHP不适合高并发

    2021-03-23 12:06:14
    PHP不适合高并发?PHP可以解决高并发,也不能说适合,只是相对其他语言弱一些,如Java和Go,不过PHP7出来以后PHP性能得到了很大的提升,性能与其它的语言之间的差距不是很大了,甚至比有的语言更快。php7的一些特性...
  • 1、源于JD(Job Description)的硬性要求 2、网络上面经高频出现,且水平参差不齐,做归纳整理耗时耗力,难辨真伪 3、是成为高级工程师的必经之路,Java工程师成长路上必须翻越的山 4、是众多架构的原理和基础 ...
  • 并发编程实战

    2018-08-11 17:46:52
    并发程序中,使用和共享对象的一些最有效的策略: 线程限制:一个线程限制的对象的,通过限制在线程中,而被线程占用,且只能被占有她的线程修改 共享只读:一个共享的只读对象,在没有额外同步的情况下,可以被...
  • Java并发编程实战_盖兹

    千次阅读 2022-04-01 19:00:29
    文章目录第一部分 基础知识第1章 简介1.1 并发简史1.2 线程的优势1.3 线程带来的风险1.4 线程无处不在(框架线程或类线程并发注意点)第2章 线程安全性2.1 什么是线程安全性2.2 原子性2.3 加锁机制内置锁:...
  • 并发--并发的一些理论知识 资源源于不但搜索,自由源于不但努力
  • Android上实现高并发,可延迟处理

    千次阅读 2020-03-17 15:55:48
    2.实现Android高并发,可延迟处理的解决方案 2.1为什么不推荐无限制创建Thread执行 2.2实现多线程并发处理解决方案 2.3具体实现如下: 2.4模拟测试多线程并发及延迟执行 1.Thread和线程池优缺点对比 1.1使用...
  • 1、 并发和并行 ...一个程序是有一个或多个线程组成,源于多任务处理的需要。 CPU线程数越多,越有利于同时运行多个程序,因为线程数等同于在某个瞬间CPU能同时并行处理的任务数。 示例:浏览器 打开我
  • C++多线程并发总结

    2020-04-13 22:52:41
    文章目录线程创建与管理并发与并行多线程并发与多进程并发C++线程创建线程创建的简单示例线程同步之互斥锁lock与unlock保护共享资源lock_guard与unique_lock保护共享资源timed_mutex与recursive_mutex提供更强大的锁...
  • 术语竞态条件源于这样一种比喻,即线程在临界区中竞态的结果会影响临界区的执行结果。 竞争条件 在同一个应用程序中运行多个线程本身不会导致问题。当多个线程访问相同的资源时,问题就出现了。例如相同的内存
  • 并发:6种限流方案

    2020-06-27 16:20:05
    漏桶算法 漏桶算法的灵感源于漏斗,如下图所示: 滑动时间算法有一个问题就是在一定范围内,比如 60s 内只能有 10 个请求,当第一秒时就到达了 10 个请求,那么剩下的 59s 只能把所有的请求都给拒绝掉,而漏桶算法...
  • 1.1.4 有什么好用且简单的索引方法 前面说到大多慢查询都源于索引,怎么建立并用好索引。这里有一些简单的规则。 索引下推:性别字段不适合建索引,但确实存在查询场景怎么办?如果是多条件查询,可以建立联合索引...
  • 并发和并行的区别 并行:在同一个时间点上同时有几件事情可以同时做,核心是有这么多事情同时做 并发:在一段时间内,能把指定的事情干完就行,核心是有这么多事情要做 举例 在一条乡村公路.在同一段时间内有许多车辆要...
  • 并发和并行的区别

    2020-07-28 20:19:16
    算法源于生活,队列是一种很常见的处理并发的方法。 并行:并行是指同时完成多个事件,是并发的一种处理手段。 ​ 在上面那个例子中,并行就是建条多车道,同时让多辆车通过。 并发 饭堂打饭模型 中午12点,开饭...
  • 之所以能高效处理源于两个主要方面: Redis使用Epoll多路复用,多路指多网络连接,复用指多连接复用一个线程。 Redis属于NoSQL内存数据库,数据操作在内存。 Redis能单机处理几十万并发请求,限制Redis的能力大小...

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 17,933
精华内容 7,173
热门标签
关键字:

并发源于