image.png

一、Java 并发的发展进程

1. 在JDK 1.x release的早期版本,就确立了 Java 最基础的线程模型

具有代表性的类和接口有:

  • java.lang.Thread
  • java.lang.ThreadGroup
  • java.lang.Runnable
  • java.lang.Process
  • java.lang.ThreadDeath

    异常类

  • java.lang.IllegalMonitorStateException

  • java.lang.IllegalStateException
  • java.lang.IllegalThreadStateException.

    2. JDK5发布了许多特性功能: 泛型 Generic、枚举类型 Enumeration、可变参数varargs、注解 Annotations等等

  • 在并发方面:提供了java.util.concurrent(并发包)

    • 原子(Atomic)类型
    • 显式锁(Lock)接口
    • 计数器(CountDownLatch)
    • 回环栅栏(CyclicBarrier)
    • 信号量(Semaphore)
    • 并发集合(concurrent collections)
    • Callable和Future接口
    • 执行器(Executor接口)

3. JDK7

  • 添加ForkJoinPool(工作窃取技术)框架的支持
  • 添加Phaser类,是可重用的同步屏障,类似于CountDownLatch和CyclicBarrier
  • TransferQueue 队列

    4. JDK8

  • 加法器(Adder)和累加器(Accumulator):原子类型的扩充与优化,主要有:LongAdder、LongAccumulator、DoubleAdder和DoubleAccumulator,比AtomicLong和AtomicDouble性能更优。

  • CompletableFuture:JDK5中Future的增强版。
  • StampedLock:JDK5中ReadWriteLock的改进版。

二、了解相关概念

1. 线程与进程的区别?

  • 进程(process)和线程(thread)是操作系统的基本概念
  • 现代操作系统调度的最小单元是线程,也叫轻量级进程(Light Weight Process)
  • 线程都拥有各自的计数器、堆栈和局部变量等属性,并且能够访问共享的内存变量
  • 一个进程可以包含多个线程

2. 并发(concurrency)和并行(parallelism)的区别?

image.png

  • 并发是两个任务在重叠的时间(交替)内开始,运行和完成; 而并行是两个任务能在多核CPU 下,同一时间开始,运行和完成
  • 并发是马上处理许多任务,而并行是马上开始执行多个任务

三、了解CAS算法

1. CAS 涉及的三个操作数

  • 需要读写的内存位置V
  • 需要进行比较的预期值A
  • 需要写入的新值U

    2. 图示

    image.png

3. CAS引发的问题

  • ABA问题,一个线程将内存值从A改为B,另一个线程又从B改回到A
    • 图示

image.png

  • 解决方法:在变量前面添加版本号,每次变量更新的时候都将版本号加1,比如juc的原子包中的AtomicStampedReference类。
  • 循环时间长开销大:CAS算法需要不断地自旋来读取最新的内存值,长时间读取不到就会造成不必要的CPU开销。
  • 只能保证一个共享变量的原子操作(jdk1.5的AtomicReference来保证应用对象之间的原子性,可以把多个变量放在一个对象里来进行CAS操作,解决了这一问题)。

四、了解Java的中线程

1. 简单线程的创建和启动

  1. //继承Thread类
  2. public class MyThread extends Thread{
  3. @Override
  4. public void run() {
  5. System.out.println("线程运行");
  6. }
  7. public static void main(String[] args) {
  8. MyThread myThread=new MyThread();
  9. myThread.start();
  10. }
  11. }
  1. //实现Runable接口
  2. public class MyRunable implements Runnable{
  3. @Override
  4. public void run() {
  5. System.out.println("线程运行");
  6. }
  7. public static void main(String[] args) {http://moguhu.com/article/detail?articleId=39
  8. Thread t=new Thread(new MyRunable());
  9. t.start();
  10. }
  11. }

2. 了解线程的生命周期

image.png
对上图进行对线程的生命周期的分析:

  • (1)新建状态(NEW):线程被创建出来,还没有调用start方法;
  • (2)可运行状态(RUNABLE):调用线程的start方法,线程等待CPU时间
  • (3)运行状态(RUNNING):线程获取到CPU时间,开始执行线程任务
  • (4)等待状态(WAITING):阻塞状态是指线程因为某种原因放弃了 cpu 使用权,也即让出了 cpu timeslice ,暂时停止运行。直到线程进入可运行状态,才有机会再次获得 cpu timeslice 转到运行状态。 
  • (5) 等待超时状态(TIME_WAITING): 超时等待状态,与WAITING不同,它可以在指定时间自行退出,释放锁
  • (6) 死亡(TERMINATED):线程 run ()、 main () 方法执行结束,或者因异常退出了 run ()方法,则该线程结束生命周期。死亡的线程不可再次复生。
  • 等待阻塞(BLOCKED)的情况分三种:
    • A:等待阻塞:线程调用wait方法,释放线程占有的锁,JVM把线程加入等待队列
    • B:同步阻塞:线程尝试获取对象锁的时候,但是该锁被别的线程占有,那么JVM把该线程加入锁池
    • C:其它阻塞:线程调用sleep,join方法(不会释放锁),请求I/O时,JVM把线程设为阻塞状态,当休眠时间到了,join等待的线程超时或者完成任务,I/O处理完毕,线程重新进入运行状态,等待CPU时间

3. 线程的优先级

  • 在Thread 类中定义了属性 priority 用来控制线程的优先级
  • 优先级的范围从1~10,在线程构建的时候可以通过setPriority(int)方法来修改优先级,默认优先级是5,优先级高的线程分配时间片的数量要多于优先级低的线程。

注意:线程优先级不能作为程序正确性的依赖,因为操作系统可以完全不用理会Java线程对于优先级的设定

4. 守护线程

  1. public class DaemonThread {
  2. public static void main(String[] args) {
  3. Thread thread = new Thread(new DaemonRunner(), "DaemonRunner");
  4. thread.setDaemon(true); //开启守护线程
  5. thread.start();
  6. }
  7. static class DaemonRunner implements Runnable {
  8. @Override
  9. public void run() {
  10. try {
  11. TimeUnit.SECONDS.sleep(100);
  12. } catch (InterruptedException e) {
  13. e.printStackTrace();
  14. } finally {
  15. //这里不会输出
  16. System.out.println("DaemonThread finally run.");
  17. }
  18. }
  19. }
  20. }

注意: 

  • Daemon属性需要在启动线程之前设置,不能在启动线程之后设置。不然会抛出IllegalThreadStateException 异常
  • 当一个Java虚拟机中不存在非Daemon线程的时候,Java虚拟机将会退出,意味着Daemon 线程会被中断
  • 所以,在构建Daemon线程时,不能依靠finally块中的内容来确保执行关闭或清理资源的逻辑

到此, Java 多线程已经入门,接下来要先了解线程的抽象内存模型。

参考