并发学习(一)

并发学习

前言知识

进程概念:进程是程序的一次执行过程,是系统程序运行的基本单位,也是资源分配的最小单位。进程是动态的,系统允许一个程序即是一个进程从创建到销毁的过程。

线程概念:线程与进程相似,但线程是比进程更小的执行单位。一个进程在执行过程中可以产生很多个线程。同类的多个线程是共享进程中的堆区以及方法区,但每个线程都有自己的程序计数器以及虚拟机栈和本地方法区栈。线程开销比进程的开销小得多,因此称线程为轻量级进程。

程序计数器:(用于存放下一条指令所在单元地址的地方)字节码解释器通过改变程序计数器依次读取指令,从而实现代码的流程控制(顺序执行、选择、循环等),每次执行完一条指令之后就将存储的值指向下一条执行指令的内存地址。在多线程的情况下,用于记录当前线程执行的位置,当线程被切换回来时能够知道线程的执行位置。

线程切换:多线程编程中一般线程的个数大于CPU核心的个数,而一个CPU核心数只能被一个线程使用,为了能够让这些线程能够有效执行,CPU采用了时间片轮转法,当一个线程时间片用完之后会重新处于就绪状态,让其它线程使用CPU,这个过程也成为上下文切换

注意:如果执行的是native方法,那么程序计数器记录的是undefined地址,只有执行的是java代码时程序计数器记录的才是下一条指令的地址

总结:程序计数器私有化是为了线程切换后能够正确执行到每一个线程上一次执行完毕之后位置

虚拟机栈以及本地方法栈:虚拟机栈在方法执行的同时会创建一个栈帧,用于存储局部变量、操作数栈以及常量池引用等信息。等方法执行完毕之后,进行栈帧的出栈操作,整个过程相当于栈帧的入栈以及出栈操作。本地方法栈同虚拟机栈类似,区别在于虚拟机栈为虚拟机执行java方法服务,而本地方法栈则为程序中的涉及到native方法服务

堆以及方法区:堆是进程中最大的一块内存,主要用于存放创建的对象,方法区主要用于存放类的加载信息、常量、静态变量、编译器编译之后的代码等数据。

img

线程的相关知识

线程的状态:一个线程可能处于6中不同的状态,其中为初始状态、运行状态、阻塞状态、等待状态、超时等待状态、终止状态

Java 线程的状态

Java 线程状态变迁

说明:由图可知,线程创建之后处于New初始化状态,当调用start()方法之后线程处于Ready(可运行态),等待CPU调度之后处于运行态(Running)

操作系统隐藏 Java 虚拟机(JVM)中的 RUNNABLE 和 RUNNING 状态,它只能看到 RUNNABLE 状态,所以 Java 系统一般将这两个状态统称为 RUNNABLE(运行中) 状态 。

当执行wait()方法时进入等待状态,只有等另一个线程执行notify方法或者nofityAll方法之后重新进入就绪态,等待CPU调度;调用wait(long millis)方法的时候线程进入超时等待状态,即使没有线程调用notify方法,该线程在时间到时会自动进入就绪态。当线程调用同步方法时,在没有获取到锁的情况下,线程进入阻塞状态。线程在执行完run方法之后变为进入终止状态。

**线程死锁:**多个线程同时处于阻塞状态,它们中的一个或者全部都在等待某个资源的释放。由于线程被无限的阻塞,因此程序将无法终止

//通过程序来进行说明,线程1抢占到资源1后,线程2又抢占到资源2,而同时,线程1又需要资源2,但此时的资源2被占用了,线程1进入阻塞状态。线程2也一样,需要资源1,也进入阻塞状态。此时程序将一直被阻塞,从而无法退出程序
public class DeadLockDemo {
    private static Object resource1 = new Object();//资源 1
    private static Object resource2 = new Object();//资源 2

    public static void main(String[] args) {
        new Thread(() -> {
            synchronized (resource1) {
                System.out.println(Thread.currentThread() + "get resource1");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread() + "waiting get resource2");
                synchronized (resource2) {
                    System.out.println(Thread.currentThread() + "get resource2");
                }
            }
        }, "线程 1").start();

