多线程随意总结

什么是线程

什么是进程

  • 正在进行的程序,是系统进行资源分配的基本单位
  • 目前操作系统都是支持多进程,可以同时执行多个进程,通过进程ID(PID)区分
  • 单核CPU在同一时刻,只能运行一个进程;宏观并行、微观串行

什么是线程

  • 线程,又称轻量级进程;进程中的一条执行路径,也是CPU的基本调度单位
  • 一个进程由一个或多个线程组成,彼此间完成不同的工作,同时执行,称为多线程
    • 迅雷是一个进程,当中的多个下载任务即是多个线程。(真正负责代码执行的是线程)
    • Java虚拟机是一个进程,当中默认包含主线程(main),可通过代码创建多个独立线程,与main并发执行

进程和线程的区别

  1. 进程是操作系统资源分配的基本单位,而线程是CPU的基本调度单位
  2. 一个程序运行后至少有一个进程
  3. 一个进程可以包含多个线程,但是至少需要有一个线程,否则这个进程是没有意义
  4. 进程间不能共享数据段地址,但同进程的线程之间可以

知乎大佬

类似进程是资源分配的最小单位,线程是CPU调度的最小单位这样的回答感觉太抽象,都不太容易让人理解。

做个简单的比喻:进程=火车,线程=车厢

  • 线程在进程下行进(单纯的车厢无法运行)
  • 一个进程可以包含多个线程(一辆火车可以有多个车厢)
  • 不同进程间数据很难共享(一辆火车上的乘客很难换到另外一辆火车,比如站点换乘)
  • 同一进程下不同线程间数据很易共享(A车厢换到B车厢很容易)
  • 进程要比线程消耗更多的计算机资源(采用多列火车相比多个车厢更耗资源)
  • 进程间不会相互影响,一个线程挂掉将导致整个进程挂掉(一列火车不会影响到另外一列火车,但是如果一列火车上中间的一节车厢着火了,将影响到所有车厢)
  • 进程可以拓展到多机,进程最多适合多核(不同火车可以开在多个轨道上,同一火车的车厢不能在行进的不同的轨道上)
  • 进程使用的内存地址可以上锁,即一个线程使用某些共享内存时,其他线程必须等它结束,才能使用这一块内存。(比如火车上的洗手间)-“互斥锁”
  • 进程使用的内存地址可以限定使用量(比如火车上的餐厅,最多只允许多少人进入,如果满了需要在门口等,等有人出来了才能进去)-“信号量”

作者:biaodianfu
链接:https://www.zhihu.com/question/25532384/answer/411179772
来源:知乎
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

线程的组成

  • 任何一个线程都具有基本的组成部分:
    • CPU时间片:操作系统(OS)会为每个线程分配执行时间
    • 运行数据:
      • 堆空间:存储线程需使用的对象,多个线程可以共享堆中的对象
      • 栈空间:存储线程需使用得局部变量,每个线程都拥有独立的栈
    • 线程的逻辑代码

线程的特点

  • 线程抢占式执行
    • 效率高
    • 可防止单一线程长时间独占CPU
  • 在单核CPU中,宏观上同时执行,微观上顺序执行

创建线程

  • 创建线程三种方式
    1. 通过继承Thread类,重写Thread的run()方法,将线程运行的逻辑放在其中
    2. 通过实现Runnable接口,实例化Thread类
    3. 实现Callable接口
  1. 继承Thread类(main方法为主线程,其中实现的线程为子线程;谁抢到CPU谁执行)

    1. 继承Thread类
    2. 覆盖run()方法
    3. 创建子类对象
    4. 调用start()方法
    • 获取和修改线程名称
      • 获取线程ID和线程名称
        • 在Thread的子类中调用this.getId()或this.getName()(必须继承Thread类)
        • 使用Thread.currentThread().getId()和Thread.currentThread().getName()(Thread.currentThread()获取当前线程)
      • 修改线程名称
        • 调用线程对象的setName()方法(必须在启动之前修改线程名称)
        • 使用线程子类的构造方法赋值(推荐)
  2. 实现Runnable接口

    1. 实现Runnable接口
    2. 覆盖run()方法
    3. 创建实现类对象 1、2、3不可用匿名内部类实现
    4. 创建线程对象
    5. 调用start()方法

