线程池原理:
- 控制运行的线程的数量,处理过程中将任务放入队列,然后在线程创建后 启动这些任务,如果线程数量超过了最大数量,超出数量的线程排队等候,等其它线程执行完毕,再从队列中取出任务来执行。
- 特点: 线程复用; 控制最大并发数; 管理线程。
线程的调度策略:线程调度器选择优先级最高的线程运行
终止线程:
- 线程体中调用了 yield 方法让出了对 cpu 的占用权利
- 线程体中调用了 sleep 方法使线程进入睡眠状态
- 线程由于 IO 操作受到阻塞
- 另外一个更高优先级线程出现
- 在支持时间片的系统中,该线程的时间片用完
多线程作用:
- 发挥多核CPU的优势
- 防止阻塞,多条线程同时运行,一条线程的代码执行读取数据阻塞, 不会影响其它任务的执行
- 便于建模
线程池的优点:
- 重用存在的线程,减少对象创建销毁的开销
- 可有效的控制最大并发线程数,提高系统资源的使用率,避免过多资源竞争,避免堵塞
- 提供定时执行、定期执行、单线程、并发数控制等功能。
线程池的组成:
- 线程池管理器:用于创建并管理线程池
- 工作线程:线程池中的线程
- 任务接口:每个任务必须实现的接口,用于工作线程调度其运行
- 任务队列:用于存放待处理的任务,提供一种缓冲机制
多线程同步和互斥:
- 线程同步是指线程之间所具有的一种制约关系,一个线程的执行依赖另一个线程的消息,当它没有得到另一个线程的消息时应等待,直到消息到达时才被唤醒。
- 线程互斥是指对于共享的进程系统资源,在各单个线程访问时的排它性;可以看成是一种特殊的线程同步。
线程间的同步方法:
- 内核模式:利用系统内核对象的单一性来进行同步,使用时需要切换内核态与用户态。内核模式下的方法有:事件,信号量,互斥量。
- 用户模式:不需要切换到内核态,只在用户态完成操作。用户模式下的方法有:原子操作(例如一个单一的全局变量),临界区。
上下文:是指某一时间点 CPU 寄存器和程序计数器的内容。
多线程的上下文切换:
- 挂起一个进程,将这个进程在 CPU 中的状态(上下文)存储于内存中的某处。
- 在内存中检索下一个进程的上下文并将其在 CPU 的寄存器中恢复。
- 跳转到程序计数器所指向的位置(即跳转到进程被中断时的代码行),以恢复该进程在程序 中。
- 不同的线程切换使用 CPU 发生的切换数据等就是上下文切换。
- CPU 控制权由一个已经正在运行的线程切换到另外一个就绪并等待获取 CPU 执行权的线程的过程。
引起线程上下文切换的原因
- 当前执行任务的时间片用完之后,系统CPU正常调度下一个任务;
- 当前执行任务碰到IO阻塞,调度器将此任务挂起,继续下一任务;
- 多个任务抢占锁资源,当前任务没有抢到锁资源,被调度器挂起,继续下一任务;
- 用户代码挂起当前任务,让出 CPU 时间;
- 硬件中断;
- CPU 给每个任务都服务一定的时间,然后把当前任务的状态保存下来,在加载下一任务的状态后,继续服务下一任务,任务的状态保存及再加载的过程就叫做上下文切换。时间片轮转的方式使多个任务在同一颗 CPU 上执行变成了可能。
线程数过多会造成什么异常:
- 线程的生命周期开销非常高
- 消耗过多的 CPU 资源。如果可运行的线程数量多于可用处理器的数量,那么有线程将会被闲置。大量空闲的线程会占用许多内存,给垃圾回收器带来压力,而且大量的线程在竞争 CPU 资源时还将产生其他性能的开销。
- 降低稳定性。JVM 在可创建线程的数量上存在一个限制,这个限制值将随着平台的不同而不同, 并且承受着多个因素制约,包括 JVM 的启动参数、Thread 构造函数中请求栈的大小,以及底层操作系统对线程的限制等。如果破坏了这些限制,那么可能抛出 OutOfMemoryError 异常。
常用的并发工具类
- CountDownLatch
- CyclicBarrier
- Semaphore
- Exchanger
唤醒阻塞的线程
- 使用 suspend()、resume()方法对于线程进行阻塞唤醒会出现死锁。
- 可以利用 Object 类的 wait()和 notify()方法实现线程阻塞。
- wait、notify 方法是针对对象的,调用任意对象的 wait()方法都将导致线程阻塞,阻塞的同时也将释放该对象的锁,调用任意对象的notify()方法则将随机解除该对象阻塞的线程,但需要重新获取该对象的锁,直到获取成功才能往下执行
- wait、notify 方法必须在 synchronized 块或方法中被调用,并且要保证同步块或方法的锁对象与调用 wait、notify方法的对象是同一个,这样在调用 wait 之前当前线程就已经成功获取某对象的锁,执行wait 阻塞后当前线程就将之前获取的对象锁释放。
- 如果线程是因为调用了wait()、sleep()或者 join()方法而导致的阻塞,可以中断线程,并且通过抛出 InterruptedException 来唤醒它;如果线程遇到了 IO 阻塞, 无能为力,因为 IO 是操作系统实现的,Java代码并没有办法直接接触到操作系统。
- Object 类中的 notify() 方法,唤醒在此对象监视器上等待的单个线程,如果所有线程都在此对象上等待,则会选择唤醒其中一个线程,选择是任意的,并在对实现做出决定时发生,线程通过调用其中一个wait() 方法,在对象的监视器上等待,直到当前的线程放弃此对象上的锁定,才能继续执行被唤醒的线程,被唤醒的线程将以常规方式与在该对象上主动同步的其他所有线程进行竞争。类似的方法还有notifyAll() ,唤醒再次监视器上等待的所有线程。
为什么要用 join()方法:
- 主线程生成并启动了子线程,需要用到子线程返回的结果,此时需要主线程需要在子线程结束后再结束,这时候就要用到 join() 方法 。join() 方法,等待其他线程终止,在当前线程中调用一个线程的 join() 方法,则当前线程转为阻塞状态,直到另一个线程结束,当前线程再由阻塞状态变为就绪状态,等待 cpu 。
线程其他方法:
- sleep():强迫一个线程睡眠N毫秒。
- isAlive(): 判断一个线程是否存活。
- join(): 等待线程终止。
- activeCount(): 程序中活跃的线程数。
- enumerate(): 枚举程序中的线程。
- currentThread(): 得到当前线程。
- isDaemon(): 一个线程是否为守护线程。
- setDaemon(): 设置一个线程为守护线程。 (用户线程和守护线程的区别在于,是否等待主线程依赖于主线程结束而结束)
- setName(): 为线程设置一个名称。
- wait(): 强迫一个线程等待。
- notify(): 通知一个线程继续运行。
- setPriority(): 设置一个线程的优先级。
- getPriority():获得一个线程的优先级。
同步锁:
- 当多个线程同时访问同一个数据时,很容易出现问题。为了避免这种情况出现,我们要保证线程同步互斥,就是指并发执行的多个线程,在同一时间内只允许一个线程访问共享数据。可以使用 synchronized 关键字来取得一个对象的同步锁
死锁、避免死锁
- 两个或多个线程被永久阻塞,它们中的一个或者全部都在等待某个资源被释放
- 死锁的原因 :在申请锁时发生了交叉闭环申请;默认的锁申请操作是阻塞的。
- 避免死锁:在一遇到多个对象锁交叉的情况,就要仔细审查这几个对象的类中的所有方法,是否存在着导致锁依赖的环路的可能性。尽量避免在一个同步方法中调用其它对象的延时方法和同步方法。
乐观锁、悲观锁
乐观锁 | 悲观锁 |
每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,可以使用版本号等机制 | 每次去拿数据的时候都认为别人会修改, 所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会 block 直到它拿到锁 |
适用于多读、少写的应用类型,冲突很少发生的时候;可以提高吞吐量;省去了锁的开销 | 传统的MySQL关系型数据库用到很多这种锁机制,适用于多写、经常产生冲突,上层应用不断的进行 retry的应用类型,先加锁可以保证写操作时数据正确 |
公平锁 、非公平锁
公平锁 | 非公平锁 |
在并发环境中,每个线程在获取锁时会先查看此锁维护的等待队列,如果为空,或者当前线程是等待队列的第一个,就占有锁,否则就会加入到等待队列中,以后会按照FIFO的规则从队列中取到自己 | 上来就直接尝试占用所,如果尝试失败,再采用类似公平锁的方式 |
优点:等待锁的线程不会饿死; | 优点:减少唤起线程的开销,整体的吞吐效率高,线程有几率不阻塞直接获得锁,CPU不必唤醒所有线程; |
缺点:整体吞吐效率相对要低,等待队列中除了第一个线程意外的所有线程都会阻塞,CPU唤醒阻塞线程的开销比非公平锁大 | 缺点:处于等待队列中的线程可能会饿死,或者等很久才能获得锁 |
独享锁、 共享锁
独享锁 | 共享锁 |
该锁一次只能被一个线程所持有 | 该锁可被多个线程所持有 |