JUC概述

JUC概述

  • java.util.concurrent 工具包的简称
  • 这是一个处理线程的工具包,JDK1.5 开始出现

1. 线程和进程的概念

1. 1 线程与进程

  1. 什么是进程?

    进程是计算机中的程序关于某数据集合上的一次运动,是系统进行资源分配调度的基本单位.

  2. 什么是线程?

    是操作系统能够进行运算调度的最小单位。它被包含在进程之中,是进程中的实际运作单位。

    它是进程的一个的一个执行路径,一个进程中至少有一个线程,进程中的多个线程共享进程的资源

  3. 操作系统在分配资源的时候是吧资源分配给进程的,但是CPU资源是被分配到线程的,也就是说线程是CPU分配的基本单位

  • 程序计数器是一块内存区域,用来记录线程的当前要执行的指令地址
    • 注意: 如果执行的是native方法,那么pc计数器记录的是 undefined 地址
  • 栈: 每个线程都有自己的栈资源,用来储存线程的 局部变量 和 调用栈帧
  • 堆 : 里面主要存放使用 new 操作创建的对象实例
  • 方法区 : 用来存放JVM 加载类、常量 以及 静态变量等信息

1.2 线程的创建

  • 在Java中,当我们启动main函数时,其实就启动了一个JVM进程,而main函数所在的线程就是这个进程中的一个一个线程,也称主线程

  • 线程创建有三种方法 :

    1. 继承Thread类,并重写run方法

      Thread t = new Thread("t1"){
          public void run(){
          }
      };
      t.start();
      

      好处:方便传参,可以在子类中添加成员变量,然后进行传递

      坏处:java不支持多继承,继承了Thread,就没法继承其他类。任务与代码没有分离

      ​ 无返回值

    2. 实现Runnable接口 配合 Thread

      将 线程 和 任务 分开

      Runnable runnable = new Runnale(){
          public void run(){
              
          }
      };
      Thread t = new Thread(runnable);
      t.start;
      
      //jdk8 lambda
      Runnable task2 = () -> log.debug("hello");
      Thread t2 = new Thread(task2,"t2");
      t2.start;
      

      好处:任务 和 代码分离

      坏处:只能用主线程中final修饰的变量

      ​ 无返回值

    3. FutureTask 配合 Thread

      FutureTask 能够接受 Callable 类型的参数,用来处理有返回结果的情况

      FutureTask<Integer> task3 = new FutureTask<>(() -> {
          log.debug("hello");
          return 100;
      });
      new Thread(task3,"t3").start;
      Integer result = task3.get();
      log.debug("结果是:{}",)
      
  • 注意: 当创建完new对象后,该线程并没有被启动执行,直到调用start方法才真正启动。但是start后,线程也并没有马上执行而是处于就绪状态,即该线程已经获取了除了CPU资源外其他资源,等待获取CPU资源后,才会真正处于运行状态。

  • 注意:不要用run()方法来开启新线程,它只会在当前线程中串行执行run()方法中的代码

1.3 线程的状态

  1. 线程状态枚举类

    Thread.State:

    • NEW(新建)
    • RUNNABLE(准备就绪)
    • BLOCKED(阻塞)
    • WAITING(不见不散)
    • TIMED_WAITING(过时不候)
    • TERMINATED(终结)
  2. 构成线程状态的5个标识分别是:线程名称、线程存活的标识、线程的执行状态、线程的优先级以及该线程是否为守护线程的标识

1.4 wait 和 sleep等方法

java.lang.Object类提供了一套等待、通知的API,它由三个wait()、notify() 和 一个 notifyAll()组成

这也意味着任何对象都可以调用这些方法

这组API利用了一个对象的条件队列,该队列就是一种数据结构,用于储存那些等待某个条件成立的线程。

