浅谈并发

目录

 

序言

线程

多线程三大特性

线程安全

Java和Go中的锁


序言

之前的一篇学习经验中也提到了并发,不过那个只是提到了很少的一部分内容,去讲并发一篇两篇根本讲不清楚,这里也只是粗略的讲一下我个人对并发的理解。

首先,我们要了解一下为什么要有并发。早期计算机没有操作系统只是纯粹用来计算的,一个程序从头到尾顺序执行,且可以访问计算机所有资源。然而这样造成极大资源浪费且效率不高。后面有了操作系统可以使计算机每次运行多个程序,操作系统为它们分配资源。

我们再看看百度百科对并发(Concurrent)的定义:并发,即在操作系统中,同一时间段有几个程序处于已启动运行到运行完毕之间的阶段,且这几个程序都在同一台处理机上,但任何一个时间点只有一个程序在处理机上运行。有人可能会说了如果一个计算机有多核CPU呢?但此时这种情况多核CPU上执行的线程互不抢占资源,这种称之为并行(Parallel)。

  • 并发(concurrency):把任务在不同的时间点交给处理器进行处理。在同一时间点,任务并不会同时运行。
  • 并行(parallelism):把每一个任务分配给每一个处理器独立完成。在同一时间点,任务一定是同时运行。

线程

说到并发,我们就不得不去了解线程。一个程序里可以有一个进程也可以有多个进程,而一个进程至少有一个线程。进程是任务调度的最小单位,每个进程各自都独立一块内存(32位机,单个程序最多使用2G内存)。线程是程序执行流的最小单位,是处理器调度和分派的最小单位,线程的栈要预留1M的内存空间。那有人可能要问一个进程最多是不是可以开辟2048个线程(2*1024M/1M)?但内存不可能全部拿来作线程的栈,肯定比这个值要小,不过我们也可以修改默认线程堆栈的大小(-Xss)来开辟更多的线程,但单个的线程的栈空间就小很多了。

Java开发中必须要了解的线程类Thread,看源码(Jdk1.8):

package java.lang;

import java.lang.ref.Reference;
import java.lang.ref.ReferenceQueue;
import java.lang.ref.WeakReference;
import java.security.AccessController;
import java.security.AccessControlContext;
import java.security.PrivilegedAction;
import java.util.Map;
import java.util.HashMap;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.locks.LockSupport;
import sun.nio.ch.Interruptible;
import sun.reflect.CallerSensitive;
import sun.reflect.Reflection;
import sun.security.util.SecurityConstants;

//JDK1.8的Thread类实现了Runnable接口
public class Thread implements Runnable {

    //定义了静态的本地方法,Object类、System类、Class类、ClassLoader类、Unsafe类等均有
    //registerNatives()方法C++源码实现,注册本地方法。Java程序需要调用一个本地应用提供的方法时,因为虚拟机只会检索本地动态库,因而虚拟机是无法定位到本地方法实现的,这个时候就只能使用registerNatives()方法进行主动链接
    private static native void registerNatives();
    //静态代码块,在类加载时执行一次
    static {
        registerNatives();
    }

    private volatile String name;
    private int            priority;
    private Thread         threadQ;
    private long           eetop;

    private boolean     single_step;

    private boolean     daemon = false;

    private boolean     stillborn = false;

    private Runnable target;

    private ThreadGroup group;

    private ClassLoader contextClassLoader;

    private AccessControlContext inheritedAccessControlContext;

    private static int threadInitNumber;

    private static synchronized int nextThreadNum() {
        return threadInitNumber++;
    }
    //ThreadLocal存储数据总是与当前线程相关,ThreadLocal类里的静态内部类ThreadLocalMap是维护本地线程值的自定义散列表,表中保存着弱引用的Entry(ThreadLocal,Object)
    ThreadLocal.ThreadLocalMap threadLocals = null;

    //ThreadLocal类是不能提供子线程访问父线程的本地变量,所以这个散列表通过InheritableThreadLocal类进行维护
    ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;
    
    //线程的初始化栈大小
    private long stackSize;

    //本地线程终止后保留JVM私有状态
    private long nativeParkEventPointer;

    //线程ID
    private long tid;
    
    //用来生成线程ID
    private static long threadSeqNumber;

    //线程状态:新建状态(New);就绪状态(Runnable);运行状态(Running);阻塞状态(Blocked);死亡状态(Dead)
    private volatile int threadStatus = 0;

    
    private static synchronized long nextThreadID() {
        return ++threadSeqNumber;
    }

    //指出当前线程是在哪个对象上阻塞,通过线程工具类LockSupport设置和获取
    volatile Object parkBlocker;

    //只有线程在进行I/O操作的时候,才需要用到这个锁
    private volatile Interruptible blocker;
    private final Object blockerLock = new Object();

    void blockedOn(Interruptible b) {
        synchronized (blockerLock) {
            blocker = b;
        }
    }
    //线程最小优先级
    public final static int MIN_PRIORITY = 1;

    //线程默认优先级
    public final static int NORM_PRIORITY = 5;

    //线程最大优先级
    public final static int MAX_PRIORITY = 10;

    //返回当前线程对象
    public static native Thread currentThread();
    //yield方法可以调度当前线程让步cpu资源
    public static native void yield();

    //执行线程休眠任务
    public static native void sleep(long millis) throws InterruptedException;

    //sleep重载(Overload)方法
    public static void sleep(long millis, int nanos)
    throws InterruptedException {
        if (millis < 0) {
            throw new IllegalArgumentException("timeout value is negative");
        }

        if (nanos < 0 || nanos > 999999) {
            throw new IllegalArgumentException(
                                "nanosecond timeout value out of range");
        }

        if (nanos >= 500000 || (nanos != 0 && millis == 0)) {
            millis++;
        }

        sleep(millis);
    }

    //在访问控制上下文中初始化一个线程
    private void init(ThreadGroup g, Runnable target, String name,
                      long stackSize) {
        init(g, target, name, stackSize, null, true);
    }

