美文网首页
Java并发编程的艺术第四章笔记

Java并发编程的艺术第四章笔记

作者: 逍遥白亦 | 来源:发表于2020-12-22 22:34 被阅读0次

第四章 Java并发编程基础

[toc]

线程简介

定义

  • 进程:进程是程序的一次执行,进程是一个程序及其数据在处理机上顺序执行时所发生的活动,进程是 具有独立功能的程序在一个数据集合上运⾏的过程,它是系统进行资源分配和调度的一个独立单位
  • 线程:操作系统能够进行运算调度的最小单位。它被包含在进程之中,是进程中的实际运作单位

为什么要使用多线程

  • 更多的处理器核心
  • 更快的响应时间
  • 更好的编程模型

优先级

现代操作系统基本采用时分的形式调度运行的线程,操作系统会分出一个个时间片,线程会分配到若干时间片,当线程的时间片用完了就会发生线程调度,并等待着下次分配。线程 分配到的时间片多少也就决定了线程使用处理器资源的多少,而线程优先级就是决定线程需 要多或者少分配一些处理器资源的线程属性。

在Java线程中,通过一个整型成员变量priority来控制优先级,优先级的范围从1~10,在线程构建的时候可以通过setPriority(int)方法来修改优先级,默认优先级是5,优先级高的线程分 配时间片的数量要多于优先级低的线程。

状态

Java线程一共有六种状态


image

Java线程状态变迁


image

Daemon线程

Daemon线程是一种支持型线程,因为它主要被用作程序中后台调度以及支持性工作。这意味着,当一个Java虚拟机中不存在非Daemon线程的时候,Java虚拟机将会退出。可以通过调用Thread.setDaemon(true)将线程设置为Daemon线程。

注意:Daemon线程的finally代码块不一定会执行

示例代码

package Daemon;

import util.SleepUtils;

public class Daemon {

    public static void main(String[] args) {
        Thread thread = new Thread(new DaemonRunner(), "Daemon");
        thread.setDaemon(true);
        thread.start();
    }

    static class DaemonRunner implements Runnable {
        @Override
        public void run() {
            try {
                System.out.println("start");
                SleepUtils.second(10);
            }finally {
                System.out.println("Daemon");
            }
        }
    }

}
package util;

import java.util.concurrent.TimeUnit;

public class SleepUtils {

    public static void second(long seconds){
        try {
            TimeUnit.SECONDS.sleep(seconds);
        }catch (InterruptedException ignored){

        }
    }

}

启动和终止线程

构造线程

在运行线程之前首先要构造一个线程对象,线程对象在构造的时候需要提供线程所需要的属性,如线程所属的线程组、线程优先级、是否是Daemon线程等信息。

启动线程

线程对象在初始化完成之后,调用start()方法就可以启动这个线程。线程start()方法的含义是:当前线程(即parent线程)同步告知Java虚拟机,只要线程规划器空闲,应立即启动调用start()方法的线程。

注意:启动一个线程前,最好为这个线程设置线程名称,因为这样在使用jstack分析程序或者进行问题排查时,就会给开发人员提供一些提示,自定义的线程最好能够起个名字。

理解中断

中断可以理解为线程的一个标识位属性,它表示一个运行中的线程是否被其他线程进行了中断操作。中断好比其他线程对该线程打了个招呼,其他线程通过调用该线程的interrupt() 方法对其进行中断操作。

线程通过检查自身是否被中断来进行响应,线程通过方法isInterrupted()来进行判断是否被中断,也可以调用静态方法Thread.interrupted()对当前线程的中断标识位进行复位。如果该线程已经处于终结状态,即使该线程被中断过,在调用该线程对象的isInterrupted()时依旧会返回false。

从Java的API中可以看到,许多声明抛出InterruptedException的方法(例如Thread.sleep(longmillis)方法)这些方法在抛出InterruptedException之前,Java虚拟机会先将该线程的中断标识位清除,然后抛出InterruptedException,此时调用isInterrupted()方法将会返回false。

过期的方法

大家对于CD机肯定不会陌生,如果把它播放音乐比作一个线程的运作,那么对音乐播放做出的暂停、恢复和停止操作对应在线程Thread的API就是suspend()、resume()和stop()。

不建议使用的原因主要有:以suspend()方法为例,在调用后,线程不会释放已经占有的资源(比如锁),而是占有着资源进入睡眠状态,这样容易引发死锁问题。同样,stop()方法在终结一个线程时不会保证线程的资源正常释放,通常是没有给予线程完成资源释放工作的机会, 因此会导致程序可能工作在不确定状态下。

安全地终止线程