        new Thread(() -> {
            synchronized (resource2) {
                System.out.println(Thread.currentThread() + "get resource2");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread() + "waiting get resource1");
                synchronized (resource1) {
                    System.out.println(Thread.currentThread() + "get resource1");
                }
            }
        }, "线程 2").start();
    }
}
//输出,出现程序无法退出的情况
Thread[线程 1,5,main]get resource1
Thread[线程 2,5,main]get resource2
Thread[线程 1,5,main]waiting get resource2
Thread[线程 2,5,main]waiting get resource1

这里补充死锁的知识,产生死锁的具备以下四个条件:1、互斥条件 2、请求与保持条件 3、不剥夺条件 4、循环等待 ;避免死锁只需要破坏这四者中的一个便可以破坏死锁

博客学习

多线程的学习

这里记录一些自己的理解,详见博客,很详细

1、synchronized同步锁对象synchronized,通过锁对象来限制访问的线程,如果对象锁为同一个时,有且只有一个线程能进入同步代码块或方法中

//实例1
public class RunnableTest {

    /**
     * @param args
     */
    public static void main(String[] args) {
        // TODO Auto-generated method stub

        class MyRunnable implements Runnable{

            private int j=5;
            @Override
            public void run() {

                synchronized(this){      //加锁的为当前对象
                    for(int i=0;i<5;i++){
                        try {
                            Thread.sleep(100);
                            System.out.println(Thread.currentThread().getName()+" loop "+i);
                        } catch (InterruptedException e) {
                            // TODO Auto-generated catch block
                            e.printStackTrace();
                        }
                    }
                }   
            }   
        }
        Runnable runnable = new MyRunnable();
        Thread t1 = new Thread(runnable,"t1");
        Thread t2 = new Thread(runnable,"t2");

        t1.start();
        t2.start();     
    }
}

//输出结果如下,即当t1线程占用锁对象时,t2线程是无法进入同步代码块中的,只有等t1线程释放掉所对象之后才能够进入
t1 loop 0
t1 loop 1
t1 loop 2
t1 loop 3
t1 loop 4
t1 loop 5
t1 loop 6
t1 loop 7
t1 loop 8
t1 loop 9
t2 loop 0
t2 loop 1
t2 loop 2
t2 loop 3
t2 loop 4
t2 loop 5
t2 loop 6
t2 loop 7
t2 loop 8
t2 loop 9

synchronized的底层原理

同步语句块的情况:

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

同步语句块的实现使用的是monitorenter和monitorexit指令,其中monitorenter指同步代码块的开始位置,而monitorexit是同步代码块的结束位置。当执行monitorenter的时候,线程试图获取锁对象的monitor(monitor对象是存在java对象中的对象头中,每个java对象都具有),获取到时将锁计数器设为1,当其余线程获取锁时,检测到锁的计数器为1则被阻塞。当执行monitorexit时将锁释放,锁计数器-1变为0;

synchronized修饰实例方法时锁对象为实例对象,修饰静态方法时锁对象为类对象;修饰代码块时指定锁对象

修饰实例方法的时候:

public class SynchronizedDemo2 {
	public synchronized void method() {
		System.out.println("synchronized 方法");
	}
}

修饰方法的时候并没有monitorenter和monitorexit指令,取而代之是 ACC_SYNCHRONIZED 标识,该标识指明了该方法是一个同步方法,jvm通过该标识来辨别一个方法是否声明为同步方法,从而执行相应的同步调用。

2、实例锁跟全局锁:实例锁即同步代码块或方法是非静态,锁对象为实例对象,而全局锁是存在于静态方法或静态代码块中的,是通过类来创建锁(static synchronized)

3、wait方法和notify方法:存在于同步代码块中,当调用wait方法时,始当前线程进入等待阻塞状态,并释放当前的锁对象,直到其余线程调用notify方法或者notifyAll方法—wait(long time)等待某段时间后重新运行,即使没有线程调用notify方法。

4、join的方法:等待子线程(即调用join方法的那个线程类)执行完成之后再执行父线程

