JAVA并发编程(二):线程知识


1. 线程和进程

  • 进程是代码在数据集合上的一次运行活动, 是系统进行资源分配和调度的基本单位。
  • 线程则是进程的一个执行路径, 一个进程中至少有一个线程,进程中的线程共享进程的资源。线程是CPU 分配的基本单位。
  • Java中,多个线程共享进程的堆和方法区资源,每个线程有自己的程序计数器和栈区域。
  • 程序计数器
    (1) 记录了该线程让出CPU时的执行地址,待再次分配到时间片时线程就可以从计数器指定的地址继续执行。
    (2) 只有执行的是Java 代码时记录的才是下一条指令的地址,而对于native方法记录的是undefined 地址。
  • 栈资源
    (1) 存储该线程的局部变量
    (2) 存放线程的调用栈帧

  • (1) 堆里面主要存放使用new 操作创建的对象实例
    (2) 堆是被进程中的所有线程共享的
  • 方法区用来存放JVM加载的类、常量及静态变量等信息,是线程共享的。

2. 线程创建

  • 继承Thread 类并重写run方法
    (1) 创建完thread对象后该线程并没有被启动执行,调用了start 方法后才真正启动了线程。
    (2) start 方法后线程处于就绪状态,等待获取CPU 资源。
    (3) 在run()方法内可以使用this获取当前线程。
  //创建线程
  MyThread thread= new MyThread();
  // 启动线程
  thread .start();
  • 实现Runnable接口的run 方法
    (1) 多个线程可共用一个task 代码逻辑。
    (2) 在实现接口的同时可以继承其他类。
  RunableTask task =new RunableTask();
  new Thread(task).start() ;
  new Thread(task).start() ;
  • 使用FutureTask 方式
    (1) 实现Callable 接口的call()方法。
    (2) 可以拿到任务的返回结果。
  static class CallerTask implements Callable{
    @Override
    public String call() throws Exception{}
  }
  //创建异步任务
  FutureTask futureTask =new FutureTask<>(new CallerTask()) ;
  //启动线程
  new Thread(futureTask).start () ;
  try {
    //等待任务执行完毕,并返回结果
    String result = futureTask.get ();
  } catch (ExecutionException e) {}

3. 线程通知与等待

  • wait()函数
    当一个线程调用一个共享变量的wait()方法时,该调用线程会被阻塞挂起,直到发生以下情况才返回:
    (1) 其他线程调用了该共享对象的notify()或者notifyAll()方法;
    (2) 其他线程调用了该线程的interrupt()方法,该线程抛出InterruptedException异常返回。

    调用wait()方法的线程需要先获取该对象的监视器锁,否则会抛出IllegalMonitorStateException异常
    通过以下方式获取监视器锁
    (1) 执行synchronized 同步代码块时, 使用该共享变量作为参数。
    (2) 调用该共享变量的方法,并且该方法使用了synchronized 修饰。


4. 线程睡眠

  • sleep方法:
    (1) 当一个执行中的线程调用了Thread 的sleep方法后,会暂时让出指定时间的执行权,不参与CPU的调度
    (2) 该线程拥有的监视器资源,比如锁还是持有不会让出。
    (3) 睡眠时间到了后该线程处于就绪状态,等待获取cpu资源。
    (4) 在睡眠期间其他线程调用了该线程的interrupt方法中断了该线程,则该线程会在调用sleep方法的地方抛出IntermptedException异常而返回。
  • yield方法:
    (1) 线程调用yield 方法时,是在暗示线程调度器当前线程请求让出自己的CPU 使用,让线程调度器现在就可以进行下一轮的线程调度,但是线程调度器可以无条件忽略这个暗示。
    (2) 调用yield 方法时后,当前线程会让出CPU 使用权,然后处于就绪状态,线程调度器下一次调度时就有可能调度到当前线程执行。

5. 线程中断

  • Java 中的线程中断是一种线程间的协作模式,通过设置线程的中断标志并不能直接终止该线程的执行, 而是被中断的线程根据中断状态自行处理。

  • interrupt方法:中断线程
    (1) 线程B可以调用线程A的interrupt方法来设置线程A 的中断标志为true并立即返回。
    (2) 如果线程A 因为调用了wait 系列函数、join 方法或者sleep 方法而被阻塞挂起,会在调用这些方法的地方抛出InterruptedException 异常而返回。

  • 线程上下文切换时机有:
    (1) 当前线程的CPU 时间片使用完处于就绪状态时
    (2) 当前线程被其他线程中断时


