参考资料
练习题 icon lost
交流讨论
笔记
img lost

文章目录

本系列文章:
  多线程(一)线程与进程、Thread
  多线程(二)Java内存模型、同步关键字
  多线程(三)线程池
  多线程(四)显式锁、队列同步器
  多线程(五)可重入锁、读写锁
  多线程(六)线程间通信机制
  多线程(七)原子操作、阻塞队列
  多线程(八)并发容器
  多线程(九)并发工具类
  多线程(十)多线程编程示例

前言

  • 计算机的组成

      一个程序要运行,首先要被加载到内存,然后数据被运送到CPU的寄存器里。寄存器用来存储数据;PC为程序计数器,用来记录要执行的程序的位置;算术逻辑单元执行具体的计算,然后将结果再传送给内存。
      CPU执行运算的大致过程:CPU读取指令,然后程序计数器存储程序的执行位置,然后从寄存器中读取原始数据,计算完成后,再将结果返回给内存,一直循环下去。
      线程之间的调度由线程调度器负责,确定在某一时刻运行哪个线程。
      线程上下文切换,简单来说,指的是CPU保存现场,执行新线程,恢复现场,继续执行原线程的一个过程

一、初识多线程

1.1 并行、并发、串行

  • 并发
      多个任务在同一个CPU核上,按细分的时间片轮流执行,从逻辑上来看任务是同时执行。
  • 并行
      单位时间内,多个处理器或多核处理器同时处理多个任务,是真正意义上的“同时进行”。
  • 串行
      有n个任务,由一个线程按顺序执行。由于任务、方法都在一个线程执行所以不存在线程不安全情况,也就不存在临界区的问题。

  图示:

  可以看出:串行是利用一个资源,依次、首尾相接地把不同的事情做完;并发也是利用一个资源,在做一件事时的空闲时间去做另一件事;并行是投入多个资源,去做多件事。
  多线程编程的实质就是将任务的处理方式由串行改成并发

1.2 并发编程的优缺点

1.2.1 并发编程的优点

  • 1、充分利用多核CPU的计算能力
      可以真正发挥出多核 CPU 的优势来,达到充分利用 CPU 的目的,采用多线程的方式去同时完成几件事情而不互相干扰。
  • 2、方便进行业务拆分,提升应用性能
      多线程并发编程是开发高并发系统的基础,利用好多线程机制可以大大提高系统整体的并发能力以及性能。

1.2.2 并发编程的缺点

  • 1、频繁的上下文切换
      时间片是CPU分配给各个线程的时间,因为时间非常短,所以CPU不断通过切换线程,达到一种"不同应用似乎是同时运行的错觉",时间片一般是几十毫秒。每次切换时,需要保存当前的状态起来,以便能够进行恢复先前状态,而这个切换时非常损耗性能,过于频繁反而无法发挥出多线程编程的优势。
  • 2、产生线程安全问题
      即死锁、线程饥饿等问题。

1.3 上下文切换

  一个CPU核心在任意时刻只能被一个线程使用,为了让这些线程都能得到有效执行,CPU采取的策略是交替地为每个线程分配时间片。当一个线程的时间片用完的时候就会重新处于就绪状态让给其他线程使用,这个过程就是上下文切换
  概括来说:当前任务在执行完CPU时间片切换到另一个任务之前会先保存自己的状态,以便下次再切换回这个任务时,可以再加载这个任务的状态。任务从保存到再加载的过程就是一次上下文切换
  在时间片切换到别的任务和切换到当前任务的时候,操作系统需要保存和恢复相应线程的进度信息。这个进度信息就是上下文,它一般包括通用寄存器的内容和程序计数器的内容。
  使用vmstat可以测量上下文切换的次数。示例:

$ vmstat 1
procs -----------memory---------- ---swap-- -----io---- --system-- -----cpu-----
r b swpd free buff cache si so bi bo in cs us sy id wa st
0 0 0 127876 398928 2297092 0 0 0 4 2 2 0 0 99 0 0
0 0 0 127868 398928 2297092 0 0 0 0 595 1171 0 1 99 0 0
0 0 0 127868 398928 2297092 0 0 0 0 590 1180 1 0 100 0 0
0 0 0 127868 398928 2297092 0 0 0 0 567 1135 0 1 99 0 0

  CS(Content Switch)表示上下文切换的次数,例子中的上下文每1秒切换1000多次。

1.3.1 上下分切换的分类

  上下文切换可以分为自发性上下文切换和非自发性上下文切换(通常说的上下文切换指的是第一种):

类型含义原因
自发性上下文切换由于自身因素导致的切出Thread.sleep(long mills);
Object.wait();
Thread.yiels();
Thread.join();
LockSupport.park();
线程发起了IO操作;
等待其他线程持有的锁 。
非自发性上下文切换由于线程调度器的原因被迫切出当前线程的时间片用完;
有一个比当前线程优先级更高的线程需要运行;
Java虚拟机的垃圾回收动作。

1.3.2 减少上下文切换的方式

  • 1、无锁并发编程
      类似ConcurrentHashMap锁分段的思想,不同的线程处理不同段的数据,这样在多线程竞争的条件下,可以减少上下文切换的时间。
  • 2、CAS算法
      利用Atomic下使用CAS算法来更新数据,使用了乐观锁,可以有效的减少一部分不必要的锁竞争带来的上下文切换。
  • 3、使用最少线程
      避免创建不需要的线程,比如任务很少,但是创建了很多的线程,这样会造成大量的线程都处于等待状态。
  • 4、协程
      在单线程里实现多任务的调度,并在单线程里维持多个任务间的切换。

1.3.3 上下文切换的优化示例

  • 1、用jstack命令dump线程信息
      此处查看pid为3117的进程:
sudo -u admin /opt/ifeve/java/bin/jstack 31177 > /home/tengfei.fangtf/dump17
  • 2、统计所有线程分别处于什么状态
      发现300多个线程处于WAITING(onobject-monitor)状态:
[tengfei.fangtf@ifeve ~]$ grep java.lang.Thread.State dump17 | awk '{print $2$3$4$5}'
| sort | uniq -c
39 RUNNABLE
21 TIMED_WAITING(onobjectmonitor)
6 TIMED_WAITING(parking)
51 TIMED_WAITING(sleeping)
305 WAITING(onobjectmonitor)
3 WAITING(parking)
  • 3、打开dump文件查看处于WAITING(onobjectmonitor)的线程在做什么
      发现这些线程基本全是JBOSS的工作线程,在await。说明JBOSS线程池里线程接收到的任务太少,大量线程都闲着:
"http-0.0.0.0-7001-97" daemon prio=10 tid=0x000000004f6a8000 nid=0x555e in
Object.wait() [0x0000000052423000]
java.lang.Thread.State: WAITING (on object monitor)
at java.lang.Object.wait(Native Method)
- waiting on <0x00000007969b2280> (a org.apache.tomcat.util.net.AprEndpoint$Worker)
at java.lang.Object.wait(Object.java:485)
at org.apache.tomcat.util.net.AprEndpoint$Worker.await(AprEndpoint.java:1464)
- locked <0x00000007969b2280> (a org.apache.tomcat.util.net.AprEndpoint$Worker)
at org.apache.tomcat.util.net.AprEndpoint$Worker.run(AprEndpoint.java:1489)
at java.lang.Thread.run(Thread.java:662)
  • 4、做出优化
      减少JBOSS的工作线程数,找到JBOSS的线程池配置信息,将maxThreads降到100:
<maxThreads="250" maxHttpHeaderSize="8192"
	emptySessionPath="false" minSpareThreads="40" maxSpareThreads="75"
	maxPostSize="512000" protocol="HTTP/1.1"
	enableLookups="false" redirectPort="8443" acceptCount="200" bufferSize="16384"
	connectionTimeout="15000" disableUploadTimeout="false" useBodyEncodingForURI= "true">
  • 5、验证
      重启JBOSS,再dump线程信息,然后统计WAITING(onobjectmonitor)的线程,发现减少了175个。WAITING的线程少了,系统上下文切换的次数就会少,因为每一次从WAITTING到RUNNABLE都会进行一次上下文的切换。
[tengfei.fangtf@ifeve ~]$ grep java.lang.Thread.State dump17 | awk '{print $2$3$4$5}'
| sort | uniq -c
44 RUNNABLE
22 TIMED_WAITING(onobjectmonitor)
9 TIMED_WAITING(parking)
36 TIMED_WAITING(sleeping)
130 WAITING(onobjectmonitor)
1 WAITING(parking)

1.4 并发编程三要素

  • 线程安全
      一般而言,如果一个类在单线程环境下能正常运行,并且在多线程环境下也能正常运行,那么就称其是线程安全的。
  • 线程安全
      一个类在单线程情况下能正常运行,但在多线程环境下无法正常运行,那么这个类就是非线程安全的。

  线程安全问题概括来说表现为3个方面:原子性、可见性和有序性。

1.4.1 原子性

  • 1、如何理解原子性
      对于涉及共享变量的操作,若该操作从其执行线程以外的任意线程来看是不可分割的,那么该操作就是原子操作,相应地我们称该操作具有原子性
      原子性问题由线程切换导致。
      原子性指的是一个或者多个操作,要么全部执行并且在执行的过程中不被其他操作打断,要么就全部都不执行。
      在理解原子操作时有两点需要注意:
  1. 原子操作是针对共享变量的操作而言的;
  2. 原子操作是在多线程环境下才有意义。

  原子操作的“不可分割”具有两层含义:

  • 1、访问(读、写)某个共享变量的操作,从其执行线程以外的任何线程来看,该操作要么已经执行结束要么尚未发生,不会“看到”该操作执行部分的中间效果。
  • 2、访问同一组共享变量的原子操作是不能够被交错的。
      在Java中,对基本数据类型数据(long/double除外,仅包括byte、boolean、short、char、float和int)的变量和引用型变量的写操作都是原子的
      虚拟机将没有被volatile修饰的64位数据(long/double)的读写操作划分为两次32位的操作来进行。
      如果要保证long/double的写操作具有原子性,可以使用volatile变量修饰long/double变量。值得注意的是:volatile关键字仅能保证变量写操作的原子性,并不能保证其他操作(如read-modify-write操作和check-then-act操作)的原子性
      Java中任何变量的读操作都是原子操作。
  • 2、原子性问题的例子
      一个关于原子性的典型例子:counter++这并不是一个原子操作,包含了三个步骤:
  1. 读取变量counter的值;
  2. 对counter加一;
  3. 将新值赋值给变量counter。
  • 3、解决原子性问题方法
      Atomic开头的原子类、synchronized、LOCK等,都可以解决原子性问题

1.4.2 可见性

  • 1、如何理解可见性
      如果一个线程对某个共享变量进行更新后,后续访问该变量的线程可以读取到本次更新的结果,那么就称这个线程对该共享变量的更新对其它线程可见(一个线程对共享变量的修改,另一个线程能够立刻看到)。
      可见性问题由缓存导致。
  • 2、如何实现可见性
     主要有三种实现可见性的方式:
  1. volatile,通过在汇编语言中添加lock指令,来实现内存可见性。
  2. synchronized,当线程获取锁时会从主内存中获取共享变量的最新值,释放锁的时候会将共享变量同步到主内存中。
  3. final,被final关键字修饰的字段在构造器中一旦初始化完成,并且没有发生this逃逸(其它线程通过 this 引用访问到初始化了一半的对象),那么其它线程就能看见 final 字段的值。
  • 3、一些可见性场景
     Java中默认的两种可见性的存在场景:
  1. 父线程在启动子线程之前对共享变量的更新对于子线程来说是可见的。
  2. 一个线程终止后该线程对共享变量的更新对于调用该线程的join方法的线程而言是可见的。

1.4.3 有序性

  有序性指的是:程序执行的顺序按照代码的先后顺序执行。有序性问题由编译优化导致。
  volatile和synchronized都可以保证有序性

  1. volatile关键字通过添加内存屏障的方式来禁止指令重排,即重排序时不能把后面的指令放到内存屏障之前。
  2. synchronized关键字同样可以保证有序性,它保证每个时刻只有一个线程执行同步代码,相当于是让线程顺序执行同步代码。

1.5 同步与异步

  • 同步
      当一个同步调用发出去后,调用者要一直等待调用结果的返回后,才能进行后续的操作。
  • 异步
      当一个异步调用发出去后,调用者不用管被调用方法是否完成,都会继续执行后面的代码。 异步调用,要想获得结果,一般有两种方式:
  1. 主动轮询异步调用的结果;
  2. 被调用方通过callback来通知调用方调用结果(常用)。

  比如在超市购物,如果一件物品没了,你等仓库人员跟你调货,直到仓库人员跟你把货物送过来,你才能继续去收银台付款,这就类似同步调用。而异步调用就像网购,在网上付款下单后就不用管了,当货物到达后你收到通知去取就好。

1.6 进程与线程

  进程是程序向操作系统申请资源的基本单位,线程是进程中可独立执行的最小单位。通常一个进程可以包含多个线程,至少包含一个线程,同一个进程中所有线程共享该进程的资源。

  • 1、根本区别
      进程是操作系统资源分配的基本单位,而线程是处理器任务调度和执行的基本单位
  • 2、资源开销
      每个进程都有独立的代码和数据空间(程序上下文),程序之间的切换会有较大的开销;线程可以看做轻量级的进程,同一类线程共享代码和数据空间,每个线程都有自己独立的运行栈和程序计数器,线程之间切换的开销小。
  • 3、包含关系
      一个进程里可以包含多个线程。
  • 4、内存分配
      同一进程的线程共享本进程的地址空间和资源,而线程之间的地址空间和资源是相互独立的
  • 5、影响关系
      一个进程崩溃后,在保护模式下不会对其他进程产生影响,但是一个线程崩溃整个进程都死掉。所以多进程要比多线程健壮。
  • 6、执行过程
      每个独立的进程有程序运行的入口、顺序执行序列和程序出口。但是线程不能独立执行,必须依存在应用程序中,由应用程序提供多个线程执行控制,两者均可并发执行。

