这是我参与8月更文挑战的第11天,活动详情查看:8月更文挑战
线程
-
程序:
- 进程:Process(正在执行中的程序)是一个静态的概念;
- 进程是程序的一次静态执行过程,占用特定的地址空间;
- 每个进程都是独立的,有3部分组成cpu、data、code
- 缺点:内存的浪费,cpu的负担;
- 进程:Process(正在执行中的程序)是一个静态的概念;
-
线程:是进程中一个“单一的连续控制流程”;
- 线程又被称为轻量级进程;
- 一个进程可拥有多个并行的线程;
- 一个进程中的线程共享相同的内存单元/内存地址空间->可以访问相同的变量和对象;而且它们从同一堆中分配对象->通信、数据交换、同步操作;
- 由于线程的通信是在同一地址空间上进行的,所以不需要额外的通信机制,这就使得通信更简便而且信息传递的速度也更快。
Javac 与Java
- Java虚拟机启动的时候会有一个进程java.exe,该进程中至少有一个线程,在负责java程序的执行。而且这个线程运行的代码存在与main方法中,该线程称之为主线程;
- 一个进程中的线程共享代码和数据空间;
- 线程结束,进程未必结束,进程结束,线程一定结束;
- 进程中包含线程,线程是进程的一部分。
线程与进程的区别
区别 | 进程 | 线程 |
---|---|---|
根本区别 | 作为资源分配的单位 | 调度和执行的单位 |
开销 | 每个进程都有独立的代码和数据空间(进程上下文),进程间的切换会有较大的开销 | 线程可以看成是轻量级的进程,同一类线程共享代码和数据空间,每个线程有独立的运行栈和程序计数器(PC),线程切换的开销小 |
所处环境 | 在操作系统中能同时运行多个任务(程序) | 在同一应用程序中有多个顺序流同时执行 |
分配内存 | 系统在运行的时候会为每个进程分配不同内存区域 | 除了CPU之外,不会为线程分配内存(线程所使用的资源是它所属的进程的资源) ,线程组只能共享资源 |
包含关系 | 没有线程的进程是可以被看作单线程的,如果一个进程内拥有多个线程,则执行过程不是一条线的,而是多条线(线程)共同完成的。 | 线程是进程的一部分,所以线程有时候被称为是轻权进程进程或者轻量级进程 |
java中实现多线程
-
在Java中负责线程的功能是Java.lang.Thread这个类;
-
可以通过Thread的实例来创建新的线程;
-
每个线程都是通过某个特定Thread对象所对应的方法run()来完成其操作的方法run()称为线程体。
-
通过调用Thread类的start()方法来启动一个线程。
-
使用:
- 需要继承Thread类;
- 必须重写run方法,指的是核心执行的逻辑;
- 线程在启动的时候,不要直接第暗涌run方法,而是通过start来调用;
- 每次运行结果不一样,因为多线程抢占CPU资源无法进行认为控制。
-
创建线程的方式二(使用了代理设计模式):
- 实现Runnable接口;
- 重写run方法;
- 创建对象,将创建好的Runnable的子类实现作为Thread的构造参数;
- 调用start()方法启动线程;
- 一般使用该方式创建线程。
- 优点:使用Runnable接口实现多线程优点,实现Runnable接口方式要通用一些;
- 避免单继承;
- 方便共享资源,同一份资源多个代理访问。
-
多线程卖票问题:
- 用Thread创建线程:
- 当创建多个对象时,类所拥有的资源分配给每个对象(线程),即每个对象中都有5张票,最终导致每个线程都卖了5张票;
- 将tickets用static关键字修饰,该资源为类所有,被该类的所有实例共享,虽然实现了所有线程只卖5张票,但是仍然存在票数不同步的情况,即已经卖了5张票,后面还有5张票卖出,显然不对等。
- 使用Runnable接口之后不需要给共享变量添加static关键字,每次创建一个对象,作为共享对象;但是仍然没有解决数据同步的问题。
- 用Thread创建线程:
线程的代理设计模式
- 代理模式(Proxy Pattern):
- 代理类主要功能是接收任务,类似于项目部,具体的任务实现则交由程序员干,那如何知道程序员能不能干这个活儿呢,程序员可以列(继承)一张能力清单(继承接口)给项目负责人,项目负责人根据这张清单(继承这个接口),来判断任务能不能完成。
线程状态
-
线程的生命周期:
-
新生状态:创建线程对象,未调用start()方法;
- 处于新生态的线程拥有自己的空间。
-
就绪状态:线程创建完成,且调用start方法之后,所有的线程会加入到等待队列,等待系统分配cpu资源;
- 处于就绪态的的线程具备了运行条件,但没有分配到cpu,一旦某个线程分配到cpu,就会从就绪状态进入执行状态,该动作称为“CPU调度”。
-
运行状态:当前线程获取到cpu资源,执行的过程中;
- 当在给定的时间片内没有执行结束,就会被系统给换下来回到就绪状态。
-
阻塞状态:处于运行状态的线程在某些情况下让出cpu并暂且停止执行,进入阻塞状态。
- 阻塞状态的线程不能进入就绪队列。只有当引起阻塞的条件消除,线程才能转入就绪队列,直到再次获取到cpu。
-
死亡状态:当运行中的线程正常执行完所有的代码逻辑或者因为异常情况导致程序结束叫做死亡状态;
- 死亡状态是线程声明周期最后一个阶段。死亡原因有三个:
- 线程正常运行,执行完所有工作;
- 线程被强制性终止,如通过stop来终止;
- 线程抛出异常为正常捕获。
- 死亡状态是线程声明周期最后一个阶段。死亡原因有三个:
-
-
线程api方法:
- Thread.currentThread():获取当前线程对象;
- Thread.currentThread():获取当前线程名称;
- Thread.currentThread().getId():获取线程id;
- Thread.currentThread().getPriority():获取线程的优先级;
- 在一般系统中范围是0-10;如果没有经过设置的话,就是默认值5,有些系统是0-100。
-
Thread.currentThread().setPriority():设置线程池的优先级;
1. 优先级越高并不一定先执行,只是优先执行的概率较大而已。
6. Thread.currentThread().isAlive():当前线程是否存活; -
Thread.currentThread().join():调用该方法的线程强制执行,其它线程处于阻塞状态,该线程执行完毕后,其他线程在执行;
8. thread.currentThread().sleep(long millis):使用当前正在执行的线程休眠mills秒,线程处于就绪状态;
9. thread.currentThread().yield():当前正在执行的线程暂停一次,允许其他线程执行,不阻塞,线程进入就绪状态,如果没有其等待执行的线程,当前线程就会马上恢复执行。
多线程的安全问题
-
使用同步解决多线程的安全问题;
- 使用同步方法也可以解决
-
使用方式:
synchronized (this) { if (tickets > 0) { System.out.println(Thread.currentThread().getName() + "正在出售第" + (tickets--) + "张票"); } } 1. 关键字:synchronized(Object类型的子类、共享对象、共享资源) 复制代码
-
同步的前提:
- 必须有两个或两个以上的线程;
- 必须是多个线程使用同一资源;
- 必须保证同步中只能有一个线程在运行。
-
练习:
- 要求:
-
生产者消费者:
- 要求:
-
需要注意的问题:
- 生产者没有生产产品,消费者就可以获取产品;
- 商品牌和名称对应不上;
-
JUC包的使用:
- BlockingQueue类;
线程池
1. 使用线程池管理线程有如下好处:
- 使用线程池可以重复利用已有的线程继续执行任务,避免线程在创建和销毁时造成的消耗;
- 由于没有线程创建和销毁时的消耗,可以提高系统响应速度;
- 通过线程池可以对线程进行合理的管理,根据系统的承受能力调整可运行线程数量大小等。
- 工作原理图:
2. 线程池的分类:
1. ThreadPoolExecutor:
- newCachedThreadPool;
- 创建一个可根据需要创建新线程的线程池,但是在以前构造的线程可用时将重用它们,并在需要的时候使用提供的ThreadFactory创建新线程。特征:
- 线程池的数量没有固定,可达到最大值(Integer.MAX.VALUE);
- 线程池中的线程可进行缓存重复利用和回收(回收时间默认为1分钟);
- 当线程池中,没有可用线程,会重新创建一个线程。
- newFixedThreadPool;
- 创建一个可重用固定线程数的线程池,以共享的无界队列方式来运行这些线程,在任意点,如果所有线程都处于处理任务的状态,此时提交一个任务,则在可用线程之前,会将任务添加到队列中等待。如果在关闭前的执行期间由于失败而导致任何线程终止,那么一个新线程将替代它执行后续的任务(如果需要)。在某个线程被显示地关闭之前,池中的线程会一直存在。特征:
- 线程池中的线程数量固定,可以很好的控制并发量;
- 线程可以被重复使用,在显示关闭之前,将一直存在;
- 超出一定量的线程任务被提交时需要在队列中等待。
- 创建一个可重用固定线程数的线程池,以共享的无界队列方式来运行这些线程,在任意点,如果所有线程都处于处理任务的状态,此时提交一个任务,则在可用线程之前,会将任务添加到队列中等待。如果在关闭前的执行期间由于失败而导致任何线程终止,那么一个新线程将替代它执行后续的任务(如果需要)。在某个线程被显示地关闭之前,池中的线程会一直存在。特征:
- newSingleThreadExecutor;
- 创建一个使用worker线程的Executor,以无界队列方式来运行该线程。(如果因为在关闭前的执行期间出现失败而终止了此线程,那么如果需要,一个新线程将替它执行后续的任务),可保证顺序地执行各个任务,并且在任意给定的时间不会有多个线程是活动的,与其他等效的newFixedThreadPool(1)不同,可保证无需重新配置此方法所返回的执行程序即可使用其他的线程。特征:
- 线程池中最多执行1个线程,之后提交的线程活动将会排在队列中依次执行。
- 创建一个使用worker线程的Executor,以无界队列方式来运行该线程。(如果因为在关闭前的执行期间出现失败而终止了此线程,那么如果需要,一个新线程将替它执行后续的任务),可保证顺序地执行各个任务,并且在任意给定的时间不会有多个线程是活动的,与其他等效的newFixedThreadPool(1)不同,可保证无需重新配置此方法所返回的执行程序即可使用其他的线程。特征:
- 创建一个可根据需要创建新线程的线程池,但是在以前构造的线程可用时将重用它们,并在需要的时候使用提供的ThreadFactory创建新线程。特征:
2. ScheduledThreadPoolExecutor:
- newScheduledThreadPool(用的较多);
- 创建一个线程池,它可安排在给定延迟后运行命令或定期地执行。特征:
- 线程池中具有指定数量的线程,即便是空线程也将保留;
- 可定时或延迟执行线程活动。
- 创建一个线程池,它可安排在给定延迟后运行命令或定期地执行。特征:
- newSingleThreadScheduledExecutor;
- 创建一个单线程执行程序,它可安排在给定延迟后运行命令或者定期执行。特征:
- 线程池中最多执行1个线程,之后提交的线程活动会排在队列中依次执行;
- 可定时或者延迟执行线程活动。
- 创建一个单线程执行程序,它可安排在给定延迟后运行命令或者定期执行。特征:
- ForkJoinPool(用得不是很多,分而治之的思想 ):
- newWorkStealingPool:
- 创建一个并行级别的线程池,并行级别决定了同一时刻最多有多少个线程在执行,若不传入并行级别参数,将默认为当前系统CPU的个数。
- newWorkStealingPool:
3. 线程池的生命周期:
-
RUNNING:能接受新提交的任务,并且也能处理阻塞队列中的任务;
-
SHUTDOWN:关闭状态,不再接受新提交的任务,但却可以继续处理阻塞队列中已保存的任务;
-
STOP:不能接受新任务,也不处理队列中的任务,会中断正在处理任务的线程;
-
TIDYING:如果所有的任务都已经终止了,workCount(有效线程数)为0,线程池进入该状态后会第暗涌terminated()方法进入TERMINATED状;
-
TERMINATED:在terminated() 方法执行完后进入该状态,默认terminated() 方法中什么也没有做。
1. 线程池的创建:
// ThreadPoolExecutor类构造方法参数含义:
/**
* Creates a new {@code ThreadPoolExecutor} with the given initial
* parameters and default thread factory.
*
* @param corePoolSize the number of threads to keep in the pool, even
* if they are idle, unless {@code allowCoreThreadTimeOut} is set
* @param maximumPoolSize the maximum number of threads to allow in the
* pool
* @param keepAliveTime when the number of threads is greater than
* the core, this is the maximum time that excess idle threads
* will wait for new tasks before terminating.
* @param unit the time unit for the {@code keepAliveTime} argument
* @param workQueue the queue to use for holding tasks before they are
* executed. This queue will hold only the {@code Runnable}
* tasks submitted by the {@code execute} method.
* @param handler the handler to use when execution is blocked
* because the thread bounds and queue capacities are reached
* @throws IllegalArgumentException if one of the following holds:<br>
* {@code corePoolSize < 0}<br>
* {@code keepAliveTime < 0}<br>
* {@code maximumPoolSize <= 0}<br>
* {@code maximumPoolSize < corePoolSize}
* @throws NullPointerException if {@code workQueue}
* or {@code handler} is null
*/
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
RejectedExecutionHandler handler) {
this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue,
Executors.defaultThreadFactory(), handler);
}
/*
1. int corePoolSize:核心线程池的数量;
2. int maximumPoolSize:最大可以创建线程的数量(大于核心线程池数量,小于最大线程池数量);
3. long keepAliveTime:线程池中线程存活时间;TimeUnit unit:时间;
4. BlockingQueue<Runnable> workQueue:阻塞队列;
5. ThreadFactory threadFactory:用来创建线程(工厂模式);
6. RejectedExecutionHandler handler:拒绝策略(饱和策略)。
*/
//newFixedThreadPool
public static ExecutorService newFixedThreadPool(int nThreads) {
return new ThreadPoolExecutor(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>()); //链表队列
}
# newCachedThreadPool
public static ExecutorService newCachedThreadPool(ThreadFactory threadFactory) {
return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
60L, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>(), //同步队列
threadFactory);
}
// newSingleThreadExecutor
public static ExecutorService newSingleThreadExecutor(ThreadFactory threadFactory) {
return new FinalizableDelegatedExecutorService
(new ThreadPoolExecutor(1, 1,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>(),
threadFactory));
}
//ScheduledThreadPoolExecutor
public ScheduledThreadPoolExecutor(int corePoolSize) {
super(corePoolSize, Integer.MAX_VALUE, 0, NANOSECONDS,
new DelayedWorkQueue()); //阻塞对列:使用较少
}
复制代码
2. 常见阻塞队列:
1.ArrayBlockingQueue:
- 基于数组的阻塞队列实现,在ArrayBlockingQueue内部,维护了一个定长数组,以便缓存队列中的数据对象,这是一个常用的阻塞队列,除了一个定长数组外,ArrayBlockingQueue内部还保存着两个整形变量,分别标识着队列的头部和尾部在数组中的位置。 ArrayBlockingQueue在生产者放入数据和消费者获取数据,都是共用同一个锁对象,由此也意味着两者无法真正并行运行,这点尤其不同于LinkedBlockingQueue;按照实现原理来分析,ArrayBlockingQueue完全可以采用分离锁,从而实现生产者和消费者操作的完全并行运行。Doug Lea之所以没这样去做,也许是因为ArrayBlockingQueue的数据写入和获取操作已经足够轻巧,以至于引入独立的锁机制,除了给代码带来额外的复杂性外,其在性能上完全占不到任何便宜。 ArrayBlockingQueue和LinkedBlockingQueue间还有一个明显的不同之处在于,前者在插入或删除元素时不会产生或销毁任何额外的对象实例,而后者则会生成一个额外的Node对象。这在长时间内需要高效并发地处理大批量数据的系统中,其对于GC的影响还是存在一定的区别。而在创建ArrayBlockingQueue时,我们还可以控制对象的内部锁是否采用公平锁,默认采用非公平锁。
2. LinkedBlockingQueue:
- 基于链表的阻塞队列,同ArrayListBlockingQueue类似,其内部也维持着一个数据缓冲队列(该队列由一个链表构成),当生产者往队列中放入一个数据时,队列会从生产者手中获取数据,并缓存在队列内部,而生产者立即返回;只有当队列缓冲区达到最大值缓存容量时(LinkedBlockingQueue可以通过构造函数指定该值),才会阻塞生产者队列,直到消费者从队列中消费掉一份数据,生产者线程会被唤醒,反之对于消费者这端的处理也基于同样的原理。而LinkedBlockingQueue之所以能够高效的处理并发数据,还因为其对于生产者端和消费者端分别采用了独立的锁来控制数据同步,这也意味着在高并发的情况下生产者和消费者可以并行地操作队列中的数据,以此来提高整个队列的并发性能。作为开发者,我们需要注意的是,如果构造一个LinkedBlockingQueue对象,而没有指定其容量大小,LinkedBlockingQueue会默认一个类似无限大小的容量(Integer.MAX_VALUE),这样的话,如果生产者的速度一旦大于消费者的速度,也许还没有等到队列满阻塞产生,系统内存就有可能已被消耗殆尽了。
3. DelayedWorkQueue:
- DelayQueue中的元素只有当其指定的延迟时间到了,才能够从队列中获取到该元素。DelayQueue是一个没有大小限制的队列,因此往队列中插入数据的操作(生产者)永远不会被阻塞,而只有获取数据的操作(消费者)才会被阻塞。
- 使用场景:DelayQueue使用场景较少,但都相当巧妙,常见的例子比如使用一个DelayQueue来管理一个超时未响应的连接队列。
4. PriorityBlockingQueue:
- 基于优先级的阻塞队列(优先级的判断通过构造函数传入的Compator对象来决定),但需要注意的是PriorityBlockingQueue并不会阻塞数据生产者,而只会在没有可消费的数据时,阻塞数据的消费者。因此使用的时候要特别注意,生产者生产数据的速度绝对不能快于消费者消费数据的速度,否则时间一长,会最终耗尽所有的可用堆内存空间。在实现PriorityBlockingQueue时,内部控制线程同步的锁采用的是公平锁。
5. SynchronousQueue:
- 一种无缓冲的等待队列,类似于无中介的直接交易,有点像原始社会中的生产者和消费者,生产者拿着产品去集市销售给产品的最终消费者,而消费者必须亲自去集市找到所要商品的直接生产者,如果一方没有找到合适的目标,那么对不起,大家都在集市等待。相对于有缓冲的BlockingQueue来说,少了一个中间经销商的环节(缓冲区),如果有经销商,生产者直接把产品批发给经销商,而无需在意经销商最终会将这些产品卖给那些消费者,由于经销商可以库存一部分商品,因此相对于直接交易模式,总体来说采用中间经销商的模式会吞吐量高一些(可以批量买卖);但另一方面,又因为经销商的引入,使得产品从生产者到消费者中间增加了额外的交易环节,单个产品的及时响应性能可能会降低。
- 声明一个SynchronousQueue有两种不同的方式,它们之间有着不太一样的行为。公平模式和非公平模式的区别:
- 如果采用公平模式:SynchronousQueue会采用公平锁,并配合一个FIFO队列来阻塞多余的生产者和消费者,从而体系整体的公平策略;
- 但如果是非公平模式(SynchronousQueue默认):SynchronousQueue采用非公平锁,同时配合一个LIFO队列来管理多余的生产者和消费者,而后一种模式,如果生产者和消费者的处理速度有差距,则很容易出现饥渴的情况,即可能有某些生产者或者是消费者的数据永远都得不到处理。
- arrayblockingqueue和linkedblockqueue的区别:
- 队列中锁的实现不同ArrayBlockingQueue实现的队列中的锁是没有分离的,即生产和消费用的是同一个锁;
- LinkedBlockingQueue实现的队列中的锁是分离的,即生产用的是putLock,消费是takeLock;
- 队列大小初始化方式不同;
- ArrayBlockingQueue实现的队列中必须指定队列的大小;
- LinkedBlockingQueue实现的队列中可以不指定队列的大小,但是默认是Integer.MAX_VALUE ;
3. 拒绝策略:
- ThreadPoolExecutor.AbortPolicy:丢弃任务并抛出:
- RejectedExecutionException:异常
- ThreadPoolExecutor.DiscardPolicy:丢弃任务,但是不抛出异常;
- ThreadPoolExecutor.DiscardOldestPolicy:丢弃队列最前面的任务,然后重新尝试执行任务(重复此过程);
- ThreadPoolExecutor.CallerRunsPolicy:由调用线程处理该任务。
4. execute方法执行逻辑:
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();
//1.判断核心线程池中正在运行线程数是否大于最大核心线程数,若不大于,则创建新的线程执行任务,否则将任务加入队列中;
if (workerCountOf(c) < corePoolSize) {
if (addWorker(command, true))
return;
c = ctl.get(); //加入阻塞队列
}
//2.判断正在运行的核心线程数以及获取阻塞队列中任务等待的数量,若大于阻塞队列的容量,执行拒绝策略,否则加入队列中等待,其次判断是否需要创建新线程,防止有线程池在检查过程中关闭,因此重新检查状态,判断是否有必要去创建新的线程。
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);
}
//3. 判断线程池中最大线程数,若大于最大数量,则执行拒绝策略。
else if (!addWorker(command, false))
reject(command);
}
复制代码
-
Callable创建线程:
- submit:方法有返回值:future;
- execute:方法无返回值 void。
-
线程池的关闭:
- 原理:遍历线程池中所有的线程,然后依次中断;
- shutdownNow首先将线程池的状态设置为stop,然后尝试停止所有的正在执行和未执行任务的任务的线程,并返回等待执行任务的列表;
- shutdown只是将线程池的状态设置为SHUTDOWN状态,然后中断所有没有正在执行任务的线程。
- 原理:遍历线程池中所有的线程,然后依次中断;