• ReentrantLock synchronized


    关于互斥锁:

    所谓互斥锁, 指的是一次最多只能有一个线程持有的锁. 在jdk1.5之前, 我们通常使用synchronized机制控制多个线程对共享资源的访问. 而现在, Lock提供了比synchronized机制更广泛的锁定操作, Lock和synchronized机制的主要区别:

    synchronized机制提供了对与每个对象相关的隐式监视器锁的访问, 并强制所有锁获取和释放均要出现在一个块结构中, 当获取了多个锁时, 它们必须以相反的顺序释放. synchronized机制对锁的释放是隐式的, 只要线程运行的代码超出了synchronized语句块范围, 锁就会被释放. 而Lock机制必须显式的调用Lock对象的unlock()方法才能释放锁, 这为获取锁和释放锁不出现在同一个块结构中, 以及以更自由的顺序释放锁提供了可能. 

    关于可重入: 

    一、2.4.1 内部锁

    Java 提供了原子性的内置锁机制: sychronized 块。它包含两个部分:锁对象的引用和这个锁保护的代码块:

    synchronized(lock) {

    // 访问或修改被锁保护的共享状态

    }

    内部锁扮演了互斥锁( mutual exclusion lock, 也称作 mutex )的角色,一个线程拥有锁的时候,别的线程阻塞等待。

    2.4.2 重进入(Reentrancy )

    重入性:指的是同一个线程多次试图获取它所占有的锁,请求会成功。当释放锁的时候,直到重入次数清零,锁才释放完毕。

    Public class Widget {

          Public synchronized void doSomething(){

               …

          }

    }

    Public class LoggingWidget extends Widget {

       Public synchronized void doSomething(){

          System.out.println(toString()+”:calling doSomething”);

          Super.doSomething();

       }

    }

    二、一般来说,在多线程程序中,某个任务在持有某对象的锁后才能运行任务,其他任务只有在该任务释放同一对象锁后才能拥有对象锁,然后执行任务。于是,想到,同一个任务在持有同一个对象的锁后,在不释放锁的情况下,继续调用同一个对象的其他同步(synchronized)方法,该任务是否会再次持有该对象锁呢? 

        答案是肯定的。同一个任务在调用同一个对象上的其他synchronized方法,可以再次获得该对象锁。 

    Java代码  收藏代码
    1. synchronized  m1(){  
    2. //加入此时对锁a的计数是N  
    3.  m2();  //进入m2的方法体之后锁计数是N+1,离开m2后是N  
    4. }  
    5. synchronized m2(){}  

     同一任务和对象锁的问题:http://www.iteye.com/topic/728485

    Java代码  收藏代码
    1. /*public class ReentrantLock  
    2. extends Object implements Lock, Serializable 
    3. */  

      

    一个可重入的互斥锁 Lock,它具有与使用 synchronized 方法和语句所访问的隐式监视器锁相同的一些基本行为和语义,但功能更强大。

    ReentrantLock 将由最近成功获得锁,并且还没有释放该锁的线程所拥有。当锁没有被另一个线程所拥有时,调用 lock 的线程将成功获取该锁并返回。如果当前线程已经拥有该锁,此方法将立即返回。可以使用 isHeldByCurrentThread() 和 getHoldCount() 方法来检查此情况是否发生。

    此类的构造方法接受一个可选的公平 参数。当设置为 true 时,在多个线程的争用下,这些锁倾向于将访问权授予等待时间最长的线程。否则此锁将无法保证任何特定访问顺序。与采用默认设置(使用不公平锁)相比,使用公平锁的程序在许多线程访问时表现为很低的总体吞吐量(即速度很慢,常常极其慢),但是在获得锁和保证锁分配的均衡性时差异较小。不过要注意的是,公平锁不能保证线程调度的公平性。因此,使用公平锁的众多线程中的一员可能获得多倍的成功机会,这种情况发生在其他活动线程没有被处理并且目前并未持有锁时。还要注意的是,未定时的 tryLock 方法并没有使用公平设置。因为即使其他线程正在等待,只要该锁是可用的,此方法就可以获得成功。

    JDK:http://www.xasxt.com/java/api/java/util/concurrent/locks/ReentrantLock.html

    Java代码  收藏代码
    1. /*构造方法摘要 
    2. ReentrantLock()  
    3.           创建一个 ReentrantLock 的实例。 
    4. ReentrantLock(boolean fair)  
    5.           创建一个具有给定公平策略的 ReentrantLock。 
    6. */  

      

    Java代码  收藏代码
    1. /**public void lock() 
    2. 获取锁。 
    3. 如果该锁没有被另一个线程保持,则获取该锁并立即返回,将锁的保持计数设置为 1。 
    4. 如果当前线程已经保持该锁,则将保持计数加 1,并且该方法立即返回。 
    5. 如果该锁被另一个线程保持,则出于线程调度的目的,禁用当前线程,并且在获得锁之前,该线程将一直处于休眠状态,此时锁保持计数被设置为 1。 
    6. */  

      

    ReentrantLock 的lock机制有2种,忽略中断锁和响应中断锁,这给我们带来了很大的灵活性。比如:如果A、B 2个线程去竞争锁,A线程得到了锁,B线程等待,但是A线程这个时候实在有太多事情要处理,就是 一直不返回,B线程可能就会等不及了,想中断自己,不再等待这个锁了,转而处理其他事情。这个时候ReentrantLock就提供了2种机制,第一,B线程中断自己(或者别的线程中断它),但是ReentrantLock 不去响应,继续让B线程等待,你再怎么中断,我全当耳边风(synchronized原语就是如此);第二,B线程中断自己(或者别的线程中断它),ReentrantLock 处理了这个中断,并且不再等待这个锁的到来,完全放弃。请看例子:

    Example1:

    Java代码  收藏代码
    1. package test;  
    2.   
    3. public interface IBuffer {  
    4.     public void write();  
    5.     public void read() throws InterruptedException;  
    6. }  

    使用Synchronized:

    Java代码  收藏代码
    1. package test;  
    2.   
    3. public class Buffer implements IBuffer {  
    4.   
    5.     private Object lock;  
    6.   
    7.     public Buffer() {  
    8.         lock = this;  
    9.     }  
    10.   
    11.     public void write() {  
    12.         synchronized (lock) {  
    13.             long startTime = System.currentTimeMillis();  
    14.             System.out.println("开始往这个buff写入数据…");  
    15.             for (;;)// 模拟要处理很长时间  
    16.             {  
    17.                 if (System.currentTimeMillis() - startTime > Integer.MAX_VALUE)  
    18.                     break;  
    19.             }  
    20.             System.out.println("终于写完了");  
    21.         }  
    22.     }  
    23.   
    24.     public void read() {  
    25.         synchronized (lock) {  
    26.             System.out.println("从这个buff读数据");  
    27.         }  
    28.     }  
    29.   
    30. }  

       使用ReentrantLock:

    Java代码  收藏代码
    1. package test;  
    2. import java.util.concurrent.locks.ReentrantLock;  
    3. public class BufferInterruptibly implements IBuffer {  
    4.   
    5.     private ReentrantLock lock = new ReentrantLock();  
    6.   
    7.     public void write() {  
    8.         lock.lock();  
    9.         try {  
    10.             long startTime = System.currentTimeMillis();  
    11.             System.out.println("开始往这个buff写入数据…");  
    12.             for (;;)// 模拟要处理很长时间  
    13.             {  
    14.                 if (System.currentTimeMillis() - startTime > Integer.MAX_VALUE)  
    15.                     break;  
    16.             }  
    17.             System.out.println("终于写完了");  
    18.         } finally {  
    19.             lock.unlock();  
    20.         }  
    21.     }  
    22.   
    23.     public void read() throws InterruptedException{  
    24.         lock.lockInterruptibly();// 注意这里,可以响应中断  
    25.         try {  
    26.             System.out.println("从这个buff读数据");  
    27.         } finally {  
    28.             lock.unlock();  
    29.         }  
    30.     }  
    31.   
    32. }  

      

    测试类(注意那两个线程不是内部类!):

    Java代码  收藏代码
    1. package test;  
    2.   
    3. public class Test {  
    4.      //是用ReentrantLock,还是用synchronized  
    5.     public static boolean useSynchronized = false;  
    6.     public static void main(String[] args) {  
    7.         IBuffer buff = null;  
    8.         if(useSynchronized){  
    9.             buff = new Buffer();  
    10.         }else{  
    11.             buff = new BufferInterruptibly();      
    12.         }  
    13.         final Writer writer = new Writer(buff);  
    14.         final Reader reader = new Reader(buff);  
    15.         writer.start();  
    16.         reader.start();  
    17.         new Thread(new Runnable() {  
    18.             public void run() {  
    19.                 long start = System.currentTimeMillis();  
    20.                 for (;;) {  
    21.                     // 等5秒钟去中断读  
    22.                     if (System.currentTimeMillis() - start > 5000) {  
    23.                         System.out.println("不等了,尝试中断");  
    24.                         reader.interrupt();  
    25.                         break;  
    26.                     }  
    27.   
    28.                 }  
    29.   
    30.             }  
    31.         }).start();  
    32.     }      
    33. }  
    34.   
    35.     class Writer extends Thread {     
    36.         private IBuffer buff;  
    37.       
    38.         public Writer(IBuffer buff) {  
    39.             this.buff = buff;  
    40.         }  
    41.       
    42.         @Override  
    43.         public void run() {  
    44.             buff.write();  
    45.         }  
    46.     }  
    47.       
    48.     class Reader extends Thread {  
    49.         private IBuffer buff;  
    50.         public Reader(IBuffer buff) {  
    51.             this.buff = buff;  
    52.         }  
    53.         @Override  
    54.         public void run() {  
    55.             try {  
    56.                 buff.read();  
    57.             } catch (InterruptedException e) {  
    58.                 System.out.println("我不读了");     
    59.             }  
    60.             System.out.println("读结束");  
    61.         }  
    62.     }  

     结果:

    使用ReentrantLock时:

    开始往这个buff写入数据…

    不等了,尝试中断

    我不读了

    读结束

    使用Synchronized时:

    开始往这个buff写入数据…

    不等了,尝试中断

    实例来源:http://blog.csdn.net/quqi99/article/details/5298017

    实例2:

    http://junlas.iteye.com/blog/846460

    实例3:

    http://www.blogjava.net/killme2008/archive/2007/09/14/145195.html

    重要:

    一个证明可中断的例子:http://yanxuxin.iteye.com/blog/566713

    关于多线程问题,signalAll,await问题:http://www.iteye.com/problems/72378

    ReentrantLock :http://hujin.iteye.com/blog/479689

    java的concurrent用法详解:

    http://www.open-open.com/bbs/view/1320131360999

    ReentrantLock-互斥同步器:

    http://www.cnblogs.com/mandela/archive/2011/04/08/2009810.html

    一个重要Example:

    Java代码  收藏代码
    1. package tags;  
    2.   
    3. import java.util.Calendar;  
    4.   
    5. public class TestLock {  
    6.     private ReentrantLock lock = null;  
    7.       
    8.     public int data = 100;     // 用于线程同步访问的共享数据  
    9.   
    10.     public TestLock() {  
    11.         lock = new ReentrantLock(); // 创建一个自由竞争的可重入锁  
    12.     }  
    13.     public ReentrantLock getLock() {  
    14.         return lock;  
    15.     }  
    16.       
    17.     public void testReentry() {  
    18.         lock.lock();  
    19.         Calendar now = Calendar.getInstance();  
    20.         System.out.println(now.getTime() + " " + Thread.currentThread() + " get lock.");  
    21.     }  
    22.   
    23.     public static void main(String[] args) {  
    24.         TestLock tester = new TestLock();  
    25.   
    26.         //1、测试可重入  
    27.         tester.testReentry();  
    28.         tester.testReentry(); // 能执行到这里而不阻塞,表示锁可重入  
    29.         tester.testReentry(); // 再次重入  
    30.   
    31.         // 释放重入测试的锁,要按重入的数量解锁,否则其他线程无法获取该锁。  
    32.         tester.getLock().unlock();  
    33.         tester.getLock().unlock();  
    34.         tester.getLock().unlock();  
    35.   
    36.         //2、测试互斥  
    37.         // 启动3个线程测试在锁保护下的共享数据data的访问  
    38.         new Thread(new workerThread(tester)).start();  
    39.         new Thread(new workerThread(tester)).start();  
    40.         new Thread(new workerThread(tester)).start();  
    41.     }  
    42.   
    43.   
    44.     // 线程调用的方法  
    45.     public void testRun() throws Exception {  
    46.         lock.lock();  
    47.   
    48.         Calendar now = Calendar.getInstance();  
    49.         try {  
    50.             // 获取锁后显示 当前时间 当前调用线程 共享数据的值(并使共享数据 + 1)  
    51.             System.out.println(now.getTime() + " " + Thread.currentThread()+ " accesses the data " + data++);  
    52.             Thread.sleep(1000);  
    53.         } catch (Exception e) {  
    54.             e.printStackTrace();  
    55.         } finally {  
    56.             lock.unlock();  
    57.         }  
    58.     }  
    59. }  
    60.   
    61. // 工作线程,调用TestServer.testRun  
    62. class workerThread implements Runnable {  
    63.   
    64.     private TestLock tester = null;  
    65.   
    66.     public workerThread(TestLock testLock) {  
    67.         this.tester = testLock;  
    68.     }  
    69.   
    70.     public void run() {  
    71.         try {  
    72.             tester.testRun();  
    73.         } catch (Exception e) {  
    74.             e.printStackTrace();  
    75.         }  
    76.     }  
    77. }  

    Example3:

    Java代码  收藏代码
    1. package tags;  
    2. import java.util.concurrent.locks.ReentrantLock;  
    3.   
    4. public class ReentrantLockSample {  
    5.   
    6.     public static void main(String[] args) {  
    7.         testSynchronized();  
    8.         //testReentrantLock();  
    9.     }  
    10.   
    11.     public static void testReentrantLock() {  
    12.         final SampleSupport1 support = new SampleSupport1();  
    13.         Thread first = new Thread(new Runnable() {  
    14.             public void run() {  
    15.                 try {  
    16.                     support.doSomething();  
    17.                 }  
    18.                 catch (InterruptedException e) {  
    19.                     e.printStackTrace();  
    20.                 }  
    21.             }  
    22.         });  
    23.   
    24.         Thread second = new Thread(new Runnable() {  
    25.             public void run() {  
    26.                 try {  
    27.                     support.doSomething();  
    28.                 }  
    29.                 catch (InterruptedException e) {  
    30.                     System.out.println("Second Thread Interrupted without executing counter++,beacuse it waits a long time.");  
    31.                 }  
    32.             }  
    33.         });  
    34.   
    35.         executeTest(first, second);  
    36.     }  
    37.   
    38.     public static void testSynchronized() {  
    39.         final SampleSupport2 support2 = new SampleSupport2();  
    40.   
    41.         Runnable runnable = new Runnable() {  
    42.             public void run() {  
    43.                 support2.doSomething();  
    44.             }  
    45.         };  
    46.   
    47.         Thread third = new Thread(runnable);  
    48.         Thread fourth = new Thread(runnable);  
    49.   
    50.         executeTest(third, fourth);  
    51.     }  
    52.   
    53.     /** 
    54.      * Make thread a run faster than thread b, 
    55.      * then thread b will be interruted after about 1s. 
    56.      * @param a 
    57.      * @param b 
    58.      */  
    59.     public static void executeTest(Thread a, Thread b) {  
    60.         a.start();  
    61.         try {  
    62.             Thread.sleep(100);  
    63.             b.start(); // The main thread sleep 100ms, and then start the second thread.  
    64.   
    65.             Thread.sleep(1000);  
    66.     // 1s later, the main thread decided not to allow the second thread wait any longer.  
    67.             b.interrupt();   
    68.         }  
    69.         catch (InterruptedException e) {  
    70.             e.printStackTrace();  
    71.         }  
    72.     }  
    73. }  
    74.   
    75. abstract class SampleSupport {  
    76.   
    77.     protected int counter;  
    78.   
    79.     /** 
    80.      * A simple countdown,it will stop after about 5s.  
    81.      */  
    82.     public void startTheCountdown() {  
    83.         long currentTime = System.currentTimeMillis();  
    84.         for (;;) {  
    85.             long diff = System.currentTimeMillis() - currentTime;  
    86.             if (diff > 5000) {  
    87.                 break;  
    88.             }  
    89.         }  
    90.     }  
    91. }  
    92.   
    93. class SampleSupport1 extends SampleSupport {  
    94.   
    95.     private final ReentrantLock lock = new ReentrantLock();  
    96.   
    97.     public void doSomething() throws InterruptedException {  
    98.         lock.lockInterruptibly(); // (1)  
    99.         System.out.println(Thread.currentThread().getName() + " will execute counter++.");  
    100.         startTheCountdown();  
    101.         try {  
    102.             counter++;  
    103.         }  
    104.         finally {  
    105.             lock.unlock();  
    106.         }  
    107.     }  
    108. }  
    109.   
    110. class SampleSupport2 extends SampleSupport {  
    111.   
    112.     public synchronized void doSomething() {  
    113.         System.out.println(Thread.currentThread().getName() + " will execute counter++.");  
    114.         startTheCountdown();  
    115.         counter++;  
    116.     }  
    117. }  

     在这个例子中,辅助类SampleSupport提供一个倒计时的功能startTheCountdown(),这里倒计时5s左右。SampleSupport1,SampleSupport2继承其并分别的具有doSomething()方法,任何进入方法的线程会运行5s左右之后counter++然后离开方法释放锁。SampleSupport1是使用ReentrantLock机制,SampleSupport2是使用synchronized机制。 


        testSynchronized()和testReentrantLock()都分别开启两个线程执行测试方法executeTest(),这个方法会让一个线程先启动,另一个过100ms左右启动,并且隔1s左右试图中断后者。结果正如之前提到的第二点:interrupt()对于synchronized是没有作用的,它依然会等待5s左右获得锁执行counter++;而ReentrantLock机制可以保证在线程还未获得并且试图获得锁时如果发现线程中断,则抛出异常清除中断标记退出竞争。所以testReentrantLock()中second线程不会继续去竞争锁,执行异常内的打印语句后线程运行结束。 

    来源:http://yanxuxin.iteye.com/blog/566713

    Example4:

    三个线程,线程名分别为A、B、C,设计程序使得三个线程循环打印“ABC”10次后终止。如:ABCABCABCABCABCABCABCABCABCABC

    Java代码  收藏代码
    1. package tags;  
    2.   
    3. import java.util.concurrent.locks.ReentrantLock;  
    4.   
    5. public class ReentrantLockPractice {  
    6.   
    7.     static ReentrantLock lock = new ReentrantLock();  
    8.     private static String[] threadArr = {"A","B","C"};  
    9.       
    10.     public static void main(String[] args){  
    11.         ReentrantLockPractice pc = new ReentrantLockPractice();  
    12.         pc.startDemo();  
    13.     }  
    14.       
    15.     void startDemo(){  
    16.         for(int i = 0;i<10;i++){  
    17.             for(String name : threadArr){  
    18.                 TestThread t = new TestThread(name);  
    19.                 t.start();  
    20.                 try {  
    21.                     Thread.sleep(100);  
    22.                 } catch (InterruptedException e) {  
    23.                     e.printStackTrace();  
    24.                 }  
    25.             }  
    26.         }  
    27.     }  
    28.       
    29.   
    30.     class TestThread extends Thread{  
    31.           
    32.         //自定义线程名字  
    33.         TestThread(String str){  
    34.             super(str);           
    35.         }  
    36.           
    37.         public void run(){  
    38.             try {  
    39.                 lock.lockInterruptibly();  
    40.                 System.out.print(Thread.currentThread().getName());  
    41.             } catch (InterruptedException e) {  
    42.                 e.printStackTrace();  
    43.             } finally{  
    44.                 lock.unlock();  
    45.             }     
    46.         }  
    47.     }  
    48.       
    49. }  

     注意与Example2的区别,一个线材类定义在内部,一个在外部,注意区别。

     其他方法:

    http://hxraid.iteye.com/blog/607228

    相同:ReentrantLock提供了synchronized类似的功能和内存语义。

    不同:

    1.ReentrantLock功能性方面更全面,比如时间锁等候,可中断锁等候,锁投票等,因此更有扩展性。在多个条件变量和高度竞争锁的地方,用ReentrantLock更合适,ReentrantLock还提供了Condition,对线程的等待和唤醒等操作更加灵活,一个ReentrantLock可以有多个Condition实例,所以更有扩展性。

    2.ReentrantLock必须在finally中释放锁,否则后果很严重,编码角度来说使用synchronized更加简单,不容易遗漏或者出错。

    3.ReentrantLock 的性能比synchronized会好点。

    4.ReentrantLock提供了可轮询的锁请求,他可以尝试的去取得锁,如果取得成功则继续处理,取得不成功,可以等下次运行的时候处理,所以不容易产生死锁,而synchronized则一旦进入锁请求要么成功,要么一直阻塞,所以更容易产生死锁。

    1、Lock的某些方法可以决定多长时间内尝试获取锁,如果获取不到就抛异常,这样就可以一定程度上减轻死锁的可能性。

    如果锁被另一个线程占据了,synchronized只会一直等待,很容易错序死锁 

    2、synchronized的话,锁的范围是整个方法或synchronized块部分;而Lock因为是方法调用,可以跨方法,灵活性更大 

    3、便于测试,单元测试时,可以模拟Lock,确定是否获得了锁,而synchronized就没办法了

    ReentrantLock比synchronized 强大在哪儿?

    简单说: 

    1、ReentrantLock可以实现fair lock 

    public ReentrantLock(boolean fair) {   

        sync = (fair)? new FairSync() : new NonfairSync();  

    }  

    所谓fair lock就是看获得锁的顺序是不是和申请锁的时间的顺序是一致的 

    2、ReentrantLock支持中断处理 

    public final void acquireInterruptibly(int arg) throws InterruptedException {  

        if (Thread.interrupted())  

            throw new InterruptedException();  

        if (!tryAcquire(arg))  

            doAcquireInterruptibly(arg);  

    }  

    就是说那些持有锁的线程一直不释放,正在等待的线程可以放弃等待。 

    3、ReentrantLock可以和condition结合使用 

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

    }  

    内置锁synchronized

    显式锁Lock

    ReentrantLock代码剖析之ReentrantLock.lock

    ReentrantLock中tryLock的使用问题(注意循环)

    synchronized是可重入锁

    如果一个获取锁的线程调用其它的synchronized修饰的方法,会发生什么?

    从设计上讲,当一个线程请求一个由其他线程持有的对象锁时,该线程会阻塞。当线程请求自己持有的对象锁时,如果该线程是重入锁,请求就会成功,否则阻塞。

    我们回来看synchronized,synchronized拥有强制原子性的内部锁机制,是一个可重入锁。因此,在一个线程使用synchronized方法时调用该对象另一个synchronized方法,即一个线程得到一个对象锁后再次请求该对象锁,是永远可以拿到锁的。

    在Java内部,同一个线程调用自己类中其他synchronized方法/块时不会阻碍该线程的执行,同一个线程对同一个对象锁是可重入的,同一个线程可以获取同一把锁多次,也就是可以多次重入。原因是Java中线程获得对象锁的操作是以线程为单位的,而不是以调用为单位的。

    synchronized可重入锁的实现

    每个锁关联一个线程持有者和一个计数器。当计数器为0时表示该锁没有被任何线程持有,那么任何线程都都可能获得该锁而调用相应方法。当一个线程请求成功后,JVM会记下持有锁的线程,并将计数器计为1。此时其他线程请求该锁,则必须等待。而该持有锁的线程如果再次请求这个锁,就可以再次拿到这个锁,同时计数器会递增。当线程退出一个synchronized方法/块时,计数器会递减,如果计数器为0则释放该锁。

  • 相关阅读:
    POJ 1061
    LightOJ 1104
    扩展欧几里得算法
    2015 HUAS Summer Trainning #5 E
    2015 HUAS Summer Trainning #5 C
    2015 HUAS Summer Trainning #5 B
    2015 HUAS Summer Trainning #5 A
    2015 HUAS Summer Trainning #4 D
    2015 HUAS Summer Trainning #4 C
    2015 HUAS Summer Trainning #4 B
  • 原文地址:https://www.cnblogs.com/jtlgb/p/8145292.html
Copyright © 2020-2023  润新知