JUC编程学习笔记

JUC编程学习笔记

什么是JUC

在这里插入图片描述
Runnable:没有返回值,效率相比Callable较低

线程和进程

进程是CPU资源分配的最小单位,线程是CPU调度的最小单位

进程是程序的一次执行过程,线程是进程的一条执行路径

java默认有2个线程:main和GC

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

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

java能开启线程吗

不能,只能通过本地native方法调用底层的C++开启线程,java无法直接操作硬件

public synchronized void start() {
        /**
         * This method is not invoked for the main method thread or "system"
         * group threads created/set up by the VM. Any new functionality added
         * to this method in the future may have to also be added to the VM.
         *
         * A zero status value corresponds to state "NEW".
         */
        if (threadStatus != 0)
            throw new IllegalThreadStateException();

        /* Notify the group that this thread is about to be started
         * so that it can be added to the group's list of threads
         * and the group's unstarted count can be decremented. */
        group.add(this);

        boolean started = false;
        try {
            start0();
            started = true;
        } finally {
            try {
                if (!started) {
                    group.threadStartFailed(this);
                }
            } catch (Throwable ignore) {
                /* do nothing. If start0 threw a Throwable then
                  it will be passed up the call stack */
            }
        }
    }
    //native本地方法,调用底层C++开启线程
    private native void start0();

并发和并行

  • 并发:多线程操作同一资源,本质是充分利用CPU资源
    CPU一核可以模拟出多条线程,快速交替

  • 并行:多核CPU多个线程同时执行–线程池

//Runtime该实例使该应用程序可以与该应用程序在其中运行的环境进行交互
//availableProcessors可用的虚拟机的最大数量的处理器;永远不会小于一个
System.out.println(Runtime.getRuntime().availableProcessors());

线程的状态

public enum State {
        //新生(线程还没被执行时的状态)
        NEW,

        //运行(正在jvm虚拟机里被执行,但它可能正在等待来自操作系统(例如处理器)的其他资源)
        RUNNABLE,

        //阻塞(处于阻塞状态的线程在等待锁去进入或重新进入一个synchronized块或方法)
        BLOCKED,

        //等待(处于等待状态的线程正在等待另一个线程执行特定操作。 例如,一个在对象上调用Object.wait()的线程正在等待另一个线程在该对象上调用Object.notify()或Object.notifyAll() 。 名为Thread.join()的线程正在等待指定的线程终止)
        WAITING,

        //超时等待(具有指定等待时间的等待线程的线程状态)
        TIMED_WAITING,

        //终止(线程已完成执行)
        TERMINATED;
    }

在这里插入图片描述
转自:Java线程的状态

wait,join进入WAITING状态等待唤醒,synchronized和wait被notify后进入BLOCKED状态主动尝试获取锁,sleep,wait(时间),join(时间)进入TIMED_WAITING等待时间唤醒线程
BLOCKED和WAITING的本质区别:处于BLOCKED状态的线程会主动尝试获取锁,而WAITING状态的线程只能被动等待唤醒,被唤醒过后才有资格去尝试获取锁(进入BLOCKED状态)。

wait和sleep

  1. 来自不同的类
    wait来自Object类
    sleep来自Thread类

  2. 锁是否释放
    wait会释放锁,sleep不会释放锁

  3. 使用范围不同
    wait必须在synchronized方法或块中
    sleep可以在任何地方使用

Lock锁

在这里插入图片描述
可重入就是说某个线程已经获得某个锁,可以再次获取锁而不会出现死锁。synchronized和reentrantLock都是可重入锁。
ReentrantLock 需要手动释放锁,所以使用 ReentrantLock的时候一定要手动释放锁,并且加锁次数和释放次数要一样。
在这里插入图片描述
公平锁:必须先来后到
非公平锁:可以插队(默认)
优缺点:
非公平锁性能高于公平锁性能。首先,在恢复一个被挂起的线程与该线程真正运行之间存在着严重的延迟。而且,非公平锁能更充分的利用cpu的时间片,尽量的减少cpu空闲的状态时间。
在这里插入图片描述

TestJUC01(synchronized锁)

