系统学习Java高并发

Java服务端或者后端需要大量的高并发计算,所以高并发在JAVA服务端或者后端编程中显的格外重要了。

首先需要有几个概念:

1. 同步和异步

同步异步是来形容方法的一次调用的,同步必须等等方法调用结束后才可以继续后续的操作,而异步方法调用就会返回(真正的执行一般在另外一个线程中)就可以继续后续操作了。

2. 并发和并行

这两个概念都是表示2个或者多个任务一起执行,而并发侧重的是多任务交替执行,就是一个时间点就只有一个任务(时间碎片很小),而并行是真正意义的同时执行(某个时间碎片有大于1个任务在执行)。

3. 临界区

临界区这个概念非常重要,就是多个线程都会操作到的,是一个公共资源或者共享的数据,但是每次操作只能一个线程使用而一旦临界区资源被占用其他的线程必须等待该资源的释放,在并行程序中,临界区资源都是受保护的,如果不保护就会出现问题,达不到预期的效果。

4. 阻塞和非阻塞

阻塞和非阻塞是形容多个线程之间的相互影响的(前提是多个线程而不是一个),一个线程占用了临界区资源那么其他线程必须在临界区之外等待,阻塞是操作系统层面挂起,上下文切换了,所以性能不高。阻塞如果一个线程一直占用不释放资源,那么其他需要该临界区资源都必须一直等。非阻塞就是运行多个线程同时进入临界区,只要保证不把数据修改坏就行。

由于临界区的存在,多线程并发必须受到控制。

根据控制并发的策略,大概可以分为一下几种:

阻塞、无饥饿、无障碍、无锁、无等待。

  • 阻塞上面已经解释了。
  • 由于线程直接的具有优先级,如果线程调度会优先调用优先级高的,那么优先级低的可能一直无法执行,就会饥饿,如果锁是公平的,都是按照新进先出就不存在饥饿了就是无饥饿。
  • 无障碍,阻塞其实是悲观锁,就是多线程一起修改临界区数据可能会被修改坏,所以每次只能一个人进行修改,其他需要等待,而无障碍的表示的一种非阻塞调度,他是一种乐观锁,他任务多个线程一起修改临界区数据也未必会把临界区数据修改坏,所以可以放开让多线程都进来,一种宽进严出的策略。如果发现一个线程在临界区操作遇到数据竞争,产生冲突,他就会回滚操作,进行重试,可能会出现死锁的情况 a依赖b b依赖a 都不断重试。
  • 无锁,是在无障碍的前提上面加一个约束,就是保证有一个线程可以胜出的,可能存在饥饿问题。
  • 无等待,是在无锁的前提上面加一个约束,就是保证所有线程都可以在有限步内完成。

JAVA的内存模型(JMM),由于并发程序比串行程序复杂很多,在并发程序下,数据访问一致性和安全性该如何保证呢?所以还需要在定义一些规则,保证多线程之间可以有效地、正确地协同工作,而JMM就是为此而生的。

JMM关键技术点都是围绕多线程的原子性、可见性、有序性来建立的。

什么是线程?

线程是进程的一个实体,是CPU调度和分派的基本单位,它是比进程更小的能独立运行的基本单位。线程自己基本上不拥有系统资源,只拥有一点在运行中必不可少的资源(如程序计数器,一组寄存器和栈),但是它可与同属一个进程的其他的线程共享进程所拥有的全部资源。

线程状态转换

  1. 新建状态(New):新创建了一个线程对象。

  2. 就绪状态(Runnable):线程对象创建后,其他线程调用了该对象的start()方法。该状态的线程位于可运行线程池中,变得可运行,等待获取CPU的使用权。

  3. 运行状态(Running):就绪状态的线程获取了CPU,执行程序代码。

  4. 阻塞状态(Blocked):阻塞状态是线程因为某种原因放弃CPU使用权,暂时停止运行。直到线程进入就绪状态,才有机会转到运行状态。

阻塞的情况分三种:

  • 等待阻塞:运行的线程执行wait()方法,JVM会把该线程放入等待池中。
  • 同步阻塞:运行的线程在获取对象的同步锁时,若该同步锁被别的线程占用,则JVM会把该线程放入锁池中。
  • 其他阻塞:运行的线程执行sleep()或join()方法,或者发出了I/O请求时,JVM会把该线程置为阻塞状态。当sleep()状态超时、join()等待线程终止或者超时、或者I/O处理完毕时,线程重新转入就绪状态。
  1. 死亡状态(Dead):线程执行完了或者因异常退出了run()方法,该线程结束生命周期。

