【Java并发专题之二】Java线程基础

使用线程更好的提高资源利用率,但也会带来上下文切换的消耗,频繁的内核态和用户态的切换消耗,如果代码设计不好,可能弊大于利。

一、线程
  进程是分配资源的最小单位,线程是程序执行的最小单位;线程是依附于进程的,一个进程可以生成多个线程,这些线程拥有共享的进程资源;

二、线程生命周期(相关API)
1、5个阶段6种状态
  5个阶段:新建(New)、就绪(Runnable)、运行(Running)、阻塞(Blocked)和死亡(Dead)。


  6种状态:

public enum State   
{  
       /** 
        * Thread state for a thread which has not yet started. 
        */  
       NEW,  
         
       /** 
        * Thread state for a runnable thread.  A thread in the runnable 
        * state is executing in the Java virtual machine but it may 
        * be waiting for other resources from the operating system 
        * such as processor. 
        */  
       RUNNABLE,  
         
       /** 
        * Thread state for a thread blocked waiting for a monitor lock. 
        * A thread in the blocked state is waiting for a monitor lock 
        * to enter a synchronized block/method or  
        * reenter a synchronized block/method after calling 
        * {@link Object#wait() Object.wait}. 
        */  
       BLOCKED,  
     
       /** 
        * Thread state for a waiting thread. 
        * A thread is in the waiting state due to calling one of the  
        * following methods: 
        * <ul> 
        *   <li>{@link Object#wait() Object.wait} with no timeout</li> 
        *   <li>{@link #join() Thread.join} with no timeout</li> 
        *   <li>{@link LockSupport#park() LockSupport.park}</li> 
        * </ul> 
        *  
        * <p>A thread in the waiting state is waiting for another thread to 
        * perform a particular action.   
        * 
        * For example, a thread that has called <tt>Object.wait()</tt> 
        * on an object is waiting for another thread to call  
        * <tt>Object.notify()</tt> or <tt>Object.notifyAll()</tt> on  
        * that object. A thread that has called <tt>Thread.join()</tt>  
        * is waiting for a specified thread to terminate. 
        */  
       WAITING,  
         
       /** 
        * Thread state for a waiting thread with a specified waiting time. 
        * A thread is in the timed waiting state due to calling one of  
        * the following methods with a specified positive waiting time: 
        * <ul> 
        *   <li>{@link #sleep Thread.sleep}</li> 
        *   <li>{@link Object#wait(long) Object.wait} with timeout</li> 
        *   <li>{@link #join(long) Thread.join} with timeout</li> 
        *   <li>{@link LockSupport#parkNanos LockSupport.parkNanos}</li>  
        *   <li>{@link LockSupport#parkUntil LockSupport.parkUntil}</li> 
        * </ul> 
        */  
       TIMED_WAITING,  
  
       /** 
        * Thread state for a terminated thread. 
        * The thread has completed execution. 
        */  
       TERMINATED;  
}

1.1、新建--NEW
New并初始化。
四种方式:
(1)继承Thread类

package test;

public class MyThread extends Thread{

    @Override
    public void run() {
        System.out.println("MyThread is running...");
    }
}

package test;

public class Main {
    public static void main(String[] args){
        new MyThread().start();//创建并启动线程 MyThread is running...
    }
}

(2)实现Runnable接口

package test;

public class MyThread implements Runnable{

    @Override
    public void run() {
        System.out.println("MyThread is running...");
    }
}

package test;

public class Main {
    public static void main(String[] args){
        Thread thread=new Thread(new MyThread());
        thread.start();//MyThread is running...
        //或者new Thread(new MyThread()).start();
    }
}

(3)实现Callable接口

和Runnable接口不一样,Callable接口提供了一个call()方法作为线程执行体,call()方法比run()方法功能要强大,可以有返回值,可以声明抛出异常
Java5提供了Future接口来代表Callable接口里call()方法的返回值:
  类FutureTask既实现了Future接口和Runnable接口,因此可以作为Thread类的target。
  在Future接口定义了几个公共方法来控制它关联的Callable任务。

>boolean cancel(boolean mayInterruptIfRunning):试图取消该Future里面关联的Callable任务
>V get():返回Callable里call()方法的返回值,调用这个方法会导致程序阻塞,必须等到子线程结束后才会得到返回值
>V get(long timeout,TimeUnit unit):返回Callable里call()方法的返回值,最多阻塞timeout时间,经过指定时间没有返回抛出TimeoutException
>boolean isDone():若Callable任务完成,返回True
>boolean isCancelled():如果在Callable任务正常完成前被取消,返回True

举例:

package test;

import jdk.nashorn.internal.codegen.CompilerConstants;

import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;

public class Main {
    public static void main(String[] args) {
        //1】创建Callable接口的实现类,并实现call()方法,然后创建该实现类的实例(从java8开始可以直接使用Lambda表达式创建Callable对象)。
        //2】使用FutureTask类来包装Callable对象,该FutureTask对象封装了Callable对象的call()方法的返回值
        FutureTask<Integer> task = new FutureTask<Integer>(new Callable<Integer>(){
            @Override
            public Integer call() throws Exception{
                System.out.println("FutureTask1 call ...");
                return 100;
            }
        });
        //从jdk1.8开始可以使用Lambda表达式创建Callable对象
        FutureTask<Integer> task2 = new FutureTask<Integer>((Callable<Integer>)()->{
            System.out.println("FutureTask2 call ...");
            return 200;
        });

        //3】使用FutureTask对象作为Thread对象的target创建并启动线程(因为FutureTask实现了Runnable接口)
        new Thread(task,"有返回值的线程task").start();//实质上还是以Callable对象来创建并启动线程
        new Thread(task2,"有返回值的线程task").start();//实质上还是以Callable对象来创建并启动线程

        try {
            //4】调用FutureTask对象的get()方法来获得子线程执行结束后的返回值
            System.out.println("子线程的返回值:" + task.get());//子线程的返回值:100
            System.out.println("子线程的返回值:" + task2.get());//子线程的返回值:200
        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (ExecutionException e) {
            e.printStackTrace();
        }

    }
}

 (4)使用线程池

JDK1.5在java.util.cocurrent包引入的Executor框架,框架内部使用了线程池机制,控制线程的启动、执行和关闭,可以简化并发编程的操作。
(4.1)最大优点是把任务的提交和执行解耦;
(4.2)避免this逃逸问题:如果我们在构造器中启动一个线程,因为另一个任务可能会在构造器结束之前开始执行,此时可能会访问到初始化了一半的对象用Executor在构造器中。
(4.3)Executor框架常用组件:
  Callable接口:定义有返回值的任务对象;
  Future接口:定义返回任务对象,使用get()获取返回值;
  Executors类:提供了一系列工厂方法用于创先线程池,返回的线程池都实现了ExecutorService接口;
  ExecutorService接口:线程池服务对象,提供线程执行方法execute(仅限Runnable任务)和提交方法submit(支持Runnable任务和Callable任务),以及取消方法shutdown();
具体点讲,提交一个Callable对象给ExecutorService,将得到一个Future对象,调用Future对象的get方法等待执行结果就好了。

#创建固定数目线程的线程池。   
public static ExecutorService newFixedThreadPool(int nThreads)
#创建一个可缓存的线程池,调用execute将重用以前构造的线程(如果线程可用)。如果现有线程没有可用的,则创建一个新线程并添加到池中。终止并从缓存中移除那些已有60秒钟未被使用的线程。
public static ExecutorService newCachedThreadPool()
#创建一个单线程化的Executor
public static ExecutorService newSingleThreadExecutor()
#创建一个支持定时及周期性的任务执行的线程池,多数情况下可用来替代Timer类
public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize)

