zoukankan      html  css  js  c++  java
  • JavaSE-24 多线程

    学习要点

    • 线程概述
    • Java中的多线程
    • 线程状态
    • 线程调度
    • 线程同步
    • 线程间通信

      

    线程概述

    1  进程

    进程就是应用程序的执行实例。

    进程特征:

    • 动态性:动态产生,动态消亡。进程启动,系统为其分配资源;进程关闭,系统释放其所占资源。
    • 并发性:与其他程序一同运行。注:一个CPU在某个给定的时刻只能处理一个程序。操作系统采用一定的策略(如时间片轮转)让多个进程交替使用CPU。
    • 独立性:独立运行,系统为其分配独立资源。

    2  线程

    进程内部的一个执行单元,他是程序中一个单一的顺序控制流程。

    线程特称:

    • 一个进程至少包含一个线程(主线程),他是进程的入口。一个进程不论有多少个线程,必须有一个主线程;一个线程至少要有一个父进程。
    • 线程可以有自己的堆栈、程序计数器和局部变量。
    • 线程与父进程的其他线程共享所有的全部资源。
    • 独立运行,采用抢占方式。
    • 一个线程可以创建和删除另外一个线程。
    • 同一个进程中的多个线程之间可以并发执行。
    • 线程的调度管理是由进程来完成的。

    3  进程和线程的关系

     

    4  多线程编程原则

    确保线程不会妨碍同一进程的其他线程。

    5  线程的分类

    • 系统级线程:又称为核心级线程,负责管理调度不同进程之间的多个线程,由操作系统直接管理。
    • 用户级线程:仅存在于用户空间,在应用程序中控制其创建、执行和消亡。

    6  多线程开发优势

    • 改善用户体验:例如在读取数据的时候,数据量太大,导致读取时间长,造成程序等待,无法打开程序的其他功能。
    • 提高资源利用率。为什么?首先,不同进程不能共享内存,但是线程可以共享内存;其次,处理器在不同线程之间的切换要比进程之间的切换要简单的多,因此消耗的资源也比较少。

    Java中实现多线程

    1  线程使用步骤

    主线程:Java程序main方法为主线程。编程中编写的线程是指除了主线程之外的其他线程。使用线程步骤:

    1. 定义一个线程
    2. 创建线程的实例
    3. 启动线程
    4. 终止线程

    2  定义线程方法

    定义线程通常有两种方法:使用java.lang.Thread类创建线程和      实现java.lang.Runnable接口创建线程。

    • 使用java.lang.Thread类创建线程

    Thread类常用方法

    方法

    说明

    void  run()

    执行任务操作的方法

    void  start()

    使该线程开始执行

    void  sleep(long millis)

    让当前正在执行的线程休眠millis毫秒

    String  getName()

    返回该线程名称

    int  getPriority()

    返回线程优先级

    void  setPriority(int newPriority)

    更改线程优先级

    Thread.State  getState()

    返回线程的状态

    Boolean  isAlive()

    测试线程是否处于活动状态

    void  join()

    等待该线程终止

    void  interrupt()

    中断线程

    void  yield()

    暂停当前正在执行的线程对象,并执行其他线程

    创建线程时继承Thread类,并重写run()方法,run()方法中编写需要执行操作任务的代码;通过start()方法启动线程。

    示例:在线程中输出1-10

    线程类:

    public class MyThread extends Thread {
    
       private int count=0; 
    
       public void run(){
    
          while(count<10){
    
             count++;
    
             System.out.println("count的值是:"+count);
    
          }
    
       }
    
    }
    

      

    线程测试类:

    public class TestThread {
    
       public static void main(String[] args) {
    
          MyThread myThread = new MyThread();
    
          myThread.start();
    
       }
    
    }
    

      

    • 实现java.lang.Runnable接口创建线程

    如果一个类无法继承Thread类,则可以采用实现Runnable接口的方式实现线程创建。这种方式可以实现多个线程之间使用同一个Runnable对象。

    线程类:

    public class MyRunnable implements Runnable {
    
       private int count = 0;
    
     
    
       public void run() {
    
          while (count < 10) {
    
             count++;
    
             System.out.println("count的值是:" + count);
    
          }
    
       } 
    
    }
    

      

    线程测试类:

    public class TestThread {
    
       public static void main(String[] args) {
    
          Thread thread = new Thread();
    
          thread.start();
    
       }
    }
    

      

    线程状态

     

    • 新生状态(New Thread)

    创建线程对象后,尚未调用start()方法。线程有了生命,只能启动和停止它,调用线程的其他操作会引发异常。

    • 可运行状态(Runnable)

    当调用start()方法启动线程后,系统为该线程分配除CPU之外的所需资源,这个线程就有了运行机会。线程只有在获得CPU资源后,才开始开始运行线程的run()方法。

    • 阻塞状态(Blocked)

    正在运行的线程因某种原因不能继续运行,则进入阻塞状态。阻塞状态是一种不可运行状态,处于阻塞状态的线程在得到一个特定的事件之后,便可转回可运行状态。

    线程阻塞的原因:

    1)        调用了Thread类的静态方法sleep() 

    2)        线程执行IO操作,如果此IO还没有完成操作,则线程被阻塞

    3)        如果一个线程的执行需要得到一个对象锁,而这个对象的锁正在被别的线程占用,则线程被阻塞

    4)        调用suspend()方法使得线程被挂起,线程进入阻塞状态。suspend()容易引起死锁,已经被列为过期方法,不推荐使用。

    阻塞状态都可以转回到可运行状态。

    • 死亡状态(Dead)

    线程的run()方法运行完毕,stop()方法被调用或则线程运行期间出现异常,线程则进入死亡状态。

    线程调度

    1  概念

    • 同一时刻有多个线程处于可运行状态,则需要排队获取CPU资源,每个线程将会分配一个线程优先级(Priority)。
    • 线程调度管理器负责线程排队和分配CPU资源,并按照现场调度算法进行调度。当线程调度管理器选中某个线程时,该线程获得CPU资源进入运行状态。
    • 线程调度属于抢占式调度,如果当前线程正在执行过程中,有一个更高优先级的线程进入可运行状态,则高优先级线程立即被调度执行。

    2  线程优先级

    用1-10表示,10表示最高优先级,默认值为5。每个优先级对应一个Thread类的公用静态常量。例如

    java.lang.Thread静态字段

    public static final int

    MAX_PRIORITY

    10

    public static final int

    MIN_PRIORITY

    1

    public static final int

    NORM_PRIORITY

    5

    设置线程优先级的方法setPriority(int grade)。

    3  实现线程调度的方法

    1)        join()方法

    • 方法概述

    join()方法使当前线程暂停执行,等待调用该方法的线程结束后再执行本线程。他有三个重载方法:

    //等待该线程终止

    public final void join()

    //等待该线程终止的时间最长为 millis 毫秒。超时为 0 意味着要一直等下去。

    public final void join(long millis)

    //等待该线程终止的时间最长为 millis 毫秒 + nanos 纳秒。

    public final void join(long millis,int nanos) 

    • join()方法阻塞线程

      示例代码:

    package com.etc.joindemo;
    
     
    
    import java.util.Date;
    
     
    
    /** 定义线程类 */
    
    public class MyThread extends Thread {
    
     
    
        public MyThread(String name) {
    
            super(name);
    
        }
    
     
    
        public void run() {
    
            // 输出当前线程名称
    
            System.out.println("当前线程为:"+Thread.currentThread().getName());
    
            long startTime=new Date().getTime();//开始时间
    
            long sum = 0;
    
            for (int i = 0; i < 1000000000; i++) {
    
                sum += i;
    
            }
    
            long endTime=new Date().getTime();//结束时间
    
            System.out.println("计算结果:" + sum);
    
            System.out.println("消耗时间:" + (endTime-startTime)+"毫秒");
    
        }
    
    }
    
     
    
    /** 测试类 */
    package com.etc.joindemo;
     
    
    public class Test {
    
        public static void main(String[] args) {
    
            for (int i = 0; i < 10; i++) {
    
                if (i == 5) {
    
                    MyThread myThread = new MyThread("MyThread");
    
                    try {
    
                        myThread.start();// 启动线程
    
                        myThread.join();// 暂停当前main线程,等待myThread线程执行完成
    
                        //myThread.join(0);
    
                        //myThread.join(100);
    
                    } catch (InterruptedException e) {
    
                        e.printStackTrace();
    
                    }
    
                }
    
                System.out.println(Thread.currentThread().getName() + " " + i);
    
            }
    
        }
    
    }
    

       

    • join()方法实现两个线程间的数据传递

     示例代码

    package com.etc.jointwo;
    
     
    
    /** 线程类 */
    
    public class MyThread extends Thread {
    
        public String value1;
    
        public String value2;
    
     
    
        public void run() {
    
            long sum = 0;
    
            for (int i = 0; i < 100000; i++) {
    
                sum += i;
    
            }
    
            System.out.println("sum=" + sum);
    
            value1 = "value1已经赋值";
    
            value2 = "value2已经赋值";
    
        }
    
    }
    
     
    
    /**测试类*/
    
    package com.etc.jointwo;
    
     
    
    public class Test {
    
       public static void main(String[] args) throws InterruptedException {
    
           MyThread myThread = new MyThread();
    
           myThread.start();
    
           System.out.println("value1:" + myThread.value1);
    
           System.out.println("value2:" + myThread.value2);
    
       }
    
    }
    

      存在的问题:run()方法没有执行完成,main线程就去访问线程类中的成员变量,输入value1和value2的值为null。如何解决?myThread.join()。

    2)        sleep()方法

    • 语法格式

    public static void sleep(long millis)

    在指定的毫秒数内让当前正在执行的线程休眠。

    • 示例代码
    /**线程类*/
    
    package com.etc.sleep;
    
     
    
    public class MyThread {
    
     
    
       /**
    
        * 当前线程休眠方法
    
        * @param s 秒
    
        */
    
       public static void wait(int s) {
    
           for (int i = 0; i < s; i++) {
    
               System.out.println(i + 1 + "秒");
    
               try {
    
                   Thread.sleep(1000);// 休眠1秒
    
               } catch (InterruptedException e) {
    
                   e.printStackTrace();
    
               }
    
           }
    
       }
    
    }
    
     
    
    /**测试类*/
    
    package com.etc.sleep;
    
    public class Test {
    
       public static void main(String[] args) {
    
           System.out.println("主线程开始等待");
    
           // MyThread.wait(5);// 主线程等待5秒再执行
    
           // Thread.sleep(5000);
    
           System.out.println("主线程恢复执行");
    
       }
    
    }
    

      

    3)        yield()方法

    • 语法格式

    public static void yield()

    yield()方法暂停当前线程执行,允许其他线程执行,该线程仍处于可运行状态,不转为阻塞状态。此时,系统选择其他相同或者更高优先级线程执行,若无其他相同或更高优先级线程,则该线程继续执行。

    • 示例代码
    /**线程A*/
    
    package com.etc.yield;
    
    public class MyThreadA extends Thread {
    
        public void run() {
    
            for (int i = 0; i < 5; i++) {
    
                System.out.println("线程A的第" + (i + 1) + "次运行。");
    
                Thread.yield();// 暂停当前线程
    
            }
    
        }
    
    }
    
    /**线程B*/
    
    package com.etc.yield;
    
    public class MyThreadB extends Thread {
    
        public void run() {
    
            for (int i = 0; i < 5; i++) {
    
                System.out.println("线程B的第" + (i + 1) + "次运行。");
    
                Thread.yield();// 暂停当前线程
    
            }
    
        }
    
    }
    
    /**测试类*/
    
    MyThreadA ta = new MyThreadA();
    
    // ta.setPriority(5);
    
    MyThreadB tb = new MyThreadB();
    
    // tb.setPriority(6);
    
    ta.start();
    
    tb.start();
    

      

    4)        sleep()方法和yield()方法的区别

    相同点——语法相同Thread.sleep()和Thread.yield()

    sleep()

    yield()

    使当前线程进入被阻塞的状态

    将当前线程转入暂停执行的状态

    即使没有其他等待运行的线程,当前线程也会等待指定的时间

    如果没有其他等待执行的线程,当前线程会马上恢复执行

    其他等待执行的线程的机会是均等的

    会将优先级相同或更高的线程运行

    上机练习

    • 需求说明

    定义一个线程A,输出1 ~ 10之间的整数,定义一个线程B,逆序输出1 ~ 10之间的整数,要求线程A和线程B交替输出

    • 分析

    使用sleep()方法阻塞当前线程

    线程同步

    1  线程同步的必要性

    之前学习的线程都是独立的,并且是异步执行。也就是说每个线程都包含了运行时所需要的数据或者方法,不需要外部资源或者方法,也不需要关心其他线程的状态和行为。

    如果一些同时运行的线程需要共享数据,此时就需要考虑其他线程的状态和行为,否则无法保证程序运行的正确性。

    2  未使用线程同步的银行取款的例子

    • 银行账户类
    /** 银行账户类 */
    
    public class Account {
    
        private int balance = 10000;// 余额
    
     
    
        /**查询余额*/
    
        public int getBalance() {
    
            return this.balance;
    
        }
    
       
    
        /**取款*/
    
        public void withdraw(int amount){
    
            balance-=amount;
    
        }
    
    }
    

      

    • 取款线程类
    /** 取款线程类 */
    
    public class TestAccount implements Runnable {
    
     
    
        // 本类所有对象创建的线程共享同一个账户对象
    
        private Account account = new Account();
    
     
    
        @Override
    
        public void run() {
    
            for (int i = 0; i < 5; i++) {
    
                makeWithdraw(2000);// 取款2000块
    
                if (account.getBalance() < 0) {
    
                    System.out.println("账户透支了!");
    
                }
    
            }
    
        }
    
     
    
        /** 取款 */
    
    private void makeWithdraw(int amt) {
    
        if (account.getBalance() >= amt) {
    
            System.out.println(Thread.currentThread().getName() + "准备取款");
    
            try {
    
                Thread.sleep(500);// 0.5秒后实现取款
    
            } catch (InterruptedException e) {
    
                e.printStackTrace();
    
            }
    
            account.withdraw(amt);// 余额足够,取款
    
            System.out.println(Thread.currentThread().getName() + "取款完成");
    
        } else {
    
            System.out.println("余额不足" + Thread.currentThread().getName()
    
                        + "取款失败!余额为:
    " + account.getBalance());
    
            }
    
        }
    
    }
    

      

    • 测试类
    //创建两个线程
    
    TestAccount ta=new TestAccount();      
    
    Thread A=new Thread(ta);
    
    Thread B=new Thread(ta);       
    
    //设置线程名称
    
    A.setName("柯南");
    
    B.setName("葫芦娃");   
    
    //启动线程
    
    A.start();
    
    B.start();
    • 测试结果

    • 导致取款账户透支的原因分析

    在取款的方法中,虽然有检查余额是否足够再取款,但是假如余额足够(还剩2000块),在取款的这几行代码执行的一小段时间内,柯南先取了2000块,而同时葫芦娃的代码也在这个时间段执行取款(葫芦娃的代码也经过余额判断进入取款代码块)也认为余额是足够的,所以也取走了2000块,从而导致取款出现透支的情况。

    3  线程同步的实现

    • 线程同步的概念

        当两个或者多个线程需要访问同一个资源时,需要以某种顺序来确保该资源在某一时刻只能被一个线程使用的方式。

    • 同步方法

    语法结构:

    访问修饰符 synchronized 返回类型 方法名( ) { }

    或者

    synchronized 访问修饰符 返回类型 方法名( ) { }

    说明:

    使用synchronized修饰的方法控制对类成员变量的访问,每个类实例对应一把锁,方法一旦执行,就独占该锁,直到该方法返回时才释放锁。synchronized机制确保了同一时刻对应每一个实例,synchronized方法只能有一个处于可执行状态,从而有效避免了类成员变量的访问冲突。

    同步方法的缺陷:

    如果一个运行时间长的方法声明为synchronized,将会影响程序运行的效率。

    使用同步方法修改银行取款的例子:

    关键代码

    /** 取款 */
    	private void makeWithdraw(int amt) {
    		synchronized (account) {
    			if (account.getBalance() >= amt) {
    				System.out.println(Thread.currentThread().getName() +
     "准备取款");
    				try {
    					Thread.sleep(500);// 0.5秒后实现取款
    				} catch (InterruptedException e) {
    					e.printStackTrace();
    				}
    				account.withdraw(amt);// 余额足够,取款
    				System.out.println(Thread.currentThread().getName() + 
    "取款完成");
    			} else {
    System.out.println("余额不足" 
    + Thread.currentThread().getName()
    						+ "取款失败!余额为:
    " + account.getBalance());
    			}
    		}
    	}
    

     

    测试结果

     

    • 同步代码块

    语法结构:

    synchronized(syncObject){

        //需要同步访问控制的代码

    }

    说明:

    synchronized块中的代码必须获得对象syncObject的锁才能执行,具体机制与同步方法一致。由于可以针对任意代码块,且可以指定任意上锁的对象,所以灵活性高。

    使用同步代码块修改银行取款的例子:

    关键代码

     

       /** 取款 */
    
        private void makeWithdraw(int amt) {
    
            synchronized (account) {
    
                if (account.getBalance() >= amt) {
    
                    System.out.println(Thread.currentThread().getName() + "准备取款");
    
                    try {
    
                        Thread.sleep(500);// 0.5秒后实现取款
    
                    } catch (InterruptedException e) {
    
                        e.printStackTrace();
    
                    }
    
                    account.withdraw(amt);// 余额足够,取款
    
                    System.out.println(Thread.currentThread().getName() + "取款完成");
    
                } else {
    
             System.out.println("余额不足"+ Thread.currentThread().getName() + "取款失败!余额为:
    " + account.getBalance());
    
                }
    
            }
    
        }
    

      

      测试结果

    • 死锁

    多线程使用同步机制时候,如果多个线程都处于等待状态而无法唤醒时,就会构成死锁(Deadlock),此时处于等待状态的多个线程占用系统资源,但是无法运行,所以也无法释放资源。

    避免死锁的方法:如果线程因某个条件未满足从而受阻,不能让其继续占有资源;如果有多个对象需要要互斥访问,应确定线程获得锁的顺序,并保证整个程序以相反的顺序释放锁。

    示例代码

    public class DeadBlock {
    
       // 内部静态类
    
       private static class Resource {
    
           public int value;
    
       }
    
       private Resource ra = new Resource();
    
       private Resource rb = new Resource();
    
       /** 读数据 */
    
       public int read() {
    
           synchronized (ra) {
    
               synchronized (rb) {
    
                   return ra.value + rb.value;
    
               }
    
           }
    
       }
    
       /** 写数据 */
    
       public void write(int a, int b) {
    
           synchronized (rb) {
    
               synchronized (ra) {
    
                   ra.value = a;
    
                   rb.value = b;
    
               }
    
           }
    
       }
    
    }
    

      

    一个线程执行read方法,另一个线程执行write方法,造成死锁。

    上机练习

    • 需求说明

    张三和李四登陆天狗网抢购iphone8,请使用多线程及同步方法模拟抢购的过程。

    天狗网确保手机库存不能出现负数。

    用户支付完成才算抢购成功。

    要求使用同步方法和同步代码块两种方式实现。

    • 分析

    定义PhoneStock类表示库存手机信息类。

    抢购过程包括加入购物车、下单、支付。

    定义两个线程分别实现抢购操作。

    线程间通信

    1         线程间通信的必要性

    线程同步可以无条件阻止其他线程对共享资源的异步访问。

    现实中,线程间不仅要同步访问同一共享的资源,而且线程间还需要彼此牵制,互相通信。

    著名的生产者和消费者问题:

     

    (一)    仓库只能放一件产品

    (二)    消费者等待

    (三)    生产者生产一件产品,放入仓库,通知消费者消费

    (四)    生产者等待

    (五)    消费者消费产品,仓库空,通知生产者生产

    (六)    生产者生产产品

    2         Java线程间通信

    • 通信机制

    wait():释放当前对象锁。同时当前线程从可运行状态转入阻塞状态。

    notify方法:通知被wait方法转入阻塞的线程,转入可运行状态。

    notifyall方法:使所有因使用wait方法转入阻塞的线程,转入可运行状态。优先级最高的线程最先执行。 

        注意:

      1. 这三个方法都是在Object类中的final方法,被所有类继承,但不允许重写。
      2. 这三个方法只能在同步方法或者同步代码块中使用,否则抛出异常。
    • 示例:使用wait()和notify()方法实现线程间通信
    /** 通信线程类 */
    
    public class ComThread implements Runnable {
    
       /** 同步run方法 */
    
       @Override
    
       public synchronized void run() {
    
           for (int i = 0; i < 5; i++) {
    
               System.out.println(Thread.currentThread().getName() + i);
    
               if (i == 2) {
    
                   try {
    
                       wait();// 退出运行状态,放弃资源锁,进入等待队列
    
                   } catch (InterruptedException e) {
    
                       e.printStackTrace();
    
                   }
    
               }
    
     
    
               if (i == 1) {
    
                   notify();// 从等待队列中唤起一个线程。等待对象锁释放。
    
               }
    
     
    
               if (i == 4) {
    
                   notify();
    
               }
    
           }
    
       }
    
    }
    
     
    
    /**测试类*/
    
    public class Test {
    
       public static void main(String[] args) {
    
           ComThread comThread=new ComThread();
    
           Thread ta=new Thread(comThread,"线程A");
    
           Thread tb=new Thread(comThread,"线程B");
    
           ta.start();
    
           tb.start();
    
       }
    
    }
    

      

    测试结果

    上机练习

    使用线程通信解决生产者消费者问题。

    • 需求描述:略
    • 实现步骤:

    1)        定义共享资源类

    2)        定义生产者线程类

    3)        定义消费者线程类

    4)        定义测试类

    • 输出结果

    参考代码:

    /** 共享资源类 */
    
    public class Data {
    
        private char c;
    
        private boolean isProduced = false;// 生产消费信号参数
    
        /** 生产产品方法 */
    
        public synchronized void putShareChar(char c) {
    
            // 产品还未消费,则生产者等待
    
            if (isProduced) {
    
                System.out.println("消费者还未消费,因此生产者停止生产");
    
                try {
    
                    wait();// 生产者等待
    
                } catch (InterruptedException e) {
    
                    e.printStackTrace();
    
                }
    
            }
    
            // 生产产品
    
            this.c = c;// 生产了一件产品(以字符替代)
    
            this.isProduced = true;// 标记已经生产
    
            notify();// 通知消费者已经生产,可以消费
    
            System.out.println("生成了产品  " + c + "  通知消费者消费...");
    
        }
    
     
    
        /** 消费产品方法 */
    
        public synchronized char getShareChar() {
    
            // 如果产品尚未生产,消费者等待
    
            if (!isProduced) {
    
                System.out.println("生产者还未生成,因此消费者停止消费");
    
                try {
    
                    wait();// 消费者等待
    
                } catch (InterruptedException e) {
    
                    e.printStackTrace();
    
                }
    
            }
    
            // 消费产品
    
            this.isProduced = false;// 标记已经消费
    
            notify();// 通知需要生产
    
            System.out.println("消费者消费了产品  " + c + "  通知生产者生产...");
    
            return this.c;
    
        }
    
    }
    
     
    
    /** 生成者线程类 */
    
    public class Producer extends Thread {
    
        private Data data;
    
        public Producer(Data data) {
    
            this.data = data;
    
        }
    
        public void run() {
    
            for (char ch = 'A'; ch <= 'Z'; ch++) {
    
                try {
    
                    Thread.sleep((int) Math.random() * 3000);// 当前线程休眠0-3秒
    
                } catch (InterruptedException e) {
    
                    e.printStackTrace();
    
                }
    
                data.putShareChar(ch);// 将产品放入仓库
    
            }
    
        }
    
    }
    
     
    
    /** 消费者线程类 */
    
    public class Consumer extends Thread {
    
        private Data data;
    
        public Consumer(Data data) {
    
            this.data = data;
    
        }
    
        public void run() {
    
            char ch;
    
            do {
    
                try {
    
                    Thread.sleep((int) Math.random() * 3000);// 当前线程休眠0-3秒
    
                } catch (InterruptedException e) {
    
                    e.printStackTrace();
    
                }
    
                ch = data.getShareChar();// 从仓库取出产品
    
            } while (ch != 'Z');
    
        }
    
    }
    
     
    
    /** 测试类 */
    
    Data data = new Data();// 共享数据
    
    Consumer consumer = new Consumer(data); // 消费者线程
    
    consumer.start();      
    
    Producer producer = new Producer(data); // 生产者线程
    
    producer.start();
    

      



    本博客文章未经许可,禁止转载和商业用途!

    如有疑问,请联系: 2083967667@qq.com


  • 相关阅读:
    亚瑟阿伦36问
    Oracle动态SQL
    Oracle分页
    Oracle游标+动态SQL
    Oracle %type %rowtype
    Oracle游标
    Oracle存储过程和Java调用
    Oracle循环
    Oracle(if判断)
    Oracle视图
  • 原文地址:https://www.cnblogs.com/rask/p/8254265.html
Copyright © 2011-2022 走看看