线程池使用规则

Java的线程池的好处

进程-线程的简单介绍

  • 进程
    进程是计算机中的程序关于某数据集合的一次运行活动,是系统进行资源分配的调度的基本单位,是操作系统结构的基础。简单来讲:进程是指运行中的应用程序,进程是一个实体,每一个进程都有它自己的地址空间。例如我们点击了QQ,就启动了一个进程,操作系统就会为这个进程分配独立的地址空间,当我们又点击浏览器,这样又启动了一个进程,操作系统将为新的进程分配新的独立的地址空间。

  • 线程
    线程是操作系统能够进行运算调度的最小单位,被包含在进程之中,是进程中的实际运作单位。一个进程至少有一个线程。一条线程指的是进程中一个单一顺序的控制流,一个进程中可以并发多个线程,每条线程并行执行不同的任务。注意:线程自己不拥有系统资源,只拥有一点在运行中必不可少的资源,但它可与同属于一个进程的其他线程共享进程所拥有的全部资源,线程有就绪,阻塞,运行三种基本状态。

另外:在Unix System和SunOS中也被称为轻量进程,但轻量进程更多指内核线程,而把用户线程称为线程

java的线程池是什么,有哪些类型,作用分别是什么

线程池是一种多线程处理形式,处理过程中将任务添加队列,然后在创建线程后自动启动这些任务,每个线程都使用默认的堆栈大小,以默认的优先级运行,并处在多线程单元中,如果某个线程在托管代码中空闲(如正在等待某个事件),则线程池将插入另一个辅助线程来使所有处理器保持繁忙。如果所有线程池都始终保持繁忙,但队列中包含挂起的工作,则线程池将在一段时间之后创建另一个辅助线程。但线程的数目永远不会超过最大值。超过最大值的其他线程可以排队,但它们要等到其他线程完成后才启动。

java里面的线程池的顶级接口Executor,Executor并不是一个线程池,而只是一个执行线程的工具,而真正的线程池是ExecutorService

java中的有哪些线程池?

  1. newCachedThreadPool:创建一个可缓存线程池
    • 线程数量不定,最大线程数为Integer.MAX_VALUE
    • 如果线程池长度超过处理需要,可灵活回收空闲线程,若无需回收,则新建线程
    • 空闲线程都有超时限制:60s
  2. newFixedThreadPool: 创建一个定长线程池
    • 创建一个指定工作线程数量的线程池
    • 空闲状态时,它们并不会被回收
    • 只有核心线程并且这些核心线程不会被回收,能够更快速的响应外界请求
  3. newScheduledThreadPool: 创建一个定长线程池
    • 核心线程数量是固定的,而非核心线程数是没有限制
    • 非核心线程闲置时会被立即回收
    • 主要用于执行定时任务和具有固定周期的重复任务
  4. newSingleThreadExecutor: 创建一个单线程化的线程池
    • 内部只有一个核心线程
    • 无界队列方式来执行该线程,这使得这些任务之间不需要处理线程同步的问题
    • 确保所有的任务都在同一个线程中按顺序中执行在任意给定的时间不会有多个线程是活动的

newSingleThreadExecutor线程池与用户单独创建一个线程的不同

有什么理由使用单线程而不使用singleThreadExecutor?

简单这样说吧。一个线程的创建方式,可以自己通过直接new Thread的方式创建并启动。也可能通过new ThreadPool的方式间接创建。具体采用哪种,跟实际应用场景有关。

如对于满足以下场景可以直接用new Thread的方式。而不需要采用ThreadPool:

  1. 线程执行任务内容已确定
  2. 线程不需要频繁创建和销毁
  3. 线程不需要重用(跟第2点有点类似)

场景举例:某项目中,需要提供一个定时记录用户在线数量的功能,记录操作异步独立进行。抽象代码如下:

/**
 * @author zhuangqing
 */
 
