【Java中的线程】java.lang.Thread 类分析

进程和线程

联想一下现实生活中的例子–烧开水,烧开水时是不是不需要在旁边守着,交给热水机完成,烧开水这段时间可以去干一点其他的事情,例如将衣服丢到洗衣机中洗衣服。这样开水烧完,衣服洗的也差不多了。这样既能喝到热水,衣服也差不多了。
操作系统中将多个程序(例如上面的热水机进程和洗衣机进程)同时进行、交替执行的称之为进程
那么操作系统为什么需要进程。
通常,操作系统进行IO处理相比计算慢得多
通过Java 进行IO 和 运算测试,IO 的速率大概比运算慢200-400倍
而IO是不需要使用CPU的,这由IO设备完成。而正在运行的程序只能等待IO设备,这样会造成性能瓶颈。所以可以通过切换到其他任务来提高CPU的吞吐率。

以下仅考虑单核情况

那么当遇到IO操作时,操作系统需要切换进程。提醒OS切换的可以是中断,定时器,进程退出等外部事件。
如果有多个任务,应该选择哪一个来运行,这需要调度器来抉择。
以及如果两个进程需要进行合作例如需要读取检索当前目录并对该目录的文件名进行排序,这称之为进程间的通信(Inter Process Communication ,IPC)问题。这里只提及一下不做深入研究。

线程的概念和进程的概念很相似,那么有了进程为什么还要线程。

  • 首先,每个进程都使用的是不同的地址空间,有了多进程之后可以使得多个并行的实体拥有了同一个地址空间。
  • 其次,线程比进程更加轻量,它们比进程更快,更容易创建也更容易撤销。
  • 最后,如果多个线程都是CPU密集型的(需要持续计算),那么不能获得性能上的增强,如果存在这大量的计算和 I/O处理,拥有多线程会加快程序执行的速度。

类比生活中,将一个面包店看作一个进程。那么里面的打蛋器,揉面机,烤炉等,这些看作它的线程。这些“线程”都在一个地址空间下(面包店),并且这些“线程”容易增删(相比于面包店,这些机器可以随时移走)。
揉面机到烤炉是需要等待的,那么这时可以切换另外一个烤炉,这样多线程的优势就出来了。

那么如何知道烤炉什么时候完成,这时候就需要知道烤炉的状态,有了状态就能够管理到每个进程。
OS中使用了状态来管理进程,同样的线程也有状态:

Java 中的多线程

Thread 是Java程序中的可执行线程,JVM允许多线程并发运行。每个线程都有一个优先级,具有较高优先级的线程优先执行,线程的优先级和创建该线程的线程相同。线程可以分为守护线程和普通线程,守护线程需要再运行之前指定,并且创建该线程的也必须是守护线程。
Thread具有生命周期(或者成为状态),可以通过一些方法来控制线程。

线程状态

Java 中的线程相应的也具有状态,和OS中的比较类似。Thread.State 表示了Thread的六种状态

public enum State {
NEW,  
RUNNABLE,
BLOCKED,  
WAITING,  
TIMED_WAITING,  
TERMINATED;
}
状态名称 说明
NEW 新建状态,线程还未开始运行
RUNNABLE 可运行状态,操作系统中的就绪和运行两种状态
BLOCKED 阻塞状态,线程阻塞于锁
WAITING 等待状态,无期限等待
TIME_WAITING 超时等待状态,有限制时间的等待,等待一定时间之后可以自行返回
TERMINATED 终止状态,标识线程已经执行完毕

线程优先级

优先级高的线程会被先调度,Thread中定义了priority字段来表示线程的优先级:

private int priority;

// 以及priority的范围

// 最小优先级
public static final int MIN_PRIORITY = 1;  
// 默认的优先级
public static final int NORM_PRIORITY = 5; 
// 最大的优先级
public static final int MAX_PRIORITY = 10;
但需要注意,当前线程优先级是随着父线程优先级改变的,如果父线程优先级为1,子线程也为1。
根据OS的不同,优先级高的线程不一定比优先级底的线程优先调度。
真正的调度顺序是由OS和线程调度算法来决定的

