多线程

理解多线程

多线程

  • 程序 :用某种语言编写的一组指令的集合

  • 进程 :正在运行的程序

  • 线程 :一个进程可以细分为多个线程

 

多核CPU:在同一时间单元内,可以执行多个线程

单核CPU:在同一时间单元内,只能执行一个线程

并行:多个CPU执行不同的任务

并发:一个CPU执行不同的任务

多线程基础

  • 新建状态:

    使用 new 关键字和 Thread 类或其子类建立一个线程对象后,该线程对象就处于新建状态。它保持这个状态直到程序 start() 这个线程。

  • 就绪状态:

    当线程对象调用了start()方法之后,该线程就进入就绪状态。就绪状态的线程处于就绪队列中,要等待JVM里线程调度器的调度。

  • 运行状态:

    如果就绪状态的线程获取 CPU 资源,就可以执行 run(),此时线程便处于运行状态。处于运行状态的线程最为复杂,它可以变为阻塞状态、就绪状态和死亡状态。

  • 阻塞状态:

    如果一个线程执行了sleep(睡眠)、suspend(挂起)等方法,失去所占用资源之后,该线程就从运行状态进入阻塞状态。在睡眠时间已到或获得设备资源后可以重新进入就绪状态。可以分为三种:

    • 等待阻塞:运行状态中的线程执行 wait() 方法,使线程进入到等待阻塞状态。

    • 同步阻塞:线程在获取 synchronized 同步锁失败(因为同步锁被其他线程占用)。

    • 其他阻塞:通过调用线程的 sleep() 或 join() 发出了 I/O 请求时,线程就会进入到阻塞状态。当sleep() 状态超时,join() 等待线程终止或超时,或者 I/O 处理完毕,线程重新转入就绪状态。

  • 死亡状态:

    一个运行状态的线程完成任务或者其他终止条件发生时,该线程就切换到终止状态。


线程包括五个转态:新建,就绪,运行,阻塞,死亡

 

 

线程的优先级

每一个 Java 线程都有一个优先级,这样有助于操作系统确定线程的调度顺序。

Java 线程的优先级是一个整数,其取值范围是 1 (Thread.MIN_PRIORITY ) - 10 (Thread.MAX_PRIORITY )。

默认情况下,每一个线程都会分配一个优先级 NORM_PRIORITY(5)。

具有较高优先级的线程对程序更重要,并且应该在低优先级的线程之前分配处理器资源。但是,线程优先级不能保证线程执行的顺序,而且非常依赖于平台。


创建一个线程

Java 提供了三种创建线程的方法:

  • 通过实现 Runnable 接口;

  • 通过继承 Thread 类本身;

  • 通过 Callable 和 Future 创建线程。

串行 并行 并发

串行:一个任务/指令 执行完,下一个任务才执行

并行:在同一时刻,同时执行多个任务/指令

并发:在微观上,在同一时刻,只能执行一个任务/指令,在宏观上,在一段时间内可执行多个任务/指令,只不 过,把时间分成若干份,使得多个任务交换执行。

并发三大特点:有序性,可见性,原子性

有序性:程序的执行顺序由代码的先后顺序执行

可见性:多个线程同时执行,当一个线程修改了共享变量的值,其他线程能够看见

原子性:一个或多个操作,要么执行完毕中途不被外界干扰所打断,要么不执行,

  • 理解synchronized

    就是一个同步监视器,俗称锁。调用它是,当有多个线程在同时调用一个方法时,当有一个线程在执行,与他同步的线程,要等该线程执行完才执行。

  • 死锁

    不同的线程分别占用对方需要的同步资源不放,都在等待对方放弃自己所需要的资源,就形成了死锁。

    不会发生异常,也不会给出任何提示,所有线程处于阻塞状态,无法继续。

  • Lock锁

就是 new 一个 ReentrantLock ,在需要使用加锁的代码块,加锁;