区别:

举例:线程池执行Runnable任务和线程池执行Callable任务

package test;

import java.util.concurrent.*;

public class ThreadPool {
    public static void main(String[] args){
        ExecutorService pool = Executors.newFixedThreadPool(5);
        //1.ExecutorService 执行实现Runable接口的线程任务,execute(仅限Runnable任务)
        pool.execute(new Runnable(){
            @Override
            public void run() {
                System.out.println(Thread.currentThread().getName() + "ExecutorService execute Runnable -- 线程");
            }
        });

        //2.ExecutorService 提交 实现Runable接口的线程 任务
        Future submit = pool.submit(new Runnable() {
            @Override
            public void run() {
                System.out.println(Thread.currentThread().getName() + "ExecutorService submit Runnable -- 线程");
            }
        });
        // Future 的get方法 : 等待计算完成,然后检索其结果
        try {
            submit.get();
        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (ExecutionException e) {
            e.printStackTrace();
        }

        //3.ExecutorService 提交 实现Callable接口的线程 任务
        Future<String> submit2 = pool.submit(new Callable<String>() {
            @Override
            public String call() throws Exception {
                return Thread.currentThread().getName()
                        + "ExecutorService submit Callable -- 线程";
            }
        });

        try {
            String result = submit2.get();
            System.out.println(result);
        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (ExecutionException e) {
            e.printStackTrace();
        }
    }


}

(4.4)ThreadPoolExecuror类:构造自定义线程池

public ThreadPoolExecutor (int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit,BlockingQueue<Runnable> workQueue)

corePoolSize:线程池中所保存的线程数,包括空闲线程。
maximumPoolSize:池中允许的最大线程数。
keepAliveTime:当线程数大于核心数时,该参数为所有的任务终止前,多余的空闲线程等待新任务的最长时间。
unit:等待时间的单位。
workQueue:任务执行前保存任务的队列,仅保存由execute方法提交的Runnable任务。

举例:

package test;

import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;

public class ThreadPoolTest {
    public static void main(String[] args){
        //创建等待队列
        BlockingQueue<Runnable> bqueue = new ArrayBlockingQueue<Runnable>(20);
        /**
         *  自定义线程池的参数
         *  核心线程数
         *  最大线程数
         *  超出核心线程的空闲线程最大保持时间
         *  时间单位
         *  阻塞队列 BlockingQueue
         */
        ThreadPoolExecutor pool = new ThreadPoolExecutor(3,5,50,TimeUnit.MILLISECONDS,bqueue);
        //创建七个任务
        Runnable t1 = new MyThread();
        Runnable t2 = new MyThread();
        Runnable t3 = new MyThread();
        Runnable t4 = new MyThread();
        Runnable t5 = new MyThread();
        Runnable t6 = new MyThread();
        Runnable t7 = new MyThread();
        //每个任务会在一个线程上执行
        pool.execute(t1);
        pool.execute(t2);
        pool.execute(t3);
        pool.execute(t4);
        pool.execute(t5);
        pool.execute(t6);
        pool.execute(t7);
        //关闭线程池
        pool.shutdown();
        /*
        pool-1-thread-1正在执行。。。
     pool-1-thread-3正在执行。。。
        pool-1-thread-2正在执行。。。
        pool-1-thread-3正在执行。。。
        pool-1-thread-1正在执行。。。
        pool-1-thread-2正在执行。。。
        pool-1-thread-1正在执行。。。
         */
    }

}

 1.2 就绪--Runnable

当线程对象调用了start()方法之后,该线程处于就绪状态,JVM会为其创建方法调用栈和程序计数器,等待系统为其分配CPU时间片。

调用start()方法与run()方法,对比如下:
(1)调用start()方法来启动线程,系统会把该run()方法当成线程执行体来处理。但如果直接调用线程对象的run()方法,则run()方法立即就会被执行,而且在run()方法返回之前其他线程无法并发执行。也就是说,系统把线程对象当成一个普通对象,而run()方法也是一个普通方法,而不是线程执行体;
(2)调用了线程的run()方法之后,该线程已经不再处于新建状态,不要再次调用线程对象的start()方法。只能对处于新建状态的线程调用start()方法,否则将引发IllegaIThreadStateExccption异常;

1.3 运行--Running
线程获得了CPU时间片才得以真正开始执行run()方法的线程执行体

1.4 阻塞--Blocked
处于运行状态的线程在某些情况下,让出CPU并暂时停止自己的运行,进入阻塞状态。

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

(1)WAITING:等待状态
线程处于 无限制等待状态,等待一个特殊的事件来重新唤醒,如:

通过wait()方法进行等待的线程等待一个notify()或者notifyAll()方法;
通过join()方法进行等待的线程等待目标线程运行结束而唤醒;

(2)TIMED_WAITING:时限等待状态
线程进入了一个 时限等待状态,如:sleep(3000),等待3秒后线程重新进行 就绪(RUNNABLE)状态 继续运行。

1.5 死亡--Dead
线程会以如下3种方式结束,结束后就处于死亡状态:
(1)run()或call()方法执行完成,线程正常结束;
(2)线程抛出一个未捕获的Exception或Error;
(3)直接调用该线程stop()方法来结束该线程—该方法不安全,通常不推荐使用;

终止(TERMINATED)状态,线程执行完毕后,进入终止(TERMINATED)状态。

package test;

public class ThreadTest {
    public static void main(String[] args) throws InterruptedException {
        Runnable interruptTask = new Runnable() {
            int i = 0;
            @Override
            public void run() {
                try {
                    //在正常运行任务时,经常进行本线程的中断标志,如果被设置了终端标志就自行停止线程
                    while (!Thread.currentThread().isInterrupted()){
                        //休眠100ms
                        Thread.sleep(100);
                        i++;
                        System.out.println(Thread.currentThread().getName() + " 状态(" + Thread.currentThread().getState() + ") loop " + i);
                    }
                }catch (InterruptedException e){
                    //在调用阻塞方法时正确处理InterruptedException异常。(例如,catch异常后就结束线程。)
                    System.out.println(Thread.currentThread().getName() + " 状态(" + Thread.currentThread().getState()
                            + ") catch InterruptedException.");
                }
            }
        };
        Thread t1 = new Thread(interruptTask,"t1");
        System.out.println(t1.getName() +" 状态("+t1.getState()+") is new.");

        // 启动“线程t1”
        t1.start();
        System.out.println(t1.getName() +" 状态("+t1.getState()+") is started.");

        // 主线程休眠300ms,然后主线程给t1发“中断”指令。
        Thread.sleep(300);
        t1.interrupt();
        System.out.println(t1.getName() +" 状态("+t1.getState()+") is interrupted.");

        // 主线程休眠300ms,然后查看t1的状态。
        Thread.sleep(300);
        System.out.println(t1.getName() +" 状态("+t1.getState()+") is interrupted now.");
    }
 
}

结果:

t1 状态(NEW) is new.
t1 状态(RUNNABLE) is started.
t1 状态(RUNNABLE) loop 1
t1 状态(RUNNABLE) loop 2
t1 状态(TIMED_WAITING) is interrupted.
t1 状态(RUNNABLE) catch InterruptedException.
t1 状态(TERMINATED) is interrupted now.

2、状态转换方法


线程常用方法一览

public class Thread{
    // 线程的启动
    public void start(); 
    // 线程体
    public void run(); 
    // 已废弃
    public void stop(); 
    // 已废弃
    public void resume(); 
    // 已废弃
    public void suspend(); 
    // 在指定的毫秒数内让当前正在执行的线程休眠
    public static void sleep(long millis); 
    // 同上,增加了纳秒参数
    public static void sleep(long millis, int nanos); 
    // 测试线程是否处于活动状态
    public boolean isAlive(); 
    // 中断线程
    public void interrupt(); 
    // 实例方法,对实例线程中断判断 测试线程是否已经中断
    public boolean isInterrupted(); 
    // 静态方法,对当前线程中断判断 测试当前线程是否已经中断
    public static boolean interrupted(); 
    //当前线程等该加入该线程后面,等待该线程终止
    public void join()    
    //当前线程等待该线程终止的时间最长为 millis 毫秒。 如果在millis时间内,该线程没有执行完,那么当前线程进入就绪状态,重新等待cpu调度   
    public void join(long millis)    
    //等待该线程终止的时间最长为 millis 毫秒 + nanos 纳秒。如果在millis时间内,该线程没有执行完,那么当前线程进入就绪状态,重新等待cpu调度
    public void join(long millis,int nanos)  
    //静态方法 让出CPU 进入就绪状态
    public static native void yield();
}

2.1 start & run

start():线程的启动;

run():线程的执行体;

2.2 sleep & yield
sleep():静态方法,使线程进入休眠一段时间,该方法在指定的时间内无法被唤醒,同时也不会释放对象锁;最好不要用Thread的实例对象调用它,因为它睡眠的始终是当前正在运行的线程,而不是调用它的线程对象,它只对正在运行状态的线程对象有效。
yield():静态方法,让当前正在执行的线程暂停,让出CPU资源给其他的线程。它不会进入到阻塞状态,而是进入到就绪状态。
实际上,当某个线程调用了yield()方法暂停之后,优先级与当前线程相同,或者优先级比当前线程更高的就绪状态的线程更有可能获得执行的机会,但是CPU不一定按照这个优先级来处理。
归根结底,Java的线程是使用操作系统的原生线程,所以无法完全实现线程控制。Thread类提供了setPriority(int newPriority)和getPriority()方法来设置和返回一个指定线程的优先级,其中setPriority方法的参数是一个整数,范围是1~10(由低到高)之间,也可以使用Thread类提供的三个静态常量:

MAX_PRIORITY = 10
MIN_PRIORITY = 1
NORM_PRIORITY= 5

2.3 join

当一个线程必须等待另一个线程执行完毕才能执行时。
join()源码:

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;  
        }  
    }  
} 

