Java并发编程基础(一篇入门)

1 并发编程简介

1.1 什么是并发编程

所谓并发编程是指在一台处理器上 “同时” 处理多个任务。并发是在同一实体上的多个事件。多个事件在同一时间间隔发生。

并发编程,从程序设计的角度来说,是希望通过某些机制让计算机可以在一个时间段内,执行多个任务。从计算机 CPU 硬件层面来说,是一个或多个物理 CPU 在多个程序之间多路复用,提高对计算机资源的利用率。从调度算法角度来说,当任务数量多于 CPU 的核数时,并发编程能够通过操作系统的任务调度算法,实现多个任务一起执行。

1.2 并发编程的重要性

对于一个 Java 程序员而言,能否熟练掌握并发编程是判断他优秀与否的重要标准之一。因为并发编程是 Java 语言中最为晦涩的知识点,它涉及操作系统、内存、CPU、编程语言等多方面的基础能力,更为考验一个程序员的内功。

1.3 并发编程的特性

并发编程有三大特性:

  • 原子性;
  • 可见性;
  • 有序性。

2 操作系统的并发

2.1 并发编程的定义

定义: 并发编程是指在一台处理器上 “同时” 处理多个任务。并发是在同一实体上的多个事件,多个事件在同一时间间隔发生

意义:开发者通过使用不同的语言,实现并发编程,充分的利用处理器(CPU)的每一个核,以达到最高的处理性能,提升服务器的资源利用率,提升数据的处理速度。

2.2 从 CPU 谈并发编程

下图展示了最简单的 CPU 核心通过缓存与主存进行通讯的模型。
Java并发编程基础(一篇入门)_第1张图片
在缓存出现后不久,系统变得越来越复杂,缓存与主存之间的速度差异被拉大,由于 CPU 的频率太快了,快到主存跟不上,这样在线程处理器时钟周期内,CPU 常常需要等待主存,这样就会浪费资源。从我们的感官上,计算机可以同时运行多个任务,但是从 CPU 硬件层面上来说,其实是 CPU 执行线程的切换,由于切换频率非常快,致使我们从感官上感觉计算机可以同时运行多个程序。

为了避免长时间的线程等待,我们一方面提升硬件指标(如多级高速缓存的诞生,这里不做讨论),另一方面引入了并发概念,充分的利用处理器(CPU)的每一个核,减少 CPU 资源等待的时间,以达到最高的处理性能。

2.3 操作系统,进程,线程之间的联系与区别

操作系统是包含多个进程的容器,而每个进程又是容纳多个线程的容器。
Java并发编程基础(一篇入门)_第2张图片
a. 什么是进程?
官方定义: 进程是计算机中的程序关于某数据集合上的一次运行活动,是系统进行资源分配和调度的基本单位。

Tips:操作系统分配的资源和调度对象其实就是 CPU 时间片。

b. 什么是线程?
官方定义: 线程是操作系统能够进行资源调度的最小单位,它被包含在进程之中,是进程中的实际运作单位,每个线程执行的都是进程代码的某个片段,特定的线程总是在执行特定的任务。

c. 线程与进程的区别?
诞生起源:先有进程,后有线程。进程由于资源利用率、公平性和便利性诞生,线程则是为了提高 CPU 的利用率、提高程序的执行效率而诞生。

概念:进程是资源分配的最小单位。 线程是程序执行的最小单位(线程是操作系统能够进行资源调度的最小单位,同个进程中的线程也可以被同时调度到多个 CPU 上运行),线程也被称为轻量级进程;

内存共享:默认情况下,进程的内存无法与其他进程共享(进程间通信通过 IPC 进行)。 线程共享由操作系统分配给其父进程的内存块。

2.4 串行,并行与并发

串行:顺序执行,按步就搬。在 A 任务执行完之前不可以执行 B。

并行:同时执行,多管齐下。指两个或两个以上事件或活动在同一时刻发生。在多道程序环境下,并行性使多个程序同一时刻可在不同 CPU 核心上同时执行。

并发:穿插执行,减少等待。指多个线程轮流穿插着执行,并发的实质是一个物理 CPU 在若干道程序之间多路复用,其目的是提高有限物理资源的运行效率。
Java并发编程基础(一篇入门)_第3张图片

3 Java 线程内存模型

3.1 什么是 Java 的内存模型

Java 内存模型(即 Java Memory Model,简称 JMM)本身是一种抽象的概念,并不真实存在,它描述的是一组规则或规范,通过这组规范定义了程序中各个变量(包括实例字段,静态字段和构成数组对象的元素)的访问方式。

3.2 Java 线程的私有内存和主内存

下图展示了Java 的内存模型。
Java并发编程基础(一篇入门)_第4张图片
工作内存(私有):由于JVM 运行程序的实体是线程,而每个线程创建时 JVM 都会为其创建一个工作内存(栈区和PC寄存器),用于存储线程私有的数据。线程私有的数据只能供自己使用,其他线程不能够访问到当前线程私有的内存空间,保证了不同的线程在处理自己的数据时,不受其他线程的影响。

