Thread
一、认识线程(thread)
1.线程(process)与进程(Thread)
进程:
进程是操作系统中运行的一个独立程序实例。每个进程都有自己独立的一块内存空间,由操作系统来分配给它们资源。在java中JVM本身就是一个进程,它从main方法开始执行,运行main的那个线程,在里面还可以创建其他线程来执行。
线程:
线程运行在进程的内部,相比较进程成本更低。
关于线程:
每个线程有自己的调用栈(call stack):用来存储方法调用、局部变量等。
线程共享进程的数据:同一进程内的线程能访问相同的堆内存数据(这也是线程安全问题的来源)。
每个线程也有自己的本地缓存(memory cache):
- 当线程读取共享数据时,它可能会把数据复制到自己的缓存里。
- 如果数据更新了,而线程还在用旧缓存,就会出现可见性问题。
2.在并发执行过程中的关键概念
1.Atomicity(原子性)
定义:一个操作是原子的,意味着它不可被中断,要么全部执行完成,要么完全不执行。
例子:
a = 5是原子操作,a++不是2.Visibility(可见性)
定义:一个线程对变量的修改,是否能被其他线程立刻看到。
问题来源:线程有自己的工作内存(缓存),变量更新可能先存在缓存里,而不是直接写入主内存。可能会导致条件竞争。
3.Order of execution(执行顺序)
在单线程程序里,代码会按照书写的顺序逐行执行。但是在并发编程中执行顺序不再保证,多线程可能交替运行,导致结果和预期不一样。
4. Critical code(临界区代码)
定义:一段只能由一个线程独占执行的代码,否则可能造成数据不一致或冲突。
例子:银行转账操作(扣钱 + 加钱)必须是临界区代码,否则会出错。
3.Thread类
在Thread类中我们需要将要执行的代码写在run()方法里面线程在启动之后将会自动地执行这个方法,如果我们需要执行run()方法里面的代码我们需要使用start()方法来启动线程,它会调用JVM的底层机制来创建新的线程进而执行run()的代码。
同时Thread类还负责线程的启动和调度(通过 JVM + 操作系统)、提供一些方法(join()、sleep()、interrupt() 等)来控制线程行为。
创建线程有两种方式:
使用Runnable接口,
implements Runnable1
2
3
4
5
6
7
8
9
10
11
12
13class MyTask implements Runnable {
public void run() {
System.out.println("Runnable 方式运行线程");
}
}
public class Main {
public static void main(String[] args) {
Thread t = new Thread(new MyTask());
t.start();
}
}继承
Thread类1
2
3
4
5
6
7
8
9
10
11
12
13class MyThread extends Thread {
public void run() {
System.out.println("继承 Thread 类运行线程");
}
}
public class Main {
public static void main(String[] args) {
MyThread t = new MyThread();
t.start();
}
}
推荐使用Runnable接口,因为 Java 不支持多继承,这样做会限制扩展性。
Using the Runnable interface allows a subclass of Thread to be used if required.
二、控制线程
1.sleep()
Thread.sleep(long millis) 会让当前正在执行的线程暂停指定毫秒数,进入 TIMED_WAITING,到点后回到可运行队列;这样能把 CPU 让给别的线程,同时sleep()是一个静态方法。
需要注意的是:
sleep()只能让“当前线程”睡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
32public class SleepOnlyCurrentThread implements Runnable {
public void run() {
System.out.println(Thread.currentThread().getName() + " 开始执行");
try {
// 在这里调用 sleep,睡的是“当前线程”,也就是 run() 里的线程
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + " 执行结束");
}
public static void main(String[] args) {
SleepOnlyCurrentThread task = new SleepOnlyCurrentThread();
Thread t1 = new Thread(task, "线程1");
Thread t2 = new Thread(task, "线程2");
t1.start();
t2.start();
try {
System.out.println("main 线程准备睡眠...");
t1.sleep(3000);// 实际上是主线程调用 sleep
System.out.println("main 线程醒来了");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}运行之后会发现
t1.sleep(3000);这段代码并不会让该线程睡眠3秒而是直接让main()的线程睡眠三秒,由此可见sleep()方法只能让该线程睡眠必须要处理
InterruptedException1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26public class SleepDemo implements Runnable{
public void run()
{
System.out.println("准备睡眠...");
//Thread.sleep(1000); // 编译错误:必须处理 InterruptedException
//正确做法
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
// 恢复中断标志,优雅退出
Thread.currentThread().interrupt();
System.out.println("线程在睡眠时被中断,安全退出");
}
System.out.println("醒来了!");
}
public static void main(String[] args) {
SleepDemo sd = new SleepDemo();
Thread t1 = new Thread(sd);
t1.start();
Thread t2 = new Thread(sd);
t2.start();
}
}这段代码编译过程中会出现
Unhandled exception type InterruptedException的报错,因此要使用sleep()时应该使用try/catch来捕获异常
2.Yield()
Thread.yield() 是一个 静态方法。它的作用是:提示调度器“当前线程愿意让出 CPU”,让同等优先级的其他线程有机会运行。但是不保证一定会切换,调度器可能忽略这个提示。
3.interrupt()
在Java中interrupt()用来通知线程该结束当前的工作了,注意:只是通知而不是强制。
一个线程可以对其他的线程执行interrupt()操作,执行之后线程对象的中断标志变量(一个布尔类型的变量)将会变为True。此时可以通过t.isInterrupted()来检查。
1 | class BusyTask implements Runnable { |
该程序即便在“主线程:中断标记是否为真?”出现之后也会一直运行,t.isInterrupted()会返回True。
当一个线程处于 sleep / wait 状态时,如果这个时候对这个线程执行interrupt(),并把中断标志设置为True,这时候由于线程正在sleep / wait,于是线程会立即抛出InterruptedException异常,抛出异常的同时JVM将中断标志立马设置为False
1 | // 定义一个任务类,实现 Runnable |
interrupt()的意义:
如果我们在等待某个线程完成,但它进入了 sleep() 或 wait() 状态(睡眠或等待),那就会无限卡住。这时候我们可以调用 interrupt(),强制唤醒它,这样程序才能继续往下走。
线程被阻塞的原因:
sleep()或wait()状态- 锁竞争一个进程拿到了锁另一个进程就必修等待
- I/O等待
4.线程之间的协调:join()
为什么需要线程之间的协调?
因为线程之间的执行顺序是不固定的,这样会导致代码执行过程中出错。假设你有 5 个线程分别做计算,最后要把结果相加,如果主线程不等它们完成就去加,会得到错误的结果。因此我们有的时候需要规范线程之间的执行顺序,等待其他线程执行完之后再执行当前的线程。
1 | // 第一个任务 |
在这个例子中线程t2会等待线程t1执行结束之后才会执行。
这时候读者可能会存在疑问如果把t1.start();注释掉,然后执行程序会怎么样?
答案是t2仍然会被执行不会因为t1没被执行而陷入一直等待的情况,原因是 join() 的语义是:等线程终止,如果它从来没启动过,那相当于立即终止。在java里面对于每一个线程对象,都会有它的状态信息,join()会不断地检查等待的线程的状态来判断是否让该线程执行,对于还没有被执行的线程,它的状态时New,join()在检查之后会返回False来告诉线程不用进行等待。
三、中断和终止线程
关于interrupt()之前已经做过介绍因此这里就不在赘述。
1.interrupted()
public static boolean interrupted()
静态方法,属于
Thread类。用来检查 当前线程 的中断状态(interrupt flag)。
会清除中断标志,调用一次之后会将原来标志位的True改为False。
1 | // 演示:Thread.interrupted() 会清除中断标志 |
从演示的结果可以看出来interrupted()会将原来标志位的True改为False。
因此在使用interrupted()时特别是interrupted()作为循环的判断条件的时,要记得把中断标志再设置会True。
1 | class WorkerThread extends Thread { |
在这里代码中的Thread.currentThread().interrupt();被注释掉了因为中断标志被重新变到了False,程序会陷入到死循环当中。
2.isInterrupted()
public boolean isInterrupted()
- 实例方法,作用在某个具体的
Thread对象上。可以用来检查别的线程的中断状态。 - 调用之后线程的中断标志不会被改变。
1 | public class IsInterruptedDemo { |
3.isAlive()
public boolean isAlive()
- 如果线程已启动并且仍在运行或者处于
waiting、sleeping或blocked状态中,isAlive()返回true。 - 如果线程已经 终止(即
run()方法执行完毕或者线程被中止),isAlive()返回false。
1 | class MyThread extends Thread { |
4.终止线程的方法
线程自然完成工作并退出
run()方法,正常地结束其生命周期,释放相关资源。对于守护线程(daemon thread),当所有非守护线程执行结束之后,所有的daemon thread都会被强制终止。
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
48class DaemonExample {
public static void main(String[] args) {
Thread userThread = new Thread(new Worker(), "用户线程");
Thread daemonThread = new Thread(new BackgroundTask(), "守护线程");
// 将后台线程设置为守护线程
daemonThread.setDaemon(true);
// 启动两个线程
userThread.start();
daemonThread.start();
System.out.println("主线程结束。");
}
}
// 模拟一个普通的用户线程
class Worker implements Runnable {
public void run() {
System.out.println(Thread.currentThread().getName() + " 开始工作。");
try {
for (int i = 1; i <= 3; i++) {
System.out.println(Thread.currentThread().getName() + " 正在执行第 " + i + " 步");
Thread.sleep(1000);
}
System.out.println(Thread.currentThread().getName() + " 工作完成!");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
// 模拟后台运行的守护线程
class BackgroundTask implements Runnable {
public void run() {
while (true) {
System.out.println(Thread.currentThread().getName() + " 正在后台运行...");
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}从示例代码中的运行结果可以看出守护进程在用户进程和主进程结束之后就会自动地结束。
线程在执行工作中收到中断信号,它会根据目前的情况,可能决定不再继续工作。
四、多线程中的数据安全
1.volatile关键字
正如前面所说线程有自己的本地缓存,当线程读取共享数据时,它可能会把数据复制到自己的缓存里,因此就有可能出现一个线程掌握新的数据信息但是其他线程掌握的是旧的数据信息的情况。
为了避免这样的情况volatile帮助我们做到了
保证可见性:所有读写操作都直接从主内存中获取或写入,而不会进入到线程本地缓存当中,并且每一次只能被一个线程加载和储存。
禁止指令重排优化:确保代码执行的顺序和我们写的逻辑一致。
需要注意的是
volatile并不能保证原子性对于i++这样的操作依然不能保证其的安全性,他只适合一些简单的读写操作。
!对于
volatile int[] arr;只有数组的引用是volatile的,而对于数组内部的元素arr[5]则并不是。!对于申明为
final的字段volatile是没有必要的
1 | class MyTask extends Thread { |
对于示例代码的的2、3两行,当running不是volatile时,程序会卡在子线程的循环里出不来,当给其加上关键字volatile时程序才能顺利结束。
接下来的代码将演示volatile并不能保证原子性
1 | public class RaceDemo { |
最后的执行结果counter的值可能小于2000(也可能是等于2000的,此时将循环的次数调大,则预期的竞争便会很容易出现)
2.锁与synchronized
临界区(Critical sections)
在程序中,从不同的并发线程访问相同数据的代码段被称为临界区,在java中我们使用锁与synchronized来控制临界区,来保证一个同步代码块在任何给定时间只能被单个线程访问
锁
在java中锁被分为内部锁和外部锁,在这里我们只讨论内部锁。锁应用于特定的代码段,如果一段代码被锁住那么其他线程则不能执行这段代码。通常我们使用synchronized关键字来修饰一段代码块以给其加上内部锁。
对于synchronized,可以用其修饰整个方法体,也可以修饰方法体下面的某段关键代码(但是必修提供内在锁的对象),以此提高并发性能。
1 | // 修饰一段代码 |
但是需要注意的是synchronized 方法本质上就是给 this 对象加锁,上述这两个修饰方法是等效的。同时,同一个对象上的锁是共享的,因此如果一个类有多个 synchronized 方法,在同一时刻,只能有一个线程执行这两个方法中的任意一个,比如:
1 | class BankAccount { |
如果一个线程在执行 deposit(),另一个线程想执行 withdraw(),它必须等 deposit() 里的锁释放。这样就可以保证 balance 数据的安全性。
**对于 static synchronized**锁的对象就是类对象,这个类的所有实例都共享同一把锁。
因此,如果要避免上述条件竞争的情况出现,给其代码块加上锁便可以避免。
1 | public class RaceDemo { |
这里也可以用synchronized来修饰类对象这样所有的实例共用一把锁也能避免上述条件竞争的发生。
3. synchronized
锁只会作用在
synchronized方法或代码块上,没被synchronized方法或代码块不会被锁限制。如果当前线程已经拿到了这个对象的锁,那么它可以在一个
synchronized方法中调用另外一个synchronized方法。1
2
3
4
5public synchronized void m1() {
m2(); // m2 不是同步方法,也能调用
}
public synchronized void m2() {}原因是对于锁而言,每个锁都有一个 计数器,当线程进入
synchronized方法时+1,退出时-1,只有在计数器为0的时候它才会让另外一个线程进入。synchronized不会自动继承,如果一个父类的方法是synchronized的,子类override这个方法时,不会自动继承synchronized修饰。必须在子类里再次显式写上synchronized,否则就是非同步方法。注意
synchronized必须加在 对象引用 上,不能加在基本类型(如int、double)上。
五、监视器(Monitor)
根据之前所讲的synchronized 做到了同一时刻只允许一个线程进入某段被保护的代码,但是很多情况下还需要线程之间的协作,比如一个线程负责往缓冲区写入数据,一个线程负责从缓冲区读出数据,这时候当缓冲区满了,线程就不能写入,缓冲区空了,线程也不能执行读取操作。
1. notify()和wait()
Monitor通过使用synchronized 、 notify()、 wait()来实现进程之间的协作,其中:
wait():让线程暂时挂起,释放当前持有的锁,线程进入等待集合(Wait Set),进入阻塞状态,等待被 notify() 或 notifyAll() 唤醒。
notify():唤醒等待集合(Wait Set)里的一个线程,被唤醒的线程会从 Wait Set 移动到 Entry Set,等待重新竞争锁,拿到锁之后它才会从 wait()里面返回,也就是从wait()方法里面退出。
notifyAll():唤醒所有wait set 里的线程,大家都去竞争锁,只有一个能真正执行,其余的还会继续等待。
Java 中每个对象都可以作为 Monitor,因为
synchronized、wait()、notify()都是依赖对象锁实现的。想要调用wait()和notify()必须先持有该对象的锁。
2.Monitor中的三种状态集合
Entry Set(入口集合)
当多个线程尝试进入某个
synchronized方法或代码块时,如果锁(Lock)已经被其他线程占有,这些线程会进入 Entry Set。这个集合里面的线程都是等待获取锁的状态,一旦锁释放,JVM 会从 Entry Set 里挑选一个线程(通常没有严格顺序,依赖 JVM 实现)去获取锁。Owner(锁的持有者)
一旦某个线程成功获取锁,它就成为Owner,可以进入临界区(critical section)执行受
synchronized保护的代码。Wait Set(等待集合)
如果线程在执行临界区时调用了
wait(),它会释放锁并进入Wait Set。进入 Wait Set 的线程并不是等待锁,而是等待 其他线程通过notify()或notifyAll()唤醒它。
3.死锁
死锁是多线程编程中常见的一种并发问题。它指的是 多个线程在竞争资源时,互相等待对方释放锁,导致所有线程都无法继续执行 的情况。比如:线程A拿到了资源1,想要资源2,线程B拿到了资源2,想要资源1,结果就是A和B都在等对方程序就卡住了。
1 | public class Deadlock { |
像示例代码这样第一个线程先是拿到了alphonse的锁然后需要调用gaston.bowBack(alphonse),但是此时第二个线程已经拿到了gaston的锁因此线程一卡住了,但是此时alphonse的锁还在线程一手里所以线程二也会卡在 alphonse.bowBack(gaston)这里。