线程的创建

线程有以下几个常用的构造方法:

Thread(Runnable target){...}
Thread(Runnable target, String name){...}
Thread(ThreadGroup group, Runnable target, String name){...}

可以指定要执行的代码,线程名称,线程组
下面是一个Demo

Thread thread = new Thread(() -> {  
    // ...  
});  
thread.setName("thread-1");  
// 设置线程为守护线程  
// thread.setDaemon(true);  
thread.start ();

如果要将线程设置为守护线程,必须在start之前就设置好。

线程和线程组

通过线程组可以对线程进行批量控制,Java 中使用 ThreadGroup来表示线程组。
每个线程组中可以嵌套线程组,是一种包含关系。
如果在执行线程时没有指定线程组,那么默认将夫线程的线程组设置为自己的线程组。

ThreadGroup的结构是树形结构:

public  
class ThreadGroup implements Thread.UncaughtExceptionHandler {
	private final ThreadGroup parent;
	Thread threads[];
	ThreadGroup groups[];
}

parent 为上一级线程组,线程组可以包含线程和线程组。

ThreadGroup 可以中断、摧毁所有活跃的线程

// 通过调用destroy来摧毁所有线程
threadGroup.destroy()

// 通过调用interrupt来中断线程组中的所有线程
threadGroup.interrupt();

线程之间的通信

wait+notify

如果需要对多个线程进行同步操作,那么需要synchronized
那么当线程A获取锁后又释放锁,这些消息如何传达给其他线程。可以通过Object提供的以下方法:
notify 会随机唤醒一个等待的线程
notifyAll会唤醒所有正在等待的线程。
wait 会进入等待状态,直到被唤醒。

如果想让多个线程合作,可以通过wait+notify的形式

public static void main(String[] args) {  
    Object lock = new Object();  
    Runnable task = () -> {  
        for (int i = 0; i < 3; i++) {  
            synchronized (lock) {  
                System.out.println(Thread.currentThread().getName() + " lock");  
                lock.notifyAll();  
                try {  
                    lock.wait();  
                    System.out.println(Thread.currentThread().getName() + " unlock");  k'|IOL
                } catch (InterruptedException e) {  
                    throw new RuntimeException(e);  
                }  
            }  
        }  
    };  
    Thread t1 = new Thread(task,"t1");  
    Thread t2 = new Thread(task,"t2");  
    t1.start();  
    t2.start();  
}

运行结果:

t1 lock
t2 lock
t1 unlock
t1 lock
t2 unlock
t2 lock
t1 unlock
t1 lock
t2 unlock
t2 lock
t1 unlock

waitnotifynotifyAll 都是native

public final native void wait(long timeoutMillis) throws InterruptedException;
public final native void notify ();
public final native void notifyAll ();

信号量

信号量,使用一个整形变量来累计唤醒次数,信号量可以保证操作是原子的。
信号量类似于红绿灯,控制者线程什么时候可以工作,什么时候不可以工作。

如下代码所示,定义了信号量为2来表示同一时间只能唤醒两个线程。
但是创建了三个线程,所以有一个线程需要在其他线程睡眠或结束时才能被唤醒

public static void main(String[] args) {  
    ExecutorService pool = Executors.newFixedThreadPool(3);  
    Semaphore semaphore = new Semaphore(2);  
    Runnable r = ()->{  
        String name = Thread.currentThread().getName();  
        try{  
            System.out.println(name +" try to wake");  
            if(semaphore.tryAcquire(2, TimeUnit.SECONDS)){  
                System.out.println(name + " is awakened");  
                // do something  
                TimeUnit.MILLISECONDS.sleep(1000);  
            }  
        }catch (InterruptedException e){  
            e.printStackTrace();  
        }finally {  
            semaphore.release();  
            System.out.println(name+" waiting or terminated");  
        }  
    };  
  
    for (int i = 0; i < 3; i++) {  
        pool.execute(r);  
    }  
  
    pool.shutdown();  
  
}

