查询目录
一张图记住线程的工作原理
实现runnable接口和继承thread类执行原理不同;
实现runnable接口是去创建任务的,所以实现接口后还需要new Thread(runnable)去把任务放到线程里跑;
继承thread类的话,只需要执行thread.start就可以开启线程;Thread类源码内部有个run()
源码里的init()如下:
private void init(ThreadGroup g, Runnable target, String name,
long stackSize, AccessControlContext acc) {
if (name == null) {
throw new NullPointerException("name cannot be null");
}
this.name = name.toCharArray();
Thread parent = currentThread();
SecurityManager security = System.getSecurityManager();
if (g == null) {
/* Determine if it's an applet or not */
/* If there is a security manager, ask the security manager
what to do. */
if (security != null) {
g = security.getThreadGroup();
}
/* If the security doesn't have a strong opinion of the matter
use the parent thread group. */
if (g == null) {
g = parent.getThreadGroup();
}
}
/* checkAccess regardless of whether or not threadgroup is
explicitly passed in. */
g.checkAccess();
/*
* Do we have the required permissions?
*/
if (security != null) {
if (isCCLOverridden(getClass())) {
security.checkPermission(SUBCLASS_IMPLEMENTATION_PERMISSION);
}
}
g.addUnstarted();
this.group = g;
this.daemon = parent.isDaemon();
this.priority = parent.getPriority();
if (security == null || isCCLOverridden(parent.getClass()))
this.contextClassLoader = parent.getContextClassLoader();
else
this.contextClassLoader = parent.contextClassLoader;
this.inheritedAccessControlContext =
acc != null ? acc : AccessController.getContext();
this.target = target;
setPriority(priority);
if (parent.inheritableThreadLocals != null)
this.inheritableThreadLocals =
ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);
/* Stash the specified stack size in case the VM cares */
this.stackSize = stackSize;
/* Set thread ID */
tid = nextThreadID();
}
其中 this.target = target;就是把这个任务赋值进线程类
所以说,创建线程的方式实质上,只有一种。
该如何正确使用多线程?
- 多线程目的: 充分利用cpu并发做事(多做事)
- 线程的本质:将代码送给cpu执行
- 用合适数量的卡车不断运送代码即可
- 这合适数量的线程就构成一个池
- 有任务要执行,就放入池中,池中的一个线程将把任务运送到cpu执行
线程池原理
处理流程如下:
- 首先线程池判断基本线程池是否已满?没满,创建一个工作线程来执行任务。满了,则进入下个流程。
- 线程池判断工作队列是否已满?没满,则将新提交的任务存储在工作队列里。满了,则进入下个流程。
- 最后线程池判断整个线程池是否已满?没满,则创建一个新的工作线程来执行任务,满了,则交给饱和策略来处理这个任务。
任务仓库就对应了new ThreadPool时的队列,任务放到队列里,线程从队列中获取任务(遵循队列先进先出规则),输送到cpu去执行;当所有线程已经被占满了,队列中未被分配的任务则处于等待状态;
当任务仓库中没有任务时,线程则处于等待状态,通常线程等待状态的方法有以下几种:block | wait | await | sleep,通常block居多,因为block会让出cpu,不抢占cpu
线程池工作原理
- 接收任务,放入仓库
- 工作线程从仓库去任务,执行
- 当没有任务时,线程阻塞,当有任务时唤醒线程执行
1)任务用什么表示
- Runnable,搭配Thread;
new Thread(runnable).start();
//创建任务 thread 执行runnable的run方法 运送到cpu
Runnable runnable = new Runnable() {
@Override
public void run() {
// 自己去实现的逻辑
try {
getString();
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
};
- Callable,搭配futureTask
FutureTask futureTask=new FutureTask<>(callable);
new Thread(futureTask).start();
//表示任务
Callable<String> callable=new Callable<String>() {
@Override
public String call() throws Exception {
return "kane";
}
};
FutureTask<String> futureTask=new FutureTask<>(callable);
new Thread(futureTask).start();
System.out.println("返回一个值:"+futureTask.get());
自定义线程池练习
线程池类:
public class FixedSizeThreadPool {
// 实现一个简易的线程池
/*
* 1.构成部分: 多个线程+任务仓库(泛型一定是Runnable)
*/
private List<Thread> threadList;
private BlockingQueue<Runnable> queue;
/*
* 当一个变量定义为 volatile 之后,将具备两种特性:
1.保证此变量对所有的线程的可见性,这里的“可见性”,如本文开头所述,
当一个线程修改了这个变量的值,volatile 保证了新值能立即同步到主内存,以及每次使用前立即从主内存刷新。
但普通变量做不到这点,普通变量的值在线程间传递均需要通过主内存(详见:Java内存模型)来完成。
2.禁止指令重排序优化。有volatile修饰的变量,赋值后多执行了一个“load addl $0x0, (%esp)”操作,
这个操作相当于一个内存屏障(指令重排序时不能把后面的指令重排序到内存屏障之前的位置),只有一个CPU访问内存时,
并不需要内存屏障;(什么是指令重排序:是指CPU采用了允许将多条指令不按程序规定的顺序分开发送给各相应电路单元处理)。
*
* */
private volatile boolean isWorking =true;
//处理线程池的构造方法
public FixedSizeThreadPool(int poolSize,int taskSize){
if (poolSize<=0||taskSize<=0) {
throw new IllegalArgumentException("非法参数");
}
this.queue=new LinkedBlockingQueue<>(taskSize);
//线程安全的list
this.threadList=Collections.synchronizedList(new ArrayList<>());
//初始化 线程池内部的线程
for(int i=0;i<poolSize;i++){
Worker worker=new Worker(this);
worker.start();
//将带任务的线程放到线程池中
threadList.add(worker);
}
}
// 定义在线程池中去做的事情
public static class Worker extends Thread {
// 传入线程池,获取阻塞队列
private FixedSizeThreadPool pool;
public Worker(FixedSizeThreadPool pool) {
this.pool = pool;
}
@Override
public void run() {
// 执行runnable中的run方法
// 无限执行
while (this.pool.isWorking||this.pool.queue.size()>0) {
Runnable task = null;
try {
if (this.pool.isWorking) {
//资源未关闭时:BlockingQueue的阻塞方式获取任务,拿不到任务的时候就会阻塞
task = this.pool.queue.take();
}else{
//资源已关闭,但队列仍存在任务时,不阻塞了,直接返回个特殊值
task=this.pool.queue.poll();
}
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
if (task!=null) {
//执行任务
task.run();
System.out.println("任务 "+Thread.currentThread().getName()+" 执行完毕!");
}
}
}
}
//将任务提交进来的方法
public boolean submit(Runnable task){
if (isWorking) {
//未关闭资源时可以往队列里放任务
return queue.offer(task);
}else{
return false;
}
}
//关闭资源
public void shutdown(){
isWorking=false;
//判断是否存在阻塞状态的线程
for(Thread thread:threadList){
if (thread.getState().equals(Thread.State.BLOCKED)) {
//中断线程
thread.interrupt();
}
}
}
}
使用线程池的类:
public class ThreadPoolDemo {
// static ThreadPoolExecutor executor=new ThreadPoolExecutor(200, 500, 1000, TimeUnit.MILLISECONDS, new LinkedBlockingDeque<>(200));
//使用自定义线程池
static FixedSizeThreadPool fixedSizeThreadPool=new FixedSizeThreadPool(3,6);
public static void main(String[] args) throws InterruptedException, ExecutionException {
long startTime = System.currentTimeMillis();
//创建任务 thread 执行runnable的run方法 运送到cpu
Runnable runnable = new Runnable() {
@Override
public void run() {
// 自己去实现的逻辑
try {
getString();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
};
for(int i=0;i<6;i++){
fixedSizeThreadPool.submit(runnable);
//如果这里执行sleep();自定义线程池也会抛异常
}
//执行完后结束
// executor.shutdown();
fixedSizeThreadPool.shutdown();
long endTime = System.currentTimeMillis();
System.out.println("程序的执行时间:" + (endTime - startTime));
}
public static void getString() throws InterruptedException {
Thread.sleep(100);
System.out.println("你好执行完毕!!!");
}
}
FutureTask源码:
首先,FutureTask类也是实现了RunnableFuture接口,存在它的run()
public void run() {
if (state != NEW ||
!UNSAFE.compareAndSwapObject(this, runnerOffset,
null, Thread.currentThread()))
return;
try {
Callable<V> c = callable;
if (c != null && state == NEW) {
V result;
boolean ran;
try {
//调用call方法
result = c.call();
ran = true;
} catch (Throwable ex) {
result = null;
ran = false;
setException(ex);
}
if (ran)
//把callable的结果set进RunnableFuture实体类,相当于放进Runnable实现类
set(result);
}
} finally {
// runner must be non-null until state is settled to
// prevent concurrent calls to run()
runner = null;
// state must be re-read after nulling runner to prevent
// leaked interrupts
int s = state;
if (s >= INTERRUPTING)
handlePossibleCancellationInterrupt(s);
}
}
2)仓库用什么?
BlockingQueue 阻塞队列, 线程安全
在队列为空时的获取阻塞, 在队列满时的放入阻塞。
BlockingQueue 是一个接口。方法以四种形式出现,对于不能立即满足但可能在将来某一时刻可以满足的操作,这四种形式的处理方式不同: 第一种是抛出一个异常; 第二种是返回一个特殊值(null 或 false, 具体取决于操作); 第三种是在操作可以成功前, 无限期地阻塞当前线程, 第四种是在放弃前只在给定的最大时间限制内阻塞。下表中总结了这些方法:
多线程的“灵异事件”示例
由于多线程的执行结果并不是幂等的,所以很多时候可能会由于配置问题发生一些“灵异事件”,例如我们举两个例子(伪代码):
例一:
new Thread( run(){
i++; ===> sout i ===几? ===> i<=6 123456 随机
});
解决方法: 加个Lock或者Synchronized
例二:
卖货示例,单机系统上,每卖一件数据库count +1,如果此时100个人同时购买,count最终会是几?
解决方法: Lock或者Synchronized???肯定是不行的; 可以利用消息中间件去缓冲处理
线程池的创建
我们可以通过ThreadPoolExecutor来创建一个线程池。
new ThreadPoolExecutor(corePoolSize, maximumPoolSize,keepAliveTime, milliseconds,runnableTaskQueue, threadFactory,handler);
创建一个线程池需要输入几个参数:
- corePoolSize(线程池的基本大小):当提交一个任务到线程池时,线程池会创建一个线程来执行任务,即使其他空闲的基本线程能够执行新任务也会创建线程,等到需要执行的任务数大于线程池基本大小时就不再创建。如果调用了线程池的prestartAllCoreThreads方法,线程池会提前创建并启动所有基本线程。
- runnableTaskQueue(任务队列):用于保存等待执行的任务的阻塞队列。可以选择以下几个阻塞队列。
- ArrayBlockingQueue:是一个基于数组结构的有界阻塞队列,此队列按 FIFO(先进先出)原则对元素进行排序。
- LinkedBlockingQueue:一个基于链表结构的阻塞队列,此队列按FIFO (先进先出) 排序元素,吞吐量通常要高于ArrayBlockingQueue。静态工厂方法Executors.newFixedThreadPool()使用了这个队列。
- SynchronousQueue:一个不存储元素的阻塞队列。每个插入操作必须等到另一个线程调用移除操作,否则插入操作一直处于阻塞状态,吞吐量通常要高于LinkedBlockingQueue,静态工厂方法Executors.newCachedThreadPool使用了这个队列。
- PriorityBlockingQueue:一个具有优先级得无限阻塞队列。
- maximumPoolSize(线程池最大大小):线程池允许创建的最大线程数。如果队列满了,并且已创建的线程数小于最大线程数,则线程池会再创建新的线程执行任务。值得注意的是如果使用了无界的任务队列这个参数就没什么效果。
- ThreadFactory:用于设置创建线程的工厂,可以通过线程工厂给每个创建出来的线程设置更有意义的名字,Debug和定位问题时非常又帮助。
RejectedExecutionHandler(饱和策略):当队列和线程池都满了,说明线程池处于饱和状态,那么必须采取一种策略处理提交的新任务。这个策略默认情况下是AbortPolicy,表示无法处理新任务时抛出异常。以下是JDK1.5提供的四种策略。n AbortPolicy:直接抛出异常。
- CallerRunsPolicy:只用调用者所在线程来运行任务。
- DiscardOldestPolicy:丢弃队列里最近的一个任务,并执行当前任务。
- DiscardPolicy:不处理,丢弃掉。
- 当然也可以根据应用场景需要来实现RejectedExecutionHandler接口自定义策略。如记录日志或持久化不能处理的任务。
- keepAliveTime(线程活动保持时间):线程池的工作线程空闲后,保持存活的时间。所以如果任务很多,并且每个任务执行的时间比较短,可以调大这个时间,提高线程的利用率。
- TimeUnit(线程活动保持时间的单位):可选的单位有天(DAYS),小时(HOURS),分钟(MINUTES),毫秒(MILLISECONDS),微秒(MICROSECONDS, 千分之一毫秒)和毫微秒(NANOSECONDS, 千分之一微秒)。