public class JoinTest {

	
	public static void main(String[] args) {


		class ThreadA extends Thread{
			
			public ThreadA(String name){
				super(name);
			}
			
			@Override
			public void run() {
				System.out.println("start "+this.getName());
				
				for(int i=0;i<1000000000;i++)
					;
				
				System.out.println("finish "+ this.getName());
			}
		}

		Thread t1 = new ThreadA("t1");
		t1.start();
		
		try {
			
			t1.join();
			
			System.out.println("finish "+Thread.currentThread().getName());
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
	}

}


//运行结果,因为调用了t1线程的join方法,所以等t1线程执行完毕之后才继续执行main线程

start t1
finish t1
finish main

5、wait和sleep的不同:同样是始当前线程进入阻塞状态,但是sleep方法不会释放当前锁对象

6、volatile关键字:用于同步线程之间的共享变量,如图

​ 所有的变量都是存储到主内存中,每个线程都是独立的工作内存,里面保存该线程使用到的变量的副本。线程要向获取到共享变量,就要先将共享变量的值存储到自己的工作目录,在取出使用。线程1如果对共享变量做了修改,线程2要进行实时的跟新,需要经过以下步骤:

把线程1中的工作内存的变量刷新到主内存;再由线程2将主内存最新的值刷新到自己的工作目录

Java内存模型图

基本概念

**可见性:**指线程之间的可见性,一个线程修改状态对另一个线程是可见的。volatile修饰的变量具有可见性。volatile修饰的变量不允许线程内部缓存和重排序,即直接修改内存。所以对其他线程是可见的。在java中,具有可见性的有volatile、synchronized和final

原子性:jvm中执行的最小单位,具有不可分割性。比如int a=0;这个操作是不可分隔的,所以具有原子性。但是a++是可以分隔的,即可以分隔成a=a+1,所以不具备原子性。在java中synchronized和在lock和unlock中操作保证了原子性。

有序性:java提供了volatile和synchronized两个关键字来保证线程之间操作的有序性。volatile具有禁止指令重排的效果,而synchronized同步锁在同一时间只允许一个线程进行操作,因此其具有有序性。

volatile关键字说明

当一个变量被volatile修饰后,不但具有可见性(即告诉jvm这个变量每次使用是需要到主存中读取的),而且还禁止指令重排(jvm在编译的时候有可能进行指令重排)以及有序性,相当于一个轻量级的synchronized。volatile的读性能消耗与普通变量几乎相同,但是写操作就慢一些,因为它要保证本地代码中插入许多内存屏障指令来保证处理器不发生乱序执行。

//synchronized和volatile关键字的区别
1、volatile关键字是线程同步的轻量级实现,所以volatile性能肯定比synchronized关键字的要好。但是volatile只能用于变量,而synchronized可以用于修饰方法和代码块
2、多线程访问volatile不会发生阻塞,而synchronized关键字可能会发生阻塞
3、volatile保证了数据的可见性,但不能保证原子性,synchronized两者都可以保证
4、volatile主要解决变量之间在多个线程的可见性,而synchronized关键字主要用于解决线程之间访问资源的同步性

7、中断线程:当我们要中止处于阻塞状态的线程时,调用interrupt方法(该方法是将中断标记设为true),调用线程的interrupt()将线程的中断标记设为true。由于处于阻塞状态,中断标记会被清除,同时产生InterruptedException异常。将InterruptedException放在适当的为止就能终止线程。

中止运行状态的线程:通常,我们通过“标记”方式终止处于“运行状态”的线程。其中包括“中断标记”和“额外添加标记”

中断标记:

@Override
public void run() {
    while (!isInterrupted()) {
        // 执行任务...
    }
}
//通过isInterrupted()来拿取中断标记的值,当需要中断这个循环的线程时,另一个线程执行interrupt将中断标记改为true,此时便会退出while循环,中断允许中的线程

额外添加标记:

private volatile boolean flag= true;   //将flag用volatile修饰利用了volatile的可见性,让所做的修改让线程看到
protected void stopTask() {
    flag = false;
}

@Override
public void run() {
    while (flag) {
        // 执行任务...
    }
}
//通过flag这个变量来判断是否要中止进程,要的话,另一个线程将flag改为false便会中止线程

8、ThreadLocal:主要解决了让每个线程绑定自己的值,存储线程私有的变量。

import java.text.SimpleDateFormat;
import java.util.Random;

public class ThreadLocalExample implements Runnable{

     // SimpleDateFormat 不是线程安全的,所以每个线程都要有自己独立的副本
    private static final ThreadLocal<SimpleDateFormat> formatter = ThreadLocal.withInitial(() -> new SimpleDateFormat("yyyyMMdd HHmm"));