主内存(共享):Java 内存模型中规定所有变量都存储在主内存(堆区和方法区),主内存是共享内存区域,所有线程都可以访问。从上图中可以看到,Java 的并发内存模型与操作系统的 CPU 运行方式极其相似,这就是 Java 的并发编程模型。通过创建多条线程,并发的进行操作,充分利用系统资源,达到高效的并发运算。

关于工作内存和主内存,详见JVM运行时数据区。

3.3 主内存操作共享变量需要注意的事项

  • 确定是否是多线程环境:多线程环境下操作共享变量需要考虑线程的安全性;
  • 确定是否有增删改操作:多线程环境下,如果对共享数据有增加,删除或者修改的操作,需要谨慎。为了保证线程的同步性,必须对该共享数据进行加锁操作,保证多线程环境下,所有的线程能够获取到正确的数据;
  • 多线程下的读操作:如果是只读操作,对共享数据不需要进行锁操作,因为数据本身未发生增删改操作,不会影响获取数据的准确性。

4 Java 多线程的创建

4.1 Thread 类介绍

位于 java.lang 包下的 Thread 类是非常重要的线程类。学习 Thread 类的使用是学习多线程并发编程的基础。它实现了 Runnable 接口,其包集成结构如下图所示。
Java并发编程基础(一篇入门)_第5张图片

Thread 类的常用方法介绍:

方法 作用
start() 启动当前的线程,调用当前线程的 run ()
run() 通常需要重写 Thread 类中的此方法,将创建要执行的操作声明在此方法中。
currentThread() 静态方法,返回代码执行的线程。
getName() 获取当前线程的名字。
setName() 设置当前线程的名字。
sleep(long millitime) 让当前进程睡眠指定的毫秒数,在指定时间内,线程是阻塞状态。
isAlive() 判断进程是否存活。
wait() 线程等待。
notify() 线程唤醒。

4.2 多线程的三种创建方式

Java 多线程有 3 种创建方式如下:

  • 方式一:继承 Thread 类的方式创建线程;
  • 方式二:实现 java.lang.Runnable 接口;
  • 方式三:实现 Callable 接口。

4.3 多线程实现之继承 Thread 类

  • 步骤 1:继承 Thread 类 extends Thread;
  • 步骤 2:复写 run () 方法,run () 方法是线程具体逻辑的实现方法。

实例

/**
 * 方式一:继承Thread类的方式创建线程
 */
public class ThreadExtendTest extends Thread{ //步骤 1
    @Override
    public void run() { //步骤 2
	    //run方法内为具体的逻辑实现
        System.out.println("create thread by thread extend");
    }
    public static void main(String[] args) {
        new ThreadExtendTest(). start();
    }
}

4.4 多线程实现之实现 Runnable 接口

Tips:由于 Java 是面向接口编程,且可进行多接口实现,相比 Java 的单继承特性更加灵活,易于扩展,所以相比方式一,更推荐使用方式二进行线程的创建。

  • 步骤 1:实现 Runnable 接口,implements Runnable;
  • 步骤 2:复写 run () 方法,run () 方法是线程具体逻辑的实现方法。

实例:

/**
 * 方式二:实现java.lang.Runnable接口
 */
public class ThreadRunnableTest implements Runnable{//步骤 1
    @Override
    public void run() {//步骤 2
		//run方法内为具体的逻辑实现
        System.out.println("create thread by runnable implements");
    }
    public static void main(String[] args) {
        new Thread(new ThreadRunnableTest()). start();
   }
}

4.5 多线程实现之实现 Callable 接口

Tips:方式一与方式二的创建方式都是复写 run 方法,都是 void 形式的,没有返回值。但是对于方式三来说,实现 Callable 接口,能够有返回值类型。

  • 步骤 1:实现 Callable 接口,implements Callable;
  • 步骤 2:复写 call () 方法,call () 方法是线程具体逻辑的实现方法。

实例

/**
 * 方式三:实现Callable接口
 */
public class ThreadCallableTest implements Callable<String> {//步骤 1
    @Override
    public String call() throws Exception { //步骤 2
	    //call 方法的返回值类型是 String
	    //call 方法是线程具体逻辑的实现方法
        return "create thread by implements Callable";
    }
    public static void main(String[] args) throws ExecutionException, InterruptedException{
        FutureTask<String> future1 = new FutureTask<String>(new ThreadCallableTest());
        Thread thread1 = new Thread(future1);
        thread1. start();
        System.out.println(future1.get());
    }
}

4.6 匿名内部类创建 Thread

首先确认,这并不是线程创建的第四种方式,先来看如何创建。

实例

Thread t = new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println("通过匿名内部类创建Thread");
            }
        });

从代码中可以看出,还是进行了一个 Runnable 接口的使用,所以这并不是新的 Thread 创建方式,只不过是通过方式二实现的一个内部类创建。

4.7 Thread 编程测验实验

实验目的:对 Thread 的创建方式进行练习,巩固本节重点内容,并在练习的过程中,使用常用的 start 方法和 sleep 方法以及 线程的 setName 方法。

