java多线程问题

java对多线程基础

最近读了java编程思想,对书上进行了总结,欢迎各位阅读指导

1.多线程的优势
(1)多线程的好处是在程序的设计上,运行速度上有所提升 如果是多处理器的设备,可以有效的提高吞吐量,但是通常可以提升单处理器上的程序运行的速度,因为,因为因为当程序阻塞的时候,例如IO引起的阻塞,如果没有多线程,程序要停止等待直到外部条件发生变化。多线程编写在编写可响应程序上有很重要的应用。
(2)多线程可以简化程序,例如仿真,没有多线程的支持很难解决的,比如游戏中有很多角色,每一个都有自己的想法
2.基本线程机制
多线程我们把程序划分多个分离的,独立运行的任务,这些任务每一个都由执行线程来驱动,单个进程可以拥有多个并发执行的任务,通过切分CPU时间,你的每个任务好像有自己的CPU一样,多线程多任务是使用多处理器最合适的方式。
(1)定义任务
线程驱动任务,这里描述任务通过实现Runnable接口来实现,要想定义任务,需要编写run方法,run()对静态方法Thread.yield的调用是对线程调度器的一种建议,建议调度器切换另一个线程

public class LiftOff implements Runnable {
    @Override
    public void run() {
        while (true){
            Thread.yield();
        }
    }
}

(2)Thread
Runnable用来定义任务,任务的驱动用Thread类,使得任务可以运行

public void main(){
        Thread thread=new Thread(new LiftOff());
        thread.start();
    }

(3)Executor
Executor为你管理Thread对象,简化了多线程,Executor在客户端和执行任务之间提供一个间接层,Executor执行任务,允许你管理异步任务的执行,而无需显示的管理线程的生命周期,ExecutorService对象使用静态的Exeutor方法创建的,这个方法可以确认Executor类型

 public void main(){
        ExecutorService exec=Executors.newCachedThreadPool();
        for(int i=0;i<5;i++){
            exec.execute(new LiftOff());
        }
        exec.shutdown();
    }

shutdown调用时防止新的任务提交给exec,你不用每次都创建线程,创建线程会有很大的开销,在事件驱动程序中,你可以从线程池获取,可以创建不同的线程池类型:
FixedThreadPool使用的Thread对象是有限的,创建一个指定工作线程数量的线程池。每当提交一个任务就创建一个工作线程,如果工作线程数量达到线程池初始的最大数,则将提交的任务存入到池队列中。FixedThreadPool是一个典型且优秀的线程池,它具有线程池提高程序效率和节省创建线程时所耗的开销的优点。但是,在线程池空闲时,即线程池中没有可运行任务时,它不会释放工作线程,还会占用一定的系统资源。

CachedThreadPool通常创建与所需数量相同的线程,回收旧线程时停止创建新线程,工作线程的创建数量几乎没有限制(其实也有限制的,数目为Interger. MAX_VALUE), 这样可灵活的往线程池中添加线程。
如果长时间没有往线程池中提交任务,即如果工作线程空闲了指定的时间(默认为1分钟),则该工作线程将自动终止。终止后,如果你又提交了新的任务,则线程池重新创建一个工作线程。在使用CachedThreadPool时,一定要注意控制任务的数量,否则,由于大量线程同时运行,很有会造成系统瘫痪

SingleThreadExecutor创建一个单线程化的Executor,即只创建唯一的工作者线程来执行任务,它只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序(FIFO, LIFO, 优先级)执行。如果这个线程异常结束,会有另一个取代它,保证顺序执行。单工作线程最大的特点是可保证顺序地执行各个任务,并且在任意给定的时间不会有多个线程是活动的。

(4)从任务中产生返回值
Runnable是工作独立的任务,没有返回值,如果你希望有返回值,可以使用Callable接口,Callable是一个泛型,表示从方法call返回的类型,使用ExecutorService.submit方法调用,

public class TaskCallable implements Callable<String> {
    private int id;
    public TaskCallable(int id) {
        this.id = id;
    }
    @Override
    public String call() throws Exception {
        return "result";
    }
}

 public static void main(){
        ExecutorService exec= Executors.newCachedThreadPool();
        List<Future<String>> list=new ArrayList<>();
        for(int i=0;i<5;i++){
            list.add(exec.submit(new TaskCallable(i)));
        }
        for(Future<String> fs:list){
            try{
                fs.get();
            }catch (Exception e){

            }
        }

    }

可以用isDone检查Future是否已经完成。可以直接调用get会阻塞,直到结果准备就绪
(5)休眠
调用sleep()方法进行休眠,可以指定休眠的时间,或者使用TimeUnit类进行休眠
(6)优先级
优先级是把线程的重要性传递给调度器,调度器会倾向于让高的优先级的线程先运行,优先级低的线程执行的频率比较低,任何时刻都可以通过setPriority设置优先级,getPriority获取优先级,经常使用的三个优先级MAX_PRIORITY,NORM_PRIORITY,MIN_PRIORITY
(7)让步
调用yield建议具有相同优先级的其他线程可以运行
(8)后台线程
后台线程是程序运行的时候,在后台提供的一种通用服务的线程,这种线程不属于程序中不可缺的一部分,所以所有不是后台线程结束的时候,程序就终止了,同时会杀死进程中所有后台线程

 for(int i=0;i<10;i++){
            Thread daemon=new Thread(new Backgound());
            daemon.setDaemon(true);  //设置成后台线程
            daemon.start();

        }

在线程启动之前调用setDaemon,才能把他设置成后台线程,如果一个线程是后台线程,那么它创建的任何线程都被设置为后台线程

(9)继承Thread

public class SimpleThread extends Thread {
    public int countDown=5;
    public static int threadCount=0;

    public SimpleThread() {
        super(Integer.toString(++threadCount));
        start();
    }

    @Override
    public void run() {
        while (true){
            if(--countDown==0){
                return;
            }
        }
    }
}

(10)加入一个线程
一个线程可以在其他线程之上调用join方法,如果某个线程在另一个线程t上调用t.join,此线程被挂起,直到目标t结束才恢复,调用join时带上超时参数,这样目标线程在这段时间到期还没有结束的话,join方法总能返回

public class Sleeper extends Thread{
    private int duration;

    public Sleeper(String name,int sleepTime) {
        super(name);
        this.duration = sleepTime;
        start();
    }

