基本概念
在计算机中,我们把一个任务称为一个进程,浏览器就是一个进程,视频播放器是另一个进程,类似的,音乐播放器和Word都是进程。某些进程内部还需要同时执行多个子任务。例如,我们在使用Word时,Word可以让我们一边打字,一边进行拼写检查,同时还可以在后台进行打印,我们把子任务称为线程。进程和线程的关系就是:一个进程可以包含一个或多个线程,但至少会有一个线程。进程和线程是包含关系,但是多任务既可以由多进程实现,也可以由单进程内的多线程实现,还可以混合多进程+多线程
多进程的缺点
- 创建进程比创建线程开销大,尤其是在Windows系统上
- 进程间通信比线程间通信要慢,因为线程间通信就是读写同一个变量,速度很快
多进程的优点
多进程稳定性比多线程高,因为在多进程的情况下,一个进程崩溃不会影响其他进程,而在多线程的情况下,任何一个线程崩溃会直接导致整个进程崩溃
线程创建
Java语言内置了多线程支持。当Java程序启动的时候,实际上是启动了一个JVM进程,然后,JVM启动主线程来执行main()方法。在main()方法中,我们又可以启动其他线程。通常可以继承Thread或实现Runnable来自定义多线程类
直接调用run()方法,相当于调用了一个普通的Java方法,当前线程并没有任何改变,也不会启动新线程。上述代码实际上是在main()方法内部又调用了run()方法,打印hello语句是在main线程中执行的,没有任何新线程被创建。
必须调用Thread实例的start()方法才能启动新线程,如果我们查看Thread类的源代码,会看到start()方法内部调用了一个private native void start0()方法,native修饰符表示这个方法是由JVM虚拟机内部的C代码实现的,不是由Java代码实现的
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33
| import java.io.*;
public class Main { public static void main(String[] args) throws IOException { Thread t1 = new Thread(new MyThread()); t1.start();
Thread t2 = new Thread(new MyRunnable()); t2.start();
Thread t = new Thread(() -> { System.out.println("start new thread!"); }); t.start(); } }
class MyThread extends Thread { @Override public void run() { System.out.println("Thread start new thread!"); } }
class MyRunnable implements Runnable { @Override public void run() { System.out.println("Runnable start new thread!"); } }
|
Java用Thread对象表示一个线程,通过调用start()启动一个新线程
- 一个线程对象只能调用一次start()方法
- 线程的执行代码写在run()方法中
- 线程调度由操作系统决定,程序本身无法决定调度顺序
- Thread.sleep()可以把当前线程暂停一段时间
线程的优先级
Thread.setPriority(int n) // 1~10, 默认值5
JVM自动把1(低)~10(高)的优先级映射到操作系统实际优先级上(不同操作系统有不同的优先级数量)。优先级高的线程被操作系统调度的优先级较高,操作系统对高优先级线程可能调度更频繁,但我们决不能通过设置优先级来确保高优先级的线程一定会先执行
线程的状态
Java线程的状态有以下几种
- New:新创建的线程,尚未执行;
- Runnable:运行中的线程,正在执行run()方法的Java代码
- Blocked:运行中的线程,因为某些操作被阻塞而挂起
- Waiting:运行中的线程,因为某些操作在等待中
- Timed Waiting:运行中的线程,因为执行sleep()方法正在计时等待
- Terminated:线程已终止,因为run()方法执行完毕
线程终止的原因有:
- 线程正常终止:run()方法执行到return语句返回;
- 线程意外终止:run()方法因为未捕获的异常导致线程终止;
- 对某个线程的Thread实例调用stop()方法强制终止(强烈不推荐使用)
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| import java.io.*;
public class Main { public static void main(String[] args) throws IOException, InterruptedException { Thread t = new Thread(() -> { System.out.println("hello"); }); System.out.println("start"); t.start(); t.join(); System.out.println("end"); } }
|
中断线程
如果线程需要执行一个长时间任务,就可能需要能中断线程。中断线程就是其他线程给该线程发一个信号,该线程收到信号后结束执行run()方法,使得自身线程能立刻结束运行
调用interruptf方法
main线程通过调用t.interrupt()从而通知t线程中断,而此时t线程正位于hello.join()的等待中,此方法会立刻结束等待并抛出InterruptedException。由于我们在t线程中捕获了InterruptedException,因此,就可以准备结束该线程。在t线程结束前,对hello线程也进行了interrupt()调用通知其中断。如果去掉这一行代码,可以发现hello线程仍然会继续运行,且JVM不会退出
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38
| public class Main { public static void main(String[] args) throws InterruptedException { Thread t = new MyThread(); t.start(); Thread.sleep(1000); t.interrupt(); t.join(); System.out.println("end"); } }
class MyThread extends Thread { public void run() { Thread hello = new HelloThread(); hello.start(); try { hello.join(); } catch (InterruptedException e) { System.out.println("interrupted!"); } hello.interrupt(); } }
class HelloThread extends Thread { public void run() { int n = 0; while (!isInterrupted()) { n++; System.out.println(n + " hello!"); try { Thread.sleep(100); } catch (InterruptedException e) { break; } } } }
|
设置标志位
我们通常会用一个running标志位来标识线程是否应该继续运行,在外部线程中,通过把HelloThread.running置为false,就可以让线程结束
要对线程间共享的变量用关键字volatile声明。这涉及到Java的内存模型。在Java虚拟机中,变量的值保存在主内存中,但是,当线程访问变量时,它会先获取一个副本,并保存在自己的工作内存中。如果线程修改了变量的值,虚拟机会在某个时刻把修改后的值回写到主内存,但是,这个时间是不确定的
volatile关键字的目的是告诉虚拟机:
- 每次访问变量时,总是获取主内存的最新值
- 每次修改变量后,立刻回写到主内存
volatile关键字解决的是可见性问题:当一个线程修改了某个共享变量的值,其他线程能够立刻看到修改后的值
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| public class Main { public static void main(String[] args) throws InterruptedException { HelloThread t = new HelloThread(); t.start(); Thread.sleep(1); t.running = false; } }
class HelloThread extends Thread { public volatile boolean running = true;
public void run() { int n = 0; while (running) { n++; System.out.println(n + " hello!"); } System.out.println("end!"); } }
|
守护线程
守护线程是指为其他线程服务的线程。在JVM中,所有非守护线程都执行完毕后,无论有没有守护线程,虚拟机都会自动退出。因此,JVM退出时,不必关心守护线程是否已结束
在守护线程中,编写代码要注意:守护线程不能持有任何需要关闭的资源,例如打开文件等,因为虚拟机退出时,守护线程没有任何机会来关闭文件,这会导致数据丢失。创建守护线程的方法和普通线程一样,只是在调用start()方法前,调用setDaemon(true)把该线程标记为守护线程
线程同步
当多个线程同时运行时,线程的调度由操作系统决定,程序本身无法决定。因此,任何一个线程都有可能在任何指令处被操作系统暂停,然后在某个时间段后继续执行。这个时候,有个单线程模型下不存在的问题就来了:如果多个线程同时读写共享变量,会出现数据不一致的问题。对变量进行读取和写入时,结果要正确,必须保证是原子操作。原子操作是指不能被中断的一个或一系列操作。
通过加锁和解锁的操作,就能保证3条指令总是在一个线程执行期间,不会有其他线程会进入此指令区间。即使在执行期线程被操作系统中断执行,其他线程也会因为无法获得锁导致无法进入此指令区间。只有执行线程将锁释放后,其他线程才有机会获得锁并执行。这种加锁和解锁之间的代码块我们称之为临界区(Critical Section),任何时候临界区最多只有一个线程能执行。保证一段代码的原子性就是通过加锁和解锁实现的。Java程序使用synchronized关键字对一个对象进行加锁
同步变量
使用synchronized:
- 找出修改共享变量的线程代码块
- 选择一个共享实例作为锁
- 使用synchronized(lockObject) { … }
原子操作
- 基本类型(long和double除外)赋值,例如:int n = m;
- 引用类型赋值,例如:Listlist = anotherList
- 在x64平台的JVM是把long和double的赋值作为原子操作实现的
- 如果是多行赋值语句,就必须保证是同步操作
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32
| public class Main { static int i; public static final Object lock = new Object(); public static void main(String[] args) throws Exception { Thread t1 = new Thread(() -> { for (int i = 1; i <= 1000; i++) { increment(); } }); Thread t2 = new Thread(() -> { for (int i = 1; i <= 1000; i++) { increment(); } });
t1.start(); t2.start(); try { t1.join(); t2.join(); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("计算值为:" + i); }
static void increment() { synchronized (lock) { i++; } } }
|
同步方法
如果一个类被设计为允许多线程正确访问,我们就说这个类就是“线程安全”的(thread-safe),上面的Counter类就是线程安全的。Java标准库的java.lang.StringBuffer也是线程安全的。还有一些不变类,例如String,Integer,LocalDate,它们的所有成员变量都是final,多线程同时访问时只能读不能写,这些不变类也是线程安全的。
最后,类似Math这些只提供静态方法,没有成员变量的类,也是线程安全的。除了上述几种少数情况,大部分类,例如ArrayList,都是非线程安全的类,我们不能在多线程中修改它们。但是,如果所有线程都只读取,不写入,那么ArrayList是可以安全地在线程间共享的
对于static方法,是没有this实例的,因为static方法是针对类而不是实例。但是我们注意到任何一个类都有一个由JVM自动创建的Class实例,因此,对static方法添加synchronized,锁住的是该类的Class实例
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37
| public class Main { public static void main(String[] args) throws InterruptedException { Counter counter = new Counter(); Thread t1 = new Thread(() -> { counter.add(10); }); Thread t2 = new Thread(() -> { counter.dec(10); });
t1.start(); t2.start(); t1.join(); t2.join(); System.out.println(counter.get()); } }
class Counter { private int count = 0; public void add(int n) { synchronized (this) { count += n; } }
public void dec(int n) { synchronized (this) { count -= n; } }
public int get() { return count; } }
|
死锁
Java的synchronized锁是可重入锁。死锁产生的条件是多线程各自持有不同的锁,并互相试图获取对方已持有的锁,导致无限等待。避免死锁的方法是多线程获取锁的顺序要一致
死锁的四个必要条件:
1.互斥条件:一个资源每次只能被一个进程使用
2.请求与保持条件:一个进程因请求资源而阻塞时对已获得的资源保持不放。
3.不剥夺条件:进程已获得的资源,在未使用完之前,不能强行剥夺
4.循环等待条件:若干个进程之间形成一种头尾相接循环等待资源关系
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70
| public class Main { public static void main(String[] args) throws InterruptedException { MakeUp g1 = new MakeUp(0, "灰姑娘"); MakeUp g2 = new MakeUp(1, "白雪公主");
g1.start(); g2.start(); } }
class Lipstick {
}
class Mirror {
}
class MakeUp extends Thread { static Lipstick lipstick = new Lipstick(); static Mirror mirror = new Mirror();
int choice; String girlName;
MakeUp(int choice, String girlName) { this.choice = choice; this.girlName = girlName; }
@Override public void run() { try { makeup(); } catch (InterruptedException e) { e.printStackTrace(); } }
private void makeup() throws InterruptedException { if (choice == 0) { synchronized (lipstick) { System.out.println(this.girlName + "获得口红的锁"); Thread.sleep(1000);
} synchronized (mirror) { System.out.println(this.girlName + "获得镜子的锁"); } } else { synchronized (mirror) { System.out.println(this.girlName + "获得镜子的锁"); Thread.sleep(2000);
} synchronized (lipstick) { System.out.println(this.girlName + "获得口红的锁"); } }
} }
|
wait和notify方法
对象的wait()方法会暂时使得此线程进入等待状态,同时会释放当前代码块持有的锁,这时其他线程可以获取到此对象的锁,当其他线程调用对象的notify()方法后,会唤醒刚才变成等待状态的线程(这时并没有立即释放锁)。注意,必须是在持有锁(同步代码块内部)的情况下使用,否则会抛出异常!
notifyAll其实和notify一样,也是用于唤醒,但是前者是唤醒所有调用wait()后处于等待的线程,而后者是看运气随机选择一个
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29
| public class Main { public static void main(String[] args) throws InterruptedException { Object o1 = new Object(); Thread t1 = new Thread(() -> { synchronized (o1) { try { System.out.println("开始等待"); o1.wait(); System.out.println("等待结束!"); } catch (InterruptedException e) { e.printStackTrace(); } } }); Thread t2 = new Thread(() -> { synchronized (o1) { System.out.println("开始唤醒!"); o1.notify(); for (int i = 0; i < 5; i++) { System.out.println(i); } } }); t1.start(); Thread.sleep(1000); t2.start(); } }
|
ThreadLocal
使用ThreadLocal类,来创建工作内存中的变量,它将我们的变量值存储在内部(只能存储一个变量),不同的线程访问到ThreadLocal对象时,都只能获取到当前线程所属的变量
即使线程2重新设定了值,也没有影响到线程1存放的值,所以说,不同线程向ThreadLocal存放数据,只会存放在线程自己的工作空间中,而不会直接存放到主内存中,因此各个线程直接存放的内容互不干扰
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| public class Main { public static void main(String[] args) throws InterruptedException { ThreadLocal<String> local = new ThreadLocal<>(); Thread t1 = new Thread(() -> { local.set("lbwnb"); System.out.println("线程1变量值已设定!"); try { Thread.sleep(2000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("线程1读取变量值:"); System.out.println(local.get()); }); Thread t2 = new Thread(() -> { local.set("yyds"); System.out.println("线程2变量值已设定!"); }); t1.start(); Thread.sleep(1000); t2.start(); } }
|
在线程中创建的子线程,无法获得父线程工作内存中的变量。可以使用InheritableThreadLocal来解决。在InheritableThreadLocal存放的内容,会自动向子线程传递。