1.7 线程调度

  一个CPU,在任意时刻只能执行一条机器指令,每个线程只有获得CPU的使用权才能执行指令。多线程的并发运行,指从宏观上看,各个线程轮流获得CPU的使用权,分别执行各自的任务。
  线程调度模型有两种:

  • 1、分时调度模型
      分时调度模型是指让所有的线程轮流获得CPU的使用权,并且平均分配每个线程占用的CPU的时间片。
  • 2、抢占式调度模型
      抢占式调度模型是指优先让运行池中优先级高的线程占用CPU,如果运行池中的线程优先级相同,那么就随机选择一个线程,使其占用CPU。处于运行状态的线程会一直运行,直至它不得不放弃CPU。

  Java虚拟机(JVM)采用抢占式调度模型

二、线程的基本使用

  在Java中创建一个线程,可以理解创建一个Thread类(或其子类)的实例。线程的任务处理逻辑可以在Thread类的run实例方法中实现,运行一个线程实际上就是让Java虚拟机执行该线程的run方法。run方法相当于线程的任务处理逻辑的入口方法,它由虚拟机在运行相应线程时直接调用,而不是由相应代码进行调用
  启动一个线程的方法是调用start方法,其实质是请求Java虚拟机运行相应的线程,而这个线程具体何时运行是由线程调度器决定的。因此,start方法调用结束并不意味着相应线程已经开始运行。

2.1 创建线程

  创建线程有4种方式。

2.1.1 继承Thread类

  使用方式:

  1. 继承Thread类;
  2. 重写run方法;
  3. 创建Thread对象;
  4. 通过start()方法启动线程。

  示例:

	/*继承Thread类*/
	public class WelcomeThread extends Thread{
		  @Override
		  public void run() {
		     System.out.printf("test");
		  }
	}
	
	public class ThreadTest1 {
		  public static void main(String[] args) {
			    // 创建线程
			    Thread welcomeThread = new WelcomeThread();
			    // 启动线程
			    welcomeThread.start();
		  }
	}

  JDK1.8后,可以使用Lambda表达式来创建,示例:

    new Thread(()->{
        System.out.println("Lambda Thread Test!");
    }).start();

2.1.2 实现Runnable接口

  使用方式:

  1. 实现Runnable接口;
  2. 重写run方法;
  3. 创建Thread对象,将实现Runnable接口的类作为Thread的构造参数;
  4. 通过start()进行启动。

  此种方式用到了代理模式,示例:

	public class RunnableDemo implements Runnable {
	    @Override
	    public void run() {
	        System.out.println(Thread.currentThread().getName());
	    }

	    public static void main(String[] args) {
	        RunnableDemo runnableDemo = new RunnableDemo();
	        Thread thread = new Thread(runnableDemo);
	        thread.start();
	    }
	}

  前两种比较的话, 推荐使用第二种方式,原因:

  1. Java是单继承,将继承关系留给最需要的类。
  2. Runnable可以实现多个相同的程序代码的线程去共享同一个资源。当以Thread方式去实现资源共享时,实际上Thread内部,依然是以Runnable形式去实现的资源共享。

2.1.3 实现Callable接口

  前两种方式比较常见,Callable的使用方式:

  1. 创建实现Callable接口的类;
  2. 以Callable接口的实现类为参数,创建FutureTask对象;
  3. 将FutureTask作为参数创建Thread对象;
  4. 调用线程对象的start()方法。

  示例:

	public class MyCallable implements Callable<Integer> {
	    @Override
	    public Integer call() {
	        System.out.println(Thread.currentThread().getName() + " call()方法执行中...");
	        return 1;
	    }
	}
	
	public class CallableTest {
	    public static void main(String[] args) {
	        FutureTask<Integer> futureTask = new FutureTask<Integer>(new MyCallable());
	        Thread thread = new Thread(futureTask);
	        thread.start();
	    }
	}

  使用该方法创建线程时,核心方法是call(),该方法有返回值,其返回值类型就是Callable接口中泛型对应的类型

2.1.4 使用Executors工具类创建线程池

  由于线程的创建、销毁是一个比较消耗资源的过程,所以在实际使用时往往使用线程池。
  在创建线程池时,可以使用现成的Executors工具类来创建,该工具类能创建的线程池有4种:newFixedThreadPool,newCachedThreadPool,newSingleThreadExecutor,newScheduledThreadPool。此处以newSingleThreadExecutor为例,其步骤为:

  1. 使用Executors类中的newSingleThreadExecutor方法创建一个线程池;
  2. 调用线程池中的execute()方法执行由实现Runnable接口创建的线程;或者调用submit()方法执行由实现Callable接口创建的线程;
  3. 调用线程池中的shutdown()方法关闭线程池。

  示例:

	public class MyRunnable implements Runnable {
	    @Override
	    public void run() {
	        System.out.println(Thread.currentThread().getName() + " run()方法执行中...");
	    }
	}
	
	public class SingleThreadExecutorTest {
	    public static void main(String[] args) {
	        ExecutorService executorService = Executors.newSingleThreadExecutor();
	        MyRunnable runnableTest = new MyRunnable();
	        for (int i = 0; i < 5; i++) {
	            executorService.execute(runnableTest);
	        }
	        System.out.println("线程任务开始执行");
	        executorService.shutdown();
	    }
	}

2.1.5 4种创建方式对比

  • 1、继承Thread类
      优点 :代码简单 。
      缺点 :该类无法继承别的类。
  • 2、实现Runnable接口
      优点:继承其他类,同一实现该接口的实例可以共享资源。
  • 3、实现Callable接口
      优点:可以获得异步任务的返回值。
  • 4、线程池
      优点:实现自动化装配,易于管理,循环利用资源。

2.2 启动线程

2.2.1 线程每次只能使用一次

  当线程的run方法执行结束,相应的线程的运行也就结束了。
  线程每次只能使用一次,即只能调用一次start方法。在线程未结束前,多次调用start方法会抛出IllegalThreadStateException,Thread类中的start方法中可以看出该逻辑:

    public synchronized void start() {
        checkNotStarted();
        hasBeenStarted = true;
        nativeCreate(this, stackSize, daemon);
    }
   
    private void checkNotStarted() {
        if (hasBeenStarted) {
            throw new IllegalThreadStateException("Thread already started");
        }
    } 

2.2.2 线程的 run()和 start()有什么区别

  两者的区别:

  1. start()方法用于启动线程,run()方法用于实现具体的业务逻辑
  2. run()可以重复调用,而start()只能调用一次

  调用start()方法来启动一个线程,无需等待run()方法体代码执行完毕,可以直接继续执行其他的代码, 此时线程是处于就绪状态,并没有运行。