    //重载初始化方法
    private void init(ThreadGroup g, Runnable target, String name,
                      long stackSize, AccessControlContext acc,
                      boolean inheritThreadLocals) {
        if (name == null) {
            throw new NullPointerException("name cannot be null");
        }

        this.name = name;

        Thread parent = currentThread();
        SecurityManager security = System.getSecurityManager();
        if (g == null) {
            /* Determine if it's an applet or not */

            /* If there is a security manager, ask the security manager
               what to do. */
            if (security != null) {
                g = security.getThreadGroup();
            }

            /* If the security doesn't have a strong opinion of the matter
               use the parent thread group. */
            if (g == null) {
                g = parent.getThreadGroup();
            }
        }

        /* checkAccess regardless of whether or not threadgroup is
           explicitly passed in. */
        g.checkAccess();

        /*
         * Do we have the required permissions?
         */
        if (security != null) {
            if (isCCLOverridden(getClass())) {
                security.checkPermission(SUBCLASS_IMPLEMENTATION_PERMISSION);
            }
        }

        g.addUnstarted();

        this.group = g;
        this.daemon = parent.isDaemon();
        this.priority = parent.getPriority();
        if (security == null || isCCLOverridden(parent.getClass()))
            this.contextClassLoader = parent.getContextClassLoader();
        else
            this.contextClassLoader = parent.contextClassLoader;
        this.inheritedAccessControlContext =
                acc != null ? acc : AccessController.getContext();
        this.target = target;
        setPriority(priority);
        if (inheritThreadLocals && parent.inheritableThreadLocals != null)
            this.inheritableThreadLocals =
                ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);
        /* Stash the specified stack size in case the VM cares */
        this.stackSize = stackSize;