实验步骤

  • a.使用 Runnable 接口创建两条线程 :t1 和 t2;
  • b.设置线程 t1 和 t2 的线程名称分别为 “ThreadOne” 和 “ThreadTwo”;
  • c.线程 t1 执行完 run () 方法后,线程睡眠 5 秒;
  • d.线程 t2 执行完 run () 方法后,线程睡眠 1 秒。

实现:

public class ThreadTest implements Runnable{

    @Override
    public void run() {
        System.out.println("线程:"+Thread.currentThread()+" 正在执行...");
    }
    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(new ThreadTest());
        t1.setName("ThreadOne");
        Thread t2 = new Thread(new ThreadTest());
        t2.setName("ThreadTwo");
        t1. start();
        t1.sleep(5000);
        t2. start();
        t1.sleep(1000);
        System.out.println("线程执行结束。");
    }
}

执行结果:

线程:Thread[ThreadOne,5,main] 正在执行...
线程:Thread[ThreadTwo,5,main] 正在执行...
线程执行结束。

5 多线程 join 方法

5.1 join 方法的作用

方法定义:多线程环境下,如果需要确保某一线程执行完毕后才可继续执行后续的代码,就可以通过使用 join 方法完成这一需求设计。

在项目实践中经常会遇到一个场景,就是需要等待某几件事情完成后主线程才能继续往下执行, 比如多个线程加载资源, 需要等待多个线程全部加载完毕再汇总处理。

Thread 类中有一个 join 方法就可以做这个事情,join 方法是 Thread 类直接提供的。join 是无参且返回值为 void 的方法。
Java并发编程基础(一篇入门)_第6张图片
如上图所示,假如有 3 个线程执行逻辑,线程 1 需要执行5秒钟,线程 2 需要执行10 秒钟,线程 3 需要执行 8 秒钟。 如果我们的开发需求是:必须等 3 条线程都完成执行之后再进行后续的代码处理,这时候我们就需要使用到 join 方法。

使用 join 方法后:

  • 第 5 秒钟: 线程 1 执行完毕;线程 2 执行了一半; 线程 3 还差 3 秒执行完毕;
  • 第 8 秒钟:线程 1 等待了 3 秒; 线程 3 执行完毕; 线程 2 还差 2 秒执行完毕;
  • 第10 秒钟: 线程 1 等待了 5 秒; 线程 3 等待了 2 秒;线程 2 执行完毕;
  • 从线程 2 执行结束的那一刻:三条线程同时进行后续代码的执行。

5.2 join 方法异常处理

join 方法是 Thread 类中的方法,为了了解该方法的异常处理,我们先来简要的看下 join 方法的 JDK 源码:

public final void join() throws InterruptedException {
        join(0);
}

从源代码中我们可以看到, join 方法抛出了异常,所以,我们在使用 join 方法的时候,需要对 join 方法的调用进行 try catch 处理或者从方法级别进行异常的抛出。

try-catch 处理示例

public class DemoTest implements Runnable{
    @Override
    public void run() {
        System.out.println("线程:"+Thread.currentThread()+" 正在执行...");
    }
    public static void main(String[] args) {
        Thread t1 = new Thread(new DemoTest());
        t1. start();
        try {
            t1.join();
        } catch (InterruptedException e) {
            // 异常捕捉处理
        }
    }
}

throws 异常处理示例:

public class DemoTest implements Runnable throws InterruptedException {
    @Override
    public void run() {...}
    public static void main(String[] args) {
        Thread t1 = new Thread(new DemoTest());
        t1. start();
        t1.join();
    }
}

5.3 join 方法如何使用

为了更好的了解 join 方法的使用,我们首先来设计一个使用的场景。

场景设计

  • 线程 1 :执行时间 5 秒钟;
  • 线程 2 :执行时间 10 秒钟;
  • 线程 3 :执行 8 秒钟。

需求:我们需要等 3 个线程都执行完毕后,再进行后续代码的执行。3 个线程执行完毕后,打印执行时间。

期望结果: 10 秒执行时间。

实现:

public class DemoTest{
    public static void main(String[] args) throws InterruptedException {
        Thread threadOne = new Thread(new Runnable() { //线程 1
            @Override
            public void run() {
                try {
                    Thread.sleep (5000 ); //线程 1 休眠 5 秒钟
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println ("线程 1 休眠 5 秒钟,执行完毕。");
            }
        });

        Thread threadTwo = new Thread(new Runnable() { //线程 2
            ...
                    Thread.sleep (10000 ); //线程 2 修眠 10 秒钟
            ...
                System.out.println ("线程 2 修眠 10 秒钟,执行完毕。");
            }
        });

        Thread threadThree = new Thread(new Runnable() {//线程 3
            ...
                    Thread.sleep (8000 ); //线程 3 修眠 8 秒钟
            ...
                System.out.println ("线程 3 修眠 8 秒钟,执行完毕。");
            }
        });

        Long startTime = System.currentTimeMillis();
        threadOne. start();threadTwo. start();threadThree. start();
        System.out.println("等待三个线程全部执行完毕再继续向下执行,我要使用 join 方法了。");
        threadOne.join(); //线程 1 调用 join 方法
        threadTwo.join(); //线程 2 调用 join 方法
        threadThree.join(); //线程 3 调用 join 方法
        Long endTime = System.currentTimeMillis();
        System.out.println("三个线程都执行完毕了,共用时: "+ (endTime - startTime) + "毫秒");
    }
}

