《Java多线程编程实战指南(核心篇)》阅读笔记

时间:2019-09-05
本文章向大家介绍《Java多线程编程实战指南(核心篇)》阅读笔记,主要包括《Java多线程编程实战指南(核心篇)》阅读笔记使用实例、应用技巧、基本知识点总结和需要注意事项,具有一定的参考价值,需要的朋友可以参考一下。
《Java多线程编程实战指南(核心篇)》阅读笔记

《Java多线程编程实战指南(核心篇)》阅读笔记

1 线程概念

1.1 进程、线程

进程
程序向操作系统申请资源(如内存空间和文件句柄)的基本单位
线程
进程中可独立执行的最小单位

一个进程可以包含多个线程,同一个进程中的所有线程共享该进程中的资源,如内存空间、文件句柄等。

1.2 Java中线程的创建

在Java平台中创建一个线程就是创建一个Thread类(或其子类)的实例;运行一个线程实际上就是让Java虚拟机执行其run方法, 使相应的任务处理逻辑代码得以执行,Thread类的start方法的作用是启动相应的线程。

start方法调用结束并不意味着相应线程已经开始运行,这个线程可能稍后才被运行,甚至也可能永远不会被运行。 因为启动一个线程的实质是请求Java虚拟机运行相应的线程,而这个线程具体何时能够运行是由线程调度器(Scheduler)决定的。

创建Java线程
Thread类的两个常用构造器是:Thread和Thread(Runnable target),Thread是线程的抽象,Runnable可以看作是对任务的抽象。
new Thread(){
    @Override
    public void run() {
        // 执行任务
    }
};
new Thread(new Runnable() {
    @Override
    public void run() {
        // 执行任务
    }
});
线程不可重复使用
线程属于“一次性用品”,我们不能通过重新调用一个已经运行结束的线程的start方法来使其重新运行。 事实上,多次调用同一个Thread实例的start方法会导致其抛出IllegalThreadStateException异常。
内存空间分配
首先在Java中,一个线程就是一个对象,对象的创建需要内存空间的分配。 与创建其他类型的Java对象所不同的是,Java虚拟机会为每个线程分配调用栈(Call Stack)所需的内存空间。 调用栈用于跟踪方法间的调用关系以及Java代码对本地代码(Native Code,通常是C代码)的调用。 另外,Java平台中的每个线程可能还有一个内核线程(具体与Java虚拟机的实现有关)与之对应。 因此相对来说,创建线程对象比创建其他类型的对象的成本要高一些。
执行线程与当前线程
Java中的任意一段代码(比如一个方法)总是由确定的线程负责执行的,这个线程就相应地被称为这段代码的执行线程; 任意一段代码都可以通过调用Thread.currentThread()来获取这段代码的执行线程,这个线程就被称为当前线程。

1.3 线程(Thread)的属性

线程的属性包括线程的编号(Id)、名称(Name)、线程类别(Daemon)和优先级(Priority)。

