多线程详解(上)

文章目录

  • 一、线程的概念
    • 1)线程是什么
    • 2)为甚要有线程
      • (1)“并发编程”成为“刚需”
      • (2)在并发编程中, 线程比进程更轻量.
    • 3)线程和进程的区别
  • 二、Thread的使用
    • 1)线程的创建
      • 继承Thread类
      • 实现Runnable接口
      • 继承Thread类(使用匿名内部类)
      • 实现Runnable接口(使用匿名内部类)
      • 使用lambda
    • 2)Thread中的构造方法
    • 3)Thread中的重要属性
    • 4)线程启动
    • 5)中断一个线程
        • 1. 通过共享的标记来进行沟通
        • 2.调用 interrupt() 方法来通知
    • 6)线程等待
    • 7)获取当前线程的引用
    • 8)休眠当前线程
  • 三、线程的状态
    • 1)线程的所有状态
  • 四、线程安全
    • 1)线程安全的概念
    • 2)线程不安全的原因
  • 五、 synchronized关键字-监视器锁monitor lock
    • 1)synchronized的特性
      • 1.互斥
    • 2.可重入
      • **关于死锁**
      • **哲学家就餐问题**
      • 死锁的成因,涉及四个必要条件
  • 六、volatile关键字
    • 1)保证内存可见性
    • 2)volatile不保证原子性
    • 3)synchronized也能保证内存可见性
  • 七、wait和notify
    • 1)wait()方法
    • 2)notify()方法
    • 3)notifyAll()方法
    • 4)wait和sleep的对比
  • 八、多线程代码案例
    • 1 )单例模式
      • (1)饿汉模式
      • (2)懒汉模式-单线程版
      • (3)懒汉模式-多线程版
      • (4)懒汉模式-多线程版(改进)
        • 理解双重if判定
        • 指令重排序
    • 2)阻塞队列
    • 生产者消费者模型
    • 标准库的阻塞队列
    • 阻塞队列的实现
    • 3)定时器
      • 标准库中的定时器
      • 实现定时器
    • 4)线程池
      • 标准库中的线程池
        • 设置线程数目的多少
        • 工厂模式
      • 线程池的实现

一、线程的概念

1)线程是什么

一个线程就是一个“执行流”,每个线程之间都可以按照顺序执行自己的代码。多个线程之间同时执行着多分代码

2)为甚要有线程

(1)“并发编程”成为“刚需”

  • 单核 CPU 的发展遇到了瓶颈. 要想提高算力, 就需要多核 CPU. 而并发编程能更充分利用多核 CPU资源.
  • 有些任务场景需要 “等待 IO”, 为了让等待 IO 的时间能够去做一些其他的工作, 也需要用到并发编程

(2)在并发编程中, 线程比进程更轻量.

  • 创建线程比创建进程更快.
  • 销毁线程比销毁进程更快.
  • 调度线程比调度进程更快.

3)线程和进程的区别

  1. 进程包含线程,一个进程里可以有一个或者多个线程
  2. 进程和线程都是用来实现并发编程的,但是线程比进程更轻量,更高效,体现在创建、销毁、调度线程都比进程快
  3. 同一个进程的线程之间,共用同一份的资源(内存+硬盘 ),省去了申请资源的开销
  4. 进程和进程之间,是具有独立性的,一个进程挂了,不会影响到其他进程;线程和线程之间(前提实在同一个进程内),是可能会相互影响的(线程安全问题+线程出现异常)
  5. 进程是资源分配的基本单位,线程是调度执行的基本单位

二、Thread的使用

1)线程的创建

继承Thread类

class MyThread extends Thread{@Overridepublic void run() {super.run ();while(true) {System.out.println ( "hello thread" );try {Thread.sleep ( 1000 );} catch (InterruptedException e) {e.printStackTrace ();}}}
}public class Demo1 {public static void main(String[] args) throws InterruptedException {Thread t = new MyThread ();t.start ();while(true){System.out.println ("hello main");Thread.sleep ( 1000 );}}
}

实现Runnable接口

class MyRunable implements Runnable{@Overridepublic void run() {while(true){System.out.println ("hello thread");try {Thread.sleep ( 1000 );} catch (InterruptedException e) {e.printStackTrace ();}}}
}public class Demo2 {public static void main(String[] args) {Runnable runnable = new MyRunable ();Thread t = new Thread (runnable);t.start ();while(true){System.out.println ("hello main");try {Thread.sleep ( 1000 );} catch (InterruptedException e) {e.printStackTrace ();}}}}

继承Thread类(使用匿名内部类)

public class Demo3 {public static void main(String[] args) throws InterruptedException {Thread t = new Thread (){@Overridepublic void run() {super.run ();while(true){System.out.println ("hello thread");try {Thread.sleep ( 1000 );} catch (InterruptedException e) {e.printStackTrace ();}}}};t.start ();while(true){System.out.println ("hello main");Thread.sleep ( 1000 );}}
}

实现Runnable接口(使用匿名内部类)

public class Demo4 {public static void main(String[] args) throws InterruptedException {Thread t= new Thread ( new Runnable () {@Overridepublic void run() {while(true) {System.out.println ( "hello thread" );try {Thread.sleep ( 1000 );} catch (InterruptedException e) {e.printStackTrace ();}}}} );t.start ();while(true){System.out.println ("hello main");Thread.sleep ( 1000 );}}
}

使用lambda

public class Demo5 {public static void main(String[] args) {Thread t =new Thread (()->{while(true){System.out.println ("hello thread");try {Thread.sleep ( 1000 );} catch (InterruptedException e) {e.printStackTrace ();}}});t.start ();while(true){System.out.println ("hello main");try {Thread.sleep ( 1000 );} catch (InterruptedException e) {e.printStackTrace ();}}}
}

2)Thread中的构造方法

在这里插入图片描述

3)Thread中的重要属性

属性获取方法
IDgetId()
名称getName()
状态getState()
优先级getPriority()
是否后台线程isDaemon()
是否存活isAlive()
是否被中断isInterrupted()
  • ID 是线程的唯一标识,不同线程不会重复
  • 名称是各种调试工具用到
  • 状态表示线程当前所处的一个情况,下面我们会进一步说明
  • 优先级高的线程理论上来说更容易被调度到
  • 关于后台线程,需要记住一点:JVM会在一个进程的所有非后台线程结束后,才会结束运行。
