目录
一、线程状态
1)NEW 刚创建。即new一个Thread实例,这时线程只是在堆内存中占据了一些空间,还没有开始运行。
2)RUNNABLE 就绪态。调用start方法后,线程便从NEW状态进入RUNNABLE就绪态,等待CPU的调度。
3)RUNNING 运行态。线程被CPU选中执行,为运行态。如果线程CPU时间片用完,或主动让出CPU时间片,则线程会从运行态回到就绪态。
4)BLOCKED 阻塞态。线程某个条件未满足,会进入阻塞态,待条件满足后,进入就绪态。如调用sleep方法会让线程进入阻塞态。
5)TERMINTED 结束。
二、线程管理API
Thread的如下方法已经被废弃,官方文档建议不适用这些有危险的代码:
- 暂停和恢复:suspend、resume
- 消亡:stop、destroy
线程阻塞和唤醒:
- sleep 线程自我休眠,休眠结束后会自己醒来。休眠中是BOLCKED状态,休眠结束后自动变成RUNNABLE状态
- wait/notify/notifyAll,等待,需要别人唤醒。如果没有别的线程来唤醒该线程,则该线程就会一直阻塞,无法再继续运行。
- join ,当前线程等待另外一个线程结束。
- interrupt,给另外一个线程发送中断信号,该线程收到中断信号后会触发InterruptedException(可解除阻塞??),程序员可以针对该异常做进一步的处理。
三、生产者消费者模型 - 多线程并发协作实践
生产者消费者模式是多线程并发协作的经典案例。生产者的作用是生产一定量的数据放到缓冲区中,并重复此过程;于此同时,消费者也在消费缓冲区中的数据。该模型需要保证生产者不会在缓冲区满时放入数据,消费者不会在缓冲区空时消耗数据,否则将引发缓冲区越界异常等。
假设有生产者和消费者两类线程,分别用于生产和消费数据,为了解耦生产者和消费者的关系,引入共享数据仓库。该共享仓库需要具有如下功能:
1)仓库满时,阻塞生产者线程;
2)仓库空时,阻塞消费者线程。
1. 使用wait/notifyAll实现生产者消费者模型
/**
* 生产者消费者模型测试
*/
public class ProConTest {
public static void main(String[] args) throws InterruptedException {
Storage storage = new Storage();
Thread p1 = new Thread(new Producer(storage));
p1.setName("生产者1");
Thread p2 = new Thread(new Producer(storage));
p2.setName("生产者2");
Thread c1 = new Thread(new Consumer(storage));
c1.setName("消费者1");
Thread c2 = new Thread(new Consumer(storage));
c2.setName("消费者2");
Thread c3 = new Thread(new Consumer(storage));
c3.setName("消费者3");
p1.start();
p2.start();
Thread.sleep(1000);
c1.start();
c2.start();
c3.start();
}
}
import java.util.ArrayList;
import java.util.List;
/**
* 共享数据区域
*/
public class Storage {
private final List<Integer> data = new ArrayList<>(4);
public String toString() {
return data.toString();
}
public synchronized void produce(Integer i) {
while (data.size() >= 10) {
try {
System.out.println("队列已满," + Thread.currentThread().getName() + "生产数据被阻塞");
this.wait();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
// 生产数据
data.add(i);
System.out.println(Thread.currentThread().getName() + "生产数据" + i);
// 唤醒所有在等待的线程
this.notifyAll();
}
public synchronized Integer consume() {
while (data.size() == 0) {
try {
System.out.println("队列为空," + Thread.currentThread().getName() + "消费数据被阻塞");
this.wait();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
Integer result = data.remove(data.size()-1 );
System.out.println(Thread.currentThread().getName() + "消费数据" + result);
this.notifyAll();
return result;
}
}
注意Storage的produce和consume方法中,判断队列为空/满的语句都是用的while,而不是if。如果使用if会出现数组越界访问异常,原因是:
假设有三个线程:consumer1、consumer2、producer。
当consumer1调用wait方法后,线程处于阻塞状态,将释放Storage 对象锁;
此时consumer2获取到锁,进入同步代码块执行,同样走到wait方法,进入阻塞状态,释放Storage 对象锁;
producer获取到锁,进入同步代码块执行,生产一个数据到仓库中,唤醒在等待的consumer1、consumer2;
consumer1获取到对象锁后,从wait方法退出,继续往下执行,从仓库中消费一个数据后,退出同步代码块,释放锁;
consumer2获取到对象锁后,从wait方法退出,继续往下执行,这时候consumer2再删除一个数据就会报错,因此consumer1删除一个元素后仓库已经为空了。
总结上述重复消费的原因是,线程从wait退出后,没有再次判断当前仓库的状态是否需要wait,因此,使用while可以使得线程从wait退出后,再次进行条件判断。
import java.util.Random;
/**
* 生产者线程
*/
public class Producer implements Runnable{
private Storage storage;
public Producer(Storage storage) {
this.storage = storage;
}
@Override
public void run() {
for(int i=0; i<4; i++) {
Integer data = new Random().nextInt(10);
storage.produce(data);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
}
/**
* 消费者线程
*/
public class Consumer implements Runnable{
private Storage storage;
public Consumer(Storage storage) {
this.storage = storage;
}
@Override
public void run() {
for (int i=0; i<4; i++) {
storage.consume();
}
}
}
2. 使用LinkedBlockingQueue实现生产者消费者模型
将 LinkedBlockingQueue 用作生产者和消费者的共享数据仓库。
import java.util.concurrent.LinkedBlockingQueue;
/**
* 生产者消费者模型测试
*/
public class ProConTest {
public static void main(String[] args) throws InterruptedException {
LinkedBlockingQueue<Integer> storage = new LinkedBlockingQueue<>(4);
Thread p1 = new Thread(new Producer(storage));
p1.setName("生产者1");
Thread p2 = new Thread(new Producer(storage));
p2.setName("生产者2");
Thread c1 = new Thread(new Consumer(storage));
c1.setName("消费者1");
Thread c2 = new Thread(new Consumer(storage));
c2.setName("消费者2");
Thread c3 = new Thread(new Consumer(storage));
c3.setName("消费者3");
p1.start();
p2.start();
Thread.sleep(1000);
c1.start();
c2.start();
c3.start();
}
}
import java.util.Random;
import java.util.concurrent.LinkedBlockingQueue;
/**
* 生产者线程
*/
public class Producer implements Runnable{
private LinkedBlockingQueue<Integer> storage;
public Producer(LinkedBlockingQueue<Integer> storage) {
this.storage = storage;
}
@Override
public void run() {
for(int i=0; i<4; i++) {
Integer data = new Random().nextInt(10);
System.out.println(Thread.currentThread().getName() + "生产数据" + data);
try {
storage.put(data);
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
}
import java.util.concurrent.LinkedBlockingQueue;
/**
* 消费者线程
*/
public class Consumer implements Runnable{
private LinkedBlockingQueue<Integer> storage;
public Consumer(LinkedBlockingQueue<Integer> storage) {
this.storage = storage;
}
@Override
public void run() {
for (int i=0; i<4; i++) {
Integer result = null;
try {
result = storage.take();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println(Thread.currentThread().getName() + "消费数据" + result);
}
}
}
四、线程暂停和终止
线程被动终止:
可以通过调用线程的interrupt方法是线程被动终止,但不建议这种方式,因为被终止的线程毫无准备,如果它持有一些数据库连接、文件、锁等资源,被动终止后就来不及释放这些资源。
线程主动终止:
建议使用线程主动终止的方式。可以在线程的执行体中监听一个共享变量,线程内部通过判断该共享变量的值,决定是否主动终止线程,终止之前可以进行资源的释放操作。外部如果要终止该线程,可以通过修改该共享变量的值实现。
示例:
public class InterruptTest {
public static void main(String[] args) throws InterruptedException {
Thread_1 thread1 = new Thread_1("thread-1");
Thread_2 thread2 = new Thread_2("thread-2");
thread1.start();
thread2.start();
// 让线程运行一会后终止
Thread.sleep(1000);
thread1.interrupt();
thread2.flag = true;
System.out.println("Main thread is existing");
}
}
class Thread_1 extends Thread {
public Thread_1(String name) {
super(name);
}
@Override
public void run() {
while(!interrupted()) {
System.out.println(Thread.currentThread().getName() + " is running");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
System.out.println(Thread.currentThread().getName() + " is existing");
}
}
class Thread_2 extends Thread {
public Thread_2(String name) {
super(name);
}
// 内存可见的共享变量。别的线程一点修改flag,所有线程都可以看到该修改,然后从内存中获取最新的值
public volatile boolean flag = false;
@Override
public void run() {
while(!flag) {
System.out.println(Thread.currentThread().getName() + " is running");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
System.out.println(Thread.currentThread().getName() + " is existing");
}
}
运行结果:
thread-1 is running |
可以看到thread-1被main线程调用interrupt后被动终止,触发interrupt异常。thread-2通过监听共享变量flag,可以主动优雅终止线程。
五、线程死锁
每个线程互相持有别人需要的锁(哲学家吃面问题)。
预防死锁,需要对加锁的资源进行等级排序,严格按顺序依次加锁。
死锁示例代码:
package org.example.thread.deadlock;
public class DeadLockTest {
static final Integer r1 = 1;
static final Integer r2 = 2;
public static void main(String[] args) {
new Thread3("thread-3").start();
new Thread4("thread-4").start();
}
}
class Thread3 extends Thread {
public Thread3 (String name) {
super(name);
}
public void run() {
synchronized (DeadLockTest.r2) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
synchronized (DeadLockTest.r1) {
System.out.println("thread-3 is running");
}
}
}
}
class Thread4 extends Thread {
public Thread4 (String name) {
super(name);
}
public void run() {
synchronized (DeadLockTest.r1) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
synchronized (DeadLockTest.r2) {
System.out.println("thread-4 is running");
}
}
}
}
执行上述main方法,可以看到控制台程序一直在运行,但没有任何输出:
按顺序加锁来预防死锁:将上述代码的加锁顺序都控制为先对r1加锁,再对r2加锁,则程序可以正常运行,结果如下:
六、守护(后台)线程
普通线程的结束,是run方法运行结束
守护线程的结束,是run方法运行结束,或main函数执行结束。
守护线程永远不要访问资源,如文件或数据库等。因为main函数执行结束后,守护线程将被动结束,来不及释放它持有的资源。
package org.example.thread.daemon;
public class DaemonTest {
public static void main(String[] args) throws InterruptedException {
Thread thread = new Thread(new Runnable3());
// 增加这句后,如果main线程退出,则子线程thread也随之退出
thread.setDaemon(true);
thread.start();
Thread.sleep(1000);
System.out.println("main thread is existing");
}
}
class Runnable3 implements Runnable {
@Override
public void run() {
while (true) {
System.out.println(Thread.currentThread().getName() + " is running");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
}
七、线程查看工具
-- 待补充