线程的状态

  1. (New 初始状态)线程对象被创建,即为初始状态。只是在堆中开辟内存,与常规对象无异

    调用start()

  2. (Ready 就绪状态)调用start()之后,进入就绪状态。等待OS选中,并分配时间片

    OS选中->

    <-时间片到期

  3. (Running 运行状态) 获得时间片之后,进入运行状态,如果时间片到期,则回到就绪状态

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-I6cgFH3y-1608218745974)(C:\Users\wenwen\AppData\Roaming\Typora\typora-user-images\image-20201113121651405.png)]

线程常见方法

  • 睡眠
    • public static void sleep(long millis)
    • 当前线程主动休眠millis毫秒
  • 让步
    • public static void yield()
    • 当前线程主动放弃时间片,回到就绪状态,竞争下一次时间片、
    • 可以尽可能的保证线程的交替输出
  • 加入
    • public final void join()
    • 谁调用该方法,则加入该线程,并阻塞当前线程,直到加入线程执行完毕 ,当前线程继续执行
  • 优先级
    • 线程对象.setPriority()
    • 线程优先级为1-10,默认为5,优先级越高,表示获取CPU机会越多
  • 守护线程
    • 线程对象.setDaemon(true);设置为守护线程
    • 线程有两类:用户线程(前台线程)、守护线程(后台线程)
    • 如果程序中所有前台线程都执行完毕了,后台线程会自动结束
    • 垃圾回收器线程属于守护线程

线程安全

  • ABA问题
  • 多线程安全问题
    • 当多线程并发访问临界资源时,如果破坏了原子操作,可能会造成数据不一致
    • 临界资源:共享资源(同一对象),一次仅允许一个线程使用,才可保证其正确性
    • 原子操作:不可分割的多部操作,被视作一个整体,其顺序和步骤不可打乱或缺省

在程序应用中,如何保证线程的安全性?(同步)

  • 同步方式1

    • 同步代码块:

      synchronized(临界资源对象){ //对临界资源对象加锁

      ​ //代码(原子操作)

      }

    • 注意:

      • 每个对象都有一个互斥锁标记,用来分配给线程的
      • 只有拥有对象互斥锁标记的线程,才能进入对该对象加锁的同步代码块
      • 线程退出同步代码块时,会释放相应的互斥锁标记
  • 同步方式2

    • 同步方法:

      synchronized 返回值类型 方法名称(形参列表0){

      ​ //对当前执行方法对象(this)加锁

      ​ //如果时静态方法 synchronized static 返回值 方法名(){} 锁的时该类

      ​ //代码(原子操作)

      }

    • 注意:

      • 只有拥有对象互斥锁标记的线程,才能进入该对象加锁的同步方法中
      • 线程退出同步方法时,会释放相应的互斥锁标记

同步规则

  • 注意:
    • 只有在调用包含同步代码块的方法,或者同步方法时,才需要对象的锁标记
    • 如果用不包含同步代码块的方法,或普通方法时,则不需要锁标记,可直接调用
  • 已知JDK中线程安全的类:
    • StringBuffer
    • Vector
    • Hashtable
    • 以上类中的公开方法,均为synchonized修饰的同步方法

经典问题

死锁

  • 当第一个线程拥有A对象锁标记,并等待B对象锁标记,同时第二个线程拥有B对象所标记,并等待A对象所标记时,产生死锁

  • 一个线程可以同时拥有多个对象的锁标记,当线程阻塞时,不会释放已经拥有的锁标记,由此可能造成死锁

  • 死锁的四个条件;

    1.互斥条件:一个资源每次只能被一个进程使用。

    2.请求与保持条件:一个进程因请求资源而阻塞时,对已获得的资源保持不放。

    3.不剥夺条件:进程已获得的资源,在末使用完之前,不能强行剥夺。

    4.循环等待条件:若干进程之间形成一种头尾相接的循环等待资源关系。

生产者、消费者:

  • 若干个生产者在生产产品,这些产品将提供给若干个消费者去消费,为了使生产者和消费者能并发执行,在两者之间设置一个能存储多个产品的缓冲区,生产者将生产的产品放入缓冲区中,消费者从缓冲区中取走产品进行消费,显然生产者和消费者之间必须保持同步,即不允许消费者到一个空的缓冲区中去产品,也不允许生产者向一个满的缓冲区中放入产品
  package com.productconsume;
  
  public class Bread {
      private int id ;
      private String productName;
  
      public Bread() {
      }
  
      public Bread(int id, String productName) {
          this.id = id;
          this.productName = productName;
      }
  
      public int getId() {
          return id;
      }
  
      public String getProductName() {
          return productName;
      }
  
      @Override
      public String toString() {
          return "Bread{" +
                  "id=" + id +
                  ", productName='" + productName + '\'' +
                  '}';
      }
  }