public class Demo6 {public static void main(String[] args) {Thread t =new Thread (()->{while(true){System.out.println ("hello thread");try {Thread.sleep ( 1000 );} catch (InterruptedException e) {e.printStackTrace ();}}},"这是新线程");System.out.println ( t.isDaemon () );//设置t为后台线程t.setDaemon ( true );System.out.println ( t.isDaemon () );t.start ();}}
  • 是否存活,即简单的理解,为 run 方法(回调方法)是否运行结束了
    Thread对象的生命周期要比系统内核中的线程更长一些——Thread对象还在,内核中的线程已经销毁了
public class Demo7 {public static void main(String[] args) throws InterruptedException {Thread t =new Thread (()->{System.out.println ("线程开始");try {Thread.sleep ( 2000 );} catch (InterruptedException e) {e.printStackTrace ();}System.out.println ("线程结束");});t.start ();System.out.println (t.isAlive ());Thread.sleep ( 3000 );System.out.println (t.isAlive ());}
}
  • 线程的中断问题,下面我们进一步说明

4)线程启动

通过覆写 run 方法创建一个线程对象,但线程对象被创建出来并不意味着线程
就开始运行了。

调用start方法,才真的在操作系统的底层创建出一个线程
start方法内部,是会调用操作系统的API,来在系统内核创建出线程

run方法就只是单纯描述了该线程要执行什么内容(会在start创建好后被自动调用)

5)中断一个线程

在java中,要销毁/终止线程,做法是比较唯一的,就是想办法让run方法尽快执行结束

常见的有以下两种方式:

1. 通过共享的标记来进行沟通

使用自定义的变量来作为标志位.

public class Demo8 {private static boolean isQuit =false;public static void main(String[] args) throws InterruptedException {Thread t = new Thread (()->{while(!isQuit){System.out.println ("线程工作中");try {Thread.sleep ( 1000 );} catch (InterruptedException e) {e.printStackTrace ();}}System.out.println ("线程工作完毕");});t.start ();Thread.sleep ( 5000 );isQuit = true;System.out.println ("设置isQuit为true");}
}

缺点

  • 需要手动创建变量
  • 当线程内部再sleep的时候,主线程修改变量,新线程不能及时响应

2.调用 interrupt() 方法来通知

Thread.currentThread().isInterrupted() 代替自定义标志位

public class Demo9 {public static void main(String[] args) throws InterruptedException {Thread t = new Thread (()->{//Thread类內部,有一个现成的标志位,可以用来判定当前循环是否要结束while(!Thread.currentThread ().isInterrupted ()){//currentThread () 哪个线程调用这个方法,就返回哪个线程的对象System.out.println ("线程工作中");try {Thread.sleep ( 1000 );//interrupt唤醒线程之后,此时sleep方法抛出异常,同时会自动清除刚才设置的标志位} catch (InterruptedException e) {//1.假装没听见,循环继续正常执行e.printStackTrace ();//2.加上一个break,表示让线程立即结束//3.做一些其他工作。,完成之后再结束break;//如果没有sleep,就没有上述操作空间}}System.out.println ("线程停止工作");});t.start ();Thread.sleep ( 5000 );System.out.println ("让t线程终止");t.interrupt();//把上述Thread对象内部的标志位设为true}
}

即使线程内部的逻辑出现阻塞(sleep)也是可以使用这个方法唤醒的

正常来说,sleep会休眠到时间到,才会结束,此处给出的interrupt就可以使sleep内部触发一个异常,从而提前被唤醒

6)线程等待

让一个线程等待另一个线程执行结束,再继续执行,本质上就是控制线程结束的顺序

join实现线程等待效果
主线程中,调用t.join(),此时就是主线程等待t线程先结束

public class Demo10 {public static void main(String[] args) throws InterruptedException {Thread t = new Thread ( () -> {for (int i = 0; i < 5; i++) {System.out.println ( "t线程工作中" );try {Thread.sleep ( 1000 );} catch (InterruptedException e) {e.printStackTrace ();}}} );t.start ();//让主线程等待t线程执行结束//一旦调用join,主线程就会出发阻塞,此时t线程可以趁机完成后续工作//一直到阻塞到t执行完毕了,join才会解除阻塞,才能继续执行System.out.println ("join 等待开始");t.join ();System.out.println ("join 等待结束");}
}

在这里插入图片描述

7)获取当前线程的引用

方法说明
public static Thread currentThread();返回当前线程对象的引用

8)休眠当前线程

方法说明
public static void sleep(long millis) throws InterruptedException;休眠当前线程 millis毫秒
public static void sleep(long millis, int nanos) throwsInterruptedException可以更高精度的休眠

线程调度是不可控的,sleep方法只能保证实际休眠时间是大于参数设置的休眠时间

public class Demo11 {public static void main(String[] args) throws InterruptedException {long beg =System.currentTimeMillis ();Thread.sleep ( 1000 );long end =System.currentTimeMillis ();System.out.println ("时间"+(end-beg)+"ms");}
}

系统会按照1000ms这个时间来控制休眠
当1000ms过了之后,系统会唤醒这个线程(阻塞->就绪)
但这个线程成了就绪状态,并不意味着立即回到cpu上运行(这中间有一个“调度开销”)

对于windows和linux这样的系统来说,调度开销可能达到ms级别

有些场景对时间精度要求很高,这时往往需要使用“实时操作系统”

三、线程的状态

1)线程的所有状态

线程的状态是一个枚举类型 Thread.State

public class Demo12 {public static void main(String[] args) {for (Thread.State state : Thread.State.values ()) {System.out.println (state);}}
}
  • NEW:安排了工作,还未开始行动
  • RUNNABLE:就绪状态(线程已经在cpu上执行了或者线程正在排队等待上cpu执行)
  • BLOCKED:阻塞,由于锁竞争导致的阻塞
  • WAITING:阻塞,由于wait这种不固定时间的方式产生的阻塞
  • TIMED_WAITING:阻塞,由于sleep这种固定时间的方式产生的阻塞
  • TERMINATED: Thread对象还在,内核中的线程已经没了

四、线程安全

1)线程安全的概念

如果多线程环境下代码运行结果符合我们的预期,即在单线程环境应该的结果,则说明这是线程安全的