这些等待的线程称为等待集合

  1. wait函数

    • 当在一个对象实例上调用wait()方法后,当前线程就会在这个对象上等待。

    • 当一个线程调用一个共享变量的wait方法时,该线程会被阻塞挂起,直到以下情况

      • 其他线程调用了该共享变量的notify()、notifyAll()
      • 其他线程调用了该线程的interrupted()方法,该线程抛出InterruptedException 返回
      • 对象的wait()方法被调用的时候,线程会放弃对象关联的监听器的所有权
    • 如果调用wait方法的线程没有 事先 获取该对象的监视器锁,则会抛出IllegalMonitorStateException

    • 如何获取?

      • 执行synchronized同步代码块时,该共享变量作为参数

        synchronized(共享变量){}
        
      • 调用该共享变量的方法,该方法用synchronized修饰

        synchronized void add(int a, int b){}
        
    • 虚假唤醒:一个线程从挂起编程运行状态,但是这个线程没有被其他线程通知,或者被中断、或者等待超时

      • 所以,我们判断条件要放在while循环中,防止虚假唤醒
    • 注意:Object的wait()方法不能随便调用,它必须包含在对应的synchronized语句中

  2. wait(long timeout):超时返回

  3. notify():会在该共享变量上的调用wait系列方法后被挂起的线程,该线程是随机的。

  4. notifyAll()

  5. join方法:Thread类提供的方法,无参,返回值为0

    比如:主线程调用了线程1的join,表示主线程在等线程1完成任务,而这时有线程中断主线程,则会抛出InterruptedException

    它让调用线程在当前线程对象上进行等待

    public class JoinMain{
        public volatile static int i = 0;
        public static class AddTread extends Thread{
            public void run(){
                for( i = 0; i < 1000000; i++);
            }
        }
        public static void main(String[] args) throws InterruptedException{
            AddThread at = new AddThread();
            at.start();
            at.join();
            System.out.println(i);
        }
    }
    
  6. sleep方法:调用后线程会暂时让出指定时间的CPU使用权,但是该线程所拥有的监视器资源还是不让出的

    • 如果睡眠期间被Interrupt 会抛出异常
  7. yield方法:Thread类提供的方法,当一个线程调用yield方法时,当前线程会让出CPU执行权,但是只是让出自己剩余的时间片,并没有被阻塞挂起,而是处于就绪状态,下次仍然可以被调度到

sleep wait
是 Thread 的静态方法 Object的方法,任何实例对象都能调用
sleep()可以在任何需要的场景下调用 wait()必须使用在同步代码块或同步方法中
不会释放锁,也无需占用锁 会释放锁,但调用它的前提是当前线程占有锁(即代码要在synchronized中)
可被interrupted方法中断 可被interrupted方法中断

1.41 终止线程

  1. stop()方法

    被标注为废弃的方法

    过于暴力

1.42 线程中断

严格来说,线程中断并不会使线程立即退出,而是给线程发送一个消息,告知目标线程。至于后续如何处理,则完全由线程决定。

public void Thread.interrupt() //中断线程
public boolean Thread.isInterrupted() //判断是否被中断
public static boolean Thread.interrupted() //判断是否被中断,并清除当前中断状态

注意:

  1. 进行了中断后,要有对中断进行处理的代码,否则这个中断也不会发生任何作用
  2. Thread.sleep()方法由于中断而抛出异常,此时,它会清除中断标记,如果不加处理,那么在下一次循环开始时,就无法捕获这个中断,故在异常处理中,要再次设置中断标记位

1.5 并发和并行

  1. 串行模式

    一次只能取得一个任务,并执行这个任务

  2. 并行模式

    同时取得多个任务,并同时去执行所取得的这些任务

  3. 并发

    同一时刻多个线程在访问同一个资源,多个线程对一个点(concurrent)

    如:春运抢票,电商秒杀

  4. 并行

    多项工作一起执行,之后再汇总

    例子:泡面,电水壶烧水,同时撕调料包

1.6 应用之异步调用

同步、异步通常用来形容一次方法调用

  1. 同步 vs 异步

    • 同步: 需要等待结果返回,才能继续运行

      是JVM的特性,它用于保证两条及两条以上的线程不会再同一个临界区中执行

    • 异步: 不需要等待结果返回

    • 注意: 同步在多线程中还有另一个意思: 让多个线程步调一致

  2. 设计

    多线程可以让方法执行变为异步的

  3. 同步的两个属性

    • 互斥
    • 可见性
  4. 同步如何实现?

    • 基于监听器实现出来的,它是控制对临界区进行访问的并发构造,必须不可分割地执行。每个Java对象都关联一个监听器,线程可以通过上锁或解锁的方式获取或者释放监听器上的锁

1.7 应用之提高效率

充分利用多核CPU的优势,提高运行效率

总结

  1. 单核CPU下,多线程不能提高效率,只是为了能够在不同的任务之间来回切换,让不同的线程轮流使用CPU
  2. 多核CPU可以并行跑多个程序,但能否提高效率还是要分情况的
  3. IO操作不占用CPU,只是我们一般拷贝文件使用的是 阻塞IO ,这时线程虽然不用CPU,但需要一直等待IO结束,没能充分利用线程。所以才有后面的 非阻塞IO异步IO 优化

