【Java】JAVA多线程和线程池


1、线程状态

(1) 新建状态

        使用 new 关键字和 Thread 类或其子类建立一个线程对象后,该线程对象就处于新建状态。它保持这个状态直到程序 start() 这个线程。

(2) 就绪状态

        当线程对象调用了start()方法之后,该线程就进入就绪状态。就绪状态的线程处于就绪队列中,要等待JVM里线程调度器的调度。

(3) 运行状态

        如果就绪状态的线程获取 CPU 资源,就可以执行 run(),此时线程便处于运行状态。处于运行状态的线程最为复杂,它可以变为阻塞状态、就绪状态和死亡状态。

(4) 阻塞状态

        如果一个线程执行了sleep(睡眠)、suspend(挂起)等方法,失去所占用资源之后,该线程就从运行状态进入阻塞状态。在睡眠时间已到或获得设备资源后可以重新进入就绪状态。可以分为三种:

  • 等待阻塞,运行状态中的线程执行 wait() 方法,使线程进入到等待阻塞状态。wait()释放锁
  • 同步阻塞,线程在获取 synchronized 同步锁失败(因为同步锁被其他线程占用)。
  • 其他阻塞,通过调用线程的 sleep() 或 join() 发出了 I/O 请求时,线程就会进入到阻塞状态。当sleep() 状态超时,join() 等待线程终止或超时,或者 I/O 处理完毕,线程重新转入就绪状态。sleep()不释放锁

(5) 死亡状态

        一个运行状态的线程完成任务或者其他终止条件发生时,该线程就切换到终止状态。

2、线程优先级

每一个 Java 线程都有一个优先级,这样有助于操作系统确定线程的调度顺序。Java 线程的优先级是一个整数,其取值范围是1(Thread.MIN_PRIORITY)到10(Thread.MAX_PRIORITY)。默认情况下,每一个线程都会分配一个优先级 NORM_PRIORITY(5)。具有较高优先级的线程对程序更重要,并且应该在低优先级的线程之前分配处理器资源。但是,线程优先级不能保证线程执行的顺序,而且非常依赖于平台。

3、同步工具synchronized、wait、notify

他们是应用于同步问题的人工线程调度工具。讲其本质,首先就要明确monitor的概念,Java中的每个对象都有一个监视器,来监测并发代码的重入。在非多线程编码时该监视器不发挥作用,反之如果在synchronized 范围内,监视器发挥作用。

wait/notify必须存在于synchronized块中。并且,这三个关键字针对的是同一个监视器(某对象的监视器)。这意味着wait之后,其他线程可以进入同步块执行。

当某代码并不持有监视器的使用权时去wait或notify,会抛出java.lang.IllegalMonitorStateException。也包括在synchronized块中去调用另一个对象的wait/notify,因为不同对象的监视器不同,同样会抛出此异常。

synchronized单独使用:

  • 代码块:如下,在多线程环境下,synchronized块中的方法获取了lock实例的monitor,如果实例相同,那么只有一个线程能执行该块内容

public class Thread1 implements Runnable { 
    Object lock; 
    public void run() { 
        synchronized(lock){ 
            //TODO 
        }
    }
}
  • 直接用于方法: 相当于上面代码中用lock来锁定的效果,实际获取的是Thread1类的monitor。更进一步,如果修饰的是static方法,则锁定该类所有实例。

public class Thread1 implements Runnable { 
    public synchronized void run() {
        //TODO 
    }
}

多线程的内存模型:main memory(主存)、working memory(线程栈),在处理数据时,线程会把值从主存load到本地栈,完成操作后再save回去(volatile关键词的作用:每次针对该变量的操作都激发一次load and save)。

针对多线程使用的变量如果不是volatile或者final修饰的,很有可能产生不可预知的结果(另一个线程修改了这个值,但是之后在某线程看到的是修改之前的值)。其实道理上讲同一实例的同一属性本身只有一个副本。但是多线程是会缓存值的,本质上,volatile就是不去缓存,直接取值。在线程安全的情况下加volatile会牺牲性能。

4、创建线程

        Java 提供了三种创建线程的方法:实现 Runnable 接口、继承 Thread 类、通过 Callable 和 Future 创建线程。

(1) 实现 Runnable 接口

public class Test {
    public static void main(String[] args) { 
        MyRunnable runnable = new MyRunnable(); 
        Thread thread = new Thread(runnable); 
        thread.start();
    } 
} 
class MyRunnable implements Runnable{ 
    public MyRunnable() {
        //TODO
    } 
    @Override public void run() { 
        //TODO
    } 
}

(2) 继承 Thread 类

public class Test { 
    public static void main(String[] args) { 
        MyThread thread = new MyThread(); 
        thread.start(); 
    } 
} 
class MyThread extends Thread{ 
    public MyThread(){ 
        //TODO
    } 
    @Override public void run() { 
        //TODO
    } 
}

Thread类相关方法:

//当前线程可转让CPU控制权,让别的就绪状态线程运行(切换)
public static Thread.yield()
 
//暂停一段时间
public static Thread.slepp()
 
//在一个项城中调用other.join(),将等待other线程执行完后才继续本线程
public join()
 
//后两个函数皆可以被打断
public interrupte()

关于中断:

它并不像stop方法那样会中断一个正在运行的线程。线程会不时地检测中断标识位,以判断线程是否应该被中断(中断标识值是否为true)。中断只会影响到wait状态、sleep状态和join状态。被打断的线程会抛出InterruptedException。Thread.interrupted()检查当前线程是否发生中断,返回boolean。synchronized在获锁的过程中是不能被中断的。

中断是一个状态!interrupt()方法只是将这个状态置为true而已。所以说正常运行的程序不去检测状态,就不会终止,而wait等阻塞方法会去检查并抛出异常。