public class Test {
    public static int userNum = 10;
    public static void main(String[] args) {
        new Thread(new Runnable() {
            @Override
            public void run() {
                while(true) {
                    System.out.println("记录用户数:" + userNum);
                    try {
                        Thread.sleep(5000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        }).start();
 
        //do other something.....
 
    }
}

该线程会一直存在异步独立运行着。这个场景下功能比较简单,是没必要采用singleThreadExecutor的。singleThreadExecutor是通过创建一个线程池来管理内部做了大量处理,如自定义很多处理类、建立容器接收任务、处理任务时的加锁解锁操作、线程的状态判断等等。采用后者相关于做了很多没必要的工作。且内存占用也比前者多

现在在换一种场景:某项目中,需要提供当时用户登录或下线时,记录用户在线数量的功能。记录操作异步独立进行。上述代码可以调整为:

/**
 * @author zhuangqing
 */
 
public class Test {
    public static int userNum = 10;
    public static void main(String[] args) {
        //do other something.....
        login();
        //do other something.....
        logout();
    }
 
    private static void logout() {
        new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println("记录用户数:" + userNum);
            }
        }).start();
    }
 
    private static void login() {
        new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println("记录用户数:" + userNum);
            }
        }).start();
    }
}

这种场景下,你会发现该实现操作会出现大量线程的频繁创建的销毁。所以我们肯定要做一些优化:使用静态代码块启动线程(类加载时自动执行)、线程并发安全问题没处理(ArrayList)、任务执行异常将导致线程终止等等,后续代码你会继续优化。最终随着你的优化,你会发现,最终代码相当于是你自己写了一个类似ThreadPool的管理类。

所以二者如何使用,需要先了解二者的所拥有的功能。再实际场景中再去做选择。

下面一一分析:

  1. newCachedThreadPool,是一种线程数量不定的线程池,并且其最大线程数为Integer.MAX_VALUE,这个数是很大的,一个可缓存线程池,如果线程池长度超过处理需要,可灵活回收空闲线程,若无需回收,则新建线程。但是线程池中的空闲线程都有超时限制,这个超时时长是60秒,超过60秒闲置线程就会被回收。调用execute重用以前构造的线程(如果线程可用)。这类线程池比较适合执行大量的耗时较少的任务,当整个线程池都处于闲置状态时,线程池中的线程都会超时被停止

实例代码:

public class PoolExecutorTest {

	public static void main(String[] args) {
		// TODO Auto-generated method stub
		
		ExecutorService mCachelThreadPool = Executors.newCachedThreadPool();
		
		for(int i = 0;i < 7;i++ ) {
			final int index = i;
			try {
				Thread.sleep(2000);
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
			mCachelThreadPool.execute(new Runnable() {
				
				@Override
				public void run() {
					System.out.println("第" +index +"个线程" +Thread.currentThread().getName()); 
				}
			});
			
		}
		
 
	}
 
}

输出结果:

在这里插入图片描述

从结果可以看到,执行第二个任务的时候第一个任务已经完成,会复用执行第一个任务的线程不用每次新建线程

  1. newFixedThreadPool ,创建一个指定工作线程数量的线程池,每当提交一个任务就创建一个工作线程,当线程 处于空闲状态时,它们并不会被回收除非线程池被关闭了,如果工作线程数量达到线程池初始的最大数,则将提交的任务存入到线程池队列(没有大小限制)中。由于newFixedThreadPool只有核心线程并且这些核心线程不会被回收,这样它更加快速底相应外界的请求

实例代码:

public class PoolExecutorTest {
 
	public static void main(String[] args) throws InterruptedException {
		// TODO Auto-generated method stub
		//设置最大线程数5个
		ExecutorService mFixedThreadPool = Executors.newFixedThreadPool(5);
		
		for(int i = 0;i < 7;i++ ) {
			final int index = i;
			mFixedThreadPool.execute(new Runnable() {
				
				@Override
				public void run() {
					System.out.println("时间是:"+System.currentTimeMillis()+"第" +index +"个线程" +Thread.currentThread().getName()); 
					try {
						Thread.sleep(2000);
					} catch (InterruptedException e) {
						e.printStackTrace();
					}	
				 }
			});
			
		}
		
 
	}
 
}

输出结果:

在这里插入图片描述

由于设置最大线程是5,所以当执行完这5个线程后,等待两秒后,在执行后面2个线程。

  1. newScheduledThreadPool, 创建一个线程池,它的核心线程数量是固定的,而非核心线程数是没有限制的,并且当非核心线程闲置时会被立即回收,它可安排为:给定延迟运行命令或者定期地执行。这类线程池主要用于执行定时任务和具有固定周期的重复任务

延迟执行实例代码:

public class PoolExecutorTest {
 