        /* Set thread ID */
        tid = nextThreadID();
    }

    //clone方法会抛出克隆不支持异常,因为无法对线程进行有意义的克隆
    @Override
    protected Object clone() throws CloneNotSupportedException {
        throw new CloneNotSupportedException();
    }

    //Thread类的构造方法
    public Thread() {
        init(null, null, "Thread-" + nextThreadNum(), 0);
    }

    //Thread类的有参构造方法
    public Thread(Runnable target) {
        init(null, target, "Thread-" + nextThreadNum(), 0);
    }

    //本包同类子类可以访问的有参构造方法
    Thread(Runnable target, AccessControlContext acc) {
        init(null, target, "Thread-" + nextThreadNum(), 0, acc, false);
    }

    //Thread类的有参构造方法
    public Thread(ThreadGroup group, Runnable target) {
        init(group, target, "Thread-" + nextThreadNum(), 0);
    }

    public Thread(String name) {
        init(null, null, name, 0);
    }

    public Thread(ThreadGroup group, String name) {
        init(group, null, name, 0);
    }

    public Thread(Runnable target, String name) {
        init(null, target, name, 0);
    }

    public Thread(ThreadGroup group, Runnable target, String name) {
        init(group, target, name, 0);
    }

    public Thread(ThreadGroup group, Runnable target, String name,
                  long stackSize) {
        init(group, target, name, stackSize);
    }

    //线程start方法使线程从创建态到就绪态,等待获取CPU资源
    public synchronized void start() {
        if (threadStatus != 0)
            throw new IllegalThreadStateException();
        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 */
            }
        }
    }

    private native void start0();
    
    //run方法使线程从就绪态到运行态
    @Override
    public void run() {
        if (target != null) {
            target.run();
        }
    }

    //系统让线程在退出前晴空
    private void exit() {
        if (group != null) {
            group.threadTerminated(this);
            group = null;
        }
        target = null;
        threadLocals = null;
        inheritableThreadLocals = null;
        inheritedAccessControlContext = null;
        blocker = null;
        uncaughtExceptionHandler = null;
    }

    //过时的stop方法,保留一般为兼容以前版本
    @Deprecated
    public final void stop() {
        SecurityManager security = System.getSecurityManager();
        if (security != null) {
            checkAccess();
            if (this != Thread.currentThread()) {
                security.checkPermission(SecurityConstants.STOP_THREAD_PERMISSION);
            }
        }
        if (threadStatus != 0) {
            resume(); // Wake up thread if it was suspended; no-op otherwise
        }
        stop0(new ThreadDeath());
    }

    //过时的stop方法
    @Deprecated
    public final synchronized void stop(Throwable obj) {
        throw new UnsupportedOperationException();
    }

    //线程中断
    public void interrupt() {
        if (this != Thread.currentThread())
            checkAccess();

        synchronized (blockerLock) {
            Interruptible b = blocker;
            if (b != null) {
                interrupt0();           // Just to set the interrupt flag
                b.interrupt(this);
                return;
            }
        }
        interrupt0();
    }

    //判断线程是否发生中断方法
    public static boolean interrupted() {
        return currentThread().isInterrupted(true);
    }

    //测试某线程是否中断
    public boolean isInterrupted() {
        return isInterrupted(false);
    }

    private native boolean isInterrupted(boolean ClearInterrupted);

    //过时的线程销毁方法,直接抛出没有此方法异常
    @Deprecated
    public void destroy() {
        throw new NoSuchMethodError();
    }

    //测试线程是否存活方法
    public final native boolean isAlive();

    //过时的线程挂起方法
    @Deprecated
    public final void suspend() {
        checkAccess();
        suspend0();
    }

    //过时的恢复挂起的线程方法
    @Deprecated
    public final void resume() {
        checkAccess();
        resume0();
    }

    //设置优先级
    public final void setPriority(int newPriority) {
        ThreadGroup g;
        checkAccess();
        if (newPriority > MAX_PRIORITY || newPriority < MIN_PRIORITY) {
            throw new IllegalArgumentException();
        }
        if((g = getThreadGroup()) != null) {
            if (newPriority > g.getMaxPriority()) {
                newPriority = g.getMaxPriority();
            }
            setPriority0(priority = newPriority);
        }
    }

    //获取优先级
    public final int getPriority() {
        return priority;
    }

    //设置线程name
    public final synchronized void setName(String name) {
        checkAccess();
        if (name == null) {
            throw new NullPointerException("name cannot be null");
        }

        this.name = name;
        if (threadStatus != 0) {
            setNativeName(name);
        }
    }

    //返回线程name
    public final String getName() {
        return name;
    }

    //获取线程组
    public final ThreadGroup getThreadGroup() {
        return group;
    }

    //返回本地活动线程数
    public static int activeCount() {
        return currentThread().getThreadGroup().activeCount();
    }

    //将每个活动线程的线程组及其子组复制到指定的数组中
    public static int enumerate(Thread tarray[]) {
        return currentThread().getThreadGroup().enumerate(tarray);
    }

    //过时的统计被挂起的线程的堆栈帧数
    @Deprecated
    public native int countStackFrames();

    //join方法是指调用线程等待该线程完成后,才能继续运行
    public final synchronized void join(long millis)
    throws InterruptedException {
        long base = System.currentTimeMillis();
        long now = 0;

        if (millis < 0) {
            throw new IllegalArgumentException("timeout value is negative");
        }

        if (millis == 0) {
            while (isAlive()) {
                wait(0);
            }
        } else {
            while (isAlive()) {
                long delay = millis - now;
                if (delay <= 0) {
                    break;
                }
                wait(delay);
                now = System.currentTimeMillis() - base;
            }
        }
    }

    //join重载方法
    public final synchronized void join(long millis, int nanos)
    throws InterruptedException {

        if (millis < 0) {
            throw new IllegalArgumentException("timeout value is negative");
        }

        if (nanos < 0 || nanos > 999999) {
            throw new IllegalArgumentException(
                                "nanosecond timeout value out of range");
        }

        if (nanos >= 500000 || (nanos != 0 && millis == 0)) {
            millis++;
        }

        join(millis);
    }

    public final void join() throws InterruptedException {
        join(0);
    }

    //打印该堆栈跟踪信息,该方法只在debug中使用
    public static void dumpStack() {
        new Exception("Stack trace").printStackTrace();
    }

    //设置守护线程,只有start后的线程才可以调用。守护线程会依赖于创建它的线程,会随着创建它的线程死亡而死亡
    public final void setDaemon(boolean on) {
        checkAccess();
        if (isAlive()) {
            throw new IllegalThreadStateException();
        }
        daemon = on;
    }

    //测试线程是否是守护线程
    public final boolean isDaemon() {
        return daemon;
    }

    //判断是否有修改当前运行的线程的权限
    public final void checkAccess() {
        SecurityManager security = System.getSecurityManager();
        if (security != null) {
            security.checkAccess(this);
        }
    }

    //返回线程字符串信息
    public String toString() {
        ThreadGroup group = getThreadGroup();
        if (group != null) {
            return "Thread[" + getName() + "," + getPriority() + "," +
                           group.getName() + "]";
        } else {
            return "Thread[" + getName() + "," + getPriority() + "," +
                            "" + "]";
        }
    }

    //返回此线程上下文类加载器
    @CallerSensitive
    public ClassLoader getContextClassLoader() {
        if (contextClassLoader == null)
            return null;
        SecurityManager sm = System.getSecurityManager();
        if (sm != null) {
            ClassLoader.checkClassLoaderPermission(contextClassLoader,
                                                   Reflection.getCallerClass());
        }
        return contextClassLoader;
    }

    //设置上下文类加载器
    public void setContextClassLoader(ClassLoader cl) {
        SecurityManager sm = System.getSecurityManager();
        if (sm != null) {
            sm.checkPermission(new RuntimePermission("setContextClassLoader"));
        }
        contextClassLoader = cl;
    }

    //判断线程是否持有锁
    public static native boolean holdsLock(Object obj);

    private static final StackTraceElement[] EMPTY_STACK_TRACE
        = new StackTraceElement[0];

    //返回线程堆栈跟踪信息集合
    public StackTraceElement[] getStackTrace() {
        if (this != Thread.currentThread()) {
            // check for getStackTrace permission
            SecurityManager security = System.getSecurityManager();
            if (security != null) {
                security.checkPermission(
                    SecurityConstants.GET_STACK_TRACE_PERMISSION);
            }
            // optimization so we do not call into the vm for threads that
            // have not yet started or have terminated
            if (!isAlive()) {
                return EMPTY_STACK_TRACE;
            }
            StackTraceElement[][] stackTraceArray = dumpThreads(new Thread[] {this});
            StackTraceElement[] stackTrace = stackTraceArray[0];
            // a thread that was alive during the previous isAlive call may have
            // since terminated, therefore not having a stacktrace.
            if (stackTrace == null) {
                stackTrace = EMPTY_STACK_TRACE;
            }
            return stackTrace;
        } else {
            // Don't need JVM help for current thread
            return (new Exception()).getStackTrace();
        }
    }

    //返回所有存活线程的堆栈信息集合
    public static Map<Thread, StackTraceElement[]> getAllStackTraces() {
        // check for getStackTrace permission
        SecurityManager security = System.getSecurityManager();
        if (security != null) {
            security.checkPermission(
                SecurityConstants.GET_STACK_TRACE_PERMISSION);
            security.checkPermission(
                SecurityConstants.MODIFY_THREADGROUP_PERMISSION);
        }

        // Get a snapshot of the list of all threads
        Thread[] threads = getThreads();
        StackTraceElement[][] traces = dumpThreads(threads);
        Map<Thread, StackTraceElement[]> m = new HashMap<>(threads.length);
        for (int i = 0; i < threads.length; i++) {
            StackTraceElement[] stackTrace = traces[i];
            if (stackTrace != null) {
                m.put(threads[i], stackTrace);
            }
            // else terminated so we don't put it in the map
        }
        return m;
    }


    private static final RuntimePermission SUBCLASS_IMPLEMENTATION_PERMISSION =
                    new RuntimePermission("enableContextClassLoaderOverride");

    //静态内部类Caches,缓存子类安全集合
    private static class Caches {
        /** cache of subclass security audit results */
        static final ConcurrentMap<WeakClassKey,Boolean> subclassAudits =
            new ConcurrentHashMap<>();

        /** queue for WeakReferences to audited subclasses */
        static final ReferenceQueue<Class<?>> subclassAuditsQueue =
            new ReferenceQueue<>();
    }

    //校验是否可以在不违反安全约束的情况下构造此线程实例
    private static boolean isCCLOverridden(Class<?> cl) {
        if (cl == Thread.class)
            return false;

        processQueue(Caches.subclassAuditsQueue, Caches.subclassAudits);
        WeakClassKey key = new WeakClassKey(cl, Caches.subclassAuditsQueue);
        Boolean result = Caches.subclassAudits.get(key);
        if (result == null) {
            result = Boolean.valueOf(auditSubclass(cl));
            Caches.subclassAudits.putIfAbsent(key, result);
        }

        return result.booleanValue();
    }

    //对指定子类执行反射检查它是否重写安全敏感的非最终方法
    private static boolean auditSubclass(final Class<?> subcl) {
        Boolean result = AccessController.doPrivileged(
            new PrivilegedAction<Boolean>() {
                public Boolean run() {
                    for (Class<?> cl = subcl;
                         cl != Thread.class;
                         cl = cl.getSuperclass())
                    {
                        try {
                            cl.getDeclaredMethod("getContextClassLoader", new Class<?>[0]);
                            return Boolean.TRUE;
                        } catch (NoSuchMethodException ex) {
                        }
                        try {
                            Class<?>[] params = {ClassLoader.class};
                            cl.getDeclaredMethod("setContextClassLoader", params);
                            return Boolean.TRUE;
                        } catch (NoSuchMethodException ex) {
                        }
                    }
                    return Boolean.FALSE;
                }
            }
        );
        return result.booleanValue();
    }

    private native static StackTraceElement[][] dumpThreads(Thread[] threads);
    private native static Thread[] getThreads();

    //返回线程的Id(唯一标识)
    public long getId() {
        return tid;
    }

    //线程状态枚举值
    public enum State {
        NEW,    //线程还没有开启——创建态
        RUNNABLE,    //线程已开启,等待系统资源——就绪态
        BLOCKED,    //线程阻塞,等待获取监视锁
        WAITING,    //线程等待其他线程;通常是因为调用了不带时限的Object.wait方法、不带时限的Thread.join方法以及LockSupport.park方法
        TIMED_WAITING,    //线程处于计时等待状态;通常是因为调用了Thread.sleep方法、带时限的Object.wait方法、带时限的Thread.join方法、LockSupport.parkNanos方法以及LockSupport.parkUntil方法
        TERMINATED;    //线程中止状态
    }

    //获取当前线程状态
    public State getState() {
        return sun.misc.VM.toThreadState(threadStatus);
    }

    //函数式编程接口
    @FunctionalInterface
    public interface UncaughtExceptionHandler {
        void uncaughtException(Thread t, Throwable e);
    }

    private volatile UncaughtExceptionHandler uncaughtExceptionHandler;

    private static volatile UncaughtExceptionHandler defaultUncaughtExceptionHandler;

    把线程未捕获的异常交给未捕获异常处理器处理
    public static void setDefaultUncaughtExceptionHandler(UncaughtExceptionHandler eh) {
        SecurityManager sm = System.getSecurityManager();
        if (sm != null) {
            sm.checkPermission(
                new RuntimePermission("setDefaultUncaughtExceptionHandler")
                    );
        }

         defaultUncaughtExceptionHandler = eh;
     }

    public static UncaughtExceptionHandler getDefaultUncaughtExceptionHandler(){
        return defaultUncaughtExceptionHandler;
    }

    public UncaughtExceptionHandler getUncaughtExceptionHandler() {
        return uncaughtExceptionHandler != null ?
            uncaughtExceptionHandler : group;
    }

    public void setUncaughtExceptionHandler(UncaughtExceptionHandler eh) {
        checkAccess();
        uncaughtExceptionHandler = eh;
    }

    private void dispatchUncaughtException(Throwable e) {
        getUncaughtExceptionHandler().uncaughtException(this, e);
    }

    //删除map集合中特殊引用队列
    static void processQueue(ReferenceQueue<Class<?>> queue,
                             ConcurrentMap<? extends
                             WeakReference<Class<?>>, ?> map)
    {
        Reference<? extends Class<?>> ref;
        while((ref = queue.poll()) != null) {
            map.remove(ref);
        }
    }
    //静态内部类WeakClassKey保存弱引用对象
    static class WeakClassKey extends WeakReference<Class<?>> {
        private final int hash;
        WeakClassKey(Class<?> cl, ReferenceQueue<Class<?>> refQueue) {
            super(cl, refQueue);
            hash = System.identityHashCode(cl);
        }

        @Override
        public int hashCode() {
            return hash;
        }

        @Override
        public boolean equals(Object obj) {
            if (obj == this)
                return true;

            if (obj instanceof WeakClassKey) {
                Object referent = get();
                return (referent != null) &&
                       (referent == ((WeakClassKey) obj).get());
            } else {
                return false;
            }
        }
    }


    //使用Contended注解来避免伪共享,一个缓存行(cache line)64字节,通过对对象或字段进行补齐(padding)避免相邻扇区预取导致的伪共享冲突
    //本地线程随机种子
    @sun.misc.Contended("tlr")
    long threadLocalRandomSeed;

    //本地线程随机偏移量(默认0)
    @sun.misc.Contended("tlr")
    int threadLocalRandomProbe;

    //二级种子
    @sun.misc.Contended("tlr")
    int threadLocalRandomSecondarySeed;

    /* Some private helper methods */
    private native void setPriority0(int newPriority);
    private native void stop0(Object o);
    private native void suspend0();
    private native void resume0();
    private native void interrupt0();
    private native void setNativeName(String name);
}