??属性???? ?类型?? ???????????????用途??????????????? 只读? ???????????????????????????????说明????????????????????????????????
编号(ID)??
??????????
??????????
??????????
??????????
long???
???????
???????
???????
???????
用于标识不同的线程,不同的线程拥有
不同的编号????????????????????????
??????????????????????????????????
??????????????????????????????????
??????????????????????????????????
是???
?????
?????
?????
?????
某个编号的线程运行结束后,该编号可能被后续创建的线程使用。不同线程?
拥有的编号虽然不同,但是这种编号的唯一性只在Java虚拟机的一次运行有?
效。也就是说重启个Java虚拟机(如重启Web服务器)后,某些线程的编号可能
与上次Java虚拟机运行的某个线程的编号一样,因此该属性的值不适合用作?
某种唯一标识,特别是作为数据库中的唯一标识(如主键)?????????????????
名称??????
(Name)????
??????????
String?
???????
???????
用于区分不同的线程,默认值与线程的
编号有关,默认值的格式为:“Thread-?
线程编号”,如“Thread-0”???????????
否???
?????
?????
Java并不禁止我们将不同的线程的名称属性设置为相同的值,尽管如此,设?
置线程的名称属性有助于代码调试和问题定位???????????????????????????
???????????????????????????????????????????????????????????????????
线程类别??
(Daemon)??
??????????
??????????
boolean
???????
???????
???????
值为tnue表示相应的线程为守护线程,
否则表示相应的线程为用户线程。该属
性的默认值与相应线程的父线程的该属
性的值相同????????????????????????
否???
?????
?????
?????
该属性必须在相应线程启动之前设置,即对setDaemon方法的调用必须在对??
start方法的调用之前,否则setDaemon方法会抛出???????????????????????
IllegalThreadStateException异常。负责一些关键任务处理的线程不适宜设
置为守护线程???????????????????????????????????????????????????????
优化级????
(Priority)
??????????
??????????
??????????
??????????
??????????
int????
???????
???????
???????
???????
???????
???????
该属性本质上是给线程调度器的提示,
用于表示应用程序希望哪个线程能够优
先得以运行。Java定义了1~10的10个优
先级,默认值一般为5(表示普通优先级
)。对于具体的一个线程而言,其优先?
级的默认值与其父线程(创建该线程的?
线程)的优先级值相等。?????????????
否???
?????
?????
?????
?????
?????
?????
一般使用默认优先级即可,不恰当地设置该属性值可能导致严重的问题(线程
饥饿)??????????????????????????????????????????????????????????????
???????????????????????????????????????????????????????????????????
???????????????????????????????????????????????????????????????????
???????????????????????????????????????????????????????????????????
???????????????????????????????????????????????????????????????????
???????????????????????????????????????????????????????????????????
线程属性的使用
线程的属性除了编号外,其他属性都是可读写的属性,即Thread类提供了相应的get方法和set方法用于读取或者设置相应的属性。
Thread.currentThread().getName();
优先级的设定
Java线程的优先级属性本质上只是一个给线程调度器的提示信息,以便于线程调度器决定优先调度哪些线程运行。 它并不能保证线程按照其优先级高低的顺序运行。另外,Java线程的优先级使用不当或者滥用则可能导致某些线程永远无法得到运行, 即产生了线程饥钱(Thread Starvation)。因此,线程的优先级并不是设置得越高越好; 一般情况下使用普通优先级即可,即不必设置线程的优先级属性。
用户线程与守护线程
按照线程是否会阻止Java虚拟机正常停止,我们可以将Java中的线程分为守护线程(Daemon Thread)和用户线程 (User Thread,也称非守护线程)。其中用户线程会阻止Java虚拟机的正常停止, 即一个Java虚拟机只有在其所有用户线程都运行结束的情况下才能正常停止; 而守护线程则不会影响Java虚拟机的正常停止,即应用程序中有守护线程在运行也不影响虚拟机的正常停止。 因此,守护线程通常用于执行一些重要性不是很高的任务,例如用于监视其他线程的运行情况。

1.4 Thread类的常用方法

???????方法??????? ??????????????????功能?????????????????? ????????????????????????????????备注????????????????????????????????
static?Thread?????
currentThread()???
返回当前线程,即当前代码的执行线程(对象)
????????????????????????????????????????
同一段代码在不同时刻对Thread.currentThread的调用,其返回值可能不同???
????????????????????????????????????????????????????????????????????
void?run()???????? 线程的任务处理逻辑?????????????????????? 该方法是由Java虚拟机直接调用的,一般情况下应用程序不应该调用该方法??
void?start()??????
??????????????????
启动线程????????????????????????????????
????????????????????????????????????????
该方法的返回并不代表相应的线程已经被启动;一个Thread实例的start方法?
只能够被调用一次,多次调用会抛出异常????????????????????????????????
void?join()??????? 等待线程运行结束???????????????????????? 线程A调用线程B的join方法,那么线程A的运行会被暂停,直到线程B运行结束
static?void???????
yield()???????????
使当前线程主动放弃其对处理器的占用,这可
能导致当前线程被暂停????????????????????
这个方法是不可靠的,该方法被调用时当前线程可能仍然继续运行(视系统当?
前的运行状况而定)???????????????????????????????????????????????????
static?void???????
sleep(long?millis)
使当前线程休眠(暂停运行)指定的时间??????
????????????????????????????????????????
????????????????????????????????????????????????????????????????????
????????????????????????????????????????????????????????????????????