中断状态是线程的一个标识位,而中断操作是一种简便的线程间交互方式,而这种交互方式最适合用来取消或停止任务。除了中断以外,还可以利用一个boolean变 量来控制是否需要停止任务并终止该线程。

线程间通信

线程开始运行,拥有自己的栈空间,就如同一个脚本一样,按照既定的代码一步一步地执行,直到终止。但是,每个运行中的线程,如果仅仅是孤立地运行,那么没有一点儿价值,或者 说价值很少,如果多个线程能够相互配合完成工作,这将会带来巨大的价值。

volatile和synchronized关键字

具体见volatile和synchronized原理及应用

等待/通知机制

定义

前者是生产者,后者就是消费者,这种模式隔离了“做什么”(what)和“怎么做”(How),在功能层面上实现了解耦,体系结构上具备了良好的伸缩性,

Java中相关方法

等待/通知的相关方法是任意Java对象都具备的,因为这些方法被定义在所有对象的超类 java.lang.Object上。
[图片上传失败...(image-3ed21-1608647593453)]

实现思路

等待/通知机制,是指一个线程A调用了对象O的wait()方法进入等待状态,而另一个线程B调用了对象O的notify()或者notifyAll()方法,线程A收到通知后从对象O的wait()方法返回,进而执行后续操作。上述两个线程通过对象O来完成交互,而对象上的wait()和notify/notifyAll()的 关系就如同开关信号一样,用来完成等待方和通知方之间的交互工作。

具体代码

等待/通知的经典范式

等待方遵循如下原则。

  • 获取对象锁
  • 如果条件不满足,那么调用对象的wait()方法,被通知后仍要检查条件
  • 条件满足则执行对应逻辑

具体伪代码

synchronized(对象) { 
    while(条件不满足) { 
        对象.wait(); 
    }
    对应的处理逻辑 
}

通知方遵循如下原则

  • 获取对象锁
  • 改变条件
  • 通知所有等待在对象上的线程

具体伪代码

synchronized(对象) { 
    改变条件 
    对象.notifyAll(); 
}

管道输入/输出流

管道输入/输出流和普通的文件输入/输出流或者网络输入/输出流不同之处在于,它主要用于线程之间的数据传输,而传输的媒介为内存。

管道输入/输出流主要包括了如下4种具体实现:PipedOutputStream、PipedInputStream、PipedReader和PipedWriter,前两种面向字节,而后两种面向字符。

Thread.join()的使用

含义

如果一个线程A执行了thread.join()语句,其含义是:当前线程A等待thread线程终止之后才从thread.join()返回。

具体代码

package TestJoin;

import java.util.concurrent.TimeUnit;

public class Join {

    public static void main(String[] args) throws InterruptedException {
        Thread previous = Thread.currentThread();
        for (int i=0; i<10; i++){
            Thread thread = new Thread(new Domino(previous),String.valueOf(i));
            thread.start();
            previous = thread;
        }
        TimeUnit.SECONDS.sleep(5);
        System.out.println(Thread.currentThread().getName() + " terminate.");
    }

    static class Domino implements Runnable {

        private Thread thread;

        public Domino(Thread thread) {
            this.thread = thread;
        }

        @Override
        public void run() {
            try {
                thread.join();
            }catch (InterruptedException ignored){

            }
            System.out.println(Thread.currentThread().getName() + " terminate.");
        }
    }

}

输出如下

main terminate.
0 terminate.
1 terminate.
2 terminate.
3 terminate.
4 terminate.
5 terminate.
6 terminate.
7 terminate.
8 terminate.
9 terminate.

从上述输出可以看到,每个线程终止的前提是前驱线程的终止,每个线程等待前驱线程终止后,才从join()方法返回,这里涉及了等待/通知机制(等待前驱线程结束,接收前驱线程结束通知)。

源码

public final synchronized void join(long millis)
    throws InterruptedException {
        long base = System.currentTimeMillis();
        long now = 0;

        if (millis < 0) {
            throw new IllegalArgumentException("timeout value is negative");
        }

        if (millis == 0) {
            while (isAlive()) {
                wait(0);
            }
        } else {
            while (isAlive()) {
                long delay = millis - now;
                if (delay <= 0) {
                    break;
                }
                wait(delay);
                now = System.currentTimeMillis() - base;
            }
        }
    }

ThreadLocal的使用

定义

ThreadLocal,即线程变量,是一个以ThreadLocal对象为键、任意对象为值的存储结构。这个结构被附带在线程上,也就是说一个线程可以根据一个ThreadLocal对象查询到绑定在这个 线程上的一个值。

相关文章

网友评论

      本文标题:Java并发编程的艺术第四章笔记

      本文链接:https://www.haomeiwen.com/subject/biyenktx.html