public class Demo14 {private static int count = 0;public static void main(String[] args) throws InterruptedException {Thread t1 = new Thread (()->{for (int i = 0; i < 50000; i++) {count++;}});Thread t2 = new Thread (()->{for (int i = 0; i < 50000; i++) {count++;}});t1.start ();t2.start ();t1.join ();t2.join ();//预期结果应该是100000System.out.println (count);}
}

2)线程不安全的原因

  1. 操作系统中,线程的调度是随机的(抢占式执行)
  2. 两个线程,针对同一个变量进行修改
  3. 修改操作,不是原子的 count++属于 非原子 的操作(先读,再修改)
    上面提到的count++,其实是由散步操作组成的:
    1).从内存中把数据读到寄存器中(在cpu内)
    2).把寄存器中的数据进行加1
    3).把寄存器的数据保存到内存中
    4.内存可见性问题
    5.指令重排序问题

五、 synchronized关键字-监视器锁monitor lock

1)synchronized的特性

1.互斥

synchronized 会起到互斥效果, 某个线程执行到某个对象的 synchronized 中时, 其他线程如果也执行到同一个对象 synchronized 就会产生“锁竞争/锁冲突”后一个线程就会阻塞等待

  • 进入 synchronized 修饰的代码块,相当于加锁
  • 退出 synchronized 修饰的代码块, 相当于 解锁

synchronized(),() 中需要放入一个用来加锁的对象
这个对象是啥不重要,重要的是通过这个对象来区分两个线程是否竞争同一个锁

synchronized用的锁是存在java对象头里的
在这里插入图片描述

可以粗略理解成, 每个对象在内存中存储的时候, 都存有一块内存表示当前的"锁定" 状态

synchronized的使用方法
法1:

public class Demo14 {private static int count = 0;public static void main(String[] args) throws InterruptedException {Object locker = new Object ();Thread t1 = new Thread (()->{for (int i = 0; i < 50000; i++) {synchronized (locker){count++;}}});Thread t2 = new Thread (()->{for (int i = 0; i < 50000; i++) {synchronized (locker){count++;}}});t1.start ();t2.start ();t1.join ();t2.join ();//预期结果应该是100000System.out.println (count);}
}

法二:

//synchronized的使用方法class Counter{public int count;synchronized public void increase(){//synchronized修饰实例方法count++;}//上述代码等价于
//    public void increase(){
//        synchronized (this){
//            count++;
//        }
//    }synchronized  public static void increase1(){//synchronized修饰静态方法}public static  void increase2(){synchronized (Counter.class){//类对象}}
}public class Demo15 {public static void main(String[] args) throws InterruptedException {Counter counter = new Counter ();Thread t1 = new Thread(()->{for (int i = 0; i < 50000; i++) {counter.increase ();}});Thread t2 = new Thread(()->{for (int i = 0; i < 50000; i++) {counter.increase ();}});t1.start ();t2.start ();t1.join ();t2.join ();System.out.println (counter.count);}
}

2.可重入

可重入锁,指的是一个线程,连续针对一把锁,加锁两次,不会出现死锁,满足这个要求,就是“可重入”,不满足,就是“不可重入”

//t线程中存在下列代码
synchronized(locker){synchronized(locker){..........}
}

一个线程没有释放锁, 然后又尝试再次加锁.就会出现死锁情况

关于死锁

1.一个线程,针对一把锁,连续加锁两次,如果是不可重入锁,就死锁了
synchronized 同步块对同一线程来说是可重入的,不会出现死锁问题
2.两个线程,两把锁(此时无论是不是可重入锁,都会死锁)
(1)线程t1 获取A锁,线程t2 获取B锁
(2)t1尝试获取B,t2尝试获取A

public class Demo16 {private static Object locker1 = new Object ();private static Object locker2 = new Object ();public static void main(String[] args) {Thread t1 = new Thread(()->{synchronized (locker1){try {Thread.sleep ( 1000 );} catch (InterruptedException e) {e.printStackTrace ();}synchronized (locker2){System.out.println ("t1 加锁成功");}}});Thread t2 = new Thread(()->{synchronized (locker2){try {Thread.sleep ( 1000 );} catch (InterruptedException e) {e.printStackTrace ();}synchronized (locker1){System.out.println ("t2 加锁成功");}}});t1.start ();t2.start ();}
}

3.N个线程,M把锁,更容易出现死锁

哲学家就餐问题

5根筷子,5个哲学家

通常情况下,整个系统可以良好运转
但是极端境况下,就会出现问题

比如,同一时刻,五个哲学家都想吃面,同时拿起左手的筷子
此时,五个哲学家发现他们的右手没有筷子,于是只能等待
等待过程中,哲学家们不会放下左手的筷子

死锁的成因,涉及四个必要条件

  1. 互斥使用(锁的基本特性)。当一个线程持有一把锁后,另一个线程也想获取到这把锁,就要等待阻塞
  2. 不可抢占(锁的基本特性)。当锁已经被线程1拿到后,线程2只能选择等待线程1主动释放,不能抢过来
  3. 请求保持(代码结构)。一个线程尝试获取多把锁(先拿到锁1之后,在尝试获取锁2,获取的时候,锁1不会释放)
  4. 循环等待/环路等待(代码结构)。等待的依赖关系,形成环了

解决死锁,核心就是破坏上述必要条件,只要破坏一个,死锁就形成不了

  • 1和2破坏不了
  • 对于3来说,调整代码结构,避免编写“锁嵌套”逻辑
  • 对于4来说,可以约定加锁顺序,就可以避免循环等待

六、volatile关键字

1)保证内存可见性

计算机运行的程序/代码,经常要访问数据.这些依赖的数据,往往就会存储在 内存 中,cpu使用这个变量的时候,就会把这个内存中的数据,先读出来,放到cpu的寄存器中

cpu读取内存的这个操作,相对与寄存器读取是非常慢的
cpu进行大部分操作,都是非常快,一旦到读/写内存,此时速度就慢下来了

为了减少上述的问题,提高效率,此时编译器就可能对代码做出优化,把一些本来要读内存的操作,优化成读取寄存器