    @Override
    public void run() {
        try {
            sleep(duration);
        }catch (Exception e){

        }

    }
}

public class Joiner extends Thread {
    private Sleeper sleeper;

    public Joiner(String name,Sleeper sleeper) {
        super(name);
        this.sleeper = sleeper;
        start();
    }

    @Override
    public void run() {
        try {
            sleeper.join();
        }catch (Exception e){

        }
    }
}


        Sleeper sleeper=new Sleeper("",1500);
        Sleeper grumpy=new Sleeper("",1500);
        Joiner dopey=new Joiner("Dopey",sleeper);
        Joiner doc=new Joiner("Doc",grumpy);
        grumpy.interrupt();

这里在Joiner中加入 sleeper.join();所以等待sleeper线程先执行,Joiner线程挂起,知道sleeper线程结束或者中断,Joiner线程才开始执行
(11)创建有响应的用户界面

public class ResponseUI extends Thread {
    public static volatile double d=1;

    public ResponseUI() {
        setDaemon(true);
        start();
    }

    @Override
    public void run() {
        while (true){
            d=d+(Math.PI+Math.E)/d;
        }

    }
}

这里在单独的一个线程进行计算,主线程负责读取用户输入,这样就可以及时响应用户
21.2.13 线程组
这里不做解释,感兴趣可以自己学习
21.2.14 捕获异常
由于线程的本质特性,使得你不能捕获从线程中逃逸的异常,线程异常弹逃出执行任务的run方法就会向外传播到控制台,下面的例子展示如何捕获线程异常。

public class ExceptionThread implements Runnable {
    @Override
    public void run() {
        throw new RuntimeException();
    }
}
try{
     ExecutorService executorService=Executors.newCachedThreadPool();
            executorService.execute(new ExceptionThread());

      }catch (Exception e){
            Log.e("","");
     }

这样是捕获不到异常的,为了解决这个问题,我们修改Executor产生现象的方式,Thread.UncaughtExceptonHandler,他允许你在每一个线程附着一个异常处理器,会在线程死亡的时候进行调用,

public class MyUnaughtExceptionHandler implements Thread.UncaughtExceptionHandler {
    @Override
    public void uncaughtException(Thread t, Throwable e) {
        Log.e("e",e.getMessage());
    }
}

public class HandlerThreadFactory implements ThreadFactory {
    @Override
    public Thread newThread(@NonNull Runnable r) {
        Thread thread=new Thread(r);
        thread.setUncaughtExceptionHandler(new MyUnaughtExceptionHandler());
        return thread;
    }
}


public class HandlerThreadFactory implements ThreadFactory {
    @Override
    public Thread newThread(@NonNull Runnable r) {
        Thread thread=new Thread(r);
        thread.setUncaughtExceptionHandler(new MyUnaughtExceptionHandler());
        return thread;
    }
}

 //最后执行
 ExecutorService executorService=Executors.newCachedThreadPool(new HandlerThreadFactory());
            executorService.execute(new ExceptionThread());

21.3 共享受限资源
多个线程同时做一件事情,可能会访问同一个资源(因为资源是进程独立的)这样会出现很多问题
21.3.1 不正确的访问资源
举个例子就知道什么是资源的冲突了

public abstract class IntGenerator {
    private volatile boolean canceled=false;
    public abstract int next();
    public void cancel(){
        canceled=true;
    }
    public boolean isCanceled(){
        return canceled;
    }
}

public class EvenGenerator extends IntGenerator {
    private int currentEvenvalue=0;
    @Override
    public int next() {
        ++currentEvenvalue;    //Dangger point here
        ++currentEvenvalue;
        return currentEvenvalue;
    }
    public static void test(){
        EvenChecker.test(new EvenGenerator(),10);
    }
}

public class EvenChecker implements Runnable {

    private IntGenerator generator;
    private final int id;

    public EvenChecker(IntGenerator generator, int id) {
        this.generator = generator;
        this.id = id;
    }

    @Override
    public void run() {
        while (!generator.isCanceled()){
            int val= generator.next();
            if(val%2!=0){
                generator.cancel();
            }
        }
    }

    public static void test(IntGenerator intGenerator,int count){
        ExecutorService exec= Executors.newCachedThreadPool();
        for(int i=0;i<count;i++){
            exec.execute(new EvenChecker(intGenerator, i));
        }
    }
}
 EvenGenerator.test();   //最后调用这个

当一个线程执行第一个 ++currentEvenvalue时,第二个线程调用了 int val= generator.next(); 这样导致程序退出 注意:递增不是原子性操作,单一的线程不安全的
21.3.2 解决共享资源竞争
解决共享资源竞争,就是防止两个线程在同一时刻访问同一个资源,当一个资源被一个线程访问的时候,在其加上锁,第一个访问资源的线程必须锁定这个资源,访问结束后释放锁,其他线程在锁释放之前,就无法访问他
基本上所有的并发模式在解决线程冲突的时候,都是采用的序列化访问共享资源的方案,锁产生了一种互相排斥的效果,所以这种机制常常称作互斥量
这里可以使用yield setPriority给线程调度器提供建议,java提供关键字synchronized的形式,为防止资冲突提供了内置支持,执行synchronized的时候,先检查锁是否可用,然后获取锁,执行代码,释放锁,
共享资源一般是已对象形式存在的内存片段,但也可以是文件,输入输出端口,或者是打印机,要控制对资源的访问,得先把他包装成一个对象,然后把要访问的资源方法标记为synchronized,如果某个线程处于一个标记为synchronized的方法调用中,那么这个线程从这个方法返回之前,其他所有要调用类型任何标记的synchronized方法线程都会被阻塞
(注意:同步方法必须是private否则synchronized就不能防止其他线程直接访问方法)
一个线程可以多次获取对象的锁,如果一个方法在同一个对象上调用了第二个方法,后者又调用了同一个对象的另一个方法,就会发生这种情况,JVM负责跟踪对象被加锁的次数,如果一个对象被解锁,计数变为0,只有首次获得锁的线程,才能继续获取锁,

(2)使用显式的Lock
Lock对象必须被显式的创建,锁定和释放,与内建锁形式相比,缺乏优雅性,但是有时候会更加灵活,

