本篇博客主要介绍Java中线程的相关概念。
什么是线程?
线程是操作系统能够进行运算调度的最小单位。它被包含在进程之中,是进程中的实际运作单位。一个线程指的是进程中一个单一顺序的控制流,一个进程中可以并发多个线程,每条线程并行执行不同的任务。所以线程可以理解为进程中的一个执行流。进程可以理解为线程组。
- 进程是资源分配的最小单位;
- 线程是CPU调度的最小单位。
同一进程中的不同线程将共享该进程中的全部资源,如虚拟地址空间,文件描述符表和信号处理等。但同一进程中的多个线程有各自的调用栈(call stack),自己的寄存器环境等。
下面,我们通过代码来感受一下什么是线程:
public class ThreadTest {
private static class MyThread extends Thread {
@Override
public void run() {
while (true) {
System.out.println(this.getName() + " is running!");
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
public static void main(String[] args) throws InterruptedException {
MyThread t1 = new MyThread();
t1.start();
MyThread t2 = new MyThread();
t2.start();
MyThread t3 = new MyThread();
t3.start();
while (true) {
System.out.println(Thread.currentThread().getName() + " is running!");
Thread.sleep(3000);
}
}
}
我们可以使用jconsole工具来观察一个Java程序的线程情况,jconsole工具在JDK安装目录下的bin文件中,由于我们在配置Java环境的时候,已经将这个路径添加到环境变量,所以我们可以直接使用win + r进行操作,然后输入jconsole即可。
可以看到除了main、Thread-0、Thread-1、Thread-2和Thread-3这些线程之外还有很多的其他线程,这些都是JVM启动的一些守护线程。
多线程的优势
我们来看一个代码,观察一下多线程在某些场合的优势:
public class ThreadTest {
private static final long COUNT = 10_0000_0000L;
public static void main(String[] args) throws InterruptedException {
concurrency();
serial();
}
/**
* 并发
* @throws InterruptedException
*/
private static void concurrency() throws InterruptedException {
long begin = System.currentTimeMillis();
// 利用一个线程计算a的值
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
int a = 0;
for (long i = 0; i < COUNT; ++i) {
--a;
}
}
});
thread.start();
// 主线程内计算b的值
int b = 0;
for (long i = 0; i < COUNT; ++i) {
--b;
}
// 等待thread线程运行结束
thread.join();
long end = System.currentTimeMillis();
System.out.printf("并发: %d毫秒%n", end - begin);
}
/**
* 串行
*/
private static void serial() {
long begin = System.currentTimeMillis();
int a = 0;
for (long i = 0; i < COUNT; ++i) {
--a;
}
int b = 0;
for (long i = 0; i < COUNT; ++i) {
--b;
}
long end = System.currentTimeMillis();
System.out.printf("串行: %d毫秒%n", end - begin);
}
}
可以看出并发还是比串行要快一些的。
线程的创建
继承Thread类
可以通过继承Thread类来创建一个线程类,该方法的好处是this代表的就是当前线程,不需要通过Thread.currentThread()
来获取当前线程的引用。
我们来看一下代码:
public class ThreadCreate {
public static void main(String[] args) {
MyThread thread = new MyThread("我的线程");
thread.start();
}
}
class MyThread extends Thread {
public MyThread(String name) {
super(name);
}
@Override
public void run() {
while (true) {
System.out.println(this.getName() + " is running!");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
我们还可以以匿名类的方式来实现:
public class ThreadCreate {
public static void main(String[] args) {
Thread thread = new Thread("我的线程") {
@Override
public void run() {
while (true) {
try {
System.out.println(this.getName() + " is running!");
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
};
thread.start();
}
}
实现Runnable接口
通过实现Runnable接口,并且调用Thread的构造方法时将Runnable对象作为target参数传入来创建线程对象。该方法的好处是可以规避类的单继承的限制;但需要通过Thread.currentThread()来获取当前线程的引用。
下面看一下代码:
public class ThreadCreate {
public static void main(String[] args) {
Thread thread = new Thread(new MyRunnable(), "我的线程");
thread.start();
}
}
class MyRunnable implements Runnable {
@Override
public void run() {
while (true) {
try {
System.out.println(Thread.currentThread().getName() + " is running!");
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
除了上述实现一个MyRunnable类创建实例之外,我们还可以使用匿名类的方式来实现,下面看代码实现:
public class ThreadCreate {
public static void main(String[] args) {
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
while (true) {
try {
System.out.println(Thread.currentThread().getName() + " is running!");
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}, "我的线程");
thread.start();
}
}
还可以使用Lambda表达式来创建Runnable子类对象。
public class ThreadCreate {
public static void main(String[] args) {
Thread thread = new Thread(()->{
while (true) {
try {
System.out.println(Thread.currentThread().getName() + " is running!");
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}, "我的线程");
thread.start();
}
}
Thread类常用方法
Thread类是JVM用来管理线程的一个类,换句话说,每个线程都有一个唯一的Thread对象与之关联。
每个执行流,也需要一个对象来描述,而Thread类的对象就是用来描述一个线程执行流的,JVM会将这些Thread对象组织起来,用于线程调度,线程管理。
Thread的常见构造方法
方法 | 说明 |
Thread() | 创建线程对象 |
Thread(Runnable target) | 使用Runnable对象创建线程对象 |
Thread(String name) | 创建线程对象,并命名 |
Thread(Runnable target, String name) | 使用Runnable对象创建线程对象,并命名 |
Thread(ThreadGroup group, Runnable target) | 线程可以被用来分组管理,分号的组即线程组 |
Thread的几个常见的属性
属性 | 获取方法 |
ID | getID() |
名称 | getName() |
状态 | getState() |
优先级 | getPriority() |
是否后台线程 | isDaemon() |
是否存活 | isAlive() |
是否被中断 | isInterrupted() |
- 关于后台线程,需要记住一点:JVM会在一个进程的所有非后台线程结束后,才会结束运行。
- 是否存活:可以简单的理解为run方法是否运行结束了。
启动一个线程
我们可以通过覆写run方法来创建一个线程对象,但线程对象被创建出来并不意味着线程就开始运行了。
- 覆写run()方法是提供给线程要做的事情;
- 而调用start()方法,线程才真正独立去执行了。
一定要区分run和start方法的区别,下面我们通过代码来演示一下:
public class ThreadDemo {
public static void main(String[] args) throws InterruptedException {
Thread thread = new Thread("我的线程") {
@Override
public void run() {
while (true) {
try {
System.out.println("我的线程");
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
};
thread.run();
while (true) {
System.out.println("main");
Thread.sleep(1000);
}
}
}
我们用jconsole来观察一下:
可以看到只有一个main线程,我们创建的线程名为“我的线程”的线程并没有被启动。thread.run();
就是在调用thread对象的方法run();并不是在启动线程。
下面,我们来看一下真正的启动线程:
public class ThreadDemo {
public static void main(String[] args) throws InterruptedException {
Thread thread = new Thread("我的线程") {
@Override
public void run() {
while (true) {
try {
System.out.println("我的线程");
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
};
thread.start();
while (true) {
System.out.println("main");
Thread.sleep(1000);
}
}
}
同样的,我们使用jconsole来观察一下:
可以看到,我的线程和main线程都运行起来了。
中断一个线程
中断一个线程有两种方式:
- 通过共享的标记来进行沟通;
- 通过调用interrupt()方法来通知。
我们先来看一下通过共享的标记进行沟通的方法:
public class ThreadDemo {
private static boolean flag= false;
public static void main(String[] args) throws InterruptedException {
new Thread("我的线程") {
@Override
public void run() {
while (!flag) {
try {
System.out.println(Thread.currentThread().getName() + " is running!");
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}.start();
Thread.sleep(5000);
flag= true;
}
}
但是这种方式有一个小问题,那就是如果目标线程正在sleep(),那么目标线程不会被立即中断,而是等sleep()结束后才会结束中断。
通过调用实例方法或者静态方法来中断一个线程:
我们来看一下三个方法:
方法 | 说明 |
public void interrupt() | 中断对象关联的线程,如果线程处于阻塞状态,则以异常的方式通知,否则设置标志位 |
public static boolean interrupted() | 判断当前线程的中断标志位是否被设置,调用后清除标志位 |
public boolean isInterrupted() | 判断当前对象关联的线程的标志位是否被设置,调用后不清除标志位 |
interrupt()方法比较好理解,就是中断对象关联的线程。
下面,我们来看一下interrupted()和isInterrupted()的区别:
- 可以看到interrupted()方法内部调用的就是isInterrupted()方法,但是interrupted()会清除标志位,isInterrupted()不会清除标志位。
- interrupted()是静态方法,isInterrupted()是实例方法。
对于这三个方法,我们可以理解为底层也是通过一个共享的标志位flag来实现的,isInterrupted()方法只是返回这个flag的值,而interrupted()不仅返回这个flag的值,还会将这个flag的值置为false。
我们再来看一下interrupt()方法:
- 通过thread对象调用interrupt()方法通知该线程停止运行;
- thread收到通知的方式有两种:
① 如果线程调用了wait/join/sleep等方法而阻塞挂起,则以InterruptedException异常的形式通知,清除中断标志;
② 否则,只是内部的一个中断标志被设置,thread可以通过Thread.interrupted()方法或者thread.isInterrupt()方法来判断当前线程的中断标志是否被设置。其中前者在调用之后会清除中断标志。
下面,我们来看一个代码:
public class ThreadDemo {
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread() {
@Override
public void run() {
while (!Thread.interrupted()) {
try {
System.out.println(Thread.currentThread().getName() + " is running!");
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
};
Thread t2 = new Thread() {
@Override
public void run() {
try {
while (!Thread.interrupted()) {
System.out.println(Thread.currentThread().getName() + " is running!");
Thread.sleep(1000);
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
};
t1.start();
t2.start();
Thread.sleep(3000);
t1.interrupt();
t2.interrupt();
}
}
可以看到try…catch和while的位置不同,两个线程表现出来的结果是不同的。
线程等待
有时候,我们需要等待一个线程完成它的工作后,才能进行自己下一步工作。这时候我们就需要一个方法等待线程的结束。
常用的线程等待的方法如下:
方法 | 说明 |
public void join() | 等待线程结束 |
public void join(long millis) | 等待线程结束,最多等millis毫秒 |
public void join(long millis, int nanos) | 等待线程结束,最多等millis毫秒nanos纳秒 |
我们来看一个代码,该代码可以实现threads数组中的线程挨个顺序执行:
public class ThreadDemo {
public static void main(String[] args) throws InterruptedException {
Thread[] threads = new Thread[5];
for (int i = 0; i < threads.length; ++i) {
threads[i] = new Thread() {
@Override
public void run() {
for (int i = 0; i < 2; ++i) {
System.out.println(Thread.currentThread().getName());
}
}
};
}
for (int i = 0; i < threads.length; ++i) {
threads[i].start();
threads[i].join();
}
}
}
如果我们把threads[i].join()
这句代码去掉会怎么样,我们来看一下:
public class ThreadDemo {
public static void main(String[] args) throws InterruptedException {
Thread[] threads = new Thread[5];
for (int i = 0; i < threads.length; ++i) {
threads[i] = new Thread() {
@Override
public void run() {
for (int i = 0; i < 2; ++i) {
System.out.println(Thread.currentThread().getName());
}
}
};
}
for (int i = 0; i < threads.length; ++i) {
threads[i].start();
}
}
}
可以看到去掉join()顺序就乱了。
线程的状态
线程的状态是一个枚举类型Thread.State,我们来看一下:
public class ThreadDemo {
public static void main(String[] args) {
for (Thread.State state : Thread.State.values()) {
System.out.println(state);
}
}
}
- 初始状态(NEW):只是安排了工作,还未开始行动;
- 运行中(RUNNABLE):CPU正在调度该线程;
- 就绪(READY):等待被系统调度;
- 等待(WAITING)、超时等待(TIMED_WAITING)、阻塞(BLOCKED):排队等着其他事情;
- 终止(TERMINATED):工作全部完成。
线程的超时等待、阻塞和等待都对应操作系统中进程的阻塞状态。只不过Java中对阻塞状态进行了细分。划分出了三种。
线程安全
我们来看一个例子来体会一下什么是线程不安全:
我们启动20个线程来对同一个变量进行自增操作,每个线程自增一万次。
public class ThreadDemo {
private static int num = 0;
public static void main(String[] args) throws InterruptedException {
Thread[] threads = new Thread[20];
for (int i = 0; i < threads.length; ++i) {
threads[i] = new Thread() {
@Override
public void run() {
for (int i = 0; i < 10000; ++i) {
++num;
}
}
};
}
for (int i = 0; i < threads.length; ++i) {
threads[i].start();
}
while (Thread.activeCount() > 1) {
Thread.yield();
}
System.out.println("num = " + num);
}
}
我们预期的结果是20万,但是这里只有17万多。这种现象我们可以称之为线程不安全,为什么会出现线程不安全的问题呢?
线程不安全的原因
原子性
我们把一段代码想像成一个房间,每个线程就是要进入这个房间的人。如果没有任何机制保证,A进入房间之后,还没有出来;B是不是也可以进入房间,打断A在房间里的隐私。这个就是不具备原子性的。
如何解决这个问题呢?我们可以给房间加一把锁,A进去把门锁上,其他人就进不来了,这样我们就保证了这段代码的原子性了。
有时也把这个现象叫做同步互斥,表示操作是互相排斥的。
一条Java语句不一定是原子的,也不一定只是一条指令。
比如刚才我们看到的++num,其实是由三步操作组成的:
- 从内存中把数据读到CPU;
- 进行数据更新;
- 把数据写回到内存中。
不保证原子性会给多线程带来什么问题?
如果一个线程正在对一个变量进行操作,中途其他线程插入进来了,如果这个操作被打断了,结果就可能是错误的。
可见性
JVM将内存组织为主内存和工作内存两部分:
- 主内存:包括本地方法区和堆;
- 工作内存:每个线程都有一个工作内存,工作内存中主要包括两个部分,一个是属于该线程私有的栈和对主存部分变量拷贝的寄存器。
所有的变量都存储在主内存中,对于所有线程都是共享的;线程之间无法直接访问对方的工作内存中的变量,线程间变量的传递均需要通过主内存来完成。
JVM执行过程中,共享变量在多线程之间不能及时看到改变,这个就是可见性问题。
有序性
一段代码逻辑如下:
- 去前台取U盘;
- 去教室写10分钟作业;
- 去前台取下快递。
如果是在单线程情况下,JVM、CPU指令集会对其进行优化,比如,按照1->3->2的方式执行,也是没问题的,可以少跑一次前台。这种叫做指令重排序。
但是如果在多线程场景下就有问题了,可能快递是在你写作业的10分钟内被另一个线程放过来的,或者被人变过了,如果指令重排序,代码就会是错误的。
有序性:如果在线程内观察,所有操作都是有序的;如果在一个线程中观察另一个线程,所有操作都是无序的。前半句指:线程内表现为串行语义,后半句是指:指令重排现象和工作内存与主内存同步延迟现象。
synchronized关键字
对于前面的线程不安全的问题,我们可以使用synchronized关键字来实现线程安全。
synchronized的用法:
- synchronized修饰普通方法,此时锁的是当前实例的对象;
- synchronized修饰静态方法,此时锁的是类的class对象;
- synchronized修饰代码块,此时锁的是括号内的对象。
我们分别使用synchronized关键字来解决一下前面的线程不安全问题。
静态同步方法:
public class ThreadDemo {
private static int num = 0;
public static synchronized void increment() {
++num;
}
public static void main(String[] args) {
Thread[] threads = new Thread[20];
for (int i = 0; i < threads.length; ++i) {
threads[i] = new Thread() {
@Override
public void run() {
for (int i = 0; i < 10000; ++i) {
increment();
}
}
};
}
for (int i = 0; i < threads.length; ++i) {
threads[i].start();
}
while (Thread.activeCount() > 1) {
Thread.yield();
}
System.out.println("num = " + num);
}
}
同步代码块:
public class ThreadDemo {
private static int num = 0;
public static void main(String[] args) {
Object o = new Object();
Thread[] threads = new Thread[20];
for (int i = 0; i < threads.length; ++i) {
threads[i] = new Thread() {
@Override
public void run() {
for (int i = 0; i < 10000; ++i) {
synchronized (o) {
++num;
}
}
}
};
}
for (int i = 0; i < threads.length; ++i) {
threads[i].start();
}
while (Thread.activeCount() > 1) {
Thread.yield();
}
System.out.println("num = " + num);
}
}
注意:
- 想要使多个线程保持同步,需要保证多个线程锁的是同一个对象。
- 使用同步代码块时,不建议去锁一个Integer或String对象,因为它们有时候不在常量池,而在堆中,就不是唯一的了,有可能多个线程锁的是不同的对象,就无法达到同步的效果。
同步方法、静态同步方法可以和同步代码块之间相互转换。
同步方法和同步代码块:
// 同步方法
public synchronized void method() {}
// 同步代码块
public void method() {
synchronized (this) {}
}
静态同步方法和同步代码块:
// 静态同步方法
public static synchronized void method() {}
// 同步代码块
public static void method() {
synchronized (ThreadDemo.class) {}
}
synchronized能够保证原子性、可见性和有序性。
synchronized不能锁null,因为synchronized锁在对象头上。null是没有对象头的。
线程间通信
我们主要来看三个方法:wait()
、notify()
和notifyAll()
。
wait方法
wait()方法就是使线程停止运行。
- 方法wait()的作用是使当前执行的线程进行等待,wait()方法是Object类的方法,该方法是用来将当前线程置入“等待队列”,并且在wait()所在的代码处停止执行,直到接到通知或被中断为止;
- wait()方法只能在同步方法中或同步块中调用。如果调用wait()时,没有持有适当的锁,会抛出异常;
- wait()方法执行后,当前线程释放锁,其他线程竞争获取锁。
我们来看一段代码:
package Thread;
public class WaitTest {
public static void main(String[] args) throws InterruptedException {
Object o = new Object();
synchronized (o) {
System.out.println("等待中...");
o.wait();
System.out.println("等待结束!");
}
System.out.println("main方法结束!");
}
}
这段代码在执行到o.wait()的时候会一直等待下去。除非被中断或唤醒。
notify方法
notify()方法就是使停止的线程继续运行。
- 方法notify()也要在同步方法或同步块中调用,该方法是用来通知那些可能等待该对象锁的其它线程,对其发出通知,使其可以重新竞争锁。如果有多个线程等待,则有线程规划器随机挑选出一个处于等待队列的线程进行唤醒;
- 在notify()方法后,当前线程不会马上释放该对象锁,要等到执行notify()方法的线程执行完同步代码块中的代码之后才会释放对象锁。虽然此时已经有线程被唤醒,但是执行notify()方法的线程还持有锁,所以被唤醒的线程依旧会等待在锁上。
我们来看一下代码:
package Thread;
public class NotifyTest {
public static void main(String[] args) {
Thread t1 = new Thread() {
@Override
public void run() {
synchronized (NotifyTest.class) {
try {
System.out.println("t1线程正在运行!");
System.out.println("t1线程正在等待!");
NotifyTest.class.wait();
System.out.println("t1线程被唤醒!");
System.out.println("t1线程即将退出!");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
};
t1.start();
Thread t2 = new Thread() {
@Override
public void run() {
synchronized (NotifyTest.class) {
System.out.println("t2线程正在运行!");
NotifyTest.class.notify();
System.out.println("t2线程即将退出!");
}
}
};
t2.start();
}
}
notifyAll方法
上面的notify()方法只能唤醒某一个等待线程,那么如果有多个线程都在等待中怎么办呢,这个时候就可以使用notifyAll方法可以一次唤醒所有的等待线程,我们直接来看代码:
package Thread;
import com.sun.xml.internal.bind.annotation.OverrideAnnotationOf;
public class NotifyAllTest {
public static void main(String[] args) {
Thread t1 = new Thread() {
@Override
public void run() {
synchronized (NotifyAllTest.class) {
try {
System.out.println("t1线程正在运行!");
System.out.println("t1线程正在等待!");
NotifyAllTest.class.wait();
System.out.println("t1线程被唤醒!");
System.out.println("t1线程即将退出!");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
};
t1.start();
Thread t2 = new Thread() {
@Override
public void run() {
synchronized (NotifyAllTest.class) {
try {
System.out.println("t2线程正在运行!");
System.out.println("t2线程正在等待!");
NotifyAllTest.class.wait();
System.out.println("t2线程被唤醒!");
System.out.println("t2线程即将退出!");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
};
t2.start();
Thread t3 = new Thread() {
@Override
public void run() {
synchronized (NotifyAllTest.class) {
System.out.println("t3线程正在运行!");
NotifyAllTest.class.notifyAll();
System.out.println("t3线程即将退出!");
}
}
};
t3.start();
}
}
线程间通信总结:
- wait()、notify()、notifyAll()三个方法的执行都必须在synchronized代码块中;
- 执行这三个方法必有持有相应的锁对象;
- wait、notify、notifyAll都是java.lang.Object类的方法,而不是Thread固有的方法。换句话说,wait、notify和notifyAll这三个方法与其说是针对线程的操作,倒不如说是针对实例的等待队列的操作。由于所有实例都有等待队列,所以wait、notify和notifyAll也就成为了Object类的方法。
wait和sleep的区别
- wait之前需要请求锁,而wait执行时会先释放锁,等被唤醒时再重新请求锁。这个锁是wait对象上的monitor lock;
- sleep是无视锁的存在的,即之前请求的锁不会释放,没有锁也不会请求;
- wait方法是Object的方法;
- sleep方法是Thread类的静态方法。