//内存可见性状况引起的问题
public class Demo17 {private static int isQuit = 0;public static void main(String[] args) {Thread t1 = new Thread (()->{while(isQuit==0){//1.load指令读取内存中isQuit的值到寄存器中//2.通过cmp指令比较寄存器的值是否为0,决定是否要继续循环////由于这个循环,循环的速度飞快,短时间内就会产生大量的循环//此时,编译器/JVM就发现,虽然执行了这么多次load,但是load出来的结果都一样,并且load操作非常费时间(1次lord可以执行上万次cmp)//所以,编译器只是第一次循环的时候,才读了内存,后续不再读内存了,而是直接从寄存器中,取出isQuit的值了//由于此时修改isQuit代码的是另一个线程,编译器没有正确的判断}System.out.println ("t1退出");});t1.start ();Thread t2 = new Thread (()->{Scanner sc =new Scanner ( System.in );System.out.println ("请输入isQuit:");isQuit = sc.nextInt ();});t2.start ();}
}

加上volatile后就可以解决这个问题了

public class Demo17 {private volatile static int isQuit = 0;public static void main(String[] args) {Thread t1 = new Thread (()->{while(isQuit==0){//1.load指令读取内存中isQuit的值到寄存器中//2.通过cmp指令比较寄存器的值是否为0,决定是否要继续循环////由于这个循环,循环的速度飞快,短时间内就会产生大量的循环//此时,编译器/JVM就发现,虽然执行了这么多次load,但是load出来的结果都一样,并且load操作非常费时间(1次lord可以执行上万次cmp)//所以,编译器只是第一次循环的时候,才读了内存,后续不再读内存了,而是直接从寄存器中,取出isQuit的值了//由于此时修改isQuit代码的是另一个线程,编译器没有正确的判断}System.out.println ("t1退出");});t1.start ();Thread t2 = new Thread (()->{Scanner sc =new Scanner ( System.in );System.out.println ("请输入isQuit:");isQuit = sc.nextInt ();});t2.start ();}
}

2)volatile不保证原子性

synchronized能保证原子性,volatile保证的是内存可见性

3)synchronized也能保证内存可见性

synchronized既能保证原子性, 也能保证内存可见性.

public class Demo18 {private volatile static int isQuit = 0;public static void main(String[] args) {Object object = new Object ();Thread t1 = new Thread (()->{synchronized (object) {while (isQuit == 0) {}}System.out.println ("t1退出");});t1.start ();Thread t2 = new Thread (()->{Scanner sc =new Scanner ( System.in );System.out.println ("请输入isQuit:");isQuit = sc.nextInt ();});t2.start ();}
}

七、wait和notify

协调执行顺序

join是影响线程结束的先后顺序
相比之下,此处希望线程不结束,也能有先后顺序的控制

wait() / wait(long timeout) 等待,让指定线程进入阻塞状态
notify() / notifyAll() 通知,唤醒对应的阻塞状态线程

wait和notify都是Object的方法,随便定义一个对象对可以用

wait,notify可以避免线程饿死

1)wait()方法

wait 要搭配 synchronized 来使用. 脱离 synchronized 使用 wait 会直接抛出异常.

wait 做的事情:

  • 使当前执行代码的线程进行等待
  • 释放当前的锁
  • 满足一定条件时被唤醒,重新尝试获取这个锁

wait 结束等待的条件:

  • 其他线程调用该对象的notify方法
  • wait等待时间超时 (wait 方法提供一个带有 timeout 参数的版本, 来指定等待时间).
  • 其他线程调用该等待线程的interrupted方法,导致wait方法抛出InterruptedException异常
public class Demo19 {public static void main(String[] args) throws InterruptedException {Object object = new Object ();synchronized (object){System.out.println ("wait 之前");object.wait ();System.out.println ("wait 之后");}}
}

这样在执行到object.wait()之后就一直等待下去,那么程序肯定不能一直这么等待下去了。这个时候就
需要使用到了另外一个方法唤醒的方法notify()

2)notify()方法

notify 方法是唤醒等待的线程

  • 方法notify()也要在同步方法或同步块中调用,该方法是用来通知那些可能等待该对象的对象锁的其它线程,对其发出通知notify,并使它们重新获取该对象的对象锁。
  • 如果有多个线程等待,则有线程调度器随机挑选出一个呈 wait 状态的线程。(并没有"先来后到")
  • 在notify()方法后,当前线程不会马上释放该对象锁,要等到执行notify()方法的线程将程序执行完,也就是退出同步代码块之后才会释放对象锁。
public class Demo20 {public static void main(String[] args) {Object object = new Object ();Thread t1 = new Thread (()->{synchronized (object){System.out.println ("wait 之前");try {object.wait ();} catch (InterruptedException e) {e.printStackTrace ();}System.out.println ("wait 之后");}});Thread t2 = new Thread (()->{synchronized (object){System.out.println ("进行通知");object.notify ();}});t1.start ();t2.start (); }
}

3)notifyAll()方法

notify方法只是唤醒某一个等待线程. 使用notifyAll方法可以一次唤醒所有的等待线程

4)wait和sleep的对比

1.wait 需要搭配 synchronized 使用. sleep 不需要
2.wait 是 Object 的方法 sleep 是 Thread 的静态方法.

八、多线程代码案例

1 )单例模式

单例模式是非常经典的的设计模式

单例模式保证某个类在程序中只存在唯一一份实例,而不会创建多个实例

这一点在很多场景上都需要. 比如 JDBC 中的 DataSource 实例就只需要一个

单例模式具体实现方式,分为"饿汉"和"懒汉"两种

(1)饿汉模式

类加载时,创建实例

class Singleton{private static Singleton instance = new Singleton ();//static成员  在Singleton类被加载的时候,就会执行到这里的创建实例操作private Singleton(){};public static Singleton getInstance(){return instance;} 
}

(2)懒汉模式-单线程版

class SingletonLazy{private static SingletonLazy instance = null;public static SingletonLazy getInstance(){//首次调用getInstance的时候才会真正创建实例if(instance==null){instance = new SingletonLazy ();}return instance;}
}

(3)懒汉模式-多线程版

上述的懒汉模式在多线程中是不安全的

线程安全问题发生在首次创建实例时,如果在多个线程中同时调用getInstance方法,就可能导致创建多个实例

如果多个线程,同时修改同一个变量,此时可能会出现线程安全问题
如果多个线程,同时读取同一个变量,此时不会出现线程安全问题

加上synchronized可以改善这里的线程安全问题

class SingletonLazy{private static SingletonLazy instance = null;public  synchronized static SingletonLazy getInstance(){if(instance==null){instance = new SingletonLazy ();}return instance;}
}

(4)懒汉模式-多线程版(改进)

上述懒汉模式-多线程版中,每次调用getInstance方法都需要加锁/解锁,开销比较高, 而懒汉模式的线程不安全只是发生在首次创建实例的时候.因此后续使用的时候, 不必再进行加锁了

class SingletonLazy{private volatile static SingletonLazy instance = null;public   static SingletonLazy getInstance(){if(instance == null) {//这两个if的执行时机可能会差异很大,执行结果也可能截然相反//第一个if用来判定是否需要加锁synchronized (SingletonLazy.class) {if (instance == null) {//第二个if用来判定是否需要new对象instance = new SingletonLazy ();}}}return instance;}
}

理解双重if判定