执行结果验证:

等待三个线程全部执行完毕再继续向下执行,我要使用 join 方法了。
线程 1 休眠 5 秒钟,执行完毕。
线程 3 修眠 8 秒钟,执行完毕。
线程 2 修眠 10 秒钟,执行完毕。
三个线程都执行完毕了,共用时: 10002毫秒

从执行的结果来看,与我们对 join 方法的理解和分析完全相符。

5.4 带参数的 join 方法使用

除了无参的 join 方法以外, Thread 类还提供了有参 join 方法如下:

public final synchronized void join(long millis)
    throws InterruptedException

该方法的参数 long millis 代表的是毫秒时间。

方法作用:等待 millis 毫秒终止线程,假如这段时间内该线程还没执行完,也不会再继续等待。

上面的代码里,我们都是调用的无参 join 方法,现在对上一个知识点代码进行如下调整:

threadOne.join(); //线程 1 调用 join 方法
threadTwo.join(3000); //线程 2 调用 join 方法
threadThree.join(); //线程 3 调用 join 方法

可以看到,线程 2 使用 join 方法 3000 毫秒的等待时间,如果 3000 毫秒后,线程 2 还未执行完毕,那么主线程则放弃等待线程 2,只关心线程 1 和线程 3。

查看执行结果

等待三个线程全部执行完毕再继续向下执行,我要使用 join 方法了。
线程 1 休眠 5 秒钟,执行完毕。
线程 3 修眠 8 秒钟,执行完毕。
三个线程都执行完毕了,共用时: 8000毫秒
线程 2 修眠 10 秒钟,执行完毕。

从执行结果来看, 总用时 8000 毫秒,因为线程 2 被放弃等待了,所以只考虑线程 1 和线程 3 的执行时间即可。

6 多线程 yield 方法

6.1 什么是 CPU 执行权

我们知道操作系统是为每个线程分配一个时间片来占有 CPU 的,正常情况下当一个线程把分配给自己的时间片使用完后,线程调度器才会进行下一轮的线程调度,这里所说的 “自己占有的时间片” 就是 CPU 分配给线程的执行权。

进一步探究,何为让出 CPU 执行权呢?

当一个线程通过某种可行的方式向操作系统提出让出 CPU 执行权时,就是在告诉线程调度器自己占有的时间片中还没有使用完的部分自己不想使用了,主动放弃剩余的时间片,并在合适的情况下,重新获取新的执行时间片。

6.2 yield 方法的作用

方法介绍:Thread 类中有一个静态的 yield 方法,当一个线程调用 yield 方法时,实际就是在暗示线程调度器当前线程请求让出自己的 CPU 使用权。

public static native void yield();

Tips:从这个源码中我们能够看到如下两点要点:

  • yield 方法是一个静态方法,静态方法的特点是可以由类直接进行调用,而不需要进行对象 new 的创建,调用方式为 Thread.yield ()。
  • 该方法除了被 static 修饰,还被 native 修饰,当一个方法被native修饰时,表示该方法的实现由非Java语言实现。

作用:暂停当前正在执行的线程对象(及放弃当前拥有的 cup 资源),并执行其他线程。yield () 做的是让当前运行线程回到就绪状态,以允许具有相同优先级的其他线程获得运行机会。

目的:yield 即 “谦让”,使用 yield () 的目的是让具有相同优先级的线程之间能适当的轮转执行。但是,实际中无法保证 yield () 达到谦让目的,因为放弃 CPU 执行权的线程还有可能被线程调度程序再次选中。

6.3 yield 方法如何使用

为了更好的了解 yield 方法的使用,我们来设计一个使用场景。

场景设计

  • 创建一个线程,线程名为 threadOne;
  • 打印一个数,该数的值为从 1 加到 10000000 的和;
  • 不使用 yield 方法正常执行,记录总的执行时间;
  • 加入 yield 方法,再次执行程序;
  • 再次记录总执行时间。

期望结果: 未加入 yield 方法之前打印的时间 < 加入 yield 方法之后的打印时间。因为 yield 方法在执行过程中会放弃 CPU 执行权并从新获取新的 CPU 执行权。

代码实现 - 正常执行:

public class DemoTest extends Thread {
    @Override
    public void run() {
        Long start = System.currentTimeMillis();
        int count = 0;
        for (int i = 1; i <= 10000000; i++) {
             count = count + i;
        }
        Long end = System.currentTimeMillis();
        System.out.println("总执行时间: "+ (end-start) + " 毫秒, 结果 count = " + count);
    }

    public static void main(String[] args) throws InterruptedException {
        DemoTest threadOne = new DemoTest();
        threadOne. start();
    }
}

执行结果验证:

总执行时间: 6 毫秒.

代码实现 - yield 执行:

public class DemoTest extends Thread {
    @Override
    public void run() {
        Long start = System.currentTimeMillis();
        int count = 0;
        for (int i = 1; i <= 10000000; i++) {
             count = count + i;
             this.yield(); // 加入 yield 方法
        }
        Long end = System.currentTimeMillis();
        System.out.println("总执行时间: "+ (end-start) + " 毫秒. ");
    }

    public static void main(String[] args) throws InterruptedException {
        DemoTest threadOne = new DemoTest();
        threadOne. start();
    }
}

执行结果验证:

总执行时间: 5377 毫秒.

从执行的结果来看,与我们对 yield 方法的理解和分析完全相符,当加入 yield 方法执行时,线程会放弃 CPU 的执行权,并等待再次获取新的执行权,所以执行时间上会更加的长。

6.4 yield 方法和 sleep 方法的区别

  • sleep () 方法给其他线程运行机会时不考虑线程的优先级,因此会给低优先级的线程以运行的机会;
  • yield () 方法只会给相同优先级或更高优先级的线程以运行的机会;
  • 线程执行 sleep () 方法后转入阻塞 (blocked) 状态,而执行 yield () 方法后转入就绪 (ready) 状态;
  • sleep () 方法声明会抛出 InterruptedException, 而 yield () 方法没有声明任何异常;
  • sleep () 方法比 yield () 方法具有更好的移植性 (跟操作系统 CPU 调度相关)。

7 线程上下文切换与死锁

7.1 理解线程的上下文切换

定义:当前线程使用完时间片后,就会处于就绪状态并让出 CPU,让其他线程占用,这就是上下文切换,从当前线程的上下文切换到了其他线程。

问题点解析:让出 CPU 的线程等下次轮到自己占有 CPU 时如何知道自己之前运行到哪里了?所以在切换线程上下文时需要保存当前线程的执行现场, 当再次执行时根据保存的执行现场信息恢复执行现场。

线程上下文切换时机: 当前线程的 CPU 时间片使用完或者是当前线程被其他线程中断时,当前线程就会释放执行权。那么此时执行权就会被切换给其他的线程进行任务的执行,一个线程释放,另外一个线程获取,就是我们所说的上下文切换时机。

7.2 什么是线程死锁

定义:死锁是指两个或两个以上的线程在执行过程中,因争夺资源而造成的互相等待的现象,在无外力作用的情况下,这些线程会一直相互等待而无法继续运行下去。
Java并发编程基础(一篇入门)_第7张图片
如上图所示死锁状态,线程 A 己经持有了资源 2,它同时还想申请资源 1,可是此时线程 B 已经持有了资源 1 ,线程 A 只能等待。

反观线程 B 持有了资源 1 ,它同时还想申请资源 2,但是资源 2 已经被线程 A 持有,线程 B 只能等待。所以线程 A 和线程 B 就因为相互等待对方已经持有的资源,而进入了死锁状态。

7.3 线程死锁的必备要素

互斥条件:进程要求对所分配的资源进行排他性控制,即在一段时间内某资源仅为一个进程所占有。此时若有其他进程请求该资源,则请求进程只能等待;

不可剥夺条件:进程所获得的资源在未使用完毕之前,不能被其他进程强行夺走,即只能由获得该资源的进程自己来释放(只能是主动释放,如 yield 释放 CPU 执行权);

请求与保持条件:进程已经保持了至少一个资源,但又提出了新的资源请求,而该资源已被其他进程占有,此时请求进程被阻塞,但对自己已获得的资源保持不放;

循环等待条件:指在发生死锁时,必然存在一个线程请求资源的环形链,即线程集合 {T0,T1,T2,…Tn}中的 T0 正在等待一个 T1 占用的资源,T1 正在等待 T2 占用的资源,以此类推,Tn 正在等待己被 T0 占用的资源。如下图所示:
Java并发编程基础(一篇入门)_第8张图片

7.4 死锁的实现

为了更好的了解死锁是如何产生的,我们首先来设计一个死锁争夺资源的场景。

场景设计:

  • 创建 2 个线程,线程名分别为 threadA 和 threadB;
  • 创建两个资源, 使用 new Object () 创建即可,分别命名为 resourceA 和 resourceB;
  • threadA 持有 resourceA 并申请资源 resourceB;
  • threadB 持有 resourceB 并申请资源 resourceA ;
  • 为了确保发生死锁现象,我们使用 sleep 方法创造该场景;

期望结果:发生死锁,线程 threadA 和 threadB 互相等待。

实现:

public class DemoTest{
    private static  Object resourceA = new Object();//创建资源 resourceA
    private static  Object resourceB = new Object();//创建资源 resourceB

