点击上方 程序猿杂货摊,选择 设为星标
优质项目,及时送达
-----
多线程一直是Java 面试中常考的基础知识,之前一直没有系统的学习过,这段时间对着廖雪峰大师的讲义从新把该知识内容整理一遍,该文章系列内容全部来源于廖雪峰官方网站Java 基础教程。
系列目录如下:
1、线程创建
2、线程的状态
3、中断线程
4、守护线程
5、线程同步
6、死锁
7、使用wait和notify
1
线程创建
//方法一:从Thread派生一个自定义类,然后覆写run()方法:class MyThread extends Thread{ @Override public void run() { System.out.println("this is MyThread extends Thread "); }}//方法二:创建Thread实例时,传入一个Runnable实例:class MyRunnable implements Runnable{ public void run() {// synchronized () System.out.println("this is MyRunnable implements Runnable"); }}//用Java8引入的lambda语法进一步简写为:Thread t3 = new Thread(() -> { System.out.println("this is lambda func"); }); System.out.println("main start...."); t3.start(); t3.join();//join()函数作用是main线程在启动t线程后,可以通过t.join()等待t线程结束后再继续运行,然后才继续往下执行自身线程
使用线程执行的打印语句,和直接在main()方法执行有区别吗?
区别大了去了。我们看以下代码:
public class Main { public static void main(String[] args) { System.out.println("main start..."); //Main Thread t = new Thread() { //Main public void run() { System.out.println("thread run..."); //t System.out.println("thread end."); //t } }; t.start(); //Main System.out.println("main end..."); //Main }}
我们用蓝色表示主线程,也就是main线程,main线程执行的代码有4行,首先打印main start,然后创建Thread对象,紧接着调用start()启动新线程。当start()方法被调用时,JVM就创建了一个新线程,我们通过实例变量t来表示这个新线程对象,并开始执行。
接着,main线程继续执行打印main end语句,而t线程在main线程执行的同时会并发执行,打印thread run和thread end语句。当run()方法结束时,新线程就结束了。而main()方法结束时,主线程也结束了。
我们再来看线程的执行顺序:
main线程肯定是先打印main start,再打印main end;
t线程肯定是先打印thread run,再打印thread end。
但是,除了可以肯定,main start会先打印外,main end打印在thread run之前、thread end之后或者之间,都无法确定。因为从t线程开始运行以后,两个线程就开始同时运行了,并且由操作系统调度,程序本身无法确定线程的调度顺序。
线程的优先级
可以对线程设定优先级,设定优先级的方法是:
Thread.setPriority(int n) // 1~10, 默认值5
小结:
Java用Thread对象表示一个线程,通过调用start()启动一个新线程;
一个线程对象只能调用一次start()方法;
线程的执行代码写在run()方法中;
线程调度由操作系统决定,程序本身无法决定调度顺序;
Thread.sleep()可以把当前线程暂停一段时间。
2
线程的状态
在Java程序中,一个线程对象只能调用一次start()方法启动新线程,并在新线程中执行run()方法。一旦run()方法执行完毕,线程就结束了。因此,Java线程的状态有以下几种:
New:新创建的线程,尚未执行;
Runnable:运行中的线程,正在执行run()方法的Java代码;
Blocked:运行中的线程,因为某些操作被阻塞而挂起;
Waiting:运行中的线程,因为某些操作在等待中;
Timed Waiting:运行中的线程,因为执行sleep()方法正在计时等待;
Terminated:线程已终止,因为run()方法执行完毕。
Java线程对象Thread的状态包括:New、Runnable、Blocked、Waiting、Timed Waiting和Terminated;
通过对另一个线程对象调用join()方法可以等待其执行结束;
可以指定等待时间,超过等待时间线程仍然没有结束就不再等待;
对已经运行结束的线程调用join()方法会立刻返回。
3
中断线程
如果线程需要执行一个长时间任务,就可能需要能中断线程。中断线程就是其他线程给该线程发一个信号,该线程收到信号后结束执行run()方法,使得自身线程能立刻结束运行。
中断一个线程非常简单,只需要在其他线程中对目标线程调用interrupt()方法,目标线程需要反复检测自身状态是否是interrupted状态,如果是,就立刻结束运行。
![4cf284673665b6c9aae46f4dfffda5b6.gif](https://i-blog.csdnimg.cn/blog_migrate/435c59a4acdb1912450a82f292246ef2.gif)
package com.sun;/** * @Auther mashang * @Date 2020-11-26 20:47 * @Version 1.0 *//** * 测试线程中断 */public class testThreadInterrupt { public static void main(String[] args) throws InterruptedException{ System.out.println("主线程执行开始...."); Thread t = new Thread(){ public void run(){ int i = 0; while(!isInterrupted()){ i++; System.out.println(i + " hello"); } } }; t.start(); /* Thread.sleep()和t.sleep() 两种方式不一样,Thread.sleep()是让当前主线程main()休眠10ms,而t.sleep(10)是让t线程休眠10ms 本程序中测试线程中断应该是让主线程main()休眠,不在继续指向下一行代码t.interrupt(),让t.start()先跑一会在中断。 */ Thread.sleep(10);// t.sleep(10); t.interrupt(); t.join(); System.out.println("end....."); }}
![4cf284673665b6c9aae46f4dfffda5b6.gif](https://i-blog.csdnimg.cn/blog_migrate/435c59a4acdb1912450a82f292246ef2.gif)
package com.sun;/** * @Auther mashang * @Date 2020-11-26 20:47 * @Version 1.0 *//** * 测试线程中断 */public class testThreadInterrupt2 { public static void main(String[] args) throws InterruptedException{ System.out.println("主线程执行开始...."); Thread t = new MyThread(); t.start(); Thread.sleep(1000); t.interrupt(); t.join();//等待t线程执行完毕 System.out.println("Main end....."); }}class HelloThread extends Thread{ public void run(){ int i = 0; while(!isInterrupted()){ i++; System.out.println(i + " hello"); try { Thread.sleep(100); } catch (InterruptedException e) { System.out.println("这是HelloThread 异常"); break; } } }}class MyThread extends Thread{ public void run(){ Thread hello = new HelloThread(); hello.start(); try { hello.join(); } catch (InterruptedException e) {// e.printStackTrace(); /* 此时的异常是由MyThread 捕获的 java.lang.InterruptedException at java.lang.Object.wait(Native Method) at java.lang.Thread.join(Thread.java:1252) at java.lang.Thread.join(Thread.java:1326) - - - - - - - -- -at com.sun.MyThread.run(testThreadInterrupt2.java:51)- - -- - -- - - - -- - */ System.out.println("这是MyThread 捕获interrupted!"); } hello.interrupt();//没有这一行,hello线程仍然会继续运行,且JVM不会退出 System.out.println("已经通知hello 执行中断 "); }}/*代码执行流程,main()主线程执行t.interrupt();中断,此时t线程正在执行hello.join(),等待hello线程执行完毕,故此时t线程会捕获InterruptedException异常,打印"这是MyThread 捕获interrupted!",t线程在执行完毕之前会通知hello线程中断hello.interrupt(),然后主线程会都等待t.join()执行完毕,main()再继续执行。 */
执行结果如下:
主线程执行开始....1 hello2 hello3 hello4 hello5 hello6 hello7 hello8 hello9 hello10 hello这是MyThread 捕获interrupted!已经通知hello 执行中断 这是HelloThread 异常Main end.....
另一个常用的中断线程的方法是设置标志位。我们通常会用一个running标志位来标识线程是否应该继续运行,在外部线程中,通过把HelloThread.running置为false,就可以让线程结束:
/** * 测试线程中断 */public class testThreadInterruptFlag { public static void main(String[] args) throws InterruptedException{ System.out.println("主线程执行开始...."); HThread hello1 = new HThread(); hello1.start(); Thread.sleep(100); hello1.running = false; hello1.join(); System.out.println("main end....."); }}class HThread extends Thread{ /* 线程间共享变量需要使用volatile关键字标记,确保每个线程都能读取到更新后的变量值。 */ public volatile boolean running = true; @Override public void run() { int i = 0; while (running){ i++; System.out.println(i+ " + hello"); } System.out.println("hello end..."); }}
注意到HelloThread的标志位boolean running是一个线程间共享的变量。线程间共享变量需要使用volatile关键字标记,确保每个线程都能读取到更新后的变量值。
volatile详解
mashang,公众号:程序猿杂货摊网易云课堂笔记总结1
小结
对目标线程调用interrupt()方法可以请求中断一个线程,目标线程通过检测isInterrupted()标志获取自身是否已中断。如果目标线程处于等待状态,该线程会捕获到InterruptedException;
目标线程检测到isInterrupted()为true或者捕获了InterruptedException都应该立刻结束自身线程;
通过标志位判断需要正确使用volatile关键字;
volatile关键字解决了共享变量在线程间的可见性问题。
4
守护线程
Java程序入口就是由JVM启动main线程,main线程又可以启动其他线程。当所有线程都运行结束时,JVM退出,进程结束。如果有一个线程没有退出,JVM进程就不会退出。所以,必须保证所有线程都能及时结束。
守护线程是指为其他线程服务的线程。在JVM中,所有非守护线程都执行完毕后,无论有没有守护线程,虚拟机都会自动退出。
创建守护线程
Thread t = new TestDaemon(); t.setDaemon(true); System.out.println(t.isDaemon()); t.start();
小结
守护线程是为其他线程服务的线程;
所有非守护线程都执行完毕后,虚拟机退出;
守护线程不能持有需要关闭的资源(如打开文件等)。
5
线程同步
当多个线程同时运行时,线程的调度由操作系统决定,程序本身无法决定。因此,任何一个线程都有可能在任何指令处被操作系统暂停,然后在某个时间段后继续执行。这个时候,有个单线程模型下不存在的问题就来了:如果多个线程同时读写共享变量,会出现数据不一致的问题。
![4cf284673665b6c9aae46f4dfffda5b6.gif](https://i-blog.csdnimg.cn/blog_migrate/435c59a4acdb1912450a82f292246ef2.gif)
public class TestThreadSyn { public static void main(String[] args) throws InterruptedException{ System.out.println("main start..."); for(int i = 0;i<1000;i++){ Thread add = new ADDThread(); Thread dec = new DECThread(); add.start(); dec.start(); add.join(); dec.join(); System.out.println("第" +i + "次" + Counter.counter); } System.out.println("main end..."); }}class Counter{ public static final Object lock = new Object(); public static int counter = 0;}class ADDThread extends Thread{ public void run(){ for(int i = 0;i<10000;i++){ /* synchronized (Counter.class) 此时用Counter类对象本身也可以锁 */ synchronized (Counter.class){ Counter.counter += 1; } /* synchronized (Counter.lock) 此时用的是Counter类中的实例对象,实例对象可以访问类的静态变量 */// synchronized (Counter.lock){// Counter.counter += 1;// } } }}class DECThread extends Thread{ public void run(){ for(int i = 0;i<10000;i++){ synchronized (Counter.class){ Counter.counter -= 1; } } }}//结果:...第993次0第994次0第995次0第996次0第997次0第998次0第999次0main end...
对变量进行读取和写入时,结果要正确,必须保证是原子操作。原子操作是指不能被中断的一个或一系列操作。
例如,对于语句:
n = n + 1;
看上去是一行语句,实际上对应了3条指令:
ILOADIADDISTORE
我们假设n的值是100,如果两个线程同时执行n = n + 1,得到的结果很可能不是102,而是101,原因在于:
┌───────┐ ┌───────┐
│Thread1│ │Thread2│
└───┬───┘ └───┬───┘
│ │
│ILOAD (100) │
│ │ILOAD (100)
│ │IADD
│ │ISTORE (101)
│IADD │
│ISTORE (101)│
▼ ▼
如果线程1在执行ILOAD后被操作系统中断,此刻如果线程2被调度执行,它执行ILOAD后获取的值仍然是100,最终结果被两个线程的ISTORE写入后变成了101,而不是期待的102。
这说明多线程模型下,要保证逻辑正确,对共享变量进行读写时,必须保证一组指令以原子方式执行:即某一个线程执行时,其他线程必须等待:
┌───────┐ ┌───────┐
│Thread1│ │Thread2│
└───┬───┘ └───┬───┘
│ │
│-- lock -- │
│ILOAD (100) │
│IADD │
│ISTORE (101) │
│-- unlock -- │
│ │-- lock --
│ │ILOAD (101)
│ │IADD
│ │ISTORE (102)
│ │-- unlock --
▼ ▼
通过加锁和解锁的操作,就能保证3条指令总是在一个线程执行期间,不会有其他线程会进入此指令区间。即使在执行期线程被操作系统中断执行,其他线程也会因为无法获得锁导致无法进入此指令区间。只有执行线程将锁释放后,其他线程才有机会获得锁并执行。这种加锁和解锁之间的代码块我们称之为临界区(Critical Section),任何时候临界区最多只有一个线程能执行。
可见,保证一段代码的原子性就是通过加锁和解锁实现的。Java程序使用synchronized关键字对一个对象进行加锁:
synchronized(lock) { n = n + 1;}
//锁的错误用法public class TestSyn5 { public static void main(String[] args) throws Exception { Thread add = new AddThread(); Thread dec = new DecThread(); add.start(); dec.start(); add.join(); dec.join(); System.out.println(Counter5.count); }}class Counter5 { public static final Object lock1 = new Object(); public static final Object lock2 = new Object(); public static int count = 0;}class AddThread extends Thread { public void run() { for (int i=0; i<10000; i++) { synchronized(Counter5.lock1) { Counter5.count += 1; } } }}class DecThread extends Thread { public void run() { for (int i=0; i<10000; i++) { synchronized(Counter5.lock2) { Counter5.count -= 1; } } }}/*结果并不是0,这是因为两个线程各自的synchronized锁住的不是同一个对象!这使得两个线程各自都可以同时获得锁:因为JVM只保证同一个锁在任意时刻只能被一个线程获取,但两个不同的锁在同一时刻可以被两个线程分别获取。两个线程可以同时操作counter,例如下图 */
┌───────┐ ┌───────┐
│Thread1│ │Thread2│
└───┬───┘ └───┬───┘
│ │
│ILOAD (100) │
│ │ILOAD (100)
│ │IDEC
│ │ISTORE (99)
│IADD │
│ISTORE (101)│
▼ ▼
线程同步实例
package com.sun;/** * @Auther mashang * @Date 2020-11-27 14:50 * @Version 1.0 */public class TestSyn3 implements Runnable{ public static int i =0; /* 不加static 时,synchronized锁住的对象是this,即当前实例,获得类实例对象的锁就可以访问当前代码块。 但是多个实例同时访问该类变量时不是线程安全的,add()方法对实例对象加锁,是实例就可以访问, 锁是给两个实例加的锁,并没有达到同步的效果 */ public static synchronized void add(){ i++; }// public synchronized void add(){// i++;// } public void run(){ for(int i = 0;i<100000;i++){ add(); } } public static void main(String[] args) throws InterruptedException { Thread t1 = new Thread(new TestSyn3()); Thread t2 = new Thread(new TestSyn3()); t1.start(); t2.start(); t1.join(); t2.join();// System.out.println("public synchronized void add() 执行结果:" + i); System.out.println("public static synchronized void add() 执行结果:" + i); }}
不加static执行结果
加static执行结果
不需要synchronized的操作JVM规范定义了几种原子操作:
基本类型(long和double除外)赋值,例如:int n = m;
引用类型赋值,例如:List list = anotherList。
long和double是64位数据,JVM没有明确规定64位赋值操作是不是一个原子操作,不过在x64平台的JVM是把long和double的赋值作为原子操作实现的。
单条原子操作的语句不需要同步。例如:
public void set(int m) { synchronized(lock) { this.value = m; }}
就不需要同步,对引用也是类似。例如:
public void set(String s) { this.value = s;}
上述赋值语句并不需要同步,但是,如果是多行赋值语句,就必须保证是同步操作,例如:
class Pair { int first; int last; public void set(int first, int last) { synchronized(this) { this.first = first; this.last = last; } }}
有些时候,通过一些巧妙的转换,可以把非原子操作变为原子操作。例如,上述代码如果改造成:
class Pair { int[] pair; public void set(int first, int last) { int[] ps = new int[] { first, last }; this.pair = ps; }}
就不再需要同步,因为this.pair = ps是引用赋值的原子操作。而语句:
int[] ps = new int[] { first, last };
这里的ps是方法内部定义的局部变量,每个线程都会有各自的局部变量,互不影响,并且互不可见,并不需要同步。
小结
多线程同时读写共享变量时,会造成逻辑错误,因此需要通过synchronized同步;
同步的本质就是给指定对象加锁,加锁后才能继续执行后续代码;
注意加锁对象必须是同一个实例;
对JVM定义的单个原子操作不需要同步。
6
死锁
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; }}
观察synchronized修饰的add()方法,一旦线程执行到add()方法内部,说明它已经获取了当前实例的this锁。如果传入的n < 0,将在add()方法内部调用dec()方法。由于dec()方法也需要获取this锁,现在问题来了:
对同一个线程,能否在获取到锁以后继续获取同一个锁?
答案是肯定的。JVM允许同一个线程重复获取同一个锁,这种能被同一个线程反复获取的锁,就叫做可重入锁。
由于Java的线程锁是可重入锁,所以,获取锁的时候,不但要判断是否是第一次获取,还要记录这是第几次获取。每获取一次锁,记录+1,每退出synchronized块,记录-1,减到0的时候,才会真正释放锁。
死锁
一个线程可以获取一个锁后,再继续获取另一个锁。例如:
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的锁}
如何避免死锁呢?答案是:线程获取锁的顺序要一致。即严格按照先获取lockA,再获取lockB的顺序,改写dec()方法如下:
public void dec(int m) { synchronized(lockA) { // 获得lockA的锁 this.value -= m; synchronized(lockB) { // 获得lockB的锁 this.another -= m; } // 释放lockB的锁 } // 释放lockA的锁}
7
使用wait和notify
在Java程序中,synchronized解决了多线程竞争的问题。例如,对于一个任务管理器,多个线程同时往队列中添加任务,可以用synchronized加锁:
多线程协调运行的原则就是:当条件不满足时,线程进入等待状态;当条件满足时,线程被唤醒,继续执行任务。
wait()方法的执行机制非常复杂。首先,它不是一个普通的Java方法,而是定义在Object类的一个native方法,也就是由JVM的C代码实现的。其次,必须在synchronized块中才能调用wait()方法,因为wait()方法调用时,会释放线程获得的锁,wait()方法返回后,线程又会重新试图获得锁。只能在锁对象上调用wait()方法。因为在getTask()中,我们获得了this锁,因此,只能在this对象上调用wait()方法:
public synchronized String getTask() { while (queue.isEmpty()) { // 释放this锁: this.wait(); // 重新获取this锁 } return queue.remove();}
当一个线程在this.wait()等待时,它就会释放this锁,从而使得其他线程能够在addTask()方法获得this锁。
现在我们面临第二个问题:如何让等待的线程被重新唤醒,然后从wait()方法返回?答案是在相同的锁对象上调用notify()方法。我们修改addTask()如下:
public synchronized void addTask(String s) { this.queue.add(s); this.notify(); // 唤醒在this锁等待的线程}
注意到在往队列中添加了任务后,线程立刻对this锁对象调用notify()方法,这个方法会唤醒一个正在this锁等待的线程(就是在getTask()中位于this.wait()的线程),从而使得等待线程从this.wait()方法返回。
![4cf284673665b6c9aae46f4dfffda5b6.gif](https://i-blog.csdnimg.cn/blog_migrate/435c59a4acdb1912450a82f292246ef2.gif)
//完整实例如下:class TaskQueen{ Queuequeue = new LinkedList<>(); public synchronized void addTask(String s){ this.queue.add(s); this.notifyAll(); 唤醒在this锁等待的所有线程 } public synchronized String getTask() throws InterruptedException { while(queue.isEmpty()) { this.wait(); } return queue.remove(); }}
注意到wait()方法返回时需要重新获得this锁。
小结
wait和notify用于多线程协调运行:
在synchronized内部可以调用wait()使线程进入等待状态;
必须在已获得的锁对象上调用wait()方法;
在synchronized内部可以调用notify()或notifyAll()唤醒其他等待线程;
必须在已获得的锁对象上调用notify()或notifyAll()方法;
已唤醒的线程还需要重新获得锁后才能继续执行。
![994e364e30c1e50032ea40de2f571cc6.png](https://i-blog.csdnimg.cn/blog_migrate/3e6451438eec58570d39def1ed0dcc2f.png)
文案@mashang 编辑@mashang