  • 外层的if是判定下看当前是否已经把instance实例创建出来了
  • 当多线程首次调用getInstance,多个线程可能发现instance为null,于是进入外层if语句,继续往下执行来竞争锁,其中竞争成功的线程,再完成创建实例的操作
  • 当实例创建完成后,其他竞争到锁的线程就会被挡在内层if语句前,也就不会继续创建其他实例了

我们来理清一下思路

  1. 假设有三个线程,开始执行getInstan,通过外层 if (instance == null)知道了实例还没有创建的消息,于是开始竞争同一把锁
  2. 其中线程2率先抢到锁,此时线程2通过内层的 if (instance == null)进一步确认实例是否已经创建,如果没创建就把实例创建出来
  3. 当线程2释放锁后,线程1和3也先后拿到了锁,也通过里层的 if (instance == null) 来确认实例是否已经创建, 发现实例已经创建出来了, 就不再创建了
  4. 后续的线程就直接跳过外层if (instance == null)就已经知道实例被创建了,从而不需要再进行加锁解锁,降低了开销

指令重排序

指令重排序,是编译器为了提高执行效率,在保持代码原有的逻辑的前提下,对代码顺序进行重新编排

指令重排序在多线程下,可能会出现误判

new操作,是可能会触发指令重排序的
new操作分为三步:

  1. 申请内存空间
  2. 在内存空间上构建对象(构造方法)
  3. 将构造好对象的内存地址,赋值给instance引用

第1步是一定先执行的,假设进行了指令重排序,执行顺序变为 1->3->2
当t1执行完 第1步和第3步 时,此时instance就已经是非空了!!!但此时,instance指向的是一个还没初始化的对象
第2步 没开始执行,就在同一时间,t2线程开始执行了!!!
t2判定Instance == null ,条件不成立!!!于是t2直接 return instance
进一步t2线程的代码就可能会访问Instance里面的属性和方法了,这时就容易出现bug了

使用volatile的原因

为了避免"指令重排序"导致读取的instance出现偏差,于是补充上 volatile .

2)阻塞队列

阻塞队列是一种特殊的队列. 也遵守 “先进先出” 的原则.
阻塞队列是一种线程安全的数据结构,带有阻塞特性:

  • 当队列满的时候,继续入队就会阻塞,直到有其他线程从队列中取走元素
  • 当队列空的时候,继续出队也会阻塞,直到有其他程序往队列中插入元素

阻塞队列的一个最经典场景就是"生产者消费者模型"

生产者消费者模型

生产者消费者模型就是通过一个容器来解决生产者和消费者的耦合问提

生产者和消费者彼此之间不直接通讯,而是通过阻塞队列来进行通讯,所以生产者生产完数据之后不用等待消费者处理,直接扔给阻塞队列,消费者不需要找生产者要数据,而是直接从阻塞队列中获取

1)阻塞队列相当于一个缓冲区,平衡的生产者和消费者之间的对数据量处理能力(削峰填谷)

比如在 “秒杀” 场景下, 服务器同一时刻可能会收到大量的支付请求. 如果直接处理这些支付请求,
服务器可能扛不住(每个支付请求的处理都需要比较复杂的流程). 这个时候就可以把这些请求都放
到一个阻塞队列中, 然后再由消费者线程慢慢的来处理每个支付请求.

2)阻塞队列也能使生产者和消费者之间解耦合

比如过年一家人一起包饺子. 一般都是有明确分工, 比如一个人负责擀饺子皮, 其他人负责包. 擀饺
子皮的人就是 “生产者”, 包饺子的人就是 “消费者”.
擀饺子皮的人不关心包饺子的人是谁(能包就行, 无论是手工包, 借助工具, 还是机器包), 包饺子的人
也不关心擀饺子皮的人是谁(有饺子皮就行, 无论是用擀面杖擀的, 还是拿罐头瓶擀, 还是直接从超
市买的).

标准库的阻塞队列

在 Java 标准库中内置了阻塞队列. 如果我们需要在一些程序中使用阻塞队列, 直接使用标准库中的即可.

  • BlockingQueue 是一个接口,真正实现的类是LinkedBlockingQueue
  • put 方法用于阻塞式的入队列,take 用于阻塞式的出队列
  • BlockingQueue 也有offer,poll,peek等方法,但这些方法不带有阻塞特性
public class Deom23 {public static void main(String[] args) throws InterruptedException {BlockingDeque<String> queue = new LinkedBlockingDeque<> ();queue.put("abc");System.out.println ( queue.take () );}
}

生产者消费者模型

public class Demo24 {public static void main(String[] args) throws InterruptedException {BlockingDeque<String> blockingDeque = new LinkedBlockingDeque<> ();Thread customer = new Thread (()->{while(true) {try {String num = blockingDeque.take ();System.out.println ( "消费元素" + num );} catch (InterruptedException e) {e.printStackTrace ();}}},"消费者");customer.start ();Thread producer = new Thread (()->{Random random = new Random ();while(true) {String num = random.nextInt (100)+"";try {blockingDeque.put ( num );System.out.println ( "生产元素" + num );Thread.sleep ( 1000 );} catch (InterruptedException e) {e.printStackTrace ();}}},"生产者");producer.start ();customer.join ();producer.join ();}
}

阻塞队列的实现

一个队列,要么空,要么满
take 和 put 只有一边能阻塞
如果put阻塞,其他线程继续调用put也会阻塞,只有靠take唤醒
如果take阻塞,其他线程继续调用take也会阻塞,只有靠put唤醒

interrupt方法,是可能中断wait的状态

class MyBlockingQueue{private String[] data = new String[1000];private volatile int head = 0;//后续代码,有的进行读和写,加上volatile,避免内存可见性问提private volatile int tail = 0;private volatile int size = 0;public synchronized void put(String elemt) throws InterruptedException {while(data.length==size) {//当wait返回时,确认一下当前队列满不满,满着就继续执行wait//队列满了//如果是队列满,继续插入就会阻塞this.wait ();//在当前代码中,如果interrupt唤醒了wait,直接整个方法就结束了,因为我们使用throws抛出的异常,这个时候代码是没事的//但当用try-catch,出现异常,方法不会结束,会继续往下执行,此时会把tail指向的元素覆盖掉.实际上此处的队列还是满的,此时tail指向的元素,并非是无效元素(就是弄丢了一个有效元素)}//队列没满data[tail] = elemt;tail = (tail+1)%data.length;//如果tail++到达了数组末尾,这时就要他回到开头,环形队列this.notify ();//这个notify是用来唤醒take中的waitsize++;}public synchronized String take() throws InterruptedException {while(size == 0){//队列为空this.wait ();}//队列不为空String ret = data[head];head = (head+1)%data.length;this.notify ();//这个notify用来唤醒put中的waitsize--;return ret;}}public class Demo25 {public static void main(String[] args) throws InterruptedException {MyBlockingQueue blockingDeque = new MyBlockingQueue ();Thread customer = new Thread (()->{while(true) {try {String num = blockingDeque.take ();System.out.println ( "消费元素" + num );Thread.sleep ( 1000 );} catch (InterruptedException e) {e.printStackTrace ();}}},"消费者");customer.start ();Thread producer = new Thread (()->{Random random = new Random ();while(true) {String num = random.nextInt (100)+"";try {blockingDeque.put ( num );System.out.println ( "生产元素" + num );} catch (InterruptedException e) {e.printStackTrace ();}}},"生产者");producer.start ();customer.join ();producer.join ();}
}

3)定时器