1.5 线程的层次关系

父线程与子线程
Java平台中的线程不是孤立的,线程与线程之间总是存在一些联系。假设线程A所执行的代码创建了线程B, 那么,习惯上我们称线程B为线程A的子线程,相应地线程A就被称为线程B的父线程。不过Java平台中并没有API用于获取一个线程的父线程, 或者获取一个线程的所有子线程。
父子线程的Daemon值
默认情况下,父线程是守护线程,则子线程也是守护线程,父线程是用户线程,则子线程也是用户线程。 另外,父线程在创建子线程后启动子线程之前可以调用该线程的setDaemon方法,将相应的线程设置为守护线程(或者用户线程)。
父子线程的优先级
一个线程的优先级默认值为该线程的父线程的优先级,即如果我们没有设置或者更改一个线程的优先级, 那么这个线程的优先级的值与父线程的优先级的值相等。
父子线程的生命周期
父线程和子线程之间的生命周期也没有必然的联系。比如父线程运行结束后,子线程可以继续运行, 子线程运行结束也不妨碍其父线程继续运行。
工作者线程
习惯上,我们也称某些子线程为エ作者线程(Worker Thread)或者后台线程(Background Thread)。 工作者线程通常是其父线程创建来用于专门负责某项特定任务的执行的。 例如,Java虚拟机中对内存进行回收的线程通常被称为GC工作者线程。

1.6 线程的生命周期

Figure 1: Java线程的状态

Java线程的状态可以使用监控工具査看,也可以通过Thread.getState()调用来获取。 Thread.getState()的返回值类型Thread.State是一个枚举类型,其定义的线程状态包括以下几种:

NEW
一个已创建而未启动的线程处于该状态。由于一个线程实例只能够被启动次,因此一个线程只可能有一次处于该状态。
RUNNABLE
该状态可以被看成一个复合状态,它包括两个子状态:READY和RUNNING。 前者表示处于该状态的线程可以被线程调度器(Scheduler)进行调度而使之处于RUNNING状态; 后者表示处于该状态的线程正在运行,即相应线程对象的run方法所对应的指令正在由处理器执行。 执行Thread.yield()的线程,其状态可能会由RUNNING转换为READY。处于READY子状态的线程也被称为活跃线程。
BLOCKED
一个线程发起一个阻塞式I/O(Blocking I/O)操作后,或者申请一个由其他线程持有的独占资源(比如锁)时,相应的线程会处于该状态, 处于Blocked状态的线程并不会占用处理器资源。当阻塞式1O操作完成后,或者线程获得了其申请的资源, 该线程的状态又可以转换为RUNNABLE。
Waiting
一个线程执行了某些特定方法之后,就会处于这种等待其他线程执行另外一些特定操作的状态。 能够使其执行线程变更为WAITING状态的方法包括:Object.wait()、Thread.join()和LockSupport.park(Object)。 能够使相应线程从WAITING变更为RUNNABLE的对应方法包括:Object.notify()、notifyAll()和LockSupport.unpark(Objec)
TIMEDWAITING
该状态和WAITING类似,差别在于处于该状态的线程并非无限制地等待其他线程执行特定操作,而是处于带有时间限制的等待状态。 当其他线程没有在指定时间内执行该线程所期望的特定操作时,该线程的状态自动转换为RUNNABLE。
TERMINATED
已经执行结束的线程处于该状态。由于一个线程实例只能够被启动一次,因此一个线程也只可能有一次处于该状态。 Thread.run()正常返回或者由于抛出异常而提前终止都会导致相应线程处于该状态。

2 多线程编程

2.1 串行、并发与并行

Figure 2: 串行、并发与并行示意图