Java高并发编程详解(汪文君版)里定义了五种状态——NEW;RUNNBALE;RUNNING;BLOCKED;TERMINATED,如下图,

了解这五种状态是了解线程的基础。线程未启动时是NEW状态,当调用线程的start方法,线程从NEW状态——>RUNNABLE状态。如果一个线程调用两次start方法,那么它会在第二次调用start方法抛出IllegalThreadStateException,因为start方法开始就做了线程状态校验,对于一个开发者而言,边缘检测、状态检测是最基本的知识。线程此时处于就绪态(Java虚拟机中执行的线程),此时等待CPU的资源,当得到CPU资源或者说拿到CPU执行权后JVM调用run方法,此时线程就从RUNNABLE状态变成RUNNING状态。当调用Object.wait方法,线程状态就变成BLOCKED状态,此时线程等待监视锁。如果是调用的不带超时参数的Object.wait方法、不带超时参数的Thread.join方法、LockSupport.park方法,线程会进入WAITING状态,如果要恢复到原来的状态需要调用Object.notify方法、LockSupport.unpark方法。如果调用Thread.sleep方法、带超时参数的Object.wait方法、带超时参数的Thread.join方法、LockSupport.parkNanos方法、LockSupport.parkUntil方法,则此线程会进入TIMED_WAITING状态。当线程run方法调用结束后线程进入TERMINATED状态。下面引用黄文海的Java多线程编程实战指南的一个图,它比较好的展现了状态流转的情况:

在Java jdk1.8.0.221版本的Thread类定义,线程定义了六种状态,这些是虚拟机状态,并没有反应操作系统线程状态。jdk在线文档链接:https://tool.oschina.net/apidocs/apidoc?api=jdk-zh

 

使用线程可以更好的发挥处理器的能力,但也同时会带来一些安全问题和性能问题。为了解决线程安全问题,我们会使用到同步容器,如Collections.synchronizedXXX(XXX代表List、Map或Set)、Vector、Hashtable、ConcurrentHashMap、CopyOnWriteArrayList、BlockingQueue等。同步容器如何实现线程安全的呢?比如Collections.synchroinzedXXX,这几个集合实现同步是通过线程安全的互斥对象mutex进行同步的。再比如ConcurrentHashMap,我这里引用一个别人的文章,我个人觉得他写的比较详细,直接放链接了:https://blog.csdn.net/qq_41737716/article/details/90549847

互斥对象加了final关键字类似于synchronized的互斥锁。synchronized的实现原理是JVM通过进入、退出对象监视器(Monitor)来实现对方法、同步代码块的同步,而对象监视器的本质依赖于底层操作系统的互斥锁(Mutex)来实现。后面的jdk版本对synchronized进行了优化,如适应性自旋(Adaptive Spinning)、锁消除(Lock Elimination)、锁膨胀(Lock Coarsening)等。自旋是为了避免频繁的切换用户态和内核态造成的资源浪费(线程挂起和线程恢复需要在内核态完成),适应性自旋是在自旋的基础上自旋时间不固定,时间由前一次在同一个锁上的自旋时间及锁的拥有者状态决定。锁消除则是JVM在JIT编译时去除不可能存在共享资源竞争的锁,如StringBuilder.append方法。锁的膨胀升级过程通过是锁的竞争加剧升级锁,由无锁——>偏向锁——>轻量级锁——>重量级锁,这个过程会修改对象头中的标志位(1bit是否是偏向锁、2bit锁标志位)。

下面看个栗子,包含对象锁/方法锁控制同步方法之间的同步,或者用类锁控制静态方法或静态变量互斥量之间的同步。并比较一下这四种方法:

a方法相比于b方法多了static关键字,static关键字可以用来修饰内部类、代码块、方法、成员变量。静态内部类不需要将内部类的实例对象绑定到外部类的实例对象上,它属于外部类而非属于外部类的对象,但它只能访问外部类的静态方法和静态成员变量;静态代码块在JVM加载类时就执行了该代码;静态方法和静态成员变量可以直接通过类名访问。

b方法加了synchronized关键字,监视器锁定的是调用当前方法的对象,监视器保证同一时刻只有一个线程在监视器区域执行。

c方法是在静态方法中给Demo类对应的Class对象加锁,static synchronized修饰的a方法也是锁定的Demo.class对象,所以a方法和c方法是等价的。

d方法是对this加了synchronized关键字,锁定的对象是调用当前方法的对象,所以d方法和b方法也是等价的。

//比较4个synchronized关键字修饰的方法及对象
public class Demo {

    //无返回值的静态方法加上synchronized关键字
    public synchronized static void a(){
        //TODO
    }
    //无返回值的方法加上synchronized关键字
    public synchronized void b(){
        //TODO
    }
    //对类对象加synchronized关键字
    public static void c(){
        synchronized (Demo.class){
            //TODO
        }
    }
    //对this加synchronized关键字
    public void d(){
        synchronized (this){
            //TODO
        }
    }
}

多线程三大特性

  1. 原子性:简而言之,一个操作或多个操作要么都执行,要么都不执行。原子性可通过锁、synchronized关键字来保证。

  2. 可见性:多个线程访问同一个变量时,一个线程修改了变量的值,其他线程都能看到修改的值。可见性可通过volatile关键字保证。

  3. 有序性:程序执行顺序按照代码的先后顺序执行。处理器为提高执行效率,可能会对代码进行优化,从而导致指令重排序。

其他,happens-before:JMM可以通过happens-before关系向程序员提供跨线程的内存可见性保证(如果A线程的写操作a与B线程的读操作b之间存在happens-before关系,尽管a操作和b操作在不同的线程中执行,但JMM向程序员保证a操作将对b操作可见)。

线程安全

在拥有共享数据的多条线程并行执行的程序中,线程安全的代码会通过同步机制保证各个线程都可以正常且正确的执行,不会出现数据污染等意外情况。可重入(Reentrant)函数必定线程安全,线程安全的不一定可重入。不可重入函数,函数调用结果不具有可再现性,可以通过互斥锁等机制使之能安全地同时被多个线程调用,那么,这个不可重入函数就是转换成了线程安全。

Java和Go中的锁

在这我们可以看一下Java的concurrent.locks包下的ReentrantLock。synchronized和ReentrantLock都是可重入的;synchronized不可响应中断,线程获取不到锁就一直等着,而ReentrantLock可以响应中断。下面给出ReentrantLock源码(jdk1.8),

package java.util.concurrent.locks;
import java.util.concurrent.TimeUnit;
import java.util.Collection;

public class ReentrantLock implements Lock, java.io.Serializable {
    private static final long serialVersionUID = 7373984872572414699L;
    //同步器
    private final Sync sync;

    //利用AQS和CAS机制实现同步
    //AQS(AbstractQueuedSynchronizer)是将每一条请求共享资源的线程封装成一个CLH(Craig, Landin, and Hagersten locks)锁队列的一个结点(Node),来实现锁的分配。
    //其中CLH是基于链表的可扩展、高性能、公平的自旋锁
    abstract static class Sync extends AbstractQueuedSynchronizer {
        private static final long serialVersionUID = -5179523762034025860L;
        abstract void lock();