Java线程:创建与启动

在java中要想实现多线程,有两种手段,一种是继续Thread类,另外一种是实现Runable接口。

  1. 扩展java.lang.Thread类。此类中有个run()方法,应该注意其用法:
public void run()

如果该线程是使用独立的Runnable运行对象构造的,则调用该Runnable对象的run方法;否则,该方法不执行任何操作并返回。Thread的子类应该重写该方法。

  1. 实现java.lang.Runnable接口。
public void run()

使用实现接口Runnable的对象创建一个线程时,启动该线程将导致在独立执行的线程中调用对象的run方法。方法run的常规协定是,它可能执行任何所需的操作。

特别说明:线程创建的时候,优秀的编码建议,指定线程的名称,在dump线程的时候这样不会是Thread-1这种了,而是自己取的线程名称。

启动线程在线程的Thread对象上调用start()方法,而不是run()或者别的方法。

线程的优先级

每一个 Java 线程都有一个优先级,这样有助于操作系统确定线程的调度顺序。

Java 线程的优先级是一个整数,其取值范围是 1 (Thread.MIN_PRIORITY ) - 10 (Thread.MAX_PRIORITY )。

默认情况下,每一个线程都会分配一个优先级 NORM_PRIORITY(5)。

具有较高优先级的线程对程序更重要,并且应该在低优先级的线程之前分配处理器资源。但是,线程优先级不能保证线程执行的顺序,而且非常依赖于平台。

Java线程常用方法详解

1. sleep()

使当前线程(即调用该方法的线程)暂停执行一段时间,让其他线程有机会继续执行,但它并不释放对象锁。也就是说如果有synchronized同步快,其他线程仍然不能访问共享数据。注意该方法要捕捉异常。

例如有两个线程同时执行(没有synchronized)一个线程优先级为MAX_PRIORITY,另一个为MIN_PRIORITY,如果没有Sleep()方法,只有高优先级的线程执行完毕后,低优先级的线程才能够执行;但是高优先级的线程sleep(500)后,低优先级就有机会执行了。总之,sleep()可以使低优先级的线程得到执行的机会,当然也可以让同优先级、高优先级的线程有执行的机会。

2. join()

join()方法使调用该方法的线程在此之前执行完毕,也就是等待该方法的线程执行完毕后再往下继续执行。注意该方法也需要捕捉异常。

3. yield()

该方法与sleep()类似,只是不能由用户指定暂停多长时间,并且yield()方法只能让同优先级的线程有执行的机会。

4. interrupt()

  • 如果当前线程没有中断它自己(这在任何情况下都是允许的),则该线程的 checkAccess 方法就会被调用,这可能抛出 SecurityException。如果线程在调用 Object 类的 wait()、wait(long) 或 wait(long, int) 方法,或者该类的 join()、join(long)、join(long, int)、sleep(long) 或 sleep(long, int) 方法过程中受阻,则其中断状态将被清除,它还将收到一个 InterruptedException。
  • 如果该线程在可中断的通道上的 I/O 操作中受阻,则该通道将被关闭,该线程的中断状态将被设置并且该线程将收到一个 ClosedByInterruptException。
  • 如果该线程在一个 Selector 中受阻,则该线程的中断状态将被设置,它将立即从选择操作返回,并可能带有一个非零值,就好像调用了选择器的 wakeup 方法一样。
  • 如果以前的条件都没有保存,则该线程的中断状态将被设置。中断一个不处于活动状态的线程不需要任何作用。

5. interrupted()

测试当前线程是否已经中断。线程的中断状态 由该方法清除。换句话说,如果连续两次调用该方法,则第二次调用将返回 false(在第一次调用已清除了其中断状态之后,且第二次调用检验完中断状态前,当前线程再次中断的情况除外)。线程中断被忽略,因为在中断时不处于活动状态的线程将由此返回 false 的方法反映出来。

一个线程在未正常结束之前, 被强制终止是很危险的事情. 因为它可能带来完全预料不到的严重后果. 所以你看到Thread.suspend, Thread.stop等方法都被Deprecated了。

