JAVA 核心基础 线程并发原理(知识点,讲解,练习,代码)

目录

 

1. 进程与线程认知强化

1.1如何理解进程与线程?

进程:

线程:

1.2如何理解多线程中的并发与并行?

并发:多线程抢占 CPU,可能不同时执行,侧重于多个任务交替执行。

并行:线程可以不共享 CPU,可每个线程一个 CPU 同时执行多个任务

1.3如果理解线程的声明周期及状态变化?

2. 线程并发安全问题认知强化

2.1. 如何理解线程安全与不安全?

2.2. 导致线程不安全的因素有哪些?

2.3. 如何保证并发线程的安全性?

2.4. Synchronized 关键字应用及原理分析?

2.5. 如何理解 volatile 关键字的应用?

2.6. 如何理解 happen-before 原则应用?

2.7. 如何理解 JAVA 中的悲观锁和乐观锁?

2.8. 如何理解线程的上下文切换?

2.9.如何理解死锁以及避免死锁问题?

3. 线程通讯与进程通讯应用基础

3.1. 如何理解进程与线程通讯?

3.2. 如何实现进程内部线程之间的通讯?

3.3. 如何实现进程之间间通讯(IPC)?


1. 进程与线程认知强化

1.1如何理解进程与线程?

首先我们先来了解一下关于线程的一些基本的定义:

进程:

进程:操作系统进行资源调度和分配的基本单位(例如浏览器,APPJVM)。

1.进程是系统资源分配的最小单位

2.一个程序拥有一个进程而一个进程可以有多个线程

线程:

线程:进程中的最小执行单位,是 CPU 资源的分配的基本单位(可以理解为一个顺序的执行流)。

1.线程是一个程序的最小执行单位

2.并发就是在单核处理器中同时处理多个任务

3.并行就是在多核处理器中同时处理多个任务

 
说明:多个线程可以共享所属进程的所有资源。
 

1.2如何理解多线程中的并发与并行?

并发:多线程抢占 CPU,可能不同时执行,侧重于多个任务交替执行。

现在的操作系统无论是 windows linux 还是 macOS 等其实都是多用户多任 务分时操作系统,使用这些操作系统的的用户可以“同时”干多件事情。但实际 上,对于单机 CPU 的计算机而言,在同一时间只能干一件事,为了看起来像是 “同时干多件事”分时操作系统把 CPU 的时间划分成了长短基本相同的时间区 间,即“时间片”,通过操作系统的管理,把时间片依次轮流的分配给各个线程 任务使用。我们看似的“同时干多件事”,其实是通过CPU 时间片技术并发完 成的。

并行:线程可以不共享 CPU,可每个线程一个 CPU 同时执行多个任务

个人认为并行只出现在多 CPU 或多核 CPU 中,而并发可理解为并行中的 一个子集。
 

1.3如果理解线程的声明周期及状态变化?

一个线程从创建,运行,到最后销毁的这个过程称之为线程的生命周期,在这个 生命周期过程中线程可能会经历如下几个状态:
新建状态,就绪状态,运行状态,阻塞状态,死 亡状态。

2. 线程并发安全问题认知强化

2.1. 如何理解线程安全与不安全?

多个线程并发执行时,仍旧能够保证数据的正确性,这种现象称之为线程安全
多个线程并发执行时,不能能够保证数据的正确性,这种现象称之为线程不安全
案例 1 :模拟多个线程同时执行售票操作
编写售票任务类:
public class TestThread implements Runnable{
    int ticket=30;
    public void run(){
        doTicket();
    }