        final boolean nonfairTryAcquire(int acquires) {
            final Thread current = Thread.currentThread();
            int c = getState();
            if (c == 0) {
                if (compareAndSetState(0, acquires)) {
                    setExclusiveOwnerThread(current);
                    return true;
                }
            }
            else if (current == getExclusiveOwnerThread()) {
                int nextc = c + acquires;
                if (nextc < 0) // overflow
                    throw new Error("Maximum lock count exceeded");
                setState(nextc);
                return true;
            }
            return false;
        }

        protected final boolean tryRelease(int releases) {
            int c = getState() - releases;
            if (Thread.currentThread() != getExclusiveOwnerThread())
                throw new IllegalMonitorStateException();
            boolean free = false;
            if (c == 0) {
                free = true;
                setExclusiveOwnerThread(null);
            }
            setState(c);
            return free;
        }

        protected final boolean isHeldExclusively() {
            return getExclusiveOwnerThread() == Thread.currentThread();
        }

        final ConditionObject newCondition() {
            return new ConditionObject();
        }

        final Thread getOwner() {
            return getState() == 0 ? null : getExclusiveOwnerThread();
        }

        final int getHoldCount() {
            return isHeldExclusively() ? getState() : 0;
        }

        final boolean isLocked() {
            return getState() != 0;
        }

        private void readObject(java.io.ObjectInputStream s)
            throws java.io.IOException, ClassNotFoundException {
            s.defaultReadObject();
            setState(0); // reset to unlocked state
        }
    }
    //非公平锁:即看哪个线程运气好,cpu时间片给到哪个线程哪个线程先获得锁
    static final class NonfairSync extends Sync {
        private static final long serialVersionUID = 7316153563782823691L;

        final void lock() {
            if (compareAndSetState(0, 1))
                setExclusiveOwnerThread(Thread.currentThread());
            else
                acquire(1);
        }

        protected final boolean tryAcquire(int acquires) {
            return nonfairTryAcquire(acquires);
        }
    }
    //公平锁:等待队列里的线程,谁等待时间最长谁先获得锁
    static final class FairSync extends Sync {
        private static final long serialVersionUID = -3000897897090466540L;

        final void lock() {
            acquire(1);
        }

        protected final boolean tryAcquire(int acquires) {
            final Thread current = Thread.currentThread();
            int c = getState();
            if (c == 0) {
                if (!hasQueuedPredecessors() &&
                    compareAndSetState(0, acquires)) {
                    setExclusiveOwnerThread(current);
                    return true;
                }
            }
            else if (current == getExclusiveOwnerThread()) {
                int nextc = c + acquires;
                if (nextc < 0)
                    throw new Error("Maximum lock count exceeded");
                setState(nextc);
                return true;
            }
            return false;
        }
    }

    //默认非公平锁
    public ReentrantLock() {
        sync = new NonfairSync();
    }
    //有参构造函数,通过参数控制是否公平
    public ReentrantLock(boolean fair) {
        sync = fair ? new FairSync() : new NonfairSync();
    }

    public void lock() {
        sync.lock();
    }

    public void lockInterruptibly() throws InterruptedException {
        sync.acquireInterruptibly(1);
    }

    public boolean tryLock() {
        return sync.nonfairTryAcquire(1);
    }
    public boolean tryLock(long timeout, TimeUnit unit)
            throws InterruptedException {
        return sync.tryAcquireNanos(1, unit.toNanos(timeout));
    }
    public void unlock() {
        sync.release(1);
    }
    public Condition newCondition() {
        return sync.newCondition();
    }
    public int getHoldCount() {
        return sync.getHoldCount();
    }
    public boolean isHeldByCurrentThread() {
        return sync.isHeldExclusively();
    }
    public boolean isLocked() {
        return sync.isLocked();
    }
    public final boolean isFair() {
        return sync instanceof FairSync;
    }
    protected Thread getOwner() {
        return sync.getOwner();
    }
    public final boolean hasQueuedThreads() {
        return sync.hasQueuedThreads();
    }
    public final boolean hasQueuedThread(Thread thread) {
        return sync.isQueued(thread);
    }
    public final int getQueueLength() {
        return sync.getQueueLength();
    }
    protected Collection<Thread> getQueuedThreads() {
        return sync.getQueuedThreads();
    }
    public boolean hasWaiters(Condition condition) {
        if (condition == null)
            throw new NullPointerException();
        if (!(condition instanceof AbstractQueuedSynchronizer.ConditionObject))
            throw new IllegalArgumentException("not owner");
        return sync.hasWaiters((AbstractQueuedSynchronizer.ConditionObject)condition);
    }
    public int getWaitQueueLength(Condition condition) {
        if (condition == null)
            throw new NullPointerException();
        if (!(condition instanceof AbstractQueuedSynchronizer.ConditionObject))
            throw new IllegalArgumentException("not owner");
        return sync.getWaitQueueLength((AbstractQueuedSynchronizer.ConditionObject)condition);
    }
    protected Collection<Thread> getWaitingThreads(Condition condition) {
        if (condition == null)
            throw new NullPointerException();
        if (!(condition instanceof AbstractQueuedSynchronizer.ConditionObject))
            throw new IllegalArgumentException("not owner");
        return sync.getWaitingThreads((AbstractQueuedSynchronizer.ConditionObject)condition);
    }
    public String toString() {
        Thread o = sync.getOwner();
        return super.toString() + ((o == null) ?
                                   "[Unlocked]" :
                                   "[Locked by thread " + o.getName() + "]");
    }
}

上面是一个可重入锁,我们再对比看看go语言中的非重入锁Mutex,它是一个互斥锁(Mutual Excusion),任何时间只允许一个goroutine在临界区域运行。零值为解锁状态,Mutex类型的锁和线程无关,可以由不同的线程加锁和解锁。下面看sync包下的mutex源码(go版本1.15.7),

package sync

import (
	"internal/race"
	"sync/atomic"
	"unsafe"
)

func throw(string) // provided by runtime

//定义互斥锁结构体,两个成员变量:状态(标志锁是否被持有)和信号量(阻塞和唤醒goroutine)
type Mutex struct {
	state int32
	sema  uint32
}

//Locker接口,包含两个函数——加锁、解锁
type Locker interface {
	Lock()
	Unlock()
}

