并发编程_并发编程实战 - CSDN
精华内容
参与话题
  • JAVA并发编程与高并发面试

    千人学习 2018-04-28 06:09:39
    [JAVA工程师必会知识点之并发编程] 1、现在几乎100%的公司面试都必须面试并发编程,尤其是互联网公司,对于并发编程的要求更高,并发编程能力已经成为职场敲门砖。 2、现在...
  • 并发编程(一)

    2020-03-11 15:57:49
    一、并发编程与高并发 并发编程与高并发是两个不同的概念,并发编程是相对于以往单线程的编程模型,而采用多线程的编程模型,以充分利用CPU资源。高并发是一类场景,应用的并发请求量非常大的情景,为了解决高并发...

    一、并发编程与高并发

    并发编程与高并发是两个不同的概念,并发编程是相对于以往单线程的编程模型,而采用多线程的编程模型,以充分利用CPU资源。高并发是一类场景,应用的并发请求量非常大的情景,为了解决高并发场景的问题,有很多技术手段,其中多线程并发编程模型是一种技术手段,还有包括扩容、缓存、队列、拆分、服务降级与熔断、数据库分库分表等等。

    并发的概念:同时拥有两个或更多的线程,如果是单核CPU,则这多个线程交替获取CPU资源,轮流执行,同一时刻只有一个处于执行状态,其它线程处于其它状态(JVM模型中,线程都有哪些状态?初始状态、就绪状态、运行中状态、阻塞、等待、超时等待、终止);如果是多核CPU,则核心数个线程将同时获取CPU资源,同时执行,多余的线程仍然需要和这些执行中的线程交替获取CPU资源。

    谈到并发,主要是考虑多个线程操作相同的资源,保证线程安全,合理调度和使用共享资源,保证正确性。

    谈到高并发,主要是考虑能够通过各种技术使得服务器能够同时处理很多请求,提高系统性能。

    二、并发编程知识点

    线程安全

    线程封闭

    线程调度

    同步容器/并发容器

    AQS

    J.U.C

    三、多线程实现计数器累加

    要保证多线程计数器累加的线程安全,关键是++操作的原子性及操作变量的数值可见性,一般情况下,维护一个变量的线程安全,可以考虑Atomic类,该类保证单调用其方法是原子操作,且保证可见性(其内部保存数据的变量由volatile修饰)

    加锁也可以实现但是太重,性能不好

    public class Counter {
        public static void main(String[] args) {
            ExecutorService executorService = Executors.newFixedThreadPool(3);
            try {
                for (int i = 0; i < 100; i++){
                    executorService.execute(new Runnable() {
                        @Override
                        public void run() {
                            int number = Counter.count.incrementAndGet();
                            System.out.println(Thread.currentThread().getId() + ", " + number);
                        }
                    });
                }
            } catch (Exception e) {
                e.printStackTrace();
            } finally {
                executorService.shutdown();
            }
        }
    
        private static AtomicInteger count = new AtomicInteger(0);
    }

    四、CPU缓存

    为什么要有CPU缓存?

    原本CPU是直接和主内存交互,但是随着CPU频率的不断提高,其与主内存的速度差异越来越大,CPU需要等内存读取,这就浪费了CPU资源。为了缓解二者之间的速度差异,加入了高速缓存(小而快)。

    而高速缓存中毕竟只能保存一小部分数据,那么为什么缓存的内容还是有效的呢?

    根据局部性原理:时间局部性和空间局部性,目前读取的数据往往在将来还会使用;使用某个位置的数据往往会使用其周围的数据。因此缓存内容是有效的,其内容不会总是失效。

    而随着发展,缓存出现多级缓存,为了实现各个CPU缓存之间数据的一致性,引入了MESI协议,缓存中的内容状态有四种,并在这四种之间不断转换,通过该协议保证各个缓存中的数据是一致的。

    • Java内存模型(JMM)

    Java内存模型规范规定了,一个线程如何以及何时能够看到其它线程修改过的共享变量的值,以及在必要时如何同步地访问共享变量。

    Java内存模型划分为主内存(存储共享变量值,各个线程共享),工作内存(每个线程独有,将主内存中的值读取副本到工作内存,然后进行操作,操作后再写回主内存)

    由于各个线程都有自己的工作内存,因此在从主内存读取值后进行操作,可能会出现各个线程操作的结果错乱的问题。为了保证数据的安全,规定了八种同步操作:

    Lock、unlock

    Read、load、use

    Assign、store、write

     

     

    而且针对上述八种操作,有一些规定,比如:

    Lock只能作用于主内存,只能有一个线程对该变量进行lock,多次lock必须对应多次unlock才释放该变量

    Read必须在load之前,read和load必须成对出现,write和store与之类似

    Assign到工作内存的值必须在某一时刻通过store和write到主内存,不能随意丢弃

    等等很多的规定

    五、线程安全性

    线程安全性最核心的定义就是正确性。正确性的含义是某个类的行为与其设计完全一致。因此线程安全性的定义是:当多个线程并发访问某个类时,不论运行时环境如何调度或者这些线程将如何交替执行,并且在主调代码中不需要额外的同步措施,这个类始终都能够表现出正确的行为,那么就称这个类是线程安全的。

    线程安全性体现在三点:

    原子性:提供了互斥访问,一段时间内只能有一个线程来对某个共享变量进行操作

    可见性:一个线程对主内存内共享变量的修改能够及时被其它线程看到

    有序性:一个线程观察其它线程中指令的执行顺序,由于指令的重排序,其观察结果往往是杂乱无序的

     

     

     

    原子性:

    总结:需要学习的有Atomic包、CAS算法、synchronized关键字、Lock包

    Atomic包中有以下的类:

    重点学习以下几个:

    AtomicInteger类

    AtomicBoolean类

    AtomicLong类与LongAdder类的区别

    AtomicLongArray类

    AtomicReference类、AtomicReferenceFieldUpdater类

    AtomicStampReference类:解决CAS的ABA问题

     

     

    • AtomicInteger类

    示例代码:

    public static AtomicInteger count = new AtomicInteger(0);
    
    private static void add(){
    
         count.incrementAndGet();//变量操作
    
    }

    示例中,对count变量的+1操作,采用的是incrementAndGet方法,此方法的源码中调用了一个名为unsafe.getAndAddInt的方法。

    public final int incrementAndGet() {
    
         return unsafe.getAndAddInt(this, valueOffset, 1) + 1;
    
    }

    而Unsafe类中的getAndAddInt方法的具体实现为:

    public final int getAndAddInt(Object var1, long var2, int var4) {
    
        int var5;
    
        do {
    
            var5 = this.getIntVolatile(var1, var2);
    
        } while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));
    
        return var5;
    
    }

    在此方法中,方法参数为要操作的对象Object var1底层当前的数值为var2要增加的值为var4。定义的var5为从底层取出来的值。采用do..while循环的方式去比较底层当前值与之前获取的是否一致比较为一致才将值进行修改,否则认为CAS操作失败进行重试。而这个比较再进行修改的方法就是compareAndSwapInt就是我们所说的CAS操作,它是一系列位于Unsafe中的native修饰方法,由底层实现。CAS取的是compareAndSwap三个单词的首字母.

    public final native boolean compareAndSwapObject(Object var1, long var2, Object var4, Object var5);
    
    public final native boolean compareAndSwapInt(Object var1, long var2, int var4, int var5);
    
    public final native boolean compareAndSwapLong(Object var1, long var2, long var4, long var6);

    另外,示例代码中的count位于方法中的操作时使用的,可以理解为JMM中的工作内存,而这里的底层数值即为主内存

    • AtomicBoolean类

          与AtomicInteger和AtomicLong类似,只不过维护一个布尔类型的变量,而且通过其方法compareAndSet可以实现控制一段代码只让一个线程执行,AtomicBoolean源码如下,只有AtomicBoolean维护的值等于expect时才会修改为update

    public final boolean compareAndSet(boolean expect, boolean update) {
        int e = expect ? 1 : 0;
        int u = update ? 1 : 0;
        return unsafe.compareAndSwapInt(this, valueOffset, e, u);
    }

    通过上述方法实现一段代码只有一个线程能执行:

    //是否发生过
    private static AtomicBoolean isHappened = new AtomicBoolean(false);
    // 请求总数
    public static int clientTotal = 5000;
    // 同时并发执行的线程数
    public static int threadTotal = 200;
    
    public static void main(String[] args) throws Exception {
        ExecutorService executorService = Executors.newCachedThreadPool();
        //通过信号量控制同时并发的线程数最大为threadTotal
        final Semaphore semaphore = new Semaphore(threadTotal);
        //通过向下计数闭锁来控制一共有clientTotal个线程执行 
        final CountDownLatch countDownLatch = new CountDownLatch(clientTotal);
        for (int i = 0; i < clientTotal ; i++) {
            executorService.execute(() -> {
                try {
                     semaphore.acquire();
                     test();
                     semaphore.release();
                 } catch (Exception e) {
                     log.error("exception", e);
                 }
                    countDownLatch.countDown();
             });
        }
        //等待闭锁值减到0
        countDownLatch.await();
        executorService.shutdown();
        log.info("isHappened:{}", isHappened.get());
    }
    
    private static void test() {
        //控制某有一段代码只执行一次
        //只有一个线程能将false置为true,其它线程均发现为true无法赋值成功
        if (isHappened.compareAndSet(false, true)) {
            log.info("execute");
        }
    }

    结果是log只会打印一次,即只有一个线程能够执行test方法中if块中的代码

    [main] INFO com.superboys.concurrency.example.Atomic.AtomicExample6 - isHappened:true
    • AtomicLong类与LongAdder类

    先来看一看这两个类的操作

    AtomicLong:
    //变量声明
    public static AtomicLong count = new AtomicLong(0);
    //变量操作
    count.incrementAndGet();
    //变量取值
    count.get();
    
    LongAdder:
    //变量声明
    public static LongAdder count = new LongAdder();
    //变量操作
    count.increment();
    //变量取值
    count

    AtomicLong与AtomicInteger类似,只是表示Long类型的值,那为何还要有LongAdder类呢?

          通过对AtomicInteger的分析我们得知,其内部通过Unsafe的方法来进行线程安全的写入操作,而Unsafe中通过不断CAS尝试直到写入成功,如果并发量比较大,则很多的线程需要进行很多次的CAS尝试,这样效率比较低。(对于基本类型long、double,jvm允许将64位的读写操作变成两个32位的读写操作来完成)

          LongAdder类为了保证在并发比较大的情况下有比较好的性能,内部通过cell数组来保存一个long数值,即将long数值拆分成很多个值分别保存在数组中的每一个位置上,对该数值的加减操作只需要对数组中的一个值进行加减即可,而如果需要获得该long值则需要将数组中的所有值进行求和。该数组默认初始化长度为2。每个线程访问该值时,通过hash算法映射到其中一个数字进行操作。这其实采用了分段锁的思想,提高了并发度,减少冲突的发生。获取其值的方法源码如下

    public String toString() {
            return Long.toString(sum());
    }
    
    public long sum() {
            Cell[] as = cells; Cell a;
            long sum = base;
            if (as != null) {
                for (int i = 0; i < as.length; ++i) {
                    if ((a = as[i]) != null)
                        sum += a.value;
                }
            }
            return sum;
    }

    从源码中可以看出,求数组的和时并没有任何加锁操作,直接将数组中的值相加求和,如果此时有并发的修改操作,将直接读取到修改的结果。实际中在处理并发度高的时候用LongAdder,而并发度低的时候用实现简单的AtomicLong效率更高。

     

    • AtomicLongArray类

          这个类实际上和AtomicLong区别不大,只是内部维护了一个Long值的数组,而不是一个Long值

     

    • AtomicReference类、AtomicReferenceFieldUpdater类

          AtomicReference类与AtomicInteger、~Long、~Boolean类似,只不过其内部维护一个通过泛型指定的引用类型,比如Integer等等,而AtomicReferenceFieldUpdater可以将一个用户自定义的类中的一个字段封装为原子,并且这个字段必须用volatile修饰、且不能static修饰,而且需要添加get方法

    @Slf4j
    public class AtomicExample5 {
    
        //原子性更新某一个类的一个实例
        private static AtomicIntegerFieldUpdater<AtomicExample5> updater
        = AtomicIntegerFieldUpdater.newUpdater(AtomicExample5.class,"count");
    
        @Getter
        public volatile int count = 100;//必须要volatile标记,且不能是static
    
        public static void main(String[] args) {
            AtomicExample5 example5 = new AtomicExample5();
    
            if(updater.compareAndSet(example5,100,120)){
                log.info("update success 1,{}",example5.getCount());
            }
    
            if(updater.compareAndSet(example5,100,120)){
                log.info("update success 2,{}",example5.getCount());
            }else{
                log.info("update failed,{}",example5.getCount());
            }
        }
    }

    此方法输出为success 1

    [main] INFO com.superboys.concurrency.example.Atomic.AtomicExample5 - update success 1,120
    [main] INFO com.superboys.concurrency.example.Atomic.AtomicExample5 - update failed,120
    • AtomicStampReference类

          以上介绍的Atomic类都是通过CAS操作来保证线程安全,但是这些类都没有解决CAS操作的ABA问题,而AtomicStampReference解决了ABA问题,其解决方法是内部维护一个版本号,只要是修改操作,就将版本号增加,这样当出现ABA问题时,线程就能通过版本号发现,从而放弃此次修改操作,也就防止了ABA问题造成影响。

    private static class Pair<T> {
            final T reference;
            final int stamp;
            private Pair(T reference, int stamp) {
                this.reference = reference;
                this.stamp = stamp;
            }
            static <T> Pair<T> of(T reference, int stamp) {
                return new Pair<T>(reference, stamp);
            }
    }
    
    private volatile Pair<V> pair;
    
    private boolean casPair(Pair<V> cmp, Pair<V> val) {
            return UNSAFE.compareAndSwapObject(this, pairOffset, cmp, val);
        }
    
    public boolean compareAndSet(V   expectedReference,
                                     V   newReference,
                                     int expectedStamp,
                                     int newStamp) {
            Pair<V> current = pair;
            return
                expectedReference == current.reference &&
                expectedStamp == current.stamp &&
                ((newReference == current.reference &&   //排除新的引用和新的版本号与底层的值相同的情况
                  newStamp == current.stamp) ||
                 casPair(current, Pair.of(newReference, newStamp)));

    通过上面的源码可以看出,通过stamp来保存版本号,观察compareAndSet方法可以看到其会对期望值和期望版本号进行比较,不同则会返回(&&是短路与),而且如果新值和新版本号均与旧值旧版本号相等,则直接返回(||也是短路操作),否则通过CAS操作casPair对值和版本号进行赋值。

     

    Synchronized关键字是通过JVM层面的锁实现互斥操作,不可以中断的锁,适合竞争不激烈,可读性好;

    Lock是类库层面的锁实现互斥操作,可中断,功能多,多样化同步,竞争激烈时优于synchronized;

    AtomicXxx类也是类库层面实现的互斥操作,竞争激烈时效果优于Lock,但是功能单一,只能维护单个值

     

    synchronized关键字:

    修饰代码块,可以指定一个实例对象作为锁,也可以指定一个类(将该类对应的类对象作为锁)作为锁

    修饰普通方法,默认调用该方法的实例作为锁,子类继承父类中的方法,方法的synchronized关键字不会继承到

    修饰静态方法,默认该方法所在类对应的类对象作为锁

     

     

    可见性:

    总结:synchronized、volatile

    导致共享变量在多个线程之间不可见的原因:

    •       线程交替执行
    •       指令重排序结合线程交替执行
    •       线程对共享变量的修改没有及时从工作内存更新到主内存

    Synchronized保证可见性:

    JMM对于synchronized有两条规定,加锁时会将线程的工作内存中保存的该共享变量清空,从主内存读取一次,从而得到最新的值;解锁时必须将该线程工作内存中的值更新到主内存

    Volatile保证可见性

    通过加入内存屏障来禁止指令重排序,以及强制规定线程对volatile变量的读必须从主内存读取(防止工作内存与主内存内容不一致)、对volatile变量的写必须直接将工作内存的值更新回主内存(防止工作内存的值没有及时更新回去,导致内容不一致)来实现。

    由以上及java内存模型可以看出,要保证可见性其实很简单,只需要保证工作内存的读写与主内存读写绑定即可,即要读工作内存则先从主内存读最新值再操作,操作后要写工作内存则一定直接写回主内存。

     

    有序性:

    总结:happens-before原则

    JMM中,允许编译器和处理器对指令进行重排序,但是重排序过程不会影响到单线程程序的执行,却会影响到多线程并发执行的正确性

    可以通过volatile、synchronized、lock实现有序性

    Java本身具有一定的有序性,可以通过happens-before原则来判断两个指令是否本身就有序,无需通过其它方式保证,如果不满足该原则,那么虚拟机可能会对其进行重排序

    Happens-before原则:

    程序次序规则一个线程内,按照代码书写顺序,书写在前面的操作先行发生于书写在后面的操作。(怎么理解,单线程程序内指令不也会重排序吗?从两个角度考虑,如果两行代码具有先后执行依赖关系,则不会对其进行指令重排序,因此满足上述规则;如果两行代码没有依赖关系,那么可能会进行指令重排序,但是重排序的执行结果和不重排序的执行结果是一致的,因此完全可以认为书写在前的还是先执行的,仍然满足上述规则)

    锁定规则,一个unlock操作先行发生于后面对同一个锁的lock操作

    Volatile规则,对volatile变量的写操作一定会将工作内存中的值及时更新回主内存,因此后面的读操作一定能读到写操作后的最新值,可以说写先行发生于读

    传递规则,A先行发生于B,B先行发生于C,立即推:A先行发生于C

    前四条比较重要,后四条比较显而易见

    线程启动原则,一个线程的start方法先行发生于该线程中的其它操作

    线程中断原则,一个线程interrupt方法先行发生于该线程检测到中断事件的发生

    线程终结规则,线程中所有的操作都先行发生于线程的终止检测

    对象终结规则,一个对象的初始化先行发生于该对象finalize方法的开始

     

    展开全文
  • Java高并发编程笔记

    千次阅读 2019-05-28 17:44:54
    观老齐《Java高并发编程笔记》笔记。 老齐的IT加油站网址:http://www.itlaoqi.com/ 什么是并发 并发就是指程序同时处理多个任务的能力。 并发编程的根源在于对多个任务情况下对访问资源的有效控制。 程序、进程...

    观老齐《Java高并发编程笔记》笔记。

    老齐的IT加油站网址:http://www.itlaoqi.com/

    什么是并发

    1. 并发就是指程序同时处理多个任务的能力。
    2. 并发编程的根源在于对多个任务情况下对访问资源的有效控制。

    程序、进程与线程

    1. 程序是静态概念,windows下通常指exe文件。
    2. 进程是动态概念,是程序在运行状态,进程说明程序在内存中的边界。
    3. 线程是进程内的一个“基本任务”,每个线程都有一个自己的功能,是CPU分配与调度的基本单位。

    临界区

    1. 临界区是用来表示一种公共资源与共享数据,可以被多个线程使用。
    2. 同一时间只能有一个线程访问临界区(阻塞状态),其他资源必须等待

    线程安全

    在拥有共享数据的多条线程并行执行的程序中,线程安全的代码会通过同步机制保证各个线程都可以正常且正确执行,不会出现数据污染等意外情况。

    Java内存模型

    Java中创建线程的三种方式

    1. 继承Thread类创建线程
    2. 实现Runnable接口创建线程
    3. 使用Callable和Future创建线程

    并发工具包-Concurrent

    1. Jdk5以后我们专门提供了一个并发工具包java.util.concurrent
    2. java.util.concurrent 包含许多线程安全的、测试良好、高性能的并发构建快,创建concurrent的目的就是要实现Collection框架对数据     结构所执行的并发操作。通过提供一组可靠的、高性能并发构建块,开发人员可以提高并发类的线程安全、可伸缩性、性能、可读性和可靠性。

    代码中的同步机制

    Synchronized(同步锁)关键字的作用就是利用一个特定的对象设置一个锁lock,在多线程并发访问的时候,同时只允许一个线程可以获得这个锁,执行特定的代码。执行后释放锁,继续由其他线程争抢。

    Synchronized的使用场景

    Synchronized可以使用在以下三种场景,对应不同的锁对象。

    1. Synchronized 代码块 – 任意对象即可
    2. Synchronized 方法 – this当前对象
    3. Synchronized 静态方法 – 该类的字节码对象

    线程的五种状态

    死锁

    线程安全与不安全区别

     

    线程安全

    线程不安全

    优点

    可靠

    速度快

    缺点

    执行速度慢

    可能与预期不符

    使用建议

    需要线程共享时使用

    在线程内部使用,无需线程间共享

    线程安全、线程不安全的类

    线程安全

    线程不安全

    Vector

    ArrayList、LinkedList

    Properties

    HashSet、TreeSet

    StringBuffer

    StringBuilder

    HashTable

    HashMap

    Java.util.concurrent

    并发是伴随着多核处理器的诞生而产生的,为了充分利用硬件资源,诞生了多线程技术。但是多线程又存在资源竞争的问题,引发了同步和互斥的问题JDK1.5 推出的java.util.concurren(并发工具包)来解决这些问题。

    New Thread的弊端

    1. new Thread() 新建对象,性能差
    2. 线程缺乏统一管理,可能无限制的新建线程 ,相互竞争,严重时会占用过多系统资源导致死机或oom

    ThreadPool – 线程池

    1. 重用存在的线程,减少对象对象、消亡的开销
    2. 线程总数可控,提高资源的利用率
    3. 避免过多资源竞争,避免阻塞
    4. 提供额外功能,定时执行、定期执行、监控等。

    线程池的种类

    在java.util.concurrent中,提供了工具类Executors(调度器)对象来创建线程池,可以创建的线程池有四种:

    1. CachedThreadPool – 可缓存线程池
      1. 可缓存线程的特点就是,无限大,如果线程池中没有可用的线程则创建,有空闲线程则利用起来
      2. Shutdown() 代表关闭线程池(等待所有线程完成)
      3. ShutdownNow() 代表立即终止线程池的运行,不等待线程,不推荐使用
    2. FixedThreadPool – 定长线程池
      1. 定长线程池的特点就是固定线程总数,空闲线程用于执行任务,如果线程都在使用后续任务则处于等待状态在线程池中的线程如果任务处于等待状态,备选的等待算法默认         FIFO(先进先出),LIFO(后进先出)
    3. SingleThreadExecutor – 单线程池
    4. ScheduledThreadPool – 调度线程池

    CountDownLatch – 倒计时锁

    CountDownLatch 倒计时锁特别适合“总 - 分任务”,例如多线程计算后的数据汇总

    CountDownLatch 类位于java.util.concurent(JUC)包下,利用它可以实现类似计数器的功能。比如有一个任务A , 他要等其他三个任务执行完毕后才能执行,此时就可以利用CountDownLatch来实现这种功能了。

    Semephore信号量

    public static void main(String[] args) {
        ExecutorService threadPool = Executors.newCachedThreadPool();
        Semaphore semaphore = new Semaphore(10); // 定义5个信号量,也就是说服务器只允许5个人在里面玩
        for (int i = 1; i <= 20 ; i++) {
            final int index = i;
            threadPool.execute(new Runnable() {
                @Override
                public void run() {
                    try {
                        semaphore.acquire();//获取一个信号量,“占用一个跑道”
                        play();
                        semaphore.release();//执行完成后,释放这个信号量。 “从跑道出去”
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            });
        }
        threadPool.shutdown();
    }
    
    public static void play(){
        try {
            System.out.println(new Date() + " " + Thread.currentThread().getName() + ":获得紫禁之巅服务器进入资格");
            Thread.sleep(2000);
            System.out.println(new Date() + " " + Thread.currentThread().getName() + ":退出服务器");
            Thread.sleep(500);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
    

    CyclicBarrier循环屏障

    private static CyclicBarrier cyclicBarrier = new CyclicBarrier(5);
    
    public static void main(String[] args) {
        ExecutorService executorService = Executors.newCachedThreadPool();
        for (int i = 1; i <=20 ; i++) {
            final int index = i;
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            executorService.execute(new Runnable() {
                @Override
                public void run() {
                    go();
                }
            });
        }
        executorService.shutdown();
    }
    
    public static void go(){
        System.out.println(Thread.currentThread().getName() + "准备就绪");
        try {
            cyclicBarrier.await();//设置屏障点,当累计5个线程都准备好时,才运行后面代码
            System.out.println(Thread.currentThread().getName()+ "开始运行");
        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (BrokenBarrierException e) {
            e.printStackTrace();
        }
    }
    

    重入锁

    1. 重入锁是指任意线程在获取到锁之后,再次获取该锁而不会被 该锁阻塞
    2. ReentrantLock设计的目标是用来代替syncchronized关键字。

    ReentrantLock与synchronized的区别

    特征

    Synchronized(推荐)

    ReentrantLock

    底层原理

    JVM实现

    Jdk实现

    性能区别

    低--->高(jdk5)

    锁的释放

    自动释放(编译器保证)

    手动释放(finally保证)

    编码程度

    简单

    复杂

    锁的粒度

    读写不区分

    读锁、写锁

    高级功能

    公平锁、非公平锁唤醒Condition分组唤醒中断等待锁

    Condition条件唤醒

    1. 我们在并行程序中,避免不了某些线程要预先规定好执行顺序,例如:先新增在修改,先买在卖。先进后出。。。,对于类似场景,使用JUC的Condition对象在合适不过。
    2. JUC中提供了Condition对象,用于让指定线程等待与唤醒,按预期顺序执行。他必须和ReentrantLock重入锁配合使用。
    3. Condition用于替代wait()和notify()方法,notify()方法用于随机唤醒等待线程。而Condition可以唤醒指定线程,这有利于更好的控制并发程序。

    Condition核心方法

    1. await()     - 阻塞当前线程,直到singal唤醒
    2. singal()     - 唤醒被await()的线程,从中断处继续执行
    3. singalAll() – 唤醒所有被await()阻塞的线程(不常用)

    JUC之Callable&Future

    1. Callable和Runnable一样代表着任务,区别在于Callable有返回值并且可以抛出异常。
    2. Future是一个接口,它用于表示异步计算的结果。提供了检查计算是否完成的方法,以等待计算的完成,并获取计算的结果。

    JUC之并发容器

    JUC之Atomic包与CAS算法

    原子性:是指一个操作或多个操作要么全部执行,且执行过程中不会被任何因素打断,要么就都不执行。

    1. Atomic包是java.util.concurrent下的另一个专门为线程安全设计的java包,包含多个原子操作类。
    2. Atomic常用类
      1. AtomicInteger
      2. AtomicIntegerArray
      3. AtomicBoolean
      4. AtomicLong
      5. AtomicLongArray

    CAS算法

    1. 锁是用来做并发最简单的方式,当然其代价也是最高的。独占锁是一种悲观锁,synchronized就是一种独占锁,他假设最坏的情况,并且只有在确保其他线程不会干扰的情况下执行,会导致其他所有需要锁的线程挂起,等待持有锁的线程释放锁。
    2. 所谓乐观锁就是,每次不加锁而是假设没有冲突而去完成某项工作,如果因为冲突失败就重试,知道成功为止。其中CAS(比较与交换,Compare And Swap)是一种有名的无锁算法。

    Atomic的应用场景

    虽然基于CAS的线程安全机制很好很高效,但是要说的是,并非所有线程安全都可以用这样的方法来实现,这只适合一些粒度比较小型,如计数器这样的需求用起来才有效,否则也不会有锁的存在了。

    展开全文
  • 并发编程知识梳理

    万次阅读 2019-04-21 17:00:27
    1.问题 ... 5、何谓响应式编程? 6、操作系统如何调度多线程? 2.关键词 同步,异步,阻塞,非阻塞,并行,并发,临界区,竞争条件,指令重排,锁,amdahl,gustafson 3.全文概要 由于...

    1.问题

    • 1、什么是线程的交互方式?

    • 2、如何区分线程的同步/异步,阻塞/非阻塞?

    • 3、什么是线程安全,如何做到线程安全?

    • 4、如何区分并发模型?

    • 5、何谓响应式编程?

    • 6、操作系统如何调度多线程?

    2.关键词

    同步,异步,阻塞,非阻塞,并行,并发,临界区,竞争条件,指令重排,锁,amdahl,gustafson

    3.全文概要

    由于单机的性能上限原因我们才不得不发展分布式技术。那么话说回来,如果单机的性能没能最大限度的榨取出来,就盲目的就建设分布式系统,那就有点本末倒置了。如果有可能的话,不要用分布式,意思是说如果单机性能满足的话,就不要折腾复杂的分布式架构。如果说分布式架构是宏观上的性能扩展,那么高并发则是微观上的性能调优,这也是上一篇性能部分拆出来的大专题。本文将从线程的基础理论谈起,逐步探究线程的内存模型,线程的交互,线程工具和并发模型的发展。扫除关于并发编程的诸多模糊概念,从新构建并发编程的层次结构。

    4.基础理论

    4.1基本概念

    开始学习并发编程前,我们需要熟悉一些理论概念。既然我们要研究的是并发编程,那首先应该对并发这个概念有所理解才是,而说到并发我们肯定要要讨论一些并行。

    • 并发:一个处理器同时处理多个任务

    • 并行:多个处理器或者是多核的处理器同时处理多个不同的任务

    然后我们需要再了解一下同步和异步的区别:

    • 同步:执行某个操作开始后就一直等着按部就班的直到操作结束

    • 异步:执行某个操作后立即离开,后面有响应的话再来通知执行者

    接着我们再了解一个重要的概念:

    • 临界区:公共资源或者共享数据

    由于共享数据的出现,必然会导致竞争,所以我们需要再了解一下:

    • 阻塞:某个操作需要的共享资源被占用了,只能等待,称为阻塞

    非阻塞:某个操作需要的共享资源被占用了,不等待立即返回,并携带错误信息回去,期待重试

     

    如果两个操作都在等待某个共享资源而且都互不退让就会造成死锁:

    • 死锁:参考著名的哲学家吃饭问题

    • 饥饿:饥饿的哲学家等不齐筷子吃饭

    • 活锁:相互谦让而导致阻塞无法进入下一步操作,跟死锁相反,死锁是相互竞争而导致的阻塞

    4.2并发级别

    理想情况下我们希望所有线程都一起并行飞起来。但是CPU数量有限,线程源源不断,总得有个先来后到,不同场景需要的并发需求也不一样,比如秒杀系统我们需要很高的并发程度,但是对于一些下载服务,我们需要的是更快的响应,并发反而是其次的。所以我们也定义了并发的级别,来应对不同的需求场景。

    • 阻塞:阻塞是指一个线程进入临界区后,其它线程就必须在临界区外等待,待进去的线程执行完任务离开临界区后,其它线程才能再进去。

    • 无饥饿:线程排队先来后到,不管优先级大小,先来先执行,就不会产生饥饿等待资源,也即公平锁;相反非公平锁则是根据优先级来执行,有可能排在前面的低优先级线程被后面的高优先级线程插队,就形成饥饿

    • 无障碍:共享资源不加锁,每个线程都可以自有读写,当监测到被其他线程修改过则回滚操作,重试直到单独操作成功;风险就是如果多个线程发现彼此修改了,所有线程都需要回滚,就会导致死循环的回滚中,造成死锁

    • 无锁:无锁是无障碍的加强版,无锁级别保证至少有一个线程在有限操作步骤内成功退出,不管是否修改成功,这样保证了多个线程回滚不至于导致死循环

    • 无等待:无等待是无锁的升级版,并发编程的最高境界,无锁只保证有线程能成功退出,但存在低级别的线程一直处于饥饿状态,无等待则要求所有线程必须在有限步骤内完成退出,让低级别的线程有机会执行,从而保证所有线程都能运行,提高并发度。

    4.3量化模型

    首先,多线程不意味着并发,但并发肯定是多线程或者多进程。我们知道多线程存在的优势是能够更好的利用资源,有更快的请求响应。但是我们也深知一旦进入多线程,附带而来的是更高的编码复杂度,线程设计不当反而会带来更高的切换成本和资源开销。但是总体上我们肯定知道利大于弊,这不是废话吗,不然谁还愿意去搞多线程并发程序,但是如何衡量多线程带来的效率提升呢,我们需要借助两个定律来衡量。

    • Amdahl

      S=1/(1-a+a/n)
      其中,a为并行计算部分所占比例,n为并行处理结点个数。这样,当1-a=0时,(即没有串行,只有并行)最大加速比s=n;当a=0时(即只有串行,没有并行),最小加速比s=1;当n→∞时,极限加速比s→ 1/(1-a),这也就是加速比的上限。

    • Gustafson

      系统优化某部件所获得的系统性能的改善程度,取决于该部件被使用的频率,或所占总执行时间的比例。

    两面列举了这两个定律来衡量系统改善后提升效率的量化指标,具体的应用我们在下文的线程调优会再详细介绍。

    5.内存模型

    宏观上分布式系统需要解决的首要问题是数据一致性,同样,微观上并发编程要解决的首要问题也是数据一致性。貌似我们搞了这么多年的斗争都是在公关一致性这个世界性难题。既然并发编程要从微观开始,那么我们肯定要对CPU和内存的工作机理有所了解,尤其是数据在CPU和内存直接的传输机制。

    5.1整体原则

    探究内存模型之前我们要抛出三个概念:

    • 原子性

      在32位的系统中,对于4个字节32位的Integer的操作对应的JVM指令集映射到汇编指令为一个原子操作,所以对Integer类型的数据操作是原子性,但是Long类型为8个字节64位,32位系统要分为两条指令来操作,所以不是原子操作。

      对于32位操作系统来说,单次次操作能处理的最长长度为32bit,而long类型8字节64bit,所以对long的读写都要两条指令才能完成(即每次读写64bit中的32bit)

    • 可见性

      线程修改变量对其他线程即时可见

    • 有序性

      串行指令顺序唯一,并行线程直接指令可能出现不一致,也即是指令被重排了

      而指令重排也是有一定原则(摘自《深入理解Java虚拟机第12章》):

      • 程序次序规则:一个线程内,按照代码顺序,书写在前面的操作先行发生于书写在后面的操作;

      • 锁定规则:一个unLock操作先行发生于后面对同一个锁额lock操作;

      • volatile变量规则:对一个变量的写操作先行发生于后面对这个变量的读操作;

      • 传递规则:如果操作A先行发生于操作B,而操作B又先行发生于操作C,则可以得出操作A先行发生于操作C;

      • 线程启动规则:Thread对象的start()方法先行发生于此线程的每个一个动作;

      • 线程中断规则:对线程interrupt()方法的调用先行发生于被中断线程的代码检测到中断事件的发生;

      • 线程终结规则:线程中所有的操作都先行发生于线程的终止检测,我们可以通过Thread.join()方法结束、Thread.isAlive()的返回值手段检测到线程已经终止执行;

      • 对象终结规则:一个对象的初始化完成先行发生于他的finalize()方法的开始;

    5.2逻辑内存

    我们谈的逻辑内存也即是JVM的内存格局。JVM将操作系统提供的物理内存和CPU缓存在逻辑分为堆,栈,方法区,和程序计数器。在《从宏观微观角度浅析JVM虚拟机》 一文我们详细介绍了JVM的内存模型分布,并发编程我们主要关注的是堆栈的分配,因为线程都是寄生在栈里面的内存段,把栈里面的方法逻辑读取到CPU进行运算。

    5.3物理内存

    而实际的物理内存包含了主存和CPU的各级缓存还有寄存器,而为了计算效率,CPU往往回就近从缓存里面读取数据。在并发的情况下就会造成多个线程之间对共享数据的错误使用。

    5.4内存映射

    由于可能发生对象的变量同时出现在主存和CPU缓存中,就可能导致了如下问题:

    • 线程修改的变量对外可见

    • 读写共享变量时出现竞争资源

    由于线程内的变量对栈外是不可见的,但是成员变量等共享资源是竞争条件,所有线程可见,就会出现如下当一个线程从主存拿了一个变量1修改后变成2存放在CPU缓存,还没来得及同步回主存时,另外一个线程又直接从主存读取变量为1,这样就出现了脏读。

    现在我们弄清楚了线程同步过程数据不一致的原因,接下来要解决的目标就是如何避免这种情况的发生,经过大量的探索和实践,我们从概念上不断的革新比如并发模型的流水线化和无状态函数式化,而且也提供了大量的实用工具。接下来我们从无到有,先了解最简单的单个线程的一些特点,弄清楚一个线程有多少能耐后,才能深刻认识多个线程一起打交道会出现什么幺蛾子。

    6.线程单元

    6.1状态

    我们知道应用启动体现的就是静态指令加载进内存,进而进入CPU运算,操作系统在内存开辟了一段栈内存用来存放指令和变量值,从而形成了进程。而其实我们的JVM也就是一个进程而且,而线程是进程的最小单位,也就是说进程是由很多个线程组成的。而由于进程的上下文关联的变量,引用,计数器等现场数据占用了打段的内存空间,所以频繁切换进程需要整理一大段内存空间来保存未执行完的进程现场,等下次轮到CPU时间片再恢复现场进行运算。这样既耗费时间又浪费空间,所以我们才要研究多线程。毕竟由于线程干的活毕竟少,工作现场数据毕竟少,所以切换起来比较快而且暂用少量空间。而线程切换直接也需要遵守一定的法则,不然到时候把工作现场破坏了就无法恢复工作了。

    线程状态

    我们先来研究线程的生命周期,看看Thread类里面对线程状态的定义就知道

    public enum State {    /**
         * Thread state for a thread which has not yet started.
         */
        NEW,    /**
         * Thread state for a runnable thread.  A thread in the runnable
         * state is executing in the Java virtual machine but it may
         * be waiting for other resources from the operating system
         * such as processor.
         */
        RUNNABLE,    /**
         * Thread state for a thread blocked waiting for a monitor lock.
         * A thread in the blocked state is waiting for a monitor lock
         * to enter a synchronized block/method or
         * reenter a synchronized block/method after calling
         * {@link Object#wait() Object.wait}.
         */
        BLOCKED,    /**
         * Thread state for a waiting thread.
         * A thread is in the waiting state due to calling one of the
         * following methods:
         * <ul>
         *   <li>{@link Object#wait() Object.wait} with no timeout</li>
         *   <li>{@link #join() Thread.join} with no timeout</li>
         *   <li>{@link LockSupport#park() LockSupport.park}</li>
         * </ul>
         *
         * <p>A thread in the waiting state is waiting for another thread to
         * perform a particular action.
         *
         * For example, a thread that has called <tt>Object.wait()</tt>
         * on an object is waiting for another thread to call
         * <tt>Object.notify()</tt> or <tt>Object.notifyAll()</tt> on
         * that object. A thread that has called <tt>Thread.join()</tt>
         * is waiting for a specified thread to terminate.
         */
        WAITING,    /**
         * Thread state for a waiting thread with a specified waiting time.
         * A thread is in the timed waiting state due to calling one of
         * the following methods with a specified positive waiting time:
         * <ul>
         *   <li>{@link #sleep Thread.sleep}</li>
         *   <li>{@link Object#wait(long) Object.wait} with timeout</li>
         *   <li>{@link #join(long) Thread.join} with timeout</li>
         *   <li>{@link LockSupport#parkNanos LockSupport.parkNanos}</li>
         *   <li>{@link LockSupport#parkUntil LockSupport.parkUntil}</li>
         * </ul>
         */
        TIMED_WAITING,    /**
         * Thread state for a terminated thread.
         * The thread has completed execution.
         */
        TERMINATED;
    }

    生命周期

    线程的状态:NEW,RUNNABLE,BLOCKED,WAITING,TIMED_WAITING,TERMINATED。注释也解释得很清楚各个状态的作用,而各个状态的转换也有一定的规则需要遵循的。

    6.2动作

    介绍完线程的状态和生命周期,接下来我了解的线程具备哪些常用的操作。首先线程也是一个普通的对象Thread,所有的线程都是Thread或者其子类的对象。那么这个内存对象被创建出来后就会放在JVM的堆内存空间,当我们执行start()方法的时候,对象的方法体在栈空间分配好对应的栈帧来往执行引擎输送指令(也即是方法体翻译成JVM的指令集)。

    线程操作

    • 新建线程:new  Thread(),新建一个线程对象,内存为线程在栈上分配好内存空间

    • 启动线程:start(),告诉系统系统准备就绪,只要资源允许随时可以执行我栈里面的指令了

    • 执行线程:run(),分配了CPU等计算资源,正在执行栈里面的指令集

    • 停止线程(过时):stop(),把CPU和内存资源回收,线程消亡,由于太过粗暴,已经被标记为过时

    • 线程中断:

      • interrupt(),中断是对线程打上了中断标签,可供run()里面的方法体接收中断信号,至于线程要不要中断,全靠业务逻辑设计,而不是简单粗暴的把线程直接停掉

      • isInterrupt(),主要是run()方法体来判断当前线程是否被置为中断

      • interrupted(),静态方法,也是用户判断线程是否被置为中断状态,同时判断完将线程中断状态复位

    • 线程休眠:sleep(),静态方法,线程休眠指定时间段,此间让出CPU资源给其他线程,但是线程依然持有对象锁,其他线程无法进入同步块,休眠完成后也未必立刻执行,需要等到资源允许才能执行

    • 线程等待(对象方法):wait(),是Object的方法,也即是对象的内置方法,在同步块中线程执行到该方法时,也即让出了该对象的锁,所以无法继续执行

    • 线程通知(对象方法):notify(),notifyAll(),此时该对象持有一个或者多个线程的wait,调用notify()随机的让一个线程恢复对象的锁,调用notifyAll()则让所有线程恢复对象锁

    • 线程挂起(过时):suspend(),线程挂起并没有释放资源,而是只能等到resume()才能继续执行

    • 线程恢复(过时):resume(),由于指令重排可能导致resume()先于suspend()执行,导致线程永远挂起,所以该方法被标为过时

    • 线程加入:join(),在一个线程调用另外一个线程的join()方法表明当前线程阻塞知道被调用线程执行结束再进行,也即是被调用线程织入进来

    • 线程让步:yield(),暂停当前线程进而执行别的线程,当前线程等待下一轮资源允许再进行,防止该线程一直霸占资源,而其他线程饿死

    • 线程等待:park(),基于线程对象的操作,较对象锁更为精准

    • 线程恢复:unpark(Thread thread),对应park()解锁,为不可重入锁

    线程分组

    为了管理线程,于是有了线程组的概念,业务上把类似的线程放在一个ThreadGroup里面统一管理。线程组表示一组线程,此外,线程组还可以包括其他线程组。线程组形成一个树,其中除了初始线程组以外的每个线程组都有一个父线程。线程被允许访问它自己的线程组信息,但不能访问线程组的父线程组或任何其他线程组的信息。

    守护线程

    通常情况下,线程运行到最后一条指令后则完成生命周期,结束线程,然后系统回收资源。或者单遇到异常或者return提前返回,但是如果我们想让线程常驻内存的话,比如一些监控类线程,需要24小时值班的,于是我们又创造了守护线程的概念。

    setDaemon()传入true则会把线程一直保持在内存里面,除非JVM宕机否则不会退出。

    线程优先级

    线程优先级其实只是对线程打的一个标志,但并不意味这高优先级的一定比低优先级的先执行,具体还要看操作系统的资源调度情况。通常线程优先级为5,边界为[1,10]。

     /**
      * The minimum priority that a thread can have.
      */
     public final static int MIN_PRIORITY = 1;/**
      * The default priority that is assigned to a thread.
      */
     public final static int NORM_PRIORITY = 5; /**
      * The maximum priority that a thread can have.
      */
     public final static int MAX_PRIORITY = 10;

    本节介绍了线程单元的转态切换和常用的一些操作方法。如果只是单线程的话,其他都没必要研究这些,重头戏在于多线程直接的竞争配合操作,下一节则重点介绍多个线程的交互需要关注哪些问题。

    7.线程交互

    其实上一节介绍的线程状态切换和线程操作都是为线程交互做准备的。不然如果只是单线程完全没必要搞什么通知,恢复,让步之类的操作了。

    7.1交互方式

    线程交互也就是线程直接的通信,最直接的办法就是线程直接直接通信传值,而间接方式则是通过共享变量来达到彼此的交互。

    • 等待:释放对象锁,允许其他线程进入同步块

    • 通知:重新获取对象锁,继续执行

    • 中断:状态交互,通知其他线程进入中断

    • 织入:合并线程,多个线程合并为一个

    7.2线程安全

    我们最关注的还是通过共享变量来达到交互的方式。线程如果都各自干活互不搭理的话自然相安无事,但多数情况下线程直接需要打交道,而且需要分享共享资源,那么这个时候最核心的就是线程安全了。

    什么是线程安全?

    当多个线程访问同一个对象时,如果不用考虑这些线程在运行时环境下的调度和交替运行,也不需要进行额外的同步,或者在调用方进行任何其他的协调操作,调用这个对象的行为都可以获取正确的结果,那这个对象是线程安全的。(摘自《深入Java虚拟机》)

    如何保证线程安全?

    我们最早接触线程安全可能是JDK提供的一些号称线程安全的容器,比如Vetor较ArrayList是线程安全,HashTable较HashMap是线程安全?其实线程安全类并不代表也不等同线程安全的程序,而线程不安全的类同样可以完成线程安全的程序。我们关注的也就是写出线程安全的程序,那么如何写出线程安全的代码呢?下面列举了线程安全的主要设计技术:

    无状态

    这个有点函数式编程的味道,下文并发模式会介绍到,总之就是线程只有入参和局部变量,如果变量是引用的话,确保变量的创建和调用生命周期都发生在线程栈内,就可以确保线程安全。

    无共享状态

    完全要求线程无状态比较难实现,必要的状态是无法避免的,那么我们就必须维护不同线程之间的不同状态,这可是个麻烦事。幸好我们有ThreadLocal这个神器,该对象跟当前线程绑定,而且只对当前线程可见,完美解决了无共享状态的问题。

    不可变状态

    最后实在没办法避免状态共享,在线程之间共享状态,最怕的就是无法确保能维护好正确的读写顺序,而且多线程确实也无法正确维护好这个共享变量。那么我们索性粗暴点,把共享的状态定位不可变,比如价格final修饰一下,这样就达到安全状态共享。

    消息传递

    一个线程通常也不是所有步骤都需要共享状态,而是部分环节才需要的,那么我们把共享状态的代码拆开,无共享状态的那部分自然不用关心,而共享状态的小段代码,则通过加入消息组件来传递状态。这个设计到并发模式的流水线编程模式,下文并发模式会重点介绍。

    线程安全容器

    JUC里面提供大量的并发容器,涉及到线程交互的时候,使用安全容器可以避免大部分的错误,而且大大降低了代码的复杂度。

    • 通过synchronized给方法加上内置锁来实现线程安全的类如Vector,HashTable,StringBuffer

    • AtomicXXX如AtomicInteger

    • ConcurrentXXX如ConcurrentHashMap

    • BlockingQueue/BlockingDeque

    • CopyOnWriteArrayList/CopyOnWriteArraySet

    • ThreadPoolExecutor

    synchronized同步

    该关键字确保代码块同一时间只被一个线程执行,在这个前提下再设计符合线程安全的逻辑

    其作用域为

    • 对象:对象加锁,进入同步代码块之前获取对象锁

    • 实例方法:对象加锁,执行实例方法前获取对象实例锁

    • 类方法:类加锁,执行类方法前获取类锁

    volatile约束

    volatile确保每次操作都能强制同步CPU缓存和主存直接的变量。而且在编译期间能阻止指令重排。读写并发情况下volatile也不能确保线程安全,上文解析内存模型的时候有提到过。

    这节我们论述了编写线程安全程序的指导思想,其中我们提到了JDK提供的JUC工具包,下一节将重点介绍并发编程常用的趁手工具。

    8.线程工具

    前文我们介绍了内存理论和线程的一些特征,大家都知道并发编程容易出错,而且出了错还不好调试排查,幸好JDK里面集成了大量实用的API工具,我们能熟悉这些工具,写起并发程序来也事半功倍。

    工具篇其实就是对锁的不断变种,适应更多的开发场景,提高性能,提供更方便的工具,从最粗暴的同步修饰符,到灵活的可重入锁,到宽松的条件,接着到允许多个线程访问的信号量,最后到读写分离锁。

    8.1同步控制

    由于大多数的并发场景都是需要访问到共享资源的,为了保证线程安全,我们不得已采用锁的技术来做同步控制,这节我们介绍的是适用不同场景各种锁技术。

    ReentrantLock

    可重入互斥锁具有与使用synchronized的隐式监视器锁具有相同的行为和语义,但具有更好扩展功能。
    ReentrantLock由最后成功锁定的线程拥有,而且还未解锁。当锁未被其他线程占有时,线程调用lock()将返回并且成功获取锁。如果当前线程已拥有锁,则该方法将立即返回。这可以使用方法isHeldByCurrentThread()和getHoldCount()来检查。

    构造函数接受可选的fairness参数。当设置为true时,在竞争条件下,锁定有利于赋予等待时间最长线程的访问权限。否则,锁将不保证特定的访问顺序。在多线程访问的情况,使用公平锁比默认设置,有着更低的吞吐量,但是获得锁的时间比较小而且可以避免等待锁导致的饥饿。但是,锁的公平性并不能保证线程调度的公平性。因此,使用公平锁的许多线程中的一个可以连续多次获得它,而其他活动线程没有进展并且当前没有持有锁。不定时的tryLock()方法不遵循公平性设置。即使其他线程正在等待,如果锁可用,它也会成功。

    • 任意指定锁的起始位置

    • 中断响应

    • 锁申请等待限时tryLock()

    • 公平锁

    Condition

    Condition从拥有监控方法(wait,notify,notifyAll)的Object对象中抽离出来成为独特的对象,高效的让每个对象拥有更多的等待线程。和锁对比起来,如果说用Lock代替synchronized,那么Condition就是用来代替Object本身的监控方法。

    Condition实例跟Object本身的监控相似,同样提供wait()方法让调用的线程暂时挂起让出资源,知道其他线程通知该对象转态变化,才可能继续执行。Condition实例来源于Lock实例,通过Lock调用newCondition()即可。Condition较Object原生监控方法,可以保证通知顺序。

    Semaphore

    锁和同步块同时只能允许单个线程访问共享资源,这个明显有些单调,部分场景其实可以允许多个线程访问,这个时候信号量实例就派上用场了。信号量逻辑上维持了一组许可证, 线程调用acquire()阻塞直到许可证可用后才能执行。 执行release()意味着释放许可证,实际上信号量并没有真正的许可证,只是采用了计数功能来实现这个功能。

    ReadWriteLock

    顾名思义读写锁将读写分离,细化了锁的粒度,照顾到性能的优化。

    CountDownLatch

    这个锁有点“关门放狗”的意思,尤其在我们压测的时候模拟实时并行请求,该实例将线程积累到指定数量后,调用countDown()方法让所有线程同时执行。

    CyclicBarrier

    CyclicBarrier是加强版的CountDownLatch,上面讲的是一次性“关门放狗”,而循环栅栏则是集齐了指定数量的线程,在资源都允许的情况下同时执行,然后下一批同样的操作,周而复始。

    LockSupport

    LockSupport是用来创建锁和其他同步类的基本线程阻塞原语。 LockSupport中的park() 和 unpark() 的作用分别是阻塞线程和解除阻塞线程,而且park()和unpark()不会遇到“Thread.suspend 和 Thread.resume所可能引发的死锁”问题。因为park() 和 unpark()有许可的存在;调用 park() 的线程和另一个试图将其 unpark() 的线程之间的竞争将保持活性。

    8.2线程池

    线程池总览

    线程多起来的话就需要管理,不然就会乱成一锅。我们知道线程在物理上对应的就是栈里面的一段内存,存放着局部变量的空间和待执行指令集。如果每次执行都要从头初始化这段内存,然后再交给CPU执行,效率就有点低了。假如我们知道该段栈内存会被经常用到,那我们就不要回收,创建完就让它在栈里面呆着,要用的时候取出来,用完换回去,是不是就省了初始化线程空间的时间,这样是我们搞出线程池的初衷。

    其实线程池很简单,就是搞了个池子放了一堆线程。既然我们搞线程池是为了提高效率,那就要考虑线程池放多少个线程比较合适,太多了或者太少了有什么问题,怎么拒绝多余的请求,除了异常怎么处理。首先我们来看跟线程池有关的一张类图。

     

     

    线程池归结起来就是这几个类的使用技巧了,重点关注ThreadPoolExecutor和Executors即可。

    创建线程池

    万变不离其宗,创建线程池的各种马甲方法最后都是调用到这方法里面,包含核心线程数,最大线程数,线程工厂,拒绝策略等参数。其中线程工厂则可以实现自定义创建线程的逻辑。

    public interface ThreadFactory {    Thread newThread(Runnable r);
    }

    创建的核心构造方法ThreadPoolExecutor.java  1301

     /**
         * Creates a new {@code ThreadPoolExecutor} with the given initial
         * parameters.
         *
         * @param corePoolSize the number of threads to keep in the pool, even
         *        if they are idle, unless {@code allowCoreThreadTimeOut} is set
         * @param maximumPoolSize the maximum number of threads to allow in the
         *        pool
         * @param keepAliveTime when the number of threads is greater than
         *        the core, this is the maximum time that excess idle threads
         *        will wait for new tasks before terminating.
         * @param unit the time unit for the {@code keepAliveTime} argument
         * @param workQueue the queue to use for holding tasks before they are
         *        executed.  This queue will hold only the {@code Runnable}
         *        tasks submitted by the {@code execute} method.
         * @param threadFactory the factory to use when the executor
         *        creates a new thread
         * @param handler the handler to use when execution is blocked
         *        because the thread bounds and queue capacities are reached
         * @throws IllegalArgumentException if one of the following holds:<br>
         *         {@code corePoolSize < 0}<br>
         *         {@code keepAliveTime < 0}<br>
         *         {@code maximumPoolSize <= 0}<br>
         *         {@code maximumPoolSize < corePoolSize}
         * @throws NullPointerException if {@code workQueue}
         *         or {@code threadFactory} or {@code handler} is null
         */
        public ThreadPoolExecutor(int corePoolSize,                              int maximumPoolSize,                              long keepAliveTime,
                                  TimeUnit unit,
                                  BlockingQueue<Runnable> workQueue,
                                  ThreadFactory threadFactory,
                                  RejectedExecutionHandler handler)

    拒绝策略包含:

        /** 实际上并未真正丢弃任务,但是线程池性能会下降
         * A handler for rejected tasks that runs the rejected task
         * directly in the calling thread of the {@code execute} method,
         * unless the executor has been shut down, in which case the task
         * is discarded.
         */
        public static class CallerRunsPolicy implements RejectedExecutionHandler 
    
        /** 粗暴停止抛异常
         * A handler for rejected tasks that throws a
         * {@code RejectedExecutionException}.
         */    public static class AbortPolicy implements RejectedExecutionHandler 
    
        /** 悄无声息的丢弃拒绝的任务
         * A handler for rejected tasks that silently discards the
         * rejected task.
         */    public static class DiscardPolicy implements RejectedExecutionHandler 
    
    
        /** 丢弃最老的请求
         * A handler for rejected tasks that discards the oldest unhandled
         * request and then retries {@code execute}, unless the executor
         * is shut down, in which case the task is discarded.
         */    public static class DiscardOldestPolicy implements RejectedExecutionHandler

    包括Executors.java中的创建线程池的方法,具体实现也是通过ThreadPoolExecutor来创建的。

    public static ExecutorService newCachedThreadPool() {
        return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
                                      60L, TimeUnit.SECONDS,
                                      new SynchronousQueue<Runnable>());
    }
    
    public static ExecutorService newFixedThreadPool(int nThreads) {
        return new ThreadPoolExecutor(nThreads, nThreads,
                                          0L, TimeUnit.MILLISECONDS,
                                          new LinkedBlockingQueue<Runnable>());
    }
    
    public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize) {
        return new ScheduledThreadPoolExecutor(corePoolSize);
    }
    
    public static ExecutorService newSingleThreadExecutor() {
        return new FinalizableDelegatedExecutorService
                (new ThreadPoolExecutor(1, 1,
                                        0L, TimeUnit.MILLISECONDS,
                                        new LinkedBlockingQueue<Runnable>()));
    }

    调用线程池

    ThreadPoolExecutor.java 1342

    /** 同步执行线程,出现异常打印堆栈信息
     * Executes the given task sometime in the future.  The task
     * may execute in a new thread or in an existing pooled thread.
     *
     * If the task cannot be submitted for execution, either because this
     * executor has been shutdown or because its capacity has been reached,
     * the task is handled by the current {@code RejectedExecutionHandler}.
     *
     * @param command the task to execute
     * @throws RejectedExecutionException at discretion of
     *         {@code RejectedExecutionHandler}, if the task
     *         cannot be accepted for execution
     * @throws NullPointerException if {@code command} is null
     */public void execute(Runnable command)/**
    * 异步提交线程任务,出现异常无法同步追踪堆栈,本质上也是调用execute()方法
    */public <T> Future<T> submit(Runnable task, T result) {    if (task == null) throw new NullPointerException();
        RunnableFuture<T> ftask = newTaskFor(task, result);
        execute(ftask);    return ftask;
    }

    线程池优化

    线程池已经是我们使用线程的一个优化成果了,而线程池本身的优化其实就是根据实际业务选择好不同类型的线程池,预估并发线程数量,控制好线程池预留线程数(最大线程数一般设为2N+1最好,N是CPU核数),这些涉及CPU数量,核数还有具体业务。

    另外我们还注意到ForkJoinPool继承了AbstractExecutorService,这是在JDK7才加上去的,目的就是提高任务派生出来更多任务的执行效率,由上图的继承关系我们可以知道跟普通线程池最大的差异是执行的任务类型不同。

    public void execute(ForkJoinTask<?> task) {    if (task == null)        throw new NullPointerException();
        externalPush(task);
    }public void execute(Runnable task) {        if (task == null)            throw new NullPointerException();
            ForkJoinTask<?> job;        if (task instanceof ForkJoinTask<?>) // avoid re-wrap
                job = (ForkJoinTask<?>) task;        else
                job = new ForkJoinTask.RunnableExecuteAction(task);
            externalPush(job);
     }

    8.3并发容器

    其实我们日常开发大多数并发场景直接用JDK 提供的线程安全数据结构足矣,下面列举了常用的列表,集合等容器,具体就不展开讲,相信大家都用得很熟悉了。

    • ConcurrentHashMap

    • CopyOnWriteArrayList

    • ConcurrentLinkedQueue

    • BlockingQueue

    • ConcurrentSkipListMap

    • Vector

    • HashTable

    9.线程调优

    9.1性能指标

    回想一下,当我们在谈性能优化的时候,我们可能指的是数据库的读写次数,也可能指网站的响应时间。通常我们会用QPS,TPS,RT,并发数,吞吐量,更进一步的还会对比CPU负载来衡量一个系统的性能。

    当然我们知道一个系统的吞吐量和响应时间跟外部网络,分布式架构等都存在强关联,性能优化也跟各级缓存设计,数据冗余等架构有很大关系,假设其他方面我们都已经完成了,聚焦到本文我们暂时关心的是单节点的性能优化。毕竟一屋不扫何以扫天下,整体系统的优化也有赖于各个节点的调优。从感官上来谈,当请求量很少的时候,我们可以很轻松的通过各种缓存优化来提高响应时间。但是随着用户激增,请求次数的增加,我们的服务也对应着需要并发模型来支撑。但是一个节点的并发量有个上限,当达到这个上限后,响应时间就会变长,所以我们需要探索并发到什么程度才是最优的,才能保证最高的并发数,同时响应时间又能保持在理想情况。由于我们暂时不关注节点以外的网络情况,那么下文我们特指的RT是指服务接收到请求后,完成计算,返回计算结果经历的时间。

    单线程

    单线程情况下,服务接收到请求后开始初始化,资源准备,计算,返回结果,时间主要花在CPU计算和CPU外的IO等待时间,多个请求来也只能排队一个一个来,那么RT计算如下

    RT = T(cpu) + T(io)

    QPS = 1000ms / RT

    多线程

    单线程情况很好计算,多线程情况就复杂了,我们目标是计算出最佳并发量,也就是线程数N

    单核情况:N = [T(cpu) + T(io)] / T(cpu)

    M核情况:N = [T(cpu) + T(io)] / T(cpu) * M

    由于多核情况CPU未必能全部使用,存在一个资源利用百分比P

    那么并发的最佳线程数 N = [T(cpu) + T(io)] / T(cpu) M P

    吞吐量

    我们知道单线程的QPS很容易算出来,那么多线程的QPS

    QPS = 1000ms / RT N = 1000ms / T(cpu) + T(io) [T(cpu) + T(io)] / T(cpu) M P= 1000ms / T(cpu) M P

    在机器核数固定情况下,也即是并发模式下最大的吞吐量跟服务的CPU处理时间和CPU利用率有关。CPU利用率不高,就是通常我们听到最多的抱怨,压测时候qps都打满了,但是cpu的load就是上不去。并发模型中多半个共享资源有关,而共享资源又跟锁息息相关,那么大部分时候我们想对节点服务做性能调优时就是对锁的优化,这个下一节会提到。

    前面我们是假设机器核数固定的情况下做优化的,那假如我们把缓存,IO,锁都优化了,剩下的还有啥空间去突破呢?回想一下我们谈基础理论的时候提到的Amdahl定律,公式之前已经给出,该定律想表达的结论是随着核数或者处理器个数的增加,可以增加优化加速比,但是会达到上限,而且增加趋势愈发不明显。

    9.2锁优化

    说真的,我们并不喜欢锁的,只不过由于临界资源的存在不得已为之。如果业务上设计能避免出现临界资源,那就没有锁优化什么事了。但是,锁优化的一些原则还是要说一说的。

    时间

    既然我们并不喜欢锁,那么就按需索取,只在核心的同步块加锁,用完立马释放,减少锁定临界区的时间,这样就可以把资源竞争的风险降到最低。

    粒度

    进一步看,有时候我们核心同步块可以进一步分离,比如只读的情况下并不需要加锁,这时候就可以用读写锁各自的读写功能。

    还有一种情况,有时候我们反而会小心翼翼的到处加锁来防止意外出现,可能出现三个同步块加了三个锁,这也造成CPU的过多停顿,根据业务其实可以把相关逻辑合并起来,也就是锁粗化。

    锁的分离和粗化具体还得看业务如何操作。

    尺度

    除了锁暂用时间和粒度外,还有就是锁的尺度,还是根据业务来,能用共享锁定的情况就不要用独享锁。

    死锁

    这个不用说都知道,死锁防不胜防,我们前面也介绍很多现成的工具,比如可重入锁,还有线程本地变量等方式,都可以一定程度避免死锁。

    9.3JVM锁机制

    我们在代码层面把锁的应用都按照安全法则做到最好了,那接下来要做的就是下钻到JVM级别的锁优化。具体实现原理我们暂不展开,后续有机会再搞个专题写写JVM锁实现。

    自旋锁(Spin Lock)

    自旋锁的原理非常简单。如果持有锁的线程可以在短时间内释放锁资源,那么等待竞争锁的那些线程不需要在内核状态和用户状态之间进行切换。 它只需要等待,并且锁可以在释放锁之后立即获得锁。这可以避免消耗用户线程和内核切换。

    但是,自旋锁让CPU空等着什么也不干也是一种浪费。 如果自旋锁的对象一直无法获得临界资源,则线程也无法在没有执行实际计算的情况下一致进行CPU空转,因此需要设置自旋锁的最大等待时间。如果持有锁的线程在旋转等待的最大时间没有释放锁,则自旋锁线程将停止旋转进入阻塞状态。

    JDK1.6开启自旋锁  -XX:+UseSpinning,1.7之后控制器收回到JVM自主控制

    偏向锁(Biased Lock)

    偏向锁偏向于第一个访问锁的线程,如果在运行过程中,同步锁只有一个线程访问,不存在多线程争用的情况,则线程是不需要触发同步的,这种情况下,就会给线程加一个偏向锁。如果在运行过程中,遇到了其他线程抢占锁,则持有偏向锁的线程会被挂起,JVM会消除它身上的偏向锁,将锁恢复到标准的轻量级锁。

    JDK1.6开启自旋锁  -XX:+UseBiasedLocking,1.7之后控制器收回到JVM自主控制

    轻量级锁(Lightweight Lock)

    轻量级锁是由偏向锁升级来的,偏向锁运行在一个线程进入同步块的情况下,当第二个线程加入锁竞争的时候,偏向锁就会升级为轻量级锁。

    重量级锁(Heavyweight Lock)

    如果锁检测到与另一个线程的争用,则锁定会膨胀至重量级锁。也就是我们常规用的同步修饰产生的同步作用。

    9.4无锁

    最后其实我想说的是,虽然锁很符合我们人类的逻辑思维,设计起来也相对简单,但是摆脱不了临界区的限制。那么我们不妨换个思路,进入无锁的时间,也就是我们可能会增加业务复杂度的情况下,来消除锁的存在。

    CAS策略

    著名的CAS(Compare And Swap),是多线程中用于实现同步的原子指令。 它将内存位置的内容与给定值进行比较,并且只有它们相同时,才将该内存位置的内容修改为新的给定值。 这是作为单个原子操作完成的。 原子性保证了新值是根据最新信息计算出来的; 如果在此期间该值已被另一个线程更新,则写入将失败。 操作的结果必须表明它是否进行了替换; 这可以通过简单的Boolean来响应,或通过返回从内存位置读取的值(而不是写入它的值)来完成。

    也就是一个原子操作包含了要操作的数据和给定认为正确的值进行对比,一致的话就继续,不一致则会重试。这样就在没有锁的情况下完成并发操作。

    我们知道原子类 AtomicInteger内部实现的原理就是采用了CAS策略来完成的。

    AtomicInteger.java  132

    /**
     * Atomically sets the value to the given updated value
     * if the current value {@code ==} the expected value.
     *
     * @param expect the expected value
     * @param update the new value
     * @return {@code true} if successful. False return indicates that
     * the actual value was not equal to the expected value.
     */public final boolean compareAndSet(int expect, int update) {    return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
    }

    类似的还有AtomicReference.java  115

    /**
     * Atomically sets the value to the given updated value
     * if the current value {@code ==} the expected value.
     * @param expect the expected value
     * @param update the new value
     * @return {@code true} if successful. False return indicates that
     * the actual value was not equal to the expected value.
     */public final boolean compareAndSet(V expect, V update) {    return unsafe.compareAndSwapObject(this, valueOffset, expect, update);
    }

    有兴趣的同学可以再了解一下Unsafe的实现,进一步可以了解Distuptor无锁框架。

    10.并发模型

    前面我们大费周章的从并发的基础概念到多线程的使用方法和优化技巧。但都是战术层面的,本节我们试着从战略的高度来扩展一下并发编程的世界。可能大多数情况下我们谈并发都会想到多线程,但是本节我们要打破这种思维,在完全不用搞多线程那一套的情况下实现并发。

    首先我们用”多线程模式“来回顾前文所讲的所有关于Thread衍生出来的定义,开发和优化的技术。

    多线程模式

    单位线程完成完整的任务,也即是一条龙服务线程。

    • 优势:

      • 映射现实单一任务,便于理解和编码

    • 劣势:

     

    • 有状态多线程共享资源,导致资源竞争,死锁问题,线程等待阻塞,失去并发意义

    • 有状态多线程非阻塞算法,有利减少竞争,提升性能,但难以实现

    • 多线程执行顺序无法预知

    流水线模型

    介绍完传统多线程工作模式后,我们来学习另外一种并发模式,传统的多线程工作模式,理解起来很直观,接下来我们要介绍的另外一种并发模式看起来就不那么直观了。

    流水线模型,特点是无状态线程,无状态也意味着无需竞争共享资源,无需等待,也就是非阻塞模型。流水线模型顾名思义就是流水线上有多个环节,每个环节完成自己的工作后就交给下一个环节,无需等待上游,周而复始的完成自己岗位上的一亩三分地就行。各个环节之间交付无需等待,完成即可交付。

     而工厂的流水线也不止一条,所以有多条流水线同时工作。

    不同岗位的生产效率是不一样的,所以不同流水线之间也可以发生协同。

    我们说流水线模型也称为响应式模型或者事件驱动模型,其实就是流水线上上游岗位完成生产就通知下游岗位,所以完成了一个事件的通知,每完成一次就通知一下,就是响应式的意思。

    流水线模型总体的思想就是纵向切分任务,把任务里面耗时过久的环节单独隔离出来,避免完成一个任务需要耗费等待的时间。在实现上又分为Actors和Channels模型

    • Actors

       

      该模型跟我们讲述的流水线模型基本一致,可以理解为响应式模型

     

     

     

    由于各个环节直接不直接交互,所以上下游之间并不知道对方是谁,好比不同环节直接用的是几条公共的传送带来接收物品,各自只需要把完成后的半成品扔到传送带,即使后面流水线优化了,去掉中间的环节,对于个体岗位来说也是无感知的,它只是周而复始的从传送带拿物品来加工。

     

    流水线的优缺点:

    优势:

    • 无共享状态:无需考虑资源抢占,死锁等问题

    • 独享内存:worker可以持有内存,合并多次操作到内存后再持久化,提升效率

    • 贴合底层:单线程模式贴合硬件运行流程,便于代码维护

    • 任务顺序可预知

    劣势:

    • 不够直观:一个任务被拆分为流水线上多个环节,代码层面难以直观理解业务逻辑

    由于流水线模式跟人类的顺序执行思维不一样,比较费解,那么有没有办法让我们编码的时候像写传统的多线程代码一样,而运行起来又是流水线模式呢?答案是肯定的,比如基于Java的Akka/Reator/Vert.x/Play/Qbit框架,或者golang就是为流水线模式而生的并发语言,还有nodeJS等等。

    流水线模型的开发实践可以参考流水线模型实践。

    其实流水线模型背后用的也还是多线程来实现,只不过对于传统多线程模式下我们需要小心翼翼来处理跟踪资源共享问题,而流水线模式把以前一个线程做的事情拆成多个,每一个环节再用一条线程来完成,避免共享,线程直接通过管道传输消息。

    这一块展开也是一个专题,主要设计NIO,Netty和Akka的编程实践,先占坑后面补上。

    函数式模型

    函数式并行模型类似流水线模型,单一的函数是无状态的,所以避免了资源竞争的复杂度,同时每个函数类似流水线里面的单一环境,彼此直接通过函数调用传递参数副本,函数之外的数据不会被修改。函数式模式跟流水线模式相辅相成逐渐成为更为主流的并发架构。具体的思想和编程实践也是个大专题,篇幅限制本文就先不展开,拟在下个专题中详细介绍《函数式编程演化》。

    11.总结

    由于CPU和I/O天然存在的矛盾,传统顺序的同步工作模式导致任务阻塞,CPU空等着没有执行,浪费资源。多线程为突破了同步工作模式的情况下浪费CPU资源,即使单核情况下也能将时间片拆分成单位给更多的线程来轮询享用。多线程在不同享状态的情况下非常高效,不管协同式还是抢占式都能在单位时间内执行更多的任务,从而更好的榨取CPU资源。

    但是多数情况下线程之间是需要通信的,这一核心场景导致了一系列的问题,也就是线程安全。内存被共享的单位由于被不同线程轮番读取写入操作,这种操作带来的后果往往是写代码的人类没想到的,也就是并发带来的脏数据等问题。解决了资源使用效率问题,又带来了新的安全问题,如何解决?悲观方式就是对于存在共享内存的场景,无论如何只同意同一时刻一个线程操作,也就是同步操作方法或者代码段或者显示加锁。或者volatile来使共享的主存跟每条线程的工作内存同步(每次读都从主存刷新,每次写完都刷到主存)

    要保证线程安全:

    • 1、不要使用多线程,

    • 2、多线程各干各的不要共享内存,

    • 3、共享的内存空间是不可变的(常量,final),

    • 4、实在要变每次变完要同步到主存volatile(依赖当前值的逻辑除外),

    • 5、原子变量,

    • 6、根据具体业务,避免脏数据(这块就是多线程最容易犯错的地方)

    线程安全后,要考虑的就是效率问题,如果不解决效率问题,那还干嘛要多线程。。。
    如果所有线程都很自觉,快速执行完就跑路,那就是我们的理想情况了。但是,部分线程又臭又长(I/O阻塞),不能让一直赖在CPU不走,就把他上下文(线程号,变量,执行到哪等数值的快照)保存到内存,然后让它滚蛋下一个线程来。但是切换太快的话也不合适,毕竟每次保存线程的作案现场也要花不少时间的,单位时间执行线程数要控制在一个适当的个数。创建线程也是一项很吃力的工作,一个线程就是在栈内存里面开辟一段内存空间,根据字节码分配临时变量空间,不同操作系统通常不一样。不能频繁的创建销毁线程。那就搞个线程池出来,用的时候拿出来,用完扔回去,简单省事。但是线程池的创建也有门道,不能无限创建不然就失去意义了。操作系统有一定上限,线程池太多线程内存爆了,系统奔溃,所以需要一个机制。容纳1024个线程,多了排队再多了扔掉。回到线程切换,由于创建线程耗费资源,切换也花费,有时候切换线程的时间甚至比让线程待在cpu无所事事更长,那就给加个自旋锁,就是让它自己再cpu打滚啥事不干,一会儿轮到它里面就能干活。

    既然多线程同步又得加锁耗资源,不同步又有共享安全问题。那能不能把这些锁,共享,同步,要注意的问题封装起来。搞出一个异步的工作机制,不用管底层的同步问题,只管业务问题。传统是工匠干活一根筋干完,事件驱动是流水线,把一件事拆分成多个环节,每个环节有唯一标识,各个环节批量生产,在流水线对接。这样在CPU单独干,不共享,不阻塞,干完自己的通知管工,高效封装了内部线程的运行规则,把业务关系暴露给管理者。

    本文主要将的数基于JAVA的传统多线程并发模型,下面例牌给出知识体系图。

     

    展开全文
  • 聊聊并发:(一)并发编程概述

    千次阅读 2018-07-28 12:33:13
    编写正确的程序很难,而编写正确的并发程序则难上加难。与串行程序相比,在并发程序中存在更多容易出错的地方。那么,为何我们还要使用并发程序?线程是Java语言中不可或缺的重要功能,它们能使复杂的异步代码变得...

    前言

    编写正确的程序很难,而编写正确的并发程序则难上加难。与串行程序相比,在并发程序中存在更多容易出错的地方。那么,为何我们还要使用并发程序?线程是Java语言中不可或缺的重要功能,它们能使复杂的异步代码变得简单,从而极大地简化了复杂系统的开发。此外,想要充分发挥多处理器系统的强大计算能力,最简单的方式就是使用线程。随着处理器数量的持续增长,如何高效地使用蝙蝠正变得越来越重要。同时在当今互联网的时代,大量的互联网应用都面对着海量的访问请求,因此,并发编程在我们的应用中成为越来越不可或缺的一部分。

    并发简史

    在早期的计算机中不包含操作系统,它们从头到尾只可以运行一个程序,操作系统的出现使得计算机每次能运行多个程序,并且不同的程序都在单独的进程中运行,之所以在计算机中加入操作系统来实现多个程序的同时执行,主要是基于下面几个原因:

    • 资源利用率

      在某些情况下,程序必须等待某个外部操作执行完成,而等待时程序无法执行其他任何工作。因此,如果在等待同时可以运行另一个程序,那么无疑将提高资源的利用率。
    • 公平性

      不同的用户和程序对于计算机上的资源有着同等的使用权。一种高效的运行方式是通过时间分片使用户和程序可以共享计算机资源,而不是一个程序从头运行到底,再启动下一个程序。
    • 便利性

      通常来说,在计算多个任务时,应该编写多个程序,每个程序执行一个任务并在必要时相互通信,这比只编写一个程序来计算所有任务更容易实现。

    线程优势

    如果使用得当,线程可以有效降低程序的开发和维护成本,同时提升复杂应用程序的性能。在服务器应用程序中,可以提升资源利用率以及系统吞吐率,线程还可以简化JVM的实现,垃圾收集器通常在一个或多个专门的线程中运行,在许多重要的Java应用中,都在一定程度生用到了线程。

    发挥多处理器的强大能力

    随着现在多处理器的普及,我们的服务器目前多数都是多个核心的,由于CPU的基本调度单位是线程,因此如果在程序中只有一个线程,那么最多同时只能在一个处理器上运行。在双处理器系统上,单线程的程序只能使用一半的CPU资源,而在拥有100个处理器的系统上,将有99%的资源无法使用。另一方面,多线程程序可同时在多个处理器上执行。如果设计正确,多线程程序可以通过提高处理器资源的利用率来提升系统吞吐率。

    异步事件的简化处理

    服务器应用程序在接受来自多个远程客户端的连接请求时,如果为每个连接都分配各自的线程并且使用同步I/O,那么就会降低这类程序的开发难度。

    在单线程应用中,如果在处理某一请求过程中出现阻塞,意味着在这个线程被阻塞的期间,对所有请求的处理都将停顿。为了避免这个问题,单线程服务器应用程序必须使用非阻塞I/O,这种I/O的复杂性要远远高于同步I/O,并且很容易出错,如果每个请求都拥有自己的处理线程,那么在处理某个请求时发生的阻塞将不会影响其他请求的处理。

    目前主流的Web容器,例如Tomcat,是支持多线程异步非阻塞模型来响应请求的,这样可以获得更大的请求吞吐量。

    线程带来的风险

    Java对线程的支持其实是一把双刃剑。虽然Java提供了相应的语言和库,以及一种明确的跨平台内存模型,这些工具简化的了并发应用程序的开发,但同时也挺高了对开发人员的技术要求,因为在更多的程序中会使用线程。

    线程安全问题

    线程安全性可能是非常复杂的,在没有充分同步的情况下,多个线程中的操作执行顺序是不可预测的,甚至可能会出现奇怪的结果。下面的例子中,UnsafeSequence类中将产生一个整数值序列,该序列中的每个值都是唯一的。在这个类中简要的说明了多个线程之间的交替操作将如何导致不可预料的结果。在单线程环境中,这个类能正确地工作,但在多线程环境中则不可以。

    public class UnsafeSequence {
    
        private int value;
    
        public int getNext() {
            return value++;
        }
    
        public static void main(String[] args) {
            UnsafeSequence unsafeSequence = new UnsafeSequence();
            Executor executors = Executors.newFixedThreadPool(8);
            for (int i = 0; i < 200; i++) {
                executors.execute(()-> System.out.println(unsafeSequence.getNext()));
            }
        }
    }
    

    输出结果:

    3 0 4 1 5 0 9 10 11 2 15 13 12 14 8 21 6 7 24 23 22 20 19 29 30 18 17 16 33 32 31 28 27 26 25 41 42 43 44 45 46 47 40 39 38 37 52 36 35 55 34 57 58 56 60 54 53 62 64 65 51 67 50 69 49 71 72 73 48 75 76 74 70 79 68 81 66 83 63 85 86 61 88 89 59 91 92 90 94 87 96 97 84 99 100 82 102 80 78 77 105 104 103 101 98 95 93 112 111 115 110 109 118 108 107 121 106 122 120 119 117 116 114 128 113 129 130 127 126 134 125 124 123 139 138 137 136 135 144 133 132 146 131 149 148 147 145 153 143 142 141 140 158 157 156 155 154 163 152 151 166 167 150 168 170 171 165 164 174 175 176 162 161 179 180 181 182 160 184 185 159 187 186 189 190 183 192 193 194 178 196 197 198 177 173 172 169 195 191 188 
    
    上面结果中没有出现199
    

    UnsafeSequence的问题在于,如果执行时机不对,那么两个线程在调用getNext时会得到相同的值,虽然递增运算value++看上去是单个操作,但事实上它包含三个独立的操作:读取value,将value加1,并将计算结果写入value。由于运行时可能将多个线程之间的操作交替执行,因此这两个线程可能同时执行读操作,从而它们得到相同的值,并都将这个值加1,结果就是,在不同线程的调用中返回了相同的数值。

    在UnsafeSequence类中说明的是一种常见的并发安全问题,称为竞态条件。在多线程环境下,getValue是否会返回唯一的值,要取决于运行时对线程中操作的交替执行方式,这并不是我们希望看到的情况。

    由于多个线程要共享相同的内存地址空间,而且是并发运行,因此它们可能会访问或修改其他线程正在使用的变量。当然,这是一种极大的便利,因为这种方式比其他线程间通信机制更容易实现数据共享。但是它也带来了巨大的风险:线程会由于无法预料的数据变化而发生错误。当多个线程同时访问和修改相同的变量时,将会在串行编程模型中引入非串行因素,而这种非串行性是很难进行分析的。要使多线程程序的行为可以预测,必须对共享变量的访问操作进行协同,这样才不会在线程之间发生彼此干扰。幸运的是,Java提供了各种同步机制来协同这种访问。看下面的例子:

    public class SafeSequence {
    
        private int value;
    
        public synchronized int getNext() {
            return value++;
        }
    
        public static void main(String[] args) {
            SafeSequence safeSequence = new SafeSequence();
            Executor executors = Executors.newFixedThreadPool(8);
            for (int i = 0; i < 200; i++) {
                executors.execute(()-> System.out.println(safeSequence.getNext()));
            }
        }
    }
    

    活跃性问题

    多线程会导致一些在单线程程序中不会出现的问题,例如活跃性问题。当某个操作无法继续执行下去时,就会发生活跃性问题。在串行程序中,活跃性问题的形式之一就是无意中造成的无限循环,从而使循环之后的代码无法得到执行。还有一些其他类型的问题,例如:如果线程A在等待线程B释放其持有的资源,而线程B永远都不释放该资源,那么A就会永远的等待下去。这就是通常所说的“死锁”。

    性能问题

    在设计良好的并发应用程序中,线程能提升程序的性能,但无论如何,线程总会带来某种程度的运行时开销。再多线程程序中,当线程调度器临时挂起活跃线程并转而运行另一个线程时,就会频繁地出现上下文切换操作,这种操作将带来极大的开销:保存和恢复执行上下文,丢失局部性,并且CPU时间将更多地花在线程调度而不是线程运行上。当线程共享数据时,必须使用同步机制,而这些机制往往会抑制某些编译器优化,使内存缓存区中的数据无效,以及增加共享内存总线的同步流量。所有这些因素都将带来额外的性能开销。

    线程无处不在

    即使在程序中没有显示地创建线程,但在框架中仍可能会创建线程,因此在这些线程中调用的代码同样必须是线程安全的。
    每个Java应用程序都会使用线程。当JVM启动时,它将为JVM的内部任务,例如垃圾收集,终结操作等创建后台线程,并创建一个主线程来运行main方法。

    并发编程中的重要概念

    同步VS异步

    同步和异步通常用来形容一次方法调用。同步方法调用一开始,调用者必须等待被调用的方法结束后,调用者后面的代码才能执行。而异步调用,指的是,调用者不用管被调用方法是否完成,都会继续执行后面的代码,当被调用的方法完成后会通知调用者。比如,在超市购物,如果一件物品没了,你得等仓库人员跟你调货,直到仓库人员给你把货物送过来,你才能继续去收银台付款,这就类似同步调用。而异步调用了,就像网购,你在网上付款下单后,什么事就不用管了,该干嘛就干嘛去了,当货物到达后你收到通知去取就好。

    并发与并行

    并发和并行是十分容易混淆的概念。并发指的是多个任务交替进行,而并行则是指真正意义上的“同时进行”。实际上,如果系统内只有一个CPU,而使用多线程时,那么真实系统环境下不能并行,只能通过切换时间片的方式交替进行,而成为并发执行任务。真正的并行也只能出现在拥有多个CPU的系统中。

    阻塞和非阻塞

    阻塞和非阻塞通常用来形容多线程间的相互影响,比如一个线程占有了临界区资源,那么其他线程需要这个资源,就必须进行等待该资源的释放,会导致等待的线程挂起,这种情况就是阻塞,而非阻塞就恰好相反,它强调没有一个线程可以阻塞其他线程,所有的线程都会尝试地往前运行。

    临界区

    临界区用来表示一种公共资源或者说是共享数据,可以被多个线程使用。但是每个线程使用时,一旦临界区资源被一个线程占有,那么其他线程必须等待。

    展开全文
  • 并发编程与高并发解决方案(一):并发编程相关基础知识 【原文链接】www.ronglexie.top 目录 基本概念 CPU多级缓存 CPU多级缓存-缓存一致性协议(MESI) MESI协议中的状态 MESI状态转换图 CPU多级缓存-乱序...
  • 【Java并发编程并发编程大合集

    万次阅读 多人点赞 2013-12-27 08:38:09
    为了方便各位网友学习以及方便自己复习之用,将Java并发编程系列内容系列内容按照由浅入深的学习顺序总结如下,点击相应的标题即可跳转到对应的文章     【Java并发编程】实现多线程的两种方法   【Java并发编程...
  • 关于Java并发编程的总结和思考

    万次阅读 多人点赞 2015-03-27 10:36:10
    Java语言从第一版本开始内置了对多线程的支持,这一点在当年是非常了不起的,但是当我们对并发编程有了更深刻的认识和更多的实践后,实现并发编程就有了更多的方案和更好的选择。本文是对并发编程的一点总结和思考,...
  • 多线程与高并发编程之基础知识(上)

    万次阅读 多人点赞 2018-10-01 21:48:25
    前言: 使用多线程,第一步就是需要知道如何实现自定义线程,因为实际开发中,需要线程完成的任务是不同的,所以我们需要根据线程任务来自定义线程,那么,JDK提供了三种自定义线程的方式,供我们实际开发中使用,...
  • 并发编程

    2018-06-02 14:34:59
    1、基于进程的并发编程使用fork()、exec()、waitpid()函数。例如构造一个并发服务器的自然方法就是:在父进程中接受客户端的连接请求,然后创建一个新的子进程来为每个新客户端提供服务。需要注意的点:1、父...
  • 并发编程模式

    千次阅读 多人点赞 2019-07-31 20:12:05
    一、future模式 在网上购物时,提交订单后,在收货的这段时间里无需一直在家里等候,可以先干别的事情。类推到程序设计中时,当提交请求时,期望得到答复时,如果这个答复可能很慢。传统的是一直等待到这个答复收到...
  • 并发编程的优缺点

    千次阅读 多人点赞 2019-10-03 21:50:50
    Java并发编程是整个Java开发体系中最难以理解,但也是最重要的知识点之一,因此学习起来比较费劲,从而导致很多人望而却步,但是无论是职场面试还是高并发高流量的系统的实现都离不开并发编程,能够真正掌握并发编程...
  • JAVA高并发编程

    万次阅读 多人点赞 2018-05-07 09:30:26
    synchronized 关键字 同步方法 同步代码块 锁的底层实现 锁的种类 volatile 关键字 wait¬ify AtomicXxx 类型组 CountDownLatch 门闩 锁的重入 ReentrantLock 同步容器 Map/Set List Queue ......
  • 并发编程面试题(2020最新版)

    万次阅读 多人点赞 2020-03-14 17:28:01
    文章目录基础知识并发编程的优缺点为什么要使用并发编程并发编程的优点)并发编程有什么缺点并发编程三要素是什么?在 Java 程序中怎么保证多线程的运行安全?并行和并发有什么区别?什么是多线程,多线程的优劣?...
  • 当然这些都是并发编程的基本知识,除了使用这些工具以外,Java并发编程中涉及到的技术原理十分丰富。为了更好地把并发知识形成一个体系,也鉴于本人没有能力写出这类文章,于是参考几位并发编程专家的博客和书籍,做...
  • Java并发编程精讲

    万人学习 2019-09-28 15:16:34
    锁实现和并发容器等高并发Java实现,去深入理解在并发编程中, 一些最容易被忽视的点,这些点也是我在多年编程经验中实际用到, 对于每个小节小课均会有实际的场景应用为你呈现。 同时这些也是面试过程中面试官...
  • 并发编程2:认识并发编程的利与弊

    千次阅读 热门讨论 2017-03-01 00:31:26
    读完本文你将了解: 多线程的优点 1提高资源利用率 2响应更快 多线程的缺点 1增加资源消耗 2上下文切换的开销 3设计编码测试的复杂度...Thanks从上篇文章 趣谈并发(1):全面认识 Thread 我们了解了 Java 中线程的基本概
  • Java并发编程最佳实例详解系列

    千次阅读 2018-04-26 20:22:51
    Java并发编程最佳实例详解系列: Java并发编程(一)线程定义、状态和属性 Java并发编程(一)线程定义、状态和属性 线程是指程序在执行过程中,能够执行程序代码的一个执行单元。在java语言中,线程有四种状态...
  • │ 高并发编程第一阶段01讲、课程大纲及主要内容介绍.wmv │ 高并发编程第一阶段02讲、简单介绍什么是线程.wmv │ 高并发编程第一阶段03讲、创建并启动线程.mp4 │ 高并发编程第一阶段04讲、线程生命周期以及...
  • java并发编程——一并发基础

    千次阅读 2016-06-29 09:51:25
    为什么使用多线程 多任务的处理,提高处理速度,减少相应时间,更好的体验 随着cpu的核心数量越来越多,提供了充足的硬件基础,使用多线程重复发挥机器的计算能力,合理利用资源 上下文切换cpu通过给每个线程分配cpu...
1 2 3 4 5 ... 20
收藏数 368,240
精华内容 147,296
关键字:

并发编程