6. 线程死锁

  • 死锁是指两个或两个以上的线程在执行过程中,因争夺对方已经持有的资源而造成的互相等待的现象。

  • 死锁的产生必须具备四个条件:
    互斥条件:该资源同时只由一个线程占用。
    请求并持有条件: 指一个线程己经持有了至少一个资源, 但又提出了新的资源请求,而新资源己被其他线程占有,所以当前线程会被阻塞,但阻塞的同时并不释放自己已经获取的资源。
    不可剥夺条件: 指线程获取到的资源在自己使用完之前不能被其他线程抢占。
    环路等待条件:发生死锁时, 必然存在一个线程-资源的环形链。

  • 避免死锁:
    (1) 只有请求并持有和环路等待条件是可以被破坏的。
    (2) 造成死锁的原因其实和申请资源的顺序有很大关系, 使用资源申请的有序性原则就可以避免死锁。
    (3) 资源申请的有序性:假如线程A 和线程B 都需要资源1, 2, 3, ... , n 时,对资源进行排序,线程A 和线程B 只有在获取了资源n-1 时才能去获取资源n 。


6. 守护线程与用户线程

  • Java 中的线程分为两类,daemon 线程(守护线程〉和user 线程(用户线程)。
  • 守护线程
    (1) 在JVM启动的同时,启动了好多守护线程, 比如垃圾回收线程
    (2) 守护线程是否结束并不影响JVM的退出,只要用户线程都退出了,就会终止JVM进程。如果你希望在主线程结束后JVM进程马上结束,那么在创建线程时可以将其设置为守护线程。
    (3) 设置守护线程: thread.setDaemon(true) ;
    (4) main线程运行结束后, JVM会自动启动一个叫作DestroyJava VM 的线程, 该线程会等待所有用户线程结束后终止JVM进程。
    (5) 在默认情况下, tomcat的接受线程和处理线程都是守护线程, 这意味着当tomcat收到shutdown 命令后并且没有其他用户线程存在的情况下tomcat 进程会马上消亡,而不会等待处理线程处理完当前的请求。

7. ThreadLocal

  • 提供了线程本地变量,如果创建了一个ThreadLocal 变量,那么访问这个变量的每个线程都会有这个变量的一个本地副本。
  • 使用
//创建ThreadLocal 变量
ThreadLocal localVariable = new ThreadLocal<> () ;
//在线程1中
localVariable.set("threadOne local variable");
//在线程2中
localVariable.set("threadTwo local variable");
//在某个线程中获取变量
localVariable.get()
  • 原理
    (1) 在每个线程内部都有一个名为threadLocals 的成员变量, 该变量的类型为HashMap,用于存储线程本地变量。key 为我们定义的ThreadLocal变量的this引用, value 则为我们使用set 方法设置的值。
    (2) ThreadLocal只是相当于一个工具类,它通过set 方法把value 值放入调用线程的threadLocals 里面并存放起来, 当调用线程调用它的get 方法时,再从当前线程的threadLocals 变量里面将其拿出来使用。
    (3) 为了防止内存溢出,在使用完后,记得通过ThreadLocal的remove方法移除变量。
    (4) 同一个ThreadLocal 变量在父线程中被设置值后, 在子线程中是获取不到的。

  • InheritableThreadLocal: 让子线程能访问到父线程中的值
    (1) InheritabIeThreadLocal 继承了ThreadLocal,set和get操作的是该线程的成员变量inheritableThreadLocals。
    (2) 在创建线程时,会判断当前线程(父线程)中的inheritableThreadLocals变量是否为空,如果不为空,会把父线程的inheritableThreadLocals 成员变量的值复制到子线程的inheritableThreadLocals变量中。这样子线程就拥有了一份父线程的可继承的变量的副本。
    (3) 父线程和子线程的相互影响关系可以查看https://blog.csdn.net/v123411739/article/details/79117430
    对于可变对象:父线程初始化, 因为Thread Construct浅拷贝, 共用索引, 子线程修改父线程跟着变; 父线程不初始化, 子线程初始化, 无Thread Construct浅拷贝, 子线程和父线程都是单独引用, 不同对象, 子线程修改父线程不跟着变。
    对于不可变对象:不可变对象由于每次都是新对象, 所以无论父线程初始化与否,子线程和父线程都互不影响。

你可能感兴趣的:(JAVA并发编程(二):线程知识)