    public static void main(String[] args) throws InterruptedException {
        ThreadLocalExample obj = new ThreadLocalExample();
        for(int i=0 ; i<10; i++){
            Thread t = new Thread(obj, ""+i);
            Thread.sleep(new Random().nextInt(1000));
            t.start();
        }
    }

    @Override
    public void run() {
        System.out.println("Thread Name= "+Thread.currentThread().getName()+" default Formatter = "+formatter.get().toPattern());
        try {
            Thread.sleep(new Random().nextInt(1000));
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        //formatter pattern is changed here by thread, but it won't reflect to other threads
        formatter.set(new SimpleDateFormat());

        System.out.println("Thread Name= "+Thread.currentThread().getName()+" formatter = "+formatter.get().toPattern());
    }

}
//输出
Thread Name= 0 default Formatter = yyyyMMdd HHmm
Thread Name= 0 formatter = yy-M-d ah:mm
Thread Name= 1 default Formatter = yyyyMMdd HHmm
Thread Name= 2 default Formatter = yyyyMMdd HHmm
Thread Name= 1 formatter = yy-M-d ah:mm
Thread Name= 3 default Formatter = yyyyMMdd HHmm
Thread Name= 2 formatter = yy-M-d ah:mm
Thread Name= 4 default Formatter = yyyyMMdd HHmm
Thread Name= 3 formatter = yy-M-d ah:mm
Thread Name= 4 formatter = yy-M-d ah:mm
Thread Name= 5 default Formatter = yyyyMMdd HHmm
Thread Name= 5 formatter = yy-M-d ah:mm
Thread Name= 6 default Formatter = yyyyMMdd HHmm
Thread Name= 6 formatter = yy-M-d ah:mm
Thread Name= 7 default Formatter = yyyyMMdd HHmm
Thread Name= 7 formatter = yy-M-d ah:mm
Thread Name= 8 default Formatter = yyyyMMdd HHmm
Thread Name= 9 default Formatter = yyyyMMdd HHmm
Thread Name= 8 formatter = yy-M-d ah:mm
Thread Name= 9 formatter = yy-M-d ah:mm

从中可以看出,Tread——0已经改变了formatter的值,但thread-2默认格式化程序与初始化的值相同。

原理

//Thread类的源代码
public class Thread implements Runnable {
 ......
//与此线程有关的ThreadLocal值。由ThreadLocal类维护
ThreadLocal.ThreadLocalMap threadLocals = null;

//与此线程有关的InheritableThreadLocal值。由InheritableThreadLocal类维护
ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;
 ......
}

从中可以看出,Thread类中有threadLocals和inheritableThreadLocals两个变量,初始化都为null;它们都属于ThreadLocalMap类型的变量,当线程调用ThreadLocal类的set或者get方法时才创建他们,调用两个方法,实际调用的时ThreadLocalMap的set和get方法。

//ThraedLocal的set方法
public void set(T value) {
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null)
            map.set(this, value);
        else
            createMap(t, value);
    }
    ThreadLocalMap getMap(Thread t) {
        return t.threadLocals;
    }

从中可以看出,调用set方法,是先获取到当前线程,通过getMap的方法获取到ThreadLocalMap对象,之后调用ThreadLocalMap的set方法。最终的变量是存储到当前线程的ThreadLocalMap中,并不是存储在ThreadLocal中。每一个Thread中都具备一个ThreadLocalMap,而ThreadLocalMap可以存储以ThreadLocal为key的键值对。比如我们在同一个线程中声明了两个 ThreadLocal 对象的话,会使用 Thread内部都是使用仅有那个ThreadLocalMap 存放数据的,ThreadLocalMap的 key 就是 ThreadLocal对象,value 就是 ThreadLocal 对象调用set方法设置的值。

ThreadLocal的内存泄露:ThreadLocalMap 中使用的 key 为 ThreadLocal ,因为ThreadLocal作为局部变量很快会被垃圾回收器回收。如果key为强引用,这样一来,ThreadLocalMap 中就会出现key为null的Entry,而且线程会在线程池中放着回收不了。这个时候就可能会产生内存泄露问题。ThreadLocalMap实现中已经考虑了这种情况,key为弱引用,在调用 get()方法的时候,会清理掉 key 为 null 的节点记录。

线程池

好处:降低资源的消耗:通过重复利用已经创建的线程,可以降低线程的创建和销毁所消耗的资源。提高响应速度:任务达到时,可以立即拿取到线程执行任务,不需要等待线程的创建。提高线程的可管理性:使用线程池可以统一的分配、调优和监控

executor框架:

1、组成部分:

  • 任务

    执行的任务需要实现Runnable接口或者Callable接口。·Runnable接口·和Callable接口实现类都可以被ThreadPoolExecutor或ScheduledThreadPoolExecutor执行

  • 任务的执行

  • 异步计算的结果

    Future接口以及其子类FutureTask类都可以代表异步计算的结果。当我们的任务提交给ThreadPoolExecutorcheduledThreadPoolExecutor执行的时候(submit方法),会返回FutureTask对象

    任务的执行相关接口

2、使用流程

  • 主线程创建实现Runnable接口或者Callable接口的线程对象
  • 把这个线程对象交给ExecutorService执行ExecutorService.execute(Runnable command)或者ExecutorService.submit(Runnable task/Callable task)
  • 执行submit方法,会返回一个FutureTask对象,这个对象实现自RunnableFuture接口,而RunnableFuture又继承自Runnable。我们也可以自己创建FutureTask,然后交给ExecutorService执行
  • 最后,主线程可以执行FutureTask.get()方法等待任务的执行完成。主线程也可以执行FutureTask.cancel(boolean may)来取消此任务的执行
public class CachedThreadPool {

    /**
     * @param args
     */
    public static void main(String[] args) {

        class MyRunnable implements Runnable{
            private int a = 5;
            @Override
            public void run() {
                synchronized(this){
                    for(int i=0;i<10;i++){
                        if(this.a>0){
                            System.out.println(Thread.currentThread().getName()+" a的值:"+this.a--);
                        }

                    }
                }
            }

        }
        ExecutorService exec = Executors.newCachedThreadPool();    
        for(int i=0;i<5;i++)
            exec.execute(new MyRunnable());   //提交执行
        exec.shutdown();

    }

}

pool-1-thread-2 a的值:5
pool-1-thread-1 a的值:5
pool-1-thread-1 a的值:4
pool-1-thread-1 a的值:3
pool-1-thread-3 a的值:5
pool-1-thread-2 a的值:4
pool-1-thread-1 a的值:2
pool-1-thread-1 a的值:1
pool-1-thread-2 a的值:3
pool-1-thread-2 a的值:2
pool-1-thread-2 a的值:1
pool-1-thread-3 a的值:4
pool-1-thread-3 a的值:3
pool-1-thread-3 a的值:2
pool-1-thread-3 a的值:1
pool-1-thread-5 a的值:5
pool-1-thread-5 a的值:4
pool-1-thread-5 a的值:3
pool-1-thread-5 a的值:2
pool-1-thread-5 a的值:1
pool-1-thread-4 a的值:5
pool-1-thread-4 a的值:4
pool-1-thread-4 a的值:3
pool-1-thread-4 a的值:2
pool-1-thread-4 a的值:1

ThreadPoolExecutor类(Executor框架的核心)

/**
     * 用给定的初始参数创建一个新的ThreadPoolExecutor。
     */
    public ThreadPoolExecutor(int corePoolSize,//线程池的核心线程数量
                              int maximumPoolSize,//线程池的最大线程数
                              long keepAliveTime,//当线程数大于核心线程数时,多余的空闲线程存活的最长时间
                              TimeUnit unit,//时间单位
                              BlockingQueue<Runnable> workQueue,//任务队列,用来储存等待执行任务的队列
                              ThreadFactory threadFactory,//线程工厂,用来创建线程,一般默认即可
                              RejectedExecutionHandler handler//拒绝策略,当提交的任务过多而不能及时处理时,我们可以定制策略来处理任务
                               ) {
        if (corePoolSize < 0 ||
            maximumPoolSize <= 0 ||
            maximumPoolSize < corePoolSize ||
            keepAliveTime < 0)
            throw new IllegalArgumentException();
        if (workQueue == null || threadFactory == null || handler == null)
            throw new NullPointerException();
        this.corePoolSize = corePoolSize;
        this.maximumPoolSize = maximumPoolSize;
        this.workQueue = workQueue;
        this.keepAliveTime = unit.toNanos(keepAliveTime);
        this.threadFactory = threadFactory;
        this.handler = handler;
    }
//重点
ThreadPoolExecutor三个重要参数
corePoolSize:核心线程的数量,定义一个最小可以同时运行的线程数量。
maximumPoolSize:最大线程数量,当任务数量达到队列的容量时,当前可以同时运行的线程数量。
workQueue:任务来临时,会先判断当前运行的线程数是否达到核心线程数,如果达到了,线程会进入阻塞队列中。
keepAliveTime:当线程池中的线程数量超过核心线程数量的时候,这个时候没有新任务提交,核心线程外的线程会等到超过这个keepAliveTime时间后回收摧毁。
unit:keepAliveTime的时间单位
threadFactory:executor创建线程的时候会用到
handler:饱和策略

线程池各个参数的关系

ThreadPoolExecutor的饱和策略(当任务workQueue饱和时,线程数也达到饱和时的策略)