    private void doTicket() {
        while (true){
            if (ticket<=0)break;
            System.out.println(ticket--);
           //让线程阻塞一下
            sleep();

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

    public static void main(String[] args) {
        //几核cpu
        Runtime r=Runtime.getRuntime();
        System.out.println("几核cpu"+r.availableProcessors());
        //创建卖票任务
        TestThread  task=new TestThread();
        //多线程模拟多个售票窗口
        Thread t1=new Thread(task);
        Thread t2=new Thread(task);
        Thread t3=new Thread(task);
        Thread t4=new Thread(task);
        //
        t1.start();
        t2.start();
        t3.start();
        t4.start();
    }
}

结果:

"C:\Program Files\Java\jdk1.8.0_251\bin\java.exe" "-javaagent:E:\Program Files\JetBrains\IntelliJ IDEA 2020.1.4\IntelliJ IDEA 2020.1\lib\idea_rt.jar=52896:E:\Program Files\JetBrains\IntelliJ IDEA 2020.1.4\IntelliJ IDEA 2020.1\bin" -Dfile.encoding=UTF-8 -classpath "C:\Program Files\Java\jdk1.8.0_251\jre\lib\charsets.jar;C:\Program Files\Java\jdk1.8.0_251\jre\lib\deploy.jar;C:\Program Files\Java\jdk1.8.0_251\jre\lib\ext\access-bridge-64.jar;C:\Program Files\Java\jdk1.8.0_251\jre\lib\ext\cldrdata.jar;C:\Program Files\Java\jdk1.8.0_251\jre\lib\ext\dnsns.jar;C:\Program Files\Java\jdk1.8.0_251\jre\lib\ext\jaccess.jar;C:\Program Files\Java\jdk1.8.0_251\jre\lib\ext\jfxrt.jar;C:\Program Files\Java\jdk1.8.0_251\jre\lib\ext\localedata.jar;C:\Program Files\Java\jdk1.8.0_251\jre\lib\ext\nashorn.jar;C:\Program Files\Java\jdk1.8.0_251\jre\lib\ext\sunec.jar;C:\Program Files\Java\jdk1.8.0_251\jre\lib\ext\sunjce_provider.jar;C:\Program Files\Java\jdk1.8.0_251\jre\lib\ext\sunmscapi.jar;C:\Program Files\Java\jdk1.8.0_251\jre\lib\ext\sunpkcs11.jar;C:\Program Files\Java\jdk1.8.0_251\jre\lib\ext\zipfs.jar;C:\Program Files\Java\jdk1.8.0_251\jre\lib\javaws.jar;C:\Program Files\Java\jdk1.8.0_251\jre\lib\jce.jar;C:\Program Files\Java\jdk1.8.0_251\jre\lib\jfr.jar;C:\Program Files\Java\jdk1.8.0_251\jre\lib\jfxswt.jar;C:\Program Files\Java\jdk1.8.0_251\jre\lib\jsse.jar;C:\Program Files\Java\jdk1.8.0_251\jre\lib\management-agent.jar;C:\Program Files\Java\jdk1.8.0_251\jre\lib\plugin.jar;C:\Program Files\Java\jdk1.8.0_251\jre\lib\resources.jar;C:\Program Files\Java\jdk1.8.0_251\jre\lib\rt.jar;C:\Users\Administrator\Desktop\课件\课件\02_oop\10\Day10_all\test\target\classes;C:\Users\Administrator\.m2\repository\org\springframework\boot\spring-boot-starter\2.4.1\spring-boot-starter-2.4.1.jar;C:\Users\Administrator\.m2\repository\org\springframework\boot\spring-boot\2.4.1\spring-boot-2.4.1.jar;C:\Users\Administrator\.m2\repository\org\springframework\spring-context\5.3.2\spring-context-5.3.2.jar;C:\Users\Administrator\.m2\repository\org\springframework\spring-aop\5.3.2\spring-aop-5.3.2.jar;C:\Users\Administrator\.m2\repository\org\springframework\spring-beans\5.3.2\spring-beans-5.3.2.jar;C:\Users\Administrator\.m2\repository\org\springframework\spring-expression\5.3.2\spring-expression-5.3.2.jar;C:\Users\Administrator\.m2\repository\org\springframework\boot\spring-boot-autoconfigure\2.4.1\spring-boot-autoconfigure-2.4.1.jar;C:\Users\Administrator\.m2\repository\org\springframework\boot\spring-boot-starter-logging\2.4.1\spring-boot-starter-logging-2.4.1.jar;C:\Users\Administrator\.m2\repository\ch\qos\logback\logback-classic\1.2.3\logback-classic-1.2.3.jar;C:\Users\Administrator\.m2\repository\ch\qos\logback\logback-core\1.2.3\logback-core-1.2.3.jar;C:\Users\Administrator\.m2\repository\org\apache\logging\log4j\log4j-to-slf4j\2.13.3\log4j-to-slf4j-2.13.3.jar;C:\Users\Administrator\.m2\repository\org\apache\logging\log4j\log4j-api\2.13.3\log4j-api-2.13.3.jar;C:\Users\Administrator\.m2\repository\org\slf4j\jul-to-slf4j\1.7.30\jul-to-slf4j-1.7.30.jar;C:\Users\Administrator\.m2\repository\jakarta\annotation\jakarta.annotation-api\1.3.5\jakarta.annotation-api-1.3.5.jar;C:\Users\Administrator\.m2\repository\org\springframework\spring-core\5.3.2\spring-core-5.3.2.jar;C:\Users\Administrator\.m2\repository\org\springframework\spring-jcl\5.3.2\spring-jcl-5.3.2.jar;C:\Users\Administrator\.m2\repository\org\yaml\snakeyaml\1.27\snakeyaml-1.27.jar;C:\Users\Administrator\.m2\repository\org\slf4j\slf4j-api\1.7.30\slf4j-api-1.7.30.jar" cn.tedu.test.TestThread
几核cpu8
30
28
29
27
26
23
25
24
22
22
22
22
21
21
20
20
19
19
19
19
18
17
18
16
15
14
13
15
12
11
10
9
8
7
6
6
5
5
4
4
3
2
1

Process finished with exit code 0

从结果来看发生了重复,出现这种情况显然表明我们这个方法根本就不是线程安全的,出现这种问题的原因有很多,我们说最常见的一种,就是我们A线程在进入方法后,拿到了trcket的值,刚把这个值读取出来还没有改变trcket的值的时候,结果线程B也进来的,那么导致线程A和线程B拿到的trcket值是一样的。

在并发编程中,这种由于不恰当的执行时序而出现不正确的结果是一种非常重要的情况,它有一个正式的名字,叫“竞态条件”。竞态条件是啥?当某个计算的正确性取决于多个线程的交替执行时序时,那么就会发生竞态条件,最常见的竞态条件类型就是“先检查后执行”操作,即通过一个可能失效的观察结果来决定下一步的动作。比如首先观察到某个条件为真(例如文件X不存在),然后根据这个观察结果采用相应的动作(创建文件X),但事实上,在你观察到这个结果以及开始创建文件之间,观察结果可能变得无效(另一个线程在这个期间创建了文件X),从而导致各种问题(未预期的异常、数据覆盖、文件被破坏等)。

与大多数并发错误一样,竞态条件并不总是会产生错误,还需要某种不恰当的执行时序。原子操作是对于访问同一个状态的所有操作(包括该操作本身)来说,这个操作是一个以原子方式执行的操作。而我们前面提到的类似trcket--的这种操作,叫做复合操作,即包含了一组必须以原子方式执行的操作以确保线程安全性。在实际情况中,应该尽可能的使用现有的线程安全对象来管理类的状态,与非线程安全的对象相比,判断线程安全对象的可能状态及其状态转换情况要更为容易,从而也更容易维护和验证线程安全性。这里值得一提的是,java中给我们弄好了很多原子操作的类型,在这个包下java.util.concurrent.atomic。

我们要保证线程的安全性

加锁时我们要保证操作的原子性(其实原子性就是一个不可分割的)

原子性我们举一个例子:

A想要从自己的帐户中转1000块钱到B的帐户里。那个从A开始转帐,到转帐结束的这一个过程,称之为一个事务。在这个事务里,要做如下操作:

 1. 从A的帐户中减去1000块钱。如果A的帐户原来有3000块钱,现在就变成2000块钱了。
 2. 在B的帐户里加1000块钱。如果B的帐户如果原来有2000块钱,现在则变成3000块钱了。
如果在A的帐户已经减去了1000块钱的时候,忽然发生了意外,比如停电什么的,导致转帐事务意外终止了,而此时B的帐户里还没有增加1000块钱。那么,我们称这个操作失败了,要进行回滚。回滚就是回到事务开始之前的状态,也就是回到A的帐户还没减1000块的状态,B的帐户的原来的状态。此时A的帐户仍然有3000块,B的帐户仍然有2000块。

我们把这种要么一起成功(A帐户成功减少1000,同时B帐户成功增加1000),要么一起失败(A帐户回到原来状态,B帐户也回到原来状态)的操作叫原子性操作。

如果把一个事务可看作是一个程序,它要么完整的被执行,要么完全不执行。这种特性就叫原子性。

修改的代码:

 private  void doTicket() {//在这里加锁性能低,单个线程使用,不符合原子性 private synchronized void doTicket() {
        while (true){
            //多线程在此代码上排队执行(同步)
            synchronized (this) {//保证操作的原子性,这里this指的是对象
                if (ticket <= 0) break;
                //获取线程的名字
                String tName = Thread.currentThread().getName();
                System.out.println(tName + ":" + ticket--);
                //让线程阻塞一下
                sleep();
            }//排他锁(同步锁)
        }
    }

这里我们可以理解为:加在上面锁的是商场,加在下面锁的是试衣间.我们在保证安全的情况下也要保证性能.

结果为: 

"C:\Program Files\Java\jdk1.8.0_251\bin\java.exe" "-javaagent:E:\Program Files\JetBrains\IntelliJ IDEA 2020.1.4\IntelliJ IDEA 2020.1\lib\idea_rt.jar=51922:E:\Program Files\JetBrains\IntelliJ IDEA 2020.1.4\IntelliJ IDEA 2020.1\bin" -Dfile.encoding=UTF-8 -classpath "C:\Program Files\Java\jdk1.8.0_251\jre\lib\charsets.jar;C:\Program Files\Java\jdk1.8.0_251\jre\lib\deploy.jar;C:\Program Files\Java\jdk1.8.0_251\jre\lib\ext\access-bridge-64.jar;C:\Program Files\Java\jdk1.8.0_251\jre\lib\ext\cldrdata.jar;C:\Program Files\Java\jdk1.8.0_251\jre\lib\ext\dnsns.jar;C:\Program Files\Java\jdk1.8.0_251\jre\lib\ext\jaccess.jar;C:\Program Files\Java\jdk1.8.0_251\jre\lib\ext\jfxrt.jar;C:\Program Files\Java\jdk1.8.0_251\jre\lib\ext\localedata.jar;C:\Program Files\Java\jdk1.8.0_251\jre\lib\ext\nashorn.jar;C:\Program Files\Java\jdk1.8.0_251\jre\lib\ext\sunec.jar;C:\Program Files\Java\jdk1.8.0_251\jre\lib\ext\sunjce_provider.jar;C:\Program Files\Java\jdk1.8.0_251\jre\lib\ext\sunmscapi.jar;C:\Program Files\Java\jdk1.8.0_251\jre\lib\ext\sunpkcs11.jar;C:\Program Files\Java\jdk1.8.0_251\jre\lib\ext\zipfs.jar;C:\Program Files\Java\jdk1.8.0_251\jre\lib\javaws.jar;C:\Program Files\Java\jdk1.8.0_251\jre\lib\jce.jar;C:\Program Files\Java\jdk1.8.0_251\jre\lib\jfr.jar;C:\Program Files\Java\jdk1.8.0_251\jre\lib\jfxswt.jar;C:\Program Files\Java\jdk1.8.0_251\jre\lib\jsse.jar;C:\Program Files\Java\jdk1.8.0_251\jre\lib\management-agent.jar;C:\Program Files\Java\jdk1.8.0_251\jre\lib\plugin.jar;C:\Program Files\Java\jdk1.8.0_251\jre\lib\resources.jar;C:\Program Files\Java\jdk1.8.0_251\jre\lib\rt.jar;C:\Users\Administrator\Desktop\课件\课件\02_oop\10\Day10_all\test\target\classes;C:\Users\Administrator\.m2\repository\org\springframework\boot\spring-boot-starter\2.4.1\spring-boot-starter-2.4.1.jar;C:\Users\Administrator\.m2\repository\org\springframework\boot\spring-boot\2.4.1\spring-boot-2.4.1.jar;C:\Users\Administrator\.m2\repository\org\springframework\spring-context\5.3.2\spring-context-5.3.2.jar;C:\Users\Administrator\.m2\repository\org\springframework\spring-aop\5.3.2\spring-aop-5.3.2.jar;C:\Users\Administrator\.m2\repository\org\springframework\spring-beans\5.3.2\spring-beans-5.3.2.jar;C:\Users\Administrator\.m2\repository\org\springframework\spring-expression\5.3.2\spring-expression-5.3.2.jar;C:\Users\Administrator\.m2\repository\org\springframework\boot\spring-boot-autoconfigure\2.4.1\spring-boot-autoconfigure-2.4.1.jar;C:\Users\Administrator\.m2\repository\org\springframework\boot\spring-boot-starter-logging\2.4.1\spring-boot-starter-logging-2.4.1.jar;C:\Users\Administrator\.m2\repository\ch\qos\logback\logback-classic\1.2.3\logback-classic-1.2.3.jar;C:\Users\Administrator\.m2\repository\ch\qos\logback\logback-core\1.2.3\logback-core-1.2.3.jar;C:\Users\Administrator\.m2\repository\org\apache\logging\log4j\log4j-to-slf4j\2.13.3\log4j-to-slf4j-2.13.3.jar;C:\Users\Administrator\.m2\repository\org\apache\logging\log4j\log4j-api\2.13.3\log4j-api-2.13.3.jar;C:\Users\Administrator\.m2\repository\org\slf4j\jul-to-slf4j\1.7.30\jul-to-slf4j-1.7.30.jar;C:\Users\Administrator\.m2\repository\jakarta\annotation\jakarta.annotation-api\1.3.5\jakarta.annotation-api-1.3.5.jar;C:\Users\Administrator\.m2\repository\org\springframework\spring-core\5.3.2\spring-core-5.3.2.jar;C:\Users\Administrator\.m2\repository\org\springframework\spring-jcl\5.3.2\spring-jcl-5.3.2.jar;C:\Users\Administrator\.m2\repository\org\yaml\snakeyaml\1.27\snakeyaml-1.27.jar;C:\Users\Administrator\.m2\repository\org\slf4j\slf4j-api\1.7.30\slf4j-api-1.7.30.jar" cn.tedu.test.TestThread
几核cpu8
Thread-0:30
Thread-0:29
Thread-0:28
Thread-0:27
Thread-0:26
Thread-0:25
Thread-0:24
Thread-0:23
Thread-0:22
Thread-0:21
Thread-0:20
Thread-0:19
Thread-0:18
Thread-0:17
Thread-0:16
Thread-0:15
Thread-0:14
Thread-0:13
Thread-3:12
Thread-3:11
Thread-3:10
Thread-3:9
Thread-3:8
Thread-3:7
Thread-3:6
Thread-3:5
Thread-3:4
Thread-3:3
Thread-3:2
Thread-3:1

Process finished with exit code 0

这样我们就能看到   多核排队运行

 

我们再来设计一个实验:

模拟多个线程同时执行计数操作
 
package cn.tedu.test;

import java.util.ArrayList;
import java.util.List;

public class TestThread04 {

    static class Counter implements Runnable{
       //禁止指定重排序 volatile
        private volatile int count;

        @Override
        public void run() {
            for (int i=0;i<10;i++){
                doCount();
            }
        }

        public  void doCount() {
            count++;//此操作非原子操作 int temp=count;result=temp=1;count=result
        }
        public int getCount(){
            return  count;
        }

        public static void main(String[] args) {
            //1.构建计数任务对象
            Counter counter = new Counter();
            //2.构建多个线程
            List<Thread> list = new ArrayList<>();
            for (int i = 0;i < 100; i++) {
                list.add(new Thread(counter));
            }
            //3.启动线程
            for (Thread t : list) {
                t.start();
            }
            //4.所有线程执行结束,输出counter的值.
            //判定线程活着的数量是否大于1
            //如果你使用的是idea则包括一个监控线程这里要写2
            while (Thread.activeCount() > 1)
                Thread.yield();

                System.out.println(counter.getCount());

        }
    }
}

结果: 

1000

Process finished with exit code 0

但是他会出现这样的情况:

993

Process finished with exit code 0

解决方法是加 synchronized 保持它的原子操作

  public synchronized void doCount() {//this
            count++;//此操作非原子操作 int temp=count;result=temp=1;count=result
}

他是把ABC三个线程都执行结束,然后再执行其他操作,保证线程的安全

注:对于所有的实例方法,默认都使用this,非实例方法静态方法一般都是类名点class,比如 public synchronized void doCount() {方法是静态的就用类名点class
 
在执行count++的时候系统会有一个指令重排序的问题比如说
 
  public synchronized void doCount() {//this
            count++;//此操作非原子操作 ABC

系统可能先执行A,然后在执行C最后执行B  ,因为JVM底层他会对我们这个指令语句执行的过程进行优化

所以我们加上volatile,他让我们安装顺序执行。

//禁止指定重排序 volatile
        private volatile int count;

在使用了volatile保证了顺序和synchronized保证了原子性后我们在测试的时候就没有问题了,都是1000了

现在就是安全的了。

2.2. 导致线程不安全的因素有哪些?

1. 多个线程并发执行。
2. 多个线程并发执行时存在共享数据集 ( 临界资源 )
3. 多个线程在共享数据集上的操作不是原子操作 ( 不可拆分的一个操作 )
 
我们先写一个代码来理解:
import java.util.Arrays;

class Container {
    /*容器*/
    private Object[] array;
    /*有效元素个数*/
    private int size;

    Container() {
        this(12);

    }

    public Container(int cap) {
        array = new Object[cap];
    }
    /*
     * 像容器size放数据
     * */

    public synchronized void add(Object data) {
        //1判定容器是否已满.满了则扩容
        if (size == array.length)
            array = Arrays.copyOf(array, 2 * array.length);
            //2放数据
            array[size] = data;
            //3.修改size的值
            size++;
    }

}

public class Test06 {
    public static void main(String[] args) {
        Container c = new Container();
        for (int i = 0; i < Integer.MAX_VALUE; i++) {
            c.add(new byte[1024 * 1024 * 10]);
            System.out.println(i);
        }
    }
}

结果为:

1
2
...
...
173
174
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
	at cn.tedu.test.Test06.main(Test06.java:38)

Process finished with exit code 1

内存溢出了

我们可以给代码加上同步代码块

 synchronized (array) {//同步代码块
        ...
        ...
}

2.3. 如何保证并发线程的安全性?

1. 对共享进行限制 ( 阻塞 ) 访问(例如加锁: syncronized Lock : 多线程在同步 方法或同步代码块上排队执行
2. 基于 CAS( 比较和交换 ) 实现非阻塞同步(基于 CPU 硬件技术支持)
a) 内存地址 (V)
b) 期望数据值 (A)
c) 需要更新的值 (B)
CAS 算法支持无锁状态下的并发更新,但可能会出现 ABA 问题,长时间自 旋问题。
3. 取消共享,每个线程一个对象实例(例如 threadlocal
a) Connection 允许多线程共享吗? ( 不允许,每个线程一个 )
b) SimpleDateFormat 允许多线程共享吗?(不允许,每个线程一个)
c) SqlSession 对象允许共享吗 ?( 不允许,每个线程一个 )
说明: Java 中的线程安全问题的主要关注点有 3 个:可见性,有序性,原子性; Java 内存模型( JMM )解决了可见性和有序性问题,而锁解决了原子性问题。
package cn.tedu.test;