标准库中的定时器

  • 标准库中提供了一个Timer类,Timer类的核心方法为schedul
  • schedule包含两个参数,第一个参数指定即将要执行的任务代码,第二个参数指定多长时间之后执行(单位毫秒)
public class Demo26 {public static void main(String[] args) {Timer timer = new Timer ();//给定时器安排一个任务,预定在xxx时间去执行timer.schedule ( new TimerTask () {//此处使用匿名内部类的写法,继承了TimerTask并创建出一个实例@Overridepublic void run() {System.out.println ("执行定时器的任务");//通过run来描述任务的详细情况}},3000 );System.out.println ("程序启动");}
}

主线程执行schedule方法的时候,就是把这个任务给放到timer对象中了
与此同时,timer里头也包含了一个线程,这个线程叫"扫描线程",一旦时间到,扫描线程就会执行刚才安排的任务

完成了任务之后,整个进程没有结束,因为Timer内部的线程阻止了进程结束

Timer里,是可以安排多个任务的

实现定时器

定时器的构成:

  • 一个带优先级的阻塞队列作为存储任务的数据结构

为啥要带优先级?

因为阻塞队列中的任务都有各自执行的时间(delay),最先执行的任务一定是delay最小的,使用带优先级的队列就可以高效的把这个delay最小任务找出来

  • 队列中的每个元素是一个MyTimerTask对象,把所有任务保存起来
  • 同时有一个线程一直扫描队首元素,看队首元素是否需要执行
//通过这个类,描述一个任务
class MyTimerTask implements Comparable<MyTimerTask>{//优先级队列需要提供比较//要有一个执行的任务private Runnable runnable;//有一个任务执行的时间private long time;//此处的delay就是schedule方法传入的"相对时间"public MyTimerTask(Runnable runnable , long delay) {this.runnable = runnable;//构造出要执行任务的绝对时间this.time = System.currentTimeMillis ()+delay;}@Overridepublic int compareTo(MyTimerTask o) {//队首元素是最小时间的值return (int) (this.time - o.time);}public long getTime() {return time;}public Runnable getRunnable() {return runnable;}
}//创建定时器
class MyTimer{private PriorityQueue<MyTimerTask> queue = new PriorityQueue<> ();private Object locker = new Object ();public void schedule(Runnable runnable,long delay){synchronized (locker) {queue.offer ( new MyTimerTask ( runnable , delay ) );locker.notify ();}}//创建一个扫描线程public MyTimer() {Thread t = new Thread (()->{//扫描线程需要不停的扫描队首元素,看是否到达时间while(true){try{synchronized (locker) {//使用while的目的是为了在wait被唤醒时,再确认一下条件while (queue.isEmpty ()) {//队列为空,阻塞等待//使用wait进行等待//这里的wait需要由另外的线程唤醒//添加新的任务就应该唤醒locker.wait ();}MyTimerTask task = queue.peek();// 比较一下看当前的队首元素是否可以执行了.long curTime = System.currentTimeMillis();if (curTime >= task.getTime()) {// 当前时间已经达到了任务时间, 就可以执行任务了task.getRunnable().run();// 任务执行完了, 就可以从队列中删除了.queue.poll();} else {// 当前时间还没到任务时间, 暂时不执行任务.// 暂时先啥都不干, 等待下一轮的循环判定了.locker.wait(task.getTime() - curTime);}}} catch (InterruptedException e) {e.printStackTrace ();}}});t.start ();}
}public class Demo27 {public static void main(String[] args) {MyTimer timer = new MyTimer();timer.schedule(new Runnable() {@Overridepublic void run() {System.out.println("3000");}}, 3000);timer.schedule(new Runnable() {@Overridepublic void run() {System.out.println("2000");}}, 2000);timer.schedule(new Runnable() {@Overridepublic void run() {System.out.println("1000");}}, 1000);System.out.println("程序开始执行");}
}

4)线程池

线程池最大的好处就是减少每次启动线程,销毁线程的开销

标准库中的线程池

  • 使用 Executors.newFixedThreadPool(10) 能创建出固定包含10个线程的线程池
  • 返回类型为 ExecutorService
  • 通过ExecutorServic.submit可以注册一个任务到线程池中
public static void main(String[] args) {ExecutorService service = Executors.newFixedThreadPool ( 10 );service.submit ( new Runnable () {@Overridepublic void run() {System.out.println ("hello");}} );}
  • Executors.newCachedThreadPool() 构造出的线程池中的线程数目能够动态适应 创建出来之后不会急着销毁,会在池子里保留一定时间,以备随时再用到
  • Executors.newFixedThreadPool(10) 构造出的线程池中的线程数目是固定的
  • Executors.newSingleThreadPool() 构造出只有一个线程的线程池
  • Executors.newScheduledThreadPool() 类似定时器,但有多个扫描线程
    这几个工厂方法的线程池,本质上是对一个 类 进行封装, ThreadPoolExecutor
    这个类功能非常丰富,提供了很多参数,标准库上述的几个工厂方法,其实就是给这个类填写了不同的参数用来构造线程池的

