今天来分析下Java中的线程。
大纲如下:
- 1. 线程的概念。
- 2. Java创建线程的方式。
- 3. 线程的常用方法。
- 4. 线程的状态切换。
- 5. 如何终止一个线程?
- 6. 线程的优先级。
- 7. 线程间的协作。
1. 线程的概念
进程是操作系统中进行保护和资源分配的基本单位,操作系统分配资源以进程为基本单位。而线程是进程的组成部分,线程共享着所属进程的内存地址,所以线程间互相通信就简单的多,通过共享进程级全局变量即可实现。一个进程最少有一个线程,线程是处理器调度的基本单位。在Java中,单核cpu场景下,多个线程不是同时执行的,而是通过cpu切换着执行。
2. Java创建线程的方式
共有三种方式,继承Thread类,实现Runnable接口,通过Future和Callable。
package io.kzw.advance.csdn_blog;
import java.util.concurrent.Callable;
import java.util.concurrent.FutureTask;
public class ThreadTest {
public static void main(String[] args) {
// 创建方式 - 继承Thread类
MyThread thread0 = new MyThread();
thread0.start();
// 创建方式 - 实现Runnable接口
Runnable runnable = new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + " start");
}
};
Thread thread1 = new Thread(runnable, "Thread1");
thread1.start();
// 创建方式 - 通过FutureTask和Callable
FutureTask<Integer> futureTask = new FutureTask<>(new Callable<Integer>() {
@Override
public Integer call() throws Exception {
System.out.println(Thread.currentThread().getName() + " start");
return 1;
}
});
Thread thread2 = new Thread(futureTask, "Thread2");
thread2.start();
try {
// 阻塞地去拿结果,也可以传入超时时间
System.out.println("线程2的返回值:" + futureTask.get());
} catch (Exception e) {
e.printStackTrace();
}
}
public static class MyThread extends Thread {
// 可以给线程设置name
public MyThread() {
super("MyThread");
}
@Override
public void run() {
super.run();
System.out.println(Thread.currentThread().getName() + " start");
}
}
}
执行输出:
MyThread start
Thread1 start
Thread2 start
线程2的返回值:1
三种方式的对比
(1) 使用Runnable和Future的方式。
优势:
由于Java只支持单继承,使用Runnable和Future的方式,可以让线程类继承其他类。在这种方式下,多个线程可以共享一个target对象(Runnable or FutureTask),非常适合多个相同线程来处理同一份资源的情况。达到比较好的解耦效果,将cpu,代码和数据分开,比较符合面向对象的设计。
劣势:
如果要使用当前线程,只能通过Thread.currentThread()的方式。
(2) 使用继承Thread的方式。
优势:
编写简单,如果要使用当前线程,直接使用this即可获取。
劣势:
不能再继承其他类。
(3) 使用Runnable和Future方式的对比。
* Callable规定(重写)的方法是call(),Runnable规定(重写)的方法是run()。
* Callable的任务执行后可返回值,而Runnable的任务是不能返回值的。
* call方法可以抛出异常,run方法不可以。
* 运行Callable任务可以拿到一个Future对象,表示异步计算的结果。它提供了检查计算是否完成的方法,以等待计算的完成,并检索计算的结果。通过Future对象可以了解任务执行情况,可取消任务的执行,还可获取执行结果。
3. 线程的常用方法
start | 调用该方法,使线程从新建状态进入就绪状态,到线程队列里排队,等待cpu分配资源执行 |
run | 定义线程对象被调度之后执行的操作,是系统调用而不是用户调用 |
sleep | 优先级高的线程可以在它的run()中调用sleep方法,使自己放弃cpu资源,睡眠一段时间,但是不会释放持有的锁 |
isAlive | 线程处于新建状态时,调用该方法会返回false,而在线程还没执行完任务,即没有进入死亡状态之前,线程调用isAlive()方法返回true |
currentThread | Thread类中的类方法,可以用类名调用,该方法返回当前正在使用CPU资源的线程 |
interrupt | 当一个线程运行时,另一个线程可以调用对应的Thread对象的interrupt()方法来中断它,该方法只是在目标线程中设置一个标志,表示它已经被中断,并立即返回。这里需要注意的是,如果只是单纯的调用interrupt()方法,线程并没有实际被中断,会继续往下执行,通常配合isInterrupted()使用,来达到中断任务执行的效果。 |
join | 等待某个线程执行完毕,再执行下面的逻辑代码 |
yield | 暂停正在执行的线程对象,把正在占用的cpu资源让出来,自身重新进入就绪状态,然后各就绪线程开始抢占cpu执行 |
setPriority | 更改线程的优先级。系统提供了三个优先级常量:NORM_PRIORITY = 5; MAX_PRIORITY = 10; MIN_PRIORITY = 1; |
setDaemon | java中线程分为两种类型:用户线程和守护线程。通过Thread.setDaemon(false)设置为用户线程;通过Thread.setDaemon(true)设置为守护线程。如果不设置次属性,默认为用户线程。 1. 主线程结束后用户线程还会继续运行,JVM存活;主线程结束后守护线程和JVM的状态由下面第2条确定。 2. 如果没有用户线程,都是守护线程,那么JVM结束(随之而来的是所有的一切烟消云散,包括所有的守护线程) |
wait | 属于Object的方法,但是放在这边,因为经常在线程中使用。必须在synchronized方法或者代码块中使用,当线程执行wait()方法时候,会释放当前的锁,然后让出CPU,进入等待状态。只有当 notify/notifyAll() 被执行时候,才会唤醒一个或多个正处于等待状态的线程,然后继续往下执行,直到执行完synchronized 代码块的代码或是中途遇到wait() ,再次释放锁 |
notify | notify方法只唤醒一个等待(对象的)线程并使该线程开始执行。所以如果有多个线程等待一个对象,这个方法只会唤醒其中一个线程,选择哪个线程取决于操作系统对多线程管理的实现 |
notifyAll | notifyAll 会唤醒所有等待(对象的)线程,尽管哪一个线程将会第一个处理取决于操作系统的实现。如果当前情况下有多个线程需要被唤醒,推荐使用notifyAll 方法。比如在生产者-消费者里面的使用,每次都需要唤醒所有的消费者或是生产者,以判断程序是否可以继续往下执行 |
4. 线程的状态切换
线程的状态分为以下5种:
(1) 新建状态(New): 新创建一个线程对象。
(2) 就绪状态(Runnable): 线程创建后,其他线程调用该线程对象的start()方法。该状态的线程将会放入到可运行线程池中,等待获取cpu的使用权。
(3) 运行状态(Running): 就绪状态的线程获取到cpu,执行程序代码。
(4) 阻塞状态(Blocked): 阻塞状态是线程因为某种原因放弃了cpu使用权,暂时停止执行。直到线程进入就绪状态,才有机会转到运行状态。阻塞的情况分为3种:
4.1 等待阻塞:运行的线程执行wait()方法,JVM会将该线程放入等待池中。
4.2 同步阻塞:运行的线程在获取对象的同步锁时,若该同步锁被别的线程占用,则JVM会把该线程放入锁池中。
4.3 其他阻塞:运行的线程执行sleep()或join()方法,或者发出了I/O请求时,JVM会把该线程置为阻塞状态。当sleep()状态超时、join()等待线程终止或者超时、或者I/O处理完毕时,线程重新转入就绪状态。
(5) 死亡状态(Dead):线程执行完了或者因异常退出了run()方法,该线程结束生命周期。
画个草图理解下:
5. 如何终止一个线程?
停止一个线程意味着在任务处理完任务之前停掉正在做的操作,也就是放弃当前的操作。停止一个线程可以用Thread.stop()方法,但最好不要用它。虽然它确实可以停止一个正在运行的线程,但是这个方法是不安全的,而且是已被废弃的方法。
在java中有以下3种方法可以终止正在运行的线程:
(1) 使用退出标志,使线程正常退出,也就是当run方法完成后线程终止。
(2) 使用stop方法强行终止,但是不推荐这个方法,因为stop和suspend及resume一样都是过期作废的方法。
* stop方法是过时的。
* stop方法会导致代码逻辑不完整,stop方法是一种"恶意" 的中断,一旦执行stop方法,即终止当前正在运行的线程,不管线程逻辑是否完整,这是非常危险的。比如直接调用stop函数,因为不能在run()里面收到停止回调,会导致IO资源没有得到释放,代码逻辑出现问题(如线程终止则把下载标记设置为false)等。
* 在多线程同步里,stop方法却会带来更大的麻烦,它会丢弃所有的锁,导致原子逻辑受损。
(3) 使用interrupt方法中断线程。
// 使用标记的方式中断
private static boolean isRunning = true;
public static void main(String[] args) {
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
while (isRunning) {
try {
Thread.sleep(100L);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("running...");
}
}
});
thread.start();
try {
Thread.sleep(1000L);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
isRunning = false;
}
}
输出:
running...
running...
running...
running...
running...
running...
running...
running...
running...
running...
public static void main(String[] args) {
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
// 使用interrupt()和isInterrupted()的方式
while (!Thread.currentThread().isInterrupted()) {
System.out.println("running...");
}
}
});
thread.start();
try {
Thread.sleep(100L);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
thread.interrupt();
}
}
running...
running...
running...
running...
running...
...............
running...
Process finished with exit code 0
最后再来解释下这三个方法:
public static boolean interrupted()
public boolean isInterrupted()
public void interrupt()
(1) public static boolean interrupted():
测试当前线程是否已经中断。返回的是上一次的中断状态,并且会清除该状态,所以连续调用两次,第一次返回true,第二次返回false。
(2) public boolean isInterrupted() :
测试线程当前是否已经中断,但是不能清除状态标识。
(3) public void interrupt() :
只是改变中断状态,不会中断一个正在运行的线程,需要用户自己去监视线程的状态并做处理。
6. 线程的优先级
线程的优先级用数字来表示,默认范围是1到10,即Thread.MIN_PRIORITY到Thread.MAX_PRIORTY。
一个线程的默认优先级是5,即Thread.NORM_PRIORTY
对优先级操作的方法:
int getPriority(): 得到线程的优先级。
void setPriority(int newPriority): 当线程被创建后,可通过此方法改变线程的优先级。
必须指出的是:线程的优先级无法保障线程的执行次序,只不过,优先级高的线程获取CPU资源的概率较大。
private static final class MyThread extends Thread {
private final String message;
MyThread(String message) {
this.message = message;
}
public void run() {
for (int i = 0; i < 3; i++) {
System.out.println(message + " " + getPriority());
}
}
}
public static void main(String args[]) {
Thread t1 = new MyThread("T1");
t1.setPriority(Thread.MIN_PRIORITY);
t1.start();
Thread t2 = new MyThread("T2");
t2.start();
Thread t3 = new MyThread("T3");
t3.setPriority(Thread.MAX_PRIORITY);
t3.start();
}
执行输出:
T2 5
T3 10
T3 10
T3 10
T1 1
T1 1
T1 1
T2 5
T2 5
7. 线程间的协作
线程间协作的两种方式:wait、notify、notifyAll和Condition。
最经典的生产者-消费者模型:当队列满时,生产者需要等待队列有空间才能继续往里面放入商品,而在等待的期间内,生产者必须释放对临界资源(即队列)的占用权。因为生产者如果不释放对临界资源的占用权,那么消费者就无法消费队列中的商品,就不会让队列有空间,那么生产者就会一直无限等待下去。因此,一般情况下,当队列满时,会让生产者交出对临界资源的占用权,并进入挂起状态。然后等待消费者消费了商品,然后消费者通知生产者队列有空间了。同样地,当队列空时,消费者也必须等待,等待生产者通知它队列中有商品了。这种互相通信的过程就是线程间的协作。
(1) wait()、notify()和notifyAll()
wait()、notify()和notifyAll()是Object类中的方法,都是native修饰的。
public static void main(String[] args) {
Object lock = new Object();
Thread1 thread1 = new Thread1(lock);
Thread2 thread2 = new Thread2(lock);
thread1.start();
try {
Thread.sleep(200);
} catch (InterruptedException e) {
e.printStackTrace();
}
thread2.start();
}
private static class Thread1 extends Thread {
private final Object lock;
Thread1(Object lock) {
this.lock = lock;
}
@Override
public void run() {
synchronized (lock) {
System.out.println("线程1获取到了锁");
try {
System.out.println("线程1 wait");
lock.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("线程1" + "获取到了锁");
}
}
}
static class Thread2 extends Thread {
private final Object lock;
Thread2(Object lock) {
this.lock = lock;
}
@Override
public void run() {
synchronized (lock) {
System.out.println("线程2获取到了锁");
lock.notify();
// 这边调用notify后,必须等线程2释放锁,线程1才能重新获取到锁
System.out.println("调用了object.notify()");
}
System.out.println("线程2释放了锁");
}
}
执行输出:
线程1获取到了锁
线程1 wait
线程2获取到了锁
调用了object.notify()
线程2释放了锁
线程1获取到了锁
(2) Condition
Condition是在java 1.5中才出现的,它用来替代传统的Object的wait()、notify()实现线程间的协作,相比使用Object的wait()、notify(),使用Condition1的await()、signal()这种方式实现线程间协作更加安全和高效。因此通常来说比较推荐使用Condition,阻塞队列实际上是使用了Condition来模拟线程间协作。
Condition是个接口,基本的方法就是await()和signal()方法。
Condition依赖于Lock接口,生成一个Condition的基本代码是lock.newCondition() 。
调用Condition的await()和signal()方法,都必须在lock保护之内,就是说必须在lock.lock()和lock.unlock之间才可以使用。
Conditon中的await()对应Object的wait()。
Condition中的signal()对应Object的notify()。
Condition中的signalAll()对应Object的notifyAll()。
private int queueSize = 10;
private PriorityQueue<Integer> queue = new PriorityQueue<Integer>(queueSize);
private Lock lock = new ReentrantLock();
private Condition notFull = lock.newCondition();
private Condition notEmpty = lock.newCondition();
public static void main(String[] args) {
ThreadTest test = new ThreadTest();
Producer producer = test.new Producer();
Consumer consumer = test.new Consumer();
producer.start();
consumer.start();
}
private class Consumer extends Thread {
@Override
public void run() {
consume();
}
private void consume() {
while (true) {
lock.lock();
try {
while (queue.size() == 0) {
try {
System.out.println("队列空,等待数据");
notEmpty.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
// 每次移走队首元素
queue.poll();
notFull.signal();
System.out.println("从队列取走一个元素,队列剩余" + queue.size() + "个元素");
} finally {
lock.unlock();
}
}
}
}
private class Producer extends Thread {
@Override
public void run() {
produce();
}
private void produce() {
while (true) {
lock.lock();
try {
while (queue.size() == queueSize) {
try {
System.out.println("队列满,等待有空余空间");
notFull.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
// 每次插入一个元素
queue.offer(1);
notEmpty.signal();
System.out.println("向队列取中插入一个元素,队列剩余空间:" + (queueSize - queue.size()));
} finally {
lock.unlock();
}
}
}
}
如果对于锁比较陌生的同学,可以先跳过线程协作的内容。我后面会单开一篇文章介绍线程间的锁问题,包括synchronized,volatile,lock,读写锁,公平锁与非公平锁,ThreadLocal,死锁等。
今天平安夜,祝大家平安夜快乐!