import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;


    class Counter01{
    private volatile int count;
    public synchronized void Count(){
        //synchronized在JDK1.6之前性能非常差会采用一些手动加锁
        //在1.6后synchronized支持自旋锁,轻量级锁,重量级锁等但是他不支持公平性
        //他都是非公平性的,假如使用公平锁就要使用下面的方法
        count++;
    }//排他锁,独占锁,非公平锁
}
class Counter02{
        private volatile int count;
        //公平锁,排他锁,独占锁
        private Lock lock=new ReentrantLock(true);
        public void Count(){
            //保证线程的安全在这里加一把锁
            //手动加锁能灵活
            lock.lock();//加锁
            try {
                count++;
            }finally {
                //必须在这里解锁,以避免死锁
                lock.unlock();//解锁
            }
        }
}
        //这种方法能保证线程的安全但是不管你怎样加锁都可能会有阻塞,使用在JDk里面又提供了一种方式
class Counter03{
        //底层使用CAS算法(基于CPU硬件实现)
        private AtomicInteger at=new AtomicInteger();
        public void count(){
             at.incrementAndGet();
        }
}
//上面是一种加锁的状态,但是我们要想让每个线程都有一份,这个就属于线程内部单例了
//单例是要确保一个对象在一定范围内他的实例只有一份,
//如果保证一个线程的内部这一个实例只有一份呢
class DateUtils{
        //因为这个SimpleDateFormat对象他线程不安全,解决办法就是不共享他,每一个线程都创建一个新的对象
        //怎么保证每个线程里面这个 SimpleDateFormat这个对象只有一份呢 Java里面提供了ThreadLocal这个对象
     private static ThreadLocal<SimpleDateFormat> td=new ThreadLocal<>();
     public static String format(Date date){
         //从当前线程获取SimpleDataFormat对象
         SimpleDateFormat sdf=td.get();
         if (sdf!=null)
             return sdf.format(date);
         System.out.println("创建SimpleDateFormat对象");
         //当前线程没有则创建SimpleDateFormat并且存储到当前线程
         sdf= new SimpleDateFormat("yyyy/MM/dd");
         td.set(sdf);
         /*我们使用ThreadLocal来保证创建的SimpleDateFormat只有一份
         *其实是ThreadLocal关联了一个线程,这个线程内部都会有一个map,执行td.set(sdf);
         * 的时候是吧sdf放到了map集合,key是ThreadLocal,值是SimpleDateFormat
         * key为td(ThreadLocal),取的时候也是从当前线程取
         * */
         return sdf.format(date);
     }
}
public class TestLock01 {
    public static void main(String[] args) {
        //每次执行都创建一个
        DateUtils.format(new Date());
        DateUtils.format(new Date());

        //每个线程一份,没有共享,就没有安全问题
        new Thread(){
            public void run(){
                DateUtils.format(new Date());
                DateUtils.format(new Date());
            };
        }.start();
    }
}

 