1.8 原理之Thread 与 Runnable 的关系

  1. 方法2 把 runnable 对象当成参数传给了 Thread的构造函数,最终调用了target.run()
  2. 方法1 重写了run 方法
  3. 方法1 是把线程和任务合并在了一起,方法2 是把线程和任务分开了
  4. 用Runnable 更容易与线程池等高级API 配合
  5. 用Runnable 让任务脱离了Thread 继承体系,更令灵活

1.9 临界区

临界区用来表示一种公共资源或者说共享数据,可以被多个线程使用。但是每一次只能有一个线程使用

1.10 并发级别

  1. 阻塞

    一个线程是阻塞的,那么在其他线程释放资源前,当前线程无法继续执行

  2. 无饥饿

  3. 无障碍

    无障碍是一种最弱的非阻塞调度。

    如果两个线程无障碍地执行,那么不会因为临界区的问题导致一方被挂起

  4. 无锁

    无锁的并行都是无障碍的

    CAS

  5. 无等待

2. 什么是上下文切换

当前线程使用完时间片后,就会处于就绪状态并让出CPU让其他线程占用,在某种程度上可以说是多个线程共享一个处理器的产物


  • 分类:
    • 自发性:
      • Thread.sleep(long millis)
      • Object.wait()/wait(long timeout)/wait(long timeout, int nanos)
      • Thread.yield(),可能不会
      • Thread.join()
      • LockSupport.park()
    • 非自发性

3. 守护线程(Daemon) 和 用户线程

守护线程时一种特殊的线程,它是系统的守护者,在后台默默地,完成一些系统性的服务

比如:垃圾回收线程、JIT线程就可以理解为守护线程

注意:设置守护线程必须在线程start前设置。

当最后一个非守护线程结束时,JVM会正常退出

4. 线程中的问题

  1. 竞态条件

    当计算的正确性取决于相对时间或者调度器所控制的多线程交叉时,静态条件就会发生

    a. check-then-act :

    if(a == 10.0){
        b = a / 2.0;
    }
    //如果a,b是局部变量,那么每个线程都会有自己的局部变量的拷贝,所以竞态不会发生
    

    b. read-modify-write:

    public int getID(){
        return count ++;
    }
    
  2. 数据竞争

    两条或两条以上的线程(在单个应用中)并发地访问同一块内存区域,同时同时至少有一条是为了写,而且这些线程没有协调对那块内存区域的访问

    private static Parser parser;
    public static Parser getInstance()
    {
        if(parse == null) 
            parse = new Paeser()
         return parse
    }
    
  3. 缓存问题

5. 额外的线程能力

  1. 线程组

一个线程组代表了一组线程

使用一个线程组,你可以对其中的所有线程进行统一操作以简化线程管理

但,多数有用的ThreadGroup方法都已经废弃,并且在获取一组活跃线程数和列举这些线程之间存在“检查时间到使用时间”这一类别竞态条件

public class ThreadGroupName implements Runnable{
    public static void main(String[] args){
        ThreadGroup tg = new ThreadGroup("PrintGroup");
        Thread t1 = new Thread(tg, new ThreadGroupName(),"T1");
        Thread t2 = new Thread(tg, new ThreadGroupName(),"T2");
        t1.start();
        t2.start();
        System.out.println(tg.activeCount());
        tg.list();
    }
    public void run(){
        String groupAndName = Thread.currentThread().getThreadGroup().getName) +"-"+Thread.currentThread().getName();
        while(true){
            System.out.println("I am" + groupName);
            try{
                Thread.sleep(1000);
            }catch(InterruptedException e){
                e.printStackTrace();
            }
        }
    }
}
  1. 线程局部变量

每个ThreadLocal的实例代表了一个线程局部变量,它为访问该变量的每个线程提供了单独的存储槽

  1. 定时器框架

java 1.3 引入 java.util.Timer 和 java.util.TimerTask 可以更加便利的构造一个任务调度框架

Timer 让你能够在一个后台线程中调度 TimerTasks 用于后续执行

定时任务都是TimerTask 子类的实例,这些子类实现了Runnable 接口。

public class TimerDemo
{
    public static void main(String[] args)
    {
        TimerTask task = new TimerTask()
        {
            public void run()
            {
                System.out.println("alarm going off!");
                System.exit(0);
            }
        };
        Timer timer = new Timer();
        timer.schedule(task,0,2000);
    }
}
posted @ 2022-02-20 15:50  ftfty  阅读(611)  评论(0编辑  收藏  举报