//线程是一个单独的资源类,没有其他附属操作
public class TestJUC01 {
    public static void main(String[] args) {
        Ticket ticket = new Ticket();
        new Thread(()->{
            for (int i = 0; i < 30; i++) {
                ticket.sale();
            }
        },"AA").start();

        new Thread(()->{
            for (int i = 0; i < 30; i++) {
                ticket.sale();
            }
        },"BB").start();

        new Thread(()->{
            for (int i = 0; i < 30; i++) {
                ticket.sale();
            }
        },"CC").start();
    }

}
//资源类 不继承Runnable接口,在开启线程的时候在Runnable的run方法里调用,进行解耦
class Ticket{
    public int number=100;
    public synchronized void sale(){
        if(number>0){
            System.out.println(Thread.currentThread().getName()+"卖出了第"+number--+"张票");
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

TestJUC02(Lock锁)

public class TestJUC02 {
    public static void main(String[] args) {
        Ticket2 ticket = new Ticket2();
        new Thread(()->{
            for (int i = 0; i < 30; i++) {
                ticket.sale();
            }
        },"AA").start();

        new Thread(()->{
            for (int i = 0; i < 30; i++) {
                ticket.sale();
            }
        },"BB").start();

        new Thread(()->{
            for (int i = 0; i < 30; i++) {
                ticket.sale();
            }
        },"CC").start();
    }

}

class Ticket2 {
    public int number = 100;
    Lock lock=new ReentrantLock();
    public void sale() {
        lock.lock();
        try {
            if (number > 0) {
                System.out.println(Thread.currentThread().getName() + "卖出了第" + number-- + "张票");
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        } finally {
            lock.unlock();
        }
    }
}

synchronized和Lock的区别

  1. synchronized是java内置的关键字;Lock是一个java类
  2. synchronized无法判断锁的状态;Lock可以判断是否获取了锁
  3. synchronized会自动释放锁;Lock必须手动释放锁,如果不释放锁则导致死锁
  4. synchronized线程1阻塞,线程2只能等待;Lock则不一定等待
  5. synchronized可重入锁,不可被中断,非公平锁;Lock可重入锁,可以判断锁,非公平锁(可设置)
  6. synchronized适合锁少量的代码同步;Lock适合锁大量的代码同步
  7. synchronized有代码块锁和方法锁;Lock只有代码块锁
  8. 使用Lock锁,JVM将花费较少的时间来调度线程,性能更好。并且具有更好的扩展性(提供更多的子类)

TestJUC03(生产者消费者问题synchronized版)

//生产者消费者模型-->利用缓冲区解决:管程法
//生产者,消费者,产品,缓冲区
public class TestJUC03 {
    public static void main(String[] args) {
        SynContainer synContainer = new SynContainer();
        new Thread(()->{
            for (int i = 0; i < 200; i++) {
                synContainer.push(new Chicken(i));
            }
        }, "a1").start();

        new Thread(()->{
            for (int i = 0; i < 200; i++) {
                synContainer.push(new Chicken(i));
            }
        }, "a2").start();

        new Thread(()->{
            for (int i = 0; i < 200; i++) {
                synContainer.pop();
            }
        }, "b1").start();

        new Thread(()->{
            for (int i = 0; i < 200; i++) {
                synContainer.pop();
            }
        },"b2").start();
    }
}

class Chicken {
    int id;

    public Chicken(int id) {
        this.id = id;
    }
}

class SynContainer {
    //容器大小为10,最多可以放10个chicken对象
    Chicken[] chickens = new Chicken[10];
    //当前容器计数器
    int count = 0;
    int sum=0;
    //需要synchronized对对象锁上后才能调用wait/notify方法进行阻塞释放锁或者唤醒解除阻塞
    public synchronized void push(Chicken chicken) {
        //用while不要用if,因为当多个线程同时进入等待的时候,notifyAll唤醒对象上所有wait方法的线程,
        // 而如果用的if方法,进入的判断后wait被唤醒后就不会再一次进行判断,有可能会造成不满足条件的线程
        // 没有进行判断wait直接执行方法,而while会在线程被唤醒后再次判断,如果被其他线程方法抢先执行了,
        // 则其他被唤醒的线程会继续被判断wait等待
        while (count == chickens.length) {
            //等待消费者消费
            System.out.println(Thread.currentThread().getName()+"产品满了");
            try {
                this.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        chickens[count] = chicken;
        count++;
        sum++;
        System.out.println(Thread.currentThread().getName() + "生产了" + count+"目前总量为"+sum);
        this.notifyAll();
    }

    public synchronized Chicken pop() {
        while(count<=0){
            //等待生产者生产
            try {
                //表示线程一直等待直到通知,与sleep的区别是会释放锁
                this.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        Chicken chicken = chickens[count - 1];
        count--;
        System.out.println(Thread.currentThread().getName() + "消费了" + count);
        //唤醒同一个对象上所有调用wait方法的线程
        this.notifyAll();
        //随机唤醒一个等待状态的线程
        //this.notify();
        return chicken;
    }
}

Condition类

在这里插入图片描述
在这里插入图片描述
在 Lock取代 synchronized方法和语句的使用,一个 Condition取代Object的使用方法。
在等待condition之前,该锁必须由当前线程持有。

TestJUC04(生产者消费者问题Lock锁)

public class TestJUC04 {
    public static void main(String[] args) {
        SynContainer2 synContainer = new SynContainer2();
        new Thread(()->{
            for (int i = 0; i < 200; i++) {
                synContainer.push(new Chicken2(i));
            }
        }, "a1").start();

        new Thread(()->{
            for (int i = 0; i < 200; i++) {
                synContainer.push(new Chicken2(i));
            }
        }, "a2").start();

        new Thread(()->{
            for (int i = 0; i < 200; i++) {
                synContainer.pop();
            }
        }, "b1").start();

        new Thread(()->{
            for (int i = 0; i < 200; i++) {
                synContainer.pop();
            }
        },"b2").start();
    }
}

class Chicken2 {
    int id;

    public Chicken2(int id) {
        this.id = id;
    }
}

class SynContainer2 {
    //容器大小为10,最多可以放10个chicken对象
    Chicken2[] chickens = new Chicken2[10];
    //当前容器计数器
    int count = 0;
    int sum=0;
    //创建Lock锁对象和该锁对应的condition对象
    Lock lock=new ReentrantLock();
    Condition condition = lock.newCondition();
    public void push(Chicken2 chicken) {
        //加锁
        lock.lock();
        try {
            while (count == chickens.length) {
                //等待消费者消费
                System.out.println(Thread.currentThread().getName()+"产品满了");
                condition.await();
            }
            chickens[count] = chicken;
            count++;
            sum++;
            System.out.println(Thread.currentThread().getName() + "生产了" + count+"目前总量为"+sum);
            //唤醒其余等待的线程
            condition.signalAll();
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            //解锁
            lock.unlock();
        }
    }

    public Chicken2 pop() {
        lock.lock();
        Chicken2 chicken;
        try {
            while(count<=0){
                //等待生产者生产
                try {
                    condition.await();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            chicken = chickens[count - 1];
            count--;
            System.out.println(Thread.currentThread().getName() + "消费了" + count);
            condition.signalAll();
        } finally {
            lock.unlock();
        }
        return chicken;
    }
}

TestJUC05(Condition监视器实现精准唤醒,且不需要标识符)

参考了很多网上的资料和视频,发现Condition监视器的精准唤醒例子都需要使用标识符,但实际上使用标识符的话是可以不用到Condition监视器的直接用Object监视器(wait/notify)来执行的。

个人感觉这些例子举得都不好,所以自己写了不用标识符的情况下可以直接用Condition来实现精准唤醒的例子,也从中发现了一些问题,具体已经写在注释。

public class TestJUC05 {
    public static void main(String[] args) throws InterruptedException {
        Data data = new Data();

        new Thread(()->{
            for (int i = 0; i < 10; i++) {
                data.B();
            }
        },"BB").start();
        new Thread(()->{
            for (int i = 0; i < 10; i++) {
                data.C();
            }
        },"CC").start();
        //让A线程最后最后执行才对下一个线程的condition监视器唤醒起效,才能进入一个循环,
        //否则一开始A线程执行得太快,有可能会导致当B线程的监视器还没等待的时候已经执行唤醒,
        //然后之后就全部线程都无法唤醒,进入死循环
        Thread.sleep(1000);
        new Thread(()->{
            for (int i = 0; i < 10; i++) {
                data.A();
            }
        },"AA").start();
    }
}

class Data{
    private Lock lock=new ReentrantLock();
    Condition condition1=lock.newCondition();
    Condition condition2=lock.newCondition();
    Condition condition3=lock.newCondition();
    //该标识符用于标识A线程是否第一次执行
    int number=0;
    public void A(){
        lock.lock();
        try {
            //第一次不用等待直接执行,之后的都需要等待线程C的唤醒
            if (number!=0) {
                condition1.await();
            }
            System.out.println("AAAAAAAAAAAAAA");
            number++;
            //让BC线程先跑都进入等待,才能对B线程进行唤醒
            condition2.signalAll();
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }
    public void B(){
        lock.lock();
        try {
            condition2.await();
            System.out.println("BBBBBBBBBBBBBBBB");
            condition3.signalAll();
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }
    public void C(){
        lock.lock();
        try {
            condition3.await();
            System.out.println("CCCCCCCCCCCCCCCC");
            condition1.signalAll();
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }
}

```java
//运行结果
AAAAAAAAAAAAAA
BBBBBBBBBBBBBBBB
CCCCCCCCCCCCCCCC
AAAAAAAAAAAAAA
BBBBBBBBBBBBBBBB
CCCCCCCCCCCCCCCC
AAAAAAAAAAAAAA
BBBBBBBBBBBBBBBB
CCCCCCCCCCCCCCCC
AAAAAAAAAAAAAA
BBBBBBBBBBBBBBBB
CCCCCCCCCCCCCCCC
......

8锁(锁的8个问题)

问题1、2:正常情况下,两个线程谁先执行?A线程执行时睡眠4秒,两个线程谁先执行?

  1. 正常情况下,两个线程谁先执行?
  2. A线程执行时睡眠4秒,两个线程谁先执行?

答案:谁先拿到锁,谁先执行。跟调用方法的顺序没有关系。AB线程都需要拿到同一个对象的锁才能执行同步方法。
由于A线程开始后主线程睡眠1秒后B线程才开始,所以A线程先拿到锁,即使A线程进入方法后睡眠了4秒,但是没有释放锁,所以等4秒后执行完线程结束后才把锁释放给B线程,B线程才能执行同步方法。

Test01(问题1、2)

public class Test01 {
    public static void main(String[] args) throws InterruptedException {
        Phone phone = new Phone();
        new Thread(()->{
            try {
                phone.send();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        },"AA").start();
        TimeUnit.SECONDS.sleep(1);
        new Thread(()->{
            phone.call();
        },"BB").start();
    }
}

class Phone{
    public synchronized void send() throws InterruptedException {
        TimeUnit.SECONDS.sleep(4);
        System.out.println("发短信");
    }
    public synchronized void call(){
        System.out.println("打电话");
    }
}
//运行结果
发短信
打电话

问题3:增加一个普通方法,同步方法和普通方法谁先执行?

  1. 增加一个普通方法,同步方法和普通方法谁先执行?

答案:由于A线程进入方法后睡眠了4秒,到B线程执行的时候,虽然B线程并没有拿到锁,但是普通方法不需要锁可以直接执行,所以B线程调用的普通方法要比睡眠4秒的A线程先执行。

Test02(问题3)

public class Test02 {
    public static void main(String[] args) throws InterruptedException {
        Phone2 phone = new Phone2();
        new Thread(()->{
            try {
                phone.send();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        },"AA").start();
        TimeUnit.SECONDS.sleep(1);
        new Thread(()->{
            phone.hello();
        },"BB").start();
    }
}

class Phone2{
    public synchronized void send() throws InterruptedException {
        TimeUnit.SECONDS.sleep(4);
        System.out.println("发短信");
    }
    public synchronized void call(){
        System.out.println("打电话");
    }
    //不是同步方法,不受锁的影响
    public void hello(){
        System.out.println("hello");
    }
}

//运行结果
hello
发短信

问题4:两个对象,两个线程执行两个不同的同步方法谁先执行?

  1. 两个对象,两个线程执行两个不同的同步方法谁先执行?

答案:虽然B线程比A线程晚1秒拿到锁,但是两个线程拿到的是不同的实例对象的锁1和2,由于A线程进入方法后睡眠了4秒,所以B线程也能拿到锁先执行2对象的同步方法,而A线程拿到1对象的锁睡眠4秒后执行的是1对象的同步方法。

Test02(问题4)

public class Test02 {
    public static void main(String[] args) throws InterruptedException {
    	//两个实例对象,两把不同的锁
        Phone2 phone1 = new Phone2();
        Phone2 phone2 = new Phone2();
        new Thread(()->{
            try {
                phone1.send();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        },"AA").start();
        TimeUnit.SECONDS.sleep(1);
        new Thread(()->{
            phone2.call();
        },"BB").start();
    }
}

class Phone2{
	//synchronized锁的对象是实例对象而不是Class类对象
    public synchronized void send() throws InterruptedException {
        TimeUnit.SECONDS.sleep(4);
        System.out.println("发短信");
    }
    public synchronized void call(){
        System.out.println("打电话");
    }
    public void hello(){
        System.out.println("hello");
    }
}


//运行结果
打电话
发短信

问题5、6:两个静态同步方法,两个不同的实例对象,谁先执行?

    1. 两个静态同步方法,两个不同的实例对象,谁先执行?

答案:A线程和B线程执行的都是静态同步方法,虽然执行的是不同实例对象的方法,但是静态同步方法锁的是Class类对象,静态同步方法由Class类来管理,所以当执行静态同步方法的时候A先拿到Class对象的锁,B只能等A执行完静态同步方法释放锁后才能拿到Class对象的锁执行静态同步方法。

Test03(问题5、6)

public class Test03 {
    public static void main(String[] args) throws InterruptedException {
        Phone3 phone1 = new Phone3();
        Phone3 phone2 = new Phone3();
        new Thread(()->{
            try {
                phone1.send();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        },"AA").start();
        TimeUnit.SECONDS.sleep(1);
        new Thread(()->{
            phone2.call();
        },"BB").start();
    }
}

class Phone3{
	//static synchronized和synchronized不同,静态同步方法由Class类对象来管理,同步方法由Class类的实例对象来管理
    public static synchronized void send() throws InterruptedException {
        TimeUnit.SECONDS.sleep(4);
        System.out.println("发短信");
    }
    public static synchronized void call(){
        System.out.println("打电话");
    }
    public void hello(){
        System.out.println("hello");
    }
}
//运行结果
发短信
打电话

一个静态同步方法,一个普通同步方法,同一个/不同的对象,谁先执行?

    1. 一个静态同步方法,一个普通同步方法,同一个/不同的实例对象,谁先执行?

答案:AB线程执行的是不同类型的同步方法,一个是静态同步方法,一个是普通的同步方法,虽然执行的实例对象是同一个,但是实际上需要用的的锁不是同一个对象的锁,静态同步方法需要Class对象的锁,同步方法需要的是实例对象的锁,两个线程执行的方法互不相干,所以按照逻辑B线程先执行。

Test04(问题7、8)

public class Test04 {
    public static void main(String[] args) throws InterruptedException {
        Phone4 phone1 = new Phone4();
        new Thread(()->{
            try {
                phone1.send();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        },"AA").start();
        TimeUnit.SECONDS.sleep(1);
        new Thread(()->{
            phone1.call();
        },"BB").start();
    }
}

class Phone4{
	//两个不同类型的方法,用的是不同的锁
    public static synchronized void send() throws InterruptedException {
        TimeUnit.SECONDS.sleep(4);
        System.out.println("发短信");
    }
    public synchronized void call(){
        System.out.println("打电话");
    }
    public void hello(){
        System.out.println("hello");
    }
}

//运行结果
打电话
发短信

集合类不安全

List不安全

TestJUC06(List不安全)

//java.util.ConcurrentModificationException并发修改异常
public class TestJUC06 {
    public static void main(String[] args) {
        //List<String> list =new ArrayList();
        //相当于ArrayList所有可能发生线程同步问题的方法都加上synchronized,效率低
        //List<String> list=new Vector<>();
        //List<String> list= Collections.synchronizedList(new ArrayList<>());
        List<String> list= new CopyOnWriteArrayList<>();
        for (int i = 0; i < 100; i++) {
            new Thread(()->{
                list.add(UUID.randomUUID().toString().substring(0,5));
                System.out.println(list);
            }).start();
        }
    }
}

CopyOnWriteArrayList之所以比Vector效率高是因为,这个类的读操作是不需要同步的,而Vector读操作和写操作都上了synchronized

SynchronizedList和Vector最主要的区别: 1.SynchronizedList有很好的扩展和兼容功能。他可以将所有的List的子类转成线程安全的类。 2.使用SynchronizedList的时候,进行遍历时要手动进行同步处理。 3.SynchronizedList可以指定锁定的对象。

Callable

FutureTask适配类简单解析

在这里插入图片描述
在这里插入图片描述

由于new Thread方式不能直接传入Callable类型进行创建线程,只能传入Runnable对象进行创建,因此有了Runnable接口的实现类FutureTask适配类

(源码注释翻译)
可取消的异步计算。 此类提供Future的基本实现,包括启动和取消计算,查询以查看计算是否完成以及检索计算结果的方法。 只有在计算完成后才能检索结果; 如果计算尚未完成,则get方法将阻塞。 一旦计算完成,就不能重新启动或取消计算(除非使用runAndReset调用计算)。
FutureTask可用于包装Callable或Runnable对象。 由于FutureTask实现Runnable ,因此FutureTask可以提交给Executor以执行。
除了用作独立类之外,此类还提供protected功能,这些功能在创建自定义任务类时可能会很有用。
自从:
1.5
类型参数:
–此FutureTask的get方法返回的结果类型

/**
     * Creates a {@code FutureTask} that will, upon running, execute the
     * given {@code Callable}.
     *
     * @param  callable the callable task
     * @throws NullPointerException if the callable is null
     */
    public FutureTask(Callable<V> callable) {
        if (callable == null)
            throw new NullPointerException();
        this.callable = callable;
        this.state = NEW;       // ensure visibility of callable
    }

    /**
     * Creates a {@code FutureTask} that will, upon running, execute the
     * given {@code Runnable}, and arrange that {@code get} will return the
     * given result on successful completion.
     *
     * @param runnable the runnable task
     * @param result the result to return on successful completion. If
     * you don't need a particular result, consider using
     * constructions of the form:
     * {@code Future<?> f = new FutureTask<Void>(runnable, null)}
     * @throws NullPointerException if the runnable is null
     */
    public FutureTask(Runnable runnable, V result) {
        this.callable = Executors.callable(runnable, result);
        this.state = NEW;       // ensure visibility of callable
    }

由源码可以看出,FutureTask构造器可以传入Runnable对象或者Callable对象进行包装,而FutureTask实现了Runble接口因此可以以new Thread方式创建线程

TestJUC07(通过FutureTask适配类创建线程)

public class TestJUC07 {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        MyThread myThread = new MyThread();
        //适配类
        FutureTask<String> task = new FutureTask<>(myThread);
        new Thread(task,"a").start();
        //内部维护了一个state,在线程B执行run方法的时候,futureTask内部的state已经不是new状态,直接return,不会执行线程中的语句
        new Thread(task,"b").start();
        //获取Callable返回值,如果线程未计算完成get方法则会阻塞
        System.out.println(task.get());
    }
}
class MyThread implements Callable<String> {

    @Override
    public String call() throws Exception {
        System.out.println("输出值");
        return "返回值";
    }
}

常用辅助类

CountDownLatch

指定线程执行完毕,再执行操作

(源码注释)
一种同步帮助,它允许一个或多个线程等待,直到在其他线程中执行的一组操作完成为止。
CountDownLatch用给定的count初始化。 由于countDown方法的调用,直到当前计数达到零为止, await方法将阻塞,此后所有释放线程都将被释放,并且任何随后的await调用将立即返回。 这是一种一次性现象-无法重置计数。 如果您需要一个用于重置计数的版本,请考虑使用CyclicBarrier 。
CountDownLatch是一种多功能的同步工具,可以用于多种用途。 以1计数初始化的CountDownLatch用作简单的on / off锁存器或gate:所有调用await线程都在gate处等待,直到被countDown的线程打开countDown 。 初始化为N的CountDownLatch可用于使一个线程等待,直到N个线程完成某个动作或某个动作已完成N次。
CountDownLatch一个有用属性是,它不需要调用countDown线程在继续进行操作之前就countDown等待计数达到零,它只是防止任何线程经过await状态,直到所有线程都可以通过为止。
用法示例:这是一对类,其中一组工作线程使用两个倒计时锁存器:
第一个是启动信号,可防止任何工人继续前进,直到驾驶员为他们做好准备为止。
第二个是完成信号,允许驾驶员等到所有工人都完成为止。

 class Driver { // ...
   void main() throws InterruptedException {
     CountDownLatch startSignal = new CountDownLatch(1);
     CountDownLatch doneSignal = new CountDownLatch(N);

     for (int i = 0; i < N; ++i) // create and start threads
       new Thread(new Worker(startSignal, doneSignal)).start();

     doSomethingElse();            // don't let run yet
     startSignal.countDown();      // let all threads proceed
     doSomethingElse();
     doneSignal.await();           // wait for all to finish
   }
 }

 class Worker implements Runnable {
   private final CountDownLatch startSignal;
   private final CountDownLatch doneSignal;
   Worker(CountDownLatch startSignal, CountDownLatch doneSignal) {
     this.startSignal = startSignal;
     this.doneSignal = doneSignal;
   }
   public void run() {
     try {
       startSignal.await();
       doWork();
       doneSignal.countDown();
     } catch (InterruptedException ex) {} // return;
   }

   void doWork() { ... }
 }

另一个典型用法是将问题分为N个部分,用Runnable描述每个部分,该Runnable执行该部分并在锁存器中递减计数,然后将所有Runnable排队给执行程序。 当所有子部分都完成时,协调线程将能够通过等待。 (当线程必须以此方式反复递减计数时,请使用CyclicBarrier 。)


 class Driver2 { // ...
   void main() throws InterruptedException {
     CountDownLatch doneSignal = new CountDownLatch(N);
     Executor e = ...

     for (int i = 0; i < N; ++i) // create and start threads
       e.execute(new WorkerRunnable(doneSignal, i));

     doneSignal.await();           // wait for all to finish
   }
 }

 class WorkerRunnable implements Runnable {
   private final CountDownLatch doneSignal;
   private final int i;
   WorkerRunnable(CountDownLatch doneSignal, int i) {
     this.doneSignal = doneSignal;
     this.i = i;
   }
   public void run() {
     try {
       doWork(i);
       doneSignal.countDown();
     } catch (InterruptedException ex) {} // return;
   }

   void doWork() { ... }
 }

内存一致性影响:在计数达到零之前,在调用countDown()操作之前,线程中的操作在另一个线程中相应的await()成功返回之后执行(意思是线程一个紧接着一个执行,在一个线程await成功返回后执行下一个线程,直到调用countDown让计数达到零)

TestJUC08(CountDownLatch倒计时)

public class TestJUC08 {
    public static void main(String[] args) throws InterruptedException {
        //倒数计数器总数为10
        CountDownLatch countDownLatch=new CountDownLatch(10);
        for (int i = 0; i < 10; i++) {
            new Thread(()->{
                System.out.println(Thread.currentThread().getName());
                //数量-1
                countDownLatch.countDown();
            },String.valueOf(i+1)).start();
        }
        //如果数量不为0则当前线程等待
        //对主线程阻塞
        countDownLatch.await();
        System.out.println("finish");
    }
}

CyclicBarrier

指定个数线程执行完毕再执行操作

TestJUC09(CyclicBarrier循环屏障)

public class TestJUC09 {
    public static void main(String[] args) {
        //CountDownLatch是阻塞主线程,CyclicBarrier是阻塞子线程,
        //相当于设置了屏障点,拦截线程,数量达到一定进行释放
        CyclicBarrier cyclicBarrier = new CyclicBarrier(10,()->{
            System.out.println("finish");
        });
        for (int i = 0; i < 10; i++) {
            //局部变量需要加final,拷贝一份参数进匿名内部类(防止内外参数不同final)
            final int temp=i;
            new Thread(()->{
                System.out.println(Thread.currentThread().getName()+temp);
                try {
                    //对子线程阻塞
                    if(cyclicBarrier.await()==0){
                        System.out.println("finish too");
                    }
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } catch (BrokenBarrierException e) {
                    e.printStackTrace();
                }
            }).start();
        }
    }
}

java为什么匿名内部类的参数引用时final?

java为什么匿名内部类的参数引用时final?

Semaphore

同一时间只能有指定数量个得到线程

TestJUC10(Semaphore信号量)

public class TestJUC10 {
    public static void main(String[] args) {

        Semaphore semaphore = new Semaphore(3);

        for (int i = 0; i < 6; i++) {
            new Thread(()->{
                try {
                    //acquire从信号量中获得许可,如果许可数量已满则阻塞
                    semaphore.acquire();
                    System.out.println(Thread.currentThread().getName()+"抢到");
                    TimeUnit.SECONDS.sleep(2);
                    System.out.println(Thread.currentThread().getName()+"离开");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }finally {
                    //release释放已获得的许可到信号量中
                    semaphore.release();
                }
            },String.valueOf(i)).start();
        }
    }
}

读写锁(ReadWriteLock)

  • 当读写锁被加了写锁时,其他线程对该锁加读锁或者写锁都会阻塞(不是失败)。
  • 当读写锁被加了读锁时,其他线程对该锁加写锁会阻塞,加读锁会成功。

TestJUC11(读写锁应用)


public class TestJUC11 {
    public static void main(String[] args) {

        MyCache myCache = new MyCache();
        for (int i = 0; i < 10; i++) {
            new Thread(()->{
                myCache.set("","");
            },String.valueOf(i)).start();
        }
        for (int i = 0; i < 10; i++) {
            new Thread(()->{
                myCache.get("");
            },String.valueOf(i)).start();
        }
    }
}

class MyCache{
    private volatile Map<String,Object> map=new HashMap<>();
    ReentrantReadWriteLock readWriteLock = new ReentrantReadWriteLock();
    void set(String s,Object o){
   		//写锁加锁
        readWriteLock.writeLock().lock();
        try {
            System.out.println(Thread.currentThread().getName()+"write");
            map.put(s,o);
            System.out.println(Thread.currentThread().getName()+"write finish");
        } finally {
        	//写锁解锁
            readWriteLock.writeLock().unlock();
        }

    }
    void get(String s){
    	//读锁加锁
        readWriteLock.readLock().lock();
        try {
            System.out.println(Thread.currentThread().getName()+"read");
            map.get(s);
            System.out.println(Thread.currentThread().getName()+"read finish");
        } finally {
        	//读锁解锁
            readWriteLock.readLock().unlock();
        }

    }
}

阻塞队列(BlockingQueue)

BlockingQueue的四组api

在这里插入图片描述

TestJUC12(BlockingQueue的四种api测试)

public class TestJUC12 {
    public static void main(String[] args) throws InterruptedException {
        test4();
    }
    //抛出异常
    public static void test1(){
        ArrayBlockingQueue arrayBlockingQueue = new ArrayBlockingQueue(3);
        System.out.println(arrayBlockingQueue.add("a"));
        System.out.println(arrayBlockingQueue.add("b"));
        System.out.println(arrayBlockingQueue.add("c"));
        //IllegalStateException: Queue full 队列已满 抛出异常
        //System.out.println(arrayBlockingQueue.add("d"));

        //返回队首元素 队列为空抛出异常
        System.out.println(arrayBlockingQueue.element());

        System.out.println(arrayBlockingQueue.remove());
        System.out.println(arrayBlockingQueue.remove());
        System.out.println(arrayBlockingQueue.remove());
        //NoSuchElementException 找不到元素 抛出异常
        //System.out.println(arrayBlockingQueue.remove());
    }

    //有返回值 不抛出异常
    public static void test2(){
        ArrayBlockingQueue arrayBlockingQueue = new ArrayBlockingQueue<>(3);
        System.out.println(arrayBlockingQueue.offer("a"));
        System.out.println(arrayBlockingQueue.offer("b"));
        System.out.println(arrayBlockingQueue.offer("c"));
        //返回false 不抛出异常
        //System.out.println(arrayBlockingQueue.offer("d"));

        //返回队首元素 队列为空返回null
        System.out.println(arrayBlockingQueue.peek());

        System.out.println(arrayBlockingQueue.poll());
        System.out.println(arrayBlockingQueue.poll());
        System.out.println(arrayBlockingQueue.poll());
        //返回null 不抛出异常
        System.out.println(arrayBlockingQueue.poll());
    }

    //阻塞
    public static void test3() throws InterruptedException {
        ArrayBlockingQueue arrayBlockingQueue = new ArrayBlockingQueue<>(3);
        //put方法如果阻塞状态下被中断 抛出中断异常
        arrayBlockingQueue.put("a");
        arrayBlockingQueue.put("b");
        arrayBlockingQueue.put("c");
        //队列满无法放入元素 没有获得锁则一直被阻塞
        //arrayBlockingQueue.put("d");

        System.out.println(arrayBlockingQueue.take());
        System.out.println(arrayBlockingQueue.take());
        System.out.println(arrayBlockingQueue.take());
        //队列空无法取出元素 阻塞
        //System.out.println(arrayBlockingQueue.take());
    }

    //阻塞超时
    public static void test4() throws InterruptedException {
        ArrayBlockingQueue arrayBlockingQueue = new ArrayBlockingQueue<>(3);
        System.out.println(arrayBlockingQueue.offer("a",2, TimeUnit.SECONDS));
        System.out.println(arrayBlockingQueue.offer("b",2, TimeUnit.SECONDS));
        System.out.println(arrayBlockingQueue.offer("c",2, TimeUnit.SECONDS));
        //等待2秒后如果无法获取锁返回false
        System.out.println(arrayBlockingQueue.offer("d",2, TimeUnit.SECONDS));

        System.out.println(arrayBlockingQueue.poll(2, TimeUnit.SECONDS));
        System.out.println(arrayBlockingQueue.poll(2, TimeUnit.SECONDS));
        System.out.println(arrayBlockingQueue.poll(2, TimeUnit.SECONDS));
        //等待2秒后如果无法获取锁返回false
        System.out.println(arrayBlockingQueue.poll(2, TimeUnit.SECONDS));
    }
}

同步队列(SynchronousQueue)

经典的生产者-消费者模式,操作流程是这样的:

  • 有多个生产者,可以并发生产产品,把产品置入队列中,如果队列满了,生产者就会阻塞;
  • 有多个消费者,并发从队列中获取产品,如果队列空了,消费者就会阻塞。

SynchronousQueue 是无缓冲等待队列,是一个不存储元素的阻塞队列,它的特别之处在于它内部没有容器生产者会直接将产品交给消费者
一个生产线程,当它生产产品(即put的时候),如果当前没有人想要消费产品(即当前没有线程执行take),此生产线程必须阻塞,等待一个消费线程调用take操作,take操作将会唤醒该生产线程,同时消费线程会获取生产线程的产品(即数据传递),这样的一个过程称为一次配对过程(当然也可以先take后put,原理是一样的)。

TestJUC13(SynchronousQueue用例)

public class TestJUC13 {
    public static void main(String[] args) throws InterruptedException {
        SynchronousQueue<Object> synchronousQueue = new SynchronousQueue<>();
        //三个需要put的线程进入排队阻塞状态,等待需要take的线程进行take操作再进行唤醒
        new Thread(()->{
            try {
                synchronousQueue.put("a");
                System.out.println(Thread.currentThread().getName()+"put"+"a");
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        },"1").start();
        new Thread(()->{
            try {
                synchronousQueue.put("b");
                System.out.println(Thread.currentThread().getName()+"put"+"b");
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        },"2").start();
        new Thread(()->{
            try {
                synchronousQueue.put("c");
                System.out.println(Thread.currentThread().getName()+"put"+"c");
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        },"3").start();
        //每两秒唤醒一个需要take的线程
        TimeUnit.SECONDS.sleep(2);
        new Thread(()->{
            try {
                System.out.println(Thread.currentThread().getName()+"take"+synchronousQueue.take());
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        },"4").start();
        TimeUnit.SECONDS.sleep(2);
        new Thread(()->{
            try {
                System.out.println(Thread.currentThread().getName()+"take"+synchronousQueue.take());
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        },"5").start();
        TimeUnit.SECONDS.sleep(2);
        new Thread(()->{
            try {
                System.out.println(Thread.currentThread().getName()+"take"+synchronousQueue.take());
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        },"6").start();
    }
}

线程池

例子:银行办理业务
corePoolSize=常驻窗口
maximumPoolSize=全部窗口(常驻窗口以外的临时窗口会关闭)
workQueue=候客区
如果办理业务窗口人满了,候客区窗口也满了,则要执行拒绝策略RejectedExecutionHandler
在这里插入图片描述

三大方法 七大参数 四种拒绝策略

三大Executors创建线程池方法:

  • newSingleThreadExecutor(新的单线程执行器)
  • newFixedThreadPool(新的固定线程池)
  • newCachedThreadPool(新的缓存线程池)

三个Executors创建线程池本质都是通过new ThreadPoolExecutor进行创建,相当于创建线程池的快捷方式

七大参数:

public ThreadPoolExecutor(int corePoolSize,//核心线程池大小(核心线程一直存在不会被空闲回收)
                              int maximumPoolSize,//最大线程池大小(如果当前任务大于核心线程大小+阻塞队列大小则进行扩容,最大为maximumPoolSize)
                              long keepAliveTime,//空闲存活时间(当超出核心线程大小的线程空闲时间大于keepAliveTime对线程进行回收)
                              TimeUnit unit,//时间单位
                              BlockingQueue<Runnable> workQueue,//阻塞队列(大于核心线程池的任务进入队列)
                              ThreadFactory threadFactory,//线程创建工厂(一般不变)
                              RejectedExecutionHandler handler//拒绝策略) {
        if (corePoolSize < 0 ||
            maximumPoolSize <= 0 ||
            maximumPoolSize < corePoolSize ||
            keepAliveTime < 0)
            throw new IllegalArgumentException();
        if (workQueue == null || threadFactory == null || handler == null)
            throw new NullPointerException();
        this.acc = System.getSecurityManager() == null ?
                null :
                AccessController.getContext();
        this.corePoolSize = corePoolSize;
        this.maximumPoolSize = maximumPoolSize;
        this.workQueue = workQueue;
        this.keepAliveTime = unit.toNanos(keepAliveTime);
        this.threadFactory = threadFactory;
        this.handler = handler;
    }

四大拒绝策略

  • AbortPolicy(终止策略)
  • CallerRunsPolicy(调用者执行策略)
  • DiscardPolicy(丢弃策略)
  • DiscardOldestPolicy(丢弃最早任务策略)

TestJUC14 (创建并测试线程池)

public class TestJUC14 {
    public static void main(String[] args) {
        //线程池中只有一个线程
        //ExecutorService pool = Executors.newSingleThreadExecutor();
        //线程池中有固定数量线程
        //ExecutorService pool = Executors.newFixedThreadPool(5);
        //线程池中不限制线程数量(最大数量为Integer.MAX_VALUE)
        //ExecutorService pool = Executors.newCachedThreadPool();
        ThreadPoolExecutor pool = new ThreadPoolExecutor(
                2,
                5,
                10L,
                TimeUnit.SECONDS,
                new LinkedBlockingQueue<>(3),
                Executors.defaultThreadFactory(),
                //最大承载:队列+最大线程池大小 5+3=8
                //new ThreadPoolExecutor.AbortPolicy()终止策略 超出最大承载始终抛出异常
                //new ThreadPoolExecutor.CallerRunsPolicy()调用者执行策略 超出最大承载让调用者线程(main)执行任务,如果线程池已关闭则舍弃任务
                //new ThreadPoolExecutor.DiscardPolicy()丢弃策略 超出最大承载直接丢弃任务,不执行任何操作
                new ThreadPoolExecutor.DiscardOldestPolicy()//丢弃最早任务策略 超出最大承载丢弃队列中最早的任务并重试执行,如果线程池已关闭则舍弃任务
        );
        try {
            for (int i = 0; i < 9; i++) {
                pool.execute(()->{
                    System.out.println(Thread.currentThread().getName()+"执行");
                });
            }
        } finally {
            pool.shutdown();
        }
    }
}

最大线程池大小的设置

  • CPU密集型
    最大线程池大小设置为 当前运行时环境cpu线程数量,可以通过下列代码进行获取。保证cpu每个线程都在运行,这时cpu利用率最高。
    (适用于cpu使用率高,io使用率没那么高的时候,应该根据cpu数量进行设置,减少线程之间的切换效率降低)
    比如,在该任务需要长时间调用cpu进行运算,如果加大线程数量,处理更多任务的时候cpu没有那么多物理线程,因此cpu需要不断切换线程导致效率下降。
Runtime.getRuntime().availableProcessors();
  • IO密集型
    最大线程池大小设置为 大于对io占用资源较大的任务数量,一般设置为数量的2倍。
    (适用于io使用率高,cpu使用率相对没那么高的时候,适当加大线程池的数量让cpu处理更多业务)
    比如,在该任务需要在io上耗费大量时间,而这些io并不需要占用cpu资源而是占用内存或硬盘资源,因此可以设置更多线程,在线程进行io时腾出cpu资源处理更多任务。

四大函数式接口

  • Function函数型接口
  • Predicate断定型接口
  • Consumer消费型接口
  • Supplier供给型接口

TestJUC15(四种函数式接口的用例)

public class TestJUC15 {
    public static void main(String[] args) {
        //函数型接口<T,R>:一个输入参数T,返回值为一个参数R
        Function<String, String> function = (str) -> {
            return str;
        };
        System.out.println(function.apply("abc"));

        //断定型接口<T>:一个输入参数T,返回值为一个布尔值
        Predicate<String> predicate = (str) -> {
            return str.isEmpty();
        };
        System.out.println(predicate.test(""));

        //消费型接口<T>:一个输入参数T,没有返回值
        /*Consumer<String> consumer = (str) -> {
            System.out.println(str);
        };*/
        Consumer<String> consumer = System.out::println;
        consumer.accept("abc");

        //供给型接口<T>:没有输入参数,返回值为一个参数T
        Supplier<String> supplier = ()->{
          return "abc";
        };
        System.out.println(supplier.get());
    }
}

Stream流式计算

TestJUC16(对User进行Stream流式计算)

//1、ID是偶数
//2、年龄必须大于20岁
//3、用户名转为大写字母
//4、用户名字母倒序
//5、只输出一个用户
public class TestJUC16 {
    public static void main(String[] args) {
        User user1 = new User(1,"a",18);
        User user2 = new User(2,"b",19);
        User user3 = new User(3,"c",20);
        User user4 = new User(4,"d",21);
        User user5 = new User(5,"e",22);
        User user6 = new User(6,"f",23);
        //asList方法返回的是Arrays的内部类ArrayList而不是java.util.ArrayList类
        // 内部类ArrayList并没有重写AbstractList类下的方法add因此不能add新数据(总是抛出异常)
        // 可以通过new ArrayList进行重新创建新的ArrayList
        List<User> users = new ArrayList<>(Arrays.asList(user1, user2, user3, user4, user5, user6));
        users.stream()//返回集合的Stream流
                .filter((a)->{return a.userId%2==0;})//对Stream流进行filter筛选
                .filter((a)->{return a.age>20;})
                .map((a)->{return a.userName.toUpperCase();})//对Stream流通过map条件获取流
                .sorted(Collections.reverseOrder())//对Stream流进行排序
                .forEach(System.out::println);//forEach输出流中每一个元素
    }
}

ForkJoin(分支合并)

在这里插入图片描述

在这里插入图片描述

特点:工作窃取
在这里插入图片描述
(1)每个工作线程都有自己的工作队列WorkQueue;

(2)这是一个双端队列,它是线程私有的;

(3)ForkJoinTask中fork的子任务,将放入运行该任务的工作线程的队头,工作线程将以LIFO的顺序来处理工作队列中的任务;

(4)为了最大化地利用CPU,空闲的线程将从其它线程的队列中“窃取”任务来执行;

(5)从队列头部压入队列,从队列头部取出任务执行,从其他队列的尾部窃取任务,以减少竞争;(LIFO后入先出

(6)双端队列的操作:push()/pop()仅在其所有者工作线程中调用,poll()是由其它线程窃取任务时调用的

(7)当只剩下最后一个任务时,还是会存在竞争,是通过CAS来实现的
在这里插入图片描述
ForkJoin框架分析
在这里插入图片描述
java 8 stream reduce详解和误区

TestJUC17(普通方法、ForkJoin方法、Stream并行流方法累加)

public class TestJUC17 {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        //ForkJoin方法
        long start1=System.currentTimeMillis();
        //创建一个ForkJoin池
        ForkJoinPool forkJoinPool = new ForkJoinPool();
        //创建一个自定义的任务
        ForkJoin task = new ForkJoin(0,20_0000_0000);
        //将此任务进行提交
        ForkJoinTask<Long> submit = forkJoinPool.submit(task);
        //获得原始结果
        //get方法的线程被中断会立即抛出InterruptedException,而join不会
        //任务异常完成的的相关异常,get方法会将相关异常都封装成ExecutionException异常,join则不封装原样抛出
        long sum = submit.get();
        long end1=System.currentTimeMillis();
        System.out.println(sum+"     "+(end1-start1));

        //普通方法
        long start2=System.currentTimeMillis();
        long sum2=0L;
        for (long i = 0; i <= 20_0000_0000; i++) {
            sum2+=i;
        }
        long end2=System.currentTimeMillis();
        System.out.println(sum2+"     "+(end2-start2));

        //并行流方法
        long start3=System.currentTimeMillis();
        //rangeClosed闭区间[] range()开区间 返回一个有序的LongStream
        //这里reduce方法的第一个参数identity是reduce的初始累加值,但是在并行的情况下多线程每个线程都会进行初始值累加
        //identity值必须是累加器功能的身份。这意味着对于所有x,accumulator.apply(identity, x)等于x 因此这里identity应为0
        //调用静态方法LongStream.rangeClosed()返回longPipeline输入的是范围Long序列,范围是[0, 20_0000_0000L]
        //调用longPipeline父类abstractPipeline方法parallel()标记sourceStage.parallel = true并返回当前longPipeline
        //调用longPipeline的reduce方法进行缩减,将一个流缩减成一个值,通过执行LongBinaryOperator函数(这里函数输入方法为sum二元累加
        long sum3 = LongStream.rangeClosed(0, 20_0000_0000L).parallel().reduce(0, Long::sum);
        long end3=System.currentTimeMillis();
        System.out.println(sum3+"     "+(end3-start3));
    }
}


class ForkJoin extends RecursiveTask<Long>{
    long start;
    long end;

    long temp=10000L;

    public ForkJoin(long start, long end) {
        this.start = start;
        this.end = end;
    }

    @Override
    protected Long compute() {
        if (end-start>temp) {
            long middle=(start+end)/2;
            ForkJoin forkJoin1 = new ForkJoin(start, middle);
            ForkJoin forkJoin2 = new ForkJoin(middle+1, end);
            forkJoin1.fork();//安排在当前任务正在运行的池中异步执行此任务(push进线程池的队列中)
            forkJoin2.fork();
            return forkJoin1.join()+forkJoin2.join();//获取原始结果
        } else {
            long sum=0L;
            for (long i = start; i <= end; i++) {
                sum+=i;
            }
            return sum;
        }
    }
}

异步调用(CompleteFuture)

在这里插入图片描述

CompletableFuture基本用法

创建CompleteFuture

    public static <U> CompletableFuture<U> supplyAsync(Supplier<U> supplier) {
        return asyncSupplyStage(asyncPool, supplier);
    }


    public static <U> CompletableFuture<U> supplyAsync(Supplier<U> supplier,
                                                       Executor executor) {
        return asyncSupplyStage(screenExecutor(executor), supplier);
    }


    public static CompletableFuture<Void> runAsync(Runnable runnable) {
        return asyncRunStage(asyncPool, runnable);
    }


    public static CompletableFuture<Void> runAsync(Runnable runnable,
                                                   Executor executor) {
        return asyncRunStage(screenExecutor(executor), runnable);
    }

    /**
     * Returns a new CompletableFuture that is already completed with
     * the given value.
     */
    public static <U> CompletableFuture<U> completedFuture(U value) {
        return new CompletableFuture<U>((value == null) ? NIL : value);
    }
static <U> CompletableFuture<U> asyncSupplyStage(Executor e,
                                                     Supplier<U> f) {
        if (f == null) throw new NullPointerException();
        CompletableFuture<U> d = new CompletableFuture<U>();
        e.execute(new AsyncSupply<U>(d, f));
        return d;
    }

thenApply

	//同步执行函数
    public <U> CompletableFuture<U> thenApply(
        Function<? super T,? extends U> fn) {
        return uniApplyStage(null, fn);
    }
	//用默认异步池执行函数
    public <U> CompletableFuture<U> thenApplyAsync(
        Function<? super T,? extends U> fn) {
        return uniApplyStage(asyncPool, fn);
    }
	//用指定异步池执行函数
    public <U> CompletableFuture<U> thenApplyAsync(
        Function<? super T,? extends U> fn, Executor executor) {
        return uniApplyStage(screenExecutor(executor), fn);
    }

TestJUC18(异步调用CompletableFuture)

public class TestJUC18 {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        //异步任务提交不影响主线程,将异步任务交给其余线程计算
        CompletableFuture completableFuture = CompletableFuture.supplyAsync(()->{
            /*try {
                //异步任务中线程睡眠2秒,睡眠2秒后已经计算完异步任务并返回结果
                TimeUnit.SECONDS.sleep(2);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }*/
            return Thread.currentThread().getName();
        })
                //当当前异步任务完成后将当前返回结果作为输入参数再次用默认异步池执行函数
                .thenApplyAsync((a)->{
                    System.out.println(a+"==="+Thread.currentThread().getName());
                    //主线程中completableFuture.get获取该值
                    return a;
                })
                .thenCombine(CompletableFuture.completedFuture("a"),(a,b)->{return a+b;})
                /*.thenAccept((a)->{})
                无返回值
                因此下一步的thenApply无法获取a,a为null,主线程中completableFuture.get也无法获取结果,值为null*/

                //thenApply同步 thenApplyAsync异步
                //如果supplyAsync执行较慢,thenApply方法执行线程和supplyAsync执行线程相同
                //如果supplyAsync执行速度快,thenApply方法执行线程和Main方法执行线程相同
                .thenApply((a)->{
                    System.out.println(a+"==="+Thread.currentThread().getName());
                    return a;});
        //主线程睡眠3秒
        TimeUnit.SECONDS.sleep(1);
        //这时输出,由于异步任务已完成可获取结果,则两行数据同时输出
        System.out.println("111111111111");
        System.out.println(completableFuture.get()+"---1");
    }
}

JMM

全面理解JMM模型
Java内存模型(即Java Memory Model,简称JMM)本身是一种抽象的概念,是一种规范,并不真实存在,它描述的是一组规则或规范,通过这组规范定义了程序中各个变量(包括实例字段,静态字段和构成数组对象的元素)的访问方式。

由于Java是跨平台语言,在不同操作系统中内存都有一定的差异性,这样久造成了并发不一致,所以JMM的作用就是用来屏蔽掉不同操作系统中的内存差异性来保持并发的一致性。同时JMM也规范了JVM如何与计算机内存进行交互。简单的来说JMM就是Java自己的一套协议来屏蔽掉各种硬件和操作系统的内存访问差异,实现平台一致性达到最终的"一次编写,到处运行"。

在这里插入图片描述

  • read 读取,作用于主内存把变量从主内存中读取到本本地内存。
  • load 加载,主要作用本地内存,把从主内存中读取的变量加载到本地内存的变量副本中
  • use 使用,主要作用本地内存,把工作内存中的一个变量值传递给执行引擎,每当虚拟机遇到一个需要使用变量的值的字节码指令时将会执行这个操作。、
  • assign 赋值 作用于工作内存的变量,它把一个从执行引擎接收到的值赋值给工作内存的变量,每当虚拟机遇到一个给变量赋值的字节码指令时执行这个操作。
  • store 存储 作用于工作内存的变量,把工作内存中的一个变量的值传送到主内存中,以便随后的write的操作。
  • write 写入 作用于主内存的变量,它把store操作从工作内存中一个变量的值传送到主内存的变量中。
  • lock 锁定 :作用于主内存的变量,把一个变量标识为一条线程独占状态。
  • unlock 解锁:作用于主内存变量,把一个处于锁定状态的变量释放出来,释放后的变量才可以被其他线程锁定。

同时在Java内存模型中明确规定了要执行这些操作需要满足以下规则:

  • 不允许read和load、store和write的操作单独出现。
  • 不允许一个线程丢弃它的最近assign的操作,即变量在工作内存中改变了之后必须同步到主内存中。
  • 不允许一个线程无原因地(没有发生过任何assign操作)把数据从工作内存同步回主内存中。
  • 一个新的变量只能在主内存中诞生,不允许在工作内存中直接使用一个未被初始化(load或assign)的变量。即就是对一个变量实施use和store操作之前,必须先执行过了assign和load操作。
  • 一个变量在同一时刻只允许一条线程对其进行lock操作,lock和unlock必须成对出现
  • 如果对一个变量执行lock操作,将会清空工作内存中此变量的值,在执行引擎使用这个变量前需要重新执行load或assign操作初始化变量的值
    如果一个变量事先没有被lock操作锁定,则不允许对它执行unlock操作;也不允许去unlock一个被其他线程锁定的变量。
  • 对一个变量执行unlock操作之前,必须先把此变量同步到主内存中(执行store和write操作)。

总线嗅探机制

在现代计算机中,CPU 的速度是极高的,如果 CPU 需要存取数据时都直接与内存打交道,在存取过程中,CPU 将一直空闲,这是一种极大的浪费,所以,为了提高处理速度,CPU 不直接和内存进行通信,而是在 CPU 与内存之间加入很多寄存器,多级缓存,它们比内存的存取速度高得多,这样就解决了 CPU 运算速度和内存读取速度不一致问题。

由于 CPU 与内存之间加入了缓存,在进行数据操作时,先将数据从内存拷贝到缓存中,CPU 直接操作的是缓存中的数据。但在多处理器下,将可能导致各自的缓存数据不一致(这也是可见性问题的由来),为了保证各个处理器的缓存是一致的,就会实现缓存一致性协议,而嗅探是实现缓存一致性的常见机制。
在这里插入图片描述注意:缓存的一致性问题,不是多处理器导致,而是多缓存导致的。

嗅探机制工作原理:每个处理器通过监听在总线上传播的数据来检查自己的缓存值是不是过期了,如果处理器发现自己缓存行对应的内存地址修改,就会将当前处理器的缓存行设置无效状态,当处理器对这个数据进行修改操作的时候,会重新从主内存中把数据读到处理器缓存中。

注意:基于 CPU 缓存一致性协议,JVM 实现了 volatile 的可见性,但由于总线嗅探机制,会不断的监听总线,如果大量使用 volatile 会引起总线风暴。所以,volatile 的使用要适合具体场景。

TestJUC19(volatile保证可见性)

//验证JMM模型的可见性原理
public class TestJUC19 {
    //volatile保证当前num值在主内存改变时,其余线程的可见性
    static volatile int num=0;
    public static void main(String[] args) throws InterruptedException {
        new Thread(() -> {
            while (num == 0) {

            }
        }).start();
        TimeUnit.SECONDS.sleep(3);
        //此时改变num的值为1,如果没有volatile保证可见性,子线程中的循环则无法停止
        num=1;
        System.out.println(num);
    }
}

volatile

volatile是jvm的轻量级同步机制

1、保证可见性

2、不保证原子性

3、禁止指令重排

TestJUC20(volatile不能保证原子性)

public class TestJUC20 {
    //volatile不能保证原子性
    //不用lock和synchronize解决原子性问题Atomic类
    static volatile AtomicInteger num=new AtomicInteger(0);
    static void add(){
        num.getAndIncrement();//+1,底层是cas
    }
    public static void main(String[] args) {
        for (int i = 0; i < 20; i++) {
            new Thread(()->{
                for (int i1 = 0; i1 < 10000; i1++) {
                    add();
                }
            }).start();
        }
        //默认有main线程和gc线程
        while (Thread.activeCount()>2){
            Thread.yield();
        }
        System.out.println(num);//不为200000
    }
}

在这里插入图片描述

单例模式

设计模式:单例模式介绍及8种写法

TestJUC21(单例模式和反射破坏单例模式)

public class TestJUC21 {
    public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
        Lazy lazy1 = Lazy.getInstance();
        //通过反射获取Lazy的无参构造器
        Constructor<Lazy> declaredConstructor = Lazy.class.getDeclaredConstructor();
        //将构造器去私有化
        declaredConstructor.setAccessible(true);
        //通过反射获取的构造器进行创建实例
        Lazy lazy2 = declaredConstructor.newInstance();
        //成功通过反射创建出新的对象(破坏了单例模式)
        System.out.println(lazy1);
        System.out.println(lazy2);
    }
}

//饿汉式
class Hungry {
    //如果jvm启动时直接创建Hungry实例,可能导致浪费空间
    private byte[] data1 = new byte[1024 * 1024];
    private byte[] data2 = new byte[1024 * 1024];
    private byte[] data3 = new byte[1024 * 1024];
    private byte[] data4 = new byte[1024 * 1024];
    private byte[] data5 = new byte[1024 * 1024];

    private Hungry() {

    }

    private final static Hungry HUNGRY = new Hungry();

    public static Hungry getInstance() {
        return HUNGRY;
    }
}

//懒汉式单例 DCL懒汉式double check lock
class Lazy {
    private Lazy() {
        System.out.println(Thread.currentThread().getName());
    }

    private static volatile Lazy LAZY;

    //双重检测锁模式DCL
    public static Lazy getInstance() {
        if (LAZY == null) {
            synchronized (Lazy.class) {
                //如果没有双重检测锁,多线程导致同时创建对象不安全
                if (LAZY == null) {
                    LAZY = new Lazy();
                    /*
                     * 1、分配内存空间
                     * 2、执行构造方法,初始化对象
                     * 3、把对象指向内存空间
                     *
                     * 指令重排
                     *
                     * 正常情况 123
                     * 多线程下 132
                     * 当A线程执行完13后还没初始化对象时,由于对象指向了内存空间,
                     * B线程判断这时LAZY!=null,直接返回LAZY,这时的LAZY并没有完成构造导致出错
                     * 因此LAZY需要添加volatile禁止指令重排导致这种情况发生
                     *
                     * */
                }
            }
        }
        return LAZY;
    }
}

//静态内部类(类加载是线程安全的)
class Static{
    private Static(){

    }
    //定义了静态内部类,初始化时并不会被执行
    private static class Instance{
        private static final Static instance=new Static();
    }
    //当调用静态方法时,静态内部类才会被加载
    public static Static getInstance(){
        return Instance.instance;
    }
}

//枚举类(不能被反射破坏)
enum Enum{
    INSTANCE;
    public static Enum getInstance(){
        return INSTANCE;
    }
}

public T newInstance(Object ... initargs)
        throws InstantiationException, IllegalAccessException,
               IllegalArgumentException, InvocationTargetException
    {
        if (!override) {
            if (!Reflection.quickCheckMemberAccess(clazz, modifiers)) {
                Class<?> caller = Reflection.getCallerClass();
                checkAccess(caller, clazz, null, modifiers);
            }
        }
        //通过反射得到的构造器创建实例时,如果创建实例的类型是枚举类型,抛出IllegalArgumentException
        if ((clazz.getModifiers() & Modifier.ENUM) != 0)
            throw new IllegalArgumentException("Cannot reflectively create enum objects");
        ConstructorAccessor ca = constructorAccessor;   // read volatile
        if (ca == null) {
            ca = acquireConstructorAccessor();
        }
        @SuppressWarnings("unchecked")
        T inst = (T) ca.newInstance(initargs);
        return inst;
    }

CAS

CompareAndSwap 比较与交换
什么是CAS机制?

public class AtomicInteger extends Number implements java.io.Serializable {
    private static final long serialVersionUID = 6214790243416807050L;

    // setup to use Unsafe.compareAndSwapInt for updates
    //Unsafe是执行底层、不安全操作的方法集合。
    //getUnsafe为调用者提供执行unsafe操作的能力。
    //调用者应该小心保护返回的Unsafe对象,因为它可用于在任意内存地址读取和写入数据。 它绝不能传递给不受信任的代码。
    private static final Unsafe unsafe = Unsafe.getUnsafe();
    private static final long valueOffset;

    static {
        try {
        	//获取内存地址的偏移值
            valueOffset = unsafe.objectFieldOffset
                (AtomicInteger.class.getDeclaredField("value"));
        } catch (Exception ex) { throw new Error(ex); }
    }
//......
}

AtomicInteger的getAndIncrement(+1)方法源码
在这里插入图片描述
如果不能进行更新值,那么继续进行循环,再次获取当前对象对应内存偏移值的value值,再次进行CAS比较和交换尝试
在这里插入图片描述

CAS的缺点:

1) CPU开销过大

在并发量比较高的情况下,如果许多线程反复尝试更新某一个变量,却又一直更新不成功,循环往复,会给CPU带来很到的压力。

2) 不能保证代码块的原子性

CAS机制所保证的知识一个变量的原子性操作,而不能保证整个代码块的原子性。比如需要保证3个变量共同进行原子性的更新,就不得不使用synchronized了。

3) ABA问题

这是CAS机制最大的问题所在。

TestJUC22(ABA问题产生)

//ABA问题
public class TestJUC22 {
    public static void main(String[] args) {
        //底层为CAS
        AtomicInteger atomicInteger = new AtomicInteger(1);
        //正确的线程
        //当同时提交了两次,同时开启了两条线程(开启了错误线程)
        new Thread(()->{
            System.out.println(atomicInteger.compareAndSet(1, 2));
            System.out.println(Thread.currentThread().getName()+"   1-->2");
            System.out.println(atomicInteger.compareAndSet(2, 1));
            System.out.println(Thread.currentThread().getName()+"   2-->1");
        },"a").start();
        //错误的线程
        //错误的线程因为某些原因被堵塞,这时正确的线程把2->1了
        //错误线程恢复运行状态并通过cas判断发现内存中的值还是1,将1->2,发生错误
        new Thread(()->{
            try {
                TimeUnit.SECONDS.sleep(2);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(atomicInteger.compareAndSet(1, 2));
            System.out.println(Thread.currentThread().getName()+"   1-->66");
        },"b").start();
    }
}

TestJUC23(解决ABA问题)

public class TestJUC23 {
    public static void main(String[] args) {
        AtomicStampedReference atomicStampedReference=new AtomicStampedReference(
                1,1);
        new Thread(()->{
            //版本号stamp
            int stamp = atomicStampedReference.getStamp();
            //flag判断是否成功CAS
            boolean flag;
            System.out.println(Thread.currentThread().getName()+"--->"+stamp);
            
            try {
                TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            
            flag = atomicStampedReference.compareAndSet(
                    1, 2, stamp, stamp + 1);
            System.out.println(flag);
            //如果CAS成功,更新stamp的值并输出stamp
            if (flag) {
                stamp = atomicStampedReference.getStamp();
                System.out.println(Thread.currentThread().getName()+"--->"+stamp);
            }
            flag = atomicStampedReference.compareAndSet(
                    2, 1, stamp, stamp + 1);
            System.out.println(flag);
            if (flag) {
                stamp = atomicStampedReference.getStamp();
                System.out.println(Thread.currentThread().getName()+"--->"+stamp);
            }
        },"a").start();


        new Thread(()->{
            int stamp = atomicStampedReference.getStamp();
            boolean flag;
            System.out.println(Thread.currentThread().getName()+"--->"+stamp);
            //由于线程b比线程a启动慢,线程a已经进行CAS更新版本号,线程b的版本号不对,因此无法进行CAS
            try {
                TimeUnit.SECONDS.sleep(3);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

            flag = atomicStampedReference.compareAndSet(
                    1, 2, 1, 2);
            System.out.println(flag);
            if (flag) {
                stamp = atomicStampedReference.getStamp();
                System.out.println(Thread.currentThread().getName()+"--->"+ stamp);
            }
        },"b").start();
    }
}

    /**
     * Atomically sets the value of both the reference and stamp
     * to the given update values if the
     * current reference is {@code ==} to the expected reference
     * and the current stamp is equal to the expected stamp.
     *
     * @param expectedReference the expected value of the reference
     * @param newReference the new value for the reference
     * @param expectedStamp the expected value of the stamp
     * @param newStamp the new value for the stamp
     * @return {@code true} if successful
     */
    public boolean compareAndSet(V   expectedReference,
                                 V   newReference,
                                 int expectedStamp,
                                 int newStamp) {
        Pair<V> current = pair;
        return
        	//CAS源码使用==来比较数据,因此需要注意包装类型的缓存范围
        	//比如Integer包装类使用了对象缓存机制,默认缓存范围是-128~127
        	//超出这个区间,所有数据都在堆中产生,不会再复用已有对象,因此CAS有可能会出现问题
            expectedReference == current.reference &&
            expectedStamp == current.stamp &&
            ((newReference == current.reference &&
              newStamp == current.stamp) ||
             casPair(current, Pair.of(newReference, newStamp)));
    }

所有相同类型的包装类对象之间值的比较,都应该用equals方法比较,对于Integer类在-128~127之间,Integer对象是在IntegerCache.cache产生,会复用已有对象,这个区间内的Integer值可以直接使用==进行判断。

可重入锁

可重入锁详解

什么是 “可重入”,可重入就是说某个线程已经获得某个锁,可以再次获取锁而不会出现死锁。

TestJUC24(可重入锁)

public class TestJUC24 {
    public static void main(String[] args) {
        Phone phone = new Phone();
        new Thread(()->{
            try {
                phone.sms();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        },"A").start();
        new Thread(()->{
            try {
                phone.sms();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        },"B").start();
    }
}

class Phone{
    public synchronized void sms() throws InterruptedException {
        System.out.println(Thread.currentThread().getName()+"sms");
        TimeUnit.SECONDS.sleep(2);
        //可重入锁让内部的锁本可以从外部获取锁
        //可重入锁防止在call方法获取锁时无法获取,导致死锁
        call();
    }
    public synchronized void call(){
        System.out.println(Thread.currentThread().getName()+"call");
    }
}

自旋锁

自旋锁(spinlock):是指当一个线程在获取锁的时候,如果锁已经被其它线程获取,那么该线程将循环等待,然后不断的判断锁是否能够被成功获取,直到获取到锁才会退出循环。
获取锁的线程一直处于活跃状态,但是并没有执行任何有效的任务,使用这种锁会造成busy-waiting。

TestJUC25(自定义自旋锁)

public class TestJUC25 {
    public static void main(String[] args) {
        Spinlock spinlock = new Spinlock();
        new Thread(()->{
            spinlock.myLock();
            try {
                System.out.println("拿到锁了");
                TimeUnit.SECONDS.sleep(2);
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                spinlock.myUnLock();
            }
        },"A").start();
        new Thread(()->{
            spinlock.myLock();
            try {
                //B线程先睡眠1秒保证A线程先拿到锁
                TimeUnit.SECONDS.sleep(1);
                System.out.println("拿到锁了");
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                spinlock.myUnLock();
            }
        },"B").start();
    }
}

class Spinlock{
    //原子引用,可以对对象进行原子操作,保证对象修改的原子性
    AtomicReference<Thread> atomicReference=new AtomicReference();
    public void myLock(){
        Thread thread = Thread.currentThread();
        //如果A先拿到锁,先把null值cas为A线程对象
        //B线程进入方法时发现内存中的对象值不为null则进行自旋直到A线程执行myUnLock方法(内存中对象值为null)
        while(!atomicReference.compareAndSet(null, thread)){

        }
        System.out.println(thread.getName()+"--->myLock");
    }
    public void myUnLock(){
        Thread thread = Thread.currentThread();
        System.out.println(thread.getName()+"--->myUnLock");
        while(!atomicReference.compareAndSet(thread,null)){

        }
    }
}

死锁

在这里插入图片描述

产生死锁的必要条件

  • 互斥条件:进程要求对所分配的资源进行排它性控制,即在一段时间内某资源仅为一进程所占用。
  • 请求和保持条件:当进程因请求资源而阻塞时,对已获得的资源保持不放。
  • 不剥夺条件:进程已获得的资源在未使用完之前,不能剥夺,只能在使用完时由自己释放。
  • 环路等待条件:在发生死锁时,必然存在一个进程–资源的环形链。

TestJUC26(死锁简单例子)

public class TestJUC26 {
    public static void main(String[] args) {
        String FirstLock="FirstLock";
        String SecondLock="SecondLock";
        new Thread(new DeadLock(FirstLock,SecondLock),"A").start();
        new Thread(new DeadLock(SecondLock,FirstLock),"B").start();
    }
}


class DeadLock implements Runnable{
    String A;
    String B;
    public DeadLock(String a, String b) {
        A = a;
        B = b;
    }

    @SneakyThrows
    @Override
    public void run() {
        //这时A线程锁了First对象并睡眠2秒让B线程锁上Second对象
        synchronized (A){
            System.out.println(Thread.currentThread().getName()+"获得了锁"+A);
            TimeUnit.SECONDS.sleep(2);
            //睡眠结束后,A线程请求获取Second对象的锁,B线程请求获取First对象的锁,导致死锁
            synchronized (B){

            }
        }
    }
}

如何排查死锁问题

1、使用 jps -l 查看java进程信息,定位问题进程号(Java Virtual Machine Process Status Tool)
在这里插入图片描述

2、使用 jstack 进程号 查看堆栈信息,定位死锁问题

在这里插入图片描述
A线程与B线程等待的锁和已获取的锁的信息
在这里插入图片描述

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值