文章目录

  • 1.线程生命周期
  • 2.同步锁与synchronized
  • 3.Object与Thread
  • 4.Thread和Runnable
  • 5.用户线程与守护线程
  • 6.线程生命周期中的阻塞
  • 7.生产者消费者实例


1.线程生命周期

先放一张经典的图:

java 为什么多线程可以提高效率 java多线程不会进入阻塞_阻塞状态

显然,在Java中,线程共包括以下5种状态

  1. 新建状态(New): 线程对象刚创建时,就进入了新建状态。例如,Thread thread = new Thread()。
  2. 就绪状态(Runnable): 也被称为"可执行状态"。线程对象被创建后,其它线程调用了该对象的start()方法,从而来启动该线程。例如,thread.start()。处于就绪状态的线程,随时可能被CPU调度执行。
  3. 运行状态(Running): 线程获取CPU权限进行执行。需要注意的是,线程只能从就绪状态进入到运行状态。
  4. 阻塞状态(Blocked): 阻塞状态是线程因为某种原因放弃CPU使用权,暂时停止运行。直到线程进入就绪状态,才有机会转到运行状态。阻塞的情况分三种:
  1. 等待阻塞 – 通过调用线程的wait()方法,让线程等待某工作的完成。
  2. 同步阻塞 – 线程在获取synchronized同步锁失败(因为锁被其它线程所占用),它会进入同步阻塞状态。
  3. 其他阻塞 – 通过调用线程的sleep()或join()或发出了I/O请求时,线程会进入到阻塞状态。当sleep()状态超时、join()等待线程终止或者超时、或者I/O处理完毕时,线程重新转入就绪状态。
  1. 死亡状态(Dead): 线程执行完了或者因异常退出了run()方法,该线程结束生命周期。

2.同步锁与synchronized

在java中,每一个对象有且仅有一个同步锁。这也意味着,同步锁是依赖于对象而存在。当我们调用某对象的synchronized方法时,就获取了该对象的同步锁。
不同线程对同步锁的访问是互斥的。synchronized的基本规则总结为下面3条:

  • 当一个线程访问"某对象"的"synchronized方法"或者"synchronized代码块"时,其他线程对"该对象"的该"synchronized方法"或者"synchronized代码块"的访问将被阻塞。
  • 当一个线程访问"某对象"的"synchronized方法"或者"synchronized代码块"时,其他线程仍然可以访问"该对象"的非同步代码块。
  • 当一个线程访问"某对象"的"synchronized方法"或者"synchronized代码块"时,其他线程对"该对象"的其他的"synchronized方法"或者"synchronized代码块"的访问将被阻塞。

synchronized方法示例

public synchronized void foo1() {
	System.out.println("synchronized methoed");
}

synchronized代码块

public void foo2() {
	synchronized (this) {
		System.out.println("synchronized methoed");
	}
}

synchronized代码块中的this是指当前对象。也可以将this替换成其他对象,例如将this替换成obj,则foo2()在执行synchronized(obj)时就获取的是obj的同步锁。
synchronized代码块可以更精确的控制冲突限制访问区域,有时候表现出比synchronized方法更高的效率。

注意,在Java中每个类也是一个对象(不知道的同学可以去了解一下反射),所以,一个类中即用static又用synchronized修饰的方法具有的锁是相对于这个类(相当于锁在该类的class或者classloader对象上)而言的,无论在多少个线程有多少个实例对象,它们都共享该锁。

关于"实例锁"和"全局锁"有一个很形象的例子:

pulbic class Something {
	public synchronized void isSyncA(){}
	public synchronized void isSyncB(){}
	public static synchronized void cSyncA(){}
	public static synchronized void cSyncB(){}
}

假设,Something有两个实例x和y。分析下面4组表达式获取的锁的情况。

  • x.isSyncA()与x.isSyncB():不能被同时访问。因为isSyncA()和isSyncB()都是访问同一个对象(对象x)的同步锁!
  • x.isSyncA()与y.isSyncA():可以同时被访问。因为访问的不是同一个对象的同步锁,x.isSyncA()访问的是x的同步锁,而y.isSyncA()访问的是y的同步锁。
  • x.cSyncA()与y.cSyncB():不能被同时访问。因为cSyncA()和cSyncB()都是static类型,x.cSyncA()相当于Something.isSyncA(),y.cSyncB()相当于Something.isSyncB(),因此它们共用一个同步锁,不能被同时反问。
  • x.isSyncA()与Something.cSyncA():可以被同时访问。因为isSyncA()是实例方法,x.isSyncA()使用的是对象x的锁;而cSyncA()是静态方法,Something.cSyncA()可以理解对使用的是"类的锁"。因此,它们是可以被同时访问的。