interrupt():设置当前中断标志位为true;

interrupted():检查当前线程是否发生中断(即中断标志位是否为true)

设置中断标志位后,只能通过wait()、sleep()、join()判断标志位,若标志位为true,会抛出InterruptedException异常,捕获异常后,手动中断线程或进行其他操作。

不能使用try/catch来捕获异常!需使用自定义异常处理器捕获异常,步骤如下:

1.定义异常处理器。实现 Thread.UncaughtExceptionHandler的uncaughtException方法

//第一步:定义符合线程异常处理器规范的"异常处理器",实现Thread.UncaughtExceptionHandler规范
 
class MyUncaughtExceptionHandler implements Thread.UncaughtExceptionHandler{
    //Thread.UncaughtExceptionHandler.uncaughtException()会在线程因未捕获的异常而临近死亡时被调用
    @Override
    public void uncaughtException(Thread t, Throwable e) {
        System.out.println("caught:"+e);
    }
}

2.定义使用该异常处理器的线程工厂

//第二步:定义线程工厂。线程工厂用来将任务附着给线程,并给该线程绑定一个异常处理器
 
class HanlderThreadFactory implements ThreadFactory{
    @Override
    public Thread newThread(Runnable r) {
        System.out.println(this+"creating new Thread");
        Thread t = new Thread(r);
        System.out.println("created "+t);
        t.setUncaughtExceptionHandler(new MyUncaughtExceptionHandler()); //设定线程工厂的异常处理器
        System.out.println("eh="+t.getUncaughtExceptionHandler());
        return t;
    }
}

3.定义一个任务,让其抛出一个异常

//第三步:我们的任务可能会抛出异常。显示的抛出一个exception
 
class ExceptionThread implements Runnable{
    @Override
    public void run() {
        Thread t = Thread.currentThread();
        System.out.println("run() by "+t);
        System.out.println("eh = "+t.getUncaughtExceptionHandler());
        throw new RuntimeException();
    }
}

4.调用实验

//第四步:使用线程工厂创建线程池,并调用其execute方法
 
public class ThreadExceptionUncaughtExceptionHandler{
    public static void main(String[] args){
        ExecutorService exec = Executors.newCachedThreadPool(new HanlderThreadFactory());
        exec.execute(new ExceptionThread());
    }
}

(3) 通过 Callable 和 Future 创建线程

上述两种创建线程的方法,在执行完任务之后无法获取执行结果。如果需要获取执行结果,就必须通过共享变量或者使用线程通信的方式来达到效果。而Callable和Future可以在任务执行完毕之后得到任务执行结果。通过以下四种方法创建线程:

创建 Callable 接口的实现类,并实现 call() 方法,该 call() 方法将作为线程执行体,并且有返回值。

创建 Callable 实现类的实例,使用 FutureTask 类来包装 Callable 对象,该 FutureTask 对象封装了该 Callable 对象的 call() 方法的返回值。

使用 FutureTask 对象作为 Thread 对象的 target 创建并启动新线程。

调用 FutureTask 对象的 get() 方法来获得子线程执行结束后的返回值。

5、三种创建方式的区别

(1) 继承Thread类创建的线程可以拥有自己独立的类成员变量,但是实现Runnable接口创建线程共享实现接口类的成员变量。两中方式创建线程都要重写run方法,run方法是线程的执行体。(Thread和runnable均可以实现单独资源和共享资源)

Eg.

a、Thread类:

启动两个线程,每个线程拥有单独的成员变量

class MyThread extends Thread{
    //TODO 
} 
new MyThread().start(); 
new MyThread().start();

启动两个线程,两个线程共享成员变量

MyThread m = new MyThread(); 
new Thread(m).start(); 
new Thread(m).start();

b、Runnable接口:

        启动两个线程,共同享有成员变量

class MyThread implements Runnable { 
    //TODO
}
 
MyThread m = new MyThread(); 
new Thread(m).start(); 
new Thread(m).start();

 启动两个线程,每个线程拥有单独的成员变量

MyThread myThread = new MyThread(); 
MyThread myThread2 = new MyThread(); 
new Thread(myThread).start(); 
new Thread(myThread2).start();

(2) 在继承Thread类创建线程中可以通过使用this获得当前线程的对象,但是在实现Runnable接口创建线程的途径中可以使用Thread.currentThread()方式来获得当前线程。

(3) 第三种方式是较为复杂的一种。Callable接口是一个与Runnable接口十分相似的接口。在Runnable中run方法为线程的执行体,但是在Callable接口中call方法是线程的执行体。下面是两个接口实现执行体的不同:

call方法有返回值,但是run方法没有

call方法可以声明抛出异常

所以可以说Callable接口是Runnable接口的增强版本。

(4) FutureTask类实现了Runnable和Future接口。和Callable一样都是泛型。

(5) Future接口是对Callable任务的执行结果进行取消,查询是否完成,获取结果的。下面是这个接口的几个重要方法:

boolean cancel(boolean myInterruptRunning),试图取消Future与Callable关联的任务

V get(), 返回Callable任务中call方法的返回值,调用该方法会导致程序阻塞,必须等到子线程结束才会有返回值。这里V表示泛型

V get(long timeout, TimeUnit unit), 返回Callable中call方法的返回值,该方法让程序最多阻塞timeout毫秒的时间,或者直到unit时间点。如果在指定的时间Callable的任务没有完成就会抛出异常TimeoutEexception

boolean isCancelled(), 如果Callable中的任务被取消,则返回true,否则返回false

boolean isDone(),如果Callable中的任务被完成,则返回true,否则返回false

6、线程池

https://blog.csdn.net/itakyubi/article/details/100060680