  • ThreadPoolExecutor.AbortPolicy:抛出 RejectedExecutionException来拒绝新任务的处理。
  • ThreadPoolExecutor.CallerRunsPolicy:调用执行自己的线程运行任务。您不会任务请求。但是这种策略会降低对于新任务提交速度,影响程序的整体性能。另外,这个策略喜欢增加队列容量。如果您的应用程序可以承受此延迟并且你不能任务丢弃任何一个任务请求的话,你可以选择这个策略。
  • ThreadPoolExecutor.DiscardPolicy 不处理新任务,直接丢弃掉。
  • ThreadPoolExecutor.DiscardOldestPolicy 此策略将丢弃最早的未处理的任务请求。

当我们在创建线程池的时候没有指定饱和策略,默认是使用ThreadPoolExecutor.AbortPolicy策略,将抛出异常拒绝新来的任务,这样新来的任务就会丢失。建议使用CallerRunsPolicy策略,为我们提供可伸缩队列

使用实例:

import java.util.Date;

/**
 * 这是一个简单的Runnable类,需要大约5秒钟来执行其任务。
 * @author 
 */
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;
    }
}
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");
    }
}
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

从上面可以看出,我们创建一个ThreadPoolExecutor,初始化的核心线程数5,最大线程数为10,阻塞队列为100,非核心线程的保持时间为1s

执行流程:

  • 创建实现Runnable接口的任务
  • 提交任务给ThreadPoolExecutor,并执行它的execute方法
//execute的源码
// 存放线程池的运行状态 (runState) 和线程池内有效线程的数量 (workerCount)
   private final AtomicInteger ctl = new AtomicInteger(ctlOf(RUNNING, 0));

    private static int workerCountOf(int c) {
        return c & CAPACITY;
    }

    private final BlockingQueue<Runnable> workQueue;

    public void execute(Runnable command) {
        // 如果任务为null,则抛出异常。
        if (command == null)
            throw new NullPointerException();
        // ctl 中保存的线程池当前的一些状态信息
        int c = ctl.get();

        //  下面会涉及到 3 步 操作
        // 1.首先判断当前线程池中之行的任务数量是否小于 corePoolSize
        // 如果小于的话,通过addWorker(command, true)新建一个线程,并将任务(command)添加到该线程中;然后,启动该线程从而执行任务。
        if (workerCountOf(c) < corePoolSize) {
            if (addWorker(command, true))
                return;
            c = ctl.get();
        }
        // 2.如果当前之行的任务数量大于等于 corePoolSize 的时候就会走到这里
        // 通过 isRunning 方法判断线程池状态,线程池处于 RUNNING 状态才会被并且队列可以加入任务,该任务才会被加入进去
        if (isRunning(c) && workQueue.offer(command)) {
            int recheck = ctl.get();
            // 再次获取线程池状态,如果线程池状态不是 RUNNING 状态就需要从任务队列中移除任务,并尝试判断线程是否全部执行完毕。同时执行拒绝策略。
            if (!isRunning(recheck) && remove(command))
                reject(command);
                // 如果当前线程池为空就新创建一个线程并执行。
            else if (workerCountOf(recheck) == 0)
                addWorker(null, false);
        }
        //3. 通过addWorker(command, false)新建一个线程,并将任务(command)添加到该线程中;然后,启动该线程从而执行任务。
        //如果addWorker(command, false)执行失败,则通过reject()执行相应的拒绝策略的内容。
        else if (!addWorker(command, false))
            reject(command);
    }

大致步骤为,先判断线程池中的核心线程是否满了,满则判断workqueue阻塞队列是否为满,满则判断线程池线程是否达到最大线程数,是则根据创建时的饱和策略处理。

图解线程池实现原理

几种常见的对比

1、Runnable和Callable

​ Runnable接口不会返回结果或抛出异常,而Callable接口可以。对于任务不需要返回结果的,推荐使用Runnable接口,而对于需要返回结果的,建议使用Callable接口

@FunctionalInterface
public interface Runnable {
   /**
    * 被线程执行,没有返回值也无法抛出异常
    */
    public abstract void run();
}

Callable.java
@FunctionalInterface
public interface Callable<V> {
    /**
     * 计算结果,或在无法这样做时抛出异常。
     * @return 计算得出的结果
     * @throws 如果无法计算结果,则抛出异常
     */
    V call() throws Exception;
}

2、execute()和smbmit()方法

  • execute方法用于提交不需要放回值的任务(Runnable),所以无法判断任务是否被线程池执行成功与否
  • sumbit方法用于提交需要返回值的任务,会返回一个FutureTask对象,通过这个对象可以判断任务执行成功与否。可以通过Future对象的get方法来获取返回值,阻塞当前的线程直到任务完成,调用get(long timeout,TimeUnit unit)方法则会阻塞当前线程一段时间后返回,又可能任务还没完成

3、shutdown()和shutdownNow()

  • shutdown方法关闭线程池,将线程池的任务变为shutdown,此时的线程池不会接收新的任务,但是队列里的任务会执行完毕
  • shutdownNow方法,关闭线程池,线程池的状态变为stop并且停止正在处理的任务以及排队的任务,并返回正在等待执行的list

4、isTerminated方法以及isShutDown方法

  • 当调用shutdown时,isShutDown返回true
  • 当调用shutdow之后,并且所有的任务完成了之后调用isTerminated返回true

几种常见的线程池

通过Executors创建:FixedThreadPool、CacheThreadPool、ScheduledThreadPoolExecutor、SingleThreadExecutor这几类线程池,都是直接或者间接的配置ThreadPoolExecutor来实现自己的功能属性,所以在阿里巴巴的开发手册中推荐直接使用ThreadPoolExecutor来创建线程池

  1. FixedThreadPool

    通过Executors的newFixedThreadPool方法来创建实现。它是一种线程数量固定的线程池。只有核心线程,因此不会销毁线程,且阻塞队列没有大小限制,为链表形式.因此其执行流程是等待核心线程空闲出来,再从阻塞队列中取出任务执行,线程永远处于活跃状态

    //executors源码
       /**
         * 创建一个可重用固定数量线程的线程池
         */
        public static ExecutorService newFixedThreadPool(int nThreads, ThreadFactory threadFactory) {
            return new ThreadPoolExecutor(nThreads, nThreads,
                                          0L, TimeUnit.MILLISECONDS,
                                          new LinkedBlockingQueue<Runnable>(),
                                          threadFactory);
        }
        
          public static ExecutorService newFixedThreadPool(int nThreads) {
            return new ThreadPoolExecutor(nThreads, nThreads,
                                          0L, TimeUnit.MILLISECONDS,
                                          new LinkedBlockingQueue<Runnable>());
        }
        
    

    执行流程:

    FixedThreadPool的execute()方法运行示意图

等待核心线程空闲出来,再从阻塞队列中取出任务执行,线程永远处于活跃状态

  1. 如果当前运行的线程数小于 corePoolSize, 如果再来新任务的话,就创建新的线程来执行任务;
  2. 当前运行的线程数等于 corePoolSize 后, 如果再来新任务的话,会将任务加入 LinkedBlockingQueue
  3. 线程池中的线程执行完 手头的任务后,会在循环中反复从 LinkedBlockingQueue 中获取任务来执行

缺点:使用无界队列LinkedBlocakingQueue,在任务比较多的时候为导致OOM(内存泄漏)

2、SingleThreadExecutor

​ 只有一个核心线程的线程池,且最大线程数=核心线程=1,这个线程池不需要处理线程同步的问题,实现代码如下:

  /**
     *返回只有一个线程的线程池
     */
    public static ExecutorService newSingleThreadExecutor(ThreadFactory threadFactory) {
        return new FinalizableDelegatedExecutorService
            (new ThreadPoolExecutor(1, 1,
                                    0L, TimeUnit.MILLISECONDS,
                                    new LinkedBlockingQueue<Runnable>(),
                                    threadFactory));
    }

  public static ExecutorService newSingleThreadExecutor() {
        return new FinalizableDelegatedExecutorService
            (new ThreadPoolExecutor(1, 1,
                                    0L, TimeUnit.MILLISECONDS,
                                    new LinkedBlockingQueue<Runnable>()));
    }

执行步骤与FixedThreadPool类似,也是使用无界队列,性能上没FixedThreadPool好,任务多时也会导致OOm

3、CachedThreadPool

​ 会根据需要创建新线程的线程池,核心线程数为0,最大线程数为Integer.MAX_VALUE

 /**
     * 创建一个线程池,根据需要创建新线程,但会在先前构建的线程可用时重用它。
     */
    public static ExecutorService newCachedThreadPool(ThreadFactory threadFactory) {
        return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
                                      60L, TimeUnit.SECONDS,
                                      new SynchronousQueue<Runnable>(),
                                      threadFactory);
    }
    
        public static ExecutorService newCachedThreadPool() {
        return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
                                      60L, TimeUnit.SECONDS,
                                      new SynchronousQueue<Runnable>());
    }

当主线程提交任务的速度大于线程池中线程的执行速度时,会不断创建新的线程来处理新加的任务,会导致耗尽cpu和内存资源。

CachedThreadPool的execute()方法的执行示意图

  1. 首先执行 SynchronousQueue.offer(Runnable task) 提交任务到任务队列。如果当前 maximumPool 中有闲线程正在执行 SynchronousQueue.poll(keepAliveTime,TimeUnit.NANOSECONDS),那么主线程执行 offer 操作与空闲线程执行的 poll 操作配对成功,主线程把任务交给空闲线程执行,execute()方法执行完成,否则执行下面的步骤 2;
  2. 当初始 maximumPool 为空,或者 maximumPool 中没有空闲线程时,将没有线程执行 SynchronousQueue.poll(keepAliveTime,TimeUnit.NANOSECONDS)。这种情况下,步骤 1 将失败,此时 CachedThreadPool 会创建新线程执行任务,execute 方法执行完成;

4、ScheduledThreadPoolExecutor

核心线程数是固定的,而非核心线程数是没有限制的,并且非核心线程被限制了会被立即回收,主要用于执行定时任务和具有固定周期的重复任务。继承自ThreadPoolExecutor,并实现了ScheduledExecutorService

    public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize) {
            return new ScheduledThreadPoolExecutor(corePoolSize);
        }

    public ScheduledThreadPoolExecutor(int corePoolSize) {
        super(corePoolSize, Integer.MAX_VALUE, 0, TimeUnit.NANOSECONDS,
              new DelayedWorkQueue());
    }

ScheduledThreadPoolExecutor使用的任务队列DelayQueue封装了一个PriorityQueue,PriorityQueue会对任务队列中的任务进行排序,所需时间短的任务会被先执行,若时间相同,先提交的任务会被先执行

ScheduledThreadPoolExecutor运行机制

  1. 当调用 ScheduledThreadPoolExecutorscheduleAtFixedRate() 方法或者**scheduleWirhFixedDelay()** 方法时,会向 ScheduledThreadPoolExecutorDelayQueue 添加一个实现了 RunnableScheduledFuture 接口的 ScheduledFutureTask
  2. 线程池中的线程从 DelayQueue 中获取 ScheduledFutureTask,然后执行任务。

ScheduledThreadPoolExecutor 为了实现周期性的执行任务,对 ThreadPoolExecutor做了如下修改:

  • 使用 DelayQueue 作为任务队列;
  • 获取任务的方不同
  • 执行周期任务后,增加了额外的处理

执行周期任务的步骤:

ScheduledThreadPoolExecutor执行周期任务的步骤

  1. 程 1 从 DelayQueue 中获取已到期的 ScheduledFutureTask(DelayQueue.take())。到期任务是指 ScheduledFutureTask的 time 大于等于当前系统的时间;
  2. 线程 1 执行这个 ScheduledFutureTask
  3. 线程 1 修改 ScheduledFutureTask 的 time 变量为下次将要被执行的时间;
  4. 线程 1 把这个修改 time 之后的 ScheduledFutureTask 放回 DelayQueue 中(DelayQueue.add())。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值