09-JavaSE【多线程详细加强解析】

01_线程:线程高并发及运行机制

目标

  • 能够理解多个线程执行后的运行机制 【了解】

路径

  1. 高并发概念

  2. 多线程的运行机制


高并发

高并发:是指在某个时间点上,有大量的线程同时访问同一资源

例如:天猫的双11购物节、12306的在线购票在某个时间点上,都会面临大量用户同时抢购同一件商品/车票的情况

多线程的运行机制

多个线程开启之后,在内存中有各自的独立栈空间。

多个线程在各自的独立栈空间中运行,运行时互不影响,也不存在某种运行顺序(CPU随机切换执行)

//线程类
public class MyThread extends Thread {
    @Override
    public void run() {
        for (int i = 0; i < 100; i++) {
            System.out.println("i = " + i);
        }
    }
}

//测试类
public class Demo {
    public static void main(String[] args) {
        //1.创建两个线程对象
        MyThread t1 = new MyThread();
        MyThread t2 = new MyThread();

        //2.启动两个线程
        t1.start();
        t2.start();
    }
}

在这里插入图片描述

小结

当start()方法执行时:开辟新线程空间,运行run方法

在进程中,只要有一个线程还在运行,当前进程不会结束

多线程在执行时:每个线程都在各自独立的栈空间中运行,互相不干扰

多线程执行是无序(CPU随机切换执行)

02_线程:线程安全问题

目标

  • 理解导致线程安全问题的原因 【了解】

路径

  1. 线程共享资源介绍
  2. 线程安全问题
  3. 线程安全问题代码演示

线程共享资源

什么是共享资源?

  • 多个线程访问了相同资源(同一个资源),这个资源就称为共享资源

    • 共享资源通常是存放在堆中(对象)或者方法区中(静态变量)

线程安全问题

  • 当多个线程同时读写共享资源的时候,如果出现了数据的脏乱的现象,就表明出现了线程的安全问题
    • 举例:在某一个时刻,大量用户(线程)在抢购车票,同一个车票卖给了多个用户

案例代码

public class Ticket implements Runnable {
    private int ticket = 100;
    /*
    * 执行卖票操作
    */
    @Override
    public void run() {
        while (true){ //窗口永远在卖票
            if (ticket > 0) {//有票 可以卖
                //出票操作
                //使用sleep模拟一下出票时间
                try {
                   Thread.sleep(100);
                } catch (InterruptedException e) {
                   e. printStackTrace();
                }
                //获取当前线程对象的名字
                String name = Thread.currentThread().getName() ;
                System.out.println(name + "正在卖: " + ticket--) ;
            }
        }
    }
}

//测试类
public class Demo {
    public static void main(String[] args) {
        //创建线程任务对象
        Ticket ticket = new Ticket();
        //创建三个窗口对象
        Thread t1 = new Thread(ticket, "窗口1");
        Thread t2 = new Thread(ticket, "窗口2");
        Thread t3 = new Thread(ticket, "窗口3");

        //同时卖票
        t1.start();
        t2.start();
        t3.start();
    }
}

通过程序运行,将会发现存在有重复售票的情况发生(数据脏乱)

小结

什么情况下会出现线程安全问题?

  • 有多个线程,同时访问同一个资源,且多个线程会对共享资源进行写(修改)操作,此时就可能会发生线程中数据脏乱的问题(线程安全问题)
03_线程:线程安全之可见性

目标

  • 理解线程的可见性概念 【了解】

路径

  1. 可见性概述
  2. 可见性问题的代码演示

可见性

可见性:当多个线程访问同一个资源时,一个线程修改了这个资源的值,其他线程能够立即看得到修改的值

可见性问题:多个线程访问同一个资源时,一个线程修改了资源的值,其他线程看不到修改后的值

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-oujShni5-1617098707199)(JavaSE%EF%BC%9ADay09_%E9%9A%8F%E5%A0%82%E7%AC%94%E8%AE%B0.assets/1758811-20190820220137162-379693173.png)]

每个线程有独立的工作内存,操作数据时,是从主内存将变量读取到自己的工作内存,然后在工作内存中进行运算再把变量写回到主内存中

案例代码

问题描述:多个线程同时访问了共享资源,其中一个线程改变了共享资源的值。但是不能实时的让其他线程更新到

//线程类
class MyThread extends Thread {
    public static int a = 0;//共享变量(全局变量) //存储在主内存中