package com.productconsume;

public class BreadCon {
    //存放面包的数组
    private Bread[] cons= new Bread[5];

    //存放面包的位置
    private int index = 0;

    //存放面包
    public synchronized void input(Bread b) throws InterruptedException {
        //判断容器有没有位置
        while (index>=5)
            this.wait();
        cons[index] = b;
        System.out.println(Thread.currentThread().getName()+ "生产了:"+b.getId());
        index++;
        //唤醒消费者
        this.notifyAll();
    }

    //取出面包
    public synchronized void output() throws InterruptedException {
        while (index<=0)
            this.wait();
        index--;
        Bread b = cons[index];
        System.out.println(Thread.currentThread().getName()+ "消费了:"+b.getId()+ "  生产者:"+b.getProductName());
        cons[index] = null;
        //唤醒生产者
        this.notifyAll();
    }
}
package com.productconsume;

public class Product implements Runnable{

    private BreadCon con;

    public Product(BreadCon con){
        super();
        this.con = con;
    }

    @Override
    public void run() {
        for (int i = 0; i < 10; i++) {
            try {
                con.input(new Bread(i,Thread.currentThread().getName()));
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }

    }
}
package com.productconsume;

public class Consume implements Runnable{
    private BreadCon con;

    public Consume(BreadCon con) {
        super();
        this.con = con;
    }