3.Object与Thread

Object与Thread是Java多线程的基础,因为每个Object与Thread都包含有与线程相关的知识。

  • Object类,定义了wait(), notify(), notifyAll()等休眠/唤醒函数。
  • Thread类,定义了一些列的线程操作函数。例如,sleep()休眠函数, interrupt()中断函数,getName()获取线程名称等。而且Thread对象还有start()就绪函数,join()阻塞函数,run()执行函数等。

Object.java定义了wait(), notify()和notifyAll()等接口。wait()的作用是让当前线程进入等待状态,同时,wait()也会让当前线程释放它所持有的锁。
而notify()和notifyAll()的作用,则是唤醒当前对象上的等待线程;notify()是唤醒单个线程,而notifyAll()是唤醒所有的线程。

Object类中关于等待/唤醒的API详细信息如下:

  • notify():唤醒在此对象监视器上等待的单个线程。
  • notifyAll():唤醒在此对象监视器上等待的所有线程。
  • wait():让当前线程处于"等待(阻塞)状态",“直到其他线程调用此对象的notify()方法或notifyAll()方法”,当前线程被唤醒(进入"就绪状态")。
  • wait(long timeout):让当前线程处于"等待(阻塞)状态",“直到其他线程调用此对象的notify()方法或notifyAll()方法,或者超过指定的时间量”,当前线程被唤醒(进入"就绪状态")。当timeout为0时,表示无限等待,直到被notify()或notifyAll()唤醒,即等价于wait()方法。
  • wait(long timeout, int nanos):让当前线程处于"等待(阻塞)状态",“直到其他线程调用此对象的notify()方法或notifyAll()方法,或者其他某个线程中断当前线程,或者已超过某个实际时间量”,当前线程被唤醒(进入"就绪状态")。

notify(), wait()依赖于"同步锁",而"同步锁"是对象锁持有,并且每个对象有且仅有一个!这就是为什么notify(), wait()等函数定义在Object类,而不是Thread类中的原因。

执行wait()进入等待状态的线程,有下面4种唤醒方式。

  • 通过notify()唤醒。
  • 通过notifyAll()唤醒。
  • 通过interrupt()中断唤醒。
  • 如果是通过调用wait(long timeout)进入等待状态的线程,当时间超时的时候,也会被唤醒。

至于Thread,看下面

4.Thread和Runnable

Thread和Runnable的相同点:都是"多线程的实现方式"。Thread和Runnable的区别:

  • Thread是类,而Runnable是接口;
  • Thread本身是实现了Runnable接口的类。我们知道"一个类只能有一个父类,但是却能实现多个接口",因此Runnable具有更好的扩展性。
  • 此外,Runnable还可以用于"资源的共享"。即,多个线程都是基于某一个Runnable对象建立的,它们会共享Runnable对象上的资源。通常,建议通过"Runnable"实现多线程!

Thread类包含start()和run()方法两个至关重要的方法:

  • start(): 它的作用是启动一个新线程,新线程会执行相应的run()方法。start()不能被重复调用。
  • run(): run()就和普通的成员方法一样,可以被重复调用。单独调用run()的话,会在当前线程中执行run(),而并不会启动新线程!