ThreadPoolExecutor 核心方法就两个 (1)构造 (2)任务注册(添加任务)

在这个包下
在这里插入图片描述

![在这里插入图片描述](https://img-blog.csdnimg.cn/b41e6b3f8c3745eb92fa2c7fee99fadc.png
在这里插入图片描述

corePoolSize 核心线程数
maximumPoolSize 最大线程数
这个线程池里的线程数目可以动态变化的
变化范围是[corePoolSize ,maximumPoolSize]

keepAliveTime 允许线程留存的时间
unit 是留存时间的单位

在这里插入图片描述
阻塞队列,用来存放线程池中的任务,可以根据需要灵活设置
需要优先级 就可以设置PriorityBlockingQueue
任务数目恒定 就可以设置ArrayBlockingQueue

在这里插入图片描述
此处使用ThreadFactory作为工厂类,由这个类负责创建线程
使用工厂类创建,主要是为了在创建过程中,对线程进行初始化


这是线程池的拒绝策略
一个线程池,能容纳的任务数量是有限的
当持续往线程池里添加任务的时候,一旦已经到达上线,继续再添加,会出现的效果跟选择的拒绝策略有关

在这里插入图片描述
直接抛出异常
在这里插入图片描述
新添加的任务,让添加任务的线程负责执行
在这里插入图片描述
丢弃任务队列中最老的任务
在这里插入图片描述
丢弃当前新加的任务

设置线程数目的多少

一个线程,执行的代码,主要有两类:
1.CPU密集型: 代码里主要的逻辑实在进行算数运算/逻辑判断
2.IO密集型: 代码里主要进行的是IO操作

假设一个线程的所有代码都是cpu密集型代码,这时,线程池设置的数量不应该超过N(N为cpu逻辑核心数).
假设一个线程的所有代码都是IO密集型代码,这时不吃cpu,线程池设置的数量就可以超过N(N为cpu逻辑核心数).一个核心可以通过调度的方式,来实现并发执行

代码不同,线程池的线程数目设置就不同
无法知道一个代码,具体多少内容是cpu密集型,多少内容是IO密集型

正确做法:使用实验的方式,对程序进行性能测试
测试过程中尝试修改线程数目来确定最优解

工厂模式

线程池对象不是我们直接new的
而是通过一个专门的方法,返回一个线程的对象

通常创建对象,使用new.new关键字会触发类的构造方法.但是,构造方法存在一定的局限性
Executors.newFixedThreadPool(10) 工厂模式(设计模式)
工厂模式是给构造方法填坑的

很多时候,构造一个对象,希望有多种构造方式
多种方式,就需要使用多个版本的构造方法来分别实现
但是构造方法要求方法是类名,不同的构造方法只能 通过重载的方式来区分了(重载=>参数类型/个数 不同)

使用工厂设计模式就能解决这个问题 使用普通方法,代替构造方法完成初始化工作 普通方法就可以使用方法的名字来区分,也就不再收到重载的规则制约了

实践中,一般单独创建一个类,给这个类创建一些静态方法,由这样的静态方法负责构造出对象

class PointFactory{public static Point makePointByXY(double x, double y){Point p = new Point ();p.setLocation ( x,y );return p;}public static Point makePointByRA(double r, double a){Point p = new Point ();p.setLocation ( r,a );return p;}}public class Demo28 {public static void main(String[] args) {Point p = PointFactory.makePointByXY ( 10,20 );}
}

在这里插入图片描述

线程池的实现

  • 使用阻塞队列来保存任务
  • 核心操作为 submit, 将任务加入到队列中
  • 实现构造方法,在里创建出线程去消费队列中的任务
class MyThreadPool{//任务队列private BlockingDeque<Runnable> queue = new LinkedBlockingDeque<> ();//通过这个方法,把任务添加到队列中public void submit(Runnable runnable) throws InterruptedException {//此处我们的拒绝策略,相当于第五种策略,阻塞等待(这是下策)queue.put ( runnable );}public MyThreadPool(int n){//创建出n个线程,负责上述队列中的任务for (int i = 0; i < n; i++) {Thread t = new Thread (()->{try {Runnable runnable = queue.take ();runnable.run ();} catch (InterruptedException e) {e.printStackTrace ();}});t.start ();}}
}public class Demo29 {public static void main(String[] args) throws InterruptedException {MyThreadPool myThreadPool = new MyThreadPool ( 4 );for (int i = 0; i < 1000; i++) {int id = i;myThreadPool.submit ( new Runnable () {@Overridepublic void run() {System.out.println ("执行任务"+id);//因为内部类变量捕获,所以打印不了i}} );}}
}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.rhkb.cn/news/135956.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

自定义类型:结构体

自定义类型&#xff1a;结构体 一&#xff1a;引入二&#xff1a;结构体类型的声明1&#xff1a;正常声明2&#xff1a;特殊声明 三&#xff1a;结构体变量的创建和初始化1:结构体变量的创建2&#xff1a;结构体变量的初始化 三&#xff1a;结构体访问操作符四&#xff1a;结构…

【C语言】每日一题(半月斩)——day3

目录 一&#xff0c;选择题 1.已知函数的原型是&#xff1a; int fun(char b[10], int *a); 2、请问下列表达式哪些会被编译器禁止【多选】&#xff08; &#xff09; 3、以下程序的输出结果为&#xff08; &#xff09; 4、下面代码段的输出是&#xff08; &#xff09;…

大数据学习1.1-Centos8虚拟机安装

1.创建新的虚拟机 2.选择稍后安装OS 3.选择Linux的CentOS8 4.选择安装路径 5.分配20g存储空间 6.自定义硬件 7.分配2g内存 8.分配2核处理器 9.选择镜像位置 10.开启虚拟机安装 推荐密码设置为root

61、SpringBoot -----跨域资源的设置----局部设置和全局设置

★ 跨域资源共享的意义 ▲ 在前后端分离的开发架构中&#xff0c;前端应用和后端应用往往是彻底隔离的&#xff0c;二者不在同一个应用服务器内、甚至不再同一台物理节点上。 因此前端应用和后端应用就不在同一个域里。▲ 在这种架构下&#xff0c;前端应用可能采用前端框架&a…

序列化和反序列化:将数据变得更加通用化

序列化与反序列化简介 序列化和反序列化是计算机领域中常用的概念&#xff0c;用于将对象或数据结构转换为字节序列&#xff08;序列化&#xff09;和将字节序列转换回对象或数据结构&#xff08;反序列化&#xff09;。 序列化是指将对象或数据结构转换为字节序列的过程。通…

前端VUE---JS实现数据的模糊搜索

实现背景 因为后端实现人员列表返回&#xff0c;每次返回的数据量在100以内&#xff0c;要求前端自己进行模糊搜索 页面实现 因为是实时更新数据的&#xff0c;就不需要搜索和重置按钮了 代码 HTML <el-dialogtitle"团队人员详情":visible.sync"centerDi…

uni-app跳转到另一个app

第一步&#xff1a; 首先要知道 app的包名 获取方式如下 第二步&#xff1a; 在第一个 demo1 app 一个页面中需要一个按钮去跳转 方法如下 <template><view class"content"><button click"tz">跳转</button></view> </…

如何在微软Edge浏览器上一键观看高清视频?

编者按&#xff1a;视频是当下最流行的媒体形式之一。但由于视频压缩、网络不稳定等原因&#xff0c;我们常常可以看到互联网上的很多视频其画面质量并不理想&#xff0c;尤其是在浏览器端&#xff0c;这极大地影响了观看体验。不过&#xff0c;近期微软 Edge 浏览器推出了一项…

FPGA纯verilog实现8路视频拼接显示,提供工程源码和技术支持

目录 1、前言版本更新说明免责声明 2、我已有的FPGA视频拼接叠加融合方案3、设计思路框架视频源选择OV5640摄像头配置及采集静态彩条视频拼接算法图像缓存视频输出 4、vivado工程详解5、工程移植说明vivado版本不一致处理FPGA型号不一致处理其他注意事项 6、上板调试验证并演示…

Jmeter接口测试简易步骤

使用Jmeter接口测试 1、首先右键添加一个线程组&#xff0c;然后我们重命名接口测试 2、在线程组上添加一个Http默认请求&#xff0c;并配置服务器的IP地址端口等信息 3、在线程组中添加一个HTTP请求&#xff0c;这里我们重命名“增加信用卡账户信息接口” 4、配置接口请求信息…

使用延迟队列解决分布式事务问题——以订单未支付过期,解锁库存为例

目录 一、前言 二、库存 三、订单 一、前言 上一篇使用springcloud-seata解决分布式事务问题-2PC模式我们说到了使用springcloud-seata解决分布式的缺点——不适用于高并发场景 因此我们使用延迟队列来解决分布式事务问题&#xff0c;即使用柔性事务-可靠消息-最终一致性方…

Kotlin simple convert ArrayList CopyOnWriteArrayList MutableList

Kotlin simple convert ArrayList CopyOnWriteArrayList MutableList Kotlin读写分离CopyOnWriteArrayList_zhangphil的博客-CSDN博客Java并发多线程环境中&#xff0c;造成死锁的最简单的场景是&#xff1a;多线程中的一个线程T_A持有锁L1并且申请试图获得锁L2&#xff0c;而多…

Redis缓存实现及其常见问题解决方案

随着互联网技术的发展&#xff0c;数据处理的速度和效率成为了衡量一个系统性能的重要指标。在众多的数据处理技术中&#xff0c;缓存技术以其出色的性能优化效果&#xff0c;成为了不可或缺的一环。而在众多的缓存技术中&#xff0c;Redis 以其出色的性能和丰富的功能&#xf…

flutter开发实战-长按TextField输入框cut、copy设置为中文复制、粘贴

flutter开发实战-长按TextField输入框cut、copy设置为中文复制、粘贴 在开发过程中&#xff0c;需要长按TextField输入框cut、copy设置为中文“复制、粘贴”&#xff0c;这里记录一下设置的代码。 一、pubspec.yaml设置flutter_localizations 在pubspec.yaml中设置flutter_l…

23下半年学习计划

大二上学期计划 现在已经是大二了&#xff0c;java只学了些皮毛&#xff0c;要学的知识还有很多&#xff0c;新的学期要找准方向&#xff0c;把要学的知识罗列&#xff0c;按部就班地完成计划&#xff0c;合理安排时间&#xff0c;按时完成学习任务。 学习node.js&#xff0c…

企业架构LNMP学习笔记48

数据结构类型操作&#xff1a; 数据结构&#xff1a;存储数据的方式 数据类型 算法&#xff1a;取数据的方式&#xff0c;代码就把数据进行组合&#xff0c;计算、存储、取出。 排序算法&#xff1a;冒泡排序、堆排序 二分。 key&#xff1a; key的命名规则不同于一般语言…

Android 12 源码分析 —— 应用层 六(StatusBar的UI创建和初始化)

Android 12 源码分析 —— 应用层 六&#xff08;StatusBar的UI创建和初始化) 在前面的文章中,我们分别介绍了Layout整体布局,以及StatusBar类的初始化.前者介绍了整体上面的布局,后者介绍了三大窗口的创建的入口处,以及需要做的准备工作.现在我们分别来细化三大窗口的UI创建和…

GitLab使用的最简便方式

GitLab介绍 GitLab是一个基于Git版本控制系统的开源平台&#xff0c;用于代码托管&#xff0c;持续集成&#xff0c;以及协作开发。它提供了一套完整的工具&#xff0c;以帮助开发团队协同工作、管理和部署代码。 往往在企业内部使用gitlab管理代码&#xff0c;记录一下将本地代…

redis 集群(cluster)

1. 前言 我们知道&#xff0c;在Web服务器中&#xff0c;高可用是指服务器可以正常访问的时间&#xff0c;衡量的标准是在多长时间内可以提供正常服务&#xff08;99.9%、99.99%、99.999% 等等&#xff09;。但是在Redis语境中&#xff0c;高可用的含义似乎要宽泛一些&#xf…

6.3 字符数组

思维导图&#xff1a; 前言&#xff1a; 主要内容&#xff1a; 前言内容整理 字符型数据和存储 字符型数据是依据字符的ASCII代码存储在内存单元中&#xff0c;通常占用一个字节的空间。ASCII代码可以被认为是整数&#xff0c;因此在C99标准中&#xff0c;字符类型被归类为整…