2.2.3 为什么不能直接调用run()方法

  新建一个线程,线程进入了新建状态。调用 start()方法,会启动一个线程并使线程进入了就绪状态,当分配到时间片后就可以开始运行了。 start()会执行线程的相应准备工作,然后自动执行run()方法的内容,这是真正的多线程工作。
  如果直接执行run()方法,会把run方法当成一个main线程下的普通方法去执行,并不会在某个线程中执行它,所以这并不是多线程工作。示例:

	public class JavaTest {
	
		public static void main(String[] args) {
			System.out.println("main方法中的线程名:"
				+Thread.currentThread().getName()); //main方法中的线程名:main
			
			Thread welcomeThread = new WelcomeThread();
			
			System.out.println("以start方法启动线程");
			welcomeThread.start();  //Thread子类中的线程名:Thread-0
			System.out.println("以run方法启动线程");
			welcomeThread.run();   //Thread子类中的线程名:main
		}
	}
	
	class WelcomeThread extends Thread{
		  @Override
		  public void run() {
		     System.out.println("Thread子类中的线程名:"
		     	+Thread.currentThread().getName());
		  }
	}

  总结: 调用start方法方可启动线程并使线程进入就绪状态,而run方法只是thread的一个普通方法调用,还是在主线程里执行。

2.2.4 线程类的构造方法、静态块是被哪个线程调用的

  线程类的构造方法、静态块是被new这个线程类所在的线程所调用的,而run方法里面的代码才是被线程自身所调用的。

2.3 线程属性

  Thread类的私有属性有许多,了解几个常用的即可:线程的编号(ID)、名称(Name)、线程类别(Daemon)和优先级(Priority)。
  这几个属性中,ID仅可读,其他都是可读写。具体:

属性属性类型用途注意事项
编号(ID)long用于标识不同的线程,不同的线程拥有不同的编号某个编号的线程运行结束后,该编号可能被后续创建的线程使用,因此该属性的值不适合用作某种唯一标识
名称(Name)String用于区分不同的线程,默认值的格式为“Thread-线程编号”尽量为不同的线程设置不同的值
线程类别(Daemon)boolean值为true表示相应的线程为守护线程,否则表示相应的线程为用户线程。该属性的默认值与相应线程的父线程的该属性的值相同该属性必须在相应线程启动之前设置,即调用setDaemon方法必须在调用start方法之前,否则会出现IllegalThreadStateException
优先级(Priority)int优先级高的线程一般会被优先运行。优先级从1到10,默认值一般为5(普通优先级),数字越大,优先级越高。
对于具体的一个线程而言,其优先级的默认值与其父线程的优先级值相等。
一般使用默认的优先级即可,不恰当地设置该属性值可能会导致严重的问题(线程饥饿)

  获取4个属性值示例:

	public static void main(String[] args)  {
		new Thread(new Runnable() {
			@Override
			public void run() {
				//10,Thread-0,5,false
				System.out.println(Thread.currentThread().getId()+","
					+Thread.currentThread().getName()+","
					+Thread.currentThread().getPriority()+","
					+Thread.currentThread().isDaemon());
			}
		}).start();
	}

2.3.1 线程优先级

  Java线程的优先级属性本质上只是一个给线程调度器的提示信息,以便于线程调度器决定优先调度哪些线程运行。每个线程的优先级都在1到10之间,1的优先级为最低,10的优先级为最高,在默认情况下优先级都是Thread.NORM_PRIORITY(常数 5)。
  虽然开发者可以定义线程的优先级,但是这并不能保证高优先级的线程会在低优先级的线程前执行。
  线程优先级特性:

  • 1、继承性
      比如A线程启动B线程,则B线程的优先级与A是一样的。
  • 2、规则性
      高优先级的线程总是大部分先执行完,但不代表高优先级线程全部先执行完。
  • 3、随机性
      优先级较高的线程不一定每一次都先执行完。

  在不同的JVM以及OS上,线程规划会存在差异,有些OS会忽略对线程优先级的设定。
  设置和获取线程优先级的方法:

	//为线程设定优先级
	public final void setPriority(int newPriority) 
	//获取线程的优先级
	public final int getPriority() 

  示例:

	public class RunnableDemo implements Runnable {
	    @Override
	    public void run() {
	    	int nowPriority = Thread.currentThread().getPriority();
	        System.out.println("1.优先级:"+nowPriority);  //1.优先级:5
	        Thread.currentThread().setPriority(Thread.MAX_PRIORITY);
	        nowPriority = Thread.currentThread().getPriority();
	        System.out.println("2.优先级:"+nowPriority);  //2.优先级:10
	    }
	
	    public static void main(String[] args) {
	        RunnableDemo runnableDemo = new RunnableDemo();
	        Thread thread = new Thread(runnableDemo);
	        thread.start();
	        
	    }
	}

2.3.2 守护线程和用户线程

  Java 中的线程分为两种:守护线程和用户线程。任何线程都可以设置为守护线程和用户线程,通过方法setDaemon(true)可以把该线程设置为守护线程,反之则为用户线程。
  用户线程:运行在前台,执行具体的任务,如程序的主线程、连接网络的子线程等都是用户线程。
  守护线程:运行在后台,为其他前台线程服务,比如垃圾回收线程,JIT(编译器)线程就可以理解为守护线程。一旦所有用户线程都结束运行,守护线程会随 JVM 一起结束工作。
  守护线程应该永远不去访问固有资源 ,如文件、数据库,因为它会在任何时候甚至在一个操作的中间发生中断。

  注意事项:

  1. setDaemon(true)必须在start()方法前执行,否则会抛出IllegalThreadStateException
  2. 在守护线程中产生的新线程也是守护线程。
  3. 不是所有的任务都可以分配给守护线程来执行,比如读写操作或者计算逻辑。
  4. 守护线程中不能依靠finally块的内容来确保执行关闭或清理资源的逻辑。因为我们上面也说过了一旦所有用户线程都结束运行,守护线程会随JVM一起结束工作,所以守护线程中的finally语句块可能无法被执行。

  设置和获取线程是否是守护线程的方法:

	//设置线程是否为守护线程
	public final void setDaemon(boolean on)
	//判断线程是否是守护线程
	public final boolean isDaemon()

2.3.3 线程名称

  相比于上面的两个属性,实际运用中,往往线程名称会被修改,目的是为了调试。获取和设置线程名称的方法:

	//获取线程名称
	public final String getName()
	//设置线程名称
	public final synchronized void setName(String name)

  示例:

	public class RunnableDemo implements Runnable {
	    @Override
	    public void run() {
	    	String nowName = Thread.currentThread().getName();
	        System.out.println("1.线程名称:"+nowName);  //1.线程名称:Thread-0
	        Thread.currentThread().setName("测试线程");
	        nowName = Thread.currentThread().getName();
	        System.out.println("2.线程名称:"+nowName);  //2.线程名称:测试线程
	    }
	
	    public static void main(String[] args) {
	        RunnableDemo runnableDemo = new RunnableDemo();
	        Thread thread = new Thread(runnableDemo);
	        thread.start();
	        
	    }
	}

2.4 线程的生命周期