ReentrantLock lock = new ReentrantLock();
lock.lock();
/*
代码块
*/
lock.unlock();
  • 谈谈对线程安全的理解

  线程安全可以理解为内存安全,堆是内存共享,可以被所有的内存访问。

  当多个线程访问同一个对象时,如果不进行额外的同步控制或者其他的协调操作,调用这个对象的行为都可以获得正确的结果,我们就说这个对象时线程安全的。

  堆是进程和线程共有的空间,分全局堆和局部堆,全局堆就是所有没有分配的空间,局部堆就是用户分配的空间,堆在操作系统对进程初始化的时候分配,运行过程中也可以向系统要额外的堆,但是用完了要还给系统,不然就是内存泄漏。

  在java中,堆是java虚拟机所管理的内存中最大的一块,是所有线程共享的一款线程区域,在虚拟机启动时创建,堆所存在的内存区域唯一目的就是存放对象实例,几乎多有的对象实例和数组都放在堆中。

  栈是每个线程独有的,保存其运行状态和局部自动变量的。栈在线程开始的时候初始化,每个线程的栈互相独立,栈是线程安全的。操作系统在切换线程的时候回自动切换栈,栈空间不需要再高级语言里显式的分配和释放。

  在每个进程的内存空间都有一块特殊的公共区域,通常称为堆内存,进程内多有的线程都可以访问该区域,这就是造成问题的潜在原因。

  • Runnable和Thread有什么区别

    其实Thread也就是实现了Runnable接口,提供了更多的方法而已。所以说Thread与Runnable并没有什么区别。如果硬要说有什么区别的话,那就是类与接口的区别,继承与实现的区别。

  • ThreadLocal

    ThreadLocal 定义了4个方法:

    get():返回此线程局部变量的当前线程副本中的值。 initialValue():返回此线程局部变量的当前线程的“初始值”。 remove():移除此线程局部变量当前线程的值。 set(T value):将此线程局部变量的当前线程副本中的值设置为指定值。

    ThreadLocal的核心是通过静态内部类ThreadLocalMap来处理数据的,我们先看下ThreadLocalMap的源码。

    static class Entry extends WeakReference<ThreadLocal<?>> {
      /** The value associated with this ThreadLocal. */
      Object value;
    Entry(ThreadLocal<?> k, Object v) {
      super(k);
      value = v;
    }
    }

Entry是ThreadLocalMap核心存储key-value的。,继承WeakReference,Entry所对应key(ThreadLocal实例)的引用为一个弱引用 。

  • ThreadLocal使用场景

数据库链接

session管理

ThreadLocal并不是为了解决多线程的数据共享,只是从另外一个方向来解决并发的问题,让变量线程局部化,就不存在并发。

  • 理解volatile

    底层原理就是

 

 

  • threadloacal内存泄漏问题如何避免

由于Thread中包含变量ThreadLocalMap,因此ThreadLocalMap与Thread的生命周期是一样长,如果都没有 手动删除对应key,都会导致内存泄漏。

但是使用弱引用可以多一层保障:弱引用ThreadLocal不会内存泄漏,对应的value在下一次ThreadLocalMap 调用set(),get(),remove()的时候会被清除。

因此,ThreadLocal内存泄漏的根源是:由于ThreadLocalMap的生命周期跟Thread一样长,如果没有手动删除 对应key就会导致内存泄漏,而不是因为弱引用。

ThreadLocal正确的使用方法:

每次使用完ThreadLocal都调用它的remove()方法清除数据

将ThreadLocal变量定义成private static,这样就一直存在ThreadLocal的强引用,也就能保证任何时候都能 通过ThreadLocal的弱引用访问到Entry的value值,进而清除掉 。

  • 线程池中线程的复用原理

    线程池最大的作用就是复用线程。在线程池中,经过同一个线程去执行不一样的任务,减少反复地创建线程带来的系统开销,就是线程的复用。

  • 为什么要使用线程池

    1.减少资源消耗 Thread线程,是操作系统的资源,创建和销毁是要有资源消耗的,如果有线程池事先准备好一批线程,创建线程和销毁线程的资源就没有了

    2.使用线程池缩短任务的执行时间。 有一个新的任务就new 一个线程,那么时间消耗为:New Thread() T1:线程的创建时间,T2:任务的执行时间 ,T3:线程的销毁时间 准备好一堆的线程就不需要T1 T3

    3.线程是稀缺而昂贵的资源,因为线程创建出来消耗CPU,消耗内存(一定消耗内存),线程执行太多,多操作系统是一种负担,使用某种机制把线程统一管理

  • 线程池中源码的参数解释

    corePoolSize:线程池核心线程数量

    maximumPoolSize:线程池最大线程数量

    keepAliverTime:当活跃线程数大于核心线程数时,空闲的多余线程最大存活时间

    unit:存活时间的单位

    workQueue:存放任务的队列

    handler:超出线程范围和队列容量的任务的处理程序

  • 线程池的实现原理

提交一个任务到线程池中,线程池的处理流程如下:

1、判断线程池里的核心线程是否都在执行任务,如果不是(核心线程空闲或者还有核心线程没有被创建)则创建一个新的工作线程来执行任务。如果核心线程都在执行任务,则进入下个流程。

2、线程池判断工作队列是否已满,如果工作队列没有满,则将新提交的任务存储在这个工作队列里。如果工作队列满了,则进入下个流程。

3、判断线程池里的线程是否都处于工作状态,如果没有,则创建一个新的工作线程来执行任务。如果已经满了,则交给饱和策略来处理这个任务。

  • 说说对守护线程的理解

    作用

    守护线程拥有自动结束自己生命周期的特性,而非守护线程不具备这个特点

    场景:

    JVM 中的垃圾回收线程就是典型的守护线程,如果说不具备该特性,会发生什么呢?

    当 JVM 要退出时,由于垃圾回收线程还在运行着,导致程序无法退出,这就很尴尬了!!!由此可见,守护线程的重要性了。

    通常来说,守护线程经常被用来执行一些后台任务,但是呢,你又希望在程序退出时,或者说 JVM 退出时,线程能够自动关闭,此时,守护线程是你的首选。

 

 

