Java 多线程入门:从概念到实战

几个多线程概念的介绍

线程状态转换

20241229154732_YGh7IluL.webp

  • 新建 (new): 新创建一个线程对象
  • 可运行 (runnable):线程对象创建后,其他线程(比如 main 线程)调用了该对象的 start() 方法。该状态的线程位于可运行线程池中,等待被线程调度选中,获取
    cpu 的使用权 。
  • 运行 (running):可运行状态(runnable) 的线程获得了 cpu 时间片(timeslice) ,执行程序代码。
  • 阻塞 (block):阻塞状态是指线程因为某种原因放弃了 cpu 使用权,也即让出了 cpu timeslice,暂时停止运行。直到线程进入可运行(runnable) 状态,才有机会再次获得
    cpu timeslice 转到运行 (running) 状态。阻塞的情况分三种:
    • 等待阻塞:运行 (running) 的线程执行 o.wait()方法,JVM 会把该线程放入等待队列 (waitting queue) 中。
    • 同步阻塞:运行 (running) 的线程在获取对象的同步锁时,若该同步锁被别的线程占用,则 JVM 会把该线程放入锁池 (lock pool) 中。
    • 其他阻塞:运行 (running) 的线程执行 Thread.sleep(long ms)或 t.join()方法,或者发出了 I/O 请求时,JVM 会把该线程置为阻塞状态。当 sleep()
      状态超时、join()等待线程终止或者超时、或者 I/O 处理完毕时,线程重新转入可运行 (runnable) 状态。
  • 死亡 (dead):线程 run()、main() 方法执行结束,或者因异常退出了 run() 方法,则该线程结束生命周期。死亡的线程不可再次复生。

新建线程

1
2
Thread thread = new Thread();
thread.start();

这样就开启了一个线程。
有一点需要注意的是

1
2
Thread thread = new Thread();
thread.run();

直接调用 run 方法是无法开启一个新线程的。
start 方法其实是在一个新的操作系统线程上面去调用 run 方法。换句话说,直接调用 run 方法而不是调用 start 方法的话,它并不会开启新的线程,而是在调用
run 的当前的线程当中执行你的操作。

1
2
3
4
5
6
7
Thread thread = new Thread("t1"){
@Override
public void run(){
System.out.println(Thread.currentThread().getName());
}
};
thread.start();

如果调用 start,则输出是 t1

1
2
3
4
5
6
7
Thread thread = new Thread("t1"){
@Override
public void run(){
System.out.println(Thread.currentThread().getName());
}
};
thread.run();

如果是 run, 则输出 main。(直接调用 run 其实就是一个普通的函数调用而已,并没有达到多线程的作用)
run 方法的实现有两种方式

第一种方式,直接覆盖 run 方法,就如刚刚代码中所示,最方便的用一个匿名类就可以实现。

1
2
3
4
5
6
7
8
9
Thread thread = new Thread("t1")
{
@Override
public void run()
{
// TODO Auto-generated method stub
System.out.println(Thread.currentThread().getName());
}
};

第二种方式

1
2
# CreateThread3() 实现了 Runnable 接口。
Thread t1=new Thread(new CreateThread3());

终止线程

Thread.stop() 不推荐使用。它会释放所有 monitor

在源码中已经明确说明 stop 方法被 Deprecated,在 Javadoc 中也说明了原因。
原因在于 stop 方法太过”暴力”了,无论线程执行到哪里,它将会立即停止掉线程。
20241229154732_QX4dTHlh.webp
当写线程得到锁以后开始写入数据,写完 id = 1,在准备将 name = 1 时被 stop, 释放锁。读线程获得锁进行读操作,读到的 id 为 1,而 name 还是
0,导致了数据不一致。
最重要的是这种错误不会抛出异常,将很难被发现。

线程中断

1
2
3
public void Thread.interrupt() // 中断线程
public boolean Thread.isInterrupted() // 判断是否被中断
public static boolean Thread.interrupted() // 判断是否被中断,并清除当前中断状态

Java 的中断是一种协作机制。也就是说调用线程对象的 interrupt 方法并不一定就中断了正在运行的线程,它只是要求线程自己在合适的时机中断自己。每个线程都有一个
boolean 的中断状态(不一定就是对象的属性,事实上,该状态也确实不是 Thread 的字段),interrupt 方法仅仅只是将该状态置为 true。对于非阻塞中的线程,
只是改变了中断状态, 即 Thread.isInterrupted() 将返回 true,并不会使程序停止;

优雅的终止线程

1
2
3
4
5
6
7
8
9
public void run(){
while(true){
if(Thread.currentThread().isInterrupted()){
System.out.println("Interruted!");
break;
}
Thread.yield();
}
}

对于可取消的阻塞状态中的线程, 比如等待在这些函数上的线程, Thread.sleep(), Object.wait(), Thread.join(), 这个线程收到中断信号后, 会抛出
InterruptedException, 同时会把中断状态置回为 false.