那么不能直接把一个线程搞挂掉, 但有时候又有必要让一个线程死掉, 或者让它结束某种等待的状态 该怎么办呢? 优雅的方法就是, 给那个线程一个中断信号, 让它自己决定该怎么办. 比如说, 在某个子线程中为了等待一些特定条件的到来, 你调用了Thread.sleep(10000), 预期线程睡10秒之后自己醒来, 但是如果这个特定条件提前到来的话, 你怎么通知一个在睡觉的线程呢? 又比如说, 主线程通过调用子线程的join方法阻塞自己以等待子线程结束, 但是子线程运行过程中发现自己没办法在短时间内结束, 于是它需要想办法告诉主线程别等我了。这些情况下, 就需要中断.中断是通过调用Thread.interrupt()方法来做的. 这个方法通过修改了被调用线程的中断状态来告知那个线程, 说它被中断了. 对于非阻塞中的线程, 只是改变了中断状态, 即Thread.isInterrupted()将返回true; 对于可取消的阻塞状态中的线程, 比如等待在这些函数上的线程, Thread.sleep(), Object.wait(), Thread.join(), 这个线程收到中断信号后, 会抛出InterruptedException, 同时会把中断状态置回为false。

//Interrupted的经典使用代码
public void run(){
    try{
        ....
        while(!Thread.currentThread().isInterrupted()&& more work to do){
            // do more work;
        }
    }
    catch(InterruptedException e){
        // thread was interrupted during sleep or wait
    }
    finally{
        // cleanup, if required
    }
}

很显然,在上面代码中,while循环有一个决定因素就是需要不停的检查自己的中断状态。当外部线程调用该线程的interrupt 时,使得中断状态置位即变为true。这是该线程将终止循环,不在执行循环中的do more work了。这说明: interrupt中断的是线程的某一部分业务逻辑,前提是线程需要检查自己的中断状态(isInterrupted())。但是当线程被阻塞的时候,比如被Object.wait, Thread.join和Thread.sleep三种方法之一阻塞时。调用它的interrput()方法。可想而知,没有占用CPU运行的线程是不可能给自己的中断状态置位的。这就会产生一个InterruptedException异常。

如果线程被阻塞,它便不能核查共享变量,也就不能停止。这在许多情况下会发生,例如调用Object.wait()、ServerSocket.accept()和DatagramSocket.receive()时,他们都可能永久的阻塞线程。即使发生超时,在超时期满之前持续等待也是不可行和不适当的,所以,要使用某种机制使得线程更早地退出被阻塞的状态。很不幸运,不存在这样一种机制对所有的情况都适用,但是,根据情况不同却可以使用特定的技术。使用Thread.interrupt()中断线程正如Example1中所描述的,Thread.interrupt()方法不会中断一个正在运行的线程。这一方法实际上完成的是,在程受到阻塞时抛出一个中断信号,这样线程就得以退出阻塞的状态。更确切的说,如果线程被Object.wait, Thread.join和Thread.sleep三种方法之一阻塞,那么,它将接收到一个中断异常(InterruptedException),从而提早地终结被阻塞状态。因此,如果线程被上述几种方法阻塞,正确的停止线程方式是设置共享变量,并调用interrupt()(注意变量应该先设置)。如果线程没有被阻塞,这时调用interrupt()将不起作用;否则,线程就 将得到异常(该线程必须事先预备好处理此状况),接着逃离阻塞状态。在任何一种情况中,最后线程都将检查共享变量然后再停止。下面示例描述了该技术。

package Concurrency.Interrupt;
class Example3 extends Thread {
    volatile Boolean stop = false;
    public static void main(String args[]) throws Exception {
        Example3 thread = new Example3();
        System.out.println("Starting thread...");
        thread.start();
        Thread.sleep(3000);
        System.out.println("Asking thread to stop...");

如果线程阻塞,将不会检查此变量,调用interrupt之后,线程就可以尽早的终结被阻塞状 态,能够检查这一变量。

thread.stop = true;

这一方法实际上完成的是,在线程受到阻塞时抛出一个中断信号,这样线程就得以退出阻塞的状态。

thread.interrupt();
Thread.sleep(3000);
System.out.println("Stopping application...");
System.exit(0);
}
public void run() {
while (!stop) {
    System.out.println("Thread running...");
    try {
        Thread.sleep(2000);
    }
    catch (InterruptedException e) {
        // 接收到一个中断异常(InterruptedException),从而提早地终结被阻塞状态
        System.out.println("Thread interrupted...");
    }
}
System.out.println("Thread exiting under request...");
}
}

