zoukankan      html  css  js  c++  java
  • Java多线程编程核心技术---线程间通信(二)

    通过管道进行线程间通信:字节流

    Java提供了各种各样的输入/输出流Stream可以很方便地对数据进行操作,其中管道流(pipeStream)是一种特殊的流,用于在不同线程间直接传送数据,一个线程发送数据到输出管道,另一个线程从输入管道中读数据。通过使用管道,实现不同线程间的通信,无需借助于类似临时文件之类的东西。

    JDK提供了4个类来使线程间可以进行通信:

    1. PipedInputStream和PipedOutputStream
    2. PipedReader和PipedWriter
    public class WriteData {
    	public void writeMethod(PipedOutputStream out) {
    		try {
    			System.out.println("write:");
    			for (int i = 0; i < 100; i++) {
    				String outData = "" + (i + 1);
    				out.write(outData.getBytes());
    				//System.out.print(outData);
    			}
    			//System.out.println();
    			out.close();
    		} catch (Exception e) {
    			e.printStackTrace();
    		}
    	}
    }
    
    public class ReadData {
    	public void readMethod(PipedInputStream input) {
    		try {
    			System.out.println("read:");
    			byte[] byteArray = new byte[20];
    			int readLength = input.read(byteArray);
    			while (readLength != -1) {
    				String newData = new String(byteArray, 0, readLength);
    				System.out.println(newData);
    				readLength = input.read(byteArray);
    			}
    			System.out.println();
    			input.close();
    		} catch (Exception e) {
    			e.printStackTrace();
    		}
    	}
    }
    
    public class ThreadWrite extends Thread {
    	private WriteData writeData;
    	private PipedOutputStream out;
    	
    	public ThreadWrite(WriteData writeData, PipedOutputStream out) {
    		super();
    		this.out = out;
    		this.writeData = writeData;
    	}
    	
    	@Override
    	public void run() {
    		writeData.writeMethod(out);
    	}
    }
    
    public class ThreadRead extends Thread {
    	private ReadData readData;
    	private PipedInputStream input;
    	
    	public ThreadRead(ReadData readData, PipedInputStream input) {
    		super();
    		this.input = input;
    		this.readData = readData;
    	}
    	
    	@Override
    	public void run() {
    		readData.readMethod(input);
    	}
    }
    
    public class Main {
    	public static void main(String[] args) {
    		try {
    			WriteData writeData = new WriteData();
    			ReadData readData = new ReadData();
    			
    			PipedInputStream inputStream = new PipedInputStream();
    			PipedOutputStream outputStream = new PipedOutputStream();
    			
    			outputStream.connect(inputStream);
    			//inputStream.connect(outputStream);
    			
    			ThreadRead threadRead = new ThreadRead(readData, inputStream);
    			ThreadWrite threadWrite = new ThreadWrite(writeData, outputStream);
    			
    			threadRead.start();//先启动读线程
    			
    			Thread.sleep(1000);
    			
    			threadWrite.start();//后启动写线程
    		} catch (Exception e) {
    			e.printStackTrace();
    		}
    	}
    }
    

    控制台打印结果如下:

    read:
    write:
    123456
    78910111213141516171
    81920212223242526272
    8293031323334353637
    383940414243
    44454647484950
    51525354555657
    58596061626364
    65666768697071
    72737475767778
    798081828384
    858687888990
    91929394959697
    9899100
    

    先启动读线程,由于没有数据被写入,读线程阻塞在readLength = input.read(byteArray),直到有数据被写入才继续往下运行。

    通过管道进行线程间通信:字符流
    public class WriteData {
    	public void writeMethod(PipedWriter writer) {
    		try {
    			System.out.println("write:");
    			for (int i = 0; i < 100; i++) {
    				String outData = "" + (i + 1);
    				writer.write(outData);
    				//System.out.print(outData);
    			}
    			//System.out.println();
    			writer.close();
    		} catch (Exception e) {
    			e.printStackTrace();
    		}
    	}
    }
    
    public class ReadData {
    	public void readMethod(PipedReader reader) {
    		try {
    			System.out.println("read:");
    			char[] array = new char[20];
    			int readLength = reader.read(array);
    			while (readLength != -1) {
    				String newData = new String(array, 0, readLength);
    				System.out.println(newData);
    				readLength = reader.read(array);
    			}
    			System.out.println();
    			reader.close();
    		} catch (Exception e) {
    			e.printStackTrace();
    		}
    	}
    }
    
    public class ThreadWrite extends Thread {
    	private WriteData writeData;
    	private PipedWriter writer;
    	
    	public ThreadWrite(WriteData writeData, PipedWriter writer) {
    		super();
    		this.writer = writer;
    		this.writeData = writeData;
    	}
    	
    	@Override
    	public void run() {
    		writeData.writeMethod(writer);
    	}
    }
    
    public class ThreadRead extends Thread {
    	private ReadData readData;
    	private PipedReader reader;
    	
    	public ThreadRead(ReadData readData, PipedReader reader) {
    		super();
    		this.reader = reader;
    		this.readData = readData;
    	}
    	
    	@Override
    	public void run() {
    		readData.readMethod(reader);
    	}
    }
    
    public class Main {
    	public static void main(String[] args) {
    		try {
    			WriteData writeData = new WriteData();
    			ReadData readData = new ReadData();
    			
    			PipedReader reader = new PipedReader();
    			PipedWriter writer = new PipedWriter();
    			
    //			writer.connect(reader);
    			reader.connect(writer);
    			
    			ThreadRead threadRead = new ThreadRead(readData, reader);
    			ThreadWrite threadWrite = new ThreadWrite(writeData, writer);
    			
    			threadRead.start();
    			
    			Thread.sleep(1000);
    			
    			threadWrite.start();
    		} catch (Exception e) {
    			e.printStackTrace();
    		}
    	}
    }
    

    控制台打印结果如下:

    read:
    write:
    12345678910111213141
    51617181920212223242
    52627282930313233343
    53637383940414243444
    54647484950515253545
    55657585960616263646
    56667686970717273747
    57677787980818283848
    58687888990919293949
    596979899100
    
    

    打印结果和前一个例子基本一样。此实验是在两个线程中通过管道流进行字符数据的传输。


    等待/通知:交叉备份

    创建20个线程,10个线程将数据备份到A数据库中,10个线程将数据备份到B数据库中,并且备份A数据库和备份B数据库是交叉进行的。

    public class DBTools {
    	volatile private boolean prevIsA = false;
    	synchronized public void backupA(){
    		try {
    			while (prevIsA) {
    				wait();
    			}
    			for (int i = 0; i < 5; i++) {
    				System.out.println("☆☆☆☆☆");
    			}
    			prevIsA = true;
    			notifyAll();
    		} catch (Exception e) {
    			e.printStackTrace();
    		}
    	}
    	
    	synchronized public void backupB(){
    		try {
    			while (!prevIsA) {
    				wait();
    			}
    			for (int i = 0; i < 5; i++) {
    				System.out.println("★★★★★");
    			}
    			prevIsA = false;
    			notifyAll();
    		} catch (Exception e) {
    			e.printStackTrace();
    		}
    	}
    }
    
    public class ThreadA extends Thread {
    	private DBTools dbTools;
    	public ThreadA(DBTools dbTools) {
    		super();
    		this.dbTools = dbTools;
    	}
    	
    	@Override
    	public void run() {
    		dbTools.backupA();
    	}
    }
    
    public class ThreadB extends Thread {
    	private DBTools dbTools;
    	public ThreadB(DBTools dbTools) {
    		super();
    		this.dbTools = dbTools;
    	}
    	
    	@Override
    	public void run() {
    		dbTools.backupB();
    	}
    }
    
    public class Main {
    	public static void main(String[] args) {
    		DBTools dbTools = new DBTools();
    		for (int i = 0; i < 20; i++) {
    			ThreadA threadA = new ThreadA(dbTools);
    			threadA.start();
    			ThreadB threadB = new ThreadB(dbTools);
    			threadB.start();
    		}
    	}
    }
    

    控制台打印结果如下:

    ......
    ★★★★★
    ★★★★★
    ★★★★★
    ★★★★★
    ★★★★★
    ☆☆☆☆☆
    ☆☆☆☆☆
    ☆☆☆☆☆
    ☆☆☆☆☆
    ☆☆☆☆☆
    ★★★★★
    ★★★★★
    ★★★★★
    ★★★★★
    ★★★★★
    ☆☆☆☆☆
    ☆☆☆☆☆
    ☆☆☆☆☆
    ☆☆☆☆☆
    ☆☆☆☆☆
    ......
    

    交替打印是使用prevIsA作为标记来实现的。


    方法join的使用

    在很多情况下,主线程创建并启动子线程,如果子线程要进行大量运算,主线程往往比子线程先运行完毕。如果主线程想要等待子线程执行完之后再结束,就需要用到join方法。

    先看一个实验。

    public class MyThread extends Thread {
    	@Override
    	public void run() {
    		try {
    			int secondValue = (int)(Math.random() * 10000);
    			System.out.println(secondValue);
    			Thread.sleep(secondValue);
    		} catch (Exception e) {
    			e.printStackTrace();
    		}
    	}
    	
    	public static void main(String[] args) {
    		MyThread thread = new MyThread();
    		thread.start();
    //		Thread.sleep(?);
    		System.out.println("我想在thread对象执行完毕之后再执行");
    		System.out.println("但是上面sleep()中的值写多少呢?");
    		System.out.println("答案是:不能确定!!!");
    	}
    }
    
    用join()方法来解决上面的问题
    public class MyThread extends Thread {
    	@Override
    	public void run() {
    		try {
    			int secondValue = (int)(Math.random() * 10000);
    			System.out.println(secondValue);
    			Thread.sleep(secondValue);
    		} catch (Exception e) {
    			e.printStackTrace();
    		}
    	}
    	
    	public static void main(String[] args) throws InterruptedException {
    		MyThread thread = new MyThread();
    		thread.start();
    //		Thread.sleep(?);
    		thread.join();
    		System.out.println("我想在thread对象执行完毕之后再执行");
    	}
    }
    
    

    控制台打印结果如下:

    9230
    我想在thread对象执行完毕之后再执行
    

    方法join()的作用是使所属线程对象x正常执行run()方法中的任务,而使当前线程进行无限期的阻塞。等线程x执行完毕之后再执行当前线程后续的代码。


    方法join()与异常

    在join()过程中,如果当前线程对象被中断,则当前线程出现异常。

    public class ThreadA extends Thread {
    	
    	@Override
    	public void run() {
    		for (int i = 0; i < Integer.MAX_VALUE; i++) {
    			String newString = new String();
    			Math.random();
    		}
    	}
    }
    
    public class ThreadB extends Thread {
    	@Override
    	public void run() {
    		try {
    			ThreadA a = new ThreadA();
    			a.start();
    			a.join();
    			System.out.println("线程B在run end处打印");
    		} catch (Exception e) {
    			System.out.println("线程B在catch处打印");
    			e.printStackTrace();
    		}
    	}
    }
    
    public class ThreadC extends Thread {
    	private ThreadB threadB;
    	public ThreadC(ThreadB threadB) {
    		super();
    		this.threadB = threadB;
    	}
    	
    	@Override
    	public void run() {
    		threadB.interrupt();
    	}
    }
    
    public class Main {
    	public static void main(String[] args) {
    		try {
    			ThreadB b = new ThreadB();
    			b.start();
    			Thread.sleep(2000);
    			ThreadC c = new ThreadC(b);
    			c.start();
    		} catch (Exception e) {
    			e.printStackTrace();
    		}
    	}
    }
    

    控制台打印结果如下:

    线程B在catch处打印
    java.lang.InterruptedException
    	at java.lang.Object.wait(Native Method)
    	at java.lang.Thread.join(Unknown Source)
    	at java.lang.Thread.join(Unknown Source)
    	at com.umgsai.thread.thread38.ThreadB.run(ThreadB.java:9)
    

    此时程序不结束,a线程扔在运行。


    方法join(long)的使用
    public class MyThread extends Thread {
    	@Override
    	public void run() {
    		try {
    			System.out.println("MyThread开始,time = " + System.currentTimeMillis());
    			Thread.sleep(5000);
    			System.out.println("MyThread结束,time = " + System.currentTimeMillis());
    		} catch (Exception e) {
    			e.printStackTrace();
    		}
    	}
    	
    	public static void main(String[] args) {
    		try {
    			MyThread thread = new MyThread();
    			thread.start();
    			thread.join(2000);
    			System.out.println("main线程结束 time = " + System.currentTimeMillis());
    		} catch (Exception e) {
    			e.printStackTrace();
    		}
    	}
    }
    

    控制台打印结果如下:

    MyThread开始,time = 1466909154102
    main线程结束 time = 1466909156103
    MyThread结束,time = 1466909159102
    

    如果将main中的join(2000)改成Thread.sleep(2000),运行效果是一样的,main线程都是等待2秒。主要区别来自于这两个方法对同步的处理上。


    方法join(long)与sleep(long)的区别

    方法join(long)的功能在内部是使用wait(long)方法来实现的,所以join(long)方法具有释放锁的特点。

    方法join(long)源代码如下:

    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;
            }
        }
    }
    

    当执行wait(long)方法后,当前线程的锁被释放,其他线程就可以调用此线程中的同步方法了。

    public class ThreadA extends Thread { 
    	private ThreadB b;
    	public ThreadA(ThreadB b) {
    		super();
    		this.b = b;
    	}
    	@Override
    	public void run() {
    		try {
    			synchronized (b) {
    				b.start();
    				Thread.sleep(6000);////不释放b对象锁
    			}
    		} catch (Exception e) {
    			e.printStackTrace();
    		}
    	}
    }
    
    public class ThreadB extends Thread { 
    	@Override
    	public void run() {
    		try {
    			System.out.println("B begin time = " + System.currentTimeMillis());
    			Thread.sleep(5000);
    			System.out.println("B end time = " + System.currentTimeMillis());
    		} catch (Exception e) {
    			e.printStackTrace();
    		}
    	}
    	
    	synchronized public void bService(){
    		System.out.println("bService time = " + System.currentTimeMillis());
    	}
    }
    
    public class ThreadC extends Thread {
    	private ThreadB threadB;
    	public ThreadC(ThreadB threadB) {
    		super();
    		this.threadB = threadB;
    	}
    	
    	@Override
    	public void run() {
    		threadB.bService();
    	}
    }
    
    public class Main {
    	public static void main(String[] args) {
    		try {
    			ThreadB b = new ThreadB();
    			ThreadA a = new ThreadA(b);
    			a.start();
    			Thread.sleep(1000);
    			ThreadC c = new ThreadC(b);
    			c.start();
    		} catch (Exception e) {
    			e.printStackTrace();
    		}
    	}
    }
    

    控制台打印结果如下:

    B begin time = 1466927915044
    B end time = 1466927920045
    bService time = 1466927921044
    

    将线程A做如下修改:

    public class ThreadA extends Thread { 
    	private ThreadB b;
    	public ThreadA(ThreadB b) {
    		super();
    		this.b = b;
    	}
    	@Override
    	public void run() {
    		try {
    			synchronized (b) {
    				b.start();
    				b.join();//释放b对象锁
    			}
    		} catch (Exception e) {
    			e.printStackTrace();
    		}
    	}
    }
    

    此时控制台打印结果如下:

    B begin time = 1466927989797
    bService time = 1466927990801
    B end time = 1466927994798
    

    方法join()后面的代码提前运行:出现意外
    public class ThreadA extends Thread {
    	private ThreadB b;
    	public ThreadA(ThreadB b) {
    		super();
    		this.b = b;
    	}
    	@Override
    	public void run() {
    		try {
    			synchronized (b) {
    				System.out.println(Thread.currentThread().getName() + " A begin time = " + System.currentTimeMillis());
    				Thread.sleep(5000);
    				System.out.println(Thread.currentThread().getName() + " A end time = " + System.currentTimeMillis());
    			}
    		} catch (Exception e) {
    			e.printStackTrace();
    		}
    	}
    }
    
    public class ThreadB extends Thread {
    	@Override
    	synchronized public void run() {
    		try {
    			System.out.println(Thread.currentThread().getName() + " B begin time = " + System.currentTimeMillis());
    			Thread.sleep(5000);
    			System.out.println(Thread.currentThread().getName() + " B end time = " + System.currentTimeMillis());
    		} catch (Exception e) {
    			e.printStackTrace();
    		}
    	}
    }
    
    public class Main {
    	public static void main(String[] args) {
    		try {
    			ThreadB b = new ThreadB();
    			ThreadA a = new ThreadA(b);
    			a.start();
    			b.start();
    			b.join(2000);
    			System.out.println("main end " + System.currentTimeMillis());
    		} catch (Exception e) {
    			e.printStackTrace();
    		}
    	}
    }
    

    P187 此节未完待续...

    类ThreadLocal的使用

    变量值的共享可以使用public static变量的形式,所有的线程都使用同一个public static变量。类ThreadLocal为每个线程绑定自己的值。

    每个线程中都有一个自己的ThreadLocalMap类对象,可以将线程自己的对象保存到其中。ThreadLocalMap针对每个thread保留一个entry,如果对应的thread不存在则会调用initValue。

    方法get()与null
    public class Run {
    	public static ThreadLocal t1 = new ThreadLocal<>();
    	public static void main(String[] args) {
    		if (t1.get() == null) {
    			System.out.println("从未放过值");
    			t1.set("我的值");
    		}
    		System.out.println(t1.get());
    		System.out.println(t1.get());
    	}
    }
    

    控制台打印结果如下:

    从未放过值
    我的值
    我的值
    

    类ThreadLocal解决的是变量在不同线程间的隔离性,也就是不同线程拥有自己的值,不同线程中的值是可以放入ThreadLocal类中进行保存的。


    验证线程变量的隔离性
    public class ThreadA extends Thread {
    	@Override
    	public void run() {
    		try {
    			for (int i = 0; i < 100; i++) {
    				Tools.t1.set("ThreadA " + (i + 1));
    				System.out.println("ThreadA getValue=" + Tools.t1.get());
    				Thread.sleep(200);
    			}
    		} catch (Exception e) {
    			e.printStackTrace();
    		}
    	}
    }
    
    public class ThreadB extends Thread {
    	@Override
    	public void run() {
    		try {
    			for (int i = 0; i < 100; i++) {
    				Tools.t1.set("ThreadB " + (i + 1));
    				System.out.println("ThreadB getValue=" + Tools.t1.get());
    				Thread.sleep(200);
    			}
    		} catch (Exception e) {
    			e.printStackTrace();
    		}
    	}
    }
    
    public class Main {
    	public static void main(String[] args) {
    		try {
    			ThreadA a = new ThreadA();
    			ThreadB b = new ThreadB();
    			a.start();
    			b.start();
    			for (int i = 0; i < 100; i++) {
    				Tools.t1.set("Thread main " + (i + 1));
    				System.out.println("Thread main getValue=" + Tools.t1.get());
    				Thread.sleep(200);
    			}
    		} catch (Exception e) {
    			e.printStackTrace();
    		}
    	}
    }
    

    控制台打印结果如下:

    ......
    Thread main getValue=Thread main 99
    ThreadB getValue=ThreadB 99
    ThreadA getValue=ThreadA 99
    Thread main getValue=Thread main 100
    ThreadA getValue=ThreadA 100
    ThreadB getValue=ThreadB 100
    

    虽然三个线程都向t1对象中set()数据值,但每个线程还是能取出自己的数据。

    第一次调用ThreadLocal类的get()方法返回值是null。可以设置默认值。

    public class ThreadLocalExt extends ThreadLocal {
    	public static ThreadLocalExt t1 = new ThreadLocalExt();
    	
    	@Override
    	protected Object initialValue() {
    		return "This is the default value";
    	}
    	
    	public static void main(String[] args) {
    		if (t1.get() == null) {
    			System.out.println("从未放过值");
    			t1.set("我的值");
    		}
    		System.out.println(t1.get());
    		System.out.println(t1.get());
    	}
    }
    

    控制台打印结果如下:

    This is the default value
    This is the default value
    

    子线程和父线程各自拥有自己的值

    public class ThreadLocalExt extends ThreadLocal {
    	@Override
    	protected Object initialValue() {
    		return new Date().getTime();
    	}
    }
    
    public class Tools {
    	public static ThreadLocalExt t1 = new ThreadLocalExt();
    }
    
    public class ThreadA extends Thread {
    	@Override
    	public void run() {
    		try {
    			for (int i = 0; i < 5; i++) {
    				System.out.println("ThreadA取值:" + Tools.t1.get());
    				Thread.sleep(100);
    			}
    		} catch (Exception e) {
    			e.printStackTrace();
    		}
    	}
    }
    
    public class Main {
    	public static void main(String[] args) {
    		try {
    			for (int i = 0; i < 5; i++) {
    				System.out.println("Main取值:" + Tools.t1.get());
    				Thread.sleep(100);
    			}
    			Thread.sleep(5000);
    			ThreadA a = new ThreadA();
    			a.start();
    		} catch (Exception e) {
    			e.printStackTrace();
    		}
    	}
    }
    

    控制台打印结果如下:

    Main取值:1467118837063
    Main取值:1467118837063
    Main取值:1467118837063
    Main取值:1467118837063
    Main取值:1467118837063
    ThreadA取值:1467118842566
    ThreadA取值:1467118842566
    ThreadA取值:1467118842566
    ThreadA取值:1467118842566
    ThreadA取值:1467118842566
    

    类InheritableThreadLocal的使用

    使用类InheritableThreadLocal可以在子线程中获得父线程继承下来的值。

    public class InheritableThreadLocalExt extends InheritableThreadLocal {
    	@Override
    	protected Object initialValue() {
    		return new Date().getTime();
    	}
    }
    
    public class Tools {
    	public static InheritableThreadLocalExt t1 = new InheritableThreadLocalExt();
    }
    
    public class ThreadA extends Thread {
    	@Override
    	public void run() {
    		try {
    			for (int i = 0; i < 5; i++) {
    				System.out.println("ThreadA取值:" + Tools.t1.get());
    				Thread.sleep(100);
    			}
    		} catch (Exception e) {
    			e.printStackTrace();
    		}
    	}
    }
    
    public class Main {
    	public static void main(String[] args) {
    		try {
    			for (int i = 0; i < 5; i++) {
    				System.out.println("Main取值:" + Tools.t1.get());
    				Thread.sleep(100);
    			}
    			Thread.sleep(5000);
    			ThreadA a = new ThreadA();
    			a.start();
    		} catch (Exception e) {
    			e.printStackTrace();
    		}
    	}
    }
    

    控制台打印结果如下:

    Main取值:1467119046785
    Main取值:1467119046785
    Main取值:1467119046785
    Main取值:1467119046785
    Main取值:1467119046785
    ThreadA取值:1467119046785
    ThreadA取值:1467119046785
    ThreadA取值:1467119046785
    ThreadA取值:1467119046785
    ThreadA取值:1467119046785
    
    值继承再修改

    将上面带做如下修改:

    public class InheritableThreadLocalExt extends InheritableThreadLocal {
    	@Override
    	protected Object initialValue() {
    		return new Date().getTime();
    	}
    	
    	@Override
    	protected Object childValue(Object parentValue) {
    		return parentValue + "我是在子线程中加的~~~";
    	}
    }
    

    重新运行程序,控制台打印结果如下:

    Main取值:1467119256537
    Main取值:1467119256537
    Main取值:1467119256537
    Main取值:1467119256537
    Main取值:1467119256537
    ThreadA取值:1467119256537我是在子线程中加的~~~
    ThreadA取值:1467119256537我是在子线程中加的~~~
    ThreadA取值:1467119256537我是在子线程中加的~~~
    ThreadA取值:1467119256537我是在子线程中加的~~~
    ThreadA取值:1467119256537我是在子线程中加的~~~
    

    需要注意的是,如果子线程在取得值的同时,主线程将InheritableThreadLocal中的值进行更改,那么子线程取到的值还是旧值。

  • 相关阅读:
    IE8,IE10下载的临时文件到哪里去了???
    安全退出,清空Session或Cookie
    删掉SQL Server登录时登录名下拉列表框中的选项
    C#中==、Equals、ReferenceEquals的区别
    [转载]C#中as和is关键字的用法
    HTML5权威指南 5.绘制图形
    HTML5权威指南 3.HTML5的结构
    HTML5权威指南 2.HTML5与HTML4的区别
    HTML5权威指南 1.Web时代的变迁
    web前端黑客技术揭秘 9.Web蠕虫
  • 原文地址:https://www.cnblogs.com/umgsai/p/5595111.html
Copyright © 2011-2022 走看看