对于取消阻塞状态中的线程:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public void run(){
while(true){
if(Thread.currentThread().isInterrupted()){
System.out.println("Interruted!");
break;
}
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
System.out.println("Interruted When Sleep");
// 设置中断状态,抛出异常后会清除中断标记位
Thread.currentThread().interrupt();
}
Thread.yield();
}
}

线程挂起

挂起(suspend)和继续执行(resume)线程

  • suspend() 不会释放锁
  • 如果加锁发生在 resume() 之前 ,则死锁发生

这两个方法都是 Deprecated 方法,不推荐使用。
原因在于,suspend 不释放锁,因此没有线程可以访问被它锁住的临界区资源,直到被其他线程 resume。因为无法控制线程运行的先后顺序,如果其他线程的
resume 方法先被运行,那则后运行的 suspend,将一直占有这把锁,造成死锁发生。

使用代码模拟:

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
public class Test{
static Object u = new Object();
static TestSuspendThread t1 = new TestSuspendThread("t1");
static TestSuspendThread t2 = new TestSuspendThread("t2");
public static class TestSuspendThread extends Thread{
public TestSuspendThread(String name){
setName(name);
}
@Override
public void run(){
synchronized (u){
System.out.println("in " + getName());
Thread.currentThread().suspend();
}
}
}
public static void main(String[] args) throws InterruptedException{
t1.start();
Thread.sleep(100);
t2.start();
t1.resume();
t2.resume();
t1.join();
t2.join();
}
}

让 t1,t2 同时争夺一把锁,争夺到的线程 suspend,然后再 resume,按理来说,应该某个线程争夺后被 resume 释放了锁,然后另一个线程争夺掉锁,再被 resume。

1
2
in t1
in t2

说明两个线程都争夺到了锁,但是控制台的红灯还是亮着的,说明 t1,t2 一定有线程没有执行完。

join 和 yeild

yeild 是个 native 静态方法,这个方法是想把自己占有的 cpu 时间释放掉,然后和其他线程一起竞争 (注意 yeild 的线程还是有可能争夺到 cpu,注意与
sleep 区别)。在 javadoc 中也说明了,yeild 是个基本不会用到的方法,一般在 debug 和 test 中使用。

join 方法的意思是等待其他线程结束,就如 suspend 那节的代码,想让主线程等待 t1,t2 结束以后再结束。没有结束的话,主线程就一直阻塞在那里。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class Test{
public volatile static int i = 0;
public static class AddThread extends Thread{
@Override
public void run(){
for (i = 0; i < 10000000; i++)
;
}
}

public static void main(String[] args) throws InterruptedException{
AddThread at = new AddThread();
at.start();
at.join();
System.out.println(i);
}
}

如果把上述代码的 at.join 去掉,则主线程会直接运行结束,i 的值会很小。如果有 join, 打印出的 i 的值一定是 10000000。

join 的本质:

1
2
3
while(isAlive()) {
wait(0);
}

join() 方法也可以传递一个时间,意为有限期地等待,超过了这个时间就自动唤醒。
这样就有一个问题,谁来 notify 这个线程呢,在 thread 类中没有地方调用了 notify?
在 javadoc 中,找到了相关解释。当一个线程运行完成终止后,将会调用 notifyAll 方法去唤醒等待在当前线程实例上的所有线程, 这个操作是 jvm 自己完成的。
所以 javadoc 中还给了我们一个建议,不要使用 wait 和 notify/notifyall 在线程实例上。因为 jvm 会自己调用,有可能与你调用期望的结果不同。

守护线程

  • 在后台默默地完成一些系统性的服务,比如垃圾回收线程、JIT 线程就可以理解为守护线程。
  • 当一个 Java 应用内,所有非守护进程都结束时,Java 虚拟机就会自然退出。

开启守护进程:

1
2
3
Thread t=new DaemonT();
t.setDaemon(true);
t.start()

线程优先级

1
2
3
public final static int MIN_PRIORITY = 1;
public final static int NORM_PRIORITY = 5;
public final static int MAX_PRIORITY = 10;

线程优先级只是表示获取锁的概率大小

基本的线程同步操作

synchronized 有三种加锁方式:

  • 指定加锁对象:对给定对象加锁,进入同步代码前要获得给定对象的锁。
  • 直接作用于实例方法:相当于对当前实例加锁,进入同步代码前要获得当前实例的锁。
  • 直接作用于静态方法:相当于对当前类加锁,进入同步代码前要获得当前类的锁。

作用于实例方法,则不要 new 两个不同的实例
作用于静态方法,只要类一样就可以了,因为加的锁是类.class,可以 new 两个不同实例。

wait 和 notify 的用法:
用什么锁住,就用什么调用 wait 和 notify