Thread

一, 概念:

程序:是为完成特定任务,用某种语言编写的一组指令的集合,即指一段静态的代码,静态对象。 进程:是程序的一次执行过程,或是正在运行的一个程序,是一个动态的过程,有它自身的产生,存在和消亡的过程。———-生命周期 线程:进程可进一步细化为线程,是一个程序内部的一条执行路径;线程是进程的一个实体,是CPU调度和分派的基本单位.线程是程序中一个单一的顺序控制流程。进程内有一个相对独立的、可调度的执行单元,是系统独立调度和分派CPU的基本单位指令运行时的程序的调度单位。 协程:轻量级线程,存在于用户态,映射到内核态的线程执行,并发时在用户态进行切换,开销极其小,并发能力恐怖;对cpu和操作系统不可见。

线程的相关API

Thread.currentThread().getName():获取当前线程的名字。
1.start():1.启动当前线程2.调用线程中的run方法
2.run():通常需要重写Thread类中的此方法,将创建的线程要执行的操作声明在此方法中
3.currentThread():静态方法,返回执行当前代码的线程
4.getName():获取当前线程的名字
5.setName():设置当前线程的名字
6.yield():主动释放当前线程的执行权 ; 礼让
7.join():在线程中插入执行另一个线程,该线程被阻塞,直到插入执行的线程完全执行完毕以后,该线程才继续执行下去; 插队
8.stop():过时方法。当执行此方法时,强制结束当前线程。depreciate
9.sleep(long millitime):线程休眠一段时间
10.isAlive():判断当前线程是否存活

线程的调度

Java的调度方法:
1.对于同优先级的线程组成先进先出队列(先到先服务),使用时间片策略
2.对高优先级,使用优先调度的抢占式策略
Java 中线程会按优先级分配 CPU 时间片运行,且优先级越高越优先执行,但优先级高并不代表能独自占用执行时间片,可能是优先级高得到越多的执行时间片,反之,优先级低的分到的执行时间少但不会分配不到执行时间。

线程的优先级

MAX_PRIORITY:10
MIN_PRIORITY:1
NORM_PRIORITY:5
方法:
getPriority():返回线程优先级
setPriority(int newPriority):改变线程的优先级
线程的默认优先级是父线程的默认优先级。

进程调度策略:

优先调度算法

先来先服务调度算法(FCFS)
短作业(进程)优先调度算法

高优先权优先调度算法

为了照顾紧迫型作业,使之在进入系统后便获得优先处理,引入了最高优先权优先(FPF)调度算法。当把该算法用于作业调度时,系统将从后备队列中选择若干个优先权最高的作业装入内存。当用于进程调度时,该算法是把处理机分配给就绪队列中优先权最高的进程。

  1. 非抢占式优先权调度

系统一旦把处理机分配给就绪队列中优先权最高的进程后,该进程便一直执行下去,直至完成;或因发生某事件使该进程放弃处理机时。

  1. 抢占式优先权调度

当在系统执行期间,只要又出现了另一个其优先权更高的进程,进程调度程序就立即停止当前进程(原优先权最高的进程)的执行,重新将处理机分配给新到的优先权最高的进程。


高响应比优先调度算法

在短作业优先算法的基础上,为每个作业引入优先权,并使作业的优先级随着等待时间的增加而以速率 a 提高,则长作业在等待一定的时间后,必然有机会分配到处理机。

时间片轮转调度

  1. 时间片轮转

系统将所有的就绪进程按先来先服务的原则排成一个队列,每次调度时,把 CPU 分配给队首进程,并令其执行一个时间片;当执行的时间片用完时,由一个计时器发出时钟中断请求,调度程序便据此信号来停止该进程的执行,并将它送往就绪队列的末尾;然后,再把处理机分配给就绪队列中新的队首进程,同时也让它执行一个时间片。

  1. 多级反馈队列

设置多个就绪队列,并为各个队列赋予不同的优先级与不同的时间片。 当一个新进程进入内存后,首先将它放入第一队列的末尾,按 FCFS 原则排队等待调度。仅当第一队列空闲时,调度程序才调度第二队列中的进程运行。

线程的生命周期

线程生命周期的阶段 描述
新建 当一个Thread类或其子类的对象被声明并创建时,新生的线程对象处于新建状态
就绪 处于新建状态的线程被start后,将进入线程队列等待CPU时间片,此时它已具备了运行的条件,只是没分配到CPU资源
运行 当就绪的线程被调度并获得CPU资源时,便进入运行状态,run方法定义了线程的操作和功能
阻塞 在某种特殊情况下,被人为挂起或执行输入输出操作时,让出CPU并临时终止自己的执行,进入阻塞状态
死亡 线程完成了它的全部工作或线程被提前强制性地中止或出现异常导致结束

image.png

线程的创建方式

方式1:继承于Thread类

1.创建一个继承于Thread类的子类 2.重写Thread类的run()方法 3.创建Thread子类的对象 4.通过此对象调用start()方法

方式2:实现Runable接口方式

1.创建一个实现了Runable接口的类 2.实现类去实现Runnable中的抽象方法:run() 3.创建实现类的对象 4.将此对象作为参数传递到Thread类中的构造器中,创建Thread类的对象 5.通过Thread类的对象调用start()

方式3:实现callable接口方式:

与使用runnable方式相比,callable功能更强大些: runnable重写的run方法不如callable的call方法强大,call方法可以有返回值 方法可以抛出异常 支持泛型的返回值 需要借助FutureTask类,获取返回结果

方式4: 使用线程池的方式:

Executors 工具类,线程池的工厂类,用于创建并返回不同类型的线程池
Executors.newCachedThreadPool() 创建一个可根据需要创建新线程的线程池
Executors.newFixedThreadPool(n) 创建一个可重用固定线程数的线程池
Executors.newSingleThreadExecutor() :创建一个只有一个线程的线程池
Executors.newScheduledThreadPool(n) 创建一个线程池,它可安排在给定延迟后运行命令或者定期地执行。
  1. package com.example.paoduantui.Thread;
  2. import java.util.concurrent.ExecutorService;
  3. import java.util.concurrent.Executors;
  4. /**
  5. *1.需要创建实现runnable或者callable接口方式的对象
  6. * 2.创建executorservice线程池
  7. * 3.将创建好的实现了runnable接口类的对象放入executorService对象的execute方法中执行。
  8. * 4.关闭线程池
  9. *
  10. * */
  11. class NumberThread implements Runnable{
  12. @Override
  13. public void run() {
  14. for(int i = 0;i<=100;i++){
  15. if (i % 2 ==0 )
  16. System.out.println(Thread.currentThread().getName()+":"+i);
  17. }
  18. }
  19. }
  20. class NumberThread1 implements Runnable{
  21. @Override
  22. public void run() {
  23. for(int i = 0;i<100; i++){
  24. if(i%2==1){
  25. System.out.println(Thread.currentThread().getName()+":"+i);
  26. }
  27. }
  28. }
  29. }
  30. public class ThreadPool {
  31. public static void main(String[] args){
  32. //创建固定线程个数为十个的线程池
  33. ExecutorService executorService = Executors.newFixedThreadPool(10);
  34. //new一个Runnable接口的对象
  35. NumberThread number = new NumberThread();
  36. NumberThread1 number1 = new NumberThread1();
  37. //执行线程,最多十个
  38. executorService.execute(number1);
  39. executorService.execute(number);//适合适用于Runnable
  40. //executorService.submit();//适合使用于Callable
  41. //关闭线程池
  42. executorService.shutdown();
  43. }
  44. }

ThreadPool

一, 线程池概念:

在面向对象编程中,创建和销毁对象是很费时间的,因为创建一个对象要获取内存资源或者其它更多资源。在Java中更是如此,虚拟机将试图跟踪每一个对象,以便能够在对象销毁后进行垃圾回收。
所以提高服务程序效率的一个 手段就是尽可能减少创建和销毁对象的次数,特别是一些很耗资源的对象创建和销毁。如何利用已有对象来服务就是一个需要解决的关键问题,其实这就是一些”池化资源”技术产生的原因。

二, 线程池的优点:

1.重用线程池中的线程,减少因对象创建,销毁所带来的性能开销;
2.能有效的控制线程的最大并发数,提高系统资源利用率,同时避免过多的资源竞争,避免堵塞;
3.能够多线程进行简单的管理,使线程的使用简单、高效。

三, 线程池的状态:

线程池的5种状态:Running、ShutDown、Stop、Tidying、Terminated。

java虚拟机内存结构

MultiThread - 图2

栈帧是存储在用户栈上的(当然内核栈同样适用)每一次函数调用涉及的相关信息的记录单元。

线程间通信方法:

wait()/ notify()/ notifayAll():此三个方法定义在Object类中的,因为这三个方法需要用到锁,而锁是任意对象都能充当的,所以这三个方法定义在Object类中。
由于wait,notify,以及notifyAll都涉及到与锁相关的操作
wait(在进入锁住的区域以后阻塞等待,释放锁让别的线程先进来操作)—— Obj.wait 进入Obj这个锁住的区域的线程把锁交出来原地等待通知
notify(由于有很多锁住的区域,所以需要将区域用锁来标识,也涉及到锁) ——- Obj.notify 新线程进入Obj这个区域进行操作并唤醒wait的线程
所以wait,notify需要使用在有锁的地方,也就是需要用synchronize关键字来标识的区域,即使用在同步代码块或者同步方法中,且为了保证wait和notify的区域是同一个锁住的区域,需要用锁来标识,也就是锁要相同的对象来充当。

线程的分类:

java中的线程分为两类:1.守护线程(如垃圾回收线程,异常处理线程),2.用户线程(如主线程)
若JVM中都是守护线程,当前JVM将退出。(形象理解,唇亡齿寒)

线程的同步:在同步代码块中,只能存在一个线程。

线程的安全问题:

什么是线程安全问题呢?
线程没来得及更新共享数据,从而导致另外线程没得到最新的数据,从而产生线程安全问题。进行操作进行操作时,线程没来得及更新共享数据,从而导致另外线程没得到最新的数据,从而产生线程安全问题。
如何解决:当一个线程在操作时,其他线程不能参与进来,直到此线程的生命周期结束
在java中,我们通过同步机制,来解决线程的安全问题。
方式一:同步代码块
使用同步监视器(锁)
Synchronized(同步监视器){
//需要被同步的代码
}

方式二:同步方法
使用同步方法,对方法进行synchronized关键字修饰
将同步代码块提取出来成为一个方法,用synchronized关键字修饰此方法。
对于runnable接口实现多线程,只需要将同步方法用synchronized修饰
而对于继承自Thread方式,需要将同步方法用static和synchronized修饰,因为对象不唯一(锁不唯一)
总结:1.同步方法仍然涉及到同步监视器,只是不需要我们显示的声明。
2.非静态的同步方法,同步监视器是this
静态的同步方法,同步监视器是当前类本身。继承自Thread。class

方式三:JDK5.0新增的lock锁方法

  1. package com.lab.test;
  2. import java.util.concurrent.locks.ReentrantLock;
  3. class Window implements Runnable{
  4. private int ticket = 100;
  5. private ReentrantLock lock = new ReentrantLock();
  6. @Override
  7. public void run() {
  8. while (true) {
  9. //2.调用锁定方法lock
  10. lock.lock();
  11. try {
  12. if (ticket > 0) {
  13. Thread.sleep(100);
  14. System.out.println(Thread.currentThread().getName() + "售出第" + ticket + "张票");
  15. ticket--;
  16. } else {
  17. break;
  18. }
  19. } catch (InterruptedException e) {
  20. e.printStackTrace();
  21. }finally {
  22. lock.unlock();
  23. }
  24. }
  25. }
  26. }
  27. public class LockTest {
  28. public static void main(String[] args) {
  29. Window w= new Window();
  30. Thread t1 = new Thread(w);
  31. Thread t2 = new Thread(w);
  32. Thread t3 = new Thread(w);
  33. t1.setName("窗口1");
  34. t2.setName("窗口2");
  35. t3.setName("窗口3");
  36. t1.start();
  37. t2.start();
  38. t3.start();
  39. }
  40. }
  41. //res:
  42. 窗口1售出第100张票
  43. 窗口1售出第99张票
  44. 窗口2售出第98张票
  45. 窗口3售出第97张票
  46. 窗口1售出第96张票
  47. 窗口1售出第95张票
  48. 窗口2售出第94张票
  49. 窗口2售出第93张票
  50. 窗口3售出第92张票
  51. 窗口1售出第91张票
  52. 窗口1售出第90张票
  53. 窗口2售出第89张票
  54. 窗口3售出第88张票
  55. 窗口1售出第87张票
  56. 窗口1售出第86张票
  57. 窗口2售出第85张票
  58. 窗口2售出第84张票

总结:Synchronized与lock的异同?

相同:二者都可以解决线程安全问题
不同:synchronized机制在执行完相应的代码逻辑以后,自动的释放同步监视器
lock需要手动的启动同步(lock()),同时结束同步也需要手动的实现(unlock())(同时以为着lock的方式更为灵活)
优先使用顺序:
LOCK-》同步代码块-》同步方法

线程的通信

通信常用方法:

通信方法 描述
wait() 一旦执行此方法,当前线程就进入阻塞状态,并释放同步监视器
notify 一旦执行此方法,就会唤醒被wait的一个线程,如果有多个线程,就唤醒优先级高的线程
notifyAll 一旦执行此方法,就会唤醒所有被wait()的线程

使用前提:这三个方法均只能使用在同步代码块或者同步方法中。

  1. package com.lab.test;
  2. class Number implements Runnable{
  3. private int number = 1;//设置共享数据(线程之间对于共享数据的共享即为通信)
  4. //对共享数据进行操作的代码块,需要线程安全
  5. @Override
  6. public synchronized void run() {
  7. while(true){
  8. //使得线程交替等待以及通知交替解等待
  9. notify();//省略了this.notify()关键字
  10. if(number<100){
  11. try {
  12. Thread.sleep(10);
  13. } catch (InterruptedException e) {
  14. e.printStackTrace();
  15. }
  16. System.out.println(Thread.currentThread().getName()+":"+number);
  17. number++;
  18. try {
  19. wait();
  20. } catch (InterruptedException e) {
  21. e.printStackTrace();
  22. }
  23. }else{
  24. break;
  25. }
  26. }
  27. }
  28. }
  29. public class Communication {
  30. public static void main(String[] args) {
  31. //创建runnable对象
  32. Number number = new Number();
  33. //创建线程,并实现runnable接口
  34. Thread t1 = new Thread(number);
  35. Thread t2 = new Thread(number);
  36. //给线程设置名字
  37. t1.setName("线程1");
  38. t2.setName("线程2");
  39. //开启线程
  40. t1.start();
  41. t2.start();
  42. }
  43. }

线程的死锁问题:

线程死锁的理解:僵持,谁都不放手,一双筷子,我一只你一只,都等对方放手(死锁,两者都进入阻塞,谁都吃不了饭,进行不了下面吃饭的操作)
出现死锁以后,不会出现提示,只是所有线程都处于阻塞状态,无法继续

  1. package com.lab.test;
  2. import org.junit.Test;
  3. public class threadPoolTest {
  4. public static void main(String[] args) {
  5. final StringBuffer s1 = new StringBuffer();
  6. final StringBuffer s2 = new StringBuffer();
  7. new Thread(){
  8. @Override
  9. public void run() {
  10. //先拿锁一,再拿锁二
  11. synchronized (s1){
  12. s1.append("a");
  13. System.out.println(s1);
  14. s2.append("1");
  15. System.out.println(s2);
  16. try {
  17. Thread.sleep(100);
  18. } catch (InterruptedException e) {
  19. e.printStackTrace();
  20. }
  21. synchronized (s2) {
  22. s1.append("b");
  23. s2.append("2");
  24. System.out.println(s1);
  25. System.out.println(s2);
  26. }
  27. }
  28. }
  29. }.start();
  30. //使用匿名内部类实现runnable接口的方式实现线程的创建
  31. new Thread(new Runnable() {
  32. @Override
  33. public void run() {
  34. synchronized (s2){
  35. s1.append("c");
  36. System.out.println(s1);
  37. s2.append("3");
  38. System.out.println(s2);
  39. synchronized (s1) {
  40. s1.append("d");
  41. s2.append("4");
  42. System.out.println(s1);
  43. System.out.println(s2);
  44. }
  45. }
  46. }
  47. }).start();
  48. }
  49. }

死锁的解决办法:

1.减少同步共享变量 2.采用专门的算法,多个线程之间规定先后执行的顺序,规避死锁问题 3.减少锁的嵌套。