//定义常量,iota为常量计数器,初始值为0
const (
    //1 << iota表示0左移一位,后面每新增一行常量都会左移一位,直到重新赋值
	mutexLocked = 1 << iota // mutex is locked
	mutexWoken              // 默认为2
	mutexStarving           // 默认为4
	mutexWaiterShift = iota // 默认为3

	starvationThresholdNs = 1e6    //1e6纳秒=1ms,即等待锁时间超过阈值进入饥饿模式
)

//互斥量m 加锁函数
func (m *Mutex) Lock() {
	// sync/atomic包下的CompareAndSwapInt32原子性的比较&m.state和0,如果相同则将mutexLocked赋值给&m.state并返回真。
	if atomic.CompareAndSwapInt32(&m.state, 0, mutexLocked) {
        //internal/race包为go内部包
		if race.Enabled {
            //获取m中非安全的指针
			race.Acquire(unsafe.Pointer(m))
		}
		return
	}
    //如果锁处于Locked状态,则调用lockSlow函数
	m.lockSlow()
}

func (m *Mutex) lockSlow() {
	var waitStartTime int64    //记录goroutine的等待时间
	starving := false          //是否处于饥饿状态标志
	awoke := false             //是否处于唤醒状态标志
	iter := 0                  //自旋次数
	old := m.state             //记录当前状态
	for {
		//判断 old的状态,如果是饥饿状态就不会自旋, runtine_canSpin 会判断当前系统环境是否支持自旋
		if old&(mutexLocked|mutexStarving) == mutexLocked && runtime_canSpin(iter) {
			//设置唤醒状态,因为此时已唤醒
			if !awoke && old&mutexWoken == 0 && old>>mutexWaiterShift != 0 &&
				atomic.CompareAndSwapInt32(&m.state, old, old|mutexWoken) {
				awoke = true
			}
			runtime_doSpin()
			iter++
			old = m.state
			continue
		}
		new := old
        //如果不是饥饿状态,新的goroutine设置锁;如果是饥饿状态,不设置锁,因为直接会把goroutine放到等待队列 
		if old&mutexStarving == 0 {
			new |= mutexLocked
		}
        //饥饿状态下等待队列数量加1 
		if old&(mutexLocked|mutexStarving) != 0 {
			new += 1 << mutexWaiterShift
		}
		//如果当前goroutine处于饥饿状态(等待时间超过1ms)那么把锁也置为饥饿状态
		if starving && old&mutexLocked != 0 {
			new |= mutexStarving
		}
		if awoke {
			//如果处于唤醒状态但是没有设置唤醒标记位,抛异常
			if new&mutexWoken == 0 {
				throw("sync: inconsistent mutex state")
			}
            //新状态需要清除唤醒状态标记位,因为此goroutine要么获取锁,要么休眠
			new &^= mutexWoken
		}
        //通过CAS操作更新锁的状态
		if atomic.CompareAndSwapInt32(&m.state, old, new) {
            //如果当前锁处于为mutexLocked和mutexStarving都为0,说明该goroutine可以直接获取锁,此时直接返回
			if old&(mutexLocked|mutexStarving) == 0 {
				break // locked the mutex with CAS
			}
			//计算goroutine的等待时间
			queueLifo := waitStartTime != 0
			if waitStartTime == 0 {
				waitStartTime = runtime_nanotime()
			}
            // 未获取到锁,queueLife = true,放到等待队列头部;queueLife = false,放到等待队列尾部
			runtime_SemacquireMutex(&m.sema, queueLifo, 1)
            //计算饥饿状态的变量
			starving = starving || runtime_nanotime()-waitStartTime > starvationThresholdNs
			old = m.state
            //如果锁处于饥饿状态
			if old&mutexStarving != 0 {
				// 如果goroutine被唤醒或者锁是饥饿模式,抛异常
				if old&(mutexLocked|mutexWoken) != 0 || old>>mutexWaiterShift == 0 {
					throw("sync: inconsistent mutex state")
				}
				delta := int32(mutexLocked - 1<<mutexWaiterShift)
                //如果此时不为饥饿状态或者此goroutine为队列最后一个,退出饥饿状态
				if !starving || old>>mutexWaiterShift == 1 {
					delta -= mutexStarving
				}
				atomic.AddInt32(&m.state, delta)
				break
			}
            //如果锁处于正常状态,此goroutine获取锁,自旋次数置0
			awoke = true
			iter = 0
		} else {
			old = m.state
		}
	}
	if race.Enabled {
		race.Acquire(unsafe.Pointer(m))
	}
}

//解锁函数
func (m *Mutex) Unlock() {
	if race.Enabled {
		_ = m.state
		race.Release(unsafe.Pointer(m))
	}
	new := atomic.AddInt32(&m.state, -mutexLocked)
    //如果new!=0说明有其他goroutine在等待锁
	if new != 0 {
		m.unlockSlow(new)
	}
}

//unlockSlow函数
func (m *Mutex) unlockSlow(new int32) {
	if (new+mutexLocked)&mutexLocked == 0 {
		throw("sync: unlock of unlocked mutex")
	}
	if new&mutexStarving == 0 {
		old := new
		for {
            //如果没有等待的goroutine或goroutine已经被唤醒/获得锁,则无需唤醒,直接返回
			if old>>mutexWaiterShift == 0 || old&(mutexLocked|mutexWoken|mutexStarving) != 0 {
				return
			}
            //唤醒等待的goroutine,并且将等待者数量减1,并且设置标识
			new = (old - 1<<mutexWaiterShift) | mutexWoken
			if atomic.CompareAndSwapInt32(&m.state, old, new) {
				runtime_Semrelease(&m.sema, false, 1)
				return
			}
			old = m.state
		}
	} else {
		//处于饥饿状态时,则直接把锁给队列中的第一个goroutine
		runtime_Semrelease(&m.sema, true, 1)
	}
}

上面给出了ReentrantLock和Mutex,后面有兴趣还可以看看其他,如下:

读写锁:java.util.concurrent.locks.ReentrantReadWriteLock和sync包下的RWMutex。读写锁分读锁和写锁,同一时刻只有一个线程可以获得写锁,在没有写锁被线程获得时,读锁是可以被多个线程持有,但写锁优先级高于读锁,写锁会阻塞其他读写锁。读写锁适用于读操作多写操作少的情况。

