Java并发深度总结:基础线程机制

时间很贪婪——有时候,它会独自吞噬所有的细节。——《追风筝的人》

1.线程状态

一个线程在某一时刻只能处于一种状态,这里讨论的线程状态特指Java虚拟机的线程状态,并不反映线程在特定操作系统下的状态,也就是说Java虚拟机的线程状态和操作系统的状态并不是一一对应的。

Java在Thread类中定义了State枚举类,列举了6种线程状态:

 public class Thread implements Runnable {
        //线程的状态以枚举的方式定义在Thread类的内部
        public enum State {
          
            NEW, 			// 新建
			
            RUNNABLE,		// 运行

            BLOCKED,		// 阻塞

            WAITING,		// 无限期等待
            
            TIMED_WAITING,	// 限期等待
            
            TERMINATED;		// 终止
        }
}

因此,Java中的线程状态分别是:

  • 新建(NEW):线程刚被创建,还未调用start启动。
  • 运行(RUNNABLE):调用了 strat 方法。该状态下,线程可能正在运行,也可能处于就绪状态,等待CPU调度。
  • 无限期等待(WAITING):让出CPU执行权,线程休眠,等待其他线程显式地唤醒。
  • 限期等待(TIMED_WAITING):让出CPU执行权,线程休眠,在一定时间内,即使没有其他线程显式地唤醒该线程,该线程也能被自动唤醒。
  • 阻塞(BLOCKED):线程被阻塞于锁,这个事件将在另外一个线程获得锁的时候可能发生。
  • 终止(TERMINATED):终止状态,线程已结束运行。

注意

  • Java中的运行(RUNNABLE)状态对应了操作系统中的就绪态(Ready)和运行态(Running),因此在Java中,处于RUNNABLE状态的线程不一定正在被执行,也可能处于就绪状态,等着被CPU调度。
  • 一个线程想被运行(Running)的先决条件是他要处于就绪状态(Ready)。一个处于就绪状态的线程意味着,该线程已经拿到了除CPU资源的其他任何需要的资源,他一拿到CPU的执行权就可以立即被执行。
  • 当进行阻塞IO操作时,线程的状态是RUNNABLE,而非WAITING或BLOCKED。
  • 在操作系统层面的阻塞和Java线程的阻塞含义相差甚远。
2.线程状态转化

每一个线程在他的生命周期内有以上6中不同的状态,这也是进程动态性的表现,随着CPU的调度和特定的事件发生,线程的状态会发生转换。

Java线程状态转化图:
在这里插入图片描述

2.1 NEW ----> RUNNABLE

当一个线程刚被创建出来时,就处于新建状态(NEW),调用Thread.start()方法,开启一个线程,该线程进入运行状态(RUNNABLE)。

运行状态(RUNNABLE)包含了正在运行(Running)和就绪状态(Ready):

  • Ready --> Running : CPU为该线程分配时间片,线程拿到CPU执行权,由就绪(Ready)状态转化为正在运行(Running)。
  • Running --> Ready :该线程的CPU时间片使用完了或该线程将CPU的执行权礼让(Thread.yield)给了其他线程,该线程由正在运行(Running)转化为就绪(Ready)状态。

注意:一个线程从新建(NEW) ---- > 运行(RUNNABLE)只会发生一次,也就是说一个线程被创建出来之后只能调用一次start()方法,多次调用start()方法启动一个线程会抛出IllegalThreadStateException,即使该线程已经运行结束了(TERMINATED)也不能再次启动。

import java.util.concurrent.TimeUnit;

public class ThreadTest {

    public static void main(String[] args) throws Exception{
        
        Thread t = new Thread(() -> {
            System.out.println("run end !");
        });

        t.start();
        Thread.sleep(1000l);
		System.out.println(t.getState()); // TERMINATED
        t.start(); // 再次启动线程:IllegalThreadStateException
    }
}
输出:
run end !
TERMINATED
Exception in thread "main" java.lang.IllegalThreadStateException
	at java.lang.Thread.start(Thread.java:708)
	...
