欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  IT编程

多线程

程序员文章站 2022-04-17 20:10:18
多线程 实现多线程共有两种方法:①继承一个Thread类;②实现Runnable、Callable接口(推荐) 一、继承Thread类实现多线程 Thread是一个线程操作的核心类,定义一个线程的主类最简单的方法就是继承Thread类,而后覆写类中的run()方法。实现如下(首先定义一个主体类,而后 ......

大一菜鸡的个人笔记,欢迎指点和交流。

多线程

多线程

原来只有一条路,车多了会拥挤,效率极低

多加几个车道,效率高,即多线程。

进程与线程

进程:在操作系统中运行的程序就是进程,比如QQ、游戏等。

一个进程可以有多个线程,比如在视频中可以同时听声音、看图像、看弹幕。

线程的特性

​ 程序是一个静态的概念。而进程是执行程序的一次执行过程,是一个动态的概念。是系统分配资源的概念。

​ 一个进程中可以包含多个线程,至少有一个线程(Main线程)。 线程是CPU调度和执行的单位

​ 很多的多线程是模拟出来的,即一个CPU在同一时间点,只能执行一段代码。因为切换的快所以有同时进行的错觉。真正的多线程是多核CPU。

多线程

线程的创建

继承Thread类(重点)

继承Thread类

  • 自定义线程类继承Thread类
  • 重写run()方法,编写线程执行体
  • 创建线程对象 调用start()方法启动线程
  • 多线程
  • 多线程

start方法——同时进行

多线程

实际上主线程和副线程是同时执行的

run方法——先后进行

多线程

先调用run方法再执行主线程

不建议使用:避免OOP单继承局限性

实现Runnable接口(最重要)

实现Runnable接口

多线程

推荐使用:避免单继承局限性,灵活方便,方便同一个对象被多个线程使用

实现Callable接口(了解)

  • 实现Callable接口

  • 重写call方法 需要返回一个值

  • 创建执行服务

  • 提交执行

  • 获取结果

  • 关闭服务

    import java.util.concurrent.ExecutorService;
    import java.util.concurrent.Executors;
    import java.util.concurrent.Future;
    
    /**
     * @author:zmc
     * @function:
     * @date: 2020/7/16 11:01
     */
    public class Callable implements java.util.concurrent.Callable {
    
    
        public static void main(String[] args) {
            //创建线程池(不正规的方法)
            ExecutorService ser = Executors.newFixedThreadPool(1);
            Callable t1 = new Callable();
    
            //提交执行
            Future r1 = ser.submit(t1);
    
            //获取结果
            // Object o = r1.get();
    
            //关闭服务
            ser.shutdown();
    
            for (int i = 0; i < 500; i++) {
                System.out.println("Main哈哈哈"+i);
            }
        }
    
        @Override
        public Object call() throws Exception {
            for (int i = 0; i < 500; i++) {
                System.out.println("Callable哈哈哈"+i);
            }
            return null;
        }
    }
    
    

    callable和主线程是同时进行的

callable的好处

  • 可以定义返回值

  • 可以抛出异常

静态代理:线程的底部原理

真实对象和代理对象都要实现同一个接口

代理对象要代理真实角色

多线程

优点

  • 代理对象可以做很多真实对象做不了的事情
  • 真实对象可以专注做自己的事情
  • 代理对象对真实对象增强

静态代理和多线程的关系

多线程

Thread相当于weddingcompany 都是代理 中间为真实对象(Runnable接口)调用了start方法

Lambda表达式:Java8新技术

λ希腊字母表排序第十一位的字母

多线程

理解Functional Interface(函数式接口)是学习Java8 lambda表达式的关键所在

函数式接口:只包含一个唯一一个抽象方法 可以用lambda表达式来创建对象

静态内部类

多线程

局部内部类

多线程

匿名内部类

多线程

Lambda表达式的使用:直接实现

多线程

有类型的写法多线程

无类型的写法

多线程

无括号的写法(只能有一个参数)

多线程

去除花括号(只能有一行实现)

多线程

Lambda表达式的优点

  • 避免匿名内部类定义过多
  • 使代码更加简洁
  • 只留下核心逻辑

线程状态

多线程

多线程

线程方法

多线程

停止线程

不推荐用stop() destroy()方法

推荐让线程自己停下来

建议使用标识变量模拟停止:

多线程

线程休眠

  • sleep(时间)指定当前线程阻塞的毫秒数

  • sleep存在异常InterrupteedException

  • sleep之后进入就绪状态

  • sleep可用于模拟网络延时,倒计时等

  • 每个对象都有一个锁 sleep不会释放锁

倒计时

多线程

线程礼让

方法:Thread.yield();

  • 礼让线程,让当前正在执行的线程暂停,但不阻塞
  • 将线程从运行状态转为就绪状态
  • 让CPU重新调度 但不一定成功

多线程

线程插队

  • Join合并插队,其他线程阻塞。待此线程执行完成后,再执行其他线程。

    多线程

观察线程状态

多线程

状态:

多线程

线程无法启动两次

线程优先级

Java提供一个线程调度器来监控程序中启动后进入就绪状态的所有线程,按照优先级来决定。

数字大的优先级高

多线程

先设置优先级,再启动

优先级高,分配的资源就多。不一定优先级高的先跑,但是概率会更高。

多线程

守护(daemon)线程

  • 线程分为用户线程(main)和守护线程(如gc线程(garbage 垃圾线程))

  • 虚拟机必须确保用户线程执行完毕

  • 虚拟机不用等待守护线程执行完毕

  • 守护线程作用:

    • 后台记录操作日志
  • 监控内存

    • 垃圾回收等等

    多线程

主线程结束,虚拟机停止还需要一段时间,然后守护线程被强行终止。

线程同步机制

多个线程操作同一个资源

并发:同一个对象被多个线程同时操作

多线程

多线程

概念

​ 为了安全,在线程访问对象时加入锁(synchronized)机制,当一个线程获得一个对象的排他锁,独占资源,其他线程必须等待此线程使用完毕后释放锁。

​ 还有以下问题:

  • 一个线程持有锁,会导致其他所有需要此锁的线程挂起。
  • 在多线程竞争下,加锁,释放锁会导致比较多的上下文切换和调度延时,引起性能问题。
  • 如果一个优先级高的线程等待一个优先级低的线程释放锁,会引起优先级倒置,引起性能问题。

线程不安全

多线程

线程同步->使线程变得安全

​ 数据对象:通过private关键字来保证数据对象只能被方法访问

​ 方法:使用synchronized关键字 包括两种用法:synchronized方法和synchronized块

synchronized方法控制对"对象"的访问,每个对象对应一把锁,每个synchronized方法都必须获得调用该方法的对象的锁才能执行,否则线程会阻塞。方法一旦执行,就独占该锁,直到该方法返回才释放锁,后面被阻塞的线程才能获得这个锁,继续执行。

多线程

缺陷

若将一个大的方法申明为synchronized 将会影响效率

实际上方法内部不是每一处都需要上锁,此时使用同步方法会导致性能浪费。

多线程

synchronized方法

多线程

synchronized代码块

同步块需要监视器

多线程

死锁

多个线程持有对方需要的资源,形成了僵持。

多线程

产生死锁的四个必要条件

  • 一个资源每次只能被一个进程使用

  • 一个进程因请求资源而阻塞时,对已获得的资源保持持有状态

  • 进程已获得的资源,在未使用完前,不能强行剥夺

  • 若干进程之前形成一种头尾相接的循环等待资源关系

  • 多线程

ReentrantLock(可重复锁)

可显式地上锁和解锁

多线程

多线程

Synchronized与ReentrantLock的对比

​ Lock是显式锁(手动上锁和解锁,一定要记得解锁),synchronized是隐式锁,出了作用域自动释放。

​ Lock只有代码块锁,synchronized有代码块锁和方法锁。

​ 使用Lock锁,JVM将花费较少的时间来调度线程,性能较synchronized更好。并且有更好的拓展性(提供更多的子类)

​ 优先使用顺序

​ Lock > 同步代码块(已经进入了方法体,分配了相应资源) > 同步方法(在方法体之外)

线程协作

消费者问题

多线程

多线程

多线程解决方案1 管程法——利用缓存区

多线程

/**
 * @author:zmc
 * @function:生产者消费者模型 缓冲区方案————管程法
 * @date: 2020/7/16 13:29
 */
public class PC {

    public static void main(String[] args) {
        SynContainer container = new SynContainer();
        new Productor(container).start();
        new Consumer(container).start();
    }
}

class Productor extends Thread{
    SynContainer container;
    public Productor(SynContainer container){
        this.container = container;
    }

    @Override
    public void run() {
        for (int i = 0; i < 10; i++) {
            System.out.println("生产了"+(i+1)+"只鸡");
            container.push(new Chicken(i));
        }
    }
}

class Consumer extends Thread{
    SynContainer container;
    public Consumer(SynContainer container){
        this.container = container;
    }

    @Override
    public void run() {
        for (int i = 0; i < 10; i++) {
            System.out.println("消费了"+(container.pop(new Chicken(i)).id+1)+"只鸡");
        }
    }
}

class Chicken{
    int id;

    public Chicken(int id) {
        this.id = id;
    }
}
//缓冲区
class SynContainer{
    Chicken[] chickens = new Chicken[10];
    int count = 0;

    public synchronized void push(Chicken chicken){
    if(count == chickens.length) {
    //满了 通知消费者消费
        try {
            this.wait();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
    chickens[count++] = chicken;
    this.notifyAll();
    }

    public synchronized Chicken pop(Chicken chicken){
        if(count == 0) {
            //满了 通知消费者消费
            try {
                this.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        chickens[count--] = chicken;
        this.notifyAll();
        return chicken;
    }
}

解决方案2 信号灯法——

多线程

/**

 * @author:zmc

 * @function: 信号灯法

 * @date: 2020/7/16 13:55
   */
   public class PC2 {

   public static void main(String[] args) {
       TV tv = new TV();
       new Player(tv).start();
       new Watch(tv).start();
   }
   static class Player extends Thread{
       TV tv = new TV();
       public Player(TV tv){
           this.tv = tv;
       }@Overridepublic void run() {for (int i = 0; i < 20; i++) {if(i%2==0){this.tv.play("正在播放快乐大本营");}else {this.tv.play("正在播放肥皂剧");}}}
   }static class Watch extends Thread{
​        TV tv = new TV();public Watch(TV tv){this.tv = tv;}@Overridepublic void run() {for (int i = 0; i < 20; i++) {
​                tv.watch();}}}


   public static class TV{
        String voice;
        boolean flag = true;public synchronized void play(String voice)  {if(!flag){try {this.wait();} catch (InterruptedException e) {
​                    e.printStackTrace();}}
​            System.out.println("演员表演了"+voice);//通知观众观看this.notifyAll();this.voice = voice;this.flag = !this.flag;}public synchronized void watch()  {if(flag){try {this.wait();} catch (InterruptedException e) {
​                    e.printStackTrace();}}
​            System.out.println("观众观看了"+voice);//通知演员观看this.notifyAll();this.voice  = voice;this.flag = !this.flag;}}
}

线程池

背景

​ 经常创建和销毁、使用量特别大的资源,比如并发情况下的线程,对性能影响很大。

原理

​ 提前创建好多个线程,放入线程池中,使用时直接获取,使用完放回池中,实现线程的高效利用。

优点

​ 提高响应速度(减少了创建新进程的时间)

​ 降低资源损耗(重复利用线程池中线程,不必每次都创建)

​ 便于线程管理

多线程

多线程

使用工具类,阿里规范要求通过ThreadPoolExecutor方法手动创建

多线程

线程组

​ 为了方便管理一批线程,我们使用ThreadGroup来表示线程组。(A线程创建了B线程,B线程就是属于A线程的线程组的)

核心信息

  • 名称
  • 优先级
  • 守护与否
  • 父线程组
  • 子线程组

线程组内部属性包括:自己的名字,父线程组,使用数组记录自己下面有哪些线程组

线程组相关的方法

ThreadGroup(String name) 创建名为name的新线程组

ThreadGroup(ThreadGroup parent,String name) 创建父线程组为parent的线程组

创建系统线程组

多线程

获取线程组名称

多线程

线程组优先级

指的是一个线程组中所有线程最大允许的线程的优先级,实际上线程中不一定有达到这个优先级的。

多线程

守护

默认值与父线程组相同

多线程

父线程组

多线程

子线程组

线程组内可以有线程组,层层嵌套形成树状结构。

创建线程组时,借助parent.add(this)方法,把自己添加到父线程组的记录数组groups[]。

对于任何一个线程,都有一个线程组,如果没有设置,就会把当前线程的线程组作为线程组。

每个线程组都知道自己有多少个线程,哪些线程;每个线程组也都知道自己有多少个线程组,哪些线程组。

子线程组相关方法

activeCount()获取线程组和子线程组所有活动线程个数的一个估计数

activeGroupCount()获取子线程组的个数的估计数

interrupt()中断线程组中的所有线程

线程组的销毁

destroy()

  • 会进行权限检验
  • 线程组必须为空——线程组的所有线程都已停止执行
  • 会把子线程组递归销毁,并且也会进行子线程组的权限检验和判空

权限检验checkAccess

checkAccess()

借助安全管理器进行权限的检验的封装

确保当前运行的线程是否有权修改此线程组

线程相关类

ThreadLocal类

​ ThreadLocalVariable(线程局部变量),为每一个使用该变量的线程提供一个变量值的副本,使得每一个线程都可以独立地改变自己的副本,而不会和其他线程的副本冲突。从线程的角度看,就好像每一个线程都完全拥有该变量一样。

方法

T get():返回此线程局部变量中当前线程副本中的值

void remove() :删除此线程局部变量中当前线程的值

void set(T value):设置此线程局部变量中当前线程副本中的值

用途

​ ThreadLocal和其他同步机制一样,都是为了解决多线程中对同一变量的访问冲突,比起上锁,ThreadLocal从另一个角度来解决多线程的并发访问,将需要并发访问的资源复制多份,每个线程拥有一份资源,每个线程都有自己的资源副本。在编写多线程代码时,可以把不安全的整个变量封装进ThreadLocal,或者把该对象与线程相关的状态使用ThreadLocal保存。

通常需要线程通信使用同步机制,仅仅隔离共享冲突则使用ThreadLocal。

包装线程不安全的集合类

​ ArrayList、LinkedList、HashSet、TreeSet、HashMap、TreeMap等都是线程不安全的。

如果要保证线程安全,可以使用Collections提供的类方法把这些集合包装成线程安全的集合。

方法
//诸如此类 获取包装好的线程安全类
HashMap map = Collections.synchronizedMap(new HashMap());

线程安全的集合类

JUC包提供了大量的集合类,此处我会在JUC笔记记录。

本文地址:https://blog.csdn.net/Rush6666/article/details/107391392

相关标签: 笔记 java