CAS算法  内存值,希望值,要更行的值
package cgb.java.thread;

import java.util.concurrent.atomic.AtomicInteger;
//JUC
public class TestThread07 {

	static class Counter{
		//CAS (基于硬件技术--CPU)
		private AtomicInteger at=new AtomicInteger();
		public int doCount(){
			return at.incrementAndGet();
		}
	}
	public static void main(String[] args) {
		Counter c1=new Counter();
	    for(int i=0;i<100;i++){
		System.out.println(c1.doCount());
	    }
	}
}

2.4. Synchronized 关键字应用及原理分析?

1. synchronized 简介:
1 synchronized 是排它锁的一种实现,支持可重入性。
 
但是他是不公平的,谁的优先级高,谁就更有机会抢到
2 )基于这种机制可以实现多线程在共享数据集上同步 ( 互斥和协作 )
2.1) 互斥:多线程在共享数据集上排队执行。
2.2) 协作:多线程在共享数据集上进行协作执行 .( 通讯 )
说明:
排他性:如果线程 T1 已经持有锁 L ,则不允许除 T1 外的任何线程 T 持有该锁 L
重入性:如果线程 T1 已经持有锁 L ,则允许线程 T1 多次获取锁 L ,更确切的说, 获取一次后,可多次进入锁。
public class Demo {
    //排他性
    //只能有一个线程进入方法内部执行
    static synchronized void doMethod01(){
        System.out.println("doMethod01()");
        //可重入性
        //这时候01和02使用的是同一把锁
        doMethod02();
    }
    static synchronized void doMethod02(){
        System.out.println("doMethod02()");
    }