join方法实现是通过调用wait方法实现。

举例:

package test;


public class MyThread extends Thread{

    @Override
    public void run() {
        for (int i = 0; i < 1000; i++) {
            System.out.println(this.getName() + "线程第" + i + "次执行!");
        }
    }
}
package test;

public class Test {
    public static void main(String[] args) throws InterruptedException {
        MyThread t=new MyThread();
        t.start();
        t.join(1);//将主线程加入到子线程后面,不过如果子线程在1毫秒时间内没执行完,则主线程便不再等待它执行完,进入就绪状态,等待cpu调度
        for(int i=0;i<20;i++){
            System.out.println(Thread.currentThread().getName() + "线程第" + i + "次执行!");
        }
    }

}

当main线程调用t.join时候,调用该对象的wait,main线程会获得线程对象t的锁,直到该对象唤醒main线程。

2.4 wait & notify/notifyAll
明确两个概念

同步队列(锁池):
假设线程A已经拥有了某个对象(注意:不是类)的锁,而其它的线程想要调用这个对象的某个synchronized方法(或者synchronized块),由于这些线程在进入对象的synchronized方法之前必须先获得该对象的锁的拥有权,但是该对象的锁目前正被线程A拥有,所以这些线程就进入了该对象的同步队列(锁池)中,这些线程状态为Blocked。
等待队列(等待池):
假设一个线程A调用了某个对象的wait()方法,线程A就会释放该对象的锁(因为wait()方法必须出现在synchronized中,这样自然在执行wait()方法之前线程A就已经拥有了该对象的锁),同时 线程A就进入到了该对象的等待队列(等待池)中,此时线程A状态为Waiting。如果另外的一个线程调用了相同对象的notifyAll()方法,那么 处于该对象的等待池中的线程就会全部进入该对象的同步队列(锁池)中,准备争夺锁的拥有权。如果另外的一个线程调用了相同对象的notify()方法,那么 仅仅有一个处于该对象的等待池中的线程(随机)会进入该对象的同步队列(锁池)。

(1)wait & notify/notifyAll这三个都是Object类的方法。

(2)使用 wait ,notify 和 notifyAll 前提是先获得调用对象的锁。
(3)调用 wait 方法后,释放持有的对象锁,线程状态有 Running 变为 Waiting,并将当前线程放置到对象的 等待队列;
(4)notify 方法:将等待队列的一个等待线程从等待队列种移到同步队列中 ,而 notifyAll 方法:将等待队列种所有的线程全部移到同步队列,被移动的线程状态由 Waiting 变为 Blocked。
(5)调用notify 或者 notifyAll 方法后,等待线程依旧不会从 wait 返回,需要调用 noitfy 的线程释放锁之后,等待线程才有机会从 wait 返回;
如果是通过notify来唤起的线程,那 先进入wait的线程会先被唤起来;如果是通过nootifyAll唤起的线程,默认情况是 最后进入的会先被唤起来,即LIFO的策略;

2.5 守护线程

调用线程对象的方法setDaemon(true),则可以将其设置为守护线程。确保该方法必须在启动线程前调用。
JRE判断程序是否执行结束的标准是所有的前台执线程行完毕了,而不管后台线程的状态,因此,在使用后台线程时候一定要注意这个问题

2.6 结束线程
正常执行完run方法,线程就结束了;线程提供的终止方法Thread.stop()、Thread.suspend、Thread.resume、Runtime.runFinalizersOnExit已经被废弃了

(1)suspend & resume (已过时)
suspend-线程进入阻塞状态,但不会释放锁。此方法已不推荐使用,因为同步时不会释放锁,会造成死锁的问题。
resume-使线程重新进入可执行状态。
为什么 Thread.suspend 和 Thread.resume 被废弃了?
Thread.suspend 天生容易引起死锁。如果目标线程挂起时在保护系统关键资源的监视器上持有锁,那么其他线程在目标线程恢复之前都无法访问这个资源。如果要恢复目标线程的线程在调用 resume 之前试图锁定这个监视器,死锁就发生了。这种死锁一般自身表现为“冻结( frozen )”进程。

(2)stop(已过时)
不推荐使用,且以后可能去除,因为它不安全。为什么 Thread.stop 被废弃了?
因为其天生是不安全的。停止一个线程会导致其解锁其上被锁定的所有监视器(监视器以在栈顶产生ThreadDeath异常的方式被解锁)。如果之前被这些监视器保护的任何对象处于不一致状态,其它线程看到的这些对象就会处于不一致状态。这种对象被称为受损的 (damaged)。当线程在受损的对象上进行操作时,会导致任意行为。这种行为可能微妙且难以检测,也可能会比较明显。
不像其他未受检的(unchecked)异常, ThreadDeath 悄无声息的杀死及其他线程。因此,用户得不到程序可能会崩溃的警告。崩溃会在真正破坏发生后的任意时刻显现,甚至在数小时或数天之后。

(3)interrupt()
interrupt()实例方法并不会使得线程中断,而是使得线程的中断标志置为true。
isInterrupted()实例方法返回当前线程的中断标记是否设置为true;
Thread.interrupted(),判断当前线程中断标记是否设置为true;

(3.1)如果线程处于被阻塞状态(线程调用sleep、wait、join方法),那么该线程将立即退出被阻塞状态,并且抛出一个InterrupedException异常,退出线程.
如果线程处于阻塞状态,此时调用interrupt()将标记设置为true,会报错InterruptedException,然后将中断标记重置为false;
如果线程处于正常活动状态,调用interrupt()将标记设置为true,然后再调用sleep、wait、join方法会报错InterruptedException,然后将中断标记重置为false;

public void run() {
    try {
        while (true) {
            // 执行任务...
        }
    } catch (InterruptedException ie) {  
        // 由于产生InterruptedException异常,退出while(true)循环,线程终止!
    }
}

(3.2)如果线程处于正常活动状态,那么会将该线程的中断标志设置为true,被设置中断标志的线程将继续运行,不受影响,开发人员应当经常检查本线程的中断标志,如果被设置了中断标志就配合退出代码自行来退出线程。

@Override
public void run() {
    while (!isInterrupted()) {
        // 执行任务...
    }
}

使用额外标记来中断任务:

private volatile boolean flag= true;
protected void stopTask() {
    flag = false;
}

@Override
public void run() {
    while (flag) {
        // 执行任务...
    }
}

三、线程上下文切换
1、上下文切换
多任务处理是计算机同时运行多个程序。在多任务处理系统中,CPU需要处理所有程序的操作,当用户来回切换它们时,需要记录这些程序执行到哪里。CPU给每个任务都服务一定的时间,然后把当前任务的状态保存下来,再加载下一任务的状态,继续服务下一任务。任务的状态保存及再加载, 这段过程就叫做上下文切换

1.1 相关概念
进程(任务):是指一个程序运行的实例。
线程:就是能并行运行并且与他们的父进程(创建他们的进程)共享同一地址空间(一段内存区域)和其他资源的 轻量级的进程。
上下文:是指某一时间点 CPU 寄存器和程序计数器的内容。
寄存器:是 CPU 内部的数量较少但是速度很快的内存(与之对应的是 CPU 外部相对较慢的 RAM 主内存)。寄存器通过对常用值(通常是运算的中间值)的快速访问来提高计算机程序运行的速度。
程序计数器:是一个专用的寄存器,用于表明指令序列中 CPU 正在执行的位置,存的值为正在执行的指令的位置或者下一个将要被执行的指令的位置,具体依赖于特定的系统。
上下文切换(进程切换或任务切换)是指CPU从一个进程或线程切换到另一个进程或线程。

1.2 切换原因
中断处理:在中断处理中,其他程序”打断”了当前正在运行的程序。当CPU接收到中断请求时,会在正在运行的程序和发起中断请求的程序之间进行一次上下文切换。中断分为硬件中断和软件中断,软件中断包括因为IO阻塞、未抢到资源或者用户代码等原因,线程被挂起。
多任务处理:在多任务处理中,CPU会在不同程序之间来回切换,每个程序都有相应的处理时间片,CPU在两个时间片的间隔中进行上下文切换。
用户态切换:对于一些操作系统,当进行用户态切换时也会进行一次上下文切换,虽然这不是必须的。

1.3 切换内容和种类
种类:

进程切换内容:PCB-process control block,寄存器、程序计数器、变量、堆栈、进程状态、虚拟页内存地址;线程切换内容:TCB-thread control block,寄存器、程序计数器、变量、堆栈、线程状态等;

1.4 切换过程
挂起一个进程,将这个进程在 CPU 中的状态(上下文)存储于内存中的某处;
恢复一个进程,在内存中检索下一个进程的上下文并将其在 CPU 的寄存器中恢复;
跳转到程序计数器所指向的位置(即跳转到进程被中断时的代码行),以恢复该进程。

2、减少线程上下文切换
2.1 统计方式
vmstat 1 :指每秒统计一次, 其中cs(context switch)列就是指上下文切换的数目. 一般情况下, 空闲系统的上下文切换每秒大概在1500以下.
[root@101 ~]$ vmstat 1

[root@101 ~]$ vmstat 1
procs -----------memory---------- ---swap-- -----io---- --system-- -----cpu-----
 r  b   swpd   free   buff  cache   si   so    bi    bo   in   cs us sy id wa st
 0  0      0 167134304 391444 50872248    0    0     0     1    0    0  1  0 99  0  0    
 0  0      0 167134432 391444 50872252    0    0     0     0  292  464  0  0 100  0  0    
 0  0      0 167134560 391444 50872252    0    0     0     0  275  468  0  0 100  0  0    
 0  0      0 167134560 391444 50872252    0    0     0    12  310  476  0  0 100  0  0    
 0  0      0 167134656 391444 50872252    0    0     0    40  313  487  0  0 100  0  0    
 0  0      0 167134896 391444 50872252    0    0     0     0  253  456  0  0 100  0  0    
 0  0      0 167135008 391444 50872252    0    0     0    28  313  546  0  0 100  0  0    
 0  0      0 167135008 391444 50872252    0    0     0     0  496  587  0  0 100  0  0    
 0  0      0 167135136 391444 50872252    0    0     0     4  690  711  0  0 100  0  0    
 0  0      0 167135408 391444 50872252    0    0     0     0  518  578  0  0 100  0  0    
 0  0      0 167135328 391444 50872388    0    0     0     0  295  474  0  0 100  0  0    
 0  0      0 167135440 391444 50872388    0    0     0     0  275  463  0  0 100  0  0
 

2.2 切换损耗

直接消耗:指的是CPU寄存器需要保存和加载, 系统调度器的代码需要执行, TLB实例需要重新加载, CPU 的pipeline需要刷掉;
间接消耗:指的是多核的cache之间得共享数据, 间接消耗对于程序的影响要看线程工作区操作数据的大小;

2.3 减少切换的方法
减少上下文切换次数便可以提高多线程程序的运行效率。

(1)无锁并发:多线程竞争时,会引起上下文切换,所以多线程处理数据时,从业务设计入手避免使用锁,如将数据的ID按照Hash取模分段,不同的线程处理不同段的数据,这在业务中常见的;
(2)CAS算法:Java的Atomic包使用CAS算法来更新数据,而不需要加锁;
(3)最少线程数:避免创建不需要的线程,线程太多,有可能出现线程切换和管理的时间,大于任务执行的时间,那效率就低了;
  高并发,低耗时:建议少线程,只要满足并发即可;
  低并发,高耗时:如果是CPU类型的任务,线程数不宜太多;但是如果是IO类型的任务,线程多一些更好,可以更充分利用CPU。;
  高并发,高耗时:1. 要分析任务类型;2. 增加排队;3. 加大线程数;
(4)使用协程:在单线程里实现多任务的调度,并在单线程里维持多个任务间的切换;

四、Java线程Dump分析
1、什么是Thread Dump?
  Thread Dump是非常有用的诊断Java应用问题的工具。每一个Java虚拟机都有及时生成所有线程在某一点状态的thread-dump的能力,Thread Dump包含当前活动线程的快照、JVM中所有Java线程的堆栈跟踪信息,堆栈信息一般包含完整的类名及所执行的方法,如果可能的话还有源代码的行数。
  一般当服务器挂起,崩溃或者性能底下时,就需要抓取服务器的线程堆栈(Thread Dump)用于后续的分析. 在实际运行中,往往一次 dump的信息,还不足以确认问题。为了反映线程状态的动态变化,需要接连多次做threaddump,每次间隔10-20s,建议至少产生三次 dump信息,如果每次 dump都指向同一个问题,我们才确定问题的典型性。

2、Thread Dump特点
(1)能在各种操作系统下使用
(2)能在各种Java应用服务器下使用
(3)可以在生产环境下使用而不影响系统的性能
(4)可以将问题直接定位到应用程序的代码行上

3、Thread Dump能诊断的问题
(1)查找内存泄露,常见的是程序里load大量的数据到缓存;
(2)发现死锁线程;

4、如何抓取Thread Dump
4.1 Windows:
转向服务器的标准输出窗口并按下Control + Break组合键, 之后需要将线程堆栈复制到文件中;

4.2 UNIX/ Linux:
首先查找到服务器的进程号(process id), 然后获取线程堆栈.
(1) ps –ef | grep java
(2) kill -3 <pid>
对于tomcat应用,dump日志会进入catalina.out;对于jar应用,如果使用nohup启动,dump会进入nohup.out。
注意:一定要谨慎, 一步不慎就可能让服务器进程被杀死。kill -9 命令会杀死进程。

4.3 JVM 自带的工具获取线程堆栈:
jdk自带命令行工具获取PID,再获取ThreadDump:
(1) jps 或 ps –ef|grepjava (获取PID)
(2) jstack [-l ] <pid> | tee -a jstack.log (获取ThreadDump)

5、分析dump
5.1 dump文件分析
5.1.1 头部信息
时间和JVM信息

2019-12-17 09:51:15 
Full thread dump OpenJDK 64-Bit Server VM (24.45-b08 mixed mode):

5.1.2 线程块信息

举例:

1."Timer-2" daemon prio=10 tid=0x00007f8b5cbcc800 nid=0x1bf5 in Object.wait() [0x00007f8c5f4f3000]
# 线程名称:Timer-2;线程类型:daemon;优先级: 10,默认是5;
# JVM线程id:tid=0x00007f8b5cbcc800,JVM内部线程的唯一标识(通过java.lang.Thread.getId()获取,通常用自增方式实现)。
# 对应系统线程id(NativeThread ID):nid=0x1bf5,和top命令查看的线程pid对应,不过一个是10进制,一个是16进制。(通过命令:top -H -p pid,可以查看该进程的所有线程信息)
# 线程状态:in Object.wait();
# 起始栈地址:[0x00007f8c5f4f3000],对象的内存地址,通过JVM内存查看工具(jmap),能够看出线程是在哪儿个对象上等待;
2.   java.lang.Thread.State: TIMED_WAITING (on object monitor) #线程在代码级的状态
3.    at java.lang.Object.wait(Native Method)  
4.    - waiting on <0x000000009d7d6e68> (a java.util.TaskQueue) #wait对象0x000000009d7d6e68
5.    at java.util.TimerThread.mainLoop(Timer.java:552)  
6.    - locked <0x000000009d7d6e68> (a java.util.TaskQueue)   #上锁 锁住对象0x000000009d7d6e68
7.    at java.util.TimerThread.run(Timer.java:505)
8.
9.   Locked ownable synchronizers:  #表明使用了一个可持有的同步器多半是线程独有并且使用了AbstractOwnableSynchronizer(或是其子类)去实现它的同步特性,ReentrantLock与ReentrantReadWriteLock就是JAVA平台提供的两个例子。
10.    - None

如何解读Java thread statck trace?

(1)上面2-10行的信息是线程Timer-2堆栈信息,是定位问题的主要依据;
(2)阅读堆栈顺序是倒序的,先读第10行,然后第9行,依此顺序进行;
(3)查看代码6-->4-->2,先上锁,锁住对象0x000000009d7d6e68,然后释放该对象锁,进入waiting状态
查看示例代码:

synchronized(obj) {  
   .........  
   obj.wait();  
   .........  
}

线程的执行过程,先用synchronized获得了这个对象的Monitor(对应于 locked <0x000000009d7d6e68> )。当执行到 obj.wait(),线程即放弃了 Monitor的所有权,进入 “wait set”队列(对应于 waiting on <0x000000009d7d6e68> )

(4)根据(二)线程生命周期的五个阶段6种状态来分析,什么情况会出现这些线程状态
(5)JVM中重要线程

Attach Listener:Attach Listener 线程是负责接收到外部的命令,而对该命令进行执行的并把结果返回给发送者。通常我们会用一些命令去要求JVM给我们一些反馈信息,如:java -version、jmap、jstack等等。 如果该线程在JVM启动的时候没有初始化,那么,则会在用户第一次执行JVM命令时,得到启动。
Signal Dispatcher:前面提到Attach Listener线程的职责是接收外部JVM命令,当命令接收成功后,会交给signal dispather线程去进行分发到各个不同的模块处理命令,并且返回处理结果。signal dispather线程也是在第一次接收外部JVM命令时,进行初始化工作。
CompilerThread0:用来调用JITing,实时编译装卸class 。 通常,JVM会启动多个线程来处理这部分工作,线程名称后面的数字也会累加,例如:CompilerThread1。
Concurrent Mark-Sweep GC Thread:并发标记清除垃圾回收器(就是通常所说的CMS GC)线程, 该线程主要针对于老年代垃圾回收。ps:启用该垃圾回收器,需要在JVM启动参数中加上:-XX:+UseConcMarkSweepGC。
DestroyJavaVM:执行main()的线程,在main执行完后调用JNI中的 jni_DestroyJavaVM() 方法唤起DestroyJavaVM 线程,处于等待状态,等待其它线程(Java线程和Native线程)退出时通知它卸载JVM。每个线程退出时,都会判断自己当前是否是整个JVM中最后一个非deamon线程,如果是,则通知DestroyJavaVM 线程卸载JVM。
Finalizer Thread:这个线程也是在main线程之后创建的,其优先级为10,主要用于在垃圾收集前,调用对象的finalize()方法;关于Finalizer线程的几点:1) 只有当开始一轮垃圾收集时,才会开始调用finalize()方法;因此并不是所有对象的finalize()方法都会被执行;2) 该线程也是daemon线程,因此如果虚拟机中没有其他非daemon线程,不管该线程有没有执行完finalize()方法,JVM也会退出;3) JVM在垃圾收集时会将失去引用的对象包装成Finalizer对象(Reference的实现),并放入ReferenceQueue,由Finalizer线程来处理;最后将该Finalizer对象的引用置为null,由垃圾收集器来回收;4) JVM为什么要单独用一个线程来执行finalize()方法呢?如果JVM的垃圾收集线程自己来做,很有可能由于在finalize()方法中误操作导致GC线程停止或不可控,这对GC线程来说是一种灾难;
Low Memory Detector:这个线程是负责对可使用内存进行检测,如果发现可用内存低,分配新的内存空间。
Reference Handler:JVM在创建main线程后就创建Reference Handler线程,其优先级最高,为10,它主要用于处理引用对象本身(软引用、弱引用、虚引用)的垃圾回收问题 。
VM Thread:这个线程就比较牛b了,是JVM里面的线程母体,根据hotspot源码(vmThread.hpp)里面的注释,它是一个单个的对象(最原始的线程)会产生或触发所有其他的线程,这个单个的VM线程是会被其他线程所使用来做一些VM操作(如:清扫垃圾等)。

(6)统计所有线程状态

[root@101 ~]$ grep java.lang.Thread.State jstack.log | awk '{print $2$3$4$5}' | sort | uniq -c
      8 RUNNABLE
      3 TIMED_WAITING(onobjectmonitor)
      4 TIMED_WAITING(sleeping)
      2 WAITING(onobjectmonitor)
     35 WAITING(parking)

5.2 几个关键状态分析

(1)wait on condition
此时线程状态大致为以下几种:
java.lang.Thread.State: WAITING (parking):一直等那个条件发生;
举例:

package test;

public class WaitingState {
    private static Object object = new Object();

    public static void main(String[] args)
    {
        Runnable task = new Runnable() {

            @Override
            public void run()
            {
                synchronized (object)
                {
                    long begin = System.currentTimeMillis();
                    long end = System.currentTimeMillis();

                    // 让线程运行5分钟,会一直持有object的监视器
                    while ((end - begin) <= 5 * 60 * 1000)
                    {
                        try
                        {
                            // 进入等待的同时,会进入释放监视器
                            object.wait();
                        }
                        catch (InterruptedException e)
                        {
                            e.printStackTrace();
                        }
                    }
                }
            }
        };

        new Thread(task, "t1").start();
        new Thread(task, "t2").start();
    }
}

dump:

2019-12-17 14:49:43
Full thread dump Java HotSpot(TM) 64-Bit Server VM (25.65-b01 mixed mode):

"Attach Listener" #24 daemon prio=9 os_prio=0 tid=0x00007f39c4001000 nid=0x2bc2 waiting on condition [0x0000000000000000]
   java.lang.Thread.State: RUNNABLE

   Locked ownable synchronizers:
    - None

"DestroyJavaVM" #23 prio=5 os_prio=0 tid=0x00007f3aa0008800 nid=0x2b2c waiting on condition [0x0000000000000000]
   java.lang.Thread.State: RUNNABLE

   Locked ownable synchronizers:
    - None

"t2" #22 prio=5 os_prio=0 tid=0x00007f3aa0129000 nid=0x2b5a in Object.wait() [0x00007f3a3f2f1000]
   java.lang.Thread.State: WAITING (on object monitor)
    at java.lang.Object.wait(Native Method)
    - waiting on <0x000000058015c6c0> (a java.lang.Object)
    at java.lang.Object.wait(Object.java:502)
    at test.WaitingState$1.run(WaitingState.java:24)
    - locked <0x000000058015c6c0> (a java.lang.Object)
    at java.lang.Thread.run(Thread.java:745)

   Locked ownable synchronizers:
    - None

"t1" #21 prio=5 os_prio=0 tid=0x00007f3aa0127800 nid=0x2b59 in Object.wait() [0x00007f3a3f3f2000]
   java.lang.Thread.State: WAITING (on object monitor)
    at java.lang.Object.wait(Native Method)
    - waiting on <0x000000058015c6c0> (a java.lang.Object)
    at java.lang.Object.wait(Object.java:502)
    at test.WaitingState$1.run(WaitingState.java:24)
    - locked <0x000000058015c6c0> (a java.lang.Object)
    at java.lang.Thread.run(Thread.java:745)

   Locked ownable synchronizers:
    - None

java.lang.Thread.State: TIMED_WAITING (parking或sleeping):定时的,那个条件不到来,也将定时唤醒自己。

举例:

package test;

import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

public class TimedWaitingState {
    // java的显示锁,类似java对象内置的监视器
    private static Lock lock = new ReentrantLock();

    // 锁关联的条件队列(类似于object.wait)
    private static Condition condition = lock.newCondition();

    public static void main(String[] args)
    {
        Runnable task = new Runnable() {

            @Override
            public void run()
            {
                // 加锁,进入临界区
                lock.lock();

                try
                {
                    condition.await(5, TimeUnit.MINUTES);
                }
                catch (InterruptedException e)
                {
                    e.printStackTrace();
                }

                // 解锁,退出临界区
                lock.unlock();
            }
        };

        new Thread(task, "t1").start();
        new Thread(task, "t2").start();
    }
}

dump :

2019-12-17 14:54:33
Full thread dump Java HotSpot(TM) 64-Bit Server VM (25.65-b01 mixed mode):

"Attach Listener" #24 daemon prio=9 os_prio=0 tid=0x00007f0c00001000 nid=0x2e99 waiting on condition [0x0000000000000000]
   java.lang.Thread.State: RUNNABLE

   Locked ownable synchronizers:
    - None

"DestroyJavaVM" #23 prio=5 os_prio=0 tid=0x00007f0ce4008800 nid=0x2e04 waiting on condition [0x0000000000000000]
   java.lang.Thread.State: RUNNABLE

   Locked ownable synchronizers:
    - None

"t2" #22 prio=5 os_prio=0 tid=0x00007f0ce4119000 nid=0x2e32 waiting on condition [0x00007f0c8bdfc000]
   java.lang.Thread.State: TIMED_WAITING (parking)
    at sun.misc.Unsafe.park(Native Method)
    - parking to wait for  <0x000000058015e348> (a java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject)
    at java.util.concurrent.locks.LockSupport.parkNanos(LockSupport.java:215)
    at java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject.await(AbstractQueuedSynchronizer.java:2163)
    at test.TimedWaitingState$1.run(TimedWaitingState.java:27)
    at java.lang.Thread.run(Thread.java:745)

   Locked ownable synchronizers:
    - None

"t1" #21 prio=5 os_prio=0 tid=0x00007f0ce4117800 nid=0x2e31 waiting on condition [0x00007f0c8befd000]
   java.lang.Thread.State: TIMED_WAITING (parking)
    at sun.misc.Unsafe.park(Native Method)
    - parking to wait for  <0x000000058015e348> (a java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject)
    at java.util.concurrent.locks.LockSupport.parkNanos(LockSupport.java:215)
    at java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject.await(AbstractQueuedSynchronizer.java:2163)
    at test.TimedWaitingState$1.run(TimedWaitingState.java:27)
    at java.lang.Thread.run(Thread.java:745)

   Locked ownable synchronizers:
    - None

(2)waiting for monitor entry

Java多线程借助Monitor实现线程之间的同步。Monitor是Java中用以实现线程之间的互斥与协作的主要手段,它可以看成是对象或者Class的锁。每一个对象都有,也仅有一个 Monitor。
每个Monitor在某个时刻,只能被一个线程拥有,该线程就是 “ActiveThread”,而其它线程都是 “Waiting Thread”,分别在两个队列“Entry Set”和“Wait Set”里等候。在“Entry Set”中等待的线程状态是“waiting for monitor entry”,而在“Wait Set”中等待的线程状态是“in Object.wait()”。

synchronized(obj) {
   .........
}

synchronized保护起来的代码段为临界区。当一个线程申请进入临界区时,它就进入了“Entry Set”队列。

第一种情况:
该 monitor不被其它线程拥有,Entry Set里面也没有其它等待线程。本线程即成为相应类或者对象的 Monitor的 Owner,执行临界区的代码。这时在Entry Set里线程将处于 “Runnable”的状态。

第二种情况:
该 monitor被其它线程拥有,本线程在 Entry Set队列中等待,线程DUMP会显示处于 “waiting for monitor entry”。

"Thread-0" prio=10 tid=0x08222eb0 nid=0x9 waiting for monitor entry [0xf927b000..0xf927bdb8] 
at testthread.WaitThread.run(WaitThread.java:39) 
- waiting to lock <0xef63bf08> (a java.lang.Object) 
- locked <0xef63beb8> (a java.util.ArrayList) 
at java.lang.Thread.run(Thread.java:595) 

临界区的设置,是为了保证其内部的代码执行的原子性和完整性。但是因为临界区在任何时间只允许线程串行通过,这和我们多线程的程序的初衷是相反的。如果在多线程的程序中,大量使用 synchronized,或者不适当的使用了它,会造成大量线程在临界区的入口等待,造成系统的性能大幅下降。如果在线程 DUMP中发现了这个情况,应该审查源码,改进程序

举例:BLOCKED

package test;

public class BlockedState {
    private static Object object = new Object();

    public static void main(String[] args)
    {
        Runnable task = new Runnable() {

            @Override
            public void run()
            {
                synchronized (object)
                {
                    long begin = System.currentTimeMillis();

                    long end = System.currentTimeMillis();

                    // 让线程运行5分钟,会一直持有object的监视器
                    while ((end - begin) <= 5 * 60 * 1000)
                    {

                    }
                }
            }
        };

        new Thread(task, "t1").start();
        new Thread(task, "t2").start();
    }
}

dump:

2019-12-17 14:31:19
Full thread dump Java HotSpot(TM) 64-Bit Server VM (25.65-b01 mixed mode):

"Attach Listener" #24 daemon prio=9 os_prio=0 tid=0x00007f1f2c001000 nid=0x210f waiting on condition [0x0000000000000000]
   java.lang.Thread.State: RUNNABLE

   Locked ownable synchronizers:
    - None

"DestroyJavaVM" #23 prio=5 os_prio=0 tid=0x00007f2010008800 nid=0x2046 waiting on condition [0x0000000000000000]
   java.lang.Thread.State: RUNNABLE

   Locked ownable synchronizers:
    - None

"t2" #22 prio=5 os_prio=0 tid=0x00007f2010121000 nid=0x2074 waiting for monitor entry [0x00007f1fac8c7000]
   java.lang.Thread.State: BLOCKED (on object monitor)
    at test.BlockedState$1.run(BlockedState.java:15)
    - waiting to lock <0x000000058015c6c0> (a java.lang.Object)
    at java.lang.Thread.run(Thread.java:745)

   Locked ownable synchronizers:
    - None

"t1" #21 prio=5 os_prio=0 tid=0x00007f201011f800 nid=0x2073 runnable [0x00007f1fac9c8000]
   java.lang.Thread.State: RUNNABLE
    at test.BlockedState$1.run(BlockedState.java:20)
    - locked <0x000000058015c6c0> (a java.lang.Object)
    at java.lang.Thread.run(Thread.java:745)

   Locked ownable synchronizers:
    - None

(3)in Object.wait()

当线程获得了 Monitor,进入了临界区之后,如果发现线程继续运行的条件没有满足,它则调用对象(一般就是被 synchronized 的对象)的 wait() 方法,放弃 Monitor,进入 “Wait Set”队列。只有当别的线程在该对象上调用了 notify() 或者 notifyAll(),“Wait Set”队列中线程才得到机会去竞争,但是只有一个线程获得对象的Monitor,恢复到运行态。在 “Wait Set”中的线程, DUMP中表现为: in Object.wait()。

"Thread-1" prio=10 tid=0x08223250 nid=0xa in Object.wait() [0xef47a000..0xef47aa38] 
 at java.lang.Object.wait(Native Method) 
 - waiting on <0xef63beb8> (a java.util.ArrayList) 
 at java.lang.Object.wait(Object.java:474) 
 at testthread.MyWaitThread.run(MyWaitThread.java:40) 
 - locked <0xef63beb8> (a java.util.ArrayList) 
 at java.lang.Thread.run(Thread.java:595)

结合(2)和(3),一般CPU很忙时,则关注runnable的线程,CPU很闲时,则关注waiting for monitor entry的线程.

(4)JDK 5.0的Lock
JDK5引入了Lock机制,可替代synchronized和Monitor的机制。但是,Lock类只是一个普通类,JVM无从得知 Lock对象的占用情况,所以在线程DUMP中,也不会包含关于Lock的信息, 关于死锁等问题,就不如用 synchronized的编程方式容易识别。

5.3 典型案例分析
(1)CPU飙高,load高,响应很慢
一个请求过程中多次dump;
对比多次dump文件的runnable线程,如果执行的方法有比较大变化,说明比较正常。如果在执行同一个方法,就有一些问题了;

(2)查找占用CPU最多的线程
使用命令:top -H -p pid(pid为被测系统的进程号),找到导致CPU高的线程ID,对应thread dump信息中线程的nid,只不过一个是十进制,一个是十六进制;
在thread dump中,根据top命令查找的线程id,查找对应的线程堆栈信息;

(3)CPU使用率不高但是响应很慢
进行dump,查看是否有很多thread struck在了i/o、数据库等地方,定位瓶颈原因;

(4)请求无法响应
多次dump,对比是否所有的runnable线程都一直在执行相同的方法,如果是的,恭喜你,锁住了!

死锁经常表现为程序的停顿,或者不再响应用户的请求。从操作系统上观察,对应进程的CPU占用率为零,很快会从top或prstat的输出中消失。
比如在下面这个示例中,是个较为典型的死锁情况:

package test;

public class DeadLock implements Runnable {

    private int value;
    private static Object o1 = new Object(), o2 = new Object();

    public DeadLock(int value) {
        this.value = value;
    }

    @Override
    public void run() {
        if (value == 0) {
            synchronized (o1) {
                try {
                    Thread.sleep(3000);
                    for (int i = 11; i < 20; i++) {
                        System.out.println("o1" + i);
                    }
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                synchronized (o2) {
                    System.out.println("o2" + value);
                }
            }

        }
        if (value == 1) {
            synchronized (o2) {
                try {
                    Thread.sleep(3000);
                    for (int i = 1; i < 10; i++) {
                        System.out.println("o2" + i);
                    }

                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                synchronized (o1) {
                    System.out.println("o1" + value);
                }
            }

        }
    }
}
package test;

public class DeathThreadDemo {
    public static void main(String[] args) {
        DeadLock dt0=new DeadLock(0);
        DeadLock dt1=new DeadLock(1);
        new Thread(dt0).start();
        new Thread(dt1).start();
    }
}

dump:

2019-12-17 14:56:50
Full thread dump Java HotSpot(TM) 64-Bit Server VM (25.65-b01 mixed mode):

"Attach Listener" #24 daemon prio=9 os_prio=0 tid=0x00007f2b70001000 nid=0x3020 waiting on condition [0x0000000000000000]
   java.lang.Thread.State: RUNNABLE

   Locked ownable synchronizers:
    - None

"DestroyJavaVM" #23 prio=5 os_prio=0 tid=0x00007f2c44008800 nid=0x2f79 waiting on condition [0x0000000000000000]
   java.lang.Thread.State: RUNNABLE

   Locked ownable synchronizers:
    - None

"Thread-1" #22 prio=5 os_prio=0 tid=0x00007f2c44121000 nid=0x2fa7 waiting for monitor entry [0x00007f2b8a7c6000]
   java.lang.Thread.State: BLOCKED (on object monitor)
    at test.DeadLock.run(DeadLock.java:42)
    - waiting to lock <0x000000058015e2b8> (a java.lang.Object)
    - locked <0x000000058015e2c8> (a java.lang.Object)
    at java.lang.Thread.run(Thread.java:745)

   Locked ownable synchronizers:
    - None

"Thread-0" #21 prio=5 os_prio=0 tid=0x00007f2c4411f800 nid=0x2fa6 waiting for monitor entry [0x00007f2b8a8c7000]
   java.lang.Thread.State: BLOCKED (on object monitor)
    at test.DeadLock.run(DeadLock.java:25)
    - waiting to lock <0x000000058015e2c8> (a java.lang.Object)
    - locked <0x000000058015e2b8> (a java.lang.Object)
    at java.lang.Thread.run(Thread.java:745)

   Locked ownable synchronizers:
    - None

在 JAVA 5中加强了对死锁的检测。线程 Dump中可以直接报告出 Java级别的死锁,如下所示:

Found one Java-level deadlock:
=============================
"Thread-1":
  waiting to lock monitor 0x00007f2b7c003828 (object 0x000000058015e2b8, a java.lang.Object),
  which is held by "Thread-0"
"Thread-0":
  waiting to lock monitor 0x00007f2b7c0062c8 (object 0x000000058015e2c8, a java.lang.Object),
  which is held by "Thread-1"

Java stack information for the threads listed above:
===================================================
"Thread-1":
    at test.DeadLock.run(DeadLock.java:42)  #标识代码行
    - waiting to lock <0x000000058015e2b8> (a java.lang.Object)
    - locked <0x000000058015e2c8> (a java.lang.Object)
    at java.lang.Thread.run(Thread.java:745)
"Thread-0":
    at test.DeadLock.run(DeadLock.java:25)  #标识代码行
    - waiting to lock <0x000000058015e2c8> (a java.lang.Object)
    - locked <0x000000058015e2b8> (a java.lang.Object)
    at java.lang.Thread.run(Thread.java:745)

Found 1 deadlock.

(5)热锁

由于多个线程对临界区或者锁的竞争,可能出现:

频繁的线程的上下文切换:从操作系统对线程的调度来看,当线程在等待资源而阻塞的时候,操作系统会将之切换出来,放到等待的队列,当线程获得资源之后,调度算法会将这个线程切换进去,放到执行队列中。
大量的系统调用:因为线程的上下文切换,以及热锁的竞争,或者临界区的频繁的进出,都可能导致大量的系统调用。
大部分CPU开销用在“系统态”:线程上下文切换,和系统调用,都会导致 CPU在 “系统态 ”运行,换而言之,虽然系统很忙碌,但是CPU用在 “用户态 ”的比例较小,应用程序得不到充分的 CPU资源。
随着CPU数目的增多,系统的性能反而下降。因为CPU数目多,同时运行的线程就越多,可能就会造成更频繁的线程上下文切换和系统态的CPU开销,从而导致更糟糕的性能。

从整体的性能指标看,由于线程热锁的存在,程序的响应时间会变长,吞吐量会降低。

一个重要的方法是 结合操作系统的各种工具观察系统资源使用状况,以及收集Java线程的DUMP信息,看线程都阻塞在什么方法上,了解原因,才能找到对应的解决方法。

参考:

Java线程的生命周期
Java线程上下文切换
Java线程Dump分析

posted @ 2019-12-16 11:55  cac2020  阅读(401)  评论(0编辑  收藏  举报