串行(Sequential)
先开始做事情A,待其完成之后再开始做事情B,依次类推,直到事情C完成。这实际上顺序逐一完成几件事情,只需要投入一个人。 在这种方式下3件事情总共耗时35(15+10+10)分钟。
并发(Concurrent)
这种方式也可以只投入一个人,这个人先开始做事情A,事情A的准备活动做好后(此时消耗了5分钟), 在等待事情A完成的这段时间内他开始做事情B,为事情B的准备活动花了2分钟之后,在等待事情B完成的这段时间内他开始做事情C, 直到10分钟之后事情C完成。这整个过程实际上是以交替的方式利用等待某件事情完成的时间来做其他事情, 在这种方式下3件事情总共耗时17(5+2+10)分钟,这比串行方式节约了一半多的时间。
并行(Parallel)
这种方式需要投入3个人,每个人负责完成其中一件事情,这3个人在同一时刻开始齐头并进地完成这些事情。 在这种方式下3件事情总共耗时15分钟(取决于耗时最长的那件事情所需的时间),比并发的方式节约了2分钟的时间。

并发往往带有部分串行,而并发的极致就是并行。从软件的角度来说,并发就是在一段时间内以交替的方式去完成多个任务, 而并行就是以齐头并进的方式去完成多个任务。

从软件角度讲,要以并发的方式去完成几个任务往往需要借助多个线程(而不是一个线程)。 从硬件的角度来说,在一个处理器一次只能够运行一个线程的情况下,由于处理器可以使用时间片(Time-slice)分配的技术在同一段时间内运行多个线程, 因此一个处理器就可以实现并发。而并行则需要靠多个处理器在同一时刻各自运行一个线程来实现。

多线程编程的实质就是将任务的处理方式由串行改为并发,即实现并发化,以发挥并发的优势。如果一个任务的处理方式可以由串行改为并发(或者并行), 那么我们就称这个任务是可并发化(或者可并行化)的。

2.2 多线程编程中存在的问题

2.2.1 竞态(Race Condition)

概念
竞态是指计算的正确性依赖于相对时间顺序或者线程的交错。竞态往往伴随着读取脏数据问题(即读取到一个过时的数据), 以及丢失更新问题(即一个线程对数据所做的更新没有体现在后续其他线程对该数据的读取上)。
  1. 竞态的出现
    public class Main {
        static class IndexGen {
            private int index;
    
            int nextIndex() {
                return index++;
            }
        }
    
        public static void main(String[] args) throws InterruptedException {
            IndexGen indexGen = new IndexGen();
            final int num = 100;
            final int[] visited = new int[num];
            final List<Thread> threads = new ArrayList<>();
            for (int i = 0; i < num; i++) {
                threads.add(new Thread(() -> visited[indexGen.nextIndex()] ++));
            }
            threads.forEach(Thread::start);
            for (Thread thread : threads) {
                thread.join();
            }
            for (int i = 0; i < 10; i++) {
                for (int j = 0; j < 10; j++) {
                    System.out.printf("%d\t", visited[i * 10 + j]);
                }
                System.out.println();
            }
        }
    }
    

    参考上述程序,我们创建一百个线程,每次通过IndexGen获取下一个index,并将其访问次数加一。我们期望的结果是每个index都只被访问一次, 但多次运行后,却发现可能出现下面的结果,其中,0,62,85被访问了两次,导致97,98,99未被访问:

    2	1	1	1	1	1	1	1	1	1	
    1	1	1	1	1	1	1	1	1	1	
    1	1	1	1	1	1	1	1	1	1	
    1	1	1	1	1	1	1	1	1	1	
    1	1	1	1	1	1	1	1	1	1	
    1	1	2	1	1	1	1	1	1	1	
    1	1	1	1	1	1	1	1	1	1	
    1	1	1	1	1	2	1	1	1	1	
    1	1	1	1	1	1	1	1	1	1	
    1	1	1	1	1	1	1	0	0	0
    

    上述例子中,依照nextIndex()方法实现的逻辑,下标总是递增的,因此不同的线程它们所“拿到”的index也不应该相同才对。 但从结果来看,不同线程却“拿到”了重复的index,即nextIndex()所返回的下标值重复了。且如果我们多次重复运行代码,所得到的结果也不完全相同。 这个现象说明,当多个线程在没有采取任何控制措施的情况下并发地更新、读取同一个共享变量时,是不安全的,可能出现意料之外的结果。

  2. 出现竞态的原因

    上述例子中的 index++ 看起来像是一个操作,实际上相当于如下3个指令:

    1oad(index,r1);  //指令①:将变量index的值从内存读到寄存器r1
    increment(r1);   //指令②:将寄存器r1的值增加1
    store(index,r1); /指令③:将奇存器r1的内容写入变量index所对应的内存空间
    

    多个线程在执行上述指定时,可能交错运行上述三个指令,导致三个线程读取到的index值重复,如下表如示:

    ? thread-0 thread-1 thread-2
    t1 未运行 执行其他操作 执行其他操作
    t2 执行其他操作 [index=0]执行指令① [index=0]执行指令①
    t3 执行其他操作 [r1=0]执行指令② [r1=0]执行指令②
    t4 [index=0]执行指令① [r1=1][index=0]执行指令③ [r1=1][index=0]执行指令③
    t5 [r1=0]执行指令② [index=1]执行其他操作 [index=1]执行其他操作
    t6 [r1=1][index=0]执行指令③ 运行结束 运行结束
    t7 [index=1]执行其他操作 运行结束 运行结束