把握几个重点:stop变量、run方法中的sleep()、interrupt()、InterruptedException。串接起来就是这个意思:当我们在run方法中调用sleep(或其他阻塞线程的方法)时,如果线程阻塞的时间过长,比如10s,那在这10s内,线程阻塞,run方法不被执行,但是如果在这10s内,stop被 设置成true,表明要终止这个线程,但是,现在线程是阻塞的,它的run方法不能执行,自然也就不能检查stop,所 以线程不能终止,这个时候,我们就可以用interrupt()方法了:我们在 thread.stop = true;语句后调用thread.interrupt()方法, 该方法将在线程阻塞时抛出一个中断 信号,该信号将被catch语句捕获到,一旦捕获到这个信号,线程就提前终结自己的阻塞状态,这 样,它就能够 再次运行run 方法了,然后检查到stop = true,while循环就不会再被执行,在执行了while后面的清理工作之后,run方法执行完 毕,线程终止。

当代码调用中须要抛出一个InterruptedException, 你可以选择把中断状态复位, 也可以选择向外抛出InterruptedException, 由外层的调用者来决定。

不是所有的阻塞方法收到中断后都可以取消阻塞状态, 输入和输出流类会阻塞等待 I/O 完成,但是它们不抛出 InterruptedException,而且在被中断的情况下也不会退出阻塞状态。

尝试获取一个内部锁的操作(进入一个 synchronized 块)是不能被中断的。

首先需要说说线程安全?关于线程安全一直在提,比如StringBuilder和StringBuffer有什么区别? 经常就会出现关于线程安全与线程非安全,可能一直在提自己没有细细想想,如果忽然问你啥是线程安全的概念?可能你需要短暂停顿几秒,线程不安全就是不提供数据访问保护,有可能出现多个线程先后更改数据造成所得到的数据是脏数据,其实关于线程安全的定义我想不到好的,百度了下,也没有发现一个特别好的解释,我就选择一个相对来说还可以的解释吧 ,线程安全就是多线程访问时,采用了加锁机制,当一个线程访问该类的某个数据时,进行保护,其他线程不能进行访问直到该线程读取完,其他线程才可使用。不会出现数据不一致或者数据污染。我觉得该描述也不完全正确,因为现在控制并发的策略很多不仅仅是加锁机制,也可以不用加锁,我觉得这样可能比较合适的解释,就是多个线程都会操作到的,是一个公共资源或者共享的数据,但是每次操作只能一个线程使用而一旦临界区资源被占用其他的线程必须等待该资源的释放,在并行程序中,临界区资源都是受保护的那么就是线程安全,不包含的就是线程不安全的。
由于并发程序要比串行程序复杂很多,一个最重要的原因就是并发程序下访问的一致性和安全性将会受到严重挑战,如何保证一个线程可以看到正确的数据呢?因此我们需要深入了解并行机制的前提下,在定义一些规则来保证多线程直接有效的,正确的协同工作,而Java内存模型(JMM)就是来做这些事情的。

JMM模型都是围绕着多线程的原子性、可见性、和有序性来说的。这块内容过于复杂,自己水平有些,可能理解的有些偏差,希望到时候大家帮忙指出来。

原子性是指一个操作是不可中断的,即使在多线程一起执行的时候,一个操作如果开始就不会别其他线程干扰到。原文是:

Atomicity

  • Accesses and updates to the memory cells corresponding to fields of any type except long or double are guaranteed to be atomic. This includes fields serving as references to other objects. Additionally, atomicity extends to volatile long and double. (Even though non-volatile longs and doubles are not guaranteed atomic, they are of course allowed to be.)
  • Atomicity guarantees ensure that when a non-long/double field is used in an expression, you will obtain either its initial value or some value that was written by some thread, but not some jumble of bits resulting from two or more threads both trying to write values at the same time. However, as seen below, atomicity alone does not guarantee that you will get the value most recently written by any thread. For this reason, atomicity guarantees per se normally have little impact on concurrent program design.

long型字段和double型字段在32位hotspot可能不是原子性的,该如何证明呢?(作为一个思考题后续章节会进行解答,并且附上程序说明),在64位hotspot下面long型字段和double型字段都是原子性的。如果32位hotspot下volatile long 和volatile double也具有原子性 。为什么在32为hotspot加了volatile long型字段和double型字段字段就一定具有原型性了呢?,这与volatile的特性有关,当我们声明共享变量为volatile后,对这个变量的读、写会很特别,理解volatile的好方法就是把对volatile变量的单个读写堪称使用同一锁对这些单个读写操作做了同步,锁的语义决定了临界区代码的执行具有原子性,所以在32为hotspot加了volatile long型字段和double型字段字段就一定具有原型性了,后续还会说volatile的,很难理解的关键词。

有序性,如果不是为了优化,为了性能,一般代码的执行顺序就是我们写的顺序从先到后一行一行执行,但是为了提高性能,我们需要优化,可能就会修改这些原先的顺序了,

