Synchronisation des threads
Lors de l'appel du même objet entre plusieurs threads, pour la sécurité et la précision du fonctionnement, l'objet doit être synchronisé pour garantir que le résultat de l'objet est correct lorsqu'il est utilisé par chaque thread, et que l'état de l'objet est raisonnablement , cette partie implique des points de connaissances tels que la synchronisation et les thread locks. Cette partie n'implique que les notions de verrouillage synchronisé et de synchronisation (Lock).
synchronized
Le mot-clé synchronisé peut modifier des objets et des méthodes. L'utilisation habituelle est la suivante :
//同步代码块 synchronized(Object object){ ... } //或者 //同步方法 public synchronized void test(){ ... }
Il existe un concept de moniteur de synchronisation, comme ci-dessus L'objet objet du bloc de code synchronisé et l'objet this de la méthode synchronisée seront surveillés de manière synchrone. Lorsque plusieurs threads appellent un bloc de code ou une méthode synchronisé en même temps, un seul thread peut obtenir le verrou d'objet surveillé synchronisé. à tout moment.Après avoir exécuté le code, le verrou sera libéré plus tard. Pendant cette période, les autres threads appelants ne peuvent qu'attendre que le verrou soit libéré avant d'appeler.
La méthode sell de la classe SellRunnable mentionnée ci-dessus utilise également synchronisé. Le code ci-dessus s'exécute trop rapidement, il ne peut donc pas être détecté si vous le modifiez, vous pouvez comprendre la différence entre synchronisé et non.
public class ThreadTest { public static void main(String[] args) { SellRunnable sellRunnable = new SellRunnable(); Thread thread1 = new Thread(sellRunnable, "1"); Thread thread2 = new Thread(sellRunnable, "2"); Thread thread3 = new Thread(sellRunnable, "3"); thread2.start(); thread1.start(); thread3.start(); } } class SellRunnable implements Runnable { //有十张票 int index = 10; public void sell() { if (index >= 1) { try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } index--; System.out.println("售货窗口:" + Thread.currentThread().getName() + " 卖出了一张票,剩余: " + index); } else { System.out.println("售货窗口:" + Thread.currentThread().getName() + " 买票时没票了"); } } @Override public void run() { while (index > 0) { System.out.println("售货窗口:" + Thread.currentThread().getName() + " 开始买票"); sell(); } } } //执行结果: 售货窗口:1 开始买票 售货窗口:2 开始买票 售货窗口:3 开始买票 售货窗口:2 卖出了一张票,剩余:9 售货窗口:2 开始买票 售货窗口:1 卖出了一张票,剩余:9 售货窗口:1 开始买票 售货窗口:3 卖出了一张票,剩余:8 售货窗口:3 开始买票 售货窗口:1 卖出了一张票,剩余:6 售货窗口:1 开始买票 售货窗口:2 卖出了一张票,剩余:6 售货窗口:2 开始买票 售货窗口:3 卖出了一张票,剩余:5 售货窗口:3 开始买票 售货窗口:1 卖出了一张票,剩余:4 售货窗口:1 开始买票 售货窗口:2 卖出了一张票,剩余:3 售货窗口:3 卖出了一张票,剩余:2 售货窗口:3 开始买票 售货窗口:2 开始买票 售货窗口:3 卖出了一张票,剩余:1 售货窗口:2 卖出了一张票,剩余:0 售货窗口:1 卖出了一张票,剩余:1 Process finished with exit code 0 //可以看到,票数减少是错误的 //sell方法添加synchronized修饰符后 执行结果: public synchronized void sell() { if (index >= 1) { try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } index--; System.out.println("售货窗口:" + Thread.currentThread().getName() + " 卖出了一张票,剩余: " + index); } else { System.out.println("售货窗口:" + Thread.currentThread().getName() + " 买票时没票了"); } } 售货窗口:2 开始买票 售货窗口:3 开始买票 售货窗口:1 开始买票 售货窗口:2 卖出了一张票,剩余:9 售货窗口:2 开始买票 售货窗口:1 卖出了一张票,剩余:8 售货窗口:1 开始买票 售货窗口:3 卖出了一张票,剩余:7 售货窗口:3 开始买票 售货窗口:1 卖出了一张票,剩余:6 售货窗口:1 开始买票 售货窗口:2 卖出了一张票,剩余:5 售货窗口:2 开始买票 售货窗口:1 卖出了一张票,剩余:4 售货窗口:1 开始买票 售货窗口:1 卖出了一张票,剩余:3 售货窗口:1 开始买票 售货窗口:3 卖出了一张票,剩余:2 售货窗口:3 开始买票 售货窗口:1 卖出了一张票,剩余:1 售货窗口:1 开始买票 售货窗口:1 卖出了一张票,剩余:0 售货窗口:2 买票时没票了 售货窗口:3 买票时没票了 Process finished with exit code 0 // 可以看到,票数是正常减少的
Après la synchronisation ci-dessus de la méthode de vente, à un certain moment, un seul thread appellera cette méthode, donc le résultat obtenu en jugeant l'index est le résultat correct.
Pendant la synchronisation ci-dessus, la sécurité des threads est assurée en réduisant l'efficacité de fonctionnement. Pour cette raison, ne synchronisez pas les méthodes et les objets inutiles dans les classes d'utilisation des threads, et synchronisez uniquement les ressources ou les objets avec concurrence.
Après l'identification de la synchronisation, les points suivants peuvent libérer le verrou :
Bloc de code, exécution de la méthode terminée (achèvement normal, retour ou pause, exception levée)
Appeler l'attente La méthode est utilisée pour mettre en pause le thread en cours.
Lorsque le thread exécute un bloc de code synchronisé, les méthodes sleep et rendement ne libéreront pas le verrou de synchronisation, et la méthode suspend ne sera pas non plus suspendue (essayez d'éviter d'utiliser suspendre et reprendre pour manipuler l'état du thread pendant le fonctionnement du thread, ce qui mène facilement à une impasse. )
Verrouillage synchronisé
Le synchronisé mentionné ci-dessus est un mot-clé en Java, et il est également mentionné dans Lors de la mise en veille ou de l'exécution d'opérations d'E/S, le thread ne libère pas le verrou de thread et les autres threads doivent attendre. Cela réduit parfois l'efficacité de l'exécution, donc une alternative qui peut libérer le verrou de thread lorsque le thread est bloqué est apparue. juste pour résoudre ce problème.
Lock est une classe en java, dans le package java.util.concurrent.locks, le code spécifique est le suivant :
public interface Lock { void lock();//加锁 void lockInterruptibly() throws InterruptedException;//加锁 boolean tryLock();//加锁 boolean tryLock(long time, TimeUnit unit) throws InterruptedException;//加锁 void unlock();//释放锁 Condition newCondition();//线程协作中用到 }
Une sous-classe d'implémentation de l'interface Lock est ReentrantLock, en java . Sous le package util.concurrent.locks, le code source de ReentrantLock est le suivant :
public class ReentrantLock implements Lock, Serializable { private static final long serialVersionUID = 7373984872572414699L; private final ReentrantLock.Sync sync; public ReentrantLock() { this.sync = new ReentrantLock.NonfairSync(); } public ReentrantLock(boolean var1) {//是否创建公平锁 this.sync = (ReentrantLock.Sync)(var1?new ReentrantLock.FairSync():new ReentrantLock. NonfairSync()); } public void lock() { this.sync.lock(); } public void lockInterruptibly() throws InterruptedException { this.sync.acquireInterruptibly(1); } public boolean tryLock() { return this.sync.nonfairTryAcquire(1); } public boolean tryLock(long var1, TimeUnit var3) throws InterruptedException { return this.sync.tryAcquireNanos(1, var3.toNanos(var1)); } public void unlock() { this.sync.release(1); } public Condition newCondition() { return this.sync.newCondition(); } public int getHoldCount() {//当前线程持有该锁的数量 return this.sync.getHoldCount(); } public boolean isHeldByCurrentThread() {//该锁是否被当前线程持有 return this.sync.isHeldExclusively(); } public boolean isLocked() {//是否被其他线程持有该锁 return this.sync.isLocked(); } public final boolean isFair() {//是否是公平锁 return this.sync instanceof ReentrantLock.FairSync; } protected Thread getOwner() {//当前锁的持有线程 return this.sync.getOwner(); } public final boolean hasQueuedThreads() {//是否有线程在等待该锁 return this.sync.hasQueuedThreads(); } public final boolean hasQueuedThread(Thread var1) {//目标线程是否在等待该锁 return this.sync.isQueued(var1); } public final int getQueueLength() {//等待该锁线程的数量 return this.sync.getQueueLength(); } protected Collection<Thread> getQueuedThreads() {//获取所有等待该锁的线程集合 return this.sync.getQueuedThreads(); } ... }
Comment utiliser Lock
lock<. 🎜>
lock() est utilisé pour acquérir le verrou Si le verrou est occupé par d'autres threads, il attendra.public class LockTest { public static void main(String[] args) { com.test.java.SellRunnable sellRunnable = new com.test.java.SellRunnable(); Thread thread1 = new Thread(sellRunnable, "1号窗口"); Thread thread2 = new Thread(sellRunnable, "2号窗口"); Thread thread3 = new Thread(sellRunnable, "3号窗口"); thread1.start(); thread2.start(); thread3.start(); } }
public class SellRunnable implements Runnable { //有十张票 int index = 10; Lock lock = new ReentrantLock(); public void sell() { try { lock.lock(); System.out.println("售货柜台:" + Thread.currentThread().getName() + "获取了票源+++++"); if (index >= 1) { index--; System.out.println("售货柜台:" + Thread.currentThread().getName() + "卖出了一张票,剩余: " + index); } else { System.out.println("售货柜台:" + Thread.currentThread().getName() + "买票时没票了000"); } } finally { lock.unlock(); } } @Override public void run() { while (index > 0) { try { Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } sell(); } } }
售货柜台:3号窗口获取了票源+++++ 售货柜台:3号窗口卖出了一张票,剩余:9 售货柜台:1号窗口获取了票源+++++ 售货柜台:1号窗口卖出了一张票,剩余:8 售货柜台:2号窗口获取了票源+++++ 售货柜台:2号窗口卖出了一张票,剩余:7 售货柜台:1号窗口获取了票源+++++ 售货柜台:1号窗口卖出了一张票,剩余:6 售货柜台:3号窗口获取了票源+++++ 售货柜台:3号窗口卖出了一张票,剩余:5 售货柜台:2号窗口获取了票源+++++ 售货柜台:2号窗口卖出了一张票,剩余:4 售货柜台:3号窗口获取了票源+++++ 售货柜台:3号窗口卖出了一张票,剩余:3 售货柜台:1号窗口获取了票源+++++ 售货柜台:1号窗口卖出了一张票,剩余:2 售货柜台:2号窗口获取了票源+++++ 售货柜台:2号窗口卖出了一张票,剩余:1 售货柜台:3号窗口获取了票源+++++ 售货柜台:3号窗口卖出了一张票,剩余:0 售货柜台:1号窗口获取了票源+++++ 售货柜台:1号窗口买票时没票了000 售货柜台:2号窗口获取了票源+++++ 售货柜台:2号窗口买票时没票了000 Process finished with exit code 0 //每一个窗口都随机获取票源、然后卖出票
public class SellRunnable implements Runnable { //有十张票 int index = 10; Lock lock = new ReentrantLock(); public void sell() { if (lock.tryLock()) { try { System.out.println("售货柜台:" + Thread.currentThread().getName() + "获取了票源+++++"); if (index >= 1) { index--; System.out.println("售货柜台:" + Thread.currentThread().getName() + "卖出了一张票,剩余:" + index); } else { System.out.println("售货柜台:" + Thread.currentThread().getName() + "买票时没票了000"); } } finally { lock.unlock(); } } else { System.out.println("售货柜台:" + Thread.currentThread().getName()+"没有获取票源!!!"); } } @Override public void run() { while (index > 0) { try { Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } sell(); } } }
售货柜台:1号窗口获取了票源+++++ 售货柜台:3号窗口没有获取票源!!! 售货柜台:2号窗口没有获取票源!!! 售货柜台:1号窗口卖出了一张票,剩余:9 售货柜台:2号窗口没有获取票源!!! 售货柜台:3号窗口获取了票源+++++ 售货柜台:3号窗口卖出了一张票,剩余:8 售货柜台:1号窗口获取了票源+++++ 售货柜台:1号窗口卖出了一张票,剩余:7 售货柜台:1号窗口没有获取票源!!! 售货柜台:3号窗口没有获取票源!!! 售货柜台:2号窗口获取了票源+++++ 售货柜台:2号窗口卖出了一张票,剩余:6 售货柜台:1号窗口获取了票源+++++ 售货柜台:2号窗口没有获取票源!!! 售货柜台:3号窗口没有获取票源!!! 售货柜台:1号窗口卖出了一张票,剩余:5 售货柜台:2号窗口获取了票源+++++ 售货柜台:1号窗口没有获取票源!!! 售货柜台:2号窗口卖出了一张票,剩余:4 售货柜台:3号窗口没有获取票源!!! 售货柜台:1号窗口获取了票源+++++ 售货柜台:2号窗口没有获取票源!!! 售货柜台:3号窗口没有获取票源!!! 售货柜台:1号窗口卖出了一张票,剩余:3 售货柜台:1号窗口获取了票源+++++ 售货柜台:1号窗口卖出了一张票,剩余:2 售货柜台:2号窗口获取了票源+++++ 售货柜台:3号窗口没有获取票源!!! 售货柜台:2号窗口卖出了一张票,剩余:1 售货柜台:1号窗口获取了票源+++++ 售货柜台:1号窗口卖出了一张票,剩余:0 售货柜台:3号窗口没有获取票源!!! 售货柜台:2号窗口没有获取票源!!! Process finished with exit code 0//没有获取到货源的票口,就直接没有等待,进入下次买票
public class SellRunnable implements Runnable { //有十张票 int index = 10; Lock lock = new ReentrantLock(); public void sell() { try { if (lock.tryLock(1000, TimeUnit.MILLISECONDS)) { try { System.out.println("售货柜台:" + Thread.currentThread().getName() + "获取了票源+++++"); if (index >= 1) { index--; System.out.println("售货柜台:" + Thread.currentThread().getName() +"卖出了一张票,剩余:" + index); } else { System.out.println("售货柜台:" + Thread.currentThread(). getName() + "买票时没票了000"); } try { Thread.sleep(2000);//人为加入买票时间 } catch (InterruptedException e) { e.printStackTrace(); } } finally { lock.unlock(); } } else { System.out.println("售货柜台:" + Thread.currentThread().getName() + "没有获取票源!!!"); } } catch (InterruptedException e) { e.printStackTrace(); } } @Override public void run() { while (index > 0) { try { Thread.sleep(500);//要不执行太快,看不出效果 } catch (InterruptedException e) { e.printStackTrace(); } sell(); } } }
售货柜台:1号窗口获取了票源+++++ 售货柜台:1号窗口卖出了一张票,剩余:9 售货柜台:2号窗口没有获取票源!!! 售货柜台:3号窗口没有获取票源!!! 售货柜台:2号窗口获取了票源+++++ 售货柜台:2号窗口卖出了一张票,剩余:8 售货柜台:3号窗口没有获取票源!!! 售货柜台:1号窗口没有获取票源!!! 售货柜台:3号窗口获取了票源+++++ 售货柜台:3号窗口卖出了一张票,剩余:7 售货柜台:1号窗口没有获取票源!!! 售货柜台:2号窗口没有获取票源!!! 售货柜台:1号窗口获取了票源+++++ 售货柜台:1号窗口卖出了一张票,剩余:6 售货柜台:2号窗口没有获取票源!!! 售货柜台:3号窗口没有获取票源!!! 售货柜台:2号窗口获取了票源+++++ 售货柜台:2号窗口卖出了一张票,剩余:5 售货柜台:3号窗口没有获取票源!!! 售货柜台:1号窗口没有获取票源!!! 售货柜台:3号窗口获取了票源+++++ 售货柜台:3号窗口卖出了一张票,剩余:4 售货柜台:1号窗口没有获取票源!!! 售货柜台:2号窗口没有获取票源!!! 售货柜台:1号窗口获取了票源+++++ 售货柜台:1号窗口卖出了一张票,剩余:3 售货柜台:2号窗口没有获取票源!!! 售货柜台:3号窗口没有获取票源!!! 售货柜台:2号窗口获取了票源+++++ 售货柜台:2号窗口卖出了一张票,剩余:2 售货柜台:3号窗口没有获取票源!!! 售货柜台:1号窗口没有获取票源!!! 售货柜台:3号窗口获取了票源+++++ 售货柜台:3号窗口卖出了一张票,剩余:1 售货柜台:1号窗口没有获取票源!!! 售货柜台:2号窗口没有获取票源!!! 售货柜台:1号窗口获取了票源+++++ 售货柜台:1号窗口卖出了一张票,剩余:0 售货柜台:2号窗口没有获取票源!!! 售货柜台:3号窗口没有获取票源!!! Process finished with exit code 0 //当买票时间大约等待时间时,则没有获取票源的窗口不买票,进入下个买票机会
try { Thread.sleep(500);//人为加入买票时间 } catch (InterruptedException e) { e.printStackTrace(); }
售货柜台:1号窗口获取了票源+++++ 售货柜台:1号窗口卖出了一张票,剩余:9 售货柜台:2号窗口获取了票源+++++ 售货柜台:2号窗口卖出了一张票,剩余:8 售货柜台:3号窗口没有获取票源!!! 售货柜台:1号窗口获取了票源+++++ 售货柜台:1号窗口卖出了一张票,剩余:7 售货柜台:2号窗口获取了票源+++++ 售货柜台:2号窗口卖出了一张票,剩余:6 售货柜台:1号窗口获取了票源+++++ 售货柜台:1号窗口卖出了一张票,剩余:5 售货柜台:3号窗口没有获取票源!!! 售货柜台:2号窗口获取了票源+++++ 售货柜台:2号窗口卖出了一张票,剩余:4 售货柜台:3号窗口获取了票源+++++ 售货柜台:3号窗口卖出了一张票,剩余:3 售货柜台:1号窗口获取了票源+++++ 售货柜台:1号窗口卖出了一张票,剩余:2 售货柜台:2号窗口获取了票源+++++ 售货柜台:2号窗口卖出了一张票,剩余:1 售货柜台:3号窗口获取了票源+++++ 售货柜台:3号窗口卖出了一张票,剩余:0 售货柜台:1号窗口获取了票源+++++ 售货柜台:1号窗口买票时没票了000 售货柜台:2号窗口获取了票源+++++ 售货柜台:2号窗口买票时没票了000 Process finished with exit code 0 //等待时间内获取到票源了,也就卖出票了
public class ThreadTest { public static void main(String[] args) { SellRunnable sellRunnable = new SellRunnable(); Thread thread1 = new Thread(sellRunnable, "1号窗口"); Thread thread2 = new Thread(sellRunnable, "2号窗口"); Thread thread3 = new Thread(sellRunnable, "3号窗口"); thread1.start(); try { Thread.sleep(500);//确保窗口1号先获取锁 } catch (InterruptedException e) { e.printStackTrace(); } thread2.start(); thread3.start(); try { Thread.sleep(2000);//等待两秒后,打断窗口2、3的等待 } catch (InterruptedException e) { e.printStackTrace(); } thread2.interrupt(); thread3.interrupt(); } } SellRunnable中等待时间加长: try { Thread.sleep(5000);//人为加入买票时间 } catch (InterruptedException e) { e.printStackTrace(); }
售货柜台:1号窗口获取了票源+++++ 售货柜台:1号窗口卖出了一张票,剩余:9 售货柜台:3号窗口被打断了 //这个地方被打断了 售货柜台:2号窗口被打断了 //这个地方被打断了 售货柜台:2号窗口获取了票源+++++ 售货柜台:2号窗口卖出了一张票,剩余:8 售货柜台:3号窗口获取了票源+++++ 售货柜台:3号窗口卖出了一张票,剩余:7 售货柜台:1号窗口获取了票源+++++ 售货柜台:1号窗口卖出了一张票,剩余:6 售货柜台:2号窗口获取了票源+++++ 售货柜台:2号窗口卖出了一张票,剩余:5 售货柜台:3号窗口获取了票源+++++ 售货柜台:3号窗口卖出了一张票,剩余:4 售货柜台:1号窗口获取了票源+++++ 售货柜台:1号窗口卖出了一张票,剩余:3 售货柜台:2号窗口获取了票源+++++ 售货柜台:2号窗口卖出了一张票,剩余:2 售货柜台:3号窗口获取了票源+++++ 售货柜台:3号窗口卖出了一张票,剩余:1 售货柜台:1号窗口获取了票源+++++ 售货柜台:1号窗口卖出了一张票,剩余:0 售货柜台:2号窗口获取了票源+++++ 售货柜台:2号窗口买票时没票了000 售货柜台:3号窗口获取了票源+++++ 售货柜台:3号窗口买票时没票了000 Process finished with exit code 0
Comparaison entre synchronisé et Lock
Grâce au code ci-dessus, vous pouvez voir la différence entre Lock et synchronisé Plusieurs connexions et différences : Les deux sont des verrous réentrants Le verrouillage réentrant signifie qu'après qu'un thread a acquis le verrou d'objet, le thread peut à nouveau acquérir le verrou d'objet Non bloqué. Par exemple, après que plusieurs méthodes (ou une méthode appelée de manière récursive) dans la même classe soient synchronisées ou verrouillées, le même thread peut acquérir le verrou de l'objet sans être bloqué lors de l'appel de ces deux méthodes. Exemple de verrou non réentrant :public class Lock{ private boolean isLocked = false; public void lock(){ while(isLocked){ wait(); } isLocked = true; } public void unlock(){ isLocked = false; notify(); } } //使用方法: public class Test{ Lock lock = new Lock(); public void test1(){ lock.lock(); test2(); lock.unlock(); } public void test2(){ lock.lock(); ... lock.unlock(); } }
public class Lock{ private boolean isLocked = false; private Thread lockedThread = null; int lockedCount = 0; public void lock(){ Thread thread = Thread.currentThread(); while(isLocked && thread != lockedThread){ wait(); } isLocked = true; lockedCount++; lockedThread = thread; } public void unlock(){ Thread thread = Thread.currentThread(); if(thread == lockedThread){ lockedCount--; if(lockedCount == 0){ isLocked = false; lockedThread = null; notify(); } } } }
当一个线程B执行被锁的对象的代码时,发现线程A已经持有该锁,那么线程B就会进入等待,但是synchronized就无法中断该等待过程,而Lock就可以通过lockInterruptibly方法抛出异常从而中断等待,去处理别的事情。
Lock可创建公平锁,synchronized是非公平锁。
公平锁的意思是按照请求的顺序来获取锁,不平公锁就无法保证线程获取锁的先后次序。
Lock可以知道是否获取到锁,synchronized不可以。
synchronized在发生异常或者运行完毕,会自动释放线程占有的锁。而Lock需要主动释放锁,否则会锁死;
synchronized在阻塞时,别的线程无法获取锁,Lock可以(这也是lock设计的一个目的)。
读写锁
多个线程对同一个文件进行写操作时,会发生冲突所以需要加锁,但是对同一个文件进行读操作的时候,使用上面的方法会造成效率的降低,所以基于这种情况,产生了ReadWriteLock这个接口:
public interface ReadWriteLock { /** * Returns the lock used for reading. * * @return the lock used for reading. */ Lock readLock();//读的锁 /** * Returns the lock used for writing. * * @return the lock used for writing. */ Lock writeLock();//写的锁 }
这个接口的实现类是ReentrantReadWriteLock,其源代码如下:
public class ReentrantReadWriteLock implements ReadWriteLock, Serializable { private static final long serialVersionUID = -6992448646407690164L; private final ReentrantReadWriteLock.ReadLock readerLock; private final ReentrantReadWriteLock.WriteLock writerLock; ... public ReentrantReadWriteLock.WriteLock writeLock() {//获取write lock return this.writerLock; } public ReentrantReadWriteLock.ReadLock readLock() {//获取read lock return this.readerLock; } ... }
使用方法和Lock一样,使用到write时调用writeLock()方法获取lock进行加锁,使用到read时调用readLock()方法进行加锁,需要注意的知识点如下:
线程A占用写锁,线程B在申请写、读的时候需要等待。
线程A占用读锁,线程B在申请写操作时,需要等待。
线程A占用读锁,线程B获取读操作时可以获取到。
总结
如果需要效率提升,则建议使用Lock,如果效率要求不高,则synchronized满足使用条件,业务逻辑写起来也简单,不需要手动释放锁。
PHP中文网,有大量免费的JAVA入门教程,欢迎大家学习!
Ce qui précède est le contenu détaillé de. pour plus d'informations, suivez d'autres articles connexes sur le site Web de PHP en chinois!