    @Override
    public void run() {
        for (int i = 0; i < 10; i++) {
            try {
                con.output();

            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}
package com.productconsume;

public class Test {
    public static void main(String[] args) {
        //容器
        BreadCon con = new BreadCon();
        //生产和消费
        Product prodcut = new Product(con);
        Consume consume = new Consume(con);
        //创建线程对象
        Thread chenchen = new Thread(prodcut, "晨晨");
        Thread bingbing = new Thread(consume, "冰冰");
        Thread liangliang = new Thread(prodcut, "亮亮");
        Thread xiaoxiao = new Thread(consume, "小小");
        //启动线程
        chenchen.start();
        bingbing.start();
        liangliang.start();
        xiaoxiao.start();
    }
}

线程通信

  • 等待:

    • public final void wait()

    • public final void wait(long timeout)

    • 必须在对obj加锁的同步代码块中。在一个线程中,调用obj.wait()时,此线程会释放其拥有的所有锁标记。同时此线程阻塞在o的等待队列中。释放锁,进入等待队列

  • 通知:

    • public final void notify()
    • public final void notifyAll()

小结

  • 线程的创建:
    • 方式1:继承Thread类
    • 实现Runnable接口(一个任务Task),传入给Thread对象并执行
  • 线程安全:
    • 同步代码块:为方法中的局部代码(原子操作)加锁
    • 同步方法:为方法中的所有代码(原子操作)加锁
  • 线程间的通信:
    • wait()/wait(long timeout):等待
    • notify()/notifyAll():通知

线程池

线程池概念

  • 问题:
    • 线程是宝贵的内存资源、单个线程约占1MB空间,过多分配易造成内存溢出
    • 频繁的创建及销毁线程会增加虚拟机回收频率、资源开销,造成程序性能下降
  • 线程池:
    • 线程容器,可设定线程分配的数量上限
    • 将预先创建的线程对象存入池中,并重用线程池中的线程对象
    • 避免了频繁的创建和销毁
    • 将任务提交给线程池,由线程池分配线程、运行任务,并在当前任务结束后复用线程

线程池的创建

  • Executor:线程池的根接口,execute()

  • ExecutorService:接口包含管理线程池的一些方法,submit(Runnable\ Callable)提交 shutdown()关闭

    • ThreadPoolExecutor实现类
      • ScheduledThreadPoolExecutor子类
  • Executors:创建线程池的工具类

    • 创建固定线程个数线程池
    • 创建缓存线程池,
    • 创建单线程池
    • 创建调度线程池 调度:周期、定时执行

Callable接口

  • public interface Callable{

    ​ public V call() throws Exception;

    }

  • 与Runnable接口类似,实现之后代表一个线程任务

  • Callable具有泛型返回值、可以声明异常

  • 基本使用

    public class Demo1 {
        public static void main(String[] args) throws Exception {
            //功能需求:使用Callable实现1-100的和\
            //1、创建Callable对象
            Callable<Integer> integerCallable = new Callable<Integer>(){
    
                @Override
                public Integer call() throws Exception {
                    System.out.println(Thread.currentThread().getName()+"计数开始");
                    int sum = 0;
                    for (int i = 1; i <= 10; i++) {
                            sum += i;
                            Thread.sleep(200);
                    }
                    return sum;
                }
            };
            //2、把Callable对象转成可执行任务
            FutureTask<Integer> integerFutureTask = new FutureTask<>(integerCallable);
            //3、创建线程
            Thread thread = new Thread(integerFutureTask);
            //4、启动线程
            thread.start();
            //5、获取结果
            Integer integer = integerFutureTask.get();
            System.out.println("结果是:"+ integer);
        }
    }
    
  • 结合线程池:

    public class Demo2 {
        public static void main(String[] args) throws Exception {
            //1、创建线程池
            ExecutorService es = Executors.newCachedThreadPool();
            //2、提交任务    Future:表示将要执行任务的结果
            Future<Integer> future = es.submit(new Callable<Integer>() {
    
                @Override
                public Integer call() throws Exception {
                    System.out.println(Thread.currentThread().getName()+"开始:");
                    int sum = 0;
                    for (int i = 1; i <= 10; i++) {
                        sum += i;
                        Thread.sleep(200);
                    }
                    return sum;
                }
            });
            Future<Integer> future2 = es.submit(new Callable<Integer>() {
    
                @Override
                public Integer call() throws Exception {
                    System.out.println(Thread.currentThread().getName()+"开始:");
                    int sum = 0;
                    for (int i = 11; i <= 20; i++) {
                        sum += i;
                        Thread.sleep(200);
                    }
                    return sum;
                }
            });
            //3、获取任务结果
            System.out.println(future.get());
            System.out.println(future2.get());;
            //4、关闭线程池
            es.shutdown();
        }
    }
    

Future接口

  • Future:表示将要完成任务的接口
  • 表示ExecutorService.submit()所返回的状态结果,就是call()的返回值
  • 方法:V get()以阻塞形式等待Future中的异步处理结果(call()的返回值)

什么是同步?什么是异步?

  • 同步:
    • 形容一次方法调用,同步一旦开始,调用者必须等待该方法返回,才能继续
    • 单挑执行路径
  • 异步
    • 形容一次方法调用,异步一旦开始,像是一次消息传递,调用者告知以后立刻返回。二者竞争时间片,并发执行
    • 多条执行路径

Lock接口

  • JDK5加入,与synchronized比较,显示定义,结构更灵活
  • 提供更多实用性方法,功能更强大、性能更优越
  • 常用方法:
    • void lock()//获取锁,如锁被占用,则等待
    • boolean tryLock()//尝试获取锁(成功返回true。失败返回false,不阻塞)
    • void unlock()//释放锁

重入锁(Lock实现类)

  • ReentrantLock:Lock接口的实现类,与synchronized一样具有互斥锁功能
  • 步骤:
    1. 创建重入锁对象
    2. 显示开启锁
    3. 显示释放锁

读写锁

  • ReentrantReadWriteLock:
    • 一种支持一写多读的同步锁,读写分离,可分别分配读锁、写锁
    • 支持多次分配读锁,使用多个读操作进行并发执行
  • 互斥规则:
    • 写-写:互斥,阻塞
    • 读-写:互斥,读阻塞写、写阻塞读
    • 读-读:不互斥、不阻塞
    • 在读操作远远高于写操作的环境中,可在保障线程安全的情况下,提高运行效率
  • 步骤:
    1. 创建读写锁
    2. 获取读锁
    3. 获取写锁
    4. 写读取方法
      1. 上锁
      2. 解锁
    5. 写写入方法
      1. 上锁
      2. 解锁
  • 效率:
    • 互斥锁运行时间20s,读写锁运行时间3s(2次写,18次读)

线程安全集合

  • Collection体系集合中,除Vector以外的线程安全集合

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-TrFLYpUV-1608218745977)(C:\Users\wenwen\AppData\Roaming\Typora\typora-user-images\image-20201115104827498.png)]

  • Map安全集合

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-r3IO5oTr-1608218745978)(C:\Users\wenwen\AppData\Roaming\Typora\typora-user-images\image-20201115105459290.png)]