    public static void main(String[] args) throws InterruptedException {
        //创建线程 threadA
        Thread threadA = new Thread(new Runnable() {
            @Override
            public void run() {
                synchronized (resourceA) {
                    System.out.println(Thread.currentThread().getName() + "获取 resourceA。");
                    try {
                        Thread.sleep(1000); // sleep 1000 毫秒,确保此时 resourceB 已经进入run 方法的同步模块
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println(Thread.currentThread().getName() + "开始申请 resourceB。");
                    synchronized (resourceB) {
                        System.out.println (Thread.currentThread().getName() + "获取 resourceB。");
                    }
                }
            }
        });
        threadA.setName("threadA");
        //创建线程 threadB
        Thread threadB = new Thread(new Runnable() { //创建线程 1
            @Override
            public void run() {
                synchronized (resourceB) {
                    System.out.println(Thread.currentThread().getName() + "获取 resourceB。");
                    try {
                        Thread.sleep(1000); // sleep 1000 毫秒,确保此时 resourceA 已经进入run 方法的同步模块
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println(Thread.currentThread().getName() + "开始申请 resourceA。");
                    synchronized (resourceA) {
                        System.out.println (Thread.currentThread().getName() + "获取 resourceA。");
                    }
                }
            }
        });
        threadB.setName("threadB");

        threadA. start();
        threadB. start();
    }
}

代码讲解:

  • 我们首先创建了两个资源 resourceA 和 resourceB;
  • 然后创建了两条线程 threadA 和 threadB。threadA 首先获取了 resourceA ,获取的方式是代码 synchronized (resourceA) ,然后沉睡 1000 毫秒;
  • 在 threadA 沉睡过程中, threadB 获取了 resourceB,然后使自己沉睡 1000 毫秒;
  • 当两个线程都苏醒时,可以确定 threadA 获取了 resourceA,threadB 获取了 resourceB,这就达到了我们做的第一步,线程分别持有自己的资源;
  • 那么第二步就是开始申请资源,threadA 申请资源 resourceB,threadB 申请资源 resourceA ,然而 resourceA 和 resourceB 都被各自线程持有,两个线程均无法申请成功,最终达成死锁状态。

执行结果验证:

threadA 获取 resourceA。
threadB 获取 resourceB。
threadA 开始申请 resourceB。
threadB 开始申请 resourceA。

由结果可知已经出现死锁,threadA 申请 resourceB,threadB 申请 resourceA,但均无法申请成功,死锁得以实验成功。

7.5 如何避免线程死锁

要想避免死锁,只需要破坏掉至少一个形成死锁的必要条件即可。

破坏互斥条件:该条件无法破坏,因为我们用锁本来就是想让他们互斥的(临界资源需要互斥访问)。

破坏请求与保持条件:一次性申请所有的资源。

破坏不剥夺条件:占用部分资源的线程进一步申请其他资源时,如果申请不到,可以主动释放它占有的资源。

破坏循环等待条件:靠按序申请资源来预防。按某一顺序申请资源,释放资源则反序释放。

感兴趣的话还可以了解一下避免死锁的经典算法——银行家算法。

8 守护线程与用户线程

8.1 守护线程与用户线程的定义及区别

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

守护线程定义:所谓守护线程,是指在程序运行的时候在后台提供一种通用服务的线程。比如垃圾回收线程就是一个很称职的守护者,并且这种线程并不属于程序中不可或缺的部分。

当所有的非守护线程结束时,程序也就终止了,同时会杀死进程中的所有守护线程。反过来说,只要任何非守护线程还在运行,程序就不会终止。

用户线程定义:某种意义上的主要用户线程,只要有用户线程未执行完毕,JVM 虚拟机不会退出。

区别: 在本质上,用户线程和守护线程并没有太大区别,唯一的区别就是当最后一个非守护线程结束时,JVM 会正常退出,而不管当前是否有守护线程,也就是说守护线程是否结束并不影响 JVM 的退出。

8.2 守护线程的特点

  • 守护线程是运行在程序后台的线程;
  • 守护线程创建的线程,依然是守护线程;
  • 守护线程不会影响 JVM 的退出,当 JVM 只剩余守护线程时,JVM 进行退出;
  • 守护线程在 JVM 退出时,自动销毁。

8.3 守护线程的创建

创建方式:将线程转换为守护线程可以通过调用 Thread 对象的 setDaemon (true) 方法来实现。

创建细节

  • thread.setDaemon (true) 必须在 thread.start () 之前设置,否则会跑出一个 llegalThreadStateException 异常。你不能把正在运行的常规线程设置为守护线程;
  • 在 Daemon 线程中产生的新线程也是 Daemon 的;
  • 守护线程应该永远不去访问固有资源,如文件、数据库,因为它会在任何时候甚至在一个操作的中间发生中断。

守护线程创建示例:

public class DemoTest {
    public static void main(String[] args) throws InterruptedException {
        Thread threadOne = new Thread(new Runnable() {
            @Override
            public void run() {
                //代码执行逻辑
            }
        });
        threadOne.setDaemon(true); //设置threadOne为守护线程
        threadOne. start();
    }
}

8.4 守护线程与 JVM 的退出实验

为了更好的了解守护线程与 JVM 是否退出的关系,我们首先来设计一个守护线程正在运行,但用户线程执行完毕导致的 JVM 退出的场景。

场景设计:

  • 创建 1 个线程,线程名为 threadOne;
  • run 方法线程 sleep 1000 毫秒后,进行求和计算,求解 1 + 2 + 3 + … + 100 的值;
  • 将线程 threadOne 设置为守护线程;
  • 执行代码,最终打印的结果;
  • 加入 join 方法,强制让用户线程等待守护线程 threadOne;
  • 执行代码,最终打印的结果。

期望结果:

  • 未加入 join 方法之前,threadOne 不能执行求和逻辑,无打印输出,因为 main 函数线程执行完毕后,JVM 退出,守护线程也就随之死亡,无打印结果;
  • 加入 join 方法后,可以打印求和结果,因为 main 函数线程需要等待 threadOne 线程执行完毕后才继续向下执行,main 函数执行完毕,JVM 退出。

Tips:main 函数就是一个用户线程,main 方法执行时,只有一个用户线程,如果 main 函数执行完毕,用户线程销毁,JVM 退出,此时不会考虑守护线程是否执行完毕,直接退出。

代码实现 - 不加入 join 方法:

public class DemoTest {
    public static void main(String[] args){
        Thread threadOne = new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                int sum = 0;
                for (int i = 1; i  <= 100; i++) {
                    sum = sum + i;
                }
                System.out.println("守护线程,最终求和的值为: " + sum);
            }
        });
        threadOne.setDaemon(true); //设置threadOne为守护线程
        threadOne. start();
        System.out.println("main 函数线程执行完毕, JVM 退出。");
    }
}

