线程的基础概念
程序:为了完成特定任务,用某种语言编写的指令集 合,是一个静态代码
进程:具有独立功能的程序的运行过程,动态概念,是系统进行资源分配和调用的一个独立单位
线程:轻量级进程,是进程中一个独立的执行线索,是 CPU 调度和分配的基本单位,线程基本上不具备系统资源
一、进程的概念
进程是操作系统结构的基础;是一个正在执行的程序;计算机中正在运行的程序实例;可以分配给处理器并由处理器执行的一个实体;由单一顺序的执行显示,一个当前状态和一组相关的系统资源所描述的活动单元。简单的说,当我们启动一个应用程序,就会有个进程。可以通过任务管理器查看到当前系统的进程有哪些。系统给进程分配了独立的内存空间
二、线程(Tread)
线程,有时被称为轻量级进程(Lightweight Process,LWP),是程序执行流的最小单元。一个标准的线程由线程ID,当前指令指针(PC),寄存器集合和堆栈组成。另外,线程是进程中的一个实体,是被系统独立调度和分派的基本单位,线程自己不拥有系统资源,只拥有一点在运行中必不可少的资源,但它可与同属一个进程的其它线程共享进程所拥有的全部资源。一个线程可以创建和撤消另一个线程,同一进程中的多个线程之间可以并发执行。由于线程之间的相互制约,致使线程在运行中呈现出间断性。
进程的启动方法
ProcessBuilder pb=new ProcessBuilder(“cmd”,"/c",“ipconfig/all”);
Process p=pb.start(); 创建、就绪、运行、阻塞、终止
Process p=Runtime.getRuntime().exec(“cmd”,"/c",“ipconfig/all”);
僵尸进程和孤儿进程
基本概念
我们知道在正常情况下,子进程是通过父进程创建的,子进程在创建新的进程。子进程的结束和父进程的运行是一个异步过程,即父进程永远无法预测子进程 到底什么时候结束。 当一个 进程完成它的工作终止之后,它的父进程需要调用wait()或者waitpid()系统调用取得子进程的终止状态。
孤儿进程:一个父进程退出,而它的一个或多个子进程还在运行,那么那些子进程将成为孤儿进程。孤儿进程将被init进程(进程号为1)所收养,并由init进程对它们完成状态收集工作。
僵尸进程:一个进程使用fork创建子进程,如果子进程退出,而父进程并没有调用wait或waitpid获取子进程的状态信息,那么子进程的进程描述符仍然保存在系统中。这种进程称之为僵死进程。
换一句话说就是例如有个进程,它定期的产 生一个子进程,这个子进程需要做的事情很少,做完它该做的事情之后就退出了,因此这个子进程的生命周期很短,但是,父进程只管生成新的子进程,至于子进程 退出之后的事情,则一概不闻不问,这样,系统运行上一段时间之后,系统中就会存在很多的僵死进程,倘若用ps命令查看的话,就会看到很多状态为Z的进程。
僵尸进程的解决方案
就是把产生大 量僵死进程的那个元凶枪毙掉(也就是通过kill发送SIGTERM或者SIGKILL信号啦)。枪毙了元凶进程之后,它产生的僵死进程就变成了孤儿进 程,这些孤儿进程会被init进程接管,init进程会wait()这些孤儿进程,释放它们占用的系统进程表中的资源,这样,这些已经僵死的孤儿进程 就能瞑目而去了。
并行和并发的基础概念
并行(parallel):指在同一时刻,有多条指令在多个处理器上同时执行。所以无论从微观还是从宏观来看,二者都是一起执行的。
并发(concurrency):指在同一时刻只能有一条指令执行,但多个进程指令被快速的轮换执行,使得在宏观上具有多个进程同时执行的效果,但在微观上并不是同时执行的,只是把时间分成若干段,使多个进程快速交替的执行。
线程的 4种创建方式
一,继承Thread
通过继承Thread类来创建并启动多线程的一般步骤如下
1】d定义Thread类的子类,并重写该类的run()方法,该方法的方法体就是线程需要完成的任务,run()方法也称为线程执行体。
2】创建Thread子类的实例,也就是创建了线程对象
3】启动线程,即调用线程的start()方法
代码实例
public class MyThread extends Thread{//继承Thread类
public void run(){undefined
//重写run方法
}
}
public class Main {undefined
public static void main(String[] args){undefined
new MyThread().start();//创建并启动线程
}
}
2,实现Runnable接口
通过实现Runnable接口创建并启动线程一般步骤如下:
1】定义Runnable接口的实现类,一样要重写run()方法,这个run()方法和Thread中的run()方法一样是线程的执行体
2】创建Runnable实现类的实例,并用这个实例作为Thread的target来创建Thread对象,这个Thread对象才是真正的线程对象
3】第三部依然是通过调用线程对象的start()方法来启动线程
代码实例:
public class MyThread2 implements Runnable {//实现Runnable接口
public void run(){undefined
//重写run方法
}
}
public class Main {undefined
public static void main(String[] args){undefined
//创建并启动线程
MyThread2 myThread=new MyThread2();
Thread thread=new Thread(myThread);
thread().start();
//或者 new Thread(new MyThread2()).start();
}
}
3,Callable接口和Future接口
和Runnable接口不一样,Callable接口提供了一个call()方法作为线程执行体,call()方法比run()方法功能要强大。
call()方法可以有返回值
call()方法可以声明抛出异常
Java5提供了Future接口来代表Callable接口里call()方法的返回值,并且为Future接口提供了一个实现类FutureTask,这个实现类既实现了Future接口,还实现了Runnable接口,因此可以作为Thread类的target。在Future接口里定义了几个公共方法来控制它关联的Callable任务。
boolean cancel(boolean mayInterruptIfRunning):视图取消该Future里面关联的Callable任务
V get():返回Callable里call()方法的返回值,调用这个方法会导致程序阻塞,必须等到子线程结束后才会得到返回值
V get(long timeout,TimeUnit unit):返回Callable里call()方法的返回值,最多阻塞timeout时间,经过指定时间没有返回抛出TimeoutException
boolean isDone():若Callable任务完成,返回True
boolean isCancelled():如果在Callable任务正常完成前被取消,返回True
介绍了相关的概念之后,创建并启动有返回值的线程的步骤如下:
1】创建Callable接口的实现类,并实现call()方法,然后创建该实现类的实例(从java8开始可以直接使用Lambda表达式创建Callable对象)。
2】使用FutureTask类来包装Callable对象,该FutureTask对象封装了Callable对象的call()方法的返回值
3】使用FutureTask对象作为Thread对象的target创建并启动线程(因为FutureTask实现了Runnable接口)
4】调用FutureTask对象的get()方法来获得子线程执行结束后的返回值
public static class MyThread3 implements Callable{
@Override
public Object call() throws Exception {
return 5;
}
}
public class MyCallable implements Callable<Integer> {
int begin=0;
int end=0;
public MyCallable(int begin, int end){
this.begin=begin;
this.end=end;
}
public Integer call() throws Exception{
int res=0;
for(int i=begin; i<=end; i++){
res+=i;
}
System.out.println(Thread.currentThread()+":"+res);
return res;
}
}
FutureTask[] arr=new FutureTask[10];
for(int i=0;i<10;i++){
int begin=i*100+1;
int end=(i+1)*100;
arr[i]=new FutureTask<>(new MyCallable(begin,end));
Thread t=new Thread(arr[i]);
t.start();
}
//获取最终计算结果
int res=0;
for(int i=0;i<arr.length;i++)
res+=((Integer)arr[i].get()); //通过FutureTask提供的get方法获取Callable实现
类中call方法的执行结果
System.out.println("1+2+3+...+1000="+res);
4,线程池
- 什么是线程池
Java中的线程池是运用场景最多的并发框架,几乎所有需要异步或并发执行任务的程序
都可以使用线程池。在开发过程中,合理地使用线程池能够带来3个好处。
第一:降低资源消耗。通过重复利用已创建的线程降低线程创建和销毁造成的消耗。
第二:提高响应速度。当任务到达时,任务可以不需要等到线程创建就能立即执行。
第三:提高线程的可管理性。线程是稀缺资源,如果无限制地创建,不仅会消耗系统资源,
还会降低系统的稳定性,使用线程池可以进行统一分配、调优和监控。但是,要做到合理利用
线程池,必须对其实现原理了如指掌。
2.线程池作用
线程池是为突然大量爆发的线程设计的,通过有限的几个固定线程为大量的操作服务,减少了创建和销毁线程所需的时间,从而提高效率。
如果一个线程的时间非常长,就没必要用线程池了(不是不能作长时间操作,而是不宜。),况且我们还不能控制线程池中线程的开始、挂起、和中止。
3.线程池四种创建方式
Java通过Executors(jdk1.5并发包)提供四种线程池,分别为:
newCachedThreadPool
创建一个可缓存线程池,如果线程池长度超过处理需要,可灵活回收空闲线程,若无可回收,则新建线程。
public class Test1 {
public static void main(String[] args) {
//1.创建可缓存的线程池,可重复利用
ExecutorService newExecutorService = Executors.newCachedThreadPool();
//创建了10个线程
for (int i = 0; i < 10; i++) {
int temp = i;
newExecutorService.execute(new Runnable() {
@Override
public void run() {
System.out.println("threadName;"+Thread.currentThread().getName()+",i"+temp);
}
});
}
}
}
newFixedThreadPool
创建一个定长线程池,可控制线程最大并发数,超出的线程会在队列中等待。
newScheduledThreadPool
public class Test1 {
public static void main(String[] args) {
//1.创建可定时线程池
ScheduledExecutorService newScheduledThreadPool = Executors.newScheduledThreadPool(5);
for (int i = 0; i < 10; i++) {
final int temp = i;
newScheduledThreadPool.schedule(new Runnable() {
public void run() {
System.out.println("i:" + temp);
}
}, 3, TimeUnit.SECONDS);
}
}
}
创建一个定长线程池,支持定时及周期性任务执行。
public class Test1 {
public static void main(String[] args) {
//1.创建可固定长度的线程池
ExecutorService newExecutorService = Executors.newFixedThreadPool(3);
//创建了10个线程
for (int i = 0; i < 10; i++) {
int temp = i;
newExecutorService.execute(new Runnable() {
@Override
public void run() {
System.out.println("threadName;"+Thread.currentThread().getName()+",i"+temp);
}
});
}
}
}
newSingleThreadExecutor
public class Test1 {
public static void main(String[] args) {
//1.创建单线程
ExecutorService newSingleThreadExecutor = Executors.newSingleThreadExecutor();
for (int i = 0; i < 10; i++) {
final int index = i;
newSingleThreadExecutor.execute(new Runnable() {
@Override
public void run() {
System.out.println("index:" + index);
try {
Thread.sleep(200);
} catch (Exception e) {
// TODO: handle exception
}
}
});
}
newSingleThreadExecutor.shutdown();
}
}
创建一个单线程化的线程池,它只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序(FIFO, LIFO, 优先级)执行。
常见线程池的优缺点
Executors 类提供了 5 种不同的线程池:newCachedThreadPool 可缓存线程池, newFixedThreadPool 固定
大小的线程池, newScheduledThreadPool 定时或者周期性定长线程池, newSingleThreadExecutor 单线程池、
newWorkStealingPool 多个任务队列的线程池
守护线程
(1) thread.setDaemon(true)必须在thread.start()之前设置,否则会跑出一个IllegalThreadStateException异常。你不能把正在运行的常规线程设置为守护线程。
(2) 在Daemon线程中产生的新线程也是Daemon的。
(3) 守护线程应该永远不去访问固有资源,如文件、数据库,因为它会在任何时候甚至在一个操作的中间发生中断。
线程组
在java的多线程处理中有线程组ThreadGroup的概念,ThreadGroup是为了方便线程管理出现了,可以统一设定线程组的一些属性,比如setDaemon,设置未处理异常的处理方法,设置统一的安全策略等等;也可以通过线程组方便的获得线程的一些信息。
每一个ThreadGroup都可以包含一组的子线程和一组子线程组,在一个进程中线程组是以树形的方式存在,通常情况下根线程组是system线程组。system线程组下是main线程组,默认情况下第一级应用自己的线程组是通过main线程组创建出来的。
工作队列
在类 Executors 中,我们可以看到不同线程池维护的工作队列是不同的,如newCachedThreadPool使用的是SynchronousQueue 同步队列,newSingleThreadScheduledExecutor使用DelayedWorkQueue,
newFixedThreadPool和newScheduledThreadPool使用LinkedBlockingQueue。它们都是实现了并发包java.util.concurrent中的BlockingQueue,下面说说这个接口。
BlockingQueue 阻塞队列
继承于队列 Queue,遵循先进先出原则(FIFO),队列提供几种基本的操作,添加元素(队尾)、移除元素(队头)、取出队头元素(不移除),每种操作都有两个方法,一种有可能抛异常,一种返回操作成功或失败。
在这个基础上,阻塞队列增加了操作锁,保证了数据安全,当然这个具体实现是在子类中完成,接口仅仅描述方法的特点,还增加两种不同的操作实现。下面描述这四种不同类型的操作:
操作,可能抛异常
操作,不抛异常(特殊如类转换异常、空指针、参数异常不属于,仅当队列已满不会跑状态异常)
无限期阻塞线程直至操作成功
有时间限制的操作
添加 移除 检查
add(e) remove(o) element()
offer(e) poll() peek()
put(e) take()
offer(e,time,unit) poll(time,unit)
以及增加了拷贝 drainTo,如线程池的 shutdownNow就是用它完成工作队列的清除以及队列中数据的拷贝,还有其他如对比元素contains,剩余容量查询remainingCapacity等。
实现的子类
ArrayBlockingQueue 数组型阻塞队列
LinkedBlockingQueue 链表型阻塞队列
DelayQueue 延时队列
SynchronousQueue 同步队列
PriorityBlockingQueue 优先阻塞队列
ArrayBlockingQueue
特点:
初始化一定容量的数组
使用一个重入锁,默认使用非公平锁,入队和出队共用一个锁,互斥
是有界设计,如果容量满无法继续添加元素直至有元素被移除
使用时开辟一段连续的内存,如果初始化容量过大容易造成资源浪费,过小易添加失败
LinkedBlockingQueue
特点:
内部使用节点关联,会产生多一点内存占用
使用两个重入锁分别控制元素的入队和出队,用Condition进行线程间的唤醒和等待
有边界的,在默认构造方法中容量是Integer.MAX_VALUE
非连续性内存空间
DelayQueue
特点:
无边界设计
添加(put)不阻塞,移除阻塞
元素都有一个过期时间
取元素只有过期的才会被取出
SynchronousQueue
特点:
内部容量是0
每次删除操作都要等待插入操作
每次插入操作都要等待删除操作
一个元素,一旦有了插入线程和移除线程,那么很快由插入线程移交给移除线程,这个容器相当于通道,本身不存储元素
在多任务队列,是最快的处理任务方式。
PriorityBlockingQueue
特点:
无边界设计,但容量实际是依靠系统资源影响
添加元素,如果超过1,则进入优先级排序
任务拒绝策略
AbortPolicy 抛出异常丢弃任务,默认策略、
CallerRunsPolicy 调用方线程执行任务、
DiscardPolicy 丢弃任务不抛异常、
DiscardOldestPolicy 丢弃最早的任务