目录:
1. 相关概念
进程是执行程序的一次执行过程,是一个动态的概念。它是系统进行资源分配和调度的基本单位。进程之间相互独立。
通常在一个进程中可以包含若干个线程,当然一个进程中至少有一个线程,不然没有存在的意义。线程是程序执行的最小单位。
关键概念:
- 原子性: 指一个操作不会被中断
- 可见性: 当某一个线程修改了某一共享变量的值,其它线程能马上直到这个修改,并获得新的值。
- 有序性: 在并发情况下程序依旧可以有序的执行(受指令重排序影响,并发程序执行时可能会出现乱序)
2. 线程的生命周期
在使用线程之前,需要了解下它的生命周期,即线程运行过程中的几个阶段。
线程的生命周期包括五个阶段:
- 新建: 当创建了一个
Thread
类的对象后,此线程进入新建状态,此时线程还未被启动。 - 就绪: 调用了线程的
start()
方法后,此线程就进入了就绪状态;这时候线程处于等待CPU分配资源阶段,只要获得了CPU的资源,就可以开始执行。 - 运行: 当就绪的线程被调度并获得CPU资源时,便进入运行状态(开始执行
run()
方法)。 - 阻塞:
- 线程在运行时,可能会因为一些原因(如
wait()
、lock()
等方法)进入阻塞状态; - 进入阻塞状态的线程需要某些特殊的机制将其唤醒,如
notify()
方法、等待时间结束等; - 被唤醒的线程不会直接被执行,而是会进入到就绪状态,与其它线程一起竞争CPU资源。
- 线程在运行时,可能会因为一些原因(如
- 死亡: 当线程执行完毕或由于一些原因被杀死,线程就进入死亡状态,此线程将被销毁,同时释放资源。
3. 线程的创建
在了解的线程的生命周期之后,就可以开始使用线程了。
线程的创建方法有3种:
-
继承Thread类,重写run方法
public class TestThread extends Thread { public TestThread(String threadName) { super(threadName); } @Override public void run() { System.out.println("利用Thread创建线程:" + Thread.currentThread().getName()); } public static void main(String[] args) { new TestThread("线程1").start(); new TestThread("线程2").start(); } }
-
实现Runnable接口
public class TestRunnable implements Runnable { @Override public void run() { System.out.println("利用Runnable创建线程:" + Thread.currentThread().getName()); } public static void main(String[] args) { new Thread(new TestRunnable(), "线程1").start(); new Thread(new TestRunnable(), "线程2").start(); } }
-
实现Callable接口
public class TestCallable implements Callable<Boolean> { @Override public Boolean call() { System.out.println("利用Callable创建线程:" + Thread.currentThread().getName()); return true; } public static void main(String[] args) { ExecutorService executorService = Executors.newFixedThreadPool(4); for (int i = 0; i < 2; i++) { executorService.submit(new TestCallable()); } // 执行完所有任务后,关闭线程池(不再接受新任务)。如果线程池已经关闭,则调用没有其他效果。 executorService.shutdown(); } }
4. 线程的中止
Thread方法提供了一个stop()
方法,但它已经被标注为废弃的方法,并不推荐使用。原因是因为stop()
方法太过暴力,强行中止一个允许了一半的线程,可能会引起数据不一致的问题。
想要结束一个线程,一般来说有以下几种方法:
-
方式一:设置标志位
如果想要中止一个线程,最好的办法就是让他“自己中止”。可以通过设置标志位的方法,在线程的内部根据标志位的值选择是否需要中止线程。当然,为避免数据不一致的问题,线程应该选择一个安全的地方停止。
// 通过设置标志位来停止线程
public class StopThread implements Runnable {
private boolean stopFlag = false; // 线程停止的标志位
@Override
public void run() {
Date nowTime = null;
while(true) {
nowTime = new Date(System.currentTimeMillis());
System.out.println(new SimpleDateFormat("HH:mm:ss").format(nowTime));
if (stopFlag) {
//在合适的地方判断标志位的值,以选择是否停止线程
System.out.println("线程已停止");
break;
}
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public void setStopFlag(boolean stopFlag) {
this.stopFlag = stopFlag;
}
public static void main(String[] args) {
StopThread stopTest = new StopThread();
Thread thread = new Thread(stopTest);
thread.start();
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
stopTest.setStopFlag(true);
}
}
-
方式二:使用中断(interrupt)
JDK也为此提供的相关的支持,利用中断也可以实现类似的功能
方法 | 功能 |
---|---|
public void Thread.interrupt() | 中断线程 |
public boolean Thread.isInterrupted() | 判断线程是否被中断,可在括号中加上参数true,表示清除当前的中断状态 |
public static boolean Thread.interrupted() | 判断线程是否被中断,并清楚当前的中断状态,其实现其实就是内部调用并返回了currentThread().isInterrupted(true) |
public class StopByInterrupt {
public static void main(String[] args) throws InterruptedException {
Thread thread = new Thread(() -> {
Date nowTime = null;
while(true) {
nowTime = new Date(System.currentTimeMillis());
System.out.println(new SimpleDateFormat("HH:mm:ss").format(nowTime));
if (Thread.currentThread().isInterrupted()) {
//在合适的地方判断标志位的值,以选择是否停止线程
System.out.println("线程已停止");
break;
}
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
/*
* public static native void sleep() throws InterruptedException
* 类似sleep()和wait()这样的操作
* 会被interrupt打断,同时抛出InterruptedException异常
* 并清除中断标志位
*/
e.printStackTrace();
System.out.println("捕获了线程中sleep的InterruptedException异常");
// 我们需要对异常进行捕获并处理,这里选择再次中断自己,是为了让线程可以正常进入到中断处理程序中,以保证数据的一致性
Thread.currentThread().interrupt();
}
}
});
thread.start();
Thread.sleep(5000);
thread.interrupt();
}
}
5. 线程间通讯
在Java中,不同的线程之间具备一定的通讯能力。为了支持多线程之间的协作,JDK针对synchronized和Lock分别提供了相应的通讯方法。
5.1 synchronized中的通讯(wait() 和 notify())
JDK为synchronized提供了两个非常重要的接口线程:等待 wait()
方法和通知notify()
方法。这两个方法并不在Thread类中,而是在 Object 类中定义。这也意味着任何对象都可以调用这两个方法。
方法 | 功能 |
---|---|
public final void wait() throws InterruptedException | 在一个对象实例上调用wait() 方法后,当前线程就会在这个对象上等待(进入这个对象的等待队列) |
public final native void notify() | 在一个对象实例上调用notify() 方法后,它就会从这个对象的等待队列中随机唤醒一个线程 |
注意点:
wait()
和notify()
方法必须在synchronized语句中使用- 无论是
wait()
还是notify()
,执行前都需要首先获得目标对象的一个监视器 - 无论是
wait()
还是notify()
,执行后都会释放它所持有的监视器
public class TestWait {
public static void main(String[] args) {
Object communicationObject = new Object();
String[] products = new String[2];
Producer producer = new Producer(communicationObject, products);
Consumer consumer = new Consumer(communicationObject, products);
new Thread(producer, "生产者1").start();
new Thread(producer, "生产者2").start();
new Thread(producer, "生产者3").start();
new Thread(consumer, "消费者1").start();
new Thread(consumer, "消费者2").start();
new Thread(consumer, "消费者3").start();
}
}
class Producer implements Runnable {
private final Object communicationObject; // 通讯对象
private String[] products;
public Producer(Object communicationObject, String[] products) {
this.communicationObject = communicationObject;
this.products = products;
}
/**
* 生产者生产产品
* @return 表示是否生产成功
*/
public boolean produce() {
for (int i = 0; i < products.length; i++) {
if (null == products[i]) {
products[i] = String.valueOf(i);
System.out.println(Thread.currentThread().getName() + ": 生产了产品" + products[i]);
return true;
}
}
return false;
}
@Override
public void run() {
int number = 0;
// 生产产品
while (number < 5) {
synchronized (communicationObject) {
if (produce()) {
// 生产成功则通知消费者进行消费
number++;
communicationObject.notify();
} else {
// 仓库存满了,等待消费者消费
try {
communicationObject.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
class Consumer implements Runnable {
private final Object communicationObject; // 通讯对象
private String[] products;
public Consumer(Object communicationObject, String[] products) {
this.communicationObject = communicationObject;
this.products = products;
}
/**
* 消费者消费产品
* @return 表示是否消费成功
*/
public boolean consume() {
for (int i = 0; i < products.length; i++) {
if (products[i] != null) {
System.out.println(Thread.currentThread().getName() + ": 消费了产品" + products[i]);
products[i] = null;
return true;
}
}
return false;
}
@Override
public void run() {
int number = 0;
// 消费产品
while (number < 5) {
synchronized (communicationObject) {
if (consume()) {
// 消费之后通知生产者可以开始生产了
number++;
communicationObject.notify();
} else {
// 产品消费完了,等待生产
try {
communicationObject.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
5.2 Lock中的通讯(Condition)
与wait()
和 notify()
相似,在锁(Lock
)中提供了Condition
接口以实现线程间通讯的功能。它们的用法十分的相似,唯一的不同就是Condition
需要用在Lock
语句块中。
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class TestCondition {
public static void main(String[] args) {
Lock lock = new ReentrantLock(); // 可重入锁实例
Condition condition = lock.newCondition(); // 通过lock对象获取condition
String[] products = new String[2];
Producer producer = new Producer(lock, condition, products);
Consumer consumer = new Consumer(lock, condition, products);
new Thread(producer, "生产者1").start();
new Thread(producer, "生产者2").start();
new Thread(producer, "生产者3").start();
new Thread(consumer, "消费者1").start();
new Thread(consumer, "消费者2").start();
new Thread(consumer, "消费者3").start();
}
}
class Producer implements Runnable {
private final Lock lock; // 可重入锁实例
private final Condition condition; // Condition实例
private String[] products; // 产品仓库
public Producer(Lock lock, Condition condition, String[] products) {
this.lock = lock;
this.condition = condition;
this.products = products;
}
/**
* 生产者生产产品
* @return 表示是否生产成功
*/
public boolean produce() {
for (int i = 0; i < products.length; i++) {
if (null == products[i]) {
products[i] = String.valueOf(i);
System.out.println(Thread.currentThread().getName() + ": 生产了产品" + i);
return true;
}
}
return false;
}
@Override
public void run() {
int number = 0;
// 生产产品
while (number < 5){
lock.lock();
if (produce()) {
// 生产成功则通知消费者进行消费
number++;
condition.signal();
} else {
// 仓库存满了,等待消费者消费
try {
condition.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
lock.unlock();
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
class Consumer implements Runnable {
private final Lock lock;
private final Condition condition;
private String[] products;
public Consumer(Lock lock, Condition condition, String[] products) {
this.lock = lock;
this.condition = condition;
this.products = products;
}
/**
* 消费者消费产品
* @return 表示是否消费成功
*/
public boolean consume() {
for (int i = 0; i < products.length; i++) {
if (products[i] != null) {
System.out.println(Thread.currentThread().getName() + ": 消费了产品" + products[i]);
products[i] = null;
return true;
}
}
return false;
}
@Override
public void run() {
int number = 0;
// 消费产品
while (number < 5){
lock.lock();
if (consume()) {
// 消费之后通知生产者可以开始生产了
number++;
condition.signal();
} else {
// 产品消费完了,等待生产
try {
condition.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
lock.unlock();
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
5.3 挂起(suspend)和继续执行(resume)
已经被标注为废弃方法,不推荐使用!
原因是suspend()
挂起线程的同时,并不会释放锁资源。利用wait()
和 notify()
方法可以很好的代替它。
6. 加入(join,等待线程结束)和谦让(yeild)
一个线程的输入可能会依赖于另一个或者多个线程的输出,此时,这个线程就需要等待依赖线程执行完毕。JDK提供了jion()
方法来实现这个功能。
而yield()
则是一个比较有趣的方法,它会使当前的线程让出CPU,让出后再去重新竞争CPU的资源。
方法 | 功能 |
---|---|
public final void join() throws InterruptedException | 一直阻塞当前线程,直到目标线程执行完毕(无限期等待) |
public final synchronized void join(long millis) throws InterruptedException | 阻塞当前线程,等待目标线程执行,如果等待时间超过最大等待时间,就放弃等待继续往下执行 |
public static native void yield() | 静态方法:使当前线程让出CPU,让出后再去重新竞争CPU的资源 |
-
join()
方法的 本质是调用当前线程对象实例上的wait()
。// 相当于在线程中调用threadA.wait(); while (isAlive()) { wait(0); // synchronized void join(long millis) }
-
被等待的线程执行完毕之后,会在退出前调用
notifyAll()
方法通知所有等待程序继续执行。 因此,不要在应用程序中调用Thread实例上的类似wait()
或notify()
等,这可能会影响系统API或者被系统API影响。// JVM源码中 lock.notify_all(thread);
-
可以被
interrupt
打断
join
举例:
public class JoinThread {
public static void main(String[] args) throws InterruptedException {
Thread thread = new Thread(() -> {
System.out.println("进入:" + Thread.currentThread().getName());
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("退出:" + Thread.currentThread().getName());
}, "join测试线程");
thread.start();
// 等待join测试线程执行完毕
thread.join();
/*
* 等价于:
* synchronized (thread) {
* thread.wait();
* }
*/
System.out.println("join测试线程执行完毕,程序继续执行。");
}
}
yield()
举例:
public class YieldThread implements Runnable {
@Override
public void run() {
for (int i = 0; i < 10; i++) {
System.out.println(Thread.currentThread().getName() + "执行到:" + i);
try {
Thread.sleep(200);
} catch (InterruptedException e) {
e.printStackTrace();
}
if (0 == i % 2) {
Thread.yield();
}
}
}
public static void main(String[] args) {
YieldThread yieldRunnable = new YieldThread();
for (int i = 0; i < 3; i++) {
new Thread(yieldRunnable, "线程" + i).start();
}
}
}
/*
执行结果:
线程0执行到:0
线程1执行到:0
线程2执行到:0
线程2执行到:1
线程0执行到:1
... ...
*/
参考书籍: Java高并发程序设计(第二版)