多线程基础
Java语言内置了多线程支持:一个Java程序实际上一个jvm的进程,jvm进程用一个主线程来执行main()方法,在主线程里又可以启动多个线程。此外,jvm还有负责垃圾回收的其他工作线程。
对于大多数Java程序来说多任务其实就是多线程实现的多任务。
和单线程相比:多线程经常需要都写共享数据,并需要同步。多线程编程的复杂度更高,调试更加困难。
多线程是Java程序最基本的并发模型,读写网络、数据库、Web开发等都需要多线程模型。
创建新线程
创建一个新线程我们需要实例化一个Thread实例,然后调用start()方法启动线程。
public class TestThread {
public static void main(String[] args) {
Thread thread = new Thread();
thread.start();// 线程启动啦
}
}
这个线程实际上什么也没做就结束了。我们希望线程能执行一些代码。
- 方法一:自定义一个类继承Thread类,重写run方法
public class TestThread {
public static void main(String[] args) {
Thread thread = new New();
thread.start(); // 启动新线程
}
}
class New extends Thread {
@Override
public void run() {
System.out.println("这是一个新线程!");
}
}
以上使用lambda写法
public class TestThread {
public static void main(String[] args) {
Thread thread = new Thread(() -> {
System.out.println("这是一个新线程");
});
thread.start(); // 启动新线程
}
}
- 方法二:实现Runable接口
public class TestThread {
public static void main(String[] args) {
New2 thread = new New2();
Thread thread1 = new Thread(thread);
thread1.start();
for (int i = 0; i < 30; i++) {
try {
Thread.sleep(200);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("Main方法。。。");
}
}
}
class New2 implements Runnable {
@Override
public void run() {
for (int i = 0; i < 30; i++) {
try {
Thread.sleep(200);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("实现Runable接口");
}
}
}
java 用 Thread 对象表示一个线程,通过 start() 方法启动一个新线程;线程的执行代码写在 run() 方法中。线程虽然可以通过 Thread.setPriority(int n) 设置优先级,但是优先级高的并不能保证一定先执行。
线程状态
在java程序中,线程对象调用start()方法启动新线程,在新线程中执行run()方法,run()方法执行完,线程就结束了。Java的线程有以下几种状态:
- New:新生状态,新创建的线程,尚未执行。
- Runnable:运行中的线程,正在执行run()方法的线程。
- Blocked:运行中的线程,因为某些操作被阻塞而挂起。
- Waiting:运行中的线程,因为某些操作在等待中。
- Time Waiting:运行中的线程,因为执行了sleep()方法正在计时等待。
- Terminated:线程一终止,因为run()方法已经执行完毕。
当线程启动后,它可以在 Runable、Blocked、Waiting、Timed Waiting 这几个状态中切换,直到变成 Terminated 状态,线程终止。
线程终止的原因有:
- 线程正常终止:线程的 run() 方法执行完毕,或执行到 return 语句返回;
- 线程的意外终止:run() 方法因为未捕获的异常而终止;
- 线程的强制终止:对某个线程的 Thread 实例调用 stop() 方法强制终止。
一个线程还可以等待另一个线程运行结束。例如,main 线程在启动 a 线程后,调用 a.join() 等待 a 线程结束后在继续运行 main 线程。
public class TestThread {
public static void main(String[] args) throws InterruptedException {
Thread a = new Thread(() -> {
for (int i = 0; i < 10; i++) {
System.out.println("多线程练习" + i + "次");
}
});
a.start();
a.join();
System.out.println("main线程执行");
}
}
如果 a 线程已经结束,调用 a.join() 方法会立即返回。此外,join(long) 重载方法可以指定一个等待时间,超过等待时间后不在继续等待。
中断线程
中断线程就是其他线程给目标线程一个中断信号,该线程收到信号后结束执行 run() 方法。可以通过对目标线程调用 interrupt() 方法,目标线程检测 interrupted 的状态,如果为 true 就结束线程。
public class TestThread {
public static void main(String[] args) throws InterruptedException {
Test t = new Test();
t.start();
Thread.sleep(5);
t.interrupt();
System.out.println("结束");
}
}
class Test extends Thread {
public void run() {
int i = 0;
while (!isInterrupted()) {
System.out.println("线程执行了" + i++ + "次。");
}
}
}
main 线程对 t 线程调用了 t.interrupt() 方法,t 线程的 while 循环会检测 isInterrupted 的状态。
如果线程处于等待状态,如,t.join() 会让 main 线程进入等待状态,此时对 main 线程调用 interrupt() 方法,join() 方法会立刻抛出 InterruptedException,只要目标线程捕获 InterruptedException ,就说明其他线程对其调用了 interrupt() 方法,该线程应该立刻解说运行。
public class TestThread {
public static void main(String[] args) throws InterruptedException {
TestA a = new TestA();
a.start();
Thread.sleep(1000);
a.interrupt(); // 中断t线程
a.join(); // 等待t线程结束
System.out.println("end");
}
}
class TestA extends Thread {
public void run() {
TestB b = new TestB();
b.start(); // 启动hello线程
try {
b.join(); // 等待hello线程结束
} catch (InterruptedException e) {
System.out.println("interrupted!");
}
b.interrupt();
}
}
class TestB extends Thread {
public void run() {
int n = 0;
while (!isInterrupted()) {
n++;
System.out.println(n + " B!");
try {
Thread.sleep(100);
} catch (InterruptedException e) {
break;
}
}
}
}
main 线程通过调用 a.interrupt() 从而通知 a 线程中断,而此时 a 线程正位于 b.join() 的等待中,此方法会立刻结束等待并抛出 InterruptedException。由于我们在 a 线程中捕获了 InterruptedException,因此,就可以准备结束该线程。在 a 线程结束前,对 b 线程也进行了 interrupt() 调用通知其中断。如果去掉这一行代码,可以发现 b 线程仍然会继续运行,且 JVM 不会退出。
另外一个中断线程的方法就是设置标志位。我们通常会用一个 Boolean 来标志一个线程是否需要停止。
public class TestThread {
public static void main(String[] args) throws InterruptedException {
TestA a = new TestA();
a.start();
Thread.sleep(2);
a.flag = false;
System.out.println("end");
}
}
class TestA extends Thread {
public volatile boolean flag = true;
public void run() {
while (flag) {
System.out.println("线程执行中...");
}
System.out.println("结束");
}
}
注意:flag 标志位是一个线程间共享变量,需要使用 volatile 关键字标记,确保每个变量都能取到最新的变量值。
守护线程
java 程序的入口就是由 jvm 启动的 main 线程,main 线程又可以启动其他线程,当所有的线程都结束后,jvm 退出,程序结束。但是有一些线程是无限循环的,比如定时任务、心跳等线程。如果这个线程不结束 jvm 就无法结束。那要怎么办呢?答案就是使用守护线程(Daemon Thread)。
守护线程是指为其他线程服务的线程。在 jvm 中,所有非守护线程都执行完毕后,无论有没有守护线程 jvm 都会推出。jvm 的推出不必关心守护线程是否已经结束。
守护线程的创建和普通的线程创建方法一样,只是在调用 start() 方法前,调用 setDaemon(true) 把线程设置为守护线程。在守护线程的代码中,不能有任何需要关闭的资源,因为守护线程结束时没有机会关闭这些资源。
线程同步
多个线程同时运行时,线程的调度由操作系统决定,任何一个线程都有可能在某个时候被操作系统暂停,然后在某个时间段后开启。这种情况下如果有多个线程读写共享变量,就会出现数据不一致的问题。如下,a 线程 +1 1000次,b 线程 -1 1000次,理论上组后的结果应该是 0。但实际上最后的结果大多不是 0。这就是因为多线程读写共享变量导致数据不一致。
public class TestThread {
public static void main(String[] args) throws InterruptedException {
TestA a = new TestA();
TestB b = new TestB();
a.start();
b.start();
a.join();
b.join();
System.out.println(Counter.counter);
}
}
class Counter {
public static int counter = 0;
}
class TestA extends Thread {
public void run() {
for (int i = 0; i < 1000; i++) {
Counter.counter += 1;
}
}
}
class TestB extends Thread {
public void run() {
for (int i = 0; i < 1000; i++) {
Counter.counter -=1;
}
}
}
造成这种情况是因为 a 线程取到值(假如是100)后还没来得及 +1 操作,线程就被暂停了,然后 b 线程取到的值也是 100 ,b 线程对他进行了 -1 操作并赋值给了变量,此时变量的值是 99。然后 a 线程又获得资源进行 +1 操作获得值是 101,并赋值给变量,变量最终值为 101,这明显是错误的,进行了 +1,-1 操作后变量的值应该还是100。
这就说明在多线程操作时,要保证逻辑的准确,在一个线程执行时,其他的线程必须等待,等待其完成后才可以执行其他的线程。这就是原子性。那怎么保证原子性呢?办法就是加锁。通过加锁保证一个线程在执行的时候,不会有其他的线程操作同一个数据,直到解锁。这种被加了锁的代码块被称为临界区(Critical Section),临界区的代码任何时候都只有一个线程执行。
保证一段代码的原子性就是通过加锁实现的,在 Java 中通过 synchronized 关键字对一个对象加锁。
synchronized(lock) {
n = n+1;
}
synchronized 保证了代码块只能由一个线程执行。我们把上边的代码加上锁在看下。
public class TestThread {
public static void main(String[] args) throws InterruptedException {
TestA a = new TestA();
TestB b = new TestB();
a.start();
b.start();
a.join();
b.join();
System.out.println(Counter.counter);
}
}
class Counter {
public static final Object lock = new Object();
public static int counter = 0;
}
class TestA extends Thread {
public void run() {
for (int i = 0; i < 1000; i++) {
synchronized (Counter.lock) {
Counter.counter += 1;
}
}
}
}
class TestB extends Thread {
public void run() {
for (int i = 0; i < 1000; i++) {
synchronized (Counter.lock) {
Counter.counter -=1;
}
}
}
}
上边的代码中 Conter.lock 实例作为锁,两个线程在执行各自的代码块儿时,需要先获得锁,才能进入代码块执行,执行结束后在 synchronized 的语句块执行完毕后自动释放锁,下一个线程就可以再次获得锁执行代码块。但是 synchroized 会降低代码的执行效率。
概括一下如何使用 synchronized :
- 找出改变共享变量的线程代码块;
- 选择一个共享实例作为锁(多个线程代码块必须使用同一把锁才能保证原子性);
- 给线程代码块加锁。
同步方法
Java 程序依靠 synchronized 对线程进行同步,使用 synchronized 的时候,锁住哪个对象非常重要。
让线程自己锁对象往往使得代码逻辑混乱,不利于封装。我们可以把 synchronized 的逻辑封装起来。比如下面的计数器:
class Counter {
private int count = 0;
public void add(int i) {
synchronized (this) {
count += i;
}
}
public void dec(int i) {
synchronized (this) {
count -= i;
}
}
public int get() {
return count;
}
}
这样线程在调用 add()、dec() 的方法的时候,它不必关心同步逻辑,因为 synchronized 代码块在方法内部。并且 synchronized 锁住的是 this ,即当前实例,这又使得创建多个 counter 实例的时候,它们之间互不影响,可以并发执行。
也可以把 synchronized 关键字提到方法上,让 synchronized 修饰这个方法。
class Counter {
private int count = 0;
public synchronized void add(int i) {
count += i;
}
public synchronized void dec(int i) {
count -= i;
}
public int get() {
return count;
}
}
因此,被 synchronized 修饰的方法都是同步方法,它表示整个方法都是用 this 实例加锁。
死锁
Java 线程的锁是可以重入的锁。
什么是重入的锁?我们来看这个例子:
public class Counter {
private int count = 0;
public synchronized void add(int n) {
if (n < 0) {
dec(-n);
} else {
count += n;
}
}
public synchronized void dec(int n) {
count += n;
}
}
执行 add() 的时候已经获得了 this 的锁,但是 add() 方法里面又调用了 dec() 的方法,由于 dec() 方法也需要获取 this 的锁,那 dec() 方法是否可以获取到 this 的锁呢?答案是可以获取。jvm 允许一个线程重复获取一个锁,这种可以被同一个线程反复获取的锁就是可重入锁。
一个线程可以在获取到一个锁后,在继续获取另一个锁。如:
public void add(int m) {
synchronized(lockA) { // 获得lockA的锁
this.value += m;
synchronized(lockB) { // 获得lockB的锁
this.another += m;
} // 释放lockB的锁
} // 释放lockA的锁
}
public void dec(int m) {
synchronized(lockB) { // 获得lockB的锁
this.another -= m;
synchronized(lockA) { // 获得lockA的锁
this.value -= m;
} // 释放lockA的锁
} // 释放lockB的锁
}
在获取多个锁的时候,不同的线程获取多个不同对象的锁可能导致死锁。对于上述代码,如果线程1和线程2分别执行 add() 和 dec() 方法时:
- 线程1:进入 add() ,获得 lockA;
- 线程2:进入 dec() ,获得 lockb。
随后:
- 线程1:准备获得 lockb ,失败,等待中;
- 线程2:准备获得 lockA ,失败,等待中。
此时,两个线程各自持有不同的锁,然后试图获取对方手里的锁,造成双方无限等待下去,这就是死锁。
死锁发生后,没有任何机制能解除死锁,只能强制结束 jvm。因此,在编写多线程时一定要防止死锁,死锁一旦形成,就只能强制结束进程。所以我们获取锁的顺序要一致。严格按照先获取 lockA ,再获取 lockB 的顺序。
public void add(int m) {
synchronized(lockA) { // 获得lockA的锁
this.value += m;
synchronized(lockB) { // 获得lockB的锁
this.another += m;
} // 释放lockB的锁
} // 释放lockA的锁
}
public void dec(int m) {
synchronized(lockA) { // 获得lockA的锁
this.value -= m;
synchronized(lockB) { // 获得lockB的锁
this.another -= m;
} // 释放lockB的锁
} // 释放lockA的锁
}
使用wait和notify
在java程序中,synchronized 解决了多线程竞争的问题,例如,对于一个任务管理器,多个线程同时往队列中添加任务,可以用 synchronized 加锁:
public class TaskQueue {
Queue<String> queue = new LinkedList<>();
public synchronized void addTask(String s) {
this.queue.add(s);
}
但是 synchronized 并没有解决多线程的协调问题。以上边的 TaskQueue 为例,我们再编写一个 getTask() 方法取出队列的第一个任务:
public class TaskQueue {
Queue<String> queue = new LinkedList<>();
public synchronized void addTask(String s) {
this.queue.add(s);
}
public synchronized String getTask() {
while (this.queue.isEmpty()) {
}
return this.queue.remove();
}
}
上述代码看上去没有问题: getTask() 内部先判断队列是否为空,如果为空就循环等待,知道一个线程往队列中放入一个任务, while() 循环退出,就可以返回队列的元素。但实际上 while 循环永远不会退出,因为线程在执行 getTask() 入口获取了 this 锁,其他线程根本无法调用 addTask() ,因为 addTask() 执行条件也是获取 this 锁。因此,此代码会在getTasd() 方法死循环。
我们在回头来想想我们当初想要实现的目标:
- 线程1可以调用 addTask() 方法不断往队列中添加任务;
- 线程2可以调用 getTask() 方法从队列中获取任务,如果队列为空则等待,直到队列中被放入了新的任务。
因此多线程协调的原则就是:当条件不满足时,线程进入等待状态;当条件满足时,线程被唤醒,继续执行任务。
对上述代码我们进行改造:
public class TaskQueue {
Queue<String> queue = new LinkedList<>();
public synchronized void addTask(String s) {
this.queue.add(s);
}
public synchronized String getTask() throws InterruptedException {
while (this.queue.isEmpty()) {
// 释放this锁
this.wait();
// 重新获得this锁
}
return this.queue.remove();
}
}
当一个线程执行到 getTask() 方法的 while 循环时,必定已经获取到了 this 锁,此时,线程执行 while 条件判断,如果条件成立(队列为空),线程将执行 this.wait(),进入等待。
这里的关键是 wait() 方法必须是在当前获取的锁对象上调用,这里获取的是 this 锁,因此调用 this.wait().
调用 wait() 方法后,线程进入等待状态,wait() 方法不会返回,直到将来某个时刻线程从等待状态被其他线程唤醒后才会返回。
有些仔细的童鞋会指出:即使线程在getTask()
内部等待,其他线程如果拿不到this
锁,照样无法执行addTask()
,肿么办?
这个问题的关键就在于wait()
方法的执行机制非常复杂。首先,它不是一个普通的Java方法,而是定义在Object
类的一个native
方法,也就是由JVM的C代码实现的。其次,必须在synchronized
块中才能调用wait()
方法,因为wait()
方法调用时,会释放线程获得的锁,wait()
方法返回后,线程又会重新试图获得锁。
因此,只能在锁对象上调用wait()
方法。因为在getTask()
中,我们获得了this
锁,因此,只能在this
对象上调用wait()
方法,当 this.wait() 等待时,它就会释放 this 锁,从而使其他线程能够在 addTask() 方法获得 this 锁。
现在我们面临第二个问题:如何将进入等待的 getTask() 方法从等待状态唤醒。答案是在相同的锁对象上调用 notify() 方法。我们修改 addTask() 如下:
public synchronized void addTask(String s) {
this.queue.add(s);
this.notify();// 唤醒this锁等待的线程
}