    public static void main(String[] args) {
        doMethod01();
    }
}

结果:

doMethod01()
doMethod02()

Process finished with exit code 0
2 Synchronized. 应用分析:
1 ) 修饰方法:同步方法(锁为当前实例或 Class 对象)
1.1 ) 修饰静态方法:默认使用的锁为方法所在类的 Class 对象
1.2 ) 修饰实例方法:默认使用的所为方法所在类的实例对象
2 ) 修饰代码块:同步代码块(代码块括号内配置的对象)
package cn.tedu.test;

public class TestSynchronized01 {
    //排他性
    //只能有一个线程进入方法内部执行
    static synchronized void doMethod01(){//锁Demo.class
        System.out.println("doMethod01()");
        //可重入性
        //这时候01和02使用的是同一把锁
        doMethod02();
    }
    static synchronized void doMethod02(){
        System.out.println("doMethod02()");
    }
    public synchronized void doMethod03(){//this
        //这里doMethod02()跟doMethod03()不可以实现同步,他们使用的是不同的锁对象
        //doMethod02()跟doMethod01()可以
        //在共享数据集上进行一个互斥和协作,他们的前提条件是使用了同一把锁
        System.out.println(this);
        //默认使用的锁为this(当前访问这个方法对象)
    }

    public static void main(String[] args) {
        doMethod01();
    }
}
package cn.tedu.test;

public class TestSynchronized02 {
    private String LOCK="CHINA";
    public void doMethod01(){
        synchronized (this){//this指向当前方法对应的实例对象

        }//同步代码块
    }
    public void doMethod02(){
        synchronized (LOCK){//synchronized (TestSynchronized02.this)这里我们可以任意的指定对象
          //  doMethod01()和doMethod02()是不同的也不可以
        }
    }
}
3. Synchronized 原理分析:基于 Monitor 对象实现同步。(监视器对象)
Java中的所有的对象都默认继承了Object这个类,Object这个类里面都有一个监视器对象.
1 )同步代码块采用 monitorenter monitorexit 指令显式的实现。
                               监视器进入,监视器退出
2 )同步方法则使用 ACC_SYNCHRONIZED 标记符隐式的实现。
Monitor对象,Java中每一个对象有一个对象头在这个对象头里面存储着一个对象,这个对象叫Monitor 对象,他可以基于这个对象实现同步,对象锁也称为同步锁是因为我们使用Synchronized 这个锁修饰代码块和修饰方法的时候他拿了一个对象,这个对象锁里面会内置一个监视器,当你拿到这个监视器的时候其他的线程就拿不到了,其他的线程只能阻塞.
4. Synchronized 锁优化:底层
为了减少获得锁和释放锁带来的性能消耗, JDK1.6 以后的锁一共有 4 种状态, 级别从低到高依次是:无锁状态(性能最高)、偏向锁状态(不公平)、轻量级锁状态和重量级锁状态, 这几个状态会随着竞争情况逐渐升级。
说明:锁可以升级但不能降级,意味着偏向锁升级成轻量级锁后不能降级成偏向 锁。这种锁升级却不能降级的策略,目的是为了提高获得锁和释放锁的效率。
无锁状态一般指的是一种自旋,假如只有一个线程频繁的访问一个方法的话,系统底层就会把Synchronized这个加锁的这个特性给去掉,就避免了线程切换的性能损耗.
 

