Java多线程

多线程

1. 概念

并行和并发

  • 并行:是指多个线程在某个时间段内,同时运行
  • 并发:尤其是指多个线程同时抢占一个资源时,这时这个资源受到了"并发"访问

线程调度机制

  1. 分时调度:为每个程序平均分配CPU时间,轮流执行。
  2. 抢占式调度:系统会根据每个线程"优先级"来决定怎样选择执行的线程,优先级高的线程,会有一定的几率被优先执行。

Java是"抢占式"调度,可以设置线程的"优先级",一共十个级别,从低到高:1–10级,默认5级。

  • 主线程:
    Java程序从main()开始运行,运行后,JVM就是"主进程",main()方法是作为一个独立的线程运行的,所以main()就是一个主线程

线程的状态和基本操作

在这里插入图片描述
阻塞(block):处于运行状态中的线程由于某种原因,暂时放弃对 CPU的使用权,停止执行,此时进入阻塞状态,直到其进入到就绪状态,才 有机会再次被 CPU 调用以进入到运行状态。

阻塞的情况分三种:
(一). 等待阻塞:运行状态中的线程执行 wait()方法,JVM会把该线程放入等待队列(waitting queue)中,使本线程进入到等待阻塞状态;
(二). 同步阻塞:线程在获取 synchronized 同步锁失败(因为锁被其它线程所占用),则JVM会把该线程放入锁池(lock pool)中,线程会进入同步阻塞状态;
(三). 其他阻塞: 通过调用线程的 sleep()或 join()或发出了 I/O 请求时,线程会进入到阻塞状态。当 sleep()状态超时、join()等待线程终止或者超时、或者 I/O 处理完毕时,线程重新转入就绪状态。

sleep() 和 wait() 区别

两者都可以暂停线程的执行

  1. 类的不同:sleep() 是 Thread线程类的静态方法,wait() 是 Object类的方法。
  2. 是否释放锁:sleep() 不释放锁;wait() 释放锁。
  3. 用途不同:Wait 通常被用于线程间交互/通信,sleep 通常被用于暂停执行。
  4. 用法不同:wait() 方法被调用后,线程不会自动苏醒,需要别的线程调用同一个对象上的 notify() 或者 notifyAll() 方法。sleep() 方法执行完成后,线程会自动苏醒。或者可以使用wait(long timeout)超时后线程会自动苏醒。

yield 方法的作用

让出了对 cpu 的占用权利,使当前线程从运行状态变为就绪状态,通常不建议使用yield()方法来控制并发线程的执行。

Thread类的常用方法

1).public static void sleep(long millis):让当前线程暂停millis毫秒
2).public final String getName():获取线程名称,每个线程都有一个默认的名字:Thread-[索引]
3).public final void setName(String name):设置线程名称
4).public static Thread currentThread() : 可以获取当前正在执行的"线程对象"
5).public void interrupt() : 中断线程
	注意:只有在run()方法中处于:Thread的sleep()状态时,才会促使run()抛出异常,在处理异常的位置,终止执行。

Java内存模型

在当前的 Java 内存模型下,线程可以把变量保存本地内存(比如机器的寄存器)中,而不是直接在主存中进行读写。这就可能造成一个线程在主存中修改了一个变量的值,而另外一个线程还继续使用它在寄存器中的变量值的拷贝,造成数据的不一致
要解决这个问题,就需要把变量声明为 volatile,这就指示 JVM,这个变量是不稳定的每次使用它都到主存中进行读取。说白了, volatile 关键字的主要作用就是保证变量的可见性,然后还有一个作用是防止指令重排

2. 创建线程的方式

继承Thread类

继承自Thread,重写run()方法,调用线程对象的start()方法启动线程

//伪代码:
class Thread{
	public void start(){
		run();
	}
	public void run(){
		System.out.println("a");
	}
}
class MyThread extends Thread{
	public void run(){
		System.out.println("b");
	}
}
main(){
	MyThread t = new MyThread();
	t.start();
}		

实现Runnable接口

自定义类实现Runnable接口,重写run()方法,创建一个Thread对象,并将自定义Runnable的对象作为参数传给Thread的构造方法,调用Thread对象的start()方法启动线程

匿名内部类的使用

  1. new Thread(){匿名的Thread子类}.start();
例如:
new Thread(){
	public void run(){
		for(int i = 0;i < 1000 ; i++){
			System.out.println("i = " + i);
		}
	}
}.start();
for(int k = 0; k < 1000 ; k++){
	System.out.println("k = " + k):
}
  1. new Thread(匿名的Runnable子类).start();
例如:
new Thread(new Runnable(){
	public void run(){
		for(int i = 0;i < 1000 ; i++){
			System.out.printl("i = " + i);
		}
	}
}).start();
for(int k = 0; k < 1000 ; k++){
	System.out.println("k = " + k):
}

实现Callable接口

  1. 创建实现Callable接口的类myCallable,重写call方法
  2. 以myCallable为参数创建FutureTask对象
  3. 将FutureTask作为参数创建Thread对象
  4. 调用线程对象的start()方法
public class MyCallable implements Callable<Integer> {

    @Override
    public Integer call() {
        System.out.println(Thread.currentThread().getName() + " call()方法执行中...");
        return 1;
    }

}
public class CallableTest {

    public static void main(String[] args) {
        FutureTask<Integer> futureTask = new FutureTask<Integer>(new MyCallable());
        Thread thread = new Thread(futureTask);
        thread.start();

        try {
            Thread.sleep(1000);
            System.out.println("返回结果 " + futureTask.get());
        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (ExecutionException e) {
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName() + " main()方法执行完成");
    }

}

执行结果:

Thread-0 call()方法执行中...
返回结果 1
main main()方法执行完成

使用Executors工具类创建线程池

Executors提供了一系列工厂方法用于创先线程池,返回的线程池都实现了ExecutorService接口。

主要有newFixedThreadPool,newCachedThreadPool,newSingleThreadExecutor,newScheduledThreadPool

public class MyRunnable implements Runnable {

    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName() + " run()方法执行中...");
    }

}
public class SingleThreadExecutorTest {

    public static void main(String[] args) {
        ExecutorService executorService = Executors.newSingleThreadExecutor();
        MyRunnable runnableTest = new MyRunnable();
        for (int i = 0; i < 5; i++) {
            executorService.execute(runnableTest);
        }

        System.out.println("线程任务开始执行");
        executorService.shutdown();
    }

}

执行结果:

线程任务开始执行
pool-1-thread-1 is running...
pool-1-thread-1 is running...
pool-1-thread-1 is running...
pool-1-thread-1 is running...
pool-1-thread-1 is running...

接口Runnable和Callable的区别

  • 如果想让线程池执行任务的话需要实现的Runnable接口或Callable接口。 Runnable接口或Callable接口实现类都可以被ThreadPoolExecutor或ScheduledThreadPoolExecutor执行。两者的区别在于 Runnable 接口不会返回结果但是 Callable 接口可以返回结果
  • 工具类 Executors 可以实现 Runnable 对象和 Callable 对象之间的相互转换。
    ( Executors.callable(Runnable task) 或 Executors.callable(Runnable task,Object resule) )
class c implements Callable<String>{
	// 注意,这里实现的是call方法,有返回值,这里可以抛出异常
	@Override
	public String call() throws Exception {
		return null;
	}
}
class r implements Runnable{
	// 注意,这里实现的是run方法,无返回值,这里不可以抛出异常
	@Override
	public void run() {
	}
}

相同点:
1、两者都是接口
2、两者都需要调用Thread.start启动线程

不同点:
1、如上面代码所示,callable的核心是call方法,允许返回值,runnable的核心是run方法,没有返回值
2call方法可以抛出异常,但是run方法不行
因为runnable是java1.1就有了,所以他不存在返回值,后期在java1.5进行了优化,就出现了callable,就有了返回值和抛异常。

在重写的run方法中,我们只能够进行异常的捕获而不能够抛出异常,原因是因为在
父类Runnable接口中,run方法没有抛出异常,则实现Runnable的子类就无法抛出异
常(子类在重写父类方法时只能够抛出与父类相同的异常或者父类异常的子类)
3、callable和runnable都可以应用于executors。而thread类只支持runnable

// 使用线程池来运行
public static void main(String[] args) throws Exception{
	//1 创建一个线程池
	//调用Executors类的静态方法
	ExecutorService service = Executors.newFixedThreadPool(10);
	//2提交runnable对象
	service.submit(new Runnable() {
		@Override
		public void run() {
		}
	});
	//3 提交callable对象
	service.submit(new Callable<String>() {
		@Override
		public String call() throws Exception {
			return null;
		}
	});
	//4 关闭线程池
	service.shutdown();
}

3. 多线程的安全性问题

线程安全是指某个方法在多线程环境中被调用时,能够正确地处理多个线程之间的共享变量,使程序功能正确完成。

解决多线程的安全性问题

多线程安全性问题产生的原因

  1. 多个线程并发的访问同一个共享资源时;
  2. 处理共享资源的代码不具有"原子性";

解决方式

1)自动锁(同步锁)synchronized

  • 同步方法【常用】
    若对实例方法加锁,会对当前对象加锁;若对静态方法加锁,就是给当前类加锁,会作用于类的所有对象实例,因为静态成员不属于任何一个实例对象,是类成员( static 表明这是该类的一个静态资源,不管new了多少个对象,只有一份)。所以如果一个线程A调用一个实例对象的非静态 synchronized 方法,而线程B需要调用这个实例对象所属类的静态 synchronized 方法,是允许的,不会发生互斥现象,因为访问静态 synchronized 方法占用的锁是当前类的锁,而访问非静态 synchronized 方法占用的锁是当前实例对象锁
class Tickets{
	private static int tickets = 100;
	//共享资源
	public synchronized static int getTicket(){
		//被同步的方法,一个线程进入后,会锁住此方法,其它线程在外面排队
		//此线程执行完毕,会释放锁,下一个线程才会进来
	}
}
  • 同步代码块【推荐】,对给定对象加锁,不会锁住整个对象(当然你也可以让它锁住整个对象)
public void show(){
	synchronzied(this){
		
	}
	...
	synchronized(this){
	}
	...
	synchronized(this){
	}
	...
}

2)手动锁Lock

1).JDK5之后出现的一个新的锁,它提供了比synchronized更灵活的锁。
2).使用方式:
	Lock lock = new ReentrantLock();
	lock.lock();
	try {
	    System.out.println("获得锁");
	} catch (Exception e) {
	    // TODO: handle exception
	} finally {
	    System.out.println("释放锁");
	    lock.unlock();
	}

3)使用Threadlocal

private static ThreadLocal<Integer> n = new ThreadLocal(){
 @Override
    protected Integer initialValue() {
        return 0;
    }
};

多线程间共享数据

一般来说,共享变量要求变量本身是线程安全的,然后在线程内使用的时候,如果有对共享变量的复合操作,那么也得保证复合操作的线程安全性。

4. 并发关键字

synchronized

1. synchronized 底层实现原理

synchronized是Java中的一个关键字,在使用的过程中并没有看到显示的加锁和解锁过程。因此有必要通过javap命令,查看相应的字节码文件。

synchronized 同步语句块的情况

public class SynchronizedDemo {
    public void method() {
        synchronized (this) {
            System.out.println("synchronized 代码块");
        }
    }
}

通过JDK 反汇编指令 javap -c -v SynchronizedDemo
在这里插入图片描述
可以看出在执行同步代码块之前之后都有一个monitor字样,其中前面的是monitorenter,后面的是离开monitorexit,不难想象一个线程也执行同步代码块,首先要获取锁,而获取锁的过程就是monitorenter ,在执行完代码块之后,要释放锁,释放锁就是执行monitorexit指令。

为什么会有两个monitorexit呢?

这个主要是防止在同步代码块中线程因异常退出,而锁没有得到释放,这必然会造成死锁(等待的线程永远获取不到锁)。因此最后一个monitorexit是保证在异常情况下,锁也可以得到释放,避免死锁。
仅有ACC_SYNCHRONIZED这么一个标志,该标记表明线程进入该方法时,需要monitorenter,退出该方法时需要monitorexit。

synchronized可重入的原理

重入锁是指一个线程获取到该锁之后,该线程可以继续获得该锁。底层原理维护一个计数器,当线程获取该锁时,计数器加一,再次获得该锁时继续加一,释放锁时,计数器减一,当计数器值为0时,表明该锁未被任何线程所持有,其它线程可以竞争获取锁。

2. synchronized 的使用

  • 普通同步方法,锁是当前实例对象
  • 静态同步方法,锁是当前类的class对象
  • 同步方法块,锁是括号里面的对象

volatile

volatile 关键字的作用

对于可见性,Java 提供了 volatile 关键字来保证可见性和禁止指令重排。 volatile 提供 happens-before 的保证,确保一个线程的修改能对其他线程是可见的。当一个共享变量被 volatile 修饰时,它会保证修改的值会立即被更新到主存,当有其他线程需要读取时,它会去内存中读取新值

从实践角度而言,volatile 的一个重要作用就是和 CAS 结合,保证了原子性,详细的可以参见 java.util.concurrent.atomic 包下的类,比如 AtomicInteger。

volatile 常用于多线程环境下的单次操作(单次读或者单次写)。

synchronized 和volatile的区别

  • volatile关键字是线程同步的轻量级实现,所以volatile性能肯定比synchronized关键字要好。但是volatile关键字只能用于变量而synchronized关键字可以修饰方法以及代码块,实际开发中使用 synchronized 关键字的场景还是更多一些
  • 多线程访问volatile关键字不会发生阻塞,而synchronized关键字可能会发生阻塞
  • volatile关键字能保证数据的可见性,但不能保证数据的原子性。synchronized关键字两者都能保证。
  • volatile关键字主要用于解决变量在多个线程之间的可见性,而 synchronized关键字解决的是多个线程之间访问资源的同步性

5. Threadlocal

概念

ThreadLocal 是一个本地线程副本变量工具类,在每个线程中都创建了一个 ThreadLocalMap 对象,简单说 ThreadLocal 就是一种以空间换时间的做法,每个线程可以访问自己内部 ThreadLocalMap 对象内的 value。通过这种方式,避免资源在多线程间共享

或者说,ThreadLocal 对象里有一个内部类ThreadLocalMap,ThreadLocalMap里有Entry数组和操作这个Entry的一些方法,Entry节点的Key是当前的ThreadLocal弱引用(弱引用可以被gc掉),值是线程要存储的数据,是线程私有的,ThreadLocal维护着ThreadLocalMap对象,通过自己的一些方法调用ThreadLocalMap的一些对应方法,从而间接影响Entry的数据,很像ThreadLocalMap的一个代理对象。而ThreadLocalMap属于线程Thread对象,每个Thread对象都有一个指向ThreadLocalMap的引用。

原理: 线程局部变量是局限于线程内部的变量,属于线程自身所有,不在多个线程间共享。Java提供ThreadLocal类来支持线程局部变量,是一种实现线程安全的方式。但是在web环境下(如 web 服务器)使用线程局部变量的时候要特别小心,在这种情况下,工作线程的生命周期比任何应用变量的生命周期都要长。任何线程局部变量一旦在工作完成后没有释放,Java 应用就存在内存泄露的风险。

经典的使用场景是为每个线程分配一个 JDBC 连接 Connection。这样就可以保证每个线程的都在各自的 Connection 上进行数据库的操作,不会出现 A 线程关了 B线程正在使用的 Connection; 还有 Session 管理 等问题。

ThreadLocal使用例子

public class TestThreadLocal {
    
    //线程本地存储变量
    private static final ThreadLocal<Integer> THREAD_LOCAL_NUM 
        = new ThreadLocal<Integer>() {
        @Override
        protected Integer initialValue() {
            return 0;
        }
    };
 
    public static void main(String[] args) {
        for (int i = 0; i <3; i++) {//启动三个线程
            Thread t = new Thread() {
                @Override
                public void run() {
                    add10ByThreadLocal();
                }
            };
            t.start();
        }
    }
    
    /**
     * 线程本地存储变量加 5
     */
    private static void add10ByThreadLocal() {
        for (int i = 0; i <5; i++) {
            Integer n = THREAD_LOCAL_NUM.get();
            n += 1;
            THREAD_LOCAL_NUM.set(n);
            System.out.println(Thread.currentThread().getName() + " : ThreadLocal num=" + n);
        }
    }   
}

打印结果:启动了 3 个线程,每个线程最后都打印到 “ThreadLocal num=5”,而不是 num 一直在累加直到值等于 15

Thread-0 : ThreadLocal num=1
Thread-1 : ThreadLocal num=1
Thread-0 : ThreadLocal num=2
Thread-0 : ThreadLocal num=3
Thread-1 : ThreadLocal num=2
Thread-2 : ThreadLocal num=1
Thread-0 : ThreadLocal num=4
Thread-2 : ThreadLocal num=2
Thread-1 : ThreadLocal num=3
Thread-1 : ThreadLocal num=4
Thread-2 : ThreadLocal num=3
Thread-0 : ThreadLocal num=5
Thread-2 : ThreadLocal num=4
Thread-2 : ThreadLocal num=5
Thread-1 : ThreadLocal num=5

ThreadLocal内存泄漏分析与解决方案

ThreadLocal造成内存泄漏的原因

ThreadLocalMap 中使用的 key 为 ThreadLocal 的弱引用,而 value 是强引用。所以,如果 ThreadLocal没有被外部强引用的情况下,在垃圾回收的时候,key 会被清理掉,而 value 不会被清理掉。这样一来,ThreadLocalMap 中就会出现key为null的Entry。假如我们不做任何措施的话,value 永远无法被GC 回收,这个时候就可能会产生内存泄露。ThreadLocalMap实现中已经考虑了这种情况,在调用 set()get()remove() 方法的时候,会清理掉 key 为 null 的记录。使用完 ThreadLocal方法后 最好手动调用remove()方法

ThreadLocal内存泄漏解决方案

  • 每次使用完ThreadLocal,都调用它的remove()方法,清除数据。

  • 在使用线程池的情况下,没有及时清理ThreadLocal,不仅是内存泄漏的问题,更严重的是可能导致业务逻辑出现问题。所以,使用ThreadLocal就跟加锁完要解锁一样,用完就清理。

6. 线程池

为什么要用线程池

  • 降低资源消耗。 通过重复利用已创建的线程降低线程创建和销毁造成的消耗
  • 提高响应速度。 当任务到达时,任务可以不需要的等到线程创建就能立即执行
  • 提高线程的可管理性。 线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一的分配,调优和监控

线程池的状态及关闭方法

状态说明
RUNNING这是最正常的状态,接受新的任务,处理等待队列中的任务
SHUTDOWN不接受新的任务提交,但是会继续处理等待队列中的任务
STOP不接受新的任务提交,不再处理等待队列中的任务,中断正在执行任务的线程
TERMINATEDterminated()方法结束后,线程池的状态就会变成这个
TIDYING(tidyihng整理)所有的任务都销毁了,workCount 为 0,线程池的状态在转换为 TIDYING 状态时,会执行钩子方法 terminated()

关闭线程池

  • shutdownNow():立即关闭线程池(暴力),正在执行中的及队列中的任务会被中断,同时该方法会返回被中断的队列中的任务列表
  • shutdown():平滑关闭线程池,正在执行中的及队列中的任务能执行完成,后续进来的任务会被执行拒绝策略
  • isTerminated():判断所有提交的任务是否已执行完毕,当正在执行的任务及对列中的任务全部都执行(清空)完就会返回true

如何创建线程池

  • 《阿里巴巴Java开发手册》中强制线程池不允许使用 Executors 去创建,而是通过 ThreadPoolExecutor 的方式,这样的处理方式让写的同学更加明确线程池的运行规则,规避资源耗尽的风险

  • Executors 返回线程池对象的弊端如下:
    FixedThreadPool 和 SingleThreadExecutor : 允许请求的队列长度为 Integer.MAX_VALUE,可能堆积大量的请求,从而导致OOM。
    CachedThreadPool 和 ScheduledThreadPool : 允许创建的线程数量为 Integer.MAX_VALUE ,可能会创建大量线程,从而导致OOM

  • 方式一: 通过构造方法ThreadPoolExecutor实现
    方式二: 通过Executor 框架的工具类Executors来实现

  • 我们可以创建三种类型的ThreadPoolExecutor:
    FixedThreadPool : 该方法返回一个固定线程数量的线程池。该线程池中的线程数量始终不变。当有一个新的任务提交时,线程池中若有空闲线程,则立即执行。若没有,则新的任务会被暂存在一个任务队列中,待有线程空闲时,便处理在任务队列中的任务。
    SingleThreadExecutor: 方法返回一个只有一个线程的线程池。若多出一个任务被提交到该线程池,任务会被保存在一个任务队列中,待线程空闲,按先入先出的顺序执行队列中的任务。
    CachedThreadPool: 该方法返回一个可根据实际情况调整线程数量的线程池。线程池的线程数量不确定,但若有空闲线程可以复用,则会优先使用可复用的线程。若所有线程均在工作,又有新的任务提交,则会创建新的线程处理任务。所有线程在当前任务执行完毕后,将返回线程池进行复用

其实,使用Executors创建线程池,方法内部还是调用了构造方法ThreadPoolExecutor实现的

  // 核心线程数=最大线程数,无界队列,请求任务可以无限放入,若处理速度跟不上,可导致OOM
  public static ExecutorService newFixedThreadPool(int var0) {
        return new ThreadPoolExecutor(var0, var0, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue());
  }
	
  // 核心线程数=最大线程数=1,无界队列,请求任务可以无限放入,若处理速度跟不上,可导致OOM
  public static ExecutorService newSingleThreadExecutor() {
        return new Executors.FinalizableDelegatedExecutorService(new ThreadPoolExecutor(1, 1, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue()));
  }
 
  // 核心线程数=0,最大线程数为Integer.MAX_VALUE,同步移交队列,每次来请求直接创建新线程来处理任务,也不使用队列缓冲,会自动回收多余线程
  public static ExecutorService newCachedThreadPool() {
        return new ThreadPoolExecutor(0, Integer.MAX_VALUE, 60L, TimeUnit.SECONDS, new SynchronousQueue());
  }
 
 // 支持定时周期性执行,注意一下使用的是延迟队列,弊端同newCachedThreadPool一致
  public static ScheduledExecutorService newScheduledThreadPool(int var0) {
        return new ScheduledThreadPoolExecutor(var0);
  }
  public ScheduledThreadPoolExecutor(int corePoolSize) {
        super(corePoolSize, Integer.MAX_VALUE, 0, NANOSECONDS, new DelayedWorkQueue());
  }

具体如何使用线程池,可以参考下面文章链接

java线程池的正确使用方式,线程池参数分析
Java线程池详解

文章概述:

  • 使用线程池需要注意的事项
  1. 若将线程池定义为成员变量,一定要定义为静态的,否则可能有多个线程池
  2. 线程池执行完任务后不可关闭,否则违背使用线程池的初衷–可重复使用
  3. run方法中一定要有try-catch,否则一旦发生异常直接导致线程终止,每次都要新建线程,等于线程池没有发挥作用,高并发下及其消耗资源

ThreadPoolExecutor详解

构造方法如下

public ThreadPoolExecutor(int corePoolSize,	// 核心线程数,线程池中初始化时常驻的线程数
   int maximumPoolSize,// 最大线程数,允许创建的最大线程数,当任务队列满的时候,新任务到来若当前线程数小于最大线程数,继续创建新线程
   long keepAliveTime,// 线程空闲等待时间,对于非核心线程空闲时间若超过这个时间后就会被销毁,注意若核心线程数等于非核心线程数,那么这个参数就不起作用了,因为没有非核心线程了
   TimeUnit unit,// 线程空闲等待时间的单位
   BlockingQueue<Runnable> workQueue,// 任务队列,若当前运行的线程数量大于等于核心线程数,那么新的任务将会被放到任务队列中,可分为有界、无界、同步三种队列类型
   ThreadFactory threadFactory,// 线程工厂,为线程池提供创建新线程的线程工厂,默认使用Executors.defaultThreadFactory()
   RejectedExecutionHandler handler) {// 拒绝策略,当前线程已经达到最大线程数,新任务到来,那么会执行拒绝策略
	......
}                         

在这里插入图片描述
任务队列

队列名称说明
ArrayBlockingQueue(有界队列)队列长度受限,当队列满了就需要创建多余的线程来执行任务
LinkedBlockingQueue(无界队列)队列长度不受限制,当请求越来越多时(任务处理速度跟不上任务提交速度造成请求堆积)可能导致内存占用过多或OOM
SynchronousQueue(同步移交队列)队列不作为任务的缓冲方式,可以简单理解为队列长度为零

拒绝策略

策略名称说明
ThreadPoolExecutor.AbortPolicy拒绝新任务【默认】,抛出 RejectedExecutionException来拒绝新任务的处理
ThreadPoolExecutor.CallerRunsPolicy让提交任务的线程去执行任务(对比前三种比较友好一丢丢),对于可伸缩的应用程序,建议使用,当最大池被填满时,此策略为我们提供可伸缩队列
ThreadPoolExecutor.DiscardPolicy丢弃策略,不处理新任务,直接丢掉,不进行任何通知
ThreadPoolExecutor.DiscardOldestPolicy丢弃最早策略,丢弃掉在队列中存在时间最久的任务
使用示例
首先创建一个 Runnable 接口的实现类(当然也可以是 Callable 接口)
import java.util.Date;

/**
 * 这是一个简单的Runnable类,需要大约5秒钟来执行其任务。
 */
public class MyRunnable implements Runnable {

    private String command;

    public MyRunnable(String s) {
        this.command = s;
    }

    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName() + " Start. Time = " + new Date());
        processCommand();
        System.out.println(Thread.currentThread().getName() + " End. Time = " + new Date());
    }

    private void processCommand() {
        try {
            Thread.sleep(5000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    @Override
    public String toString() {
        return this.command;
    }
}

编写测试程序,我们这里以阿里巴巴推荐的使用 ThreadPoolExecutor 构造函数自定义参数的方式来创建线程池。

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

public class ThreadPoolExecutorDemo {

    private static final int CORE_POOL_SIZE = 5;
    private static final int MAX_POOL_SIZE = 10;
    private static final int QUEUE_CAPACITY = 100;
    private static final Long KEEP_ALIVE_TIME = 1L;
    public static void main(String[] args) {

        //使用阿里巴巴推荐的创建线程池的方式
        //通过ThreadPoolExecutor构造函数自定义参数创建
        ThreadPoolExecutor executor = new ThreadPoolExecutor(
                CORE_POOL_SIZE,
                MAX_POOL_SIZE,
                KEEP_ALIVE_TIME,
                TimeUnit.SECONDS,
                new ArrayBlockingQueue<>(QUEUE_CAPACITY),
                new ThreadPoolExecutor.CallerRunsPolicy());

        for (int i = 0; i < 10; i++) {
            //创建WorkerThread对象(WorkerThread类实现了Runnable 接口)
            Runnable worker = new MyRunnable("" + i);
            //执行Runnable
            executor.execute(worker);
        }
        //终止线程池
        executor.shutdown();
        while (!executor.isTerminated()) {
        }
        System.out.println("Finished all threads");
    }
}

可以看到我们上面的代码指定了:

  1. corePoolSize: 核心线程数为 5。
  2. maximumPoolSize :最大线程数 10
  3. keepAliveTime : 等待时间为 1L。
  4. unit: 等待时间的单位为 TimeUnit.SECONDS。
  5. workQueue:任务队列为 ArrayBlockingQueue,并且容量为 100;
  6. handler:饱和策略为 CallerRunsPolicy。

Output

pool-1-thread-2 Start. Time = Tue Nov 12 20:59:44 CST 2019
pool-1-thread-5 Start. Time = Tue Nov 12 20:59:44 CST 2019
pool-1-thread-4 Start. Time = Tue Nov 12 20:59:44 CST 2019
pool-1-thread-1 Start. Time = Tue Nov 12 20:59:44 CST 2019
pool-1-thread-3 Start. Time = Tue Nov 12 20:59:44 CST 2019
pool-1-thread-5 End. Time = Tue Nov 12 20:59:49 CST 2019
pool-1-thread-3 End. Time = Tue Nov 12 20:59:49 CST 2019
pool-1-thread-2 End. Time = Tue Nov 12 20:59:49 CST 2019
pool-1-thread-4 End. Time = Tue Nov 12 20:59:49 CST 2019
pool-1-thread-1 End. Time = Tue Nov 12 20:59:49 CST 2019
pool-1-thread-2 Start. Time = Tue Nov 12 20:59:49 CST 2019
pool-1-thread-1 Start. Time = Tue Nov 12 20:59:49 CST 2019
pool-1-thread-4 Start. Time = Tue Nov 12 20:59:49 CST 2019
pool-1-thread-3 Start. Time = Tue Nov 12 20:59:49 CST 2019
pool-1-thread-5 Start. Time = Tue Nov 12 20:59:49 CST 2019
pool-1-thread-2 End. Time = Tue Nov 12 20:59:54 CST 2019
pool-1-thread-3 End. Time = Tue Nov 12 20:59:54 CST 2019
pool-1-thread-4 End. Time = Tue Nov 12 20:59:54 CST 2019
pool-1-thread-5 End. Time = Tue Nov 12 20:59:54 CST 2019
pool-1-thread-1 End. Time = Tue Nov 12 20:59:54 CST 2019

线程池的execute()和submit()的区别

  • execute() 方法用于提交不需要返回值的任务,所以无法判断任务是否被线程池执行成功与否;
  • submit() 方法用于提交需要返回值的任务。线程池会返回一个future类型的对象,通过这个future对象可以判断任务是否执行成功,并且可以通过future的get() 方法来获取返回值get()方法会阻塞当前线程直到任务完成,而使用get(long timeout,TimeUnit unit) 方法则会阻塞当前线程一段时间后立即返回,这时候有可能任务没有执行完
// 演示
public class RunnableTestMain {

    public static void main(String[] args) {
        ExecutorService pool = Executors.newFixedThreadPool(2);
        
        /**
         * execute(Runnable x) 没有返回值。可以执行任务,但无法判断任务是否成功完成。
         */
        pool.execute(new RunnableTest("Task1")); 
        
        /**
         * submit(Runnable x) 返回一个future。可以用这个future来判断任务是否成功完成。请看下面:
         */
        Future future = pool.submit(new RunnableTest("Task2"));
        
        try {
            if(future.get()==null){//如果Future's get返回null,任务完成
                System.out.println("任务完成");
            }
        } catch (InterruptedException e) {
        } catch (ExecutionException e) {
            //否则我们可以看看任务失败的原因是什么
            System.out.println(e.getCause().getMessage());
        }

    }

}

public class RunnableTest implements Runnable {
    
    private String taskName;
    
    public RunnableTest(final String taskName) {
        this.taskName = taskName;
    }

    @Override
    public void run() {
        System.out.println("Inside "+taskName);
        throw new RuntimeException("RuntimeException from inside " + taskName);
    }

}

Future和FutureTask

Future接口用来表示执行异步任务的结果存储器,当一个任务的执行时间过长就可以采用这种方式:把任务提交(submit)给子线程去处理,主线程不用同步等待,当向线程池提交了一个Callable或Runnable任务时就会返回Future,用Future可以获取任务执行的返回结果。Future的主要方法包括:

  • get()方法:返回任务的执行结果,若任务还未执行完,则会一直阻塞直到完成为止,如果执行过程中发生异常,则抛出异常,但是主线程是感知不到并且不受影响的,除非调用get()方法进行获取结果则会抛出ExecutionException异常;
  • get(long timeout, TimeUnit unit):在指定时间内返回任务的执行结果,超时未返回会抛出TimeoutException,这个时候需要显式的取消任务;
  • cancel(boolean mayInterruptIfRunning):取消任务,boolean类型入参表示如果任务正在运行中是否强制中断;
  • isDone():判断任务是否执行完毕,执行完毕不代表任务一定成功执行,比如任务执行失但也执行完毕、任务被中断了也执行完毕都会返回true,它仅仅表示一种状态说后面任务不会再执行了;
  • isCancelled():判断任务是否被取消;

下面来实际演示Future和FutureTask的用法:

   public static void main(String[] args) throws ExecutionException, InterruptedException {
        ExecutorService executorService = Executors.newFixedThreadPool(10);
        Future<Integer> future = executorService.submit(new Task());
        Integer integer = future.get();
        System.out.println(integer);
        executorService.shutdown();
    }
 
    static class Task implements Callable<Integer> {
        @Override
        public Integer call() throws Exception {
            System.out.println("子线程开始计算");
            int sum = 0;
            for (int i = 0; i <= 100; i++) {
                sum += i;
            }
            return sum;
        }
    }
  public static void main(String[] args) throws ExecutionException, InterruptedException {
        ExecutorService executorService = Executors.newFixedThreadPool(10);
        FutureTask<Integer> futureTask = new FutureTask<>(new Task());
        executorService.submit(futureTask);
        Integer integer = futureTask.get();
        System.out.println(integer);
        executorService.shutdown();
    }
 
    static class Task implements Callable<Integer> {
        @Override
        public Integer call() throws Exception {
            System.out.println("子线程开始计算");
            int sum = 0;
            for (int i = 0; i <= 100; i++) {
                sum += i;
            }
            return sum;
        }
    }

线程池的真实使用案例

初始化线程池的使用

  • 使用场景

    • 从Excel中读取项目编号,根据项目编号查询当前系统中的项目信息然后使用Webservice技术初始化到下一阶段的系统中去。
    • 这个过程涉及读取Excel信息、解析Excel信息、查询每一个编号对应当前系统信息、处理这些信息、将处理后的信息写入xml文件、调用webservice接口初始化到下一阶段系统中去、调用webservice接口初始化到统一列表系统中去、记录日志等过程。
    • 这个过程很复杂,涉及到的编号比较多时,每个编号都要重复这些过程,如果采用同步的方式,可能需要的时间就很长,然后才能得到响应,所以这里采用多线程的方式,异步执行多个任务,每个线程负责一个编号的初始化,所有线程收到任务后立即响应,然后再异步执行各自的服务,期间收集初始化过程的一些信息,等所有线程执行结束后将收集的信息汇总,用于查看初始化结果信息,使用多线程的同时还能增强系统资源的使用效率
  • 代码

/**
 * 初始化项目信息至下一阶段申报系统的Controller
 * 
 * @author changyanbo
 *
 */
@Controller
@RequestMapping(value = "${adminPath}/data/init")
public class InitDataController extends BaseController {
	
	
	// 导出时为了增加效率 使用线程池
	public static ThreadPoolExecutor executor;
	// 初始化结果报告,来自所有线程的数据汇总
	public InitReport initReport = new InitReport();
	// 经费需要的数据,来自所有线程的数据汇总
	public static List<WsProvideAwInitUnit> wsProvideAwInitUnitList = Collections.synchronizedList(new ArrayList<>());// 单位list
	public static List<WsProvideAwInitXm> wsProvideAwInitXmList = Collections.synchronizedList(new ArrayList<>());// 项目list
	
	
	/**
	 * 创建初始化数据并调用接口推送到下一阶段申报系统,同时将初始化生成的xml文件保存到指定目录
	 * @param file
	 * @param request
	 * @param response
	 * @param model
	 * @return
	 */
	@RequestMapping(value = "createXmlAndInit")
	@ResponseBody
	public String createXmlAndInitSbs(MultipartFile file, HttpServletRequest request, HttpServletResponse response, Model model) {
		// 每次初始化创建固定大小线程池,大小200
		executor = (ThreadPoolExecutor) Executors.newFixedThreadPool(200);
		initReport = new InitReport();
		List<InitExcelForXm> dataListForXm = new ArrayList<>();
		try {
			// 导入上传Excel文件对象并解析为项目信息、开通信息
			ImportExcel ieForXm = new ImportExcel(file, 0, 0);
			dataListForXm = ieForXm.getDataList(InitExcelForXm.class);
			ImportExcel ieForOpenInfo = new ImportExcel(file, 0, 1);
			List<InitExcelForOpenInfo> dataListForOpen = ieForOpenInfo.getDataList(InitExcelForOpenInfo.class);
			
			// 检查上传初始化Excel文件的完整性
			String checkResult = InitUtils.checkExcelBeforeInit(dataListForXm, dataListForOpen);
			if (checkResult != null) {
				return ResultUtils.buildJsonError(checkResult);
			}
			
			// 用于初始化统一列表
			InitExcelForOpenInfo initExcelForOpenInfo = dataListForOpen.get(0);
			
			// 每次执行将收集数据的对象重置
			initReport.setInitDate(DateUtils.getDate("yyyy-MM-dd HH:mm:ss"));
			initReport.setInitTotalNum(dataListForXm.size());
			wsProvideAwInitUnitList.clear();
			wsProvideAwInitXmList.clear();
			
			// 多线程异步执行初始化
			for (InitExcelForXm initExcelForXm : dataListForXm) {
				executor.execute(new Runnable() {
					@SuppressWarnings("static-access")
					@Override
					public void run() {
						// 项目编号
						String xmbh = initExcelForXm.getBh();
						// 记录日志
						LogInit logInit = new LogInit();
						LogInit logInitQuery = new LogInit();// 用于查询本次初始化的该编号的这条日志
						logInitQuery.setBh(xmbh);
						logInitQuery.setBeginDate(DateUtils.formatDate(logInit.getCreateDate(), "yyyy-MM-dd HH:mm:ss"));
						try {
							// 根据项目编号查询项目信息、人员信息、单位信息、文件信息
							Xm xm = new Xm();
							xm.setBh(xmbh);
							......
							// 设置日志初始状态,记录日志
							logInit.setIsNewRecord(true);
							......
							
							// 工具类使用new方式,避免使用static方法引起多线程并发问题
							InitUtils utils = new InitUtils();
							// 处理项目信息、单位信息、人员信息、文件信息、校验表
							......
							
							// 日志对象记录好各阶段初始化状态,收集到项目下各项统计值,进行保存
							logInitService.save(logInit);
							
							// 封装经费所需的数据
							initUtils.packageUnitDataForAW(wsProvideAwInitUnitList, unitList);
							initUtils.packageXmDataForAW(wsProvideAwInitXmList, xm, initExcelForXm);
							
							// 统计初始化数目,将当前编号项目下的各项数据累加到initReport静态对象中去
							synchronized (initReport) {
								Map<String, Integer> initNumRecordMap = initReport.getInitNumRecordMap();
								initNumRecordMap.put("xmNum", initNumRecordMap.get("xmNum")==null ? 1 : new AtomicInteger(initNumRecordMap.get("xmNum")).addAndGet(1));
								initNumRecordMap.put("unitNum", initNumRecordMap.get("unitNum")==null ? unitList.size() : new AtomicInteger(initNumRecordMap.get("unitNum")).addAndGet(unitList.size()));
								initNumRecordMap.put("personNum", initNumRecordMap.get("personNum")==null ? personList.size() : new AtomicInteger(initNumRecordMap.get("personNum")).addAndGet(personList.size()));
								initNumRecordMap.put("fileNum", initNumRecordMap.get("fileNum")==null ? fileList.size() : new AtomicInteger(initNumRecordMap.get("fileNum")).addAndGet(fileList.size()));
								initNumRecordMap.put("validateNum", initNumRecordMap.get("validateNum")==null ? validateList.size() : new AtomicInteger(initNumRecordMap.get("validateNum")).addAndGet(validateList.size()));
								initReport.setInitNumRecordMap(initNumRecordMap);
							}
							
							// 设置初始化数据对象,将处理好的各项数据封装到initData中去
							InitData2Rws initData = new InitData2Rws();
							......
							// 填充码表信息,将码表信息也放到initData对象中去
							initData.setCodetable(new Codetable());
							utils.handleInitCodetable(initData, xm);
							
							
							// 下载xml到指定路径,用于初始化日志记录
							String filename = xmbh + ".xml";
							logInit.setFilename(filename);
							// 将initData转为xml,保存
							String xmlStatus = utils.saveAsXml(filename, initData);
							// 将保存结果状态记录到日志,更新日志对象
							logInit.setXmlStatus(xmlStatus);
							logInit.setIsNewRecord(false);
							logInit.setId(logInitService.getByEntity(logInitQuery).getId());
							logInitService.save(logInit);
							// 将保存结果状态更新initReport对象中的成功失败信息
							if("1".equals(xmlStatus)){
								// xml成功个数自增
								initReport.xmlSuccIncrement();
							}else{
								initReport.addXmlFailList(xmbh);
							}
							
							// 调用下一阶段申报系统接口进行初始化
							......
							String returnXml = WebServicesUtils.send("init.nextJd",JaxbMapper.toXml(wsInitParams, initData, true));
							WsInitResult wsInitResult = JaxbMapper.fromXml(returnXml, WsInitResult.class);
							
							// 根据下一阶段初始化的返回结果更新initReport和logInit对应项数据
							logger.info("初始化数据到下一阶段接收到的返回值:"+wsInitResult.getIsInitSuccess());
							if("1".equals(wsInitResult.getIsInitSuccess())){
								initReport.initSuccIncrement();
								logInit.setNextJdStatus("1");
							}else{
								initReport.addInitFailList(xmbh);
								logInit.setNextJdStatus("2");
							}
							logInitService.save(logInit);
							
							// 收集接口返回的结果信息(错误信息、码表同步信息)
							synchronized (initReport) {
								if(!wsInitResult.getErrorMsgList().isEmpty()){
									initReport.appendErrorMsgList(wsInitResult.getErrorMsgList());
									logInit.setExceptionMessage(wsInitResult.getErrorMsgList().toString());
								}
								Map<String, String> sychrMbInfoMap = wsInitResult.getRecordMapForInitMb();
								
								// 收集码表同步信息
								Map<String, HashSet<String>> collectSychrMbSetMap = initReport.getCollectSyncMbSetMap();
								initCodetableUtils.collectSyncMbLog(collectSychrMbSetMap, sychrMbInfoMap);
								initReport.setCollectSyncMbSetMap(collectSychrMbSetMap);
								logInit.setCodetableSyncinfo(sychrMbInfoMap.toString());
							}
							
							// 调用经费接口,初始化经费信息 @TODO
							
							// 调用统一列表接口(无论向下一阶段和经费接口是否始化成功都调用,该接口会更新覆盖)
							Map<String, String> returnMsg = initUtils.sendWsInitTylb(xm, initExcelForOpenInfo);
							logger.info("统一列表接口返回:【{}】", returnMsg);
							if(returnMsg != null && returnMsg.get("revMsg").contains("成功")){
								logInit.setTylbStatus("1");
							}else{
								logInit.setTylbStatus("2");
							}
							logInit.setSendTylbInfo(returnMsg.get("sendMsg"));
							logInit.setRevTylcInfo(returnMsg.get("revMsg"));
							// 判断接口返回成功状态,更改日志
							logInitService.save(logInit);
							
						} catch (Exception e) {
							logger.error("项目【{}】初始化失败!", xmbh, e);
							initReport.addXmlFailList(xmbh);
						}finally{
							String isInitSucc = !"1".equals(logInit.getXmlStatus())? "2" : !"1".equals(logInit.getNextJdStatus())? "2" : !"1".equals(logInit.getJfStatus()) ? "2" : !"1".equals(logInit.getTylbStatus()) ? "2" : "1";
							logInit.setInitStatus(isInitSucc);
							
							logInit.setId(logInitService.getByEntity(logInitQuery).getId());
							logInit.setIsNewRecord(false);
							logInitService.save(logInit);
						}// end try
					}
				}); // end execute
			} // end for
			
			// 所有任务书分发完毕后,关闭线程池
			executor.shutdown();
			// 调用经费接口,初始化经费信息(待所有编号向下一阶段初始化成功后调用) TODO
			
		} catch (Exception e) {
			logger.error("初始化失败!", e);
			return ResultUtils.buildJsonError("初始化失败!" + e.getMessage());
		}
		return ResultUtils.buildJsonNormal("初始化进行中,一共需要初始化" + dataListForXm.size() + "个项目。");
	}
	

	/**
	 * 获取初始化进度
	 * @return
	 */
	@RequestMapping(value = "getInitProgress")
	@ResponseBody
	public String getInitProgress() {
		try {
			if(executor == null){
				return ResultUtils.buildJsonError("没有可获取的初始化报告,请点击步骤2进行初始化后,再重新获取!");
			}
			
			String splitLine = "<br>------------------------------------------------------------------------------------------------>>";
			String linePrefix = "<br>→ ";
			
			// xml待生成数
			int xmlLeftNum = initReport.getInitTotalNum() - initReport.getInitXmlSuccessNum() - initReport.getInitXmlFaildList().size();
			// 总初始化剩余数
			int initLeftNum = initReport.getInitTotalNum() - initReport.getInitSuccessNum() - initReport.getInitFaildList().size();
			
			String xmlFinishTip = xmlLeftNum == 0 ? "<br>→ XML文件生成完毕!" : "";
			String initFinishTip = xmlLeftNum == 0 ? "<br>→ 任务书初始化完毕!" : "";

			String xmlFaildTip = initReport.getInitXmlFaildList().size() == 0 ? "" : ",失败编号:<br>" + Arrays.toString(initReport.getInitXmlFaildList().toArray());
			String initFaildTip = initReport.getInitFaildList().size() == 0 ? "" : ",失败编号:<br>" + Arrays.toString(initReport.getInitFaildList().toArray());
			// 1.初始化时间
			StringBuilder initDate = new StringBuilder("【初始化时间】").append(initReport.getInitDate());
						
			// 2.xml生成进度报告
			StringBuilder initXmlProgressReport = new StringBuilder();
			initXmlProgressReport.append(splitLine).append("<br>【XML生成进度报告】")
			.append(linePrefix).append("XML生成路径:").append(Global.getConfig("file.initXMLFilePath")).append(xmlFinishTip)
			.append(linePrefix).append("当前成功数:").append(initReport.getInitXmlSuccessNum())
			.append(linePrefix).append("待生成数:").append(xmlLeftNum)
			.append(linePrefix).append("失败数:").append(initReport.getInitXmlFaildList().size()).append(xmlFaildTip);
			
			// 3.初始化进度报告
			StringBuilder initProgressReport = new StringBuilder();
			initProgressReport.append(splitLine).append("<br>【下一阶段申报系统初始化进度报告】").append(initFinishTip)
			.append(linePrefix).append("当前成功数:").append(initReport.getInitSuccessNum())
			.append(linePrefix).append("待初始化数:").append(initLeftNum)
			.append(linePrefix).append("失败数:").append(initReport.getInitFaildList().size()).append(initFaildTip);
			
			// 4.初始化异常报告
			StringBuilder initErrorReport = new StringBuilder();
			if(!initReport.getErrorMsgList().isEmpty()){
				initErrorReport.append(splitLine).append("<br>【初始化异常报告】")
				.append(linePrefix).append(initReport.getErrorMsgList());
			}
			
			// 5.初始化表数据报告
			StringBuilder initDataReport = new StringBuilder();
			// 6.同步码表初始化报告
			StringBuilder initMbReport = new StringBuilder();
			// 若所有线程已经执行结束,则根据汇总后的initReport输出所有的初始化报告
			if(executor.isTerminated()){
				initDataReport.append(splitLine).append("<br>【初始化后需要进行核对的数量(请等待系统执行完毕后再看数量)】")
				.append(linePrefix).append("项目数:").append(initReport.getInitNumRecordMap().get("xmNum"))
				.append(linePrefix).append("人员数:").append(initReport.getInitNumRecordMap().get("personNum"))
				.append(linePrefix).append("单位数:").append(initReport.getInitNumRecordMap().get("unitNum"))
				.append(linePrefix).append("文件数:").append(initReport.getInitNumRecordMap().get("fileNum"))
				.append(linePrefix).append("校验表记录数:").append(initReport.getInitNumRecordMap().get("validateNum"));
				
				if(!initReport.getCollectSyncMbSetMap().isEmpty()){
					initMbReport.append(splitLine).append("<br>【码表同步报告】")
					.append(linePrefix).append("<span style='color:black'>同步的专项信息:</span>").append(initReport.getCollectSyncMbSetMap().get("zxInitRecord"))
					.append(linePrefix).append("<span style='color:black'>同步的一级指南:</span>").append(initReport.getCollectSyncMbSetMap().get("dir1InitRecord"))
					.append(linePrefix).append("<span style='color:black'>同步的二级指南:</span>").append(initReport.getCollectSyncMbSetMap().get("dir2InitRecord"))
					.append(linePrefix).append("<span style='color:black'>同步的三级指南:</span>").append(initReport.getCollectSyncMbSetMap().get("dir3InitRecord"))
					.append(linePrefix).append("<span style='color:black'>同步的四级指南:</span>").append(initReport.getCollectSyncMbSetMap().get("dir4InitRecord"))
					.append(linePrefix).append("<span style='color:black'>同步的推荐单位:</span>").append(initReport.getCollectSyncMbSetMap().get("recommendUnitInitRecord"));
				}
			}
			
			return ResultUtils.buildJsonNormalWithData("获取进度成功!", initDate.append(initXmlProgressReport)
					.append(initDataReport).append(initProgressReport)
					.append(initMbReport).append(initErrorReport));
					
		} catch (Exception e) {
			e.printStackTrace();
			return ResultUtils.buildJsonError("获取进度失败!" + e.getMessage()); 
		}
	}
	
	
	/**
	 * 查看奥威数据生成进度
	 * @return
	 */
	@RequestMapping("/getDataForAwProgress")
	@ResponseBody
	public String getDataForAwProgress() {
		if(initReport == null || initReport.getInitTotalNum() == 0){
			return ResultUtils.buildJsonError("没有要生成的数据,请点击步骤2进行初始化后,再重新获取!");
		}
		if(!executor.isTerminated()){
			return ResultUtils.buildJsonError("数据正在生成...,请稍后再试!");
		}
		return ResultUtils.buildJsonNormal("奥威所需的初始化数据已生成!");
	}

	
	/**
	 * 下载提供奥威的信息
	 * @param response
	 */
	@RequestMapping("/getDataForAw")
	public void getDataForAw(String type, HttpServletResponse response) {
		try {
			// 若所有线程已经执行结束,则导出数据
			if(executor.isTerminated()){
				// 导出生成的文件
				String unitFileName = "给奥威的初始化信息_" + DateUtils.getDate("yyyyMMddHHmmss") + ".xlsx";
				new ExportExcel(WsProvideAwInitXm.class, WsProvideAwInitUnit.class)
				.setDataList(wsProvideAwInitXmList, 0)
				.setDataList(wsProvideAwInitUnitList, 1)
				.write(response, unitFileName).dispose();
			}
		} catch (IOException e) {
			logger.error("给奥威的信息下载失败!", e);
			e.printStackTrace();
		}
	}
}

8. 线程相关面试题

介绍一下Atomic 原子类

所谓原子类说简单点就是具有原子/原子操作特征的类。并发包 java.util.concurrent(JUC) 的原子类都存放在 java.util.concurrent.atomic 下

  • JUC 包中4类原子类
  • 基本类型
    使用原子的方式更新基本类型
    AtomicInteger:整形原子类
    AtomicLong:长整型原子类
    AtomicBoolean :布尔型原子类
  • 数组类型
    使用原子的方式更新数组里的某个元素
    AtomicIntegerArray:整形数组原子类
    AtomicLongArray:长整形数组原子类
    AtomicReferenceArray :引用类型数组原子类
  • 引用类型
    AtomicReference:引用类型原子类
    AtomicStampedRerence:原子更新引用类型里的字段原子类
    AtomicMarkableReference :原子更新带有标记位的引用类型
  • 对象的属性修改类型
    AtomicIntegerFieldUpdater:原子更新整形字段的更新器AtomicLongFieldUpdater:原子更新长整形字段的更新器
    AtomicStampedReference :原子更新带有版本号的引用类型。该类将整数值与引用关联起来,可用于解决原子的更新数据和数据的版本号,可以解决使用 CAS 进行原子更新时可能出现的 ABA 问题

介绍一下 AtomicInteger 类的原理

  • AtomicInteger 类主要利用 CAS (compare and swap) + volatile 和 native 方法来保证原子操作,从而避免synchronized 的高开销,执行效率大为提升
  • CAS的原理是拿期望的值和原本的一个值作比较,如果相同则更新成新的值;另外 value 是一个volatile变量,在内存中可见,因此 JVM 可以保证任何时刻任何线程总能拿到该变量的最新值
  • 参考链接

原子类的使用

CAS算法

CAS:Compare and Swap,即比较再交换。CAS算法实现了区别于synchronouse同步锁的一种乐观锁,synchronized关键字保证同步的,这是一种独占锁,也是是悲观锁。

对CAS的理解,CAS是一种无锁算法,CAS有3个操作数,内存值V,旧的预期值A,要修改的新值B。当且仅当旧的预期值A和内存值V相同时,将内存值V修改为B,否则什么都不做。

当两者进行比较时,如果相等,则证明共享数据没有被修改,替换成新值,然后继续往下运行;如果不相等,说明共享数据已经被修改,放弃已经所做的操作,然后重新执行刚才的操作。容易看出 CAS 操作是基于共享数据不会被修改的假设,采用了类似于数据库的commit-retry 的模式。当同步冲突出现的机会很少时,这种假设能带来较大的性能提升

CAS的缺点

  • ABA 问题
    如果一个变量V初次读取的时候是A值,并且在准备赋值的时候检查到它仍然是A值,那我们就能说明它的值没有被其他线程修改过了吗?很明显是不能的,因为在这段时间它的值可能被改为其他值,然后又改回A,那CAS操作就会误认为它从来没有被修改过。这个问题被称为CAS操作的 "ABA"问题
  • 循环时间长开销大
    自旋CAS(也就是不成功就一直循环执行直到成功)如果长时间不成功,会给CPU带来非常大的执行开销。
  • 只能保证一个共享变量的原子操作
    CAS 只对单个共享变量有效,当操作涉及跨多个共享变量时 CAS 无效。 但是从 JDK 1.5开始,提供了AtomicReference类来保证引用对象之间的原子性,你可以把多个变量放在一个对象里来进行 CAS 操作;所以我们可以使用锁或者利用AtomicReference类把多个共享变量合并成一个共享变量来操作

CAS与synchronized的使用情景

  • 简单的来说CAS适用于写比较少的情况下(多读场景,冲突一般较少),synchronized适用于写比较多的情况下(多写场景,冲突一般较多)
  • 对于资源竞争较少(线程冲突较轻)的情况,使用synchronized同步锁进行线程阻塞和唤醒切换以及用户态内核态间的切换操作额外浪费消耗cpu资源;而CAS基于硬件实现,不需要进入内核,不需要切换线程,操作自旋几率较少,因此可以获得更高的性能。
  • 对于资源竞争严重(线程冲突严重)的情况,CAS自旋的概率会比较大,从而浪费更多的CPU资源,效率低于synchronized。

线程相关博客链接收藏

Executor线程池的简单使用

9. 多线程中常见锁的分类

参考链接1
参考链接2

  • 由于多线程可能同时操作同一块内存的需求出现,多线程编程中出现了临界区这个概念。为了解决这个访问冲突的问题,各种锁应运而生,锁如果以等待线程的运行方式分主要分为自旋锁和互斥锁两类,或者说按照阻塞非阻塞来划分

    • 自旋锁(spinlock):
      是指当一个线程在获取锁的时候,如果锁已经被其它线程获取,那么该线程将循环等待(不会睡眠),然后不断的判断锁是否能够被成功获取,直到获取到锁才会退出循环
      自旋锁是非阻塞锁,一直占用CPU,他在未获得锁的情况下,一直尝试得到锁也就是自旋,所以占用着CPU,如果不能在很短的时间内获得锁,会使CPU效率降低。

    • 互斥锁(mutex)
      某个线程要更改共享数据时,先将其锁定,此时资源的状态为“锁定”,其他线程不能更改;直到该线程释放资源,将资源的状态变成“非锁定”,其他的线程才能再次锁定该资源。互斥锁保证了每次只有一个线程进行写入操作,从而保证了多线程情况下数据的正确性。
      互斥锁是阻塞锁,当某线程无法获取互斥变量时,该线程会被CPU直接挂起,该线程不再消耗CPU时间,当其他线程释放互斥锁后,操作系统会激活那个被挂起的线程,让其投入运行。

CAS操作中的自旋

public final int getAndAddInt(Object var1, long var2, int var4) {
    int var5;
    do {
        var5 = this.getIntVolatile(var1, var2);
    } while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));

    return var5;
}
  • 读写锁(RWLock):
    读写锁实际是一种特殊的自旋锁,它把对共享资源的访问者划分成读者和写者,读者只对共享资源进行读访问,写者则需要对共享资源进行写操作。读写互斥,读读共享。
    1.多读者可以同时读
    2.写者写时不允许读,不允许其他写者写
    3.读者读时不允许写者写

  • 还有锁的其他分类:如悲观锁和乐观锁,公平锁和非公平锁。
    悲观锁和乐观锁这个概念主要用于数据库高并发的场景:
    1.悲观锁的共享资源每次只给一个线程使用,其它线程阻塞,用完后再把资源转让给其它线程。
    2.乐观锁对共享资源不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,可以使用版本号机制和CAS算法实现。乐观锁适用于多读的应用类型,这样可以提高数据吞吐量。

  • 公平锁和非公平锁主要在于线程的资源公平性:
    1.公平锁按照线程申请锁的顺序来获取共享资源的使用权。
    2.非公平锁使用线程抢占机制获取共享资源,效率最高,吞吐量大但有可能会出现线程饿死的情况,如果抢占失败就采用公平锁的方式到末尾排队。
    ReentrantLock:可以指定构造方法的boolean类型来指定是公平锁还是非公平锁,默认是非公平锁
    synchronized:是一种非公平锁

  • 可重入锁(又名递归锁)
    可重入锁:指的是同一线程外层方法获得锁之后,内层递归(或调用)方法仍然能够获得该锁的代码
    可重入锁的最大作用是避免死锁
    ReentrantLock和synchronized就是一个典型的可重入锁

class People{

    Lock lock = new ReentrantLock();

    public void get(){
        lock.lock();
        try{
            System.out.println(Thread.currentThread().getName()+" get");
            set();
        }finally {
            lock.unlock();
        }
    }

    public void set(){
        lock.lock();
        try{
            System.out.println(Thread.currentThread().getName()+" set");
        }finally {
            lock.unlock();
        }
    }

}

public class ReentrantLockDemo {

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

        People people = new People();

        new Thread(()->{
            people.get();
        },"t3").start();

        new Thread(()->{
            people.get();
        },"t4").start();

    }

}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值