目录
1 线程
1.1 线程是什么?
说到线程,我们首先需要先了解进程,什么是进程?通俗的说我们一个程序运行起来就是一个进程,它是操作系统分配系统资源的最小单位;而一个进程由一个或多个线程组成,线程是CPU进行任务分配和调度的最小单位。
创建进程需要系统分配一块完成的内存区域,同一进程的多个线程之间堆和方法区式共享的,虚拟机栈、本地方法栈和程序计数器是私有的。
1.2 线程的创建和使用
1.2.1 继承Thread类重写run()
继承Thread类重写run(),这种方法有一个弊端,继承父类不强制需要重写父类的方法,而且Java是单继承多实现的,如果继承Thread类表示将不能再继承别的类。创建方法如下图所示。
1.2.2 实现Runnable接口实现run()
这种方法是除了线程池最常用的方法,他需要实现Runnable接口,实现接口必须强制实现里面的方法,然后他后续还能继续继承和实现其他类,而且Runnable接口里面只有一个run(),我们平时可以使用lambda表达式创建,很方便。创建方法如下图所示。
1.2.3 实现Callable接口实现call()
这种方法是针对需要有返回值的,它里面也只有一个call(),但是它不能直接使用,需要使用其他类比如FutureTask来创建一个中间类,再将中间类传入Thread中创建线程,这种方法一般也很少使用。创建方法如下图所示。
1.2.4 使用线程池创建
使用线程池创建是我们最常使用的方法,它可以使用new对象的形式创建,也可以使用Excutor工具类创建,但是后者阿里巴巴不推荐,具体的后面我会继续说明。创建方式在下面我会进行叙述。
1.3 线程的生命周期
线程在被创建以后会进入新建状态,调用它的start()会进入就绪状态,由于线程之间的CPU资源是抢占式的,当线程抢占到CPU资源后会进入运行状态,在它运行过程中也可能被其他线程抢走CPU资源,当它的CPU资源被抢走以后会重新进入就绪状态,在它的程序计数器中会记录它当前运行的行号,等它下次抢占到CPU资源的时候会读取程序计数器中的行号继续执行,在运行过程中也可能因为锁的问题进入阻塞状态,比如sleep()、wait()、挂起线程、IO流的操作等都会让线程进入一个阻塞状态,需要特定的条件才能唤醒线程,让线程重新进入就绪状态,比如sleep()时间到了以后会自动恢复、notify()/notifyAll()唤醒wait()、恢复线程、执行完IO流操作等,当线程运行完以后会进入死亡状态,等待gc回收。
2 锁机制
2.1 Java三种锁机制
2.1.1 同步方法
顾名思义,同步方法是作用在方法上的, 使用synchronized关键字进行修饰,如果修饰的是静态方法,它的锁对象为当前类.class,如果修饰的是普通方法,那么它的锁对象为this,通过锁对象的wait()进入阻塞状态,通过锁对象的notify()/notifyAll()唤醒线程,它在进入wait()方法时会自动释放锁。
2.1.2 同步代码块
它的使用类似于同步方法,但它时使用在方法内部的,使用synchronized关键字修饰代码块,它的锁对象可以为任意对象,通过对象的wait()进入阻塞状态,对象的notify()/notifyAll()唤醒线程,它在进入wait()方法时也会自动释放锁。
2.1.3 同步锁
同步锁的实现时通过Lock的lock()和unlock()实现的,而且两个方法是成双成对的出现的,如果加锁以后不进行解锁会出现死锁的情况,通常我们会将unlock()放在finally中来保证它的执行。
2.2 三种锁机制的区别
1、粒度不同,同步方法>同步代码块>同步锁
2、效率不同,同步锁>同步代码块>同步方法
3、方便性不同,同步方法>同步代码块/同步锁
2.3 synchronized锁机制的原理
2.3.1 同步方法
在JVM常量池里面的方法表结构中的ACC_SYNCHRONIZED访问标志区分一个方法是否是同步方法,方法被调用时会先检查ACC_SYNCHRONIZED是否被设置了,如果设置了线程会先持有monitor,当方法执行完时会将释放monitor。
2.3.2 同步代码块
执行同步代码块时会生成一个monitor监视器,它里面有一个计数器,当获取到锁对象时,会将计数器的count设为1,执行完代码块或执行wait()会将count减1,count为0时会释放锁;
将class文件通过javap -c反编译成汇编语言后,里面有一个monitorEntry和monitorExit,执行monitorEntry时会尝试获取锁对象,count+1,执行monitorExit时会将count-1,count为0时释放锁。
2.4 synchronized和同步锁
1、synchronized是Java关键字,而Lock是一个接口;
2、synchronized会自动释放锁,Lock必须使用unlock()来释放锁,否则会出现死锁的情况;
3、synchronized的性能小于Lock;
4、synchronized无法获取锁,Lock可以通过tryLock()来尝试获取锁;
3 线程池
3.1 什么是线程池以及线程池的作用?
关于线程池,提到池我们应该不陌生,最先想到的就是容器,简单点说,它就是管理线程的一个容器,主要进行线程的创建、调度和回收;
作用:
1、降低资源消耗;重复使用已创建的线程可以降低创建和销毁线程造成的资源消耗;
2、提高响应速度;能够节约创建线程的等待时间;
3、提高线程的可管理性;线程是稀缺资源,如果无限次的创建,不仅浪费系统资源, 还会降低系统稳定性。
3.2 线程池的创建
3.2.1 通过普通传参方式
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.acc = System.getSecurityManager() == null ?
null :
AccessController.getContext();
this.corePoolSize = corePoolSize;
this.maximumPoolSize = maximumPoolSize;
this.workQueue = workQueue;
this.keepAliveTime = unit.toNanos(keepAliveTime);
this.threadFactory = threadFactory;
this.handler = handler;
}
上面代码是ThreadPoolExcutor类的构造方法之一,它有七大参数,也是创建线程池的七大参数,接下来我来一一解释。
corePoolSize:核心线程的数量,也是线程池的初始线程数,核心线程默认是不会被线程池回收的,当然也可以设置allowCoreThreadTimeOut来允许核心线程被回收;
maximumPoolSize:最大线程数,包括核心线程和非核心线程;
keepAliveTime:保持存活时间,用来设置非核心线程的过期时间,如果非核心线程在此时间段没有接收到任务就会被线程池回收;
unit:时间单位;
workQueue:工作队列,这个会在后面列举几个常用的工作队列;
threadFactory:线程工厂,用于创建线程;
handler:拒绝策略,当任务数超过最大线程数时拒绝执行任务的方法。
3.2.2 通过Executors工具类创建
3.2.2.1、newCachedThreadPool 可缓存型线程池
public static ExecutorService newCachedThreadPool() {
return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
60L, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>());
}
这是创建可缓存型线程池的源代码,根据线程池的七大参数,我们不难发现它的核心线程数为0;最大线程数为Integer.MAX_VALUE,也就是2^31,我们也可以直接说成无限制的,因为我们日常中不可能使用到2^31个线程,所以它的所有线程都是普通线程;而且它的存活时间为60s,也就是当线程60s内没有任务就会被线程池回收;它的工作队列为SynchronousQueue,它是一种不存储元素的阻塞队列,默认创建的时非公平的。
3.2.2.2、newScheduledThreadPool 可定时的线程池
public ScheduledThreadPoolExecutor(int corePoolSize,
ThreadFactory threadFactory,
RejectedExecutionHandler handler) {
super(corePoolSize, Integer.MAX_VALUE, 0, NANOSECONDS,
new DelayedWorkQueue(), threadFactory, handler);
}
这是它底层的一部分代码,他需要我们传递一个核心线程数进去,最大线程数也是2^31,只不过它既有核心线程也有非核心线程;它的过期时间为0,一旦线程执行完任务就会被回收;使用的工作队列为DelayedWorkQueue优先队列;
public ScheduledFuture<?> scheduleAtFixedRate(Runnable command,
long initialDelay,
long period,
TimeUnit unit) {
if (command == null || unit == null)
throw new NullPointerException();
if (period <= 0)
throw new IllegalArgumentException();
ScheduledFutureTask<Void> sft =
new ScheduledFutureTask<Void>(command,
null,
triggerTime(initialDelay, unit),
unit.toNanos(period));
RunnableScheduledFuture<Void> t = decorateTask(command, sft);
sft.outerTask = t;
delayedExecute(t);
return t;
}
这是newScheduledThreadPool的方法之一,initialDelay为第一次执行时需要等待的时间,period为每次执行需要等待的时间,如果前面有sleep(),会与sleep()的时间叠加。
public ScheduledFuture<?> scheduleWithFixedDelay(Runnable command,
long initialDelay,
long delay,
TimeUnit unit) {
if (command == null || unit == null)
throw new NullPointerException();
if (delay <= 0)
throw new IllegalArgumentException();
ScheduledFutureTask<Void> sft =
new ScheduledFutureTask<Void>(command,
null,
triggerTime(initialDelay, unit),
unit.toNanos(-delay));
RunnableScheduledFuture<Void> t = decorateTask(command, sft);
sft.outerTask = t;
delayedExecute(t);
return t;
}
这是newScheduledThreadPool的另一个方法,他与上面的主要不同就是它的delay不会与sleep()叠加。
3.2.2.3、newSingleThreadExcutor 单线程线程池
public static ExecutorService newSingleThreadExecutor() {
return new FinalizableDelegatedExecutorService
(new ThreadPoolExecutor(1, 1,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>()));
}
上面代码是其底层源码的一部分,我们可以看出它的底层也是基于ThreadPoolExcutor的,它的核心线程数为1,总的线程数也为1,所以他只有一个核心线程,它的工作队列为LinkedBlockingQueue,是一种基于链表的双向有界阻塞队列,最大为Integer.MAx_Value,按先进先出的顺序对元素进行排序。
3.2.2.4、 newFixedThreadPool 定长型线程池
public static ExecutorService newFixedThreadPool(int nThreads) {
return new ThreadPoolExecutor(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>());
}
他也是基于ThreadPoolExecutor的,它的最大线程数等于它的核心线程数等于传入的参数,工作队列也是LinkedBlockingQueue。
其实Exectors的四种创建线程池的方式都是基于ThreadPoolExecutor的,只是它帮我们封装了一些代码,但它的灵活性不够高,更建议使用ThreadPoolExecutor来创建线程池。
3.3 线程的执行(源码)
3.3.1 执行execute
public void execute(Runnable command) {
//如果传入的command为null;抛出异常
if (command == null)
throw new NullPointerException();
//获取线程池的运行状态和有效线程数
int c = ctl.get();
//如果当前线程的数量小于核心线程数,会添加一个核心线程
if (workerCountOf(c) < corePoolSize) {
if (addWorker(command, true))
return;
c = ctl.get();
}
//判断线程池是否处于运行状态,如果是则把任务添加到队列
if (isRunning(c) && workQueue.offer(command)) {
//再次获取线程池的运行状态和有效线程数
int recheck = ctl.get();
//当前线程池不处于RUNNING状态,移除任务队列workQueue中的任务对象并执行拒绝策略
if (! isRunning(recheck) && remove(command))
reject(command);
//当线程池的线程数为0时,添加一个非核心线程
else if (workerCountOf(recheck) == 0)
addWorker(null, false);
}
//如果添加非核心线程失败执行拒绝策略
else if (!addWorker(command, false))
reject(command);
}
本文对线程、三种做机制以及线程池的概念做了一些梳理,如有错误的地方还请帮忙指出!