2.2 RUNNABLE <----> WAITING

运行状态(RUNNABLE)和无限期等待(WAITING)两种状态可以相互转换:

RUNNABLE --> WAITING:

  • 调用了没有设置Timeout参数的Object.wait()方法。
  • 调用了没有设置Timeout参数的Thread.join()方法。
  • LockSupport.park()方法。

WAITING --> RUNNABLE :

  • 其他线程调用了Object.notify() 或 Object.notifyAll() 将处于WAITING 状态的线程唤醒。
  • LockSupport.unpark(Thread)方法。

注意:

  • RUNNABLE --> WAITING状态转换中,准确的说是运行状态中的正在运行(Running) 向 WAITING状态转换,因为处于就绪状态的线程没有CPU的执行权,因此也就不能直接由就绪(Ready)转化为 无限期等待(WAITING)状态。
  • 同样的,WAITING --> RUNNABLE状态转换中,实际上是无限等待(WAITING )转化为运行状态中的就绪(Ready)状态。
2.3 RUNNABLE <----> TIMED_WAITING

与运行状态和无限期等待的相互转换类似:

RUNNABLE --> TIMED_WAITING:

  • 调用了没有设置Timeout参数的Object.wait()方法。
  • 调用了没有设置Timeout参数的Thread.join()方法。
  • Thread.sleep(long millis)方法。
  • LockSupport.parkNanos()方法。
  • LockSupport.parkUntil()方法。

TIMED_WAITING --> RUNNABLE :

  • 其他线程调用了Object.notify() 或 Object.notifyAll() 将处于WAITING 状态的线程唤醒。
  • LockSupport.unpark(Thread)方法。
  • 达到设置的超时时间,该线程被自动唤醒。
2.4 RUNNABLE <----> BLOCKED

Java中的阻塞状态(BLOCKED)指:线程被阻塞于锁,也就是线程要进入同步方法或同步代码块时,其他的线程已经持有同步方法或同步代码块的锁,因此该线程等待其他线程释放锁而处于阻塞状态。

RUNNABLE --> BLOCKED:

  • 线程进入同步方法或同步代码块时,其他线程未释放锁。

BLOCKED --> RUNNABLE:

  • 线程被阻塞后,竞争锁成功。
2.5 RUNNABLE ----> TERMINATED

当发生以下情况时,线程会由运行状态转换为终止状态:

  • 线程run()方法正常运行结束。
  • 线程run()方法抛出未被捕获的异常。
  • 调用了Thread.stop()方法,强制地终止了线程。

再次说明一下:处于终止状态(TERMINATED)的线程无法再次启动(调用start()方法),即使线程对象还在。

3.线程控制
3.1 线程休眠:Thread.sleep

Thread类的静态方法:线程休眠,需指定当前线程的休眠时间,可能抛出异常。该方法让线程由运行状态(RUNNABLE)进入限期等待(TIMED_WAITING),让出CPU执行权,不释放对象锁(持锁等待),即线程在获得锁后调用该方法不会释放已获得的锁,其他线程不能访问共享数据。

该方法常用来暂停当前线程的执行,让其他线程执行:

public class ThreadSleep {