	public static void main(String[] args) throws InterruptedException {
		// TODO Auto-generated method stub
		
		//设置池中核心数量是2
        ScheduledExecutorService mScheduledThreadPool = Executors.newScheduledThreadPool(2);  
        System.out.println("现在的时间:"+System.currentTimeMillis());
        mScheduledThreadPool.schedule(new Runnable() {
			
			@Override
			public void run() {
				// TODO Auto-generated method stub
				System.out.println("现在的时间:"+System.currentTimeMillis());
				
			}
		}, 4, TimeUnit.SECONDS);//这里设置延迟4秒执行
		
 
	}
 
}

执行的结果如下:

在这里插入图片描述

误差可以忽略,实际结果确实延迟了4秒执行。

定期执行示例代码

public class PoolExecutorTest {
 
	public static void main(String[] args) throws InterruptedException {
		// TODO Auto-generated method stub
		
		//设置池中核心数量是2
        ScheduledExecutorService mScheduledThreadPool = Executors.newScheduledThreadPool(2);  
        System.out.println("现在的时间:"+System.currentTimeMillis());
        mScheduledThreadPool.scheduleAtFixedRate(new Runnable() {
			
			@Override
			public void run() {
				// TODO Auto-generated method stub
				System.out.println("现在的时间:"+System.currentTimeMillis());
				
			}
		}, 2, 3,TimeUnit.SECONDS);//这里设置延迟2秒后每3秒执行一次
		
 
	}
 
}

执行的结果如下:

可发现确实延迟2秒后每隔3秒后就会执行一次,程序不退出就一直执行下去。

  1. newSingleThreadExecutor,这类线程池内部只有一个核心线程,以无界队列方式来执行该线程,这使得这些任务之间不需要处理线程同步的问题,它确保所有的任务都在同一个线程中按顺序中执行,并且可以在任意给定的时间不会有多个线程是活动的

示例代码:

public class PoolExecutorTest {
 
	public static void main(String[] args) throws InterruptedException {
		// TODO Auto-generated method stub
		
		ExecutorService mSingleThreadPool = Executors.newSingleThreadExecutor();     
        for(int i = 0;i < 7;i++) {
        	final int number = i;
        	mSingleThreadPool.execute(new Runnable() {
				
				@Override
				public void run() {
					System.out.println("现在的时间:"+System.currentTimeMillis()+"第"+number+"个线程");
	                try {
						Thread.sleep(2000);
					} catch (InterruptedException e) {
						e.printStackTrace();
					}
					
				}
			});
        	
        }
 
	}
 
}

执行的结果如下:

在这里插入图片描述

可发现是有顺序地去执行上面6个线程。

1. 使用线程池的优点

  1. 降低资源消耗重用线程池的线程避免因为线程的创建和销毁锁带来的性能开销

  2. 提高响应速度:有效控制线程池的最大并发数避免大量的线程之间因抢占系统资源而阻塞