2.3 多线程编程的优势和风险

2.3.1 优势

提高系统的吞吐率(Throughput)
多线程编程使得一个进程中可以有多个并发(Concurrent,即同时进行的)的操作。 例如,当一个线程因为I/O操作而处于等待时,其他线程仍然可以执行其操作。
提高响应性(Responsiveness)
在使用多线程编程的情况下,对于GUI软件(如桌面应用程序)而言, 一个慢的操作(比如从服务器上下载大文件)并不会导致软件的界面出现被“冻住”而无法响应用户的其他操作的现象; 对于Web应用程序而言,一个请求的处理慢了并不会影响其他请求的处理。
充分利用多核(Multicore)处理器资源
如今多核处理器的设备越来越普及,就算是手机这样的消费类设备也普遍使用多核处理器。 实施恰当的多线程编程有助于我们充分利用设备的多核处理器资源,从而避免了资源浪费。
最小化对系统资源的使用
一个进程中的多个线程可以共享其所在进程所申请的资源(如内存空间), 因此使用多个线程相比于使用多个进程进行编程来说,节约了对系统资源的使用。
简化程序的结构
线程可以简化复杂应用程序的结构。

2.3.2 风险

线程安全(Thread Safe)问题
多个线程共享数据的时候,如果没有采取相应的并发访问控制措施,那么就可能产生数据一致性问题, 如读取脏数据(过期数据)、丢失更新(某些线程所做的更新被其他线程所做的更新覆盖)等。
线程活性(Thread Liveness)问题
一个线程从其创建到运行结束的整个生命周期会经历若干状态。 从单个线程的角度来看,RUNNABLE状态是我们所期望的状态,但实际上, 代码编写不当可能导致某些线程一直处于等待其他线程释放锁的状态(BLOCKED状态),即产生了死锁(Deadlock)。 另外,线程是一种稀缺的计算资源,一个系统所拥有的处理器数量相比于该系统中存在的线程数量而言总是少之又少的, 某些情况下可能出现线程饥饿(Starvation)的问题,即某些线程永远无法获取处理器执行的机会而永远处于RUNNABLE状态的READY子状态。
上下文切换(Context Switch)
处理器从执行一个线程转向执行另外一个线程的时候操作系统所需要做的一个动作被称为上下文切换。 由于处理器资源的稀缺性,因此上下文切换可以被看作多线程编程的必然副产物,它増加了系统的消耗,不利于系统的吞吐率。
可靠性
多线程编程一方面可以有利于可靠性,例如某个线程意外提前终止了,但这并不影响其他线程继续其处理。 另一方面,线程是进程的一个组件,它总是存在于特定的进程中的,如果这个进程由于某种原因意外提前终止, 比如某个Java进程由于内存泄漏导致Java虚拟机崩溃而意外终止,那么该进程中所有的线程也就随之而无法继续运行。 因此,从提高软件可靠性的角度来看,某些情况下可能要考虑多进程多线程的编程方式,而非简单的单进程多线程方式。

Date: 2019-09-03 Tus

Author: C.Wong

Created: 2019-09-05 四 02:27

Validate

$flag 上一页 下一页

上一篇:java-线程池

下一篇:java、tomcat安装