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

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

  1. 后端开发与架构
  2. 2016.01.07
  3. 11 min read

几个多线程概念的介绍

线程状态转换

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() 方法,则该线程结束生命周期。死亡的线程不可再次复生。

新建线程

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

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

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

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

Thread thread = new Thread("t1"){
	@Override
	public void run(){
		System.out.println(Thread.currentThread().getName());
	}
};
thread.start();

如果调用 start,则输出是 t1

Thread thread = new Thread("t1"){
	@Override
	public void run(){
		System.out.println(Thread.currentThread().getName());
	}
};
thread.run();

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

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

Thread thread = new Thread("t1")
{
	@Override
	public void run()
	{
		// TODO Auto-generated method stub
		System.out.println(Thread.currentThread().getName());
	}
};

第二种方式

# 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,导致了数据不一致。 最重要的是这种错误不会抛出异常,将很难被发现。

线程中断

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

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

优雅的终止线程

public void run(){
    while(true){
        if(Thread.currentThread().isInterrupted()){
           System.out.println("Interruted!");
           break;
        }
        Thread.yield();
    }
}

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

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

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,将一直占有这把锁,造成死锁发生。

使用代码模拟:

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。

in t1
in t2

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

join 和 yeild

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

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

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 的本质:

while(isAlive()) {
   wait(0);
}

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

守护线程

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

开启守护进程:

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

线程优先级

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

Java 并发编程 学习笔记