Thread类中其他常用的方法:

  • join(): 作用是让"主线程/父线程"等待"子线程"结束之后才能继续运行。
  • interrupt(): 可以中断本线程,同时设置中断标记为true。本线程中断自己是允许的;其它线程调用本线程的interrupt()方法时,会通过checkAccess()检查权限,可能抛SecurityException异常。
    注意,interrupt()方法和InterruptException异常,是java专门用来处理线程阻塞的。如果因为一些特殊的原因,想提前中断一些阻塞的线程,以让他们提前解除阻塞状态,然后继续执行下去。只需要在其他线程调用指定线程的interrupt()方法即可,这时候原来阻塞的对应的线程就会抛出InterruptException异常,通过catch捕获异常就可以继续往下面执行了。比如线程方法sleep()和Object的实例方法wait(),都会导致当前线程阻塞,这时候就可以通过interrupt()方法来提前退出阻塞状态。
  • isInterrupted(): 判断线程的中断标记是不是为true。
  • interrupted(): 除了返回中断标记之外,它还会清除中断标记(即将中断标记设为false)。
  • setName(String)与getName: 设置和获取线程名。
  • setPriority(int)与getPriority: 设置和获取优先级。
  • isAlive(): 线程是否还存活(线程在start后还没有结束的话就算是存活)。
  • setDaemon(boolean): 是否设置当前线程为守护线程(守护线程的概念下面再讲)。
  • isDaemon(): 是否是守护线程。

Thread类中常用的静态方法:

  • yield(): 作用是让步。它能让当前线程由"运行状态"进入到"就绪状态",从而让其它具有相同优先级的等待线程获取执行权;但是,并不能保证在当前线程调用yield()之后,其它具有相同优先级的线程就一定能获得执行权;也有可能是当前线程又进入到"运行状态"继续运行!
  • sleep(long): 可以让当前线程休眠,即当前线程会从"运行状态"进入到"休眠(阻塞)状态"。sleep()会指定休眠时间,线程休眠的时间会大于/等于该休眠时间(因为代码运行需要时间,很难精确);在线程重新被唤醒时,它会由"阻塞状态"变成"就绪状态",从而等待cpu的调度执行。wait()会释放对象的同步锁,而sleep()则不会释放锁。
  • currentThread(): 返回当前运行的线程。

而Runnable接口只有一个run方法,它必须依赖Thread才能实现多线程。

5.用户线程与守护线程

java中有两种线程:用户线程和守护线程。可以通过isDaemon()方法来区别它们:如果返回false,则说明该线程是"用户线程";否则就是"守护线程"。
用户线程一般用户执行用户级任务,而守护线程也就是"后台线程",一般用来执行后台任务。需要注意的是:Java虚拟机在"用户线程"都结束后会后退出。
在一些运行的主线程中创建新的子线程时,子线程的优先级被设置为等于"创建它的主线程的优先级",当且仅当"创建它的主线程是守护线程"时"子线程才会是守护线程"。

当Java虚拟机启动时,通常有一个单一的非守护线程(该线程通过是通过main()方法启动)。JVM会一直运行直到下面的任意一个条件发生,JVM就会终止运行:

  • 调用了exit()方法,并且exit()有权限被正常执行。
  • 所有的"非守护线程"都死了(即JVM中仅仅只有"守护线程")。

每一个线程都被标记为"守护线程"或"用户线程"。当只有守护线程运行时,JVM会自动退出。

6.线程生命周期中的阻塞

与阻塞相关的方法有:Object的wait、notify、notifyAll,Thread的sleep、yield、interrupt、join、isInterrupted、interrupted。与阻塞相关的关键字有synchronized。

其中synchronized导致的同步阻塞最易理解,只是多个线程抢占一把锁而已。

而wait导致的等待阻塞可以被notify/notifyAll唤醒,甚至interrupt方法也可以。为什么interrupt()方法可以提前中断阻塞呢?其实是因为每个线程都会有一个中断状态位,暂且叫做interruptStatus吧。当前执行sleep()和wait()这些方法的时候,当前线程会把该interrruptStatus状态位设置为true,以标识当前线程为阻塞状态。当调用该线程的Interrupt()方法的话,就会重置interruptStatus状态为为false。而sleep()和wait()方法内部会不断地轮询检查InterruptStatus状态值,如果某一时刻变为false的时候,当前线程就会中断阻塞状态,通过抛出InterrupException的方式来中断阻塞状态,然后继续执行下去。

至于sleep、yield、join导致的其他阻塞也有各种方法可以中断或者退出。