    @Override
    public void run() {
        try {
            Thread.sleep(2000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        System.out.println("将主内存的值,a修改为1");
        a = 1;//在线程自己的栈空间中运算后,再把运算的结果写入到主内存中的共享变量
    }
}

//测试类
public class Demo01 {
    public static void main(String[] args) {
        new MyThread().start();//开启线程

        while (true) {
            if (MyThread.a == 1) {
                System.out.println("主线程检测到了主内存中a的值为1");
            }
        }
    }
}

小结

可见性:

  • 多个线程在访问同一个资源时,一个线程修改了共享资源中的值,其他线程应该立刻能看到(正常的)

可见性问题(线程安全问题):

  • 多个线程在访问同一个资源时,一个线程修改了共享资源中的值,其他线程看不到共享资源修改后的值

原理:JVM内存模型

  • 线程把主内存中的数据,读取自己的工作栈空间,在自己的工作栈空间中运算后,再把运算后的数据写入到主内存中
04_线程:线程安全之有序性

目标

  • 能够理解有序性的概念 【了解】

路径

  1. 有序性概述
  2. 重排概念

有序性

有序性:多线程在执行的时候,代码执行顺序应该和代码的编写顺序要一样

有序性问题:

  • 代码在编译期间,为了优化指令,对指令进行了重排

    在单线程环境下是没问题的,但是在多线程环境下会出现不确定的结果

重排概念

  • 有些时候“编译器”在编译代码时,会对代码进行“重排”

    • int a = 10;     //1
      int b = 20;     //2
      int c = a + b;  //31行和第2行可能会被"重排":可能先编译第2行,再编译第1行
      在执行第3行之前,会将1,2编译完毕。12先编译谁,不影响第3行的结果
      
  • 在多线程中,代码重排,可能会对另一个线程访问的结果产生影响

在这里插入图片描述

多线程中,通常是不希望对一些代码进行重排的(书写顺序和执行顺序一致)

小结

出现有序性问题原因:

  • 编译器在编译代码,重排代码的顺序。造成代码执行顺序和代码书写顺序不一致
  • 由于多线程是由CPU随机切换执行,就会发生:执行中数据脏乱
05_线程:线程安全之原子性

目标

  • 理解原子性的概念 【了解】

路径

  1. 原子性概述
  2. 原子性问题代码演示

原子性

原子性:一个操作或者多个操作,要么全部执行并且执行的过程不会被任何因素打断,要么就都不执行

原子性问题:

  • 一个操作或者多个操作,执行的过程中被其他因素影响,导致了数据结果的脏乱

案例代码

public class MyThread extends Thread {
    static int num = 0;

    @Override
    public void run() {
        for (int i = 0; i < 100; i++) {
            num++;
        }
        System.out.println(Thread.currentThread().getName()+"线程执行结束");
    }
}

public class Test1 {
    public static void main(String[] args) {
        new MyThread().start();
        new MyThread().start();

        try {
            Thread.sleep(2000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("数据的值:"+MyThread.num);
    }
}

小结

原子性:线程中有多个操作在执行时,要保证这些操作要全部完成之后,才执行其他线程中的操作

  • 要么全部执行,要么全部不执行

出现原子性的原子性的原因:

  • 当前线程在执行多个操作时,被CPU切换到其他线程上执行,而其他线程执行时影响了当前线程的操作,造成数据脏乱
06_线程:volatile关键字

目标

  • 使用volatile解决线程的可见性问题、有序性问题 【掌握】

路径

  1. volatile关键字介绍
  2. 使用volatile解决可见性问题
  3. 使用volatile解决有序性问题
  4. volatile对于原子性问题的验证

volatile关键字

volatile是一个"变量修饰符",它只能修饰"成员变量",它能强制线程每次使用前都从主内存获取值,并能保证此变量不会被编译器优化(重排)

  • volatile能解决变量的可见性、有序性

  • volatile不能解决变量的原子性

volatile解决可见性问题

public class MyThread extends Thread {
    //共享资源
    volatile static int a = 0; //在变量时添加关键字:volatile
    //volatile关键字的作用:
    //1、不允许重排
    //2、每次使用数据时,都是从主内存中获取

    public void run() {
        try {
            Thread.sleep(2000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        System.out.println("将主内存的值,a修改为1");
        a = 1;//在线程自己的栈空间中运算后,再把运算的结果写入到主内存中的共享变量
        System.out.println("修改成功!");
        System.out.println(a);
    }
}

volatile解决有序性问题

当变量被修饰为volatile时,会禁止代码重排

测试:volatile能否解决原子性问题

public class MyThread extends Thread {
   volatile static int num = 0;

    @Override
    public void run() {
        for (int i = 0; i < 100; i++) {
            num++;
            System.out.println(num);
        }
        System.out.println(Thread.currentThread().getName()+"线程执行结束");
    }
}
public class Test1 {
    public static void main(String[] args) {
        new MyThread().start();
        new MyThread().start();

        try {
            Thread.sleep(2000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("数据的值:"+MyThread.num);
    }
}


//运行结果:  缺少2(数据脏乱问题存在)
1
3
4
5
6
7

小结

针对线程中的可见性问题、有序性问题,可以使用volatile关键字解决

使用方式:volatile是一个变量修饰符号

class{
    //共享变量
    volatile static int num;
}

原理:

  • 可见性:每次都是从主内存中获取数据
  • 有序性:volatile关键字修饰的变量,不允许进行重排

volatile不能解决线程中原子性问题

07_线程:原子类

目标

  • 使用AtomicInteger解决可见性,原子性,有序性问题 【掌握】

路径

  1. 原子类概述
  2. 原子类AtomicInteger的介绍
  3. 使用原子类解决原子性问题

原子类

原子类,可以保证数据的原子性,可见性,有序性(可以保证线程安全)

在java.util.concurrent.atomic包下定义了一些对“变量”操作的“原子类”:

  • java.util.concurrent.atomic.AtomicInteger:对int变量操作的“原子类”;

  • java.util.concurrent.atomic.AtomicLong:对long变量操作的“原子类”;

  • java.util.concurrent.atomic.AtomicBoolean:对boolean变量操作的“原子类”;

简单理解:原子类作为共享变量的类型存在

原子类AtomicInteger

构造方法:

public AtomicInteger() //创建具有初始值 0 的新 AtomicInteger
public AtomicInteger(int initialValue) //创建具有给定初始值的新 AtomicInteger

常用方法:

public int getAndIncrement()  //以原子方式将当前值加 1。 
public int get()  //获取当前值

AtomicInteger解决原子性问题

import java.util.concurrent.atomic.AtomicInteger;

public class MyThread4 extends Thread {
    //使用原子类
    static AtomicInteger num = new AtomicInteger();

    @Override
    public void run() {
        for (int i = 0; i < 100; i++) {
            System.out.println(Thread.currentThread().getName()+"="+num.getAndIncrement());
        }
        System.out.println(Thread.currentThread().getName() + "线程执行结束");
    }
}


public class Test1 {
    public static void main(String[] args) {
        new MyThread4().start();
        new MyThread4().start();

        try {
            Thread.sleep(2000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("数据的值:"+MyThread4.num);
    }
}

小结

原子类,是用来解决线程的可见性问题、可序性问题、原子性问题

原子类:AtomicInteger、AtomicLong 、Atomic…

08_线程:原子类CAS机制

目标

  • 能够理解CAS思想 【了解】

路径

  1. CAS原理介绍
  2. 底层源码分析

CAS原理

原子类为什么能够实现原子性,底层的原理?

  • 自旋+CAS机制(CAS:Compare And Swap 先比较然后再做交换)

在这里插入图片描述

源码分析

//AtomicInteger类:
public final int incrementAndGet() {
  		//this:就是原子类对象   AtomicInteger对象
  		//VALUE: 是原子类对象value在对象中的地址偏移量,目的是为了能够实时的获取对象中value的值
        return U.getAndAddInt(this, VALUE, 1) + 1;
                         //     Obj    地址    1 
                         //     this+VALUE = AtomicInteger对象中的value属性值   
}

Unsafe类:                //      当前对象     地址偏移量      1
public final int getAndAddInt(Object o, long offset, int delta) {
                           //    o+offset=AtomicInteger对象中的value属性值 
        int v;
  
        do {
            //v = AtomicInteger对象中的value属性值  //假设v=10
            v = getIntVolatile(o, offset); //实时获取获取原子类对象中value的值 
            
        } while (!weakCompareAndSetInt(o, offset, v, v + delta));
                             //   当前对象  地址偏移量  10   10+1
        return v;
}

 public final boolean weakCompareAndSetInt(Object o, long offset,
                                              int expected,
                                              int x) {
      
        //o: 就是原子类对象            
        //offset:原子类对象,value属性的位置
        //expected: 期望值(旧值)
        //x : 将要设置交换的值
        return compareAndSetInt(o, offset, expected, x);
                                        //   10     11
}

		
public final native boolean compareAndSetInt(Object o, long offset,
                                                 int expected,
                                                 int x);
		//o和offset :实时获取最新值
        //expected : 获取的旧值  //之前读取主内存中的值
        //x : 将要设置交换的值     //要写入到主内存中的值

        //获取AtomicInteger对象中最新的值: o+offset
        //拿获取到的value值 和  expected变量的值   进行比较
        //相等: 把x的值 写入到主内存中
        //不相等:自旋(重新读主内存中的值 => 修改 => CAS判断 ......)
      

小结

CAS原理 :

  • 在把线程栈空间中的数据写入到主内存之前,先拿主内存中的值和当前栈空间中的值(旧值),进行比较
  • 相等:(其他没有干扰) 把栈空间中修改后的数据写入到主内存中
  • 不相等:(其他线程干扰,影响了主内存中的数据值)
    • 重新从主内存中读取新的值
    • 线程栈空间重新运算…
09_线程:操作数组的原子类

目标

  • 使用数组原子类解决原子性问题 【掌握】

路径

  1. 数组原子类介绍
  2. 原子类AtomicIntegetArray
  3. 使用AtomicIntegetArray类解决数组原子性问题

数组相关的原子类介绍

java.util.concurrent.atomic.AtomicIntegetArray:对int数组操作的原子类java.util.concurrent.atomic.AtomicLongArray:对long数组操作的原子类java.utio.concurrent.atomic.AtomicReferenceArray:对引用类型数组操作的原子类

以上原子类型表示的数组,能够保证数据的安全

原子类AtomicIntegetArray

构造方法:

AtomicIntegerArray(int length)  //创建给定长度的新 AtomicIntegerArray。 
AtomicIntegerArray(int[] array)  //创建与给定数组具有相同长度的新AtomicIntegerArray,并从给定数组复制其所有元素 

常用方法:

int getAndIncrement(int i)   //以原子方式将索引 i 的元素加 1。 
int get(int i)   //获取位置 i 的当前值。 

解决原子性问题

普通数组中存在的原子性问题

//线程
class MyThread1 extends Thread {
    public static int[] arr = new int[10];

    @Override
    public void run() {
        for (int i = 0; i < 10000; i++) {
            arr[0]++;//对0索引对应的元素进行自增
        }
        System.out.println(getName() + "自增完成~~");
    }
}

//测试类
public class Demo02 {
    public static void main(String[] args) throws InterruptedException {
        new MyThread1().start();
        new MyThread1().start();
        new MyThread1().start();

        Thread.sleep(1000);
        System.out.println("累加结果:" + MyThread1.arr[0]);
    }
}

使用AtomicIntegerArray原子类解决数组的原子性问题:

//线程
public class MyThread extends Thread{
    //使用普通类型的数组(共同资源)
    //public static int[] arr = new int[10];

    //使用数组原子类
   static AtomicIntegerArray array = new AtomicIntegerArray(10);

    @Override
    public void run() {
        for (int i = 0; i < 10000; i++) {
            //arr[0]++;//对0索引对应的元素进行自增
            array.getAndIncrement(0);
        }
        System.out.println(getName() + "自增完成~~");
    }
}
//测试
public class Test01 {
    public static void main(String[] args){
        new MyThread().start();
        new MyThread().start();
        new MyThread().start();

        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("累加结果:" + MyThread.array.get(0));
    }
}

小结

在多线程中,如果共公资源是一个数组,那么数组也会存在:原子性问题

针对int类型数组中的原子性问题,需要使用:AtomicIntegerArray原子类解决int类型数组

原子类所属的包:java.util.concurrent.atomic

10_线程:多行代码原子性问题

目标

  • 能够理解多行代码原子性问题及解决思路 【了解】

路径

  1. 代码演示:多行代码中出现的原子性问题
  2. 分析案例中原子性问题出现的原因

多行代码中出现的原子性问题

//线程
class TicketTask implements Runnable {
    //原子类
    private AtomicInteger tickets = new AtomicInteger(100);

    @Override
    public void run() {
        while (true) {
            if (tickets.get()> 0) {
                try {
                    Thread.sleep(50);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                String name = Thread.currentThread().getName();
                
                System.out.println(name + ":" + tickets.get());
                
                tickets.getAndDecrement();//卖完一张减1
                
            } else {
                break;
            }
        }

    }
}

//测试类
public class Demo01 {
    public static void main(String[] args) {
        //卖票任务
        TicketTask ticketTask = new TicketTask();//定义一个任务,让三个线程完成

        Thread t1 = new Thread(ticketTask,"【窗口1】");
        Thread t2 = new Thread(ticketTask,"【窗口2】");
        Thread t3 = new Thread(ticketTask,"【窗口3】");

        //执行
        t1.start();
        t2.start();
        t3.start();

    }
}

//运行结果:
【窗口2】 出售编号为:100的票
【窗口1】 出售编号为:100的票
【窗口3】 出售编号为:100的票
【窗口1】 出售编号为:97的票
【窗口2】 出售编号为:97的票
【窗口3】 出售编号为:97的票
【窗口2】 出售编号为:94的票
【窗口1】 出售编号为:94的票

以上程序中有多行代码都使用了共享资源(使用了原子类)

当有多行代码访问了共享资源时,在多线程环境下,即使使用原子类也无法解决安全问题

分析为什么多行代码情况下,原子类无法解决线程安全问题
在这里插入图片描述

在多线程中,有多个位置,针对原子类进行操作,此时程序中还是存在原子性问题:

tickNum.get()>0 、tickNum.get()、 tickNum.getAndDecrement()

以上3处代码要做为一个整体来执行(要么全部不成功、要么全部成功)

小结

当线程中,使用了原子类,但有多处位置针对原子类进行操作,此时多线程中还是会存在原子性问题

当有多行代码访问了共享资源时,在多线程环境下,即使使用原子类也无法解决安全问题

  • 解决方案:同步 synchronized
11_线程:synchronized关键字

目标

  • 能够使用synchronized代码块解决线程安全问题 【掌握】

路径

  1. synchronized关键字介绍
  2. synchronized的使用方式
  3. 使用synchronized代码块解决多行代码原子性问题

synchronized

  • synchronized关键字:表示“同步”的。它可以对“多行代码”进行“同步”:将多行代码当成是一个完整的整体,一个线程如果进入到这个代码块中,会全部执行完毕,执行结束后,其它线程才会执行。这样可以保证这多行的代码作为完整的整体,被一个线程完整的执行完毕。

synchronized的意义:

  • 把多行代码做为一个整体来执行
  • 只有做为整体的多行代码全部执行完之后,才会执行其它线程

synchronized的使用方式

synchronized有两种应用方式:

  • 同步代码块

    • synchronized(同步锁){
          //存在原子性问题的多行代码
      }
      
  • 同步方法

同步机制的理念:

  • 在同步代码中添加了一个锁,利用锁实现了同步机制
    • 当线程A进入同步代码中执行时,会获取到一个锁(锁被线程A拿到)
    • 当线程B被CPU切换并执行时,线程B要进入同步代码中执行,此时需要拿到锁才能执行,因为线程B没有锁,只能等待,等待线程A释放锁。

使用synchronized同步代码块,解决多行代码的原子性问题

import java.util.concurrent.atomic.AtomicInteger;

//线程任务
public class Ticket implements Runnable {
    //原子类(票数)【共享资源】
    AtomicInteger tickNum = new AtomicInteger(100);

    //同步代码块上的锁只是一个概念,可以使用任意类型作为锁存在
    Object lock = new Object();

    @Override
    public void run() {
        while (true) {
            //同步代码块上需要一个对象锁,可以使用任意类型的对象作为锁存在
            synchronized (lock) {
                if (tickNum.get() > 0) {//票还没卖完
//                    try {
//                        //Thread.sleep(50); //模拟出票时间
//                        //lock.wait(50);
//                    } catch (InterruptedException e) {
//                        e.printStackTrace();
//                    }
                    String name = Thread.currentThread().getName(); //获取当前线程的名称
                    System.out.println(name + " 出售编号为:" + tickNum.get() + "的票");
                    //修改原子类的值
                    tickNum.getAndDecrement();//相当于value-1
                } else {//票已卖完
                    break;
                }
            }
        }
    }
}


public class Test1 {
    public static void main(String[] args) {
        //实例化线程任务
        Ticket task = new Ticket();

        //创建三个卖票窗口
        Thread t1 = new Thread(task, "【窗口1】");
        Thread t2 = new Thread(task, "【窗口2】");
        Thread t3 = new Thread(task, "【窗口3】");

        t1.start();
        t2.start();
        t3.start();
    }
}

小结

当程序中有多行代码针对共享资源进行操作时,会存在:原子性问题

可以使用同步机制,来解决多行代码操作共享资源时存在的原子性问题

同步的使用:

  • 同步代码块

    • Object lock = new Object();
      synchronized(lock){
          //多行操作共享资源的代码
      }
      
  • 同步方法

12_线程:同步方法

目标

  • 能够使用同步方法解决线程安全问题 【掌握】

路径

  1. 同步方法的定义
  2. 使用同步方法解决线程安全问题

同步方法

使用关键字synchronized修饰的方法,称为:同步方法

同步方法的意义 :

  • 书写在方法体中的代码,相当于一个同步代码块(方法上添加了锁)

同步方法的书写格式:

修饰符 synchronized 返回值类型  方法名(参数类型 参数){
    
}

使用同步方法解决线程安全问题

public class Ticket implements Runnable {
    //原子类(票数)【共享资源】
    AtomicInteger tickNum = new AtomicInteger(100);

    @Override
    public void run() {
        while (true) {
            //调用同步方法
            ticket();
        }
    }

    //自定义一个方法
    public synchronized void ticket() {
        if (tickNum.get() > 0) {
            String name = Thread.currentThread().getName(); //获取当前线程的名称
            System.out.println(name + " 出售编号为:" + tickNum.get() + "的票");
            //修改原子类的值
            tickNum.getAndDecrement();//相当于value-1
        }
    }
}


//测试类
public class Test1 {
    public static void main(String[] args) {
        //实例化线程任务
        Ticket task = new Ticket();

        //创建三个卖票窗口
        Thread t1 = new Thread(task, "【窗口1】");
        Thread t2 = new Thread(task, "【窗口2】");
        Thread t3 = new Thread(task, "【窗口3】");

        t1.start();
        t2.start();
        t3.start();
    }
}

疑问:同步方法中的锁在哪里?

答案:同步方法上的锁会根据不同的方法使用不同的锁(隐式)

//非静态方法
public synchronized void method(){
    //同步方法中的锁:this (拿当前对象做为锁)
}
//静态方法
public static synchronized void method(){
    //静态方法中不能使用关键字:this、super
    
    //静态同步方法中的锁:类名.class (这个知识点在反射技术中讲解)
}

小结

同步方法定义:

修饰符 synchronized 返回值类型  方法名(参数类型 参数){
    
}
  • 同步方法在执行时,会获取到同步锁(this或类名.class),拿到锁后就进入方法体中执行
  • 其他线程在调用同步方法时,判断是否有同步锁,如果没有拿锁,则在方法外等待
13_线程:Lock锁

目标

  • 能够使用Lock锁解决线程安全问题 【掌握】

路径

  1. Lock介绍
  2. 使用Lock解决线程安全问题

Lock

java.util.concurrent.locks.Lock机制提供了比同步代码块和同步方法更广泛的锁定操作,同步代码块/同步方法具有的功能Lock都有,除此之外更强大。

Lock是一个接口,无法实例化,需要使用子类ReentrantLock进行实例化:

public ReentrantLock()  //无参构造方法
    
//示例
Lock lock = new ReentrantLock();    

Lock是一个接口,里面定义了获取锁及释放锁的功能:

public void lock()     //添加同步锁
public void unlock()   //释放同步锁  

可以使用lock()和unlock()代替同步代码块

使用Lock锁解决线程安全问题

public class Ticket implements Runnable {
    //原子类(票数)【共享资源】
    AtomicInteger tickNum = new AtomicInteger(100);

    //实例化Lock
    Lock lock = new ReentrantLock();

    @Override
    public void run() {
        while (true) {
            lock.lock(); //添加上Lock锁

            if (tickNum.get() > 0) {//票还没卖完

                String name = Thread.currentThread().getName(); //获取当前线程的名称
                System.out.println(name + " 出售编号为:" + tickNum.get() + "的票");
                //修改原子类的值
                tickNum.getAndDecrement();//相当于value-1
            } else {//票已卖完
                break;
            }

            lock.unlock();//释放Lock锁
        }
    }
}


public class Test1 {
    public static void main(String[] args) {
        //实例化线程任务
        Ticket task = new Ticket();

        //创建三个卖票窗口
        Thread t1 = new Thread(task, "【窗口1】");
        Thread t2 = new Thread(task, "【窗口2】");
        Thread t3 = new Thread(task, "【窗口3】");

        t1.start();
        t2.start();
        t3.start();
    }
}

小结

JDK中提供了一个比synchronized同步机制更强大的一个Lock锁

使用Lock的方式:

//创建Lock对象
Lock lock = new ReenTrantLock();

//在方法体中,添加lock锁、释放lock锁
public void method(){
    //....
    lock.lock();
      //.......
    
    lock.unlock();  
} 

在之前学习的集合:

  • List:ArrayList //线程不安全
  • Set:HashSet //线程不安全
  • Map:HashMap //线程不安全
14_线程:CopyOnWriteArrayList类

目标

  • 使用CopyOnWriteArrayList解决多线程中集合容器安全问题 【掌握】

路径

  1. 演示:多线程环境下ArrayList
  2. CopyOnWriteArrayList类介绍
  3. 演示:使用CopyOnWriteArrayList解决多线程中集合安全问题

多线程环境下ArrayList

ArrayList类是线程不安全的集合

//线程
class MyThread extends Thread {
    public static ArrayList<Integer> list = new ArrayList<>();

    @Override
    public void run() {
        for (int i = 0; i < 10000; i++) {
            list.add(i);
        }
        System.out.println(getName() + "添加完毕~~");
    }
}

public class Demo01 {
    public static void main(String[] args) throws InterruptedException {
        new MyThread().start();
        new MyThread().start();

        Thread.sleep(1000);
        System.out.println("集合元素个数:"+MyThread.list.size());
    }
}

CopyOnWriteArrayList类

java.util.concurrent.CopyOnWriteArrayList

  • 是一个线程安全的List集合类(可以理解为是一个线程安全的ArrayList集合)

构造方法:

public CopyOnWriteArrayList()  //创建一个线程安全的ArrayList集合对象

使用CopyOnWriteArrayList集合解决线程安全问题

import java.util.concurrent.CopyOnWriteArrayList;

public class MyThread1 extends Thread{
  //创建集合(共享资源)  //线程安全的ArrayList集合
  static CopyOnWriteArrayList<Integer> list =new CopyOnWriteArrayList<>();

    @Override
    public void run() {
        for (int i = 0; i < 10000; i++) {
            list.add(i);
        }
        System.out.println("集合添加完成!!!");
    }
}

//测试类
public class Test01 {
    public static void main(String[] args) {
        MyThread1 t1 = new MyThread1();
        MyThread1 t2 = new MyThread1();

        t1.start();
        t2.start();

        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        System.out.println("集合的大小为:"+MyThread1.list.size());
    }
}

小结

在开发中,如操作List集合时,遇到线程不安全的情况,需要使用:JUC并发包下的集合对象

  • java.util.concurrnet.CopyOnWriteArrayList类
15_线程:CopyOnWriteArraySet类

目标

  • 使用CopyOnWriteArraySet解决多线程中集合容器安全问题 【掌握】

路径

  1. 演示:多线程环境下HashSet
  2. CopyOnWriteArraySet类介绍
  3. 演示:使用CopyOnWriteArraySet解决多线程中集合安全问题

多线程环境下HashSet

  • HashSet在多线程环境是不安全的,去重不彻底
//线程
class MyThread extends Thread {
    public static HashSet<Integer> set = new HashSet<>();

    @Override
    public void run() {
        for (int i = 0; i < 10000; i++) {
            set.add(i);
        }
        System.out.println(getName() + "添加结束!!");

    }
}

//测试类
public class Demo01 {
    public static void main(String[] args) throws InterruptedException {
        new MyThread().start();
        new MyThread().start();
        new MyThread().start();
        
        Thread.sleep(2000);
        System.out.println("set集合元素大小:" + MyThread.set.size());//10884

    }
}

CopyOnWriteArraySet类

java.util.concurrent.CopyOnWriteArraySet

  • 是一个线程安全的Set集合类(可以理解为是一个线程安全的HashSet集合)

构造方法:

public CopyOnWriteArraySet() //创建一个线程安全的HashSet集合对象

使用CopyOnWriteArraySet类解决集合安全问题

import java.util.concurrent.CopyOnWriteArraySet;

public class MyThread2 extends Thread {
    //创建集合(共享资源)
    public static CopyOnWriteArraySet<Integer> set = new CopyOnWriteArraySet<>();
    
    @Override
    public void run() {
        for (int i = 0; i < 10000; i++) {
            set.add(i);
        }
        System.out.println(getName() + "添加结束!!");
    }
}

//测试类
public class Test02 {
    public static void main(String[] args) {
        new MyThread2().start();
        new MyThread2().start();
        new MyThread2().start();

        try {
            Thread.sleep(2000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("set集合元素大小:" + MyThread2.set.size());
    }
}

小结

当程序中使用HashSet集合时,遇到线程不安全问题,使用:CopyOnWriteArraySet集合代替

16_线程:ConcurrentHashMap类

目标

  • 使用ConcurrentHashMap解决多线程中集合容器安全问题 【掌握】

路径

  1. 演示:多线程环境下HashMap
  2. ConcurrentHashMap类介绍
  3. 演示:使用ConcurrentHashMap解决多线程中集合安全问题

多线程环境下HashMap

HashSet的底层是HashMap实现,HashSet是不安全的可以推断出HashMap也是不安全

//线程
class MyThread extends Thread{
    public static HashMap<Integer, String> map = new HashMap<>();

    @Override
    public void run() {
        for (int i = 0; i < 10000; i++) {
            map.put(i, "");
        }

        System.out.println(getName()+"数据添加完毕");

    }
}

//测试类
public class Demo01 {
    public static void main(String[] args) throws InterruptedException {
        new MyThread().start();
        new MyThread().start();
        new MyThread().start();
        Thread.sleep(2000);
        System.out.println("集合元素大小:"+MyThread.map.size());
    }
}

ConcurrentHashMap类

构造方法:

ConcurrentHashMap() //创建一个线程安全的HashMap集合对象

使用ConcurrentHashMap类解决线程安全问题

import java.util.concurrent.ConcurrentHashMap;

public class MyThread3 extends Thread{
    //创建集合(共享资源)
    public static ConcurrentHashMap<Integer, String> map = new ConcurrentHashMap<>();

    @Override
    public void run() {
        for (int i = 0; i < 10000; i++) {
            map.put(i, "");
        }
        System.out.println(getName()+"数据添加完毕");
    }
}



public class Test03 {
    public static void main(String[] args) throws InterruptedException {
        new MyThread3().start();
        new MyThread3().start();
        new MyThread3().start();
        Thread.sleep(2000);
        System.out.println("集合元素大小:"+MyThread3.map.size());
    }
}

小结

ArrayList类:List集合,属于线程不安全的集合对象

HashSet类:Set集合,属于线程不安全的集合对象

HashMap类:Map集合,属于线程不安全的集合对象

针对以上三个线程不安全的集合对象,如要保证线程是安全的,需要使用JUC并发包下的集合对象:

  • CopyOnWriteArrayList , 是一个线程安全的List集合对象
  • CopyOnWirteArraySet ,是一个线程安全的Set集合对象
  • ConcurrnetHashMap , 是一个线程安全的Map集合对象

说明:线程不安全的集合执行效率高、线程安全的集合执行效率低

  • 当开发中对线程安全没有要求时,就使用普通的集合对象
17_线程:CountDownLatch类

目标

  • 能够使用并理解CountDownLatch类的功能

路径

  1. CountDownLatch类介绍
  2. CountDownLatch类常用方法
  3. 案例:CountDownLatch的应用

CountDownLatch类

  • java.util.concurrent.CountDownLatch 是一个同步辅助类

  • 在完成一组正在其他线程中执行的操作之前,CountDownLatch允许一个或多个线程一直等待

    • 例如:
      线程1要执行打印:A和C
      线程2要执行打印:B
      但线程1在打印A后,要线程2打印B之后才能打印C,
      所以:线程1在打印A后,必须等待线程2打印完B之后才能继续执行
      

CountDownLatch类中方法

构造方法:

public CountDownLatch(int count)  //创建一个指定计数器的CountDownLatch对象
//参数:count就是要计数的次数

常用方法:

public void await() throws InterruptedException// 让当前线程等待
public void countDown()	// 计数器进行减1(如果计数到达零,则释放所有等待的线程)  
示例:打印A、B、C (按顺序打印)
    
线程1执行打印:A、C
线程2要执行打印:B

CountDownLatch c = new CountDownLatch(1);//要等待1个线程  count=1    
    
线程1:
     打印 A
     c.await();//处于等待
               //等到count=0时,结束等待,继续执行
     打印 C
     
线程2:
     打印 B
     c.countDown();//对count进行-1   count=0

案例代码

需求:

  • 甲,乙,丙三人在开发一个项目。甲和乙负责开发功能,丙负责发布项目

  • 甲开发功能需要3秒

  • 乙开发功能需要5秒

/*思路:
      甲,乙分成一组:负责计数
      丙 分组一组:负责等待  (甲乙执行完,再执行)
*/
//步骤:
//1. 定义一个CountDownLatch计数器,计数两次(甲,乙各自计数一次)
//2. 分别定义线程表示甲,乙,丙


import java.util.concurrent.CountDownLatch;

public class CountDownDemo1 {
    public static void main(String[] args) {
        //创建一个计数器对象(计数2次)
        CountDownLatch downLatch = new CountDownLatch(2);

        //匿名内部类
        //丙
        new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println("丙 进入项目组.....");
                try {
                    //只要计数器不等于0,就处于一直等待状态
                    //当计数器等于0,结束等待,继续向下执行
                    downLatch.await();//等待甲乙开发结束
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println("甲和乙开完功能,丙开始部署项目上线~~~~~");
            }
        }).start();
        
        //甲
        new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println("甲 进入项目组.....");
                try {
                    Thread.sleep(3000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println("甲 项目开发结束!");
                downLatch.countDown();//计数器-1
            }
        }).start();
        
        //乙
        new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println("乙 进入项目组.....");
                try {
                    Thread.sleep(5000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println("乙 项目开发结束!");
                downLatch.countDown();//计数器-1
            }
        }).start();
    }
}

小结

java.util.concurrent.CountDownLatch类:是一个线程辅导类

  • 应用场景:当执行的线程要等待其他线程中的任务完成后才继续执行时,可以使用CountDownLatch(线程计数器类)
18_线程:CyclicBarrier类

目标

  • 能够使用并理解CyclicBarrier的功能

路径

  1. CyclicBarrier类介绍
  2. CyclicBarrier类常用方法
  3. 案例:CyclicBarrier的应用

CyclicBarrier类

  • java.util.concurrent.CyclicBarrier 是一个同步辅助类
  • CyclicBarrier的字面意思是可循环使用(Cyclic)的屏障(Barrier)。
  • CyclicBarrier 允许一组线程(多个线程)互相等待,直到到达某个公共屏障点
    • 它要做的事情是,让一组线程到达一个屏障(也可以叫同步点)时被阻塞,直到最后一个线程到达屏障时,屏障才会开门,所有被屏障拦截的线程才会继续运行。
    • 例如:公司召集5名员工开会,等5名员工都到了,会议开始

CyclicBarrier 应用场景:

  • 当一组线程全部达到共同点(公共屏障点),才会执行另一个线程
    • 当同一组线程中还有线程未达到共同点,已达到共同点的线程进入阻塞状态(等待)

CyclicBarrier类中方法

构造方法:

public CyclicBarrier(int parties, Runnable barrierAction)  //创建一个循环屏障,定义指定相互等待的线程个数(parties),同时指定一个Runnable任务,当线程都达到屏障点时,先执行这个任务
    
参数:
int parties   要等待的线程个数
Runnable barrierAction  当所有线程都达到屏障点后,要执行任务   

常用方法:

public int await() //每个线程调用await方法时告诉CyclicBarrier已经到达了屏障,然后当前线程被阻塞

案例代码

需求:用代码实现甲,乙,丙要开会,只有三人都同时到场才能开会。

  • 甲达到现场需要3秒,乙达到现场需要2秒,丙达到现场需要5秒
/*步骤:
1. 定义循环屏障的对象,设定参与等待的数量为3
2. 使用三个线程模拟甲,乙,丙
*/

public class CyclicDemo1 {
//    需求:用代码实现甲,乙,丙要开会,只有三人都同时到场才能开会。
//    甲达到现场需要3秒,乙达到现场需要2秒,丙达到现场需要5秒
    public static void main(String[] args) {

        //创建循环屏障点类
        CyclicBarrier cyclicBarrier = new CyclicBarrier(3, new Runnable() {
            @Override
            public void run() {
                System.out.println("人到齐了,可以开会了");
            }
        });

        //甲
        new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println("甲 进入公司了....");

                try {
                    Thread.sleep(3000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println("甲 等待其他同事...");
                try {
                    cyclicBarrier.await();//+1
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } catch (BrokenBarrierException e) {
                    e.printStackTrace();
                }
            }
        }).start();

        //丙
        new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println("丙 进入公司了....");

                try {
                    Thread.sleep(5000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println("丙 等待其他同事...");
                try {
                    cyclicBarrier.await();//+1
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } catch (BrokenBarrierException e) {
                    e.printStackTrace();
                }
            }
        }).start();


        //乙
        new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println("乙 进入公司了....");

                try {
                    Thread.sleep(2000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println("乙 等待其他同事...");
                try {
                    cyclicBarrier.await();//+1
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } catch (BrokenBarrierException e) {
                    e.printStackTrace();
                }
            }
        }).start();

    }
}

小结

CyclicBarrier类应用场景:

  • 在执行某个线程任务之前,需要先达成其他任务时,使用CyclicBarrier
    • 例如:开发时要求人员全部达到,才开会
19_线程:Semaphore类

目标

  • 能够使用并理解Semaphore的功能

路径

  1. Semaphore类介绍
  2. Semaphore类常用方法
  3. 案例:Semaphore的应用

Semaphore类

  • java.util.concurrent.Semaphore ,主要作用是控制线程的并发数量
  • Semaphore可以设置同时允许几个线程执行,常用于控制访问特定资源的线程数目

事例:控制某个旅游景点,参观的人数。某个景点只允许同时2个人访问。可以使用Semaphore去实现

Semaphore类中方法

构造方法:

public Semaphore(int permits)	//permits 表示许可线程的数量 

public Semaphore(int permits, boolean fair)	//fair表示公平性
//参数:
//permits  初始的许可线程数量
//fair     如果此信号量保证在争用时按先进先出的顺序授予许可,则为 true;否则为 false。    

常用方法:

public void acquire() throws InterruptedException	//表示获取许可
public void release()								//表示释放许可

案例代码

需求:控制某个旅游景点的参观人数。

  • 某个景点只允许同时2个人访问(使用Semaphore实现)

  • 有10个人访问景点,每个人访问景点需要花费时间3秒

/*步骤:
1. 创建信号量对象,设定许可数为2
2. 创建线程执行访问资源的任务,使用信号量Semaphore去控制并发数量
*/
import java.util.concurrent.Semaphore;

//模拟实现,参观景点的过程
class VisitAction implements Runnable {
    private Semaphore semaphore;

    public VisitAction(Semaphore semaphore) {
        this.semaphore = semaphore;
    }

    @Override
    public void run() {

        String name = Thread.currentThread().getName();
        System.out.println(name + "来到了景点的门口~~~~~~~");

        try {
            semaphore.acquire();//获取许可
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        System.out.println(name + "进入到景点,开始参观++++++++");

        try {
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        System.out.println(name + "离开景点,结束参观--------");

        semaphore.release();//释放许可
        
    }
}

//测试类
public class Demo01 {
    public static void main(String[] args) {
        //创建信号量,指定许可数为2
        Semaphore semaphore = new Semaphore(2);
        //参观景点任务
        VisitAction visitAction = new VisitAction(semaphore);

        //模拟10个线程表示10个参观者
        for (int i = 0; i < 10; i++) {
            new Thread(visitAction, "游客" + i).start();
        }
    }
}

小结

Semaphore类应用场景:

  • 当针对共享资源,进行线程访问量的设置时,可以使用Semaphore类

JUC并发包下的类:

  • CountDownLatch类
    • 应用场景:
      • 当前线程在执行时,可以等待其他线程先执行完,再执行当前线程
  • CyclicBarrier类
    • 应用场景:
      • 多个线程一起执行,当多个线程达到某个设置的共同点时,此时激活另一个线程,执行激活的线程
  • Semaphoer类
    • 应用场景:
      • 多个线程在访问共同资源时,可以针对共同资源进行线程并发数量的限制
20_线程:Exchanger类

目标

  • 能够使用并理解Exchanger的功能

路径

  1. Exchanger类介绍
  2. Exchanger类常用方法
  3. 案例:Exchanger的应用

Exchanger类

  • java.util.concurrent.Exchanger ,是一个用于线程间协作的工具类
  • Exchanger可以实现两个线程之间的数据交换
    • 这两个线程通过exchange方法交换数据,如果第一个线程先执行exchange()方法,它会一直等待第二个线程也执行exchange方法,当两个线程都到达同步点时,这两个线程就可以交换数据,将本线程生产出来的数据传递给对方

Exchanger类中方法

构造方法:

Exchanger()   //创建一个新的 Exchanger

常用方法:

 V exchange(V x) //等待另一个线程到达此交换点(除非当前线程被中断),然后将给定的对象传送给该线程,并接收该线程的对象
 //参数:v    传递给另一个线程的数据
 //返回值:V   接收到的另一个线程发送的数据    

案例代码

需求:模拟两个人交换礼物

import java.util.concurrent.Exchanger;
//线程A
class ThreadA extends Thread{
    Exchanger<String> ex ;
    public ThreadA(Exchanger<String> exchanger){
        this.ex = exchanger;
    }

    @Override
    public void run() {
        String name = Thread.currentThread().getName();
        System.out.println(name + "把'礼物A'送给别人...");
        try {
            System.out.println(name+"赠送并接收他人的礼物="+ex.exchange("礼物A"));
            //把 礼物A 发送给另一个线程
            //当另一个线程也执行:exchange()方法,就会接收到'礼物A'
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}
//线程B
class ThreadB extends Thread{
    Exchanger<String> ex ;
    public ThreadB(Exchanger<String> exchanger){
        this.ex = exchanger;
    }

    @Override
    public void run() {
        String name = Thread.currentThread().getName();
        System.out.println(name + "把'礼物B'送给别人...");
        try {
            System.out.println(name+"赠送并接收他人的礼物="+ex.exchange("礼物B"));
            //把 礼物B 发送给另一个线程
            //当另一个线程也执行:exchange()方法,就会接收到'礼物B'
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}


public class Test01 {
    public static void main(String[] args) {
        //创建对象
        Exchanger<String> exchanger = new Exchanger<>();

        //实例化两个线程对象(保证两个对象使用的是同一个Exchanger对象)
        ThreadA t1 = new ThreadA(exchanger);
        ThreadB t2 = new ThreadB(exchanger);

        t1.start();
        t2.start();
    }
}

小结

Exchanger类:

  • 当两个线程之间要进行数据的交互时,可以使用Exchaner类实现
  • 要保证两个线程使用的是同一个Exchaner对象
20_线程:Exchanger类

目标

  • 能够使用并理解Exchanger的功能

路径

  1. Exchanger类介绍
  2. Exchanger类常用方法
  3. 案例:Exchanger的应用

Exchanger类

  • java.util.concurrent.Exchanger ,是一个用于线程间协作的工具类
  • Exchanger可以实现两个线程之间的数据交换
    • 这两个线程通过exchange方法交换数据,如果第一个线程先执行exchange()方法,它会一直等待第二个线程也执行exchange方法,当两个线程都到达同步点时,这两个线程就可以交换数据,将本线程生产出来的数据传递给对方

Exchanger类中方法

构造方法:

Exchanger()   //创建一个新的 Exchanger

常用方法:

 V exchange(V x) //等待另一个线程到达此交换点(除非当前线程被中断),然后将给定的对象传送给该线程,并接收该线程的对象
 //参数:v    传递给另一个线程的数据
 //返回值:V   接收到的另一个线程发送的数据    

案例代码

需求:模拟两个人交换礼物

import java.util.concurrent.Exchanger;
//线程A
class ThreadA extends Thread{
    Exchanger<String> ex ;
    public ThreadA(Exchanger<String> exchanger){
        this.ex = exchanger;
    }

    @Override
    public void run() {
        String name = Thread.currentThread().getName();
        System.out.println(name + "把'礼物A'送给别人...");
        try {
            System.out.println(name+"赠送并接收他人的礼物="+ex.exchange("礼物A"));
            //把 礼物A 发送给另一个线程
            //当另一个线程也执行:exchange()方法,就会接收到'礼物A'
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}
//线程B
class ThreadB extends Thread{
    Exchanger<String> ex ;
    public ThreadB(Exchanger<String> exchanger){
        this.ex = exchanger;
    }

    @Override
    public void run() {
        String name = Thread.currentThread().getName();
        System.out.println(name + "把'礼物B'送给别人...");
        try {
            System.out.println(name+"赠送并接收他人的礼物="+ex.exchange("礼物B"));
            //把 礼物B 发送给另一个线程
            //当另一个线程也执行:exchange()方法,就会接收到'礼物B'
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}


public class Test01 {
    public static void main(String[] args) {
        //创建对象
        Exchanger<String> exchanger = new Exchanger<>();

        //实例化两个线程对象(保证两个对象使用的是同一个Exchanger对象)
        ThreadA t1 = new ThreadA(exchanger);
        ThreadB t2 = new ThreadB(exchanger);

        t1.start();
        t2.start();
    }
}

小结

Exchanger类:

  • 当两个线程之间要进行数据的交互时,可以使用Exchaner类实现
  • 要保证两个线程使用的是同一个Exchaner对象
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

编程小栈

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值