2.4.1 从代码角度理解

  在Thread类中,线程状态是一个枚举类型:

    public enum State {
        NEW,
        RUNNABLE,
        BLOCKED,
        WAITING,
        TIMED_WAITING,
        TERMINATED
    }

  线程的状态可以通过public State getState()来获取,该方法的返回值是一个枚举类型,线程状态定义如下:

  • 1、NEW
      一个已创建而未启动的线程处于该状态。
  • 2、RUNNABLE
      该状态可以被看成一个复合状态。它包括两个子状态:READY和RUNNING。前者表示处于该状态的线程可以被线程调度器进行调度而使之处于RUNNING状态,后者表示线程正在运行状态。
      执行Thread.yield()的线程,其状态可能由RUNNING转换为READY。
  • 3、BLOCKED
      处于BLOCKED状态的线程并不会占处理器资源,当阻塞式IO操作完成后,或线程获得了其申请的资源,状态又会转换为RUNNABLE。
  • 4、WAITING
      一个线程执行了某些特定方法之后就会处于这种等待其他线程执行另外一些特定操作的状态。
      能够使线程变成WAITING状态的方法包括:Object.wait()、Thread.join(),能够使线程从WAITING状态变成RUNNABLE状态的方法有:Object.notify()、Object.notifyAll()。
  • 5、TIMED_WAITING
      该状态和WAITING类似,差别在于处于该状态的线程并非无限制地等待其他线程执行特定操作,而是处于带有时间限制的等待状态。
      当其他线程没有在特定时间内执行该线程所期待的特定操作时,该线程的状态自动转换为RUNNABLE。
  • 6、TERMINATED
      已经执行结束的线程处于该状态。
      Thread.run()正常返回或由于抛出异常而提前终止都会导致相应线程处于该状态。

  6种状态的转换:

2.4.2 从使用角度理解

  在实际开发中,往往将线程的状态理解为5种:新建、可运行、运行、阻塞、死亡。

  • 1、新建(new)
      新创建了一个线程对象。用new方法创建一个线程后,线程对象就处于新建状态。
  • 2、可运行(runnable)
      线程对象创建后,当调用线程对象的start()方法,该线程处于就绪状态,但还没分配到CPU,处于线程就绪队列,等待系统为其分配CPU。
  • 3、运行(running)
      可运行状态(runnable)的线程获得了CPU时间片,执行程序代码。
      就绪状态是进入到运行状态的唯一入口,也就是说,线程要想进入运行状态执行,首先必须处于就绪状态中;
      如果在给定的时间片内没有执行结束,就会被系统给换下来回到等待执行状态。
  • 4、阻塞(block)
      处于运行状态中的线程由于某种原因,暂时放弃对CPU的使用权,停止执行,此时进入阻塞状态,直到其进入到就绪(runnable)状态,才有机会再次被CPU调用以进入到运行状态。
      阻塞的情况分三种:
  1. 等待阻塞
      运行状态中的线程执行 wait()方法,JVM会把该线程放入等待队列中,使本线程进入到等待阻塞状态;
  2. 同步阻塞
      线程在获取 synchronized 同步锁失败(因为锁被其它线程所占用),则JVM会把该线程放入锁池中,线程会进入同步阻塞状态;
  3. 其他阻塞
      通过调用线程的 sleep()或 join()或发出了 I/O 请求时,线程会进入到阻塞状态。当 sleep()状态超时、join()等待线程终止或者超时、或者 I/O 处理完毕时,线程重新转入就绪状态。
  • 5、死亡(dead)
      线程run()、main()方法执行结束,或者因异常退出了run()方法,则该线程结束生命周期。

2.5 Thread类的常用方法

  以下是Thread类中较常用的几个方法,并不包含线程间协作的方法(如await、notify等),这些方法的使用随后介绍。其中的yield方法并不常用,但常常拿来和sleep、await等方法进行比较,所以也介绍下。

方法功能备注
static Thread currentThread()返回当前线程,即当前代码的执行线程
void run()用于实现线程的任务处理逻辑该方法由Java虚拟机直接调用
void start()启动线程调用该方法并不代表相应的线程已经被启动,线程是否启动是由虚拟机去决定的
void join()等待相应线程运行结束若线程A调用线程B的join方法,那么线程A的运行会被暂停,直到线程B运行结束
static void yield()使当前线程主动放弃其对处理器的占用,这可能导致当前线程被暂停这个方法是不可靠的,该方法被调用时当前线程仍可能继续运行
void interrupt()中断线程
static void sleep(long millis)使当前线程休眠(暂停运行)指定的时间

2.5.1 interrupted

  中断可以理解为线程的一个标志位,它表示了一个运行中的线程是否被其他线程进行了中断操作,常常被用于线程间的协作
  其他线程可以调用指定线程的interrupt()方法对其进行中断操作,同时指定线程可以调用isInterrupted()来感知其他线程对其自身的中断操作,从而做出响应。
  另外,也可以调用Thread的静态方法interrupted()对当前线程进行中断操作,该方法会清除中断标志位。需要注意的是,当抛出InterruptedException时候,会清除中断标志位,此时再调用isInterrupted,会返回false。
  和中断相关的方法有3个:

方法名详细解释备注
public void interrupt()中断该线程对象如果该线程被调用了Object wait/Object wait(long),或者被调用sleep(long),join()/join(long)方法时会抛出interruptedException并且中断标志位将会被清除
public boolean isinterrupted()测试该线程对象是否被中断中断标志位不会被清除
public static boolean interrupted()查看当前中断信号是true还是false并且清除中断信号中断标志位会被清除

  关于interrupt和isinterrupted的使用,示例:

public class JavaTest {
    public static void main(String[] args) throws InterruptedException {
        //sleepThread睡眠1000ms
        final Thread sleepThread = new Thread() {
            @Override
            public void run() {
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                super.run();
            }
        };
        //busyThread一直执行死循环
        Thread busyThread = new Thread() {
            @Override
            public void run() {
                while (true) ;
            }
        };
        sleepThread.start();
        busyThread.start();
        sleepThread.interrupt();
        busyThread.interrupt();
        while (sleepThread.isInterrupted()) ;
        System.out.println("sleepThread isInterrupted: " + sleepThread.isInterrupted());
        System.out.println("busyThread isInterrupted: " + busyThread.isInterrupted());
    }
}

  测试结果:

  在上面的代码中,开启了两个线程分别为sleepThread和BusyThread, sleepThread睡眠1s,BusyThread执行死循环。然后分别对着两个线程进行中断操作,可以看出sleepThread抛出InterruptedException后清除标志位,而busyThread就不会清除标志位。
  另外,可以通过中断的方式实现线程间的简单交互,因为可以通过isInterrupted()方法监控某个线程的中断标志位是否清零,针对不同的中断标志位进行不同的处理。

2.5.2 join

  join方法也是一种线程间协作的方式,很多时候,一个线程的输入可能非常依赖于另一个线程的输出。如果在一个线程threadA中执行了threadB.join(),其含义是:当前线程threadA会等待threadB线程终止后,threadA才会继续执行

方法名详细注释备注
public final void join() throws InterruptedException等待这个线程死亡。如果任何线程中断当前线程,如果抛出InterruptedException异常时,当前线程的中断状态将被清除
public final void join(long millis) throws InterruptedException等待这个线程死亡的时间最多为millis毫秒。
如果参数为 0,意味着永远等待。
如果millis为负数,抛出IllegalArgumentException异常
public final void join(long millis, int nanos) throws InterruptedException等待最多millis毫秒加上这nanos纳秒。如果millis为负数或者nanos不在0-999999范围抛出IllegalArgumentException异常

  看个例子:

public class JoinDemo {
    public static void main(String[] args) {
        Thread previousThread = Thread.currentThread();
        for (int i = 1; i <= 10; i++) {
            Thread curThread = new JoinThread(previousThread);
            curThread.start();
            previousThread = curThread;
        }
    }

    static class JoinThread extends Thread {
        private Thread thread;

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

        @Override
        public void run() {
            try {
                thread.join();
                System.out.println(thread.getName() + " terminated.");
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

  测试结果:

  在上面的例子中一个创建了10个线程,每个线程都会等待前一个线程结束才会继续运行。可以通俗的理解成接力,前一个线程将接力棒传给下一个线程,然后又传给下一个线程…

2.5.3 sleep

  sleep方法为:

	public static native void sleep(long millis)

  显然sleep是Thread的静态方法,它的作用是:让当前线程按照指定的时间休眠,其休眠时间的精度取决于处理器的计时器和调度器。需要注意的是如果当前线程获得了锁,sleep方法并不会失去锁
  Thread.sleep方法经常拿来与Object.wait()方法进行比较,sleep和wait两者主要的区别:

  1. sleep()方法是Thread的静态方法,而wait是Object实例方法;
  2. wait()方法必须要在同步方法或者同步块中调用,也就是必须已经获得对象锁。而sleep()方法没有这个限制可以在任何地方使用。
  3. wait()方法会释放占有的对象锁,使得该线程进入等待池中,等待下一次获取资源。而sleep()方法只是会让出CPU并不会释放掉对象锁;
  4. sleep()方法在休眠时间达到后,如果再次获得CPU时间片就会继续执行,而wait()方法必须等待Object.notift/Object.notifyAll通知后,才会离开等待池,并且再次获得CPU时间片才会继续执行。

  关于sleep方法的使用,示例:

    public static void main(String[] args) throws InterruptedException {
    	new Thread(new Runnable() {
    	    @Override
    	    public void run() {
    	    	System.out.println("第一个线程的执行时间:"+new Date());
    	    }
    	}).start();
    	System.out.println("sleep2秒");
    	Thread.sleep(2000);
    	new Thread(new Runnable() {
    	    @Override
    	    public void run() {
    	    	System.out.println("第二个线程的执行时间:"+new Date());
    	    }
    	}).start();
    }

  结果示例:

  可以看出,第2个线程的执行时间是晚于第1个线程2秒的。

2.5.4 yield

  yield方法为:

	public static native void yield()

  yield方法的作用:使当前线程从执行状态(运行状态)变为可执行态(就绪状态)
  yield方法是一个静态方法,一旦执行,它会是当前线程让出CPU。但是,让出了CPU并不是代表当前线程不再运行了。线程调度器可能忽略此此消息,并且如果在下一次竞争中,又获得了CPU时间片当前线程依然会继续运行。另外,让出的时间片只会分配给大于等于当前线程优先级的线程。

  在线程中,用priority来表示优先级,priority的范围从1~10。在构建线程的时候可以通过 setPriority(int) 方法进行设置,默认优先级为5,优先级高的线程相较于优先级低的线程优先获得处理器时间片。

  需要注意的是,sleep()和yield()方法,同样都是当前线程会交出处理器资源,而它们不同的是,sleep()交出来的时间片其他线程都可以去竞争,也就是说都有机会获得当前线程让出的时间片。而yield()方法只允许大于等于当前线程优先级的线程,竞争CPU时间片

2.6 线程相关的一些问题

2.6.1 interrupt、interrupted和isInterrupted方法的区别

  interrupt:用于中断线程。调用该方法的线程的状态为将被置为”中断”状态
  线程中断仅仅是设置线程的中断状态标识,不会停止线程。需要用户自己去监视线程的状态为并做处理。支持线程中断的方法(也就是线程中断后会抛出interruptedException 的方法)就是在监视线程的中断状态,一旦线程的中断状态标识被置为“中断状态”,就会抛出中断异常。
  interrupted:是静态方法,查看当前中断信号是true还是false并且清除中断信号。如果一个线程被中断了,第一次调用 interrupted 则返回 true,第二次和后面的就返回 false 了。
  isInterrupted:查看当前中断信号是true还是false

2.6.2 sleep方法和yield方法有什么区别

  • 1、sleep()方法给其他线程运行机会时不考虑线程的优先级,因此会给低优先级的线程以运行的机会;yield()方法只会给相同优先级或更高优先级的线程以运行的机会
  • 2、线程执行 sleep()方法后转入阻塞(blocked)状态,而执行 yield()方法后转入就绪(ready)状态
  • 3、sleep()方法声明抛出 InterruptedException(其它线程可以使用 interrupt 方法打断正在睡眠的线程,这时 sleep 方法会抛出 InterruptedException),而 yield()方法没有声明任何异常;
  • 4、sleep()方法比 yield()方法具有更好的可移植性,通常不建议使用yield()方法来控制并发线程的执行;
  • 5、建议用TimeUnit的sleep代替Thread的sleep来获得更好的可读性。

2.6.3 线程怎么处理异常

  如果线程运行中产生了异常,首先会生成一个异常对象。我们平时throw抛出异常,就是把异常交给JVM处理。JVM首先会去找有没有能够处理该异常的处理者(首先找到当前抛出异常的调用者,如果当前调用者无法处理,则会沿着方法调用栈一路找下去),能够处理的调用者实际就是看方法的catch关键字,JVM会把该异常对象封装到catch入参,允许开发者手动处理异常。
  若找不到能够处理的处理者(实际就是没有手动catch异常,比如未受检异常),就会交该线程处理;JVM会调用Thread类的dispatchUncaughtException()方法,该方法调用了getUncaughtExceptionHandler(),uncaughtExceptoin(this,e)来处理了异常,如果当前线程设置了自己的UncaughtExceptionHandler,则使用该handler,调用自己的uncaughtException方法。如果没有,则使用当前线程所在的线程组的Handler的uncaughtExceptoin()方法,如果线程中也没有设置,则直接把异常定向到System.err中,打印异常信息(控制台红色字体输出的异常就是被定向到System.err的异常)。

2.6.4 Thread.sleep(0)的作用是什么

  由于Java采用抢占式的线程调度算法,因此可能会出现某条线程常常获取到CPU控制权的情况,为了让某些优先级比较低的线程也能获取到CPU控制权,可以使用Thread.sleep(0)手动触发一次操作系统分配时间片的操作,这也是平衡CPU控制权的一种操作。

2.6.5 一个线程如果出现了运行时异常会怎么样

  1. 如果这个异常没有被捕获的话,这个线程就停止执行了。
  2. 另外重要的一点是:如果这个线程持有某个对象的监视器器,那么这个对象监视器器会被立即释放。

2.6.6 终止线程运行的几种情况

  1. 线程体中调用了yield方法让出了对CPU的占用权利;
  2. 线程体中调用了sleep方法使线程进入睡眠状态;
  3. 线程由于IO操作受到阻塞;
  4. 另外一个更高优先级线程出现,导致当前线程未分配到时间片;
  5. 在支持时间片的系统中,该线程的时间片用完。
  6. 使用stop方法强行终止,但是不推荐这个方法,因为stop是过期作废的方法。
  7. 使用interrupt方法中断线程。

2.7 多线程使用示例

  使用多线程下载文件,可以简单分为以下几步:

  • 1、获取目标文件的大小
      在本地留好足量的空间来存储。
  • 2、确定要开启几个线程
      所开线程的最大数量=(CPU核数+1),本例子中开三个线程。
  • 3、 计算平均每个线程需要下载多少个字节的数据
      理想情况下多线程下载是按照平均分配原则的,即:单线程下载的字节数等于文件总大小除以开启的线程总条数,当不能整除时,则最后开启的线程将剩余的字节一起下载。
  • 4、计算各个线程要下载的字节范围
      在下载过程中,各个线程都要明确自己的开始索引(startIndex)和结束索引(endIndex)。
  • 5、使用for循环开启子线程进行下载
  • 6、获取各个线程的目标文件的开始索引和结束索引的范围
  • 7、创建文件,接收下载的流

  示例:

package ThreadTest;

import java.io.InputStream;
import java.io.RandomAccessFile;
import java.net.HttpURLConnection;
import java.net.URL;

public class DownloadTest {
	private static final String path = "http://down.360safe.com/se/360se9.1.0.426.exe";
	public static void main(String[] args) throws Exception {
	    /*第一步:获取目标文件的大小*/
	    int totalSize = new URL(path).openConnection().getContentLength();
	    System.out.println("目标文件的总大小为:"+totalSize+"B");
	    
	    /*第二步:确定开启几个线程。开启线程的总数=CPU核数+1;例如:CPU核数为4,则最多可开启5条线程*/
	    int availableProcessors = Runtime.getRuntime().availableProcessors();
	    System.out.println("CPU核数是:"+availableProcessors);
	    
	    int threadCount = 3;
	    /*第三步:计算每个线程要下载多少个字节*/
	    int blockSize = totalSize/threadCount;
	
	    /*每次循环启动一条线程下载*/
	    for(int threadId=0; threadId<3;threadId++){
	        /*第四步:计算各个线程要下载的字节范围*/
	        /*每个线程下载的开始索引*/
	        int startIndex = threadId * blockSize;
	        /*每个线程下载的结束索引*/
	        int endIndex = (threadId+1)* blockSize-1;
	        /*如果是最后一条线程*/
	        if(threadId == (threadCount -1)){
	            endIndex = totalSize -1;
	        }
	        /*第五步:启动子线程下载*/
	        new DownloadThread(threadId,startIndex,endIndex).start();
	    }
	}

	private static class DownloadThread extends Thread{
	    private int threadId;
	    private int startIndex;
	    private int endIndex;
	    public DownloadThread(int threadId, int startIndex, int endIndex) {
	        super();
	        this.threadId = threadId;
	        this.startIndex = startIndex;
	        this.endIndex = endIndex;
	    }
	
	    @Override
	    public void run(){
	        System.out.println("第"+threadId+"条线程,下载索引:"+startIndex+"~"+endIndex);
	        /*每条线程要去找服务器拿取一段数据*/
	        try {
	            URL url = new URL(path);
	            HttpURLConnection connection = (HttpURLConnection)url.openConnection();
	            /*设置连接超时时间*/
	            connection.setConnectTimeout(5000);
	            /*第六步:获取目标文件的[startIndex,endIndex]范围*/
	            connection.setRequestProperty("range", "bytes="+startIndex+"-"+endIndex);
	            connection.connect();
	            /*获取响应码,当服务器返回的是文件的一部分时,响应码不是200,而是206*/
	            int responseCode = connection.getResponseCode();
	            if (responseCode == 206) {
	                //拿到目标段的数据
	                InputStream is = connection.getInputStream();
	                /*第七步:创建一个RandomAccessFile对象,将返回的字节流写到文件指定的范围*/
	                String fileName = getFileName(path);
	                /*创建一个可读写的文件,即把文件下载到D盘*/
	                RandomAccessFile raf = new RandomAccessFile("d:/"+fileName, "rw");
	                /*注意:让raf写字节流之前,需要移动raf到指定的位置开始写*/
	                raf.seek(startIndex);
	                /*将字节流数据写到文件中*/
	                byte[] buffer = new byte[1024];
	                int len = 0;
	                while((len=is.read(buffer))!=-1){
	                    raf.write(buffer, 0, len);
	                }
	                is.close();
	                raf.close();
	                System.out.println("第 "+ threadId +"条线程下载完成 !");
	            } else {
	                System.out.println("下载失败,响应码是:"+responseCode);
	            }
	        } catch (Exception e) {
	            e.printStackTrace();
	        }
	    }
	}

	/*获取文件的名称*/
	private static String getFileName(String path){
	    int index = path.lastIndexOf("/");
	    String fileName = path.substring(index+1);
	    return fileName ;
	}
}

  测试结果:

目标文件的总大小为:48695168B
CPU核数是:4
第0条线程,下载索引:0~16231721
第1条线程,下载索引:16231722~32463443
第2条线程,下载索引:32463444~48695167
第 1条线程下载完成 !
第 0条线程下载完成 !
第 2条线程下载完成 !

  下载文件:

三、线程的活性故障

  线程活性故障是由于资源稀缺性或程序自身的问题和缺陷导致线程一直处于非RUNNABLE状态,或者线程虽然处于RUNNABLE状态,但是其要执行的任务却一直无法进展的故障现象。

3.1 死锁

  死锁是指两个或两个以上的进程(线程)在执行过程中,由于竞争资源或者由于彼此通信而造成的一种阻塞的现象,若无外力作用,它们都将无法推进下去
  如图所示,线程A持有资源2,线程B持有资源1,他们同时都想申请对方的资源,所以这两个线程就会互相等待而进入死锁状态:

3.1.1 死锁的产生条件

  当线程产生死锁时,这些线程及相关的资源将满足如下全部条件:

  • 1、互斥
      一个资源只能被一个线程(进程)占用,直到被该线程(进程)释放。
  • 2、请求与保持(不主动释放)条件
      一个线程(进程)因请求被占用资源(锁)而发生阻塞时,对已获得的资源保持不放
  • 3、不剥夺(不能被强占)条件
      线程(进程)已获得的资源,在末使用完之前不能被其他线程强行剥夺,只有自己使用完毕后才释放资源
  • 4、循环等待(互相等待)条件
      当发生死锁时,所等待的线程(进程)必定会形成一个环路(类似于死循环),造成永久阻塞

  用一句话该概括:

  两个或多个线程持有并且不释放独有的锁,并且还需要竞争别的线程所持有的锁,导致这些线程都一直阻塞下去。

  这些条件是死锁产生的必要条件而非充分条件,也就是说只要产生了死锁,那么上面的这些条件一定同时成立,但是上述条件即便同时成立也不一定产生死锁。
  如果把锁看作一种资源,这种资源正好符合“资源互斥”和“资源不可抢夺”的要求。那么,可能产生死锁的特征就是在持有一个锁的情况下去申请另外一个锁,通常是锁的嵌套,示例:

	//内部锁
	public void deadLockMethod1(){
		synchronized(lockA){
			//...
			synchronized(lockB){
				//...
			}
		}
	}
	
	//显式锁
	public void deadLockMethod2(){
		lockA.lock();
		try{
			//...
			lockB.lock();
			try{
				//...
			}finally{
				lockB.unlock();
			}
		}finally{
			lockA.unlock();
		}
	}

  示例:

	private static Object lockObject1 = new Object();
	private static Object lockObject2 = new Object();
	
	public static void main(String[] args)  {
		new Thread(new Runnable() {
			@Override
			public void run() {
				try {
					test1();
				} catch (InterruptedException e) {
					e.printStackTrace();
				}
			}
		}).start();
		new Thread(new Runnable() {
			@Override
			public void run() {
				try {
					test2();
				} catch (InterruptedException e) {
					e.printStackTrace();
				}
			}
		}).start();
	}

	public static void test1() throws InterruptedException{
		synchronized (lockObject1) {
			System.out.println(Thread.currentThread().getName()
					+"获取到lockObject1,正在获取lockObject2");
			Thread.sleep(1000);
			synchronized (lockObject2) {
				System.out.println(Thread.currentThread().getName()
						+"获取到lockObject2");
			}
		}
	}
	
	public static void test2() throws InterruptedException{
		synchronized (lockObject2) {
			System.out.println(Thread.currentThread().getName()
					+"获取到lockObject2,正在获取lockObject1");
			Thread.sleep(1000);
			synchronized (lockObject1) {
				System.out.println(Thread.currentThread().getName()
						+"获取到lockObject1");
			}
		}
	}

  以下结果表明已经出现了死锁:

Thread-1获取到lockObject2,正在获取lockObject1
Thread-0获取到lockObject1,正在获取lockObject2

3.1.2 死锁的规避

  由上文可知,要产生死锁需要同时满足四个条件,所以,只要打破其中一个条件就可以避免死锁的产生。常用的规避方法有如下几种:

  • 1、粗锁法
      用一个粒度较粗的锁替代原来的多个粒度较细的锁,这样涉及的线程都只需要申请一个锁从而避免了死锁。粗锁法的缺点是它明显降低了并发性并可能导致资源浪费。
  • 2、锁排序法
      相关线程使用全局统一的顺序申请锁。假设有多个线程需要申请锁(资源),那么只需要让这些线程依照一个全局(相对于使用这种资源的所有线程而言)统一的顺序去申请这些资源,就可以消除“循环等待资源”这个条件,从而规避死锁。一般,可以使用对象的hashcode作为资源的排序依据。
  • 3、使用ReentrantLock.tryLock(long timeout, TimeUnit unit) 申请锁
      ReentrantLock.tryLock(long timeout, TimeUnit unit) 允许为申请锁这个操作加上一个超时时间。在超时事件内,如果相应的锁申请成功,该方法返回true。如果在tryLock执行的那一刻相应的锁正在被其他线程持有,那么该方法会使当前线程暂停,直到这个锁申请成功(此时该方法返回true)或者等待时间超过指定的超时时间(此时该问题返回false)。因此,使用ReentrantLock.tryLock(long timeout, TimeUnit unit) 来申请锁可以避免一个线程无限制地等待另外一个线程持有的资源,从而最终能够消除死锁产生的必要条件中的“占用并等待资源”。示例:
	boolean locked = false;
	try {
		locked = lock.tryLock(5, TimeUnit.SECONDS);
	} catch (InterruptedException e) {
		e.printStackTrace();
	} finally {
		if(locked) lock.unlock();
	}
  • 4、使用开放调用----在调用外部方法时不加锁
      开放调用是一个方法在调用方法(包括其他类的方法以及当前类的可覆盖方法)的时候不持有任何锁。显然,开放调用能够消除死锁产生的必要条件中的“持有并等待资源”。
  • 5、使用锁的替代品
      使用一些锁的替代品(无状态对象、线程特有对象以及volatile关键字等),在条件允许的情况下,使用这些替代品在保障线程安全的前提下不仅能够避免锁的开销,还能够直接避免死锁。

3.2 线程饥饿和活锁

  线程饥饿是指一直无法获得其所需的资源而导致任务一直无法进展的一种活性故障。
  线程饥饿的一个典型例子是在争用的情况下使用非公平模式的读写锁。此种情况下,可能会导致某些线程一直无法获取其所需的资源(锁),即导致线程饥饿。
  把锁看作一种资源的话,其实死锁也是一种线程饥饿。死锁的结果是故障线程都无法获得其所需的全部锁中的一个锁,从而使其任务一直无法进展,这相当于线程无法获得其所需的全部资源(锁)而使得其任务一直无法进展,即产生了线程饥饿。由于线程饥饿的产生条件是一个(或多个)线程始终无法获得其所需的资源,显然这个条件的满足并不意味着死锁的必要条件(而不是充分条件)的满足,因此线程饥饿并不会导致死锁。
  Java中导致饥饿的原因:

  1. 高优先级线程抢占了所有的低优先级线程的 CPU 时间。
  2. 线程被永久堵塞在一个等待进入同步块的状态,因为其他线程总是能在它之前持续地对该同步块进行访问。
  3. 线程在等待一个本身也处于永久等待完成的对象(比如调用这个对象的 wait 方法),因为其他线程总是被持续地获得唤醒。

  线程饥饿涉及的线程,其生命周期不一定就是WAITING或BLOCKED状态,其状态也可能是RUNNING(说明涉及的线程一直在申请宁其所需的资源),这时饥饿就会演变成活锁。
  活锁指线程一直处于运行状态,但是其任务却一直无法进展的一种活性故障。

3.3 死锁与活锁的区别,死锁与饥饿的区别

  • 死锁
      是指两个或两个以上的进程(或线程)在执行过程中,因争夺资源而造成的一种互相等待的现象,若无外力作用,它们都将无法推进下去。

  • 活锁
      任务或者执行者没有被阻塞,由于某些条件没有满足,导致一直重复尝试,却一直获得不了锁。

  • 饥饿
      一个或者多个线程因为种种原因无法获得所需要的资源,导致一直无法执行的状态。

  • 1、活锁与死锁的区别
      活锁和死锁的区别在于:处于活锁的实体是在不断的改变状态,这就是所谓的“活”, 而处于死锁的实体表现为等待;活锁有可能自行解开,死锁则不能。活锁可以认为是一种特殊的饥饿。

  • 2、死锁活锁与饥饿的区别
      进程会处于饥饿状态是因为持续地有其它优先级更高的进程请求相同的资源。不像死锁或者活锁,饥饿能够被解开。例如,当其它高优先级的进程都终止时并且没有更高优先级的进程强占资源。

资料来源 多线程(一)线程与进程、Thread
博客作者 m0_37741420
前往答题
我的笔记