2.5. 如何理解 volatile 关键字的应用?

1. 定义: volatile 一般用于修饰属性变量
1) 保证共享变量的可见性 .( 尤其是多核或多 cpu 场景下 )
2) 禁止指令的重排序操作(例如: count++ 底层会有三个步骤)
3) 不保证原子性(例如不能保证一个线程执行完 count++ 所有指令其它线程才能 执行。)
2. 应用场景分析:
1) 状态标记( boolean 类型属性)
2) 安全发布 ( 线程安全单例中的对象安全发布 - 双重检测机制 )
3) 读写锁策略(一个写,并发读,类似读写锁)
状态标记代码示例 :
package cgb.java.thread;
class Looper{
//这里我们加一个volatile的话下面looper.stop();就可以停掉线程,不加的话虽然下面已经更改了值
//但是并不能停掉,这就是第一点可见性
	private volatile boolean isStop;
	public void loop() {
		for(;;) {
			if(isStop)break;
		}
	}
	public void stop() {
		isStop=true;
	}
}
public class TestVolatile01 {
	public static void main(String[] args) throws InterruptedException {
		//Thread main=Thread.currentThread();
		Looper looper=new Looper();
		Thread t=new Thread(){
			@Override
			public void run() {
				looper.loop();
			}
		};
		t.start();
		t.join(3000);		
		looper.stop();			
	}	
}
安全发布代码示例:
package cn.tedu.test;

public class pp {
}
class Singleton{
    private Singleton(){}
    //这里我们要加上volatile不加的话下面可能会发生顺序颠倒问题
    //分配空间,instance赋值初始化,属性初始化.(发生指令重排序)
    private static volatile Singleton instance;
    private static Singleton getSingleton(){//大对象,稀少用
        if (instance==null){
            synchronized (Singleton.class){
                instance=new Singleton();//分配空间,属性初始化,instance赋值初始化
            }
        }
    return instance;
    }
}
读写锁应用案例:
class Counter{
private volatile int count;
public int getCount() {//read
return count; }
public synchronized void doCount() {//write
count++;
} }

2.6. 如何理解 happen-before 原则应用?

 

JMM 中如果一个操作 A Happened-bofore 另一个操作 B ,那么 A 操作的结果 对 B 操作的结果是可见的 , 那么我们称这种方式为 happened-before 原则.
说明: JMM 中基于 happened-before 规则,判定数据是否存在竞争,线程是否安全,以及多线程环境下变量值是否是可见的
 

2.7. 如何理解 JAVA 中的悲观锁和乐观锁?

JAVA 中为了保证多线程并发访问的安全性,提供了基于锁的应用,大体可归纳为两大类,即悲观锁和乐观锁。
 
悲观锁&乐观锁定义说明:
 
1 ) 悲观锁:假定会发生并发冲突,屏蔽一切可违反数据完整性的操作 . 同一时刻只能有一个线程执行写操作。
存在性能问题.
例如 java 中可以基于 syncronized,Lock ReadWriteLock 等实现。
 
2) 乐观锁:假设不会发生冲突,只在提交操作时检查是否违反数据完整性 . 多个线程可以并发执行写操作但只能有一个线程写操作成功。
可以并发的更新.
例如 java 中可借助 CAS Compare And Swap )算法实现 ( 此算法依赖硬件CPU)。
 
悲观锁 & 乐观锁应用场景说明:
1) 悲观锁适合写操作比较多的场景,写可以保证写操作时数据正确。
2) 乐观锁适合读操作比较多的场景,不加锁的特点能够使其读操作的性能大幅提升
乐观锁虽然我们可以并发的去更新,但是因为底层需要不断的比较交换,可能会影响我们的性能.
 
悲观锁 & 乐观锁应用案例分析
 
悲观锁实现计数器:
 
方案 1
 
 
class Counter{
private int count;
public synchronized int count() {
count++;
return count; } }
方案 2
 
class Counter{
private int count;
private Lock lock=new ReentrantLock();//默认是false
public int count() {
lock.lock();
try {
count++;
 return count; }finally {
lock.unlock();
} } }
乐观锁实现计数器:
 
 
class Counter{
private AtomicInteger at=new AtomicInteger();//CAS
public int count() {
return at.incrementAndGet();
} }
其中 AtomicInteger 是基于 CAS 算法实现。
 
 

2.8. 如何理解线程的上下文切换?

 
 
一个线程得到 CPU 执行的时间是有限的。当此线程用完为其分配的 CPU 时 间以后,cpu 会切换到下一个线程执行。
 
在线程切换之前,线程需要将当前的状态进行保存,以便下次再次获得 CPU 时间片时可以加载对应的状态以继续执行剩下的任务。而这个切换过程是需要耗 费时间的,会影响多线程程序的执行效率,所以在在使用多线程时要减少线程的频繁切换。那如何实现呢?
 
减少多线程上下文切换的方案如下:
1) 无锁并发编程:锁的竞争会带来线程上下文的切换
2) CAS 算法: CAS 算法在数据更新方面,可以达到锁的效果
3) 使用最少线程:避免不必要的线程等待
CPU切换线程也是需要消耗时间的,主线程去启动线程池的核心线程数最好与这个CPU的核数或者CPU的个数相同,比如现在有一个服务器是32个CPU,一个CPU是8核,核心线层数就可以写32*8,这是一个可以并行执行的数量;除了核心线程数,还有最大线程数,与你的并发量相关,比如我们现在需要的请求为两万,但是我们最多并行量只有一万,我们就要设计一个队列,没有拿到CPU的那些线程就放入队列等待.
4) 使用协程:单线程完成多任务的调度和切换,避免多线程
BIO,NIO,AIO
在Netty这个框架里面就使用了协程.