    public static void main(String[] args) {
        Thread t = new Thread(()->{
            while (true) {
                try {
                    //do something...
                    Thread.sleep(2000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });
		
		t.start();
    }
}

当前线程可能是一个服务程序,为了防止单处理器下,CPU占用率100%,可以设置当前线程休眠,让其他线程工作。

注意:sleep是静态方法,最好不要用Thread的实例对象调用它,因为它睡眠的始终是当前正在运行的线程,而不是调用它的线程对象,它只对正在运行状态的线程对象有效。

public class ThreadSleep {

    public static void main(String[] args) throws Exception{

        Thread t = new Thread(()->{
        });

        t.start();

        System.out.println("main...");
        t.sleep(2000);  // main 线程睡眠 2000毫秒 而不是t线程 不建议
        System.out.println("main end...");
    }
}

从控制台输出情况可以看到,两个输出语句大约间隔2s,所以是main线程休眠了,而非 t 线程。

3.2 线程礼让:Thread.yield

Thead类的静态方法:线程礼让,让当前线程释放CPU的执行权,当前线程从正在运行(Running)状态转换到就绪状态(Ready),重新竞争CPU的执行权。不释放当前线程已持有的锁

注意:

  • yield()方法,用于暗示操作系统调度其他线程,仅仅是暗示,没有任何机制保证他会被执行,也就是说yield()方法的实现依赖于操作系统的任务调度器,调用yield()方法并不一定能保证线程礼让一定能够成功。
  • 通常线程优先级比当前线程更高的、处于就绪状态的线程,更有可能获得执行的机会。
public class ThreadYield {

    public static void main(String[] args) {
		// Thread-0
        new Thread(()->{
            int count = 0;
            while (true) {
                count++;
               System.out.println(Thread.currentThread().getName() + "---" + count);
            }

        }).start();
		
		// Thread-1
        new Thread(()->{
            int count = 0;
            while (true) {
                Thread.yield();
                count++;
                System.out.println(Thread.currentThread().getName() + "---" + count);
            }

        }).start();
    }
}

运行截图:

在这里插入图片描述
从运行结果中可以看到,Thread-1 线程礼让后,其执行的次数远远小于Thread-0。

3.3 线程中断:Thread.interrupt

Thread类的普通成员方法:interrupt()方法会设置线程的中断标记为true。注意:interrupt()方法仅会设置该线程的中断状态位为true,并不会真正的让线程中断,设置线程中断标记不影响线程的继续执行

public class ThreadInterrupt {

    public static void main(String[] args) throws Exception{
       Thread t = new Thread(() -> {

            while (true) {
                System.out.println(Thread.currentThread().getName()+ "---Running...");
            }

        });

        t.start();

        t.interrupt();
        System.out.println("调用interrupt()"); //Thread-0 一直在运行,并没有被中断
    }
}

上面的代码,展示了interrupt()方法的调用不会对线程的运行带来影响。那么如何通过interrupt()方法实现线程的中断呢,下面给出了两种方式:

  • 捕获InterruptException异常:线程设置休眠后(wait、join、sleep),调用线程的interrupt()方法,会抛出 InterruptedException,且中断标志被清除,重新设置为false。
  • 使用isInterrupted()判断线程的中断标志。
public class ThreadInterrupt {

    public static void main(String[] args) {

        Thread t1 = new Thread(()->{
            while (!Thread.currentThread().isInterrupted()) {
                // do something
            }

            System.out.println(Thread.currentThread().getName() + "---线程结束...");
        });
        t1.start();
        t1.interrupt();

        Thread t2 = new Thread(()->{
            while (true) {
                try {
                    // do something...
                    Thread.sleep(2000l);
                } catch (InterruptedException e) {
                    System.out.println(Thread.currentThread().getName() + "---线程结束...");
                    break;
                }
            }
        });
        t2.start();
        t2.interrupt();
    }
}
输出:
Thread-0---线程结束...
Thread-1---线程结束...

除了使用中断的方式来终结一个线程,还可以通过:

  • Thread.stop:已过时。调用该方法即刻停止run()方法中剩余的全部工作,因此这种终结线程的方式不会保证线程的资源正常释放,如文件、数据库连接的释放等,此外,stop()方法会释放所有的锁,从而导致原子逻辑受损。因此不建议使用。
  • 通过自定义标记位终结线程:这种方式与通过中断标志位终结线程类似。
public class ThreadInterrupt {

    public static void main(String[] args) throws Exception{
		// 1.调用 thread.stop()
        Thread t = new Thread(() -> {
            while (true) {
                // do something...
                System.out.println(Thread.currentThread().getName() + "---Running...");
            }
        });

        t.start();
        Thread.sleep(10);
        t.stop();

		// 2.自定义标志位
        MyRunnable myRunnable = new MyRunnable();
        Thread t1 = new Thread(myRunnable);
        t1.start();
        Thread.sleep(10);
        myRunnable.cancel();
    }
}

class MyRunnable implements Runnable {

    private volatile boolean on = true;

    @Override
    public void run() {
        while (on) {
            // do something...
        }
    }

    public void cancel() {
        this.on = false;
    }
}

Thread.interrupted() 和 Thread.isInterrupted()

  • Thread.isInterrupted()仅仅检测线程对象中断标志的状态。
  • Thread.interrupted() 也可以检测线程对象中断标志的状态,并且还会将中断标志位清除,即重新设置为false。因此请谨慎使用。
  • Thread.isInterrupted()是普通成员方法,Thread.interrupted() 是静态方法。Thread.interrupted()与Thread.sleep类似,他返回并清除的是当前线程的中断标志,而不是调用这个方法的线程。
public class Interrupted {
    public static void main(String[] args) throws Exception {
        Thread thread = new Thread(() -> {
            while (true) {}
        });
        thread.start();
        thread.interrupt();
       
        //获取中断标志并重置,虽然是thread.interrupted(),但实际上是获取主线程的中断标志,因为在主线程中调用的
        System.out.println("interrupted:" + thread.interrupted());
        
        //获取中断标志并重置,也是获取主线程的中断标志
        System.out.println("interrupted:" + Thread.interrupted());
    }
}
3.4 线程等待与唤醒:Object.wait & notify & notifyAll
3.4.1 Object.wait

Object类的普通方法:线程等待,该方法使当前线程让出CPU的执行权,并释放锁,让其他线程可以进入同步方法或同步代码块,当前线程被放入对象等待队列中,等待被notify或notifyAll唤醒。

该方法有三个重载方法,一个是没有timeout 参数的方法,其他两个是有 timeout 参数的方法 。

  • Object.wait():使当前线程进入无限期等待状态(WAITING),直到被notify或notifyAll唤醒。
  • Object.wait(long):使当前线程进入限期等待状态(TIMED_WAITING),在一定时间内,即使没有notify或notifyAll也能自动被唤醒。
  • Object.wait(long,int):对于超时时间更细力度的控制,单位为纳秒。(实际上并不能控制到超时时间到纳秒级别,而是在原来毫秒级别上+1)

注意:wait、notify、notifyAll 虽然是Object的一部分,但他们只能在同步方法或同步代码块中被调用,否则在运行时会抛出IllegalMonitorStateException异常。也就是说调这些方法前必须持有对象的锁。

3.4.3 Object.notify & notifyAll
  • notify():随机唤醒等待队列中等待同一共享资源的一个线程,并使该线程退出等待队列,也就是notify()方法仅通知一个线程,唤醒的线程再重新竞争锁。

  • notifyAll():使所有正在等待队列中等待同一共享资源的全部线程退出等待队列,唤醒的线程再重新竞争锁。

注意:

  • notify() & notifyAll 并不是唤醒任何处于wait()状态的线程,而是唤醒具有同一锁对象的线程。

  • 调用notify() & notifyAll后,当前线程并不会马上释放该对象锁,要等到执行notify()方法的线程执行完才会释放对象锁。

  • 被唤醒的线程也不会立即执行而是尝试获得锁(他必须先重新获得他wait时释放的锁,因此刚被唤醒的线程处于阻塞状态)。

问题:写两个线程,一个线程打印1-52,另一个线程打印A-Z,打印结果为12A34B…5152Z

class Print {
 
    private int flag = 1;//信号量。当值为1时打印数字,当值为2时打印字母
    private int count = 1;
 
 	// 同步方法的锁对象为this
    public synchronized void printNum() {
        // 不打印数字就进入等待
        if (flag != 1) {
            try {
                wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
         //打印数字
        System.out.print(2 * count - 1);
        System.out.print(2 * count);
        flag = 2;
        notify();
    }
 
    public synchronized void printChar() {
         // 不打印字母就进入等待
        if (flag != 2) {
            try {
                wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
         //打印字母
        System.out.print((char) (count - 1 + 'A'));
        count++;//当一轮循环打印完之后,计数器加1
        flag = 1;
        notify();
    }
}
 
public class Test {
    public static void main(String[] args) {
    
        Print print = new Print();
        
        new Thread(() -> {
            for (int i = 0; i < 26; i++) {
                print.printNum();
            }
        }).start();
        
        new Thread(() -> {
            for (int i = 0; i < 26; i++) {
                print.printChar();
            }
        }).start();
    }
}
3.4.3 线程休眠(sleep)与线程等待(wait)的区别
  • sleep是Thread类的静态方法;wait是Object的普通方法。
  • sleep不会释放对象锁;而wait会释放对象锁。
  • wait的执行必须在同步方法或同步代码块中进行,而sleep则不需要。
  • wait和sleep都可以使当前线程休眠,并且在此期间被中断(调用interrupt())都会抛出InterruptedException。
3.5 线程合并:Thread.join

Thread类的普通方法:线程合并,即当前线程会等待调用join()的线程结束。join()方法底层通过 wait()/notifyAll() 实现。

public class ThreadJoin {

    static int r = 0;

    public static void main(String[] args) throws Exception{

        Thread t1 = new Thread(() -> {
            try {
                Thread.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            r = 10;
        });
        t1.start();
        t1.join(); 
        System.out.println("r=" + r);
    }
}
3.6 LockSupport 类

LockSupport是一个线程休眠工具类,所有的方法都是静态方法,可以让线程在任意位置被等待和唤醒,在等待和唤醒时不需要获取对象锁,因此比wait / notify 更加灵活。

  • void park():使当前线程进入无限期等待(WAITING)状态。
  • void parkNanos(long nanos) :使当前线程进入限期等待(TIMED_WAITING)状态。超过时间后自动被唤醒。
  • void unpark(Thread) :唤醒指定处于等待状态的线程。
4.线程优先级

Java中的优先级有10级,从1-10,数字越大,优先级越高。

  • Thread.MIN_PRIORITY = 1 :最小优先级
  • Thread.MAX_PRIORITY = 10 :最大优先级
  • Thread.NORM_PRIORITY = 5 :默认优先级

在线程启动之前(start),通过setPriority()可以设置线程的优先级。同样的,线程优先级越高只能代表该线程被操作系统调度的概率越大,并不是优先级高的线程一定会在优先级低的线程之前被执行。因此线程的调度依然具有不确定性和随机性。

5.守护线程(Daemon )

Java 中的线程分为两类:守护线程和用户线程。守护线程又被称为协程、后台线程。在JVM启动时会调用main 函数, main 函数所在的线程就是一个用户线程,JVM内的垃圾回收线程就是一个守护线程。

在线程启动之前(start),通过setDaemon(true)将线程设置为守护线程。当守护线程的用户线程结束运行,则用户线程不会等待守护线程运行结束,而是直接终止守护线程。

public class ThreadTest {

    public static void main(String[] args) throws Exception{
    
        Thread t = new Thread(() -> {

            try {
                System.out.println("start ADaemon...");
                TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedException e){
                System.out.println("catch InterruptedException...");
            }finally {
                System.out.println("finally...");
            }
        });

        t.setDaemon(true);
        t.start();

        Thread.sleep(1);

        System.out.println(Thread.currentThread().getName() + "---运行结束!");
    }
}

输出:
start ADaemon...
main---运行结束!

上面的例子说明了当用户线程结束后,守护线程自动终止。并且守护线程的finally 代码块并没有被执行。因此,在构建守护线程时,不能依靠finally块中的内容来确保执行关闭或清理资源的逻辑。这也同时说明了finally代码块并不是一定会执行(守护线程被提前终止)。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值