一、创建线程的方式
1、继承Thread
public class MyThread extends Thread {
@Override
public void run() {
for (int i = 0; i < 30; i++) {
System.out.println("run:" + i);
}
}
}
public class Test {
public static void main(String[] args) {
MyThread myThread = new MyThread();
myThread.start();
for (int i = 0; i < 30; i++) {
System.out.println(Thread.currentThread().getName());
}
}
}
2、实现接口Runable
public class RunnableImpl implements Runnable {
@Override
public void run() {
for (int i = 0; i < 30; i++) {
System.out.println(Thread.currentThread().getName() + ":" + i);
}
}
}
public class Test {
public static void main(String[] args) {
Thread thread = new Thread(new RunnableImpl());
thread.start();
for (int i = 0; i < 30; i++) {
System.out.println(Thread.currentThread().getName() + ":" + i);
}
}
}
3、实现接口Callable
Callalble接口支持返回执行结果,需要调用FutureTask.get()得到,此方法会阻塞主进程的继续往下执行,如果不调用不会阻塞。
public class Test01 {
public static void main(String[] args) throws ExecutionException, InterruptedException {
TestCallable testCallable = new TestCallable();
// FutureTask是Callable最重要的类,run方法在这里,它实现了runable
FutureTask futureTask = new FutureTask(testCallable);
Thread thread = new Thread(futureTask,"aaa");
// 注意:如果用同一个futureTask,只会使用一个线程
// Thread thread2 = new Thread(futureTask,"BBB");
thread.start();
// 待其计算完成后,再执行后续的流程
while (!futureTask.isDone()){
}
System.out.println(futureTask.isDone());
System.out.println(futureTask.get());
System.out.println(futureTask.isDone());
}
}
class TestCallable implements Callable<Integer> {
@Override
public Integer call() throws Exception {
return new Random().nextInt(10);
}
}
二、创建线程池的方式
线程生命周期:新生、就绪、运行、死亡 �线程池是为了节省新生、就绪、死亡的时间
1、固定大小线程池
不允许用,底层用的LinkBlockingQueue()无限长度,Integer.MAX_VALUE,容易堆积大量请求,造成OOM。
Executors.newFixedThreadPool(1);
以下代码演示三个三个任务打印:
1、制造阻塞2000ms,让所有线程启用并全部放入list,此时有三个线程是核心线程(它们已经开始执行,即重写的方法call()已经开始执行),另外的47个线程在阻塞队列(它们还没开始执行,及重写的方法call() 还没执行)。
2、当线程全部启动之后,进入List.forEach循环,此时future.get()核心线程(阻塞2000ms中),待核心线程阻塞结束,便会执行并输出,此时会循环3次,输出核心线程中的三个线程。
3、接着阻塞队列中的47个会出来3个进入核心线程,并继续阻塞2000ms,然后接着输出,依次循环至全部输出结束。
public class callableTest02 {
public static void main(String[] args) throws ExecutionException, InterruptedException {
ExecutorService executorService = Executors.newFixedThreadPool(3);
List<Future> list = new ArrayList<>();
// 先启动所有线程,然后在再执行
for (int i = 0; i < 50; i++) {
Future<String> future = executorService.submit(new Callable<String>() {
@Override
public String call() throws Exception {
// 制造阻塞
Thread.sleep(2000);
return Thread.currentThread().getName();
}
});
list.add(future);
}
// 将执行的结果放到list里面,通过list打印,及每次执行线程池中的三个核心线程
list.forEach(future -> {
try {
System.out.println(future.get());
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}
});
executorService.shutdown();
}
}
输出:
pool-1-thread-1
pool-1-thread-2
pool-1-thread-3
2、单线程池
需要保证顺序执行各个任务的场景
不允许用,底层用的LinkBlockingQueue()无限长度,Integer.MAX_VALUE,容易堆积大量请求,造成OOM。
Executors.newSingleThreadExecutor()
public class newSingleThreadExecutor {
public static void main(String[] args) {
ExecutorService executorService = Executors.newSingleThreadExecutor();
for (int i = 0; i < 100; i++) {
executorService.execute(() -> System.out.println(Thread.currentThread().getName()));
}
executorService.shutdown();
}
}
3、周期性线程池
该线程池可以执行定时任务
不允许使用,底层创建大量线程,Integer.MAX_VALUE,容易堆积大量请求,造成OOM。
Executors.newScheduledThreadPool(3);
public class newScheduledThreadPoolTest {
public static void main(String[] args) {
ScheduledExecutorService executorService = Executors.newScheduledThreadPool(3);
for (int i = 0; i < 100; i++) {
executorService.schedule(new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName());
}
// 三秒后执行
}, 3, TimeUnit.SECONDS);
}
executorService.shutdown();
}
}
4、缓存线程池
无限创建线程( Integer.MAX_VALUE)
不允许使用,底层创建大量线程,Integer.MAX_VALUE,容易堆积大量请求,造成OOM。
Executors.newCachedThreadPool()
public class newCachedThreadPoolTest {
public static void main(String[] args) {
ExecutorService executorService = Executors.newCachedThreadPool();
for (int i = 0; i < 1; i++) {
executorService.execute(() -> System.out.println(Thread.currentThread().getName()));
}
executorService.shutdown();
}
}
三、自定义线程池
1、线程池7大参数
public ThreadPoolExecutor(int corePoolSize, //常驻核心线程(必定干活的)
int maximumPoolSize, //线程池能够容纳的最大线程数(包含核心线程)
long keepAliveTime, //多余的空闲线程(超过corePoolSize数量的线程)的存活时间
TimeUnit unit, // 时间单位
BlockingQueue<Runnable> workQueue, //等待区
ThreadFactory threadFactory, // 线程工厂,用于创建线程的一些属性,例如名称
RejectedExecutionHandler handler)
public class ThreadPool02 {
public static void main(String[] args) {
ExecutorService executorService = new ThreadPoolExecutor(
// corePoolSize
5,
// maximumPoolSize
5,
// keepAliveTime
60,
// TimeUnit unit
TimeUnit.SECONDS,
new LinkedBlockingQueue<>(5),
new ThreadFactoryBuilder().setNameFormat("itemPushThread-%d").setDaemon(true).build(),
new ThreadPoolExecutor.DiscardPolicy()
);
}
}
在Java中有两类线程:User Thread(用户线程)、Daemon Thread(守护线程) 用个比较通俗的比如,任何一个守护线程都是整个JVM中所有非守护线程的保姆: 只要当前JVM实例中尚存在任何一个非守护线程没有结束,守护线程就全部工作;只有当最后一个非守护线程结束时,守护线程随着JVM一同结束工作。
Daemon的作用是为其他线程的运行提供便利服务,守护线程最典型的应用就是 GC (垃圾回收器),它就是一个很称职的守护者。 User和Daemon两者几乎没有区别,唯一的不同之处就在于虚拟机的离开:如果 User Thread已经全部退出运行了,只剩下Daemon Thread存在了,虚拟机也就退出了。 因为没有了被守护者,Daemon也就没有工作可做了,也就没有继续运行程序的必要了。
2、线程池运行原理
1.在创建了线程池后,等待提交过来的任务请求。
2.当调用execute()方法添加一个请求任务时,线程池会做如下判断:
2.1如果正在运行的线程数量小于corePoolSize,那么马上创建线程运行这个任务;
2.2如果正在运行的线程数量大于或等于corePoolSize,那么将这个任务放入队列;
2.3如果这时候队列满了且正在运行的线程数量还小于maximumPoolSize,那么还是要创建非核心线程立刻运行这个任务; 2.4如果队列满了且正在运行的线程数量大于或等于maximumPoolSize,那么线程池会启动饱和拒绝策略来执行。
3.当一个线程完成任务时,它会从队列中取下一个任务来执行。
4.当一个线程无事可做超过一定的时间(keepAliveTime)时,线程池会判断:如果当前运行的线程数大于corePoolSize,那么多出来的线程就被停掉。所以线程池的所有任务完成后它最终会收缩到corePoolSize的大小。
四、拒绝策略
1、AbortPolicy
这种拒绝策略在拒绝任务时,会直接抛出一个类型为 RejectedExecutionException 的 RuntimeException,让你感知到任务被拒绝了,于是你便可以根据业务逻辑选择重试或者放弃提交等策略。
2、DiscardPolicy
这种拒绝策略正如它的名字所描述的一样,当新任务被提交后直接被丢弃掉,也不会给你任何的通知,相对而言存在一定的风险,因为我们提交的时候根本不知道这个任务会被丢弃,可能造成数据丢失。
3、DiscardOldestPolicy
如果线程池没被关闭且没有能力执行,则会丢弃任务队列中的头结点,通常是存活时间最长的任务,这种策略与第二种不同之处在于它丢弃的不是最新提交的,而是队列中存活时间最长的,这样就可以腾出空间给新提交的任务,但同理它也存在一定的数据丢失风险。
4、CallerRunsPolicy
相对而言它就比较完善了,当有新任务提交后,如果线程池没被关闭且没有能力执行,则把这个任务交于提交任务的线程执行,也就是谁提交任务,谁就负责执行任务。这样做主要有两点好处:
4.1 第一点新提交的任务不会被丢弃,这样也就不会造成业务损失。
4.2 第二点好处是,由于谁提交任务谁就要负责执行任务,这样提交任务的线程就得负责执行任务,而执行任务又是比较耗时的,在这段期间,提交任务的线程被占用,也就不会再提交新的任务,减缓了任务提交的速度,相当于是一个负反馈。在此期间,线程池中的线程也可以充分利用这段时间来执行掉一部分任务,腾出一定的空间,相当于是给了线程池一定的缓冲期。
五、合理配合线程池
1、CPU密集型:该任务需要大量运算,而且没有阻塞,CPU一直在全速运行(例如一个for循环无限跑)
CPU密集型任务只有在真正的多核CPU上才能得到加速(通过多线程),如果是单核CPU上,无论你开几个模拟的多线程,该任务都不能得到加速,因为CPU总的运算能力就那么多。尽可能少的线程数量:一般公式:CPU核数+1个线程 的线程池,即8核CPU,可以设置9个线程
2、IO密集型:不是一直在执行任务,例如(不停的去数据库、redis拿数据,存数据)
方案一:应配置尽可能多的线程,如CPU核数*2
方案二:CPU核数/1-阻塞系数 阻塞系数在0.8-0.9之间,比如8核CPU:8/(1-0.9)=80个线程数