public class EvenGenerator extends IntGenerator {
    private int currentEvenvalue=0;
    private Lock lock=new ReentrantLock();
    @Override
    public int next() {
        lock.lock();
        ++currentEvenvalue;    //Dangger point here
        ++currentEvenvalue;
        lock.unlock();
        return currentEvenvalue;
    }
    public static void test(){
        EvenChecker.test(new EvenGenerator(),10);
    }
}

Lock可以指定等待的时间获取锁的时间,ReentrantLock允许你尝试但最终未获取锁,这样如果有人获取了这个锁,你可以决定离开去执行一些其他的事情,而不是等待直到这个锁释放
显示的Lock对象在加锁释放锁的方面,相对于内建的synchornized来说,还赋予你更细粒的控制力,实现一些同步很有用的,例如:遍历链表节点的传递加锁机制,这种遍历必须在方法当前节点的锁之前捕获下一个节点的锁

21.3.3 原子性与易变性

首先理解概念:什么是原子性:原子性操作是指不能被线程调度机制中断的操作,一旦开始执行,那么一定会在上下文切换之前完成
所以理论上来讲,原子操作是不需要线程同步的
除了long double之外的基本类型的操作属于原子性操作,但是JVM可以把64位的读取和写入当做两个不可分离的32位来执行,这样就会发生读写和写入上下文切换导致不同的任务看到不同的结果的可能性,但是当你定义long double变量的时候,使用volatile关键字的时候,就会获取(简单的赋值与返回操作)原子性操作,
多处理器上,可视性问题,一个线程做出了修改,即使在中断的意义上讲是原子性的,对其他任务也可能是不可视的(暂时的存储在本地处理器的缓存中),因此不同的任务对应用状态有不同的视图,同步机制强制在处理器系统中,一个任务作出修改必须在应用中可视的,如果没有同步机制,那么修改的可视性无法确定,volatile关键字还保证了应用中的可视性,如果一个域声明了volatile,只要你对这个域产生了写操作,那么所有的读操作都可以看到这个修改,即使处理器本地使用了缓存也一样,volatile域会立即写入到主存,而读取操作就发生在主存里,
一个任务的所做的任何读写操作对这个任务来说都是可视的,如果域只需要对任务内部可见,就不需要声明volatile,当一个域的值依赖于他之前的值(例如递增的计算器)volatile就无法工作,如果某个域受到其他域的限制,那么volatile也无法工作,例如Range类的lower和upper边界必须遵循lower<=upper的限制,
volatile修饰的域,每次线程都是从主存中读取,(这样的话,自增的时候,可能两个线程读取相同的值),使用volatile不适应synchronized的唯一安全的情况是类中只有一个可变的域
什么是原子性操作:赋值和返回操作通常是原子性的,在C++中,i++; i+=2; 可能也是原子性的,但是在java中不是原子性的,
下边的这个例子来说明一下自增这中被认为是原子性操作,在多线程情况下,也会出现问题

//这是一个集合,用来存储自增生成的数据(循环数组)
public class CircularSet {
    private int[] array;
    private int len;
    private int index=0;

    public CircularSet(int len) {
        this.len = len;
        array=new int[len];
        for(int i=0;i<len;i++){
            array[i]=-1;
        }
    }
    public synchronized void add(int i){
        array[index]=i;
        index=++index%len;
    }
    public synchronized boolean contains(int val){
        for(int i=0;i<len;i++){
            if(array[i]==val)
                return true;
        }
        return false;
    }
}

//这里通过自增生成int类型的数
public class SerialNumberGenerator {
    private static volatile int serialsNumber=0;
    public static int nextSerialNumber(){
        return serialsNumber++;
    }
}


/**
 *
 *Test方法开启多个任务来并发执行run中的方法
 * run方法检查数是否重复,如果重复的话,退出程序,正常多线程的情况下原子操作不会发生重复的数据,如果重复了,说明自增不是原子操作
 */

public class SerialNumberChecker {
    private static int SIZE=10;
    private static CircularSet serials=new CircularSet(1000);
    private static ExecutorService exec= Executors.newCachedThreadPool();
    static class SerialChecker implements Runnable{
        @Override
        public void run() {
            while (true){
                int serial = SerialNumberGenerator.nextSerialNumber();
                if(serials.contains(serial)){
                    Log.e("退出","退出");
                    System.exit(0);
                }
                serials.add(serial);
            }

        }
    }
    public static void test(){
        for(int i=0;i<SIZE;i++){
            exec.execute(new SerialChecker());
        }
    }


}

21.3.4 原子类
原子类 AtomicInterger AtomicLong AtomicReference等特殊的原子性变量类

public class IntTestNew implements Runnable {
    private AtomicInteger i=new AtomicInteger();
    public int getValue(){
        return i.get();
    }
    public void evenIncrement(){
        i.addAndGet(2);
    }
    @Override
    public void run() {
        while (true){
            evenIncrement();   //这里是原子性操作
        }
    }

    public static void test(){
        ExecutorService exec= Executors.newCachedThreadPool();
        IntTestNew intTestNew=new IntTestNew();
        exec.execute(intTestNew);
        while (true){
            int val=intTestNew.getValue();
            if(val%2!=0){
                Log.e("","程序执行退出");
            }
        }
    }


}

这个例子使用AtomicInteger而取消了synchronized,Test方法获取的值都是2倍数的,这个不会有问题,因为自增的原子性操作

21.3.5 临界区
定义:有时候你希望防止多个线程访问方法内部的部分代码,而不是整个方法,通过这种方式分离出来的代码段被称为临界区,也需要使用synchroized
synichroized( 锁对象){
}
这样的方式也叫做同步代码块,使用同步代码块,而不是对整个方式进行同步,这样的话,多线程访问对象的性能得到提升
下边的例子展示了使用同步代码块保证线程安全的

public class Pair {
    private int x,y;

    public Pair(int x, int y) {
        this.x = x;
        this.y = y;
    }
    public Pair(){
        this(0,0);
    }

    public int getX() {
        return x;
    }

    public int getY() {
        return y;
    }
    public void incrementX(){
        x++;
    }
    public void incrementY(){
        y++;
    }
    public String toString(){
        return x+y+"";
    }

    public class PairValuesNotEqualException extends RuntimeException{
        public PairValuesNotEqualException() {
            super("Pair Value not equal:"+Pair.this );
        }

    }
    public void checkState(){
        if(x!=y){
            throw new PairValuesNotEqualException();
        }
    }

    abstract static class PainrManager{
        AtomicInteger checkCounter=new AtomicInteger(0);
        protected Pair p=new Pair();
        private List<Pair> storge= Collections.synchronizedList(new ArrayList<Pair>());
        public synchronized Pair getPair(){
            return new Pair(p.getX(),p.getY());
        }
        protected void store(Pair p){
            storge.add(p);
            try{
                TimeUnit.MILLISECONDS.sleep(50);
            }catch (Exception e){

            }
        }
        public abstract void increment();
    }

    public static class PairManager1 extends PainrManager{
        @Override
        public void increment() {
            p.incrementX();
            p.incrementY();
            store(p);
        }
    }
    public static class PairManager2 extends PainrManager{
        @Override
        public void increment() {
            Pair temp;
            synchronized (this){
                p.incrementX();
                p.incrementY();
                temp=getPair();
            }
            store(temp);
        }
    }

}
public class PairChecker implements Runnable {
    private Pair.PainrManager pm;
    public PairChecker(Pair.PainrManager pm){
        this.pm=pm;

    }
    @Override
    public void run() {
        while (true){
            pm.checkCounter.incrementAndGet();   //原子类型的操作
            pm.getPair().checkState();   //检查自增是否x y同时
        }

    }
}

public class PairManipulator implements Runnable {
    private Pair.PainrManager pm;

    public PairManipulator(Pair.PainrManager pm) {
        this.pm = pm;
    }

    @Override
    public void run() {
        while (true){
            pm.increment();
        }
    }
}

public class CirticalSection {
    static void testApproaches(Pair.PainrManager pman1, Pair.PainrManager pman2){
        ExecutorService exec= Executors.newCachedThreadPool();
        PairManipulator
                pm1=new PairManipulator(pman1),    //执行线程不安全的自增
                pm2=new PairManipulator(pman2);    //执行线程安全的自增
        PairChecker
                pcheck1=new PairChecker(pman1),
                pcheck2=new PairChecker(pman2);
        exec.execute(pm1);
        exec.execute(pm2);
        exec.execute(pcheck1);
        exec.execute(pcheck2);

    }
    public static void test(){
        Pair.PainrManager
                pman1=new Pair.PairManager1(),
                pman2=new Pair.PairManager2();
        testApproaches(pman1,pman2);

    }
}

除此之外,你还可以使用显示的lock,创建临界区

21.3.6 在其他对象上同步
同步锁对象是可以自定义的,不一定使用当前的对象this,但是要保证所有先关的任务都是在同一个对象上同步的

public class DualSynch {
    private Object synObject=new Object();  //定义同步锁
    public synchronized void f(){   //当前的对象this作为同步锁
        for(int i=0;i<5;i++){
            Thread.yield();
        }
    }
    public void g(){    //同步锁是自定义的对象
        synchronized (synObject){
            for(int i=0;i<5;i++){
                Thread.yield();
            }
        }
    }
    public static void test(){
        final DualSynch dualSynch=new DualSynch();
        new Thread(){
            @Override
            public void run() {
                dualSynch.f();
            }
        }.start();
        dualSynch.g();
    }
}

f() g()方法都会同时运行,因为两个同步方法使用的锁不是一个对象

21.3.7线程本地存储
防止线程共享资源上产生冲突的第二种解决方式就是根除对共享变量的共享,线程本地存储是自动化存储,可以使用相同变量在不同线程创建不同的存储

public class Accessor implements Runnable{
    private final int id;

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

    @Override
    public void run() {
        while (!Thread.currentThread().isInterrupted()){
            ThreadLocalVariableHolder.increment();
            Thread.yield();

        }

    }
}

public class ThreadLocalVariableHolder {
    private static ThreadLocal<Integer> value=new ThreadLocal<Integer>(){
        private Random rand=new Random(47);
        protected synchronized Integer initialValue(){
            return rand.nextInt(10000);
        }
    };
    public static void increment(){    //不是同步的
        value.set(value.get()+1);
    }
    public static int get(){      //不是同步的,因为ThreadLocal保证不会出现进程条件
        return value.get() ;
    }

    public static void test(){
        ExecutorService exec= Executors.newCachedThreadPool();
        for(int i=0;i<5;i++){
            exec.execute(new Accessor(i));
        }

    }
}

ThreadLocal对象通常当做静态存储,在创建ThreadLocal时,你只能通过get set方式来访问该对象中的内容,get返回与对象相关的副本,set会把参数插入到为其线程存储的对象,返回存储原有的对象

21.4 终结任务
cancel isCanceled 方法放在一个所有的任务都可以看到的类中,这些任务通过检查isCancel来确定何时终止,对于这个问题来说,这是个合理的方式,但是某些情况下,要求任务必须快速的终结自己,
21.4.1 装饰性花园
记录每天多个门进入花园的总人数


public class Count {
    private int count=0;
    private Random rand=new Random(47);
    public synchronized int increment(){   //synchronized用来控制现场对方法的访问
        int temp=count;
        if(rand.nextBoolean()){
            Thread.yield();
        }
        return (count= ++temp);

    }
    public synchronized int value(){
        return count;
    }
}

public class Entrance implements Runnable {
    private static Count count=new Count();   //用来记录总数
    private static List<Entrance> entrances=new ArrayList<>();
    private int number=0;
    private final int id;
    private static volatile boolean canceled=false;
    public static void cancel(){
        canceled=true;
    }
    public Entrance(int id) {
        this.id = id;
        entrances.add(this);
    }

    @Override
    public void run() {
        while (!canceled){
            synchronized (this){
                ++number;
            }
            count.increment();
        }

    }
    public synchronized int getValue(){
        return number;
    }
    public String toString(){
        return "";
    }
    public static int getTotalCount(){
        return count.value();

    }
    public static int sumEntrances(){
        int sum=0;
        for(Entrance entrance:entrances){
            sum+=entrance.getValue();
        }
        return sum;

    }

}

这里用静态变量canceled来控制线程的退出,volatile修饰变量保证了变量的可见性原子性,根据canceled判断是否退出线程

21.4.2 在阻塞时终结
首先说明一下线程的状态:
(1)新建:线程创建的时候,只会短暂的处于这种状态,分配必须的系统资源,执行初始化,获取资格(获取CPU的资格),之后调度器把这个线程转成可运行状态或者阻塞状态
(2)就绪:这种情况下,还要调度器把时间片分给线程,线程就可以运行,线程可以运行或者不运行,只要调度器分配时间片给线程线程就可以运行,不同于死亡与阻塞的状态
(3)阻塞:线程能够运行,但是某个条件阻止了他的运行,当线程处于阻塞状态的时候,调度器忽略线程,不会分配任何时间片,直到线程处于就绪的状态
(4) 死亡:处于死亡或者终止的线程不在可调度,得不到CPU的时间,不再是可运行的

进入阻塞状态:
任务进入阻塞状态,可能有如下几个原因:
1.通过sleep()使任务进入休眠的状态,这种情况下,任务在指定的时间内不会运行
2.wait()使线程挂起,直到线程得到notify() notifyAll()消息,线程才会进入就绪状态
3.任务等待某个输入输出
4.任务试图在某个对象上调用期同步方法,但是对象锁不可用,因为另一个任务已经获取了锁
注意:如果你希望终止阻塞状态的任务,对于处于阻塞状态的任务,你不能等待代码检查到其状态值得某一点,因而觉得让他主动的终止,那么你必须强制这个任务跳出阻塞状态

21.4.3 中断
Thread包含interrupt方法,因此你可以终止被阻塞的任务,这个方法设置线程中断状态,如果一个线程被阻塞或者试图执行一个阻塞的操作,那么设置这个线程的中断状态会抛出InterruptedExeption,当抛出这个异常或任务调用Thread.interrupted()提供了离开run循环而不抛出异常的第二种方式
为了调用interrupt(),你必须持有Thread对象,你可能已经注意到了,新的concurrent类库似乎避免了对Thread的直接操作,转成Executor,如果执行shutDownNow,会发送一个interrupt调用给他启动的所有线程,有时候你希望中断某个单一的任务,如果使用Executor,那么通过调用submit而不是executor启动任务,就可以持有该任务的上下文,submit返回泛型的Furture

//这个表示的是IO阻塞
public class IOBlocked implements Runnable{
    private InputStream in;

    public IOBlocked(InputStream in) {
        this.in = in;
    }

    @Override
    public void run() {
        try {
            Log.e("wait for read io","wait for read io");
            in.read();
        }catch (IOException e){

        }
        Log.e("Exiting IOBlock run","Exiting IOBlock run");


    }
}

//sleep线程阻塞
public class SleepBlocked implements Runnable{
    @Override
    public void run() {
        try {
            TimeUnit.SECONDS.sleep(100);
        }catch (InterruptedException e){
            Log.e("InterruptedException","InterruptedException");
        }
    }
}

//同步线程阻塞
public class SynchronizedBlock implements Runnable{
    public synchronized void f(){
        while (true){
            Thread.yield();
        }
    }

    public SynchronizedBlock() {
        new Thread(){
            @Override
            public void run() {
                f();
            }
        };

    }

    @Override
    public void run() {
        Log.e("","try to call f()");   //这里会同步锁阻塞
        f();
    }
}

public class Interrupting {
    public static ExecutorService exec= Executors.newCachedThreadPool();
    public static void test1(Runnable r) throws InterruptedException{
        Future<?> f=exec.submit(r);
        TimeUnit.MILLISECONDS.sleep(100);
        f.cancel(true);   //这里开始执行中断程序

    }
    public static void test(){
        try {
            test1(new SleepBlocked());    //sleep是可以中断的
            test1(new IOBlocked(System.in));    //IO阻塞不可以中断
            test1(new SynchronizedBlock());     //同步锁阻塞不可以中断


        }catch (InterruptedException e){

        }



    }
}

这里分别展示了不同的线程阻塞,但是IO线程阻塞,Synchornized线程阻塞是不可以中断的
有一个略显笨拙但是行之有效的方法,关闭任务在其上发生阻塞的底层资源

下边的这个例子展示了关闭资源,IO阻塞的线程中断

 public static void test() throws Exception{
        ExecutorService exec= Executors.newCachedThreadPool();
        ServerSocket server=new ServerSocket(8080);
        InputStream socketInput=new Socket("localhost",8080).getInputStream();
        exec.execute(new IOBlocked(socketInput));
        exec.execute(new IOBlocked(System.in));
        TimeUnit.MILLISECONDS.sleep(100);
        exec.shutdownNow();   //关闭所有的线程
        TimeUnit.MILLISECONDS.sleep(100);
        socketInput.close();   //关闭socket
        TimeUnit.MILLISECONDS.sleep(100);
        System.in.close();

    }

nio提供更人性化的 I/O中断,被阻塞的I/O会自动的响应中断

public class NIOBlocked implements Runnable {
    private final SocketChannel sc;

    public NIOBlocked(SocketChannel sc) {
        this.sc = sc;
    }

    @Override
    public void run() {
        try{
            sc.read(ByteBuffer.allocate(1));    //waiting for read

        }catch (Exception e){

        }

    }
public class NIOBlocked implements Runnable {
    private final SocketChannel sc;

    public NIOBlocked(SocketChannel sc) {
        this.sc = sc;
    }

    @Override
    public void run() {
        try{
            sc.read(ByteBuffer.allocate(1));    //waiting for read

        }catch (Exception e){

        }

    }

 public static void test() throws Exception {
        ExecutorService exec= Executors.newCachedThreadPool();
        ServerSocket server=new ServerSocket(8080);
        InetSocketAddress isa=new InetSocketAddress("localhost",8080);
        SocketChannel sc1=SocketChannel.open(isa);
        SocketChannel sc2=SocketChannel.open(isa);
        Future<?> f=exec.submit(new NIOBlocked(sc1));
        exec.execute(new NIOBlocked(sc2));
        exec.shutdown();
        TimeUnit.MILLISECONDS.sleep(100);
        f.cancel(true);
        TimeUnit.MILLISECONDS.sleep(100);
        sc2.close();

    }

上边的例子,不用释放资源也可以中断IO阻塞

如果在一个对象上调用synchronized方法,这个对象锁被其他任务获取,那么调用任务会阻塞(挂起),直到这个锁获取
下边的例子是说明,同一个互斥的任务如何被同一个任务多次获取

public class MultiLock {
    public synchronized void f1(int count){
        if(count-->0){
            Log.e("",count+"");
            f2(count);
        }
    }
    public synchronized void f2(int count){
        if(count-->0){
            Log.e("",count+"");
            f1(count);
        }
    }
    public static void test(){
        final MultiLock multiLock=new MultiLock();
        new Thread(){
            @Override
            public void run() {
                multiLock.f1(10);
            }
        };
    }
}

一个任务可以同一个对象的synchronized方法,以为这个任务已经持有锁了
跟前面额不可中断的IO类似,任务如果以不可中断的方式阻塞,那么都会有潜在锁住程序的可能性,Java SE5并发类提供一个特性,在ReentrantLock上阻塞的任务具备可以被中断的能力,这与synchronized方法或者临界区上阻塞的任务不同
21.4.4检查中断
中断发送的时刻是任务要进入阻塞操作或者已经在阻塞操作内部时,但是你已经编写可能会发送阻塞的代码,那么你只能阻塞调用的抛出异常来退出,那么无法总是离开run循环,如果你调用了interrupt的时候,没有产生任何阻塞的时候,你需要用interrupted检查中断状态,这个状态可以用interrupt来设置,interrupted返回值可以告诉你interrupt是否被调用过

21.5 线程之间的协作
之前我们学习了怎么保证资源的安全性,使用同步锁,只要获取锁的任务单一的执行操作相关的方法,或者临界资源,互斥的性质
现在除了互斥,任务需要把自己挂起,知道外部条件发生变化,表示任务可以继续执行 wait notify Java SE5还提供了await signal 方法

21.5.1 wait 与notify
当任务等待一个资源或者一个条件的时候,我们不想让任务进行空循环(忙等待),这会占用CPU,我们调用wait,把任务挂起,只有在notify或者notifyAll的时候,这个任务才会被唤醒,检查发生的变化,wait提供一种在任务之间对活动同步的方式
调用sleep yield的时候没有释放锁,调用wait的时候,线程被挂起(阻塞),锁进行释放,其他的线程可以获取锁
有两种wait的调用:1.接受毫秒数作为参数 但是与sleep不同的是,wait期间锁是释放的,可以通过notify notifyAll,或者时间到期,从wait恢复执行,
2.wait不接受任何的参数,会无限的等下去,知道接受到notify notifyAll
wait notify notifyAll 这些方式是object的一部分,不是Thread的一部分,wait方法可以在任何的synchronized方法里,实际上只能在同步方法或同步代码块调用这些方法,(因为不需要锁,非同步方法可以调用sleep方法),如果非同步方法里调用这些方法,可以编译过,但是运行会抛出异常
让另一对象执行某种操作维护自己的锁,要这样做的话,首先先得到对象的锁,比如要向对象x对象发送notifyAll,那么必须在获取x的锁的同步代码这样做

synchronized(x){
x.notifyAll();
}

下面的例子展示了线程之间的合作

//定义一个Car,实现相关的的方法  notify wait
public class Car {
    private boolean WaxOn=false;
    public synchronized void waxed(){
        WaxOn=true;
        notifyAll();
    }
    public synchronized void buffed(){
        WaxOn=false;
        notifyAll();
    }
    public synchronized void waitForWaxing() throws InterruptedException{
        while (WaxOn==false){
            wait();
        }

    }
    public synchronized void waitForBuffing() throws InterruptedException{
        while (WaxOn==true){
            wait();
        }

    }

}

public class WaxOn implements Runnable {
    private Car car;

    public WaxOn(Car car) {
        this.car = car;
    }

    @Override
    public void run() {
        try {
            while (!Thread.interrupted()){   //WaxOn
                TimeUnit.MILLISECONDS.sleep(200);
                car.waxed();
                car.waitForBuffing();
            }

        }catch (InterruptedException e){

        }

    }
}


public class WaxOff implements Runnable {
    private Car car;

    public WaxOff(Car car) {
        this.car = car;
    }

    @Override
    public void run() {
        try {
            while (Thread.interrupted()){
                car.waitForBuffing();
                TimeUnit.MILLISECONDS.sleep(100);
                car.buffed();
            }

        }catch (InterruptedException e){

        }

    }
}

public static void main(String[] args) throws Exception{
        Car car=new Car();
        ExecutorService exec= Executors.newCachedThreadPool();
        exec.execute(new WaxOff(car));
        exec.execute(new WaxOn(car));
        TimeUnit.MILLISECONDS.sleep(100);
        exec.shutdownNow();
    }

while 包围着wait:
因为你可能多个任务出于相同的原因等待同一个锁,第一个任务可能会改变这种状况,如果属于这种状况,那么这个任务应该被再次挂起,直到感兴趣的条件发生了变化
这个任务从wait唤醒的时候,有可能会有某个其他任务已经做出了改变,从而使得这个任务不在执行,此时在通过wait挂起任务
也有可能
多个任务出于不同的原因等待对象上的锁,这种情况下检查是否已经由正确的原因唤起,如果不是再次调用wait
错失的信号
两个线程使用notify wait或者notifyAll wait进行协调的时候,有可能会错失某个信号

T1:
synchronized(shareMonitor){
  //do someting
  shareMonitor.notify();
}
T2:
while(someCondition){
  synchronized(shareMonitor){
     shareMonitor.wait();
  }
}

可能出现错失信号的原因T1先执行了notify,然后T2执行wait
21.5.2 notify 与notifyAll
使用notify不是对notifyAll的优化,使用notify时,在众多等待同一个锁的任务,只有一个会被唤醒,如果你使用notify必须保证被唤醒的任务是恰当的任务,所以必须所有任务等待相同的条件(如果等待不同的条件就不知道是否唤醒恰当的任务)
使用notifyAll时,只有等待这个锁的任务都被唤醒,

public class Blocker {
    synchronized void waitingCall(){
        try{
            while (Thread.interrupted()){
                wait();
            }

        }catch (InterruptedException e){}
    }
    public synchronized void prod(){
        notify();
    }
    public synchronized void prodAll(){
        notifyAll();
    }

}

public class Task1 implements Runnable {
    public static Blocker blocker=new Blocker();
    @Override
    public void run() {
        blocker.waitingCall();

    }
}

public class Task2 implements Runnable{
    static Blocker blocker=new Blocker();
    @Override
    public void run() {
        blocker.waitingCall();
    }
}

ExecutorService exec2 = Executors.newCachedThreadPool();
        for(int i=0;i<5;i++){
            exec2.execute(new Task1());
        }
        exec2.execute(new Task2());
        Timer timer=new Timer();
        timer.scheduleAtFixedRate(new TimerTask() {
            boolean prod=true;
            @Override
            public void run() {
                if(prod){
                    //notify
                    Task1.blocker.prod();
                    prod=false;
                }else{
                    Task1.blocker.prodAll();
                    prod=true;
                }
            }
        },100,100);

上边的例子task1只会唤醒task1自身对象的锁,不会唤醒 task2 的锁

21.5.3 生产者与消费者
现在用代码来模拟一个厨师服务员的生成这消费者,厨师准备好食物,告诉服务员上菜,服务员上菜之后继续等待

public class Restaurant {
    Meal meal;
    ExecutorService exec= Executors.newCachedThreadPool();
    Waiter waiter=new Waiter(this);
    Chef chef=new Chef(this);

    public Restaurant() {
        exec.execute(waiter);
        exec.execute(chef);
    }
    public static void test(){
        new Restaurant();
    }
}

public class Waiter implements Runnable {
    private Restaurant restaurant;
    public Waiter(Restaurant restaurant) {
        this.restaurant=restaurant;
    }

    @Override
    public void run() {
        try {
            while (!Thread.interrupted()){
                synchronized (this){
                    while (restaurant.meal==null){
                        wait();
                    }
                }
                synchronized (restaurant.chef){
                    restaurant.meal=null;
                    restaurant.chef.notifyAll();
                }
            }

        }catch (InterruptedException e){

        }

    }
}

public class Chef implements Runnable {
    private Restaurant restaurant;
    private int count=0;

    public Chef(Restaurant restaurant) {
        this.restaurant = restaurant;
    }

    @Override
    public void run() {
        try{
            while (!Thread.interrupted()){
                synchronized (this){    //当前对象作为同步锁
                    if(restaurant.meal!=null)  //如果食物不是null
                        wait();
                }
                if(++count>10){     //判断是否符合中断所有线程的条件
                    restaurant.exec.shutdownNow();
                }
                synchronized (restaurant.waiter){    //waiter对象作为同步锁,唤醒waiter,new食物对象
                    restaurant.meal=new Meal(count);
                    restaurant.waiter.notifyAll();

                }
            }

        }catch (InterruptedException e){

        }

    }
}

使用显示的Lock和Condition
Condition上调用await挂起任务,调用signal唤醒一个任务,signalAll唤醒所有这个Condition被挂起的任务,调用await signal signalAll之前,必须拥有这个锁,Lock和Condition在复杂的多线程问题上使用的比较多

21.5.4 生产者消费者 队列
wait notify方法已一种非常低级的方式解决了任务互相操作的问题,每次交互的时候都握手,可以使用更高的抽象级别,使用同步队列解决同步的问题,同步队列任何时刻只允许一个任务插入或移除元素,在java.util.oncurrent.BlockingQuene接口提供了这个队列,你可以使用LinkedBlockingQueue是一个无界的对列,还可以使用ArrayBlockingQueue有固定大小,如果消费者试从队列中获取任务,而队列此时为空,那么队列可以挂起消费任务,有更多的元素时恢复消费者任务,阻塞队列可以解决大量的问题,方式与wait notifyAll相比简单,

public class LiftOffRunner implements Runnable {
    private BlockingQueue<LiftOff> rockets;

    public LiftOffRunner(BlockingQueue<LiftOff> rockets) {
        this.rockets = rockets;
    }
    public void add(LiftOff lo){
       try {
           rockets.put(lo);
       }catch (InterruptedException e){

       }

    }

    @Override
    public void run() {
        try{
            while (!Thread.interrupted()){
                LiftOff rocket=rockets.take();
                rocket.run();
            }

        }catch (InterruptedException e){

        }

    }


}

public class TestBlockingQueue {
    public static void getKey(){
        try{
            new BufferedReader(new InputStreamReader(System.in)).readLine();
        }catch (IOException e){

        }

    }
    public static void test(String message, BlockingQueue<LiftOff> queue){
        LiftOffRuner runner=new LiftOffRuner(queue);
        Thread t=new Thread(runner);
        t.start();   //开始运行
        for(int i=0;i<5;i++){
            runner.add(new LiftOff());
        }
        getKey();

    }

    public static void main(){   //这里把任务加入到阻塞对列
        test("",new LinkedBlockingQueue<LiftOff>());
        test("",new LinkedBlockingQueue<LiftOff>(3));
        test("",new LinkedBlockingQueue<LiftOff>());
    }
}

这里不需要使用同步,因为已经由BlockingQueue解决

吐司BlockingQueue
一个制作吐司 给吐司摸黄油 果酱

//定义一个吐司类
public class Toast {
    public enum Status{DRY,BUTTERED,JAMMED}
    private Status status=Status.DRY;
    private final int id;
    public Toast(int idn){
        id=idn;
    }
    public void butter(){
        status=Status.BUTTERED;
    }
    public void jam(){
        status=Status.JAMMED;
    }
    public Status getStatus(){
        return status;
    }
    public int getId(){
        return id;
    }

}

//定义吐司的阻塞对列
public class ToastQueue extends LinkedBlockingQueue<Toast> {

}

//new 一个吐司,放在toastQueue
public class Toaster implements Runnable{
    private ToastQueue toastQueue;
    private int count;
    private Random rand=new Random(47);
    public Toaster(ToastQueue tq){
        toastQueue=tq;
    }
    @Override
    public void run() {
        try {
            while (!Thread.interrupted()){
                TimeUnit.MICROSECONDS.sleep(100+rand.nextInt(500));
                Toast toast=new Toast(count++);
                toastQueue.put(toast);
            }

        }catch (Exception e){

        }

    }
}

//从dryQueue阻塞队列取出吐司,更改状态,放入butteredQueue
public class Butterer implements Runnable {
    private ToastQueue dryQueue,butteredQueue;
    public Butterer(ToastQueue dry,ToastQueue buttered){
        dryQueue=dry;
        butteredQueue=buttered;
    }

    @Override
    public void run() {
        try{
            while (!Thread.interrupted()){
                Toast t=dryQueue.take();
                t.butter();
                butteredQueue.put(t);

            }

        }catch (Exception e){

        }

    }
}



//从butteredQueue取出更改状态放入finishedQueue
public class Jammer implements Runnable{
    private ToastQueue butteredQueue,finishedQueue;
    public Jammer(ToastQueue buttered,ToastQueue finished){
        butteredQueue=buttered;
        finishedQueue=finished;
    }
    @Override
    public void run() {
        try {
            while (Thread.interrupted()){
                Toast t=butteredQueue.take();
                t.jam();
                finishedQueue.put(t);

            }

        }catch (Exception e){

        }


    }
}

try{
            ToastQueue
                    dryQueue=new ToastQueue(),
                    bufferedQueue=new ToastQueue(),
                    finishedQueue=new ToastQueue();
            ExecutorService exec3=Executors.newCachedThreadPool();
            exec3.execute(new Toaster(dryQueue));
            exec3.execute(new Butterer(dryQueue,bufferedQueue));
            exec3.execute(new Jammer(bufferedQueue,finishedQueue));
            exec3.execute(new Eater(finishedQueue));
            TimeUnit.SECONDS.sleep(1);
            exec3.shutdownNow();
        }catch (InterruptedException e){

        }

这个是一个关于阻塞队列的优秀例子,这个实例没有用到任何显示的同步(使用Lock或者synchronized关键字同步),因为同步队列(内部实现了同步)
以为队列的阻塞,使得处理过程被自动的挂起和恢复,解耦了wait notify存在的类与类之间的耦合,因为每个类只和阻塞队列进行交互
21.5.5 任务间使用管道进行输入输出
通过输入输出在线程间通信通常很有用,提供线程功能的类库 以管道的形式对线程的输入输出提供了支持,对应的类就行PipedWriter PipedReader,管道基本上是一个阻塞队列
下边的例子就是两个线程使用一个管道进行通信

public class Sender implements Runnable {
    private Random rand=new Random();
    private PipedWriter out=new PipedWriter();
    public PipedWriter getPipedWriter(){
        return out;
    }

    @Override
    public void run() {
        try {
            while (true){
                for(char c='A';c<'z';c++){
                    out.write(c);
                    TimeUnit.SECONDS.sleep(1);

                }
            }

        }catch (InterruptedException e){

        }catch (IOException e){

        }
    }
}

public class Recever implements Runnable {
    private PipedReader in;

    public Recever(Sender sender) {

        try {
            in=new PipedReader(sender.getPipedWriter());
        }catch (IOException e){

        }


    }

    @Override
    public void run() {
        try {
            while (true){
                in.read();
            }

        }catch (IOException e){

        }

    }
}

 Sender sender=new Sender();
            Receiver receiver=new Receiver(sender);
            ExecutorService execTest=Executors.newCachedThreadPool();
            execTest.execute(sender);
            execTest.execute(receiver);
            TimeUnit.SECONDS.sleep(1);
            execTest.shutdownNow();

Sender Recever代表两个互相通信的任务,Sender创建了一个PipedWriter,是一个独立的对象,但是对于Recever,PipedWriter构造必须与PipedReader关联,Sender把数据塞进Writer,Recever当调用read时,没有跟多的数据,管道自动阻塞
PipedReader与普通IO不同 PipedReader可中断

21.6 死锁
任务可以变成阻塞状态,就可能出现这样的状况,某个任务等待另一个任务,而后者又等待别的任务,直到最后的一个任务等待第一个任务释放锁,这样就会产生死锁
下面讨论一个经典的死锁问题:

public class Chopsystick {
    private boolean taken=false;
    public synchronized void take() throws InterruptedException{
        while (taken){
            wait();
        }
        taken=true;

    }
    public synchronized void drop(){
        taken=false;
        notifyAll();
    }

}

public class Philosopher implements Runnable {
    private Chopsystick left;
    private Chopsystick right;
    private final int id;
    private final int ponderFactor;
    private Random rand=new Random(47);

    public Philosopher(Chopsystick right,Chopsystick left,int id, int ponderFactor) {
        this.id = id;
        this.ponderFactor = ponderFactor;
        this.left=left;
        this.right=right;
    }

    private void pause() throws InterruptedException{
        if(ponderFactor==0)
            return;
        TimeUnit.SECONDS.sleep(1);

    }
    @Override
    public void run() {
        try {
            while (!Thread.interrupted()){
                pause();     //思考花费一段时间
                right.take();   //判断taken,如果是true,wait
                left.take();
                pause();     //吃饭
                right.drop();   //taken为false notifyAll唤醒
                left.drop();

            }

        }catch (InterruptedException e){

        }

    }
}



 public static void main(String[] args) throws Exception{
        int ponder=5;
        if(args.length>0){
            ponder=Integer.parseInt(args[0]);
        }
        int size=5;
        if(args.length>1){
            size=Integer.parseInt(args[0]);
        }
        ExecutorService exec= Executors.newCachedThreadPool();
        Chopsystick[] chopsysticks=new Chopsystick[size];
        for(int i=0;i<size;i++){    //对筷子初始化
            chopsysticks[i]=new Chopsystick();
        }
        for(int i=0;i<size;i++){   //哲学家分别使用左边的筷子 右边的筷子
            exec.execute(new Philosopher(chopsysticks[i],chopsysticks[(i+1)%size],i,ponder));
        }



    }

有一种可能,所有的哲学家都获取了左边的筷子,等待右边的筷子,这个时候,都不会释放资源,都在等待资源,会产生死锁

发送死锁的条件:
1.互斥条件:任务资源至少有一个不能共享,这个一根筷子只能被一个哲学家使用
2.至少有一个任务持有一个资源,等待另一个任务持有的资源,哲学家拿着一根筷子,等待另一根筷子
3.资源不能被任务抢占,例如哲学家很有礼貌,不从别人手里抢筷子
4.必须循环等待

那么防止死锁,破坏一个条件即可,最容易的是破坏第四个条件,最容易的解决办法就是让最后一个哲学家先拿左边的筷子,再拿右边的,这样破坏了循环
等待的条件

到这里多线程的先关基础就总结完毕,有什么疑问请参考书java编程思想


快捷键

  • 加粗 Ctrl + B
  • 斜体 Ctrl + I
  • 引用 Ctrl + Q
  • 插入链接 Ctrl + L
  • 插入代码 Ctrl + K
  • 插入图片 Ctrl + G
  • 提升标题 Ctrl + H
  • 有序列表 Ctrl + O
  • 无序列表 Ctrl + U
  • -
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值