文章目录
一、java线程实现和创建方式
1.继承Thread类
Thread 类本质上是实现了 Runnable 接口的一个实例,代表一个线程的实例。启动线程的唯一方法就是通过 Thread 类的 start()实例方法。start()方法是一个 native 方法,它将启动一个新线程,并执行 run()方法。
static class MyThread extends Thread{
public void run(){
System.out.println("MyThread 开始运行");
}
}
public static void main(String[] args) {
MyThread myThread=new MyThread();
myThread.start();
}
2.实现Runnable接口
java中,类不可以继承多个父类,但可以实多个接口,当一个类已经有父类时,想变为线程类就可以实现
Runnable接口。
static class MyThread2 implements Runnable{
@Override
public void run() {
System.out.println("MyThread2 开始运行");
}
}
public static void main(String[] args) {
MyThread2 myThread2=new MyThread2();
Thread thread=new Thread(myThread2);
thread.start();
}
3.实现Callable接口(线程执行结果有返回值)
有时候我们需要等待一个线程执行的结果再决定下一步的操作就可以实现Callable接口。执行Callable 任务后,可以获取一个 Future 的对象,在该对象上调用 get 就可以获取到 Callable 任务返回的 Object 。
static class MyThread3 implements Callable<String> {
@Override
public String call() throws Exception {
Thread.sleep(10000L);
return "ok";
}
}
public static void main(String[] args) {
MyThread3 myThread3=new MyThread3();
FutureTask<String> futureTask = new FutureTask<>(myThread3);
Thread thread=new Thread(futureTask);
thread.start();
//获取线程执行结果。如果此时获取结果的任务还未执行完成,会让出CPU,直至任务执行完成才获取结果。
try {
String s = futureTask.get();
System.out.println(s);
} catch (Exception e) {
e.printStackTrace();
}
}
4.基于线程池
线程和数据库连接这些资源都是非常宝贵的资源。那么每次需要的时候创建,不需要的时候销毁,是非常浪费资源的。那么我们就可以使用缓存的策略,也就是使用线程池。
ExecutorService threadPool= Executors.newFixedThreadPool(10);
threadPool.execute(new Runnable() {
@Override
public void run() {
System.out.println("基于线程池运行线程");
}
});
二、线程池分类
Java 里面线程池的顶级接口是 Executor,但是严格意义上讲 Executor 并不是一个线程池,而只是一个执行线程的工具。真正的线程池接口是 ExecutorService。
1.newCachedThreadPool
创建一个可根据需要创建新线程的线程池,但是在以前构造的线程可用时将重用它们。对于执行很多短期异步任务的程序而言,这些线程池通常可提高程序性能。调用 execute 将重用以前构造的线程(如果线程可用)。如果现有线程没有可用的,则创建一个新线程并添加到池中。终止并从缓存中移除那些已有 60 秒钟未被使用的线程。因此,长时间保持空闲的线程池不会使用任何资源。
2.newFixedThreadPool
创建一个可重用固定线程数的线程池,以共享的无界队列方式来运行这些线程。在任意点,在大
多数 nThreads 线程会处于处理任务的活动状态。如果在所有线程处于活动状态时提交附加任务,
则在有可用线程之前,附加任务将在队列中等待。如果在关闭前的执行期间由于失败而导致任何
线程终止,那么一个新线程将代替它执行后续的任务(如果需要)。在某个线程被显式地关闭之
前,池中的线程将一直存在。
3.newSingleThreadExecutor
Executors.newSingleThreadExecutor()返回一个线程池(这个线程池只有一个线程),这个线程池可以在线程死后(或发生异常时)重新启动一个线程来替代原来的线程继续执行下去。
4.newScheduledThreadPool
创建一个线程池,它可安排在给定延迟后运行命令或者定期地执行。
ScheduledExecutorService threadPool= Executors.newScheduledThreadPool(2);
threadPool.schedule(new Runnable() {
@Override
public void run() {
System.out.println("延迟3秒执行");
}
},3,TimeUnit.SECONDS);
threadPool.scheduleAtFixedRate(new Runnable() {
@Override
public void run() {
System.out.println("延迟1秒后每3秒执行");
}
},1,3,TimeUnit.SECONDS);
三、线程的生命周期
- 新建状态
当程序使用 new 关键字创建了一个线程之后,该线程就处于新建状态,此时仅由 JVM 为其分配内存,并初始化其成员变量的值。 - 就绪状态
当线程对象调用了 start()方法之后,该线程处于就绪状态。Java 虚拟机会为其创建方法调用栈和程序计数器,等待调度运行。 - 运行状态
如果处于就绪状态的线程获得了 CPU,开始执行 run()方法的线程执行体,则该线程处于运行状
态。 - 阻塞状态
阻塞状态是指线程因为某种原因放弃了 cpu 使用权,也即让出了 cpu timeslice,暂时停止运行。直到线程进入可运行(runnable)状态,才有机会再次获得 cpu timeslice 转到运行(running)状态。阻塞的情况分三种:
等待阻塞(o.wait->等待对列):
运行(running)的线程执行 o.wait()方法,JVM 会把该线程放入等待队列(waitting queue)中。
同步阻塞(lock->锁池):
运行(running)的线程在获取对象的同步锁时,若该同步锁被别的线程占用,则 JVM 会把该线程放入锁池(lock pool)中。
其他阻塞(sleep/join):
运行(running)的线程执行 Thread.sleep(long ms)或 t.join()方法,或者发出了 I/O 请求时,JVM 会把该线程置为阻塞状态。当 sleep()状态超时、join()等待线程终止或者超时、或者 I/O处理完毕时,线程重新转入可运行(runnable)状态。 - 死亡状态
线程会以下面三种方式结束,结束后就是死亡状态。
正常结束:
run()或 call()方法执行完成,线程正常结束。
异常结束:
线程抛出一个未捕获的 Exception 或 Error。
调用 stop:
直接调用该线程的 stop()方法来结束该线程—该方法通常容易导致死锁,不推荐使用。
四、终止线程的方式
- 正常运行结束
- 使用退出标志退出线程
一般 run()方法执行完,线程就会正常结束,然而,常常有些线程是伺服线程。它们需要长时间的
运行,只有在外部某些条件满足的情况下,才能关闭这些线程。使用一个变量来控制循环,例如:
最直接的方法就是设一个 boolean 类型的标志,并通过设置这个标志为 true 或 false 来控制 while
循环是否退出。
static class MyThread extends Thread{
public volatile boolean exit = false;
public void run(){
while (!exit) {
System.out.println("MyThread 开始运行");
}
}
}
定义了一个退出标志 exit,当 exit 为 true 时,while 循环退出,exit 的默认值为 false.在定义 exit时,使用了一个 Java 关键字 volatile,这个关键字的目的是使 exit 同步,也就是说在同一时刻只能由一个线程来修改 exit 的值。
3.Interrupt 方法结束线程
使用 interrupt()方法来中断线程有两种情况:
- 线程处于阻塞状态:如使用了 sleep,同步锁的 wait,socket 中的 receiver,accept 等方法时,
会使线程处于阻塞状态。当调用线程的 interrupt()方法时,会抛出 InterruptException 异常。
阻塞中的那个方法抛出这个异常,通过代码捕获该异常,然后 break 跳出循环状态,从而让
我们有机会结束这个线程的执行。通常很多人认为只要调用 interrupt 方法线程就会结束,实
际上是错的, 一定要先捕获 InterruptedException 异常之后通过 break 来跳出循环,才能正
常结束 run 方法。 - 线程未处于阻塞状态:使用 isInterrupted()判断线程的中断标志来退出循环。当使用interrupt()方法时,中断标志就会置 true,和使用自定义的标志来控制循环是一样的道理。
public class ThreadSafe extends Thread {
public void run() {
while (!isInterrupted()){ //非阻塞过程中通过判断中断标志来退出
try{
Thread.sleep(5*1000);//阻塞过程捕获中断异常来退出
}catch(InterruptedException e){
e.printStackTrace();
break;//捕获到异常之后,执行 break 跳出循环
} }
}
}
- stop 方法终止线程(线程不安全)
程序中可以直接使用 thread.stop()来强行终止线程,但是 stop 方法是很危险的,就象突然关
闭计算机电源,而不是按正常程序关机一样,可能会产生不可预料的结果,不安全主要是:
thread.stop()调用之后,创建子线程的线程就会抛出 ThreadDeatherror 的错误,并且会释放子
线程所持有的所有锁。一般任何进行加锁的代码块,都是为了保护数据的一致性,如果在调用
thread.stop()后导致了该线程所持有的所有锁的突然释放(不可控制),那么被保护数据就有可能呈
现不一致性,其他线程在使用这些被破坏的数据时,有可能导致一些很奇怪的应用程序错误。因
此,并不推荐使用 stop 方法来终止线程。
五、sleep 与 wait 区别(面试常问)
- 对于 sleep()方法,我们首先要知道该方法是属于 Thread 类中的。而 wait()方法,则是属于Object 类中的。
- sleep()方法导致了程序暂停执行指定的时间,让出 cpu 该其他线程,但是他的监控状态依然保持者,当指定的时间到了又会自动恢复运行状态。
- 在调用 sleep()方法的过程中,线程不会释放对象锁。
- 而当调用 wait()方法的时候,线程会放弃对象锁,进入等待此对象的等待锁定池,只有针对此对象调用 notify()方法后本线程才进入对象锁定池准备获取对象锁进入运行状态。
六、start 与 run 区别
- start()方法来启动线程,真正实现了多线程运行。这时无需等待 run 方法体代码执行完毕,
可以直接继续执行下面的代码。 - 通过调用 Thread 类的 start()方法来启动一个线程, 这时此线程是处于就绪状态, 并没有运
行。 - 方法 run()称为线程体,它包含了要执行的这个线程的内容,线程就进入了运行状态,开始运
行 run 函数当中的代码。 Run 方法运行结束, 此线程终止。然后 CPU 再调度其它线程
七、线程池核心参数和提交任务流程(面试常问)
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;
}
- corePoolSize
核心线程数,线程池中会一直存在的线程数量。最大值为Integer.MAX_VALUE即2^31-1。 - maximumPoolSize
最大线程数,线程池中最多存在的线程数量。最大值为Integer.MAX_VALUE即2^31-1。 - keepAliveTime
当线程数超过核心线程数但是超过keepAliveTime时间段后依然没有任务的线程则销毁。 - unit
keepAliveTime的时间单位。 - workQueue
任务队列,提交的任务超过最大线程数时,存放在任务队列中。
a.ArrayBlockingQueue(数组的有界阻塞队列)
ArrayBlockingQueue 在创建时必须设置大小,按FIFO排序(先进先出)。新任务进来后,会放到该队列的队尾,有界的数组可以防止资源耗尽问题。当线程池中线程数量达到corePoolSize后,再有新任务进来,则会将任务放入该队列的队尾,等待被调度。如果队列已经是满的,则创建一个新线程,如果线程数量已经达到maxPoolSize,则会执行拒绝策略。
b.LinkedBlockingQueue(链表的无界阻塞队列)
按 FIFO 排序任务,可以设置容量(有界队列),不设置容量则默认使用 Integer.Max_VALUE 作为容量 (无界队列)。创建线程池的工厂方法 Executors.newSingleThreadExecutor、Executors.newFixedThreadPool,都使用了这个队列,并且都没有设置容量(无界队列),因此可能会造成队列的任务一直累积从而内存溢出。
c.SynchronousQueue(一个不缓存任务的阻塞队列)
新任务进来时,不会缓存,而是直接被调度执行该任务,如果没有可用线程,则创建新线程,如果线程数量达到maxPoolSize,则执行拒绝策略。
d.PriorityBlockingQueue(具有优先级的无界阻塞队列)
优先级通过参数Comparator实现。
e.DelayQueue(这是一个无界阻塞延迟队列)
底层基于 PriorityBlockingQueue 实现的,队列中每个元素都有过期时间,当从队列获取元素(元素出队)时,只有已经过期的元素才会出队,而队列头部的元素是过期最快的元素。快捷工厂方法 Executors.newScheduledThreadPool 所创建的线程池使用此队列。 - threadFactory
线程的创建工厂。 - handler
拒绝策略, 线程任务队列满了之后可根据拒绝策略对任务进行相应操作。
a.AbortPolicy:丢弃任务并抛出 RejectedExecutionException 异常。(默认)
b.DiscardPolicy:丢弃任务,但是不抛出异常
c.DiscardOldestPolicy:丢弃队列最前面的任务,然后重新尝试执行任务(重复此过程) 。也就是当任务被拒绝添加时,会抛弃任务队列中最旧的任务也就是最先加入队列的,再把这个新任务从队尾添加进去,等待执行。
d.CallerRunsPolicy:谁调用,谁处理。由调用线程(即提交任务给线程池的线程)处理该任务,如果线程池已经被shutdown则直接丢弃.
2.线程池提交任务流程
excute源码如下:
public void execute(Runnable command) {
if (command == null)
throw new NullPointerException();
/*
* Proceed in 3 steps:
*
* 1. If fewer than corePoolSize threads are running, try to
* start a new thread with the given command as its first
* task. The call to addWorker atomically checks runState and
* workerCount, and so prevents false alarms that would add
* threads when it shouldn't, by returning false.
*
* 2. If a task can be successfully queued, then we still need
* to double-check whether we should have added a thread
* (because existing ones died since last checking) or that
* the pool shut down since entry into this method. So we
* recheck state and if necessary roll back the enqueuing if
* stopped, or start a new thread if there are none.
*
* 3. If we cannot queue task, then we try to add a new
* thread. If it fails, we know we are shut down or saturated
* and so reject the task.
*/
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();
if (! isRunning(recheck) && remove(command))
reject(command);
else if (workerCountOf(recheck) == 0)
addWorker(null, false);
}
else if (!addWorker(command, false))
reject(command);
}
流程图如下:
总结
以上为个人学习过程中对java的一些学习总结,如有错误,欢迎各位批评指导,如有侵权,请联系本人删除,谢谢!