2.9.如何理解死锁以及避免死锁问题?

多个线程互相等待已经被对方线程正在占用的锁,导致陷入彼此等待对方释放锁的状态,这个过程称之为死锁.
 
死锁案例分析-1
 
package cn.tedu.test;


    class SyncTask01 implements Runnable {
        private Object obj1;
        private Object obj2;
        public SyncTask01(Object o1, Object o2) {
            this.obj1 = o1;
            this.obj2 = o2;
        }
        @Override
        public void run() {
            synchronized (obj1) {
                work();
                synchronized (obj2) {
                    work();
                }
            }
        }
        private void work() {
            try {Thread.sleep(30000);} catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

public class TestDeadLock01 {
    public static void main(String[] args) throws Exception {
        Object obj1 = new Object();
        Object obj2 = new Object();
        Thread t1 = new Thread(new SyncTask01(obj1, obj2), "t1");
        Thread t2 = new Thread(new SyncTask01(obj2, obj1), "t2");
        t1.start();
        t2.start();
    }
}
死锁案例分析 -2
 
package cn.tedu.test;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.concurrent.TimeUnit;
    class SyncTask02 implements Runnable{
        private List<Integer> from;
        private List<Integer> to;
        private Integer target;
        public SyncTask02(List<Integer> from,List<Integer> to,Integer target) {
            this.from=from;
            this.to=to;
            this.target=target; }
        @Override
        public void run() {
            moveListItem(from, to, target);
        }
        private static void moveListItem (List<Integer> from,
                                          List<Integer> to, Integer item) {
            log("attempting lock for list", from);
            synchronized (from) {
                log("lock acquired for list", from);
                try {
                    TimeUnit.SECONDS.sleep(1);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                log("attempting lock for list ", to);
                synchronized (to) {
                    log("lock acquired for list", to);
                    if(from.remove(item)){
                        to.add(item);
                    }
                    log("moved item to list ", to);
                }
            }
        }
        private static void log (String msg, Object target) {
            System.out.println(Thread.currentThread().getName() + ": " + msg + " " +
                    System.identityHashCode(target));
        } }
    public class TestDeadLock02 {
        public static void main(String[] args) {
            List<Integer> list1 = new ArrayList<>(Arrays.asList(2, 4, 6, 8, 10));
            List<Integer> list2 = new ArrayList<>(Arrays.asList(1, 3, 7, 9, 11));

            Thread thread1 = new Thread(new SyncTask02(list1, list2, 2));
            Thread thread2 = new Thread(new SyncTask02(list2, list1, 9));
            thread1.start();
            thread2.start();
        }
    }
如何避免死锁呢?
1) 避免一个线程中同时获取多个锁
2) 避免一个线程在一个锁中获取其他的锁资源
3) 考虑使用定时锁来替换内部锁机制,如 lock.tryLock(timeout)
拿不到线程就把锁放弃

3. 线程通讯与进程通讯应用基础

3.1. 如何理解进程与线程通讯?

线程通讯 java 中的多线程通讯主要是共享内存(变量)等方式。
进程通讯 java 中进程通讯( IPC )主要是 Socket MQ 等。
 
通常为了提高性能,通常在浏览器使用ajax技术,实现异步加载,局部刷新,来改善用户体验!
这个时候一般是浏览器的主线程去创建一个工作线程,由工作线程去执行耗时操作,把操作的结果拿过来反馈给主线程.有主线程进行ui的一些操作
包括手机中包括好多APP一般是主线程负责呈现数据,主线程负责接受事件,然后把这个事件的处理交给工作线程,工作线程去执行耗时操作,这个时候主线程与工作线程就进行了交互.除了主线程与工作线程的通信,我们还有工作线程与工作线程之间的通信,这种通信我们称为进程内的通信.
再比如线程数,连接池就进行了通信
 

线程池

阻塞队列

3.2. 如何实现进程内部线程之间的通讯?

 
3.2.1. 基于 wait/nofity/notifyall 实现
 
1. wait()/notify()/notifyall ()方法定义说明:
1) Wait: 阻塞正在使用监视器对象的线程,同时释放监视器对象
2) notify: 唤醒在监视器对象上等待的单个线程,但不释放监视器对象,此时调
用该方法的代码继续执行,直到执行结束才释放对象锁
3) notifyAll: 唤醒在监视器对象上等待的所有线程,但不释放监视器对象,此
时调用该方法的代码继续执行,直到执行结束才释放对象锁
2. wait()/notify()/notifyall ()方法应用说明
1 ) 这些方法必须应用在同步代码块或同步方法中
 
2 ) 这些方法必须由监视器对象(对象锁)调用
说明:使用 wait/notify/notifyAll 的作用一般是为了避免轮询带来的性能损失。
3. wait()/notify()/notifyall() 应用案例实现:
手动实现阻塞式队列,并基于 wait()/notifyAll() 方法实现实现线程在队列上的通讯
 
现有一生产者消费者模型,生产者和消费者并发操作容器对象。
代码实现:实现一线程安全的容器类 ( 基于数组实现一个阻塞式队列 )
 

JAVA 核心基础  线程并发原理(知识点,讲解,练习,代码)
        31/100
        Theshy08



/**
 * 有界消息队列:用于存取消息
 * 1)数据结构:数组(线性结构)
 * 2)具体算法:FIFO(先进先出)-First in First out
 */
public class BlockContainer<T> {//类泛型
    /**用于存储数据的数组*/
    private Object[] array;
    /**记录有效元素个数*/
    private int size;
    public BlockContainer () {
        this(16);//this(参数列表)表示调用本类指定参数的构造函数
    }
    public BlockContainer (int cap) {
        array=new Object[cap];//每个元素默认值为null
    } }
 
向容器添加 put 方法,用于放数据。
 
/**
 * 生产者线程通过put方法向容器放数据
 * 数据永远放在size位置
 * 说明:实例方法内部的this永远指向
 * 调用此方法的当前对象(当前实例)
 * 注意:静态方法中没有this,this只能
 * 应用在实例方法,构造方法,实例代码块中
 */