目前的编译器和处理器常常会对指令做重排的。

  1. 编译器优化的重排序。编译器在不改变单线程语义的前提下,可以重新安排语句的执行顺序。

  2. 指令级并行的重排序。现代处理器采用了指令级并行技术来将多条指令重叠执行。如果不存在数据依赖性,处理器可以改变语句对应机器指令的执行顺序。

  3. 内存系统的重排序。由于处理器换用缓存和读/写缓冲区,这使得加载和存储操作看上去可能是在乱序中执行。上述的1属于编译器重排序,2和3属于处理器重排序。

这些重排序可能会导致多线程程序出现内存可见性问题。对于编译器,JMM的编译器重排序规则会禁止特定类型的编译器重排序(不是所有的编译器重排序都要禁止)。对于处理器重排序,JMM的处理器重排序规则会要求Java编译器在生成指令序列时,插入特定类型的内存屏障指令,通过内存屏障指令来禁止特定类型的处理器重排序。关于内存屏障后续等深入了解了在来聊聊。

可见性是指当一个线程修改了某个共享变量的值,其他线程是否能够立即知道关于这个值的修改,可见性是一个复杂的综合性的问题,有一些关于缓存优化或者硬件优化会导致可见性的问题之外,上面提到的关于指令重排也会影响到可见性问题。

还有几个概念介绍:

happens-before简介

从JDK 5开始,Java使用新的JSR-133内存模型(除非特别说明,本文针对的都是JSR-133内存模型)。JSR-133使用happens-before的概念来阐述操作之间的内存可见性。在JMM中,如果一个操作执行的结果需要对另一个操作可见,那么这两个操作之间必须要存在happens-before关系。这里提到的两个操作既可以是在一个线程之内,也可以是在不同线程之间。

与程序员密切相关的happens-before规则如下:

  • 程序顺序规则:一个线程中的每个操作,happens-before于该线程中的任意后续操作。
  • 监视器锁规则:对一个锁的解锁,happens-before于随后对这个锁的加锁。
  • volatile变量规则:对一个volatile域的写,happens-before于任意后续对这个volatile域的读。
  • 传递性:如果A happens-before B,且B happens-before C,那么A happens-before C。
    注意:两个操作之间具有happens-before关系,并不意味着前一个操作必须要在后一个操作之前执行!happens-before仅仅要求前一个操作(执行的结果)对后一个操作可见,且前一个操作按顺序排在第二个操作之前。happens-before的定义很微妙,一个happens-before规则对应于一个或多个编译器和处理器重排序规则。对于Java程序员来说,happens-before规则简单易懂,它避免Java程序员为了理解JMM提供的内存可见性保证而去学习复杂的重排序规则以及这些规则的具体实现方法。

数据依赖性

如果两个操作访问同一个变量,且这两个操作中有一个为写操作,此时这两个操作之间就存在数据依赖性。数据依赖分为下列3中类型:

  1. 写后读,a=1;b=a;写一个变量之后,再读这个变量。
  2. 写后写,a=1;a=2;写一个变量之后,再写这个变量。
  3. 读后写,a=b;b=1;读一个变量之后,再写这个变量。

上面3种情况,只要重排序两个操作的执行顺序,程序的执行结果就会被改变。前面提到过,编译器和处理器可能会对操作做重排序。编译器和处理器在重排序时,会遵守数据依赖性,编译器和处理器不会改变存在数据依赖关系的两个操作的执行顺序。

这里所说的数据依赖性仅针对单个处理器中执行的指令序列和单个线程中执行的操作,不同处理器之间和不同线程之间的数据依赖性不被编译器和处理器考虑。

as-if-serial语义

as-if-serial语义的意思是:不管怎么重排序(编译器和处理器为了提高并行度),(单线程)程序的执行结果不能被改变。编译器、runtime和处理器都必须遵守as-if-serial语义。

为了遵守as-if-serial语义,编译器和处理器不会对存在数据依赖关系的操作做重排序,因为这种重排序会改变执行结果。但是,如果操作之间不存在数据依赖关系,这些操作就可能被编译器和处理器重排序。

as-if-serial语义把单线程程序保护了起来,遵守as-if-serial语义的编译器、runtime和处理器共同为编写单线程程序的程序员创建了一个幻觉:单线程程序是按程序的顺序来执行的。as-if-serial语义使单线程程序员无需担心重排序会干扰他们,也无需担心内存可见性问题。

你可能感兴趣的:(系统学习Java高并发)