执行结果验证:

main 函数线程执行完毕, JVM 退出。

从结果上可以看到,JVM 退出了,守护线程还没来得及执行,也就随着 JVM 的退出而消亡了。

代码实现 - 加入 join 方法:

public class DemoTest {
    public static void main(String[] args){
        Thread threadOne = new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                int sum = 0;
                for (int i = 1; i  <= 100; i++) {
                    sum = sum + i;
                }
                System.out.println("守护线程,最终求和的值为: " + sum);
            }
        });
        threadOne.setDaemon(true); //设置threadOne为守护线程
        threadOne. start();
        try {
            threadOne.join(); // 加入join 方法
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("main 函数线程执行完毕, JVM 退出。");
    }
}

执行结果验证:

守护线程,最终求和的值为: 5050
main 函数线程执行完毕, JVM 退出。

从结果来看,守护线程不决定 JVM 的退出,除非强制使用 join 方法使用户线程等待守护线程的执行结果,但是实际的开发过程中,这样的操作是不允许的,因为守护线程,默认就是不需要被用户线程等待的,是服务于用户线程的。

8.4 守护线程的使用场景

  • 为其它线程提供服务支持的情况,可选用守护线程;
  • 根据开发需求,程序结束时,这个线程必须正常且立刻关闭,就可以作为守护线程来使用;
  • 如果一个正在执行某个操作的线程必须要执行完毕后再释放,否则就会出现不良的后果的话,那么这个线程就不能是守护线程,而是用户线程;
  • 正常开发过程中,一般心跳监听,垃圾回收,临时数据清理等通用服务会选择守护线程。

9 ThreadLocal 的使用

9.1 ThreadLocal 概述

诞生:早在 JDK 1.2 的版本中就提供 java.lang.ThreadLocal,ThreadLocal 为解决多线程程序的并发问题提供了一种新的思路。使用这个工具类可以很简洁地编写出优美的多线程程序。

概述:ThreadLocal 很容易让人望文生义,想当然地认为是一个 “本地线程”。其实,ThreadLocal 并不是一个 Thread,而是 Thread 的局部变量,也许把它命名为 ThreadLocalVariable 更容易让人理解一些。

当使用 ThreadLocal 维护变量时,ThreadLocal 为每个使用该变量的线程提供独立的变量副本,所以每一个线程都可以独立地改变自己的副本,而不会影响其它线程所对应的副本。

总体概括:从线程的角度看,目标变量就象是线程的本地变量,这也是类名中 “Local” 所要表达的意思。

9.2 ThreadLocal 的作用与使用场景

作用: ThreadLocal 是 JDK 包提供的,它提供了线程本地变量,也就是如果你创建了一个 ThreadLocal 变量,那么访问这个变量的每个线程都会有这个变量的一个本地副本。当多个线程操作这个变量时,实际操作的是自己本地内存里面的变量,从而避免了线程安全问题。
Java并发编程基础(一篇入门)_第9张图片
ThreadLocal 是线程本地存储,在每个线程中都创建了一个 ThreadLocalMap 对象,每个线程可以访问自己内部 ThreadLocalMap 对象内的 value。通过这种方式,避免资源在多线程间共享。

使用场景: 如为每个线程分配一个 JDBC 连接 Connection。这样就可以保证每个线程的都在各自的 Connection 上进行数据库的操作,不会出现 A 线程关了 B 线程正在使用的 Connection。还有 Session 管理等问题。

9.3 ThreadLocal set 方法

方法介绍:set 方法用于设置 ThreadLocal 变量的值,设置成功后,该变量只能够被当前线程访问,其他线程不可直接访问操作改变量。

实例:

public class DemoTest{
    public static void main(String[] args){
        ThreadLocal<String> localVariable = new ThreadLocal<> () ;
        localVariable.set("Hello World");
    }
}

