• Java并发编程——线程基础知识


                                                                                                                                                                                                                                                                            

    一、线程与进程

    1.1 进程与进程

    进程

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

    线程

    • 一个进程之内可以分为一到多个线程。
    • 一个线程就是一个指令流,将指令流中的一条条指令以一定的顺序交给 CPU 执行
    • Java 中,线程作为最小调度单位,进程作为资源分配的最小单位。 在 windows 中进程是不活动的,只是作 为线程的容器(这里感觉要学了计算机组成原理之后会更有感觉吧!)

    二者对比

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

    1.2 并行与并发

    并发

    在单核 cpu 下,线程实际还是串行执行的。操作系统中有一个组件叫做任务调度器,将 cpu 的时间片(windows 下时间片最小约为 15 毫秒)分给不同的程序使用,只是由于 cpu 在线程间(时间片很短)的切换非常快,人类感 觉是同时运行的 。一般会将这种线程轮流使用 CPU 的做法称为并发(concurrent)

    并行

    多核 cpu下,每个核(core) 都可以调度运行线程,这时候线程可以是并行的,不同的线程同时使用不同的cpu在执行。

    二者对比

    引用 Rob Pike 的一段描述:并发(concurrent)是同一时间应对(dealing with)多件事情的能力,并行(parallel)是同一时间动手做(doing)多件事情的能力

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

    应用

    同步和异步的概念

    以调用方的角度讲,如果需要等待结果返回才能继续运行的话就是同步,如果不需要等待就是异步

    1) 设计

    多线程可以使方法的执行变成异步的,比如说读取磁盘文件时,假设读取操作花费了5秒,如果没有线程的调度机制,这么cpu只能等5秒,啥都不能做。

    2) 结论

    • 比如在项目中,视频文件需要转换格式等操作比较费时,这时开一个新线程处理视频转换,避免阻塞主线程
    • tomcat 的异步 servlet 也是类似的目的,让用户线程处理耗时较长的操作,避免阻塞 tomcat 的工作线程
    • ui 程序中,开线程进行其他操作,避免阻塞 ui 线程

    二、java线程

    2.1 创建和运行线程

    方法一,直接使用 Thread

    // 构造方法的参数是给线程指定名字,,推荐给线程起个名字
    Thread t1 = new Thread("t1") {
     @Override
     // run 方法内实现了要执行的任务
     public void run() {
     log.debug("hello");
     }
    };
    t1.start();

    使用继承方式的好处是,在run()方法内获取当前线程直接使用this就可以了,无须使用Thread.currentThread()方法;不好的地方是Java不支持多继承,如果继承了Thread类,那么就不能再继承其他类。另外任务与代码没有分离,当多个线程执行一样的任务时需要多份任务代码。

    方法二,使用 Runnable 配合 Thread

    把【线程】和【任务】(要执行的代码)分开,Thread 代表线程,Runnable 可运行的任务(线程要执行的代码)

    // 创建任务对象
    Runnable task2 = new Runnable() {
     @Override
     public void run() {
     log.debug("hello");
     }
    };
    // 参数1 是任务对象; 参数2 是线程名字,推荐给线程起个名字
    Thread t2 = new Thread(task2, "t2");
    t2.start();

    通过实现Runnable接口,并且实现run()方法。在创建线程时作为参数传入该类的实例即可。

    Java 8 以后可以使用 lambda 精简代码

    // 创建任务对象
    Runnable task2 = () -> log.debug("hello");
    // 参数1 是任务对象; 参数2 是线程名字,推荐
    Thread t2 = new Thread(task2, "t2");
    t2.start();

    小结

    方法1 是把线程和任务合并在了一起,方法2 是把线程和任务分开了,用 Runnable 更容易与线程池等高级 API 配合,用 Runnable 让任务类脱离了 Thread 继承体系,更灵活。通过查看源码可以发现,方法二其实到底还是通过方法一执行的!

    方法三,FutureTask 配合 Thread

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

        public static void main(String[] args) throws ExecutionException, InterruptedException {
            // 实现多线程的第三种方法可以返回数据
            FutureTask futureTask = new FutureTask<>(new Callable<Integer>() {
                @Override
                public Integer call() throws Exception {
                    log.debug("多线程任务");
                    Thread.sleep(100);
                    return 100;
                }
            });
            // 主线程阻塞,同步等待 task 执行完毕的结果
            new Thread(futureTask,"我的名字").start();
            log.debug("主线程");
            log.debug("{}",futureTask.get());
        }

    Future就是对于具体的Runnable或者Callable任务的执行结果进行取消、查询是否完成、获取结果。必要时可以通过get方法获取执行结果,该方法会阻塞直到任务返回结果。

    public interface Future<V> {
        boolean cancel(boolean mayInterruptIfRunning);
        boolean isCancelled();
        boolean isDone();
        V get() throws InterruptedException, ExecutionException;
        V get(long timeout, TimeUnit unit)
            throws InterruptedException, ExecutionException, TimeoutException;
    }

    Future提供了三种功能:   

    1. 判断任务是否完成;   

    2. 能够中断任务;   

    3. 能够获取任务执行结果。

    FutureTask是Future和Runable的实现

    2.2 查看进程线程的方法

    windows

    • 任务管理器可以查看进程和线程数,也可以用来杀死进程
    • tasklist 查看进程
    • taskkill 杀死进程

    linux

    • ps -fe 查看所有进程
    • ps -fT -p <PID> 查看某个进程(PID)的所有线程
    • kill 杀死进程
    • top 按大写 H 切换是否显示线程
    • top -H -p <PID> 查看某个进程(PID)的所有线程

    Java

    • jps 命令查看所有 Java 进程
    • jstack <PID> 查看某个 Java 进程(PID)的所有线程状态
    • jconsole 来查看某个 Java 进程中线程的运行情况(图形界面)

    jconsole 远程监控配置

    • 需要以如下方式运行你的 java 类
    java -Djava.rmi.server.hostname=`ip地址` -Dcom.sun.management.jmxremote -
    Dcom.sun.management.jmxremote.port=`连接端口` -Dcom.sun.management.jmxremote.ssl=是否安全连接 -
    Dcom.sun.management.jmxremote.authenticate=是否认证 java类
    • 修改 /etc/hosts 文件将 127.0.0.1 映射至主机名

    如果要认证访问,还需要做如下步骤

    • 复制 jmxremote.password 文件
    • 修改 jmxremote.password 和 jmxremote.access 文件的权限为 600 即文件所有者可读写
    • 连接时填入 controlRole(用户名),R&D(密码)

    2.3 线程运行原理

    虚拟机栈与栈帧

    拟机栈描述的是Java方法执行的内存模型:每个方法被执行的时候都会同时创建一个栈帧(stack frame)用于存储局部变量表、操作数栈、动态链接、方法出口等信息,是属于线程的私有的。当java中使用多线程时,每个线程都会维护它自己的栈帧!每个线程只能有一个活动栈帧,对应着当前正在执行的那个方法。

    程序入栈过程:

    之后方法依次出栈:

    线程上下文切换(Thread Context Switch)

    因为以下一些原因导致 cpu 不再执行当前的线程,转而执行另一个线程的代码

    • 线程的 cpu 时间片用完(每个线程轮流执行,看前面并行的概念)
    • 垃圾回收
    • 有更高优先级的线程需要运行
    • 线程自己调用了sleepyieldwaitjoinparksynchronizedlock 等方法

    Thread Context Switch 发生时,需要由操作系统保存当前线程的状态,并恢复另一个线程的状态(包括程序计数器、虚拟机栈中每个栈帧的信息,如局部变量、操作数栈、返回地址等),Java 中对应的概念 就是程序计数器(Program Counter Register),它的作用是记住下一条 jvm 指令的执行地址,是线程私有的。

    • Java 创建的线程是内核级线程,线程的调度是在内核态运行的,而线程中的代码是在用户态运行,所以线程切换(状态改变)会导致用户与内核态转换,这是非常消耗性能
    • Java 中 main 方法启动的是一个进程也是一个主线程,main 方法里面的其他线程均为子线程,main 线程是这些线程的父线程

    当发生上下文切换时

     t1进程运行完,再切换回来。

    三、线程方法

    Thread 类 API:

    3.1 start  VS run

    调用start

        public static void main(String[] args) {
            Thread thread = new Thread(){
              @Override
              public void run(){
                  log.debug("我是一个新建的线程正在运行中");
                  FileReader.read(fileName);
              }
            };
            thread.setName("新建线程");
            thread.start();
            log.debug("主线程");
        }

    输出:程序在 t1 线程运行, run()方法里面内容的调用是异步的 Test4.java

    11:59:40.711 [main] DEBUG com.concurrent.test.Test4 - 主线程
    11:59:40.711 [新建线程] DEBUG com.concurrent.test.Test4 - 我是一个新建的线程正在运行中
    11:59:40.732 [新建线程] DEBUG com.concurrent.test.FileReader - read [test] start ...
    11:59:40.735 [新建线程] DEBUG com.concurrent.test.FileReader - read [test] end ... cost: 3 ms

    调用run

    将上面代码的thread.start();改为 thread.run();输出结果如下:程序仍在 main 线程运行, run()方法里面内容的调用还是同步的

    12:03:46.711 [main] DEBUG com.concurrent.test.Test4 - 我是一个新建的线程正在运行中
    12:03:46.727 [main] DEBUG com.concurrent.test.FileReader - read [test] start ...
    12:03:46.729 [main] DEBUG com.concurrent.test.FileReader - read [test] end ... cost: 2 ms
    12:03:46.730 [main] DEBUG com.concurrent.test.Test4 - 主线程

    小结

    • 直接调用 run() 是在主线程中执行了 run(),没有启动新的线程
    • 使用 start() 是启动新的线程,通过新的线程间接执行 run()方法 中的代码

    3.2 sleep VS yield

    sleep方法

    •   调用 sleep() 会让当前线程从 Running(运行状态) 进入 Timed Waiting 状态(阻塞)
    •  其它线程可以使用interrupt 方法打断正在睡眠的线程,那么被打断的线程这时就会抛出 InterruptedException异常【注意:这里打断的是正在休眠的线程,而不是其它状态的线程】
    •  睡眠结束后的线程未必会立刻得到执行 (需要分配到cpu时间片)
    •  建议用 TimeUnit 的 sleep() 代替 Thread 的 sleep()来获得更好的可读性

    yield方法

    • 调用 yield 会让当前线程从Running 进入 Runnable 就绪状态,然后调度执行其它线程
    • 具体的实现依赖于操作系统的任务调度器(就是可能没有其它的线程正在执行,虽然调用了yield方法,但是也没有用)

    小结

    • yield使cpu调用其它线程,但是cpu可能会再分配时间片给该线程;而sleep需要等过了休眠时间之后才有可能被分配cpu时间片

    3.3 线程优先级

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

    thread1.setPriority(Thread.MAX_PRIORITY); //设置为优先级最高

    3.4 join方法

    • 主线程中调用t1.join,则主线程等待t1线程执行完之后继续执行
    private static void test1() throws InterruptedException {
        log.debug("开始");
        Thread t1 = new Thread(() -> {
            log.debug("开始");
            sleep(1);
            log.debug("结束");
            r = 10;
        },"t1");
        t1.start();
        // t1.join(); 
        // 这里如果不加t1.join(), 此时主线程不会等待t1线程给r赋值, 主线程直接就输出r=0结束了
        // 如果加上t1.join(), 此时主线程会等待到t1线程执行完才会继续执行.(同步), 此时r=10;
        log.debug("结果为:{}", r);
        log.debug("结束");
    }

     下图, 因为开辟了t1线程. 此时程序中有两个线程; main线程和t1线程; 此时在main线程中调用t1.join, 所以main线程只能阻塞等待t1线程执行完. t1线程在1s后将r=10, t1线程执行完, 此时main线程才会接着执行。

    等待多个结果
    问,下面代码 cost 大约多少秒?

        private static void test2() throws InterruptedException {
            Thread t1 = new Thread(() -> {
                sleep(1);
                r1 = 10;
            });
            Thread t2 = new Thread(() -> {
                sleep(2);
                r2 = 20;
            });
            t1.start();
            t2.start();
            long start = System.currentTimeMillis();
            log.debug("join begin");
            t1.join();
            log.debug("t1 join end");
            t2.join();
            log.debug("t2 join end");
            long end = System.currentTimeMillis();
            log.debug("r1: {} r2: {} cost: {}", r1, r2, end - start);
        }

    分析如下

    • 第一个 join:等待 t1 时, t2 并没有停止, 而在运行
    • 第二个 join:1s 后, 执行到此, t2 也运行了 1s, 因此也只需再等待 1s

    如果颠倒两个 join 呢?

     

    有时效的 join
    等够时间

    static int r1 = 0;
    static int r2 = 0;

    public
    static void test3() throws InterruptedException { Thread t1 = new Thread(() -> { sleep(2); r1 = 10; }); long start = System.currentTimeMillis(); t1.start(); // 线程执行结束会导致 join 结束 log.debug("join begin"); t1.join(3000); long end = System.currentTimeMillis(); log.debug("r1: {} r2: {} cost: {}", r1, r2, end - start); }

    没等够时间

        static int r1 = 0;
        static int r2 = 0;
    public static void test3() throws InterruptedException { Thread t1 = new Thread(() -> { sleep(2); r1 = 10; }); long start = System.currentTimeMillis(); t1.start(); // 线程执行结束会导致 join 结束 log.debug("join begin"); t1.join(1500); long end = System.currentTimeMillis(); log.debug("r1: {} r2: {} cost: {}", r1, r2, end - start); }

    3.5 interrupt 方法详解

    本文后面的中断和打断两次同义。

    该方法用于打断 sleep,wait,join的线程, 在阻塞期间cpu不会分配给时间片

    • 先了解一些interrupt()方法的相关知识:博客地址
    • 如果一个线程在在运行中被打断,打断标记会被置为true
    • 如果是打断因sleep wait join方法而被阻塞的线程,会将打断标记置为false

    sleep,wait,join的线程,这几个方法都会让线程进入阻塞状态,以 sleep 为例

    public static void main(String[] args) throws InterruptedException {
            Thread t1 = new Thread(() -> {
                log.debug("sleep...");
                try {
                    Thread.sleep(5000); // wait, join
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            },"t1");
    
            t1.start();
            Thread.sleep(1000);
            log.debug("interrupt");
            t1.interrupt();
            // 如果是打断sleep,wait,join的线程, 即使打断了, 标记也为false
            log.debug("打断标记:{}", t1.isInterrupted());
        }

    打断正常运行的线程

    打断正常运行的线程, 线程并不会暂停,只是调用方法Thread.currentThread().isInterrupted();的返回值为true,可以判断Thread.currentThread().isInterrupted();的值来手动停止线程

     public static void main(String[] args) throws InterruptedException {
            Thread t1 = new Thread(() -> {
                while(true) {
                    boolean interrupted = Thread.currentThread().isInterrupted();
                    if(interrupted) {
                        log.debug("被打断了, 退出循环");
                        break;
                    }
                }
            }, "t1");
            t1.start();
            Thread.sleep(1000);
            log.debug("interrupt");
            t1.interrupt(); 
        }

    3.6  终止模式之两阶段终止模式

     当我们在执行线程一时,想要终止线程二,这是就需要使用interrupt方法来优雅的停止线程二。


    Two Phase Termination,就是考虑在一个线程T1中如何优雅地终止另一个线程T2?这里的优雅指的是给T2一个料理后事的机会(如释放锁)。

    错误思路:

    如下所示:那么线程的isInterrupted()方法可以取得线程的打断标记

    •  如果线程在睡眠sleep期间被打断,打断标记是不会变的,为false,但是sleep期间被打断会抛出异常,我们据此手动设置打断标记为true;
    •  如果是在程序正常运行期间被打断的,那么打断标记就被自动设置为true。处理好这两种情况那我们就可以放心地来料理后事啦!

    下图①就是正常运行打断, ②是在睡眠中被打断

    public class Test13 {
        public static void main(String[] args) throws InterruptedException {
            Monitor monitor = new Monitor();
            monitor.start();
            Thread.sleep(3500);
            monitor.stop();
        }
    }
    
    class Monitor {
    
        Thread monitor;
    
        /**
         * 启动监控器线程
         */
        public void start() {
            //设置线控器线程,用于监控线程状态
            monitor = new Thread() {
                @Override
                public void run() {
                    //开始不停的监控
                    while (true) {
                        //判断当前线程是否被打断了
                        if(Thread.currentThread().isInterrupted()) {
                            System.out.println("处理后续任务");
                            //终止线程执行
                            break;
                        }
                        System.out.println("监控器运行中...");
                        try {
                            //线程休眠
                            Thread.sleep(1000);
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                            //如果是在休眠的时候被打断,不会将打断标记设置为true,这时要重新设置打断标记
                            Thread.currentThread().interrupt();
                        }
                    }
                }
            };
            monitor.start();
        }
    
        /**
         *     用于停止监控器线程
         */
        public void stop() {
            //打断线程
            monitor.interrupt();
        }
    }

    3.7 sleep,yiled,wait,join 对比

    sleep,join,yield,interrupted是Thread类中的方法
    wait/notify是object中的方法

    •   sleep 不释放锁、释放cpu
    •   join 释放锁、抢占cpu
    •   yiled 不释放锁、释放cpu
    •   wait 释放锁、释放cpu

    还有一些不推荐使用的方法,这些方法已过时,容易破坏同步代码块,造成线程死锁

    3.8 守护线程

        当Java进程中有多个线程在执行时,只有当所有非守护线程都执行完毕后,Java进程才会结束。但当非守护线程全部执行完毕后,守护线程无论是否执行完毕,也会一同结束。   

    Java中的线程分为两类,分别为用户线程和守护线程,在JVM启动时会调用main函数,main函数所在的线程就是一个用户线程,JVM内部还启动了好多守护线程,如垃圾回收线程。

    注意:       

    • 垃圾回收器线程就是一种守护线程       
    • Tomcat 中的 Acceptor 和 Poller 线程都是守护线程,所以 Tomcat 接收到 shutdown 命令后,不会等

    3.9 线程状态之五种状态

    五种状态的划分主要是从操作系统的层面进行划分的

    1. 初始状态,仅仅是在语言层面上创建了线程对象,即Thead thread = new Thead();,还未与操作系统线程关联
    2. 可运行状态,也称就绪状态,指该线程已经被创建,与操作系统相关联,等待cpu给它分配时间片就可运行
    3. 运行状态,指线程获取了CPU时间片,正在运行
      1. 当CPU时间片用完,线程会转换至【可运行状态】,等待 CPU再次分配时间片,会导致我们前面讲到的上下文切换
    4. 阻塞状态
      1. 如果调用了阻塞API,如BIO读写文件,那么线程实际上不会用到CPU,不会分配CPU时间片,会导致上下文切换,进入【阻塞状态】
      2. 等待BIO操作完毕,会由操作系统唤醒阻塞的线程,转换至【可运行状态】
      3. 与【可运行状态】的区别是,只要操作系统一直不唤醒线程,调度器就一直不会考虑调度它们,CPU就一直不会分配时间片
    5. 终止状态,表示线程已经执行完毕,生命周期已经结束,不会再转换为其它状态

    3.10 线程状态之六种状态

    这是从 Java API 层面来描述的,我们主要研究的就是这种。状态转换详情图:地址 根据 Thread.State 枚举,分为六种状态 Test12.java

    1. NEW 跟五种状态里的初始状态是一个意思
    2. RUNNABLE 是当调用了 start() 方法之后的状态,注意,Java API 层面的 RUNNABLE 状态涵盖了操作系统层面的【可运行状态】、【运行状态】和【io阻塞状态】(由于 BIO 导致的线程阻塞,在 Java 里无法区分,仍然认为是可运行)
    3. BLOCKEDWAITINGTIMED_WAITING 都是 Java API 层面对【阻塞状态】的细分,后面会在状态转换一节 详述

    更为详细的状态切换

     六种状态测试:

    @Slf4j(topic = "c.TestState")
    public class TestState {
        public static void main(String[] args) throws IOException {
            // new
            Thread t1 = new Thread("t1") {
                @Override
                public void run() {
                    log.debug("running...");
                }
            };
            // 运行
            Thread t2 = new Thread("t2") {
                @Override
                public void run() {
                    while(true) { // runnable
    
                    }
                }
            };
            t2.start();
    
            Thread t3 = new Thread("t3") {
                @Override
                public void run() {
                    log.debug("running...");
                }
            };
            t3.start();
    
            Thread t4 = new Thread("t4") {
                @Override
                public void run() {
                    synchronized (TestState.class) {
                        try {
                            Thread.sleep(1000000); // timed_waiting
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    }
                }
            };
            t4.start();
    
            Thread t5 = new Thread("t5") {
                @Override
                public void run() {
                    try {
                        t2.join(); // waiting t2没运行完,t5一直在等待。
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            };
            t5.start();
    
            Thread t6 = new Thread("t6") {
                @Override
                public void run() {
                    synchronized (TestState.class) { // blocked
                        try {
                            Thread.sleep(1000000);
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    }
                }
            };
            t6.start();
    
            try {
                Thread.sleep(500);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            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());
            System.in.read();
        }
    }

    作者:王陸

    -------------------------------------------

    个性签名:罔谈彼短,靡持己长。做一个谦逊爱学的人!

    本站使用「署名 4.0 国际」创作共享协议,转载请在文章明显位置注明作者及出处。鉴于博主处于考研复习期间,有什么问题请在评论区中提出,博主尽可能当天回复,加微信好友请注明原因

  • 相关阅读:
    九九乘法表
    计算器实现
    分装的日期类
    杨辉三角
    99乘法表
    素数
    java输出100以内质数
    跳台阶
    Counting Sheep
    课上作业
  • 原文地址:https://www.cnblogs.com/wkfvawl/p/15406958.html
Copyright © 2020-2023  润新知