  3. 提高线程的可管理性:能够对线程进行简单的管理,并提供一下特定的操作如:可以提供定时定期单线程并发数控制等功能

2. 使用线程池的风险

虽然线程池是构建多线程应用程序的强大机制,但使用它并不是没有风险的。用线程池构建的应用程序容易遭受任何其它多线程应用程序容易遭受的所有并发风险诸如同步错误和死锁,它还容易遭受特定于线程池的少数其它风险诸如与池有关的死锁、资源不足和线程泄漏

2.1 死锁

任何多线程应用程序都有死锁风险。当一组进程或线程中的每一个都在等待一个只有该组中另一个进程才能引起的事件时,我们就说这组进程或线程 死锁了。死锁的最简单情形是:线程 A 持有对象 X 的独占锁,并且在等待对象 Y 的锁,而线程 B 持有对象 Y 的独占锁,却在等待对象 X 的锁。除非有某种方法来打破对锁的等待(Java 锁定不支持这种方法),否则死锁的线程将永远等下去。

虽然任何多线程程序中都有死锁的风险,但线程池却引入了另一种死锁可能,在那种情况下,所有池线程都在执行已阻塞的等待队列中另一任务的执行结果的任务,但这一任务却因为没有未被占用的线程而不能运行。当线程池被用来实现涉及许多交互对象的模拟,被模拟的对象可以相互发送查询,这些查询接下来作为排队的任务执行,查询对象又同步等待着响应时,会发生这种情况。

2.2 资源不足

线程池的一个优点在于:相对于其它替代调度机制(有些我们已经讨论过)而言,它们通常执行得很好。但只有恰当地调整了线程池大小时才是这样的。线程消耗包括内存和其它系统资源在内的大量资源。除了 Thread 对象所需的内存之外,每个线程都需要两个可能很大的执行调用堆栈。除此以外,JVM可能会为每个 Java 线程创建一个本机线程,这些本机线程将消耗额外的系统资源。最后,虽然线程之间切换的调度开销很小,但如果有很多线程,环境切换也可能严重地影响程序的性能。

如果线程池太大,那么被那些线程消耗的资源可能严重地影响系统性能。在线程之间进行切换将会浪费时间,而且使用超出比您实际需要的线程可能会引起资源匮乏问题,因为池线程正在消耗一些资源,而这些资源可能会被其它任务更有效地利用。除了线程自身所使用的资源以外,服务请求时所做的工作可能需要其它资源,例如 JDBC 连接、套接字或文件。这些也都是有限资源,有太多的并发请求也可能引起失效,例如不能分配 JDBC 连接。

2.3 并发错误

线程池和其它排队机制依靠使用 wait() 和 notify() 方法,这两个方法都难于使用。如果编码不正确,那么可能丢失通知,导致线程保持空闲状态,尽管队列中有工作要处理。使用这些方法时,必须格外小心。而最好使用现有的、已经知道能工作的实现,例如 util.concurrent 包

2.4 线程泄漏

各种类型的线程池中一个严重的风险线程泄漏,当从池中除去一个线程以执行一项任务,而在任务完成后该线程却没有返回池时,会发生这种情况。发生线程泄漏的一种情形出现在任务抛出一个 RuntimeException 或一个 Error 时。如果池类没有捕捉到它们,那么线程只会退出而线程池的大小将会永久减少一个。当这种情况发生的次数足够多时,线程池最终就为空,而且系统将停止,因为没有可用的线程来处理任务。

有些任务可能会永远等待某些资源或来自用户的输入,而这些资源又不能保证变得可用,用户可能也已经回家了,诸如此类的任务会永久停止,而这些停止的任务也会引起和线程泄漏同样的问题。如果某个线程被这样一个任务永久地消耗着,那么它实际上就被从池除去了。对于这样的任务,应该要么只给予它们自己的线程,要么只让它们等待有限的时间

2.5 请求过载

仅仅是请求就压垮了服务器,这种情况是可能的。在这种情形下,我们可能不想将每个到来的请求都排队到我们的工作队列,因为排在队列中等待执行的任务可能会消耗太多的系统资源并引起资源缺乏。在这种情形下决定如何做取决于您自己;在某些情况下,您可以简单地抛弃请求,依靠更高级别的协议稍后重试请求,您也可以用一个指出服务器暂时很忙的响应来拒绝请求

线程池满了,往线程池里提交任务会发生什么样的情况,具体分几种情况

  • 如果你使用的LinkedBlockingQueue(阻塞队列),也就是无界队列的话,没关系,继续添加任务到阻塞队列中等待执行,因为LinkedBlockingQueue可以近乎认为是一个无穷大的队列,可以无限存放任务;如果你使用的是有界队列比方说ArrayBlockingQueue的话,任务首先会被添加到ArrayBlockingQueue中,ArrayBlockingQueue满了,则会使用拒绝策略RejectedExecutionHandler处理满了的任务,默认是AbortPolicy
  • 线程池的饱和策略:当阻塞队列满了,且没有空闲的工作线程,如果继续提交任务,必须采取一种策略处理该任务,线程池提供了4种策略多线程:当你提交任务时,线程队列已经满了,这时会发生什么?

    ThreadPoolExecutor.AbortPolicy:丢弃任务抛出RejectedExecutionException异常。注:默认策略!!!!!!
    ThreadPoolExecutor.DiscardPolicy:也是丢弃任务,但是不抛出异常
    ThreadPoolExecutor.DiscardOldestPolicy丢弃队列最前面的任务,然后重新尝试执行任务(重复此过程)
    ThreadPoolExecutor.CallerRunsPolicy由调用线程处理该任务

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值