Tips: set 方法可以设置任何类型的值,无论是 String 类型 ,Integer 类型,Object 等类型,原因在于 set 方法的 JDK 源码实现是基于泛型的实现,此处只是拿 String 类型进行的举例。

实例:

public void set(T value) { // T value , 泛型实现,可以 set 任何对象类型
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null)
            map.set(this, value);
        else
            createMap(t, value);
    }

9.4 ThreadLocal get 方法

方法介绍:get 方法用于获取 ThreadLocal 变量的值,get 方法没有任何入参,直接调用即可获取。

实例:

public class DemoTest{
    public static void main(String[] args){
        ThreadLocal<String> localVariable = new ThreadLocal<> () ;
        localVariable.set("Hello World");
        System.out.println(localVariable.get());
    }
}

结果验证:

Hello World

探究-观察如下程序:

public class DemoTest{
    public static void main(String[] args){
        ThreadLocal<String> localVariable = new ThreadLocal<> () ;
        localVariable.set("Hello World");
        localVariable.set("World is beautiful");
        System.out.println(localVariable.get());
        System.out.println(localVariable.get());
    }
}

探究解析:

从程序中来看,我们进行了两次 set 方法的使用。

第一次 set 的值为 Hello World ;第二次 set 的值为 World is beautiful。接下来我们进行了两次打印输出 get 方法,那么这两次打印输出的结果都会是 World is beautiful。 原因在于第二次 set 的值覆盖了第一次 set 的值,所以只能 get 到 World is beautiful。

结果验证:

World is beautiful
World is beautiful

总结: ThreadLocal 中只能设置一个变量值,因为多次 set 变量的值会覆盖前一次 set 的值,我们之前提出过,ThreadLocal 其实是使用 ThreadLocalMap 进行的 value 存储,那么多次设置会覆盖之前的 value,这是 get 方法无需入参的原因,因为只有一个变量值。

9.5 ThreadLocal remove 方法

方法介绍:remove 方法是为了清除 ThreadLocal 变量,清除成功后,该 ThreadLocal 中没有变量值。

实例:

public class DemoTest{
    public static void main(String[] args){
        ThreadLocal<String> localVariable = new ThreadLocal<> () ;
        localVariable.set("Hello World");
        System.out.println(localVariable.get());
        localVariable.remove();
        System.out.println(localVariable.get());
    }
}

Tips: remove 方法同 get 方法一样,是没有任何入参的,因为 ThreadLocal 中只能存储一个变量值,那么 remove 方法会直接清除这个变量值。

结果验证:

Hello World
Null

9.6 多线程下的 ThreadLocal

对 ThreadLocal 的常用方法我们已经进行了详细的讲解,而多线程下的 ThreadLocal 才是它存在的真实意义,那么问了更好的学习多线程下的 ThreadLocal,我们来进行场景的创建,通过场景进行代码实验,更好的体会并掌握 ThreadLocal 的使用。

场景设计:

  • 创建一个全局的静态 ThreadLocal 变量,存储 String 类型变量;
  • 创建两个线程,分别为 threadOne 和 threadTwo;
  • threadOne 进行 set 方法设置,设置完成后沉睡 5000 毫秒,苏醒后进行 get 方法打印;
  • threadTwo 进行 set 方法设置,设置完成后直接 get 方法打印,打印完成后调用 remove 方法,并打印 remove 方法调用完毕语句;
  • 开启线程 threadOne 和 threadTwo ;
  • 执行程序,并观察打印结果。

预期结果:在 threadOne 设置成功后进入了 5000 毫秒的休眠状态,此时由于只有 threadTwo 调用了 remove 方法,不会影响 threadOne 的 get 方法打印,这体现了 ThreadLocal 变量的最显著特性,线程私有操作。

实例:

public class DemoTest{
    static ThreadLocal<String> local = new ThreadLocal<>();
    public static void main(String[] args){

        Thread threadOne = new Thread(new Runnable() {
            @Override
            public void run() {
                local.set("threadOne's local value");
                try {
                    Thread.sleep(5000); //沉睡5000 毫秒,确保 threadTwo 执行 remove 完成
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(local.get());
            }
        });
        Thread threadTwo = new Thread(new Runnable() {
            @Override
            public void run() {
                local.set("threadTwo's local value");
                System.out.println(local.get());
                local.remove();
                System.out.println("local 变量执行 remove 操作完毕。");
            }
        });
        threadTwo. start();
        threadOne. start();
    }
}

结果验证:

threadTwo's local value
local 变量执行 remove 操作完毕。
threadOne's local value

从以上结果来看,在 threadTwo 执行完 remove 方法后,threadOne 仍然能够成功打印,这更加证明了 ThreadLocal 的专属特性,线程独有数据,其他线程不可侵犯。

9.7 小结

ThreadLocal 是解决线程安全问题一个很好的思路,它通过为每个线程提供一个独立的变量副本解决了变量并发访问的冲突问题。在很多情况下,ThreadLocal 比直接使用 synchronized 同步机制解决线程安全问题更简单,更方便,且结果程序拥有更高的并发性。

ps:以上内容来自对慕课教程的学习与总结。

你可能感兴趣的:(Java开发,java,多线程,并发)