1.为什么要使用并发编程

  • 充分利用多核CPU的计算能力:通过并发编程的形式可以将多核CPU的计算能力发挥到极致,性能得到提升。
  • 方便进行业务拆分,提升系统并发能力和性能:在特殊的业务场景下,先天的就适合于并发编程。现在的系统动不动就要求百万级甚至千万级的并发量,而多线程并发编程正是开发高并发系统的基础,利用好多线程机制可以大大提高系统整体的并发能力以及性能。面对复杂业务模型,并行程序会比串行程序更适应业务需求,而并发编程更能吻合这种业务拆分 。

2.进程与线程

2.1 进程与线程

进程

  • 程序由指令和数据组成,但这些指令要运行,数据要读写,就必须将指令加载至CPU,数据加载至内存。在指令运行过程中还需要用到磁盘,网络等设备。进程就是用来加载指令,管理内存,管理IO的。
  • 当一个程序被运行,从磁盘加载这个程序的代码至内存,这是就开启了一个进程。
  • 进程就可以是为程序的一个实例。大部分程序可以同时运行多个实例进程(例如记事本,画图,浏览器等),也由的程序只能启动一个实例进程(例如网易云音乐,360安全卫士等)

线程

  • 一个进程之内可以分为一到多个线程
  • 一个线程就是一个指令流,将指令流的一条条指令以一定的顺序交给CPU执行
  • java中,线程作为最小调度单位,进程作为资源分配的最小单位。在windows中进程是不活动的,只是作为线程的容器。

二者对比

  • 进程基本上相互独立的,而线程存在于进程内,是进程的一个子集。
  • 进程拥有共享的资源,如内存空间等,但其内部的线程共享
  • 进程通信较为复杂:同一台计算机的进程通信称为IPC;不同计算机间的进程通信,需要通过网络,并遵守共同的协议,例如HTTP
  • 线程通信相对简单,一位内它们共享进程内的内存,一个例子是多个线程可以访问同一个共享变量
  • 线程更轻量,线程上下文切换成本一般要比进程上下文切换底

2.2 并行与并发

单核cpu下,线程实际还是串行的。操作系统中有一个组件叫做任务调度器,将cpu的时间片(windows下时间片最小约为15毫秒)分给不同的线程使用,只是由于cpu在线程间(时间片很短)的切换非常快,人类感觉是同时运行的。总结一句话就是:微观串行,宏观并行。

Java并发编程的基石 java并发编程深度解析_System

Java并发编程的基石 java并发编程深度解析_Java并发编程的基石_02

引用Rob Pike的一段描述

  • 并发(concurrent)是同一时间应对(dealing with)多件事情的能力。
  • 并行(parallel)是同一时间动手做(doing)多件事情的能力。

例子

  • 家庭主妇做饭,打扫卫生,给孩子喂奶,她一个人轮流交替做这多件事,这时就是并发
  • 家庭主妇雇了个保姆,她们一起做这些事,这时既有并发,也有并行(这时会产生竞争,例如锅只有一口,一个人用锅时,另外一个就得等待)
  • 雇了3个保姆,一个专门做饭,一个专门打扫卫生,一个专门喂奶,互不干扰,这时是并行。

 

2.3应用

Java并发编程的基石 java并发编程深度解析_System_03

Java并发编程的基石 java并发编程深度解析_ide_04

Java并发编程的基石 java并发编程深度解析_Java并发编程的基石_05

3.java线程

3.1 创建和运行线程

方法一:直接使用Thread

//创建线程对象
        Thread t = new Thread(){
            @Override
            public void run() {
                //要执行的任务
                System.out.println("do some thing");
            }
        };
        //启动线程
        t.start();

方法二:使用Runnable配合Thread

把【线程】和任务(要执行的代码)分开

  • Thread代表线程
  • runnable可运行的任务(线程要执行的代码)
Runnable runnable = new Runnable() {
            @Override
            public void run() {
                //要执行的任务
                System.out.println("do some thing");
            }
        };
        //创建线程对象
        Thread t = new Thread(runnable);
        //启动线程
        t.start();

java8精简

//java8方式
        Runnable runnable = ()-> System.out.println("do some thing");
        //创建线程对象:参数1是任务对象,参数2是线程名称
        Thread t = new Thread(runnable,"t2");
        //启动线程
        t.start();

原理之Thread与Runnable的关系

Java并发编程的基石 java并发编程深度解析_ide_06

  • 方法1是把线程和任务合并在了一起,方法2是把线程和任务分开了
  • 用runnable更容易与线程池等高级API配合
  • 用runnable让任务类脱离了Thread继承体系,更灵活。

方法三:FutureTask配合Thread

FutureTask能够接收Callable类型的参数,用来处理返回结果的情况

//创建任务对象
        FutureTask<Integer> task = new FutureTask<>(()->{
            System.out.println("hello");
            return 100;
        });

        //创建线程对象:参数1是任务对象,参数2是线程名称
        Thread t = new Thread(task,"t3");
        //启动线程
        t.start();

        //主线程阻塞,同步等待task执行完毕的结果
        Integer result = task.get();
        System.out.println("结果是:"+result);

 

3.2 观察多个线程同时运行

logback.xml

<?xml version="1.0" encoding="UTF-8"?>
<configuration>
  <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
    <encoder>
      <pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%n</pattern>
    </encoder>
  </appender>

  <logger name="c" level="debug" additivity="false">
    <appender-ref ref="STDOUT"/>
  </logger>

  <root level="DEBUG">
    <appender-ref ref="STDOUT"/>
  </root>
</configuration>

pom.xml

<dependencies>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <version>1.18.12</version>
        </dependency>
        <dependency>
            <groupId>ch.qos.logback</groupId>
            <artifactId>logback-classic</artifactId>
            <version>1.2.3</version>
        </dependency>
    </dependencies>
@Slf4j(topic = "c.TestMultiThread")
public class TestMultiThread {
    public static void main(String[] args) {
        new Thread(() -> {
             while (true){
                 log.debug("running");
             }
        },"t1").start();

        new Thread(()->{
            while (true){
                log.debug("running");
            }
        },"t2").start();

    }
}

3.3 查看线程和进程

Java并发编程的基石 java并发编程深度解析_时间片_07

Java并发编程的基石 java并发编程深度解析_时间片_08

3.4 原理之线程运行

栈与栈帧

jvm中由堆,栈,方法区所组成,其中栈内存是给谁用的呢?其实就是线程,每个线程启动后,虚拟机就会为其分配一块栈内存。

  • 每个栈由多个栈帧组成,对应着每次方法调用时所占用的内存
  • 每个线程只能由一个活动栈帧,对应着当前正在执行的那个方法

线程上下文切换(thread context switch)

  • 因为以下原因导致cpu不再执行当前的线程,转而执行另一个线程的代码
  • 线程的cpu时间片用完
  • 垃圾回收
  • 更高优先级的线程需要运行
  • 线程自己调用了sleep,yield,wait,join,park,synchronized,lock等方法程序

当context switch发生时需要由操系统保存当前线程的状态,并恢复另一个线程的状态,java中对应的概念就是程序计数器,她的作用是记住下一条jvm指令的执行地址,是线程私有

  • 状态包括程序计数器,虚拟机栈中每个栈帧的信息,如局部变量,操作数栈,返回地址等
  • context switch频繁发生会影响性能

Java并发编程的基石 java并发编程深度解析_Java并发编程的基石_09

Java并发编程的基石 java并发编程深度解析_Java并发编程的基石_10

3.5 常见方法

Java并发编程的基石 java并发编程深度解析_System_11

Java并发编程的基石 java并发编程深度解析_System_12

 3.6 run与start

Java并发编程的基石 java并发编程深度解析_时间片_13

3.7 sleep与yield 

sleep

  • 调用sleep会让当前线程从running进入timed warting状态
  • 其它线程可以使用interrupt方法打断正在睡眠的线程,这时sleep方法会抛出interruptedException
  • 睡眠结束的线程未必会立刻得到执行
  • 建议用timeUnit的sheep代替Thread的sleep来获得更好得可读性

yield

  • 调用yield会让当前线程从running进入runnable状态,然后调度执行其它同优先级得线程。如果这时没有同优先得线程,那么不能保证让当前线程暂停的效果。
  • 具体的实现依赖于操作系统的任务调度器

线程优先级

  • 线程优先级会提示调度器优先调度该线程,但它仅仅是一个提示,调度器可以忽略它
  • 如果cpu比较忙,那么优先级高的线程会获得更多的时间片,但cpu闲时,优先级几乎没作用

sleep实现

在没有利用cpu来计算时,不要让while(true)空转浪费cpu,这时可以用yield或sleep来让出cpu的使用权给其它程序

while (true){
            try {
                Thread.sleep(5000);
                System.out.println(Thread.currentThread().getName());
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
  • 可以用wait或条件变量达到类似的效果
  • 不同的是,后两种都需要加锁,并且需要相应的唤醒操作,一般适用于要进行同步的场景
  • sleep适用于无需锁同步的场景

 

3.8 join方法详解

为什么需要join

下面的代码执行,打印r是什么?

public class JoinDemo {
    static  int r = 0;
    public static void main(String[] args) throws InterruptedException {
        test();
    }

    public static  void test() throws InterruptedException {
        System.out.println("开始");
        Thread t1 = new Thread(()->{
            System.out.println("开始");
            try {
                Thread.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("结束");
            r=10;
        });
        t1.start();
//        t1.join();
        System.out.println("结果为:"+r);
        System.out.println("结束");
    }

}

 

Java并发编程的基石 java并发编程深度解析_时间片_14

分析:

  • 因为主线程和线程t1是并行执行的,t1线程需要1秒后才能算出r=10
  • 而主线程一开始就要打印r的结果,所以只能打印出r=0

解决方法

  • 用sleep行不行?为什么?
  • 用join,加在t1.start()之后即可

 

Java并发编程的基石 java并发编程深度解析_时间片_15

Java并发编程的基石 java并发编程深度解析_Java并发编程的基石_16

Java并发编程的基石 java并发编程深度解析_ide_17

Java并发编程的基石 java并发编程深度解析_Java并发编程的基石_18

Java并发编程的基石 java并发编程深度解析_ide_19

3.9 interrupt方法详解

Java并发编程的基石 java并发编程深度解析_ide_20

Java并发编程的基石 java并发编程深度解析_时间片_21

Java并发编程的基石 java并发编程深度解析_System_22

3.10 不推荐的方法

Java并发编程的基石 java并发编程深度解析_时间片_23

3.11 主线程与守护线程

Java并发编程的基石 java并发编程深度解析_System_24

Java并发编程的基石 java并发编程深度解析_ide_25

3.12 线程五种状态

Java并发编程的基石 java并发编程深度解析_ide_26

Java并发编程的基石 java并发编程深度解析_时间片_27

3.13 线程的六种状态

Java并发编程的基石 java并发编程深度解析_Java并发编程的基石_28

Java并发编程的基石 java并发编程深度解析_ide_29

@Slf4j(topic = "c.TestState")
public class TestState {
    public static void main(String[] args) {
        Thread t1 = new Thread("t1"){
            @Override
            public void run() {   //NEW  没有start()
                log.debug("running...");
            }
        };

        Thread t2 = new Thread("t2"){
            @Override
            public void run() {  //RUNNABLE  一直在运行
                while (true){

                }

            }
        };
        t2.start();

        Thread t3 = new Thread("t3"){
            @Override
            public void run() {  //TERMINATED  运行完结束了
                log.debug("running...");
            }
        };
        t3.start();

        Thread t4 = new Thread("t4"){
            @Override
            public void run() {  //TIMED_WAITING  有时间的等待中
               synchronized (TestState.class){
                   try {
                       Thread.sleep(1000000);
                   }catch (InterruptedException E){
                       E.printStackTrace();
                   }
               }
            }
        };
        t4.start();

        Thread t5 = new Thread("t5"){
            @Override
            public void run() {  //WAITING  一直等待着t2运行完
                try {
                    t2.join();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        };
        t5.start();

        Thread t6 = new Thread("t6"){
            @Override
            public void run() {  //BLOCKED  获取锁,但是没获取到
                synchronized (TestState.class){
                    try {
                        Thread.sleep(1000000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        };
        t6.start();


        log.debug("t1 state {}",t1.getState());
        log.debug("t2 state {}",t2.getState());
        log.debug("t3 state {}",t3.getState());
        log.debug("t4 state {}",t4.getState());
        log.debug("t5 state {}",t5.getState());
        log.debug("t6 state {}",t6.getState());
    }
}

3.14 习题

引用之统筹


阅读华罗庚《统筹方法》,给出烧水泡茶的多线程解决方案。

Java并发编程的基石 java并发编程深度解析_ide_30

 

 

小结

Java并发编程的基石 java并发编程深度解析_Java并发编程的基石_31

 

参考:https://www.bilibili.com/video/BV1jE411j7uX