结果

pool-1-thread-1 try to wake
pool-1-thread-3 try to wake
pool-1-thread-1 is awakened
pool-1-thread-2 try to wake
pool-1-thread-3 is awakened
pool-1-thread-2 is awakened
pool-1-thread-1 waiting or terminated
pool-1-thread-3 waiting or terminated
pool-1-thread-2 waiting or terminated

join

join是Thread类的一个实例方法。作用是让当前线程等待,等目标线程完成自己再运行。
例如当前线程需要等待目标线程的结果,就可以使用join
下面是一个小测试 — 主线程需要使用到子线程的结果,需要等待子线程计算的结果,这个时候就可以用join

private static volatile int data = 0;
public static void main(String[] args) throws InterruptedException {  
  
    Runnable r = ()->{  
      // to calculate  
      data = 1;  
        try {  
            TimeUnit.SECONDS.sleep(2);  
        } catch (InterruptedException e) {  
            e.printStackTrace();  
        }  
        System.out.println(Thread.currentThread().getName()+" is calculated");  
    };  
  
    Thread t1 = new Thread(r, "T1");  
  
    t1.start();  
    t1.join();  
    System.out.println("use "+ data);  
}

下面是Thrad.join的实现,是一个同步的方法,使其能够保证线程安全,改变线程状态的还是使用的wait

public final synchronized void join(long millis)  
throws InterruptedException {  
    long base = System.currentTimeMillis();  
    long now = 0;  
  
    if (millis < 0) {  
        throw new IllegalArgumentException("timeout value is negative");  
    }  
  
    if (millis == 0) {  
        while (isAlive()) {  
            wait(0);  
        }  
    } else {  
        while (isAlive()) {  
            long delay = millis - now;  
            if (delay <= 0) {  
                break;  
            }  
            wait(delay);  
            now = System.currentTimeMillis() - base;  
        }  
    }  
}

sleep

sleepThread类的一个静态方法。作用是让线程睡眠一会。

sleepwait不同的是,前者不会释放锁而后者会。
sleep必须指定时间,而wait不用
sleepwait都让出时间片,但是前者不释放锁,后者会,所以sleep更加容易造成死锁的情况。

线程异常处理

测试一段代码:

public static void main(String[] args) {  
    try {  
        new Thread(()->{  
            try {  
                int num = 1/0;  
            } catch (Exception e) {  
                e.printStackTrace();            
			}
		}).start();  
    } catch (Exception e) {  
        System.out.println("catch by zero exception");  
    }  
}

创建一个任务并在主线程中捕获异常,当运行后,并不能处理该异常。控制台并未打印出输出语句:

java.lang.ArithmeticException: / by zero

对线程A不能捕获线程B的异常做出的解释 //www.zhihu.com/question/67790293
简答来说就是,线程之间是相互独立的。线程A不可能对线程B时时关照,因为线程A也有自己的事情需要做,如果B出问题了,那么A也会收到干扰,这对多线程来说是很糟糕的。

可以通过设置线程异常处理器来处理:

public static void main(String[] args) {  
    Thread thread = new Thread(() -> {  
        int num = 1 / 0;  
    });  
    thread.setUncaughtExceptionHandler((t, e) -> {  
        e.printStackTrace();  
        System.out.println("catch by zero exception");  
    });  
    thread.start();  
}

这样就能够正常的处理异常了

java.lang.ArithmeticException: / by zero
...
catch by zero exception

UncaughtExceptionHandlerThread的内置注释接口,所有需要处理未被捕获异常的都需要实现改接口:

@FunctionalInterface
public interface UncaughtExceptionHandler {  
	void uncaughtException(Thread t, Throwable e);  
}

创建线程只需要通过setUncaughtExceptionHandler去设置

public void setUncaughtExceptionHandler(UncaughtExceptionHandler eh) {  
    checkAccess();  
    uncaughtExceptionHandler = eh;  
}
Tags: