• Java多线程技能


     下面进入Java多线程的学习,首先介绍Thread类的核心方法

    *   线程的启动

    *   线程的暂停   

    *   线程停止

    *   线程的优先级

    *   线程安全的相关问题

    一、进程

     要学习多线程就不得不提到进程,进程是什么,当我们打开windows系统的任务管理器里面运行着的.exe文件可以被看作是一个个进程。进程是受操作系统管理的基本运行单元。

    那什么是线程呢?

    线程是进程中独立运行的子任务。比如QQ.exe运行时有很多的子任务在运行,发QQ消息,下载文件,视频电话等,他们每一个任务可以看做一个线程在工作。这样做的优点是什么呢?可以最大限度的利用CPU空闲的时间去处理其他任务。 CPU不停地在这些任务之间切换,由于切换的速度非常快,给用户的感觉就像是几个任务在同时运行。所以使用多线程技术后,可以在同一时间内运行更多不同种类的任务。为了更好的了解多线程的优点我们可以看下图 ,了解一下单任务的缺点。

    在图1-3中,任务1和任务2是单独运行的,互不相关的任务。任务1是在等待远程的服务器返回数据,以便进行后期的处理,这时的CPU一直处在等待的状态下一直闲在那里。虽然任务2 的运行时间只有1秒,但是必须要等待任务1 运行完了之后10秒后才能运行任务2 ,那么他们花费的总时间就是11秒。本程序是在单任务运行环境中,所以任务2 有非常长时间的等待,系统运行效率大幅降低。单任务的特点就是排队执行,也就是同步。这样会导致CPU的利用率大幅降低。

    在图1-4中是多任务的运行环境下,CPU可以在任务1 和任务2 中任意切换,使得任务2 不必等到10秒之后再运行,运行的效率大大提升。

    这就是要是用的多线程技术也就是我们要学习多线程的原因。这是多线程的优点,使用多线程也就是在使用异步。

    注意 :多线程是异步的,千万不要把编译工具中代码的顺序当成线程执行的顺序,线程被调用的时机是随机的。

    二、使用多线程。

     一个进程在运行时至少有一个线程在运行,这种情况在Java中也是存在的。这些线程在后台默默的执行,比如说调用public static void main(String[] args)main方法的线程就是这样的,而且他是由jvm创建的。

    public class Test {
        public static void main(String[] args) {
            System.out.println(Thread.currentThread().getName());
            System.out.println(Thread.currentThread().getId());
        }
    }

    程序运行结果

    控制台输出的main其实上就是一个名称叫做main的线程在执行main()方法中的代码。在这里说明:控制台输出的main和main()方法没有任何的关系,只是名字相同而已。

      创建多线程的方法:

    在Java JDK中已经自带了对多线程技术的支持,可以很方便的进行多线程的编程。实现多线程的方法主要有两种,一一种是继承Thread类,一种是实现Runable接口。

    2.1  我们先看继承Thread类。首先我们先了解一下Thread 类的结构。

    从上源代码可以看出Thread类继承了Runable接口,他们之间具有多态关系。

    我们现在来创建一个类MyThread 继承Thread类,并且重写run()方法,在run()方法中写线程要执行任务的代码。

    public class MyThread extends Thread  {
        @Override
        public void run() {
            super.run();
            //在run()方法中写线程要执行任务的代码
            System.out.println("MyThread");
        }
    }

    写运行线程的代码:

    public class MyThreadTest  {
        public static void main(String[] args) {
            MyThread myThread =new MyThread();
            myThread.start();
            System.out.println("运行结束。。");
        }
    }

    运行结果:

    根据运行的结果,我们发现MyThread.java 中的run()方法执行的时间比较晚,这也说明在使用多线程技术的时候,代码的运行结果与代码的执行顺序或调用顺序无关。

    线程是一个子任务,CPU以随机的时间来调用线程中的run()方法,所以就会出现“运行结束”早输出,“MyThread”后输出的结果。

    注意 :如果多次调用start()方法就会出现异常:   Exception in thread "main" java.lang.IllegalThreadStateException。

    上面介绍了线程的调用是随机的,那么现在我们来演示一下线程的随机性。

    public class MyThread extends Thread  {
        @Override
        public void run() {
            super.run();
            //在run()方法中写线程要执行任务的代码
            try {
            for (int i =0;i<10;i++){
                int  time = (int)Math.random() * 1000;
                    Thread.sleep(time);
                System.out.println("run :"+Thread.currentThread().getName());
            }
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    创建运行类;

    public class MyThreadTest  {
        public static void main(String[] args) {
            try {
            MyThread myThread =new MyThread();
            myThread.setName("myThread");
            myThread.start();
            for (int i = 0;i<10 ;i++){
              int time  =(int) Math.random()*1000;
                Thread.sleep(time);
                System.out.println("main : "+Thread.currentThread().getName());
            }
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    运行的结果:

    在代码中使用随机数的形式使线程达到挂起的效果,从而表现CPU执行哪个线程具有不确定性。

    Thread.java 类中的start()方法通知“线程规划器”此线程准备好了,等待调用线程对象的run()方法。这个过程就是让系统安排个时间去调用Thread类的run()方法,就是使线程得到运行,启动线程,具有异步执行的效果。如果调用Thread类的run()方法就不是异步而是同步。那此线程对象就不是交给线程规划器来进行处理,而是有main主线程来调用run()方法,需等到run()方法中的代码执行完之后才能执行后面的代码。

    另外注意一点:执行start()方法的顺序不代表线程启动的顺序。

    package Test;
    
    public class MyThread3 extends Thread {
        private int i;
    
        public MyThread3(int i) {
            super();
            this.i = i;
        }
    
        @Override
        public void run( ) {
            super.run();
            System.out.println(i);
        }
    }
    package Test;
    
    public class MyThread3Test {
        public static void main(String[] args) {
             MyThread3 t1 =new MyThread3(1);
             MyThread3 t2 =new MyThread3(2);
             MyThread3 t3 =new MyThread3(3);
             MyThread3 t4 =new MyThread3(4);
             MyThread3 t5 =new MyThread3(5);
             MyThread3 t6 =new MyThread3(6);
             MyThread3 t7 =new MyThread3(7);
             MyThread3 t8 =new MyThread3(8);
             MyThread3 t9 =new MyThread3(9);
             MyThread3 t10 =new MyThread3(10);
             MyThread3 t11 =new MyThread3(11);
             MyThread3 t12 =new MyThread3(12);
             MyThread3 t13 =new MyThread3(13);
             MyThread3 t14 =new MyThread3(14);
             MyThread3 t15 =new MyThread3(15);
             t1.start();
             t2.start();
             t3.start();
             t4.start();
             t5.start();
             t6.start();
             t7.start();
             t8.start();
             t9.start();
             t10.start();
             t11.start();
             t12.start();
             t13.start();
             t14.start();
             t15.start();
        }
    }

    运行结果:

    程序运行后如上图所示。

     2.2   下面是线程的另外一种实现的方法:实现Runable接口。

    如果我们想创建一个线程但是他已经有了父类,就不能再继承Thread类了,因为Java不支持多继承,这时就可以用实现Runable接口的方法来应对。

    首先我们看一个用Runable接口实现的线程的示例代码:

    package runableTest;
    
    public class MyRunable implements Runnable {
        @Override
        public void run() {
            System.out.println("运行中。。。");
        }
    }
    package runableTest;
    
    public class Test {
        public static void main(String[] args) {
            MyRunable my = new MyRunable();
             Thread t =new Thread(my);
             t.start();
        }
    }

     为什么我们要这样写他的测试类呢。在下图Thread类的构造函数中,可以看到有两个是包含Runable接口的。说明构造函数支持传入Runable的对象。

    运行结果:

    使用继承Thread.java 的方式来开发多线程应用程序在设计上是具有局限性的,因为Java支持单继承,多实现。为了改变这种限制我们可以使用实现Runable接口的方式实现多线程。

    另外Thread.java 类底层也是实现了Runable接口。

    这也就意味着Thread类的构造函数不仅可以传入Runable接口对象,还可以传入Thread类的对象,这样做可以将Thread类的Run()方法交由其他线程来调用。

    2.3   实例变量与线程安全

    自定义的线程中的实例变量与其他线程有共享与不共享之分,这在多线程交互时是很重要的技术。

    (1)不共享数据的情况

    通过实例看不共享数据情况:

    package Test.共享;
    
    public class MyThread extends Thread {
        int count = 5;
    
        public MyThread(String name) {
            super(name);
            setName(name);
        }
    
        @Override
        public void run() {
            super.run();
            while(count >0){
                count--;
                System.out.println("由"+Thread.currentThread().getName()+"计算 :"+count);
            }
    
        }
    }
    package Test.共享;
    
    public class Test {
        public static void main(String[] args) {
            MyThread m1 = new MyThread("A");
            MyThread m2 = new MyThread("B");
            MyThread m3 = new MyThread("C");
            m1.start();
            m2.start();
            m3.start();
        }
    }

    运行结果:

    线程不共享的情况中:我们创建了三个线程,每个线程都有各自的count变量,每个线程各自减少各自的count变量的值。

    如果我们想让三个线程共同减少一个变量时,这种情况就是线程共享。

    线程共享的情况;如图

    比如投票时,就可以实现多线程可以同时处理同一个人的票数。

    package Test.共享;
    
    public class MyThread1 extends Thread {
    private int count = 5; 
        @Override
        public void run() {
            super.run();
            count--;
            System.out.println("由"+Thread.currentThread().getName()+"计算 :"+count);
        }
    }
    package Test.共享;
    
    public class Test1 {
        public static void main(String[] args) {
            MyThread1 thread =new MyThread1();
             Thread a =new Thread(thread,"A");
             Thread b =new Thread(thread,"B");
             Thread c =new Thread(thread,"C");
             Thread d =new Thread(thread,"D");
             Thread e =new Thread(thread,"E");
             a.start();
             b.start();
             c.start();
             d.start();
             e.start();
    
        }
    }
    package Test.共享;
    
    public class Test1 {
        public static void main(String[] args) {
            MyThread1 thread =new MyThread1();
             Thread a =new Thread(thread,"A");
             Thread b =new Thread(thread,"B");
             Thread c =new Thread(thread,"C");
             Thread d =new Thread(thread,"D");
             Thread e =new Thread(thread,"E");
             a.start();
             b.start();
             c.start();
             d.start();
             e.start();
    
        }
    }

    运行结果:

    如运行结果所示,线程A和线程B打印出的值都是3,说明A和B同时对Count进行处理,产生了“非线程安全”的问题。而我们想要得到的打印结果是依次递减的。

    在某些jvm中,i--的操作要分为3步:

    1. 取得原有i值.

    2.计算i-1.

    3.对i进行赋值。

    在这3个步骤中,如果有多个线程同时访问,那移动会出现非线程安全问题。

    这就是典型的销售场景。五个销售点同时出售火车票的问题。每一个销售员卖掉一张票之后,不可以得到相同的剩余数量。每个销售员卖出票进行减一操作后,其他销售员才可以在剩余的票数进行减一。这就需要使多个线程之间进行同步,也就是按顺序排队的方式进行减一的操作。更改代码。

    package Test.共享;
    
    public class MyThread1 extends Thread {
    private int count = 5;
        @Override
       synchronized public void run() {
            super.run();
            count--;
            System.out.println("由"+Thread.currentThread().getName()+"计算 :"+count);
        }
    }

    重新运行就不会出现count值相同的情况:

       在run()方法前加上synchronized关键字,使多个线程在执行run()方法时,以排队的方式进行处理。当一个线程调用run()前首先判断run()有没有被上锁。如果上锁说明正有其他线程调用run方法,需要等到run方法调用结束之后其他线程才可调用,这样实现了线程排队调用run方法,也达到了按顺序count值减一的效果。synchronized可以在任意方法,对象上加锁,而加锁的这段代码被称为“互斥区”或“临界区”。

       当一个线程想要执行同步方法里的代码时,线程首先去尝试拿这把锁,如果可以拿到,这个线程就可以执行synchronized里面的代码,如果不能拿到这把锁,这个线程将不断的尝试去拿这把锁,直到拿到为止。同时还会有多个线程去争抢这把锁。

    本节中出现了一个术语“非线程安全”。非线程安全主要是指多个线程对同一个对象中的同一个实例变量进行操作时会出现值被更改,值不同步的情况,进而影响程序的执行流程。接下来学习如何解决“非线程安全”问题。

    现在我们创建一个非线程安全的环境。

    package Test.线程安全;
    
    public class LoginServlet {
        private static String usernameRef;
        private static String passwordRef;
        public static void doPost(String username,String password)   {
                try {
                    usernameRef = username;
                    if (username.equals("kitty")) {
                        Thread.sleep(5000);
                    }
                    passwordRef = password;
                    System.out.println("username :"+usernameRef  + " password  :"+passwordRef);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
    
        }
    package Test.线程安全;
    
    public class ALogin extends Thread  {
        @Override
        public void run() {
             LoginServlet.doPost("kitty","Dai15713131313131");
        }
    }
    package Test.线程安全;
    
    public class BLogin extends Thread  {
        @Override
        public void run() {
             LoginServlet.doPost("guohang","guohang5212521521");
        }
    }
    package Test.线程安全;
    
    public class Run {
        public static void main(String[] args) {
            ALogin a =new ALogin();
            BLogin b =new BLogin();
            a.start();
            b.start();
        }
    }

    运行结果:

    这样我们可以看出,发生了线程安全的问题,那么结果“非线程安全”的方法也是加上synchronized关键字,更改代码如下:

    package Test.线程安全;
    
    public class LoginServlet {
        private static String usernameRef;
        private static String passwordRef;
        synchronized public static void doPost(String username,String password)   {
                try {
                    usernameRef = username;
                    if (username.equals("kitty")) {
                        Thread.sleep(5000);
                    }
                    passwordRef = password;
                    System.out.println("username :"+usernameRef  + " password  :"+passwordRef);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
    
        }

    运行结果:

    2.4  留意  i-- 与System.out.println() 的异常。

    在前面章节中,我们解决了非线程安全的时候使用synchronized关键字,接下来通过案例细化一下println()方法和 i++ 联合使用的时候有可能出现的另外一种异常。

    package Test.samesnm;
    
    public class MyThread extends Thread {
        private int i = 5;
        @Override
        public void run() {
            System.out.println("i="+ (i--)+"threadName"+Thread.currentThread().getName());
        }
    }
     package Test.samesnm;
    
    public class Test {
        public static void main(String[] args) {
             MyThread run =new MyThread();
             Thread t1 =new Thread(run);
             Thread t2 =new Thread(run);
             Thread t3 =new Thread(run);
             Thread t4 =new Thread(run);
             Thread t5 =new Thread(run);
    
             t1.start();
             t2.start();
             t3.start();
             t4.start();
             t5.start();
        }
    }

    运行结果:

    本实验的目的是:虽然你println()方法在内部是同步的,但i--的操作确实在进入println()之前发生的,所以有发生非线程安全问题的概率。

     

    所以,为了防止发生非线程安全的问题,还是一年继续使用同步方法。

    package Test.samesnm;
    
    public class MyThread extends Thread {
        private int i = 5;
        @Override
         synchronized  public void run() {
            System.out.println("i="+ (i--)+"threadName"+Thread.currentThread().getName());
        }
    }

     三 、currentThread()方法

    currentThread()方法可返回代码段正在被哪个线程调用的信息。通过下面的事例代码说明:
    package Test.currentThread方法;
    
    public class Run1 {
        public static void main(String[] args) {
            System.out.println(Thread.currentThread().getName());
        }
    }

    运行结果:

    结果说明:main()方法是被名为main的线程调用。

    继续

    package Test.currentThread方法;
    
    public class MyThread extends Thread{
        public MyThread() {
            System.out.println("构造方法的打印 :"+Thread.currentThread().getName());
        }
    
        @Override
        public void run() {
            super.run();
            System.out.println("run 方法的打印 :"+Thread.currentThread().getName());
        }
    }
    package Test.currentThread方法;
    
    public class Run2 {
        public static void main(String[] args) {
            MyThread t =new MyThread();
            t.start();
        }
    }

    运行结果:

    由上运行结果可以发现,MyThread.java 类的构造函数是被main线程调用的,而run方法是被名为Thread-0的线程调用的,run方法是自动调用的方法。

    更改Run2.java 的代码

     构造方法 和 run 方法 均被main主线程所调用。

    继续测试;

    package Test.currentThread方法;
    
    public class CountOperate extends Thread {
    
        public CountOperate() {
            System.out.println("CountOperate ---- begin--");
            System.out.println("Thread.currentThread().getName() = "+Thread.currentThread().getName());
            System.out.println("this.name = "+this.getName());
            System.out.println("CountOperate ---- end--");
        }
    
        @Override
        public void run() {
            System.out.println("run ---- begin--");
            System.out.println("Thread.currentThread().getName() = "+Thread.currentThread().getName());
            System.out.println("this.name = "+this.getName());
            System.out.println("run ---- end--");
        }
    }
    package Test.currentThread方法;
    
    public class CountOperateTest {
        public static void main(String[] args) {
    
            CountOperate countOperate =new CountOperate();
            Thread thread =new Thread(countOperate);
            thread.setName("kitty");
            thread.start();
        }
    }

    运行结果:

    四、isAive()方法

    方法isAive()判断当前线程是否处于活动状态。

    package Test.isAive方法;
    
    public class MyThread extends Thread {
        @Override
        public void run() {
            System.out.println("run ="+this.isAlive());
        }
    }
    package Test.isAive方法;
    
    public class MythreadTest {
        public static void main(String[] args) {
            MyThread t =new MyThread();
            System.out.println("begin =="+t.isAlive());
            t.start();
            System.out.println("end =="+t.isAlive());
        }
    }

    运行结果:

    方法isAIve()是测试线程是否处在活动状态。什么是活动状态呢。就是线程已经启动且尚未终止。线程处在正在运行或者是准备开始运行的状态,就认为线程是存活的。

    虽然这里输出的结果是true但是这个值是不确定的,输出true是因为线程还没有执行完毕。更改代码 。

    package Test.isAive方法;
    
    public class MythreadTest {
        public static void main(String[] args) {
            try {
                MyThread t =new MyThread();
                System.out.println("begin =="+t.isAlive());
                t.start();
                Thread.sleep(1000);
                System.out.println("end =="+t.isAlive());
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
    
        }
    }

    则输出的结果为:false,因为线程t 在一秒内已经执行完毕。

     另外,在使用isAlive()方法时,如果将线程对象以构造函数的方法传递给Thread对象进行Start()启动时,运行结果和前面事例是有差异的。造成这样的原因是来自于Thread.currentThread()和this的差异。

    package Test.isAive方法;
    
    public class CountOperateA extends Thread {
    
        public CountOperateA() {
            System.out.println("CountOperateA ---- begin--");
            System.out.println("CountOperateA -- Thread.currentThread().getName() = "+Thread.currentThread().getName());
            System.out.println("CountOperateA--- this.name = "+this.getName());
            System.out.println("CountOperateA---Thread.currentThread().isAlive() = "+Thread.currentThread().isAlive());
            System.out.println("CountOperateA----this.isAive = "+this.isAlive());
            System.out.println("CountOperateA ---- end--");
        }
    
        @Override
        public void run() {
            System.out.println("run ---- begin--");
            System.out.println("run === Thread.currentThread().getName() = "+Thread.currentThread().getName());
            System.out.println("run ===this.name = "+this.getName());
            System.out.println("run ===Thread.currentThread().isAlive() = "+Thread.currentThread().isAlive());
            System.out.println("run === this.isAive = "+this.isAlive());
            System.out.println("run ---- end--");
        }
    }
    package Test.isAive方法;
    
    import Test.currentThread方法.CountOperate;
    
    public class CountOperateTest {
        public static void main(String[] args) {
            CountOperateA countOperate =new CountOperateA();
            Thread thread =new Thread(countOperate);
            thread.setName("kitty");
            System.out.println("main begin==== isAive : "+Thread.currentThread().isAlive());
            thread.start();
            System.out.println("main end==== isAive : "+Thread.currentThread().isAlive());
        }
    }

    运行结果:

    五、sleep()方法。

    方法sleep()的作用是在指定的秒数内让当前“正在执行的线程”休眠(暂停执行),这个“正在执行的线程”是指this.currentThread()返回的线程。

    package Test.sleepff;
    
    public class MyThread extends Thread{
        @Override
        public void run() {
            try {
                System.out.println("run  threadname  "+this.currentThread().getName()+" begin");
                Thread.sleep(2000);
                System.out.println("run  threadname  "+this.currentThread().getName()+" end");
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
    
    
    package Test.sleepff;
    
    public class Test {
        public static void main(String[] args) {
            MyThread myThread = new MyThread();
            System.out.println("begin " + System.currentTimeMillis());
            myThread.run();
            System.out.println("end " + System.currentTimeMillis());
        }
    }
    
    

    直接调用run()方法程序运行结果:

    创建一个新的MyThread2.Java

    package Test.sleepff;
    
    public class MyThread2 extends Thread{
        @Override
        public void run() {
            try {
                System.out.println("run  threadname  "+this.currentThread().getName()+" begin = "+System.currentTimeMillis());
                Thread.sleep(2000);
                System.out.println("run  threadname  "+this.currentThread().getName()+" end = "+System.currentTimeMillis());
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
    package Test.sleepff;
    
    public class Test2 {
        public static void main(String[] args) {
            MyThread2 myThread = new MyThread2();
            System.out.println("begin " + System.currentTimeMillis());
            myThread.start();
            System.out.println("end " + System.currentTimeMillis());
        }
    }

    运行结果:

     由于main线程与Mythread2线程是异步执行的,所以首先打印的信息是begin和end。而MyThread2线程是随后运行的,在最后两行打印run  begin和run  end 的信息。

    六、getId()方法。

     

    package Test.getIdff;
    
    public class MyThreadTest{
        public static void main(String[] args) {
            System.out.println(Thread.currentThread().getName()+"  "+Thread.currentThread().getId());
        }
    }

     

    七、停止线程。

             停止线程是在多线程开发时很重要的技术点,掌握此技术可以对线程的停止进行有效的处理。使用Java内置支持多线程的类设计多线程应用是很常见的事情,但是如果处理不好就会导致超出预期的行为并且难以定位错误。本节讨论如何更好的停止一个线程。停止一个线程意味着在线程在处理完任务之前停掉正在做的操作,也就是放弃当前的操作。虽然这看起来非常简单,但是必须做好防范措施,以便达到预期的效果。停止一个线程可以使用Thread.stop()方法,但是最好不使用他,虽然他确实可以停止一个正在运行的线程,但是这个方法是不安全的,而且是已经被废弃的,在将来的Java版本中,这个方法将不可用活不被支持。

    大多数停止一个线程的操作使用Thread.interupt(),尽管方法的名称是“停止,中止”的意思,但是这个方法不会中止一个正在运行的线程,还需要加入一个判断才可以完成线程的停止。

    1)使用退出标志,使线程正常退出,也就是当run()方法完成后线程中止。

    2)使用stop方法强行终止线程,但是不推荐,因为stop和suspend及resume一样,都是作废过期的方法,使用他们可能产生不可预期的结果。

    3)使用interupt方法中断线程。

    这3个方法会在后面的章节进行介绍。

    7.1    停止不了的线程

    调用interrupt()方法来停止线程,但是interrupt()方法的使用效果并不像for+break语句那样,马上就停止循环。调用interrupt()方法仅仅是在当前线程中打了一个停止的标记,并不是真的停止线程。

    package Test.stop;
    
    public class MyThread extends Thread {
        @Override
        public void run() {
             for (int i =0;i<5000;i++){
                 System.out.println("i = "+(i+1));
             }
        }
    }
    package Test.stop;
    
    public class Test {
        public static void main(String[] args) {
            try {
                MyThread thread =new MyThread();
                thread.start();
                Thread.sleep(2000);
                thread.interrupt();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

     从运行结果看来interrupt()并没有停止线程。

    7.2   判断线程是否是停止状态。

     


  • 相关阅读:
    java连接Mysql数据库
    js数组的操作
    Eclipse安装flash builder4.6插件
    MyEclipse 7.5,MyEclipse 8.0到10不好安装FLEX插件了
    关于MyEclipse10的破解激活
    用PHP做Linux/Unix下守护进程
    Debugging Tip: “Disallowed Key Character” Error In CodeIgniter
    股指期货模拟系统
    几个基本的设计原则
    基于mirror driver的windows屏幕录像
  • 原文地址:https://www.cnblogs.com/cuixiaomeng/p/11384234.html
Copyright © 2020-2023  润新知