Collections工具类中提供了多个可以获得线程安全集合的方法

  • public static Collection synchronizedCollection(Collection c)

  • public static List synchronizedList(List list)

  • public static Set synchronizedSet(Set s)

  • public static <K,V> Map<K,V> synchronizedMap(Map<K,V> m)

  • public static SortedSet synchronizedSortedSet(SortedSet s)

  • public static <K,V> SortedMap<K,V> synchronizedSortedMapt(SortedMap<K,V> m)

  • JDK1.2提供,接口统一、维护性高,但性能没有提升,均以synchronized实现

  • 现在不常用,性能不高

CopyOnWriteArrayList

  • 线程安全的ArrayList,加强版的读写分离
  • 写有锁,读无锁,读写之间不阻塞,优于读写锁
  • 写入时,先copy一个容器副本、在添加新元素,最后替换引用
  • 缺点:浪费空间
  • 使用方式与ArrayList无异

CopyOnWriteArraySet(有序)

  • 线程安全的Set,底层使用CopyOnWriteArrayList实现
  • 唯一不同在于,使用addIfAbsent()添加元素,会遍历数组
  • 如存在元素,则不添加(扔掉副本)
  • 重复依据:equals方法

Queue接口(队列)

  • Collection的子接口,表示队列FIFO(First In First Out)先进先出
  • 常用方法:
    • 抛出异常:
      • boolean add(E e)//顺序添加一个元素(到达上限后,再添加则会抛出异常)
      • E remove()//获得第一个元素并移除(如果队列没有元素时,则抛出异常)
      • E element()//获得第一个元素但不移除(如果队列没有元素时,则抛出异常)、
    • 返回特殊值:推荐使用
      • boolean offer(E e)//顺序添加一个元素(到达上限后,再添加则会返回false)
      • E poll()//获得第一个元素并移除(如果队列没有元素时,则返回null)
      • E peek()//获得第一个元素但不移除(如果队列没有元素时,则返回null)

ConcurrentLinkQueue

  • 线程安全、可高效读写的队列,高并发下性能最好的队列
  • 无锁、CAS比较交换算法(硬件所支持的算法),修改的方法包含三个核心参数(V,E,N)
  • V:要更新的变量、E:预期值、N:新值
  • 只有当V==E时,V=N;否则表示已被更新过,则取消当前操作

BlockingQueue接口(阻塞队列)

  • Queue的子接口,阻塞的队列,增加了两个线程状态为无期限等待的方法

  • 方法:

    • void put (E e )//将指定元素插入此队列中,如果没有可用空间,则等待
    • E take()//获取并移除此队列头部元素,如果没有可用元素,则等待
  • 实现类:

    • ArrayBlockQueue:
      • 数组结构实现,有界队列(手工固定上限)
    • LinkedBlockingQueue:
      • 链表结构实现,有界队列(默认上限Integer.MAX_VALUE)
  • 使用阻塞队列实现生产者和消费者

public class Demo1 {
    public static void main(String[] args) {
        //1、创建队列
        ArrayBlockingQueue<Integer> integers = new ArrayBlockingQueue<>(5);
        //2、创建两个线程
        Thread thread = new Thread(new Runnable() {
            @Override
            public void run() {
                for (int i = 1; i <= 10; i++) {
                    try {
                        integers.put(i);
                        System.out.println(Thread.currentThread().getName()+"生产了"+i+"号面包");
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        },"龙龙");
        Thread thread2 = new Thread(new Runnable() {
            @Override
            public void run() {
                for (int i = 1; i <= 10; i++) {
                    try {
                        Integer take = integers.take();
                        System.out.println(Thread.currentThread().getName()+"消费了"+take+"号面包");
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        },"丽丽");

        thread.start();
        thread2.start();
    }

ConcurrentHashMap

  • 初始容量默认为16段(Segment),使用分段锁设计
  • 不对整个Map加锁,而是为每个Segment加锁
  • 当多个对象存入同一个Segment时,才需要互斥
  • 最理想状态为16个对象分别存入16个Segment,并行数量16
  • 使用方法与HashMap无异
  • JDK1.8之后采用CAS

总结

  • ExecutorService线程池接口、Executors工厂
  • Callable线程任务、Future异步返回值
  • Lock、ReentrantLock重入锁、ReentrantReadWriteLock读写锁
  • CopuOnWriteArrayList线程安全的ArrayList
  • CopyOnWriteArraySet线程安全的Set
  • ConCurrentLinkedQueue线程安全的Queue
  • ArrayBlockingQueue线程安全的阻塞Queue(生产者、消费者)
  • ConcurrentHashMap线程安全的HashMap
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值