问题

  1. 如何开启线程?怎么保证线程安全?

    线程和进程的区别:进程是操作系统进行资源分配的最小单元;线程是操作系统进行任务分配的最小单位;线程隶属于进程。

    如何开启线程?

    • 继承Thread类,重写run方法;

    • 实现Runnable接口,实验run方法;

    • 实现Callable接口,实现call方法,通过FutureTask创建一个线程,获取到线程执行的返回值

    • 通过线程池来开启线程

    怎么保证线程安全?加锁

    • JVM提供的锁,也就是Synchronized关键字

    • JDK提供的各种锁Lock

  2. volatile 和synchronized 有什么区别?volatile是否线程安全呢?

    dcl单例为什么要加volatile?

    synchronized关键字。用来加锁。volatile只是保持变量的线程可见性,通常适用于一个线程写,多个线程读的场景。

    不能保证线程安全。volatile关键字只能保证线程的可见性,不能保证原子性。

    DCL:double check lock双重检查锁,如下面的代码

    public class Singleton {
       private static volatile Singleton INSTANCE = null;
       private Singleton(){}
       public static Singleton getInstance(){
           if(INSTANCE==null){//第一次检查
               synchronized (Singleton.class){
                   if(INSTANCE==null){//第二次检查
                       INSTANCE = new Singleton();
                  }
              }
          }
           return INSTANCE;
      }
    }

为什么要加两次判空,第一次判空能不能不加? 效率问题:假设第一次判空不加,那么每次进入这个方法,INSTANCE不论是不是null,都会执行下面的synchronized代码块,多线程下会出现锁的竞争,而除了第一次初始化,后面的都不会为null,判空的效率比加锁高。

为什么要进行第二次判空? 防止多次初始化:多线程下,有可能会出现两个线程都经过了前面第一次检查,来到了下面的synchronized这里,如果不判空,就会出现一个线程new了一个Singleton出来,然后释放锁,第二个线程进来又会new一个Singleton出来。

volatile能不能不加? volatile作用:

保持内存可见性 防止指令重排序 volatile这里的作用就是防止指令重排,INSTANCE = new Singleton();这一行主要做了下面几件事: 在内存中开辟空间 执行构造方法初始化对象 将对象的引用赋值给INSTANCE变量 在不加volatile的情况下,第2和第3步是有可能发生指令重排的,即执行顺序变成了1、3、2,假如我刚好执行到第3步,还没执行第2步,这时候另外一个线程调了这个方法,获取到的是还没执行初始化函数的对象,在上面的代码中,初始化函数什么都没做,所以没什么影响。但是如果初始化函数中需要做一些操作,那就有影响了。

  • 用Callable创建线程

    package com.ljx.test01.ThreadTest;

    import java.util.concurrent.Callable;
    import java.util.concurrent.ExecutionException;
    import java.util.concurrent.FutureTask;

    /**
    * @author 李捷禧
    * Date: 2022/11/6
    * ClassName: ThreadTest
    * email: 2465594828@qq.com
    */
    public class ThreadTest implements Callable<Integer> {

       public static void main(String[] args) throws ExecutionException, InterruptedException {
           ThreadTest threadTest = new ThreadTest();
           FutureTask<Integer> integerFutureTask = new FutureTask<>(threadTest);
           new Thread(integerFutureTask).start();
           new Thread(integerFutureTask).start();
           //子线程返回的过程及结果时间
           Integer result = integerFutureTask.get();
           //子线程返回结果
           System.out.println("子线程的返回结果:"+result);
           System.out.println(Thread.currentThread().getName()+"->我是主线程!");
      }
       @Override
       public Integer call() throws Exception {
           try {

          } catch (Exception e) {
               e.printStackTrace();
          }
           System.out.println(Thread.currentThread().getName()+"->我是子线程!");
           return 1;
      }
    }
public class TestNumber{
   public static void main(String[] args) {
       ThreadTest threadTest = new ThreadTest();
       ThreadTest02 threadTest02 = new ThreadTest02();
       Thread thread = new Thread(threadTest02);
       threadTest.start();
       thread.start();
       new Thread(new Runnable() {
           @Override
           public void run() {
               System.out.println("我是李");
          }
      }).start();
  }
}

class ThreadTest extends Thread{
   @Override
   public void run() {
       System.out.println("我是重写的run方法");
  }
}
class ThreadTest02 implements Runnable{

   @Override
   public void run() {
       System.out.println("我是runnable接口");
  }
}
posted @ 2023-03-12 00:08  jessi呀  阅读(63)  评论(0编辑  收藏  举报