如果本线程是处于阻塞状态:调用线程的wait(), wait(long)或wait(long, int)会让它进入等待(阻塞)状态,或者调用线程的join(),join(long),join(long, int),sleep(long),sleep(long, int)也会让它进入阻塞状态。
若线程在阻塞状态时调用了它的interrupt()方法,那么它的"中断状态"会被清除并且会收到一个InterruptedException异常。例如,线程通过wait()进入阻塞状态,此时通过interrupt()中断该线程;调用interrupt()会立即将线程的中断标记设为"true",但是由于线程处于阻塞状态,所以该"中断标记"会立即被清除为"false",同时,会产生一个InterruptedException的异常。
如果线程被阻塞在一个Selector选择器中,那么通过interrupt()中断它时;线程的中断标记会被设置为true,并且它会立即从选择操作中返回。如果不属于前面所说的情况,那么通过interrupt()中断线程时,它的中断标记会被设置为"true"。
中断一个"已终止的线程"不会产生任何操作。

Thread中的stop()和suspend()方法,由于固有的不安全性,已经建议不再使用!所以终止线程的方法只有:

  • 终止处于"阻塞状态"的线程:通常,我们通过"中断"方式终止处于"阻塞状态"的线程。当线程由于被调用了sleep(), wait(), join()等方法而进入阻塞状态;
    若此时调用线程的interrupt()将线程的中断标记设为true。由于处于阻塞状态,中断标记会被清除,同时产生一个InterruptedException异常。
    将InterruptedException放在适当的位置(try catch中)就能终止线程。
  • 终止处于"运行状态"的线程:通常,我们通过"标记"方式终止处于"运行状态"的线程。其中,包括"中断标记"和"额外添加标记"。
    isInterrupted()是判断线程的中断标记是不是为true。当线程处于运行状态,并且我们需要终止它时;可以调用线程的interrupt()方法,使用线程的中断标记为true,
    即isInterrupted()会返回true。此时,就会退出while循环。或者自己添加一个volatile修饰的flag来终止线程。

    综合线程处于"阻塞状态"和"运行状态"的终止方式,比较通用的终止线程的形式如下:
@Override
public void run() {
	try {
		// 1. isInterrupted()保证,只要中断标记为true就终止线程。
		while (!isInterrupted()) {
			// 执行任务...
		}
	} catch (InterruptedException ie) {
		// 2. InterruptedException异常保证,当InterruptedException异常产生时,线程被终止。
	}
}

7.生产者消费者实例

下面通过wait()/notify()方式实现生产者/消费者模型:

interface DepotInte {
	void produce(int val);
	void consume(int val);
}
// 使用接口的理由是以后还有其他版本(如使用java.util.concurrent.locks.Lock
// 与java.util.concurrent.locks.Condition)的实现的生产者消费者实例。

class Producer {
	private DepotInte depot;

	public Producer(DepotInte depot) {
		this.depot = depot;
	}

	public void produce(int val) {
		new Thread(() -> depot.produce(val)).start();
	}
}

class Consumer {
	private DepotInte depot;

	public Consumer(DepotInte depot) {
		this.depot = depot;
	}

	public void consume(int val) {
		new Thread(() -> depot.consume(val)).start();
	}
}

class Depot implements DepotInte {
	private int size;
	private int capacity;

	public Depot(int capacity) {
		this.capacity = capacity;
		this.size = 0;
	}

	public synchronized void produce(int val) {
		try {
			int left = val;
			while (left > 0) {
				while (this.size >= this.capacity) wait();
				int inc = (this.size + left) > this.capacity ? (this.capacity - this.size) : left;
				this.size += inc;
				left -= inc;
				System.out.println(Thread.currentThread().getName() + " produce(" + val + ") --> left=" + left +
						", inc=" + inc + ", size=" + this.size);
				notifyAll();
			}
		} catch (InterruptedException e) {
			System.out.println("produce InterruptedException");
		}
	}

	public synchronized void consume(int val) {
		try {
			int left = val;
			while (left > 0) {
				while (this.size <= 0) wait();
				int dec = (this.size > left) ? left : this.size;
				this.size -= dec;
				left -= dec;
				System.out.println(Thread.currentThread().getName() + " consume(" + val + ") --> left=" + left +
						", dec=" + dec + ", size=" + this.size);
				notifyAll();
			}
		} catch (InterruptedException e) {
			System.out.println("consume InterruptedException");
		}
	}
}

    public static void main(String[] args) {
        Depot depot = new Depot(100);
        Producer producer = new Producer(depot);
        Consumer consumer = new Consumer(depot);

        producer.produce(60);
        producer.produce(120);
        consumer.consume(90);
        consumer.consume(150);
        producer.produce(110);
    }