目录
前言:
上一部分,我们一起学习了双重检查锁定和延迟初始化的内容,这一部分,我们来学习多线程并发编程的基础内容
1. 线程简介
1.1 什么是线程
操作系统在运行一个程序时,会创建一个进程,比如打开一个qq,那就是启动了一个qq的进程。而线程也叫做轻量级进程,是操作系统中的最小调度单元,在一个进程里可以创建多个线程,每个线程都有自己的计数器,堆栈和局部变量,而且可以访问内存中的共享变量。
1.2 线程的优先级
线程都是通过获得CPU时间片来进行执行,当线程的时间片用完了就会发生线程调度,并等待下次分配。线程分配到时间片的多少决定了线程使用处理器资源的多少,而线程优先级则决定了哪些线程能获得更多或更少的CPU资源。
java线程的优先级由一个整形成员变量priority来表示,在创建线程的时候用setPriority(int)方法来修改优先级,线程优先级的范围从1-10。线程的默认优先级是5,优先级高的线程分配到的时间片要多于优先级低的线程,需要注意的是,程序的正确性并不依赖于优先级的高低,也就是说不能保证优先级高的线程一定会比优先级低的线程先执行完。
1.3 线程的状态
一个线程在给定的一个时刻只能处于以下6种状态中的1种
状态 | 说明 |
---|---|
NEW | 初始状态,线程被创建,还未调用start()方法时 |
RUNNABLE | 运行状态,java线程操作系统中的就绪和运行态统一称为“可运行”,实际当线程获得CPU时间片后,才是真正的运行状态 |
BLOCKED | 阻塞状态,表示线程阻塞于锁 |
WAITING | 等待状态,表示当前线程需要等待其他线程通知或者中断 |
TIME_WAITING | 超时等待状态,不同与WAITING,它可以在指定时间内自行返回 |
TERMINATED | 终止状态,表示当前线程已执行完毕 |
线程的状态间变化图如下
1.4 守护线程
守护线程也叫做“精灵”线程,主要用于程序中的后台调度和支持性的工作,当jvm中不存在deamon线程的时候,jvm将会推出,java中的垃圾回收线程就是守护线程,当垃圾回收线程完成垃圾回收之后,程序就真正的退出了。
2. 线程中断
如何创建和启动一个线程相信大家一定都不会陌生,这里我们来学习一下中断的概念,中断可以理解为线程的一个标识位属性,它表示一个运行中的线程是否被其他线程进行了中断操作。其他线程通过调用该线程的interrupt()方法对该线程进行终端操作。
线程通过isInterrupt方法来检查自身是否被中断,也可以调用Thread.interrupted来复位。
因为实际使用中interrupt中断用得不是很多,关于中断的具体使用推荐大家看一篇文章https://www.cnblogs.com/jenkov/p/juc_interrupt.html
3. 线程间通信
3.1 等待/通知机制
要想让一个多线程的程序效率高起来,不可缺少的就是多个线程之间的协作完成任务,java中有一个通知/等待机制可以让线程间可以通信,使得多个线程间的合作变得方便,通知/等待的相关方法是java任意对象都有的,也就是定义在Object类中的方法,整理如下
方法名 | 描述 |
---|---|
notify() | 通知一个对象上等待的线程,使其从wait()方法返回,返回的前提是该线程获得了对象的锁 |
notifyAll() | 通知所有等待获取该对象锁的线程 |
wait() | 一旦调用该方法,则线程进入WAITING状态,只有等到另外的线程通知或者中断才能返回 |
wait(long) | 超时等待一段时间,在设定的时间内没有收到通知则返回 |
wait(long,int) | 对于超时时间的更细粒度的控制 |
下面来看一个例子,这段代码之后我们会对wait(),notify()和notifyAll()的使用注意细节
public class WaitNotify {
static boolean flag = true;
static Object lock = new Object();
public static void main(String[] args) {
Thread waitThread = new Thread(new Wait(), "WaitThread");
waitThread.start();
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
Thread notifyThread = new Thread(new Notify(), "notifyThread");
notifyThread.start();
}
static class Wait implements Runnable {
@Override
public void run() {
//加锁,持有lock的Monitor
synchronized (lock) {
//条件不满足时,继续wait,同时释放了lock的锁
while (flag) {
try {
System.out.println(Thread.currentThread() + "flag is true.");
lock.wait();//等待,挂起线程把时间片分给下一线程Notify
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//条件满足时,完成工作
System.out.println(Thread.currentThread() + "flag is false.");
}
}
}
static class Notify implements Runnable {
@Override
public void run() {
//加锁,持有lock的Monitor
synchronized (lock) {
//获取lock的锁,然后通知,通知时不会释放锁
//直到当前线程释放了lock之后,WaitThread才能从wait方法中返回
System.out.println(Thread.currentThread() + "hold lock");
lock.notifyAll();
flag = false;
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//再次加锁
synchronized (lock) {
System.out.println("hold lock again");
} //此处Notify线程释放锁,Wait线程可以收到通知
}
}
}
输出结果为
Thread[WaitThread,5,main]flag is true.
Thread[notifyThread,5,main]hold lock
Thread[notifyThread,5,main]hold lock again
Thread[WaitThread,5,main]flag is false.
上面第三行和第四行的顺序可能会互换。上面的代码例子主要说明调用wait(),notify(),notifyAll()需要注意的细节
- 调用wait(),notify(),notifyAll()前要先对调用对象加锁
- 调用wait()后,线程状态由RUNNING转为WAITING,并把当前线程放入对象的等待队列
- notify(),notifyAll()调用后,等待线程不会马上从wait()返回,需要调用notify(),notifyAll()的线程释放锁后,等待线程才有机会从wait()返回
- notify()方法是把一个等待队列中的等待线程从等待队列移到同步队列中,notifyAll()方法是把所有的等待队列中的线程全部移到同步队列中,被移动的线程状态由WAITING变为BLOCKED
- 从wait()方法返回的前提是获得了调用对象的锁
下面用一个图来描述一下
3.2 Thread.join的使用
在很多情况下,主线程生成并启动了子线程,如果子线程里要进行大量的耗时的运算,主线程往往将于子线程之前结束,但是如果主线程处理完其他的事务后,需要用到子线程的处理结果,也就是 主线程需要等待子线程执行完成之后再结束,这个时候就要用到join()
方法了。
具体join的使用方式,大家可以看我转载的《Java Thread.join()详解》这篇文章,这篇文章讲的很详细。
3.3 ThreadLocal的使用
ThreadLocal,即线程变量,ThreadLocal为每个使用该变量的线程提供独立的变量副本,所以每一个线程都可以独立地改变自己的副本,而不会影响其它线程所对应的副本。从线程的角度看,目标变量就象是线程的本地变量,这也是类名中“Local”所要表达的意思。ThreadLocal相当于维护了一个map,key就是当前的线程,value可以是任意类型的对象。下面看一段代码,演示ThreadLocal的简单使用
public class ThreadLocalTest {
private static final ThreadLocal<Integer> getNum = new ThreadLocal<Integer>() {
@Override
public Integer initialValue() { //匿名内部类设置threadLocal的初始值
return 0;
}
};
public int nextNum() {
getNum.set(getNum.get() + 1);//设置初始值+1
return getNum.get(); //返回threadLocal中保存下来的值
}
public static void main(String[] args) {
ThreadLocalTest ts = new ThreadLocalTest();
TestClient t1 = new TestClient(ts);
TestClient t2 = new TestClient(ts);
TestClient t3 = new TestClient(ts);
t1.start();
t2.start();
t3.start();
}
private static class TestClient extends Thread {
ThreadLocalTest ts;
public TestClient(ThreadLocalTest ts) {
this.ts = ts;
}
@Override
public void run() {
for (int i = 0; i < 5; i++) {
System.out.println(Thread.currentThread().getName() + "-----num:" + ts.nextNum());//循环5次,每次打印threadLocal中取出的线程变量的值
}
}
}
}
运行结果
Thread-1-----num:1
Thread-2-----num:1
Thread-2-----num:2
Thread-0-----num:1
Thread-2-----num:3
Thread-1-----num:2
Thread-1-----num:3
Thread-2-----num:4
Thread-0-----num:2
Thread-2-----num:5
Thread-1-----num:4
Thread-1-----num:5
Thread-0-----num:3
Thread-0-----num:4
Thread-0-----num:5
可以看出,虽然有3个线程共享ThreadLocalTest实例,但是每个线程取得的threadLocal中保存的值都是从1到5,并没有互相干扰,说明ThreadLocal为每个线程都提供了独立的副本。
总结
这一部分我们主要学习了线程的基本概念,线程的优先级和线程的状态,线程间通信,下一部分我们来学习java中的锁。