public synchronized void put(T t){//同步锁:this
        //1.判定容器是否已满,满了则等待
        while(size==array.length)
        try{this.wait();}catch(Exception e){}
        //2.放数据
        array[size]=t;
        //3.有效元素个数加1
        size++;
        //4.通知消费者取数据
        this.notifyAll();
        }
向容器类添加 take 方法,用于从容器取数据。
 

JAVA 核心基础  线程并发原理(知识点,讲解,练习,代码)
        31/100
        Theshy08



/**
 * 消费者通过此方法取数据
 * 位置:永远取下标为0的位置的数据
 * @return
 */
@SuppressWarnings("unchecked")
public synchronized T take(){
        //1.判定容器是否为空,空则等待
        while(size==0)
        try{this.wait();}catch(Exception e){}
        //2.取数据
        Object obj=array[0];
        //3.移动元素
        System.arraycopy(
        array,//src 原数组
        1, //srcPos 从哪个位置开始拷贝
        array, //dest 放到哪个数组
        0, //destPost 从哪个位置开始放
        size-1);//拷贝几个
        //4.有效元素个数减1
        size--;
        //5.将size位置为null
        array[size]=null;
        //6.通知生产者放数据
        this.notifyAll();//通知具备相同锁对象正在wait线程
        return (T)obj;
        }
 
3.2.2. 基于 Condition 实现
 
 
1. Condition 类定义说明
Condition 是一个用于多线程间协同的工具类,基于此类可以方便的对持有锁的 线程进行阻塞或唤醒阻塞的线程。它的强大之处在于它可以为多个线程间建立不 同的 Condition ,通过 signal()/signalall() 方法指定要唤醒的不同线程。
2. Condition 类应用说明
1) 基于 Lock 对象获取 Condition 对象
2) 基于 Condition 对象的 await()/signal()/signalall() 方法实现线程阻塞或唤醒。
3. Condition 类对象的应用案例实现:
手动实现阻塞式队列,并基于 wait()/notifyAll() 方法实现实现线程在队列上的通讯。
 
 
/**
 * 有界消息队列:用于存取消息
 * 1)数据结构:数组(线性结构)
 * 2)具体算法:FIFO(先进先出)-First in First out
 */
public class BlockContainer<T> {//类泛型
    /**用于存储数据的数组*/
    private Object[] array;
    /**记录有效元素个数*/
    private int size;
    public BlockContainer() {
        this(16);//this(参数列表)表示调用本类指定参数的构造函数
    }
    public BlockContainer(int cap) {
        array=new Object[cap];//每个元素默认值为null
    }
    //JDK1.5以后引入的可重入锁(相对于synchronized灵活性更好)
    private ReentrantLock lock=new ReentrantLock(true);// true表示使用公平锁,默认是非公平锁
    private Condition producerCondition=lock.newCondition();//通讯条件
    private Condition consumerCondition=lock.newCondition();//通讯条件
}
向容器中添加 put 方法,用于向容器放数据
 
/**
 * 生产者线程通过put方法向容器放数据
 * 数据永远放在size位置
 * 说明:实例方法内部的this永远指向
 * 调用此方法的当前对象(当前实例)
 * 注意:静态方法中没有this,this只能
 * 应用在实例方法,构造方法,实例代码块中
 */
public void put(T t){//同步锁:this
        System.out.println("put");
        lock.lock();
        try{
        //1.判定容器是否已满,满了则等待
        while(size==array.length)
        //等效于Object类中的wait方法
        try{producerCondition.await();}catch(Exception e){e.printStackTrace();}
        //2.放数据
        array[size]=t;
        //3.有效元素个数加1
        size++;
        //4.通知消费者取数据
        consumerCondition.signalAll();//等效于object类中的notifyall()
        }finally{
        lock.unlock();
        }}
在容器类中添加 take 方法用于从容器取数据
 
/**
 * 消费者通过此方法取数据
 * 位置:永远取下标为0的位置的数据
 * @return
 */
@SuppressWarnings("unchecked")
public T take(){
        System.out.println("take");
        lock.lock();
        try{
        //1.判定容器是否为空,空则等待
        while(size==0)
        try{consumerCondition.await();}catch(Exception e){}
        //2.取数据
        Object obj=array[0];
        //3.移动元素
        System.arraycopy(
        array,//src 原数组
        1, //srcPos 从哪个位置开始拷贝
        array, //dest 放到哪个数组
        0, //destPost 从哪个位置开始放
        size-1);//拷贝几个
        //4.有效元素个数减1
        size--;
        //5.将size位置为null
        array[size]=null;
        //6.通知生产者放数据
        producerCondition.signalAll();//通知具备相同锁对象正在wait线程
        return (T)obj;
        }finally{
        lock.unlock();
        }
        }

3.3. 如何实现进程之间间通讯(IPC)?

3.3.1. 基于 socket 实现进程间通讯?
基于 BIO 实现的简易 server 服务器
 

public class BioMainServer01 {
    private Logger log=LoggerFactory.getLogger(BioMainServer01.class);
    private ServerSocket server;
    private volatile boolean isStop=false;
    private int port;
    public BioMainServer01(int port) {
        this.port=port; }
    public void doStart()throws Exception {
        server=new ServerSocket(port);
        while(!isStop) {
            Socket socket=server.accept();
            log.info("client connect");
            doService(socket);
        }
        server.close();
    }
    public void doService(Socket socket) throws Exception{
        InputStream in=socket.getInputStream();
        byte[] buf=new byte[1024];
        int len=-1;
        while((len=in.read(buf))!=-1) {
            String content=new String(buf,0,len);
            log.info("client say {}", content);
        }
        in.close();
        socket.close();
    }
    public void doStop() {
        isStop=false; }
    public static void main(String[] args)throws Exception {
        BioMainServer01 server=new BioMainServer01(9999);
        server.doStart();
    } }
启动服务,然后打开浏览器进行访问或者通过如下客户端端访问
 


public class BioMainClient {
    public static void main(String[] args) throws Exception{
        Socket socket=new Socket();
        socket.connect(new InetSocketAddress("127.0.0.1", 9999));
        OutputStream out=socket.getOutputStream();
        Scanner sc=new Scanner(System.in);
        System.out.println("client input:");
        out.write(sc.nextLine().getBytes());
        out.close();
        sc.close();
        socket.close();
    } }

 

 
 
 
 
 
  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值