线程同步协调:Java中java.util.concurrent包下的同步辅助类CountDownLatch、Semaphore、CyclicBarrier、Phaser、Exchanger和sync包下的WaitGroup。

线程条件控制:java.util.concurrent.locks.Condition和Con。

单例:sync包下的Once。

线程池:Java中Executors提供的四种线程池以及Go中sync包下的Pool函数。

线程让步:Java中Thread.yield方法和Go中runtime.Gosched()比较。

最大效率使用CPU:通过runtime包下的NumCPU函数读取当前CPU核数,然后使用runtime.GOMAXPROCS(runtime.NumCPU())让调度器最大使用CPU资源。(Go1.5版本之前默认单核,之后使用这段代码并发执行)

 

知识点看了再多,不用就容易忘记,如果没有高并发的使用场景,我们可以通过题目训练来掌握更多的知识,比如leetCode.1114题按序打印:https://leetcode-cn.com/problems/print-in-order/,leetCode.1226哲学家进餐:https://leetcode-cn.com/problems/the-dining-philosophers/

我下面给出的一个栗子是之前工作时遇到的场景,通过dubbo接口调用底层服务查非敏感数据并导出到excel表,如果一次性查找过多的表单,dubbo接口可能调用超时。这时通过多次分页查询去获取数据,当然这个是在页数较少的情况下,如果页数过多Mybatis-plus框架的selectPage方法效率会比较低。其中,一页查找的数量大概在500单左右。下面我给出这个case的代码方法实现(仅供参考,不是很好的实现,存在一些问题,能用而已,我描述一下具体使用情况,通常一次需要查找一个月的订单,业务低频一个月3000单左右,这里使用的是固定线程池,核心线程数和最大线程数都是5,这里的一次调用大概会有6、7个线程进来,超过5个线程后,其他任务会放入到队列里。如果线程数等于最大线程数且任务队列已满,则会拒绝处理任务而抛出异常,所以当一次查找的订单数过多时,还需要手动去改线程池的配置,这种方法不是很好):

Java代码实现:

package algorithm;

import lombok.Builder;
import lombok.Data;
import lombok.ToString;

import java.util.*;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class SelectPageDemo {
    @Builder
    @Data
    @ToString
    static class OrderInfo {
        private String orderCode;
        private Integer number;
    }

    //订单总数
    private static final int COUNT = 3000;
    //每页大小
    private static final int PAGE_SIZE = 500;
    //创建的固定线程池,阿里巴巴Java开发手册中提到,使用Executors创建线程池可能会导致OOM(OutOfMemory ,内存溢出)
    private static ExecutorService executorService = Executors.newFixedThreadPool(5);
    //获得唯一订单编号
    private static String getOrderCode(){
        //java.util.UUID为通用唯一识别码:当前时间+时钟序列+IEEE机器识别号,长度36
        return UUID.randomUUID().toString().substring(24);
    }

    public static void main(String[] args) {
        //最终结果集
        List<OrderInfo> result = new ArrayList<>();
        List<OrderInfo> dataList = Collections.synchronizedList(result);
        //页数
        int page_num = COUNT / PAGE_SIZE;
        //多线程处理
        CountDownLatch countDownLatch = new CountDownLatch(page_num);
        for (int i = 1; i <= page_num; i++) {
            int j = i;
            executorService.submit(() -> {
                //这里是从调用dubbo接口拿取底层服务的数据放到DTO对象里最后添加到结果集
                System.out.println("执行完第" + j + "批次任务");
                for (int k = 1; k <= PAGE_SIZE; k++) {
                    dataList.add(OrderInfo.builder().orderCode(getOrderCode()).number((j - 1) * PAGE_SIZE + k).build());
                }
                countDownLatch.countDown();
            });
        }
        executorService.shutdown();
        try {
            countDownLatch.await();
        } catch (Exception e) {
            e.printStackTrace();
        }

        //输出结果,根据OrderInfo对象的number字段排序输出
        result.sort(Comparator.comparing(o -> o.number));
        for (OrderInfo order : result) {
            System.out.println(order);
        }
    }
}

上面提到过使用Executors创建线程池可能会导致OOM,我们先看一下阿里的开发手册里对于Java线程池的使用规范说明:

【强制】线程池不允许使用Executors去创建,而是通过ThreadPoolExecutor的方式,这样的处理方式让写的同学更加明确线程池的运行规则,规避资源耗尽的风险。

说明:Executors返回的线程池对象的弊端如下:

  1. FixedThreadPool和SingleThreadPool:允许的请求队列长度为Integer.MAX_VALUE,可能会堆积大量的请求,从而导致OOM。
  2. CachedThreadPool和ScheduledThreadPool:允许的创建线程数量为Integer.MAX_VALUE,可能会创建大量的线程,从而导致OOM。

为什么会导致内存泄漏,可以参看一下线程池的可视化动画:https://zhuanlan.zhihu.com/p/112527671

一开始大家写的东西可能或多或少有些问题,所以尽可能去多了解一些自己存在的问题,看了之后,大家可能就直到怎么改了。java.util.concurrent包下的ThreadPoolExecutor里有4个静态内部类都实现了RejectedExecutionHandler接口,即四种线程池的拒绝策略:

  • AbortPolicy:线程池默认拒绝策略,当任务队列已满且线程数达到最大线程池数时会直接丢弃任务并抛出RejectedExecutionException异常。
  • CallerRunsPolicy:该策略在任务被拒绝添加到队列中后,会用调用execute函数的上层线程去执行被拒绝的任务。
  • DiscardPolicy:该策略在任务被拒绝后,不会抛异常也不会被执行。
  • DiscardOldestPolicy:该策略在任务被拒绝后,会抛弃任务队列中最先加入的任务,再把这个被拒绝的任务添加进去。

看了这些之后,线程池的创建应该按照下面这样写,这样自己跑测试的时候也能直到自己线程池的创建是否有问题,如下:

    //创建了一个核心线程数为5,最大线程数为5,空闲线程存活时间30s,等待队列大小10,拒绝策略默认丢弃
    private static ThreadPoolExecutor executor = new ThreadPoolExecutor(5,5,30,
            TimeUnit.SECONDS,
            new LinkedBlockingDeque<>(10),
            new ThreadPoolExecutor.AbortPolicy());

 

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值