>  기사  >  Java  >  Java 잠금을 완전히 마스터합니다(그래픽 및 텍스트 분석).

Java 잠금을 완전히 마스터합니다(그래픽 및 텍스트 분석).

WBOY
WBOY앞으로
2022-06-14 11:47:321907검색

이 기사는 배타적 잠금, 비관적 잠금, 낙관적 잠금, 공유 잠금 등을 포함하여 Java 잠금과 관련된 문제를 주로 소개하는 java에 대한 관련 지식을 제공합니다. 함께 살펴보고 모두에게 도움이 되기를 바랍니다.

Java 잠금을 완전히 마스터합니다(그래픽 및 텍스트 분석).

추천 학습: "java 비디오 튜토리얼"

낙관적 잠금과 비관적 잠금

비관적 잠금

비관적 잠금은 인생에서 비관적인 사람, 비관적인 사람에 해당합니다. 항상 생각합니다. 일이 잘못된 방향으로 흘러가는 것에 대해. 悲观锁对应于生活中悲观的人,悲观的人总是想着事情往坏的方向发展。

举个生活中的例子,假设厕所只有一个坑位了,悲观锁上厕所会第一时间把门反锁上,这样其他人上厕所只能在门外等候,这种状态就是「阻塞」了。

回到代码世界中,一个共享数据加了悲观锁,那线程每次想操作这个数据前都会假设其他线程也可能会操作这个数据,所以每次操作前都会上锁,这样其他线程想操作这个数据拿不到锁只能阻塞了。

Java 잠금을 완전히 마스터합니다(그래픽 및 텍스트 분석).

在 Java 语言中 synchronizedReentrantLock等就是典型的悲观锁,还有一些使用了 synchronized 关键字的容器类如 HashTable 等也是悲观锁的应用。

乐观锁

乐观锁 对应于生活中乐观的人,乐观的人总是想着事情往好的方向发展。

举个生活中的例子,假设厕所只有一个坑位了,乐观锁认为:这荒郊野外的,又没有什么人,不会有人抢我坑位的,每次关门上锁多浪费时间,还是不加锁好了。你看乐观锁就是天生乐观!

回到代码世界中,乐观锁操作数据时不会上锁,在更新的时候会判断一下在此期间是否有其他线程去更新这个数据。

Java 잠금을 완전히 마스터합니다(그래픽 및 텍스트 분석).

乐观锁可以使用版本号机制CAS算法实现。在 Java 语言中 java.util.concurrent.atomic包下的原子类就是使用CAS 乐观锁实现的。

两种锁的使用场景

悲观锁和乐观锁没有孰优孰劣,有其各自适应的场景。

乐观锁适用于写比较少(冲突比较小)的场景,因为不用上锁、释放锁,省去了锁的开销,从而提升了吞吐量。

如果是写多读少的场景,即冲突比较严重,线程间竞争激励,使用乐观锁就是导致线程不断进行重试,这样可能还降低了性能,这种场景下使用悲观锁就比较合适。

独占锁和共享锁

独占锁

独占锁是指锁一次只能被一个线程所持有。如果一个线程对数据加上排他锁后,那么其他线程不能再对该数据加任何类型的锁。获得独占锁的线程即能读数据又能修改数据。

Java 잠금을 완전히 마스터합니다(그래픽 및 텍스트 분석).

JDK中的synchronizedjava.util.concurrent(JUC)包中Lock的实现类就是独占锁。

共享锁

共享锁是指锁可被多个线程所持有。如果一个线程对数据加上共享锁后,那么其他线程只能对数据再加共享锁,不能加独占锁。获得共享锁的线程只能读数据,不能修改数据。

Java 잠금을 완전히 마스터합니다(그래픽 및 텍스트 분석).

在 JDK 中 ReentrantReadWriteLock 就是一种共享锁。

互斥锁和读写锁

互斥锁

互斥锁是独占锁的一种常规实现,是指某一资源同时只允许一个访问者对其进行访问,具有唯一性和排它性。

Java 잠금을 완전히 마스터합니다(그래픽 및 텍스트 분석).

互斥锁一次只能一个线程拥有互斥锁,其他线程只有等待。

读写锁

读写锁

삶의 예를 들자면 화장실에 구덩이가 하나밖에 없다고 가정해 보세요. 비관적으로 변기를 잠그면 문이 즉시 잠기게 되어 화장실에 가는 다른 사람들은 문 밖에서만 기다릴 수 있게 됩니다. 상태가 "차단"되었습니다.

다시 코드 세계로 돌아가서, 스레드가 이 데이터를 조작하려고 할 때마다 다른 스레드도 이 데이터를 조작할 수 있다고 가정하므로 각 작업 전에 잠금이 설정됩니다. 이 데이터는 잠금을 얻을 수 없으며 차단만 가능합니다. 🎜🎜Java 잠금을 완전히 마스터합니다(그래픽 및 텍스트 분석).🎜🎜Java 언어에서는 synchronizedReentrantLock이 일반적인 비관적 잠금입니다. HashTable과 같이 동기화된 키워드를 사용하는 컨테이너 클래스도 있습니다. code code> 등도 비관적 잠금의 응용 프로그램입니다. 🎜🎜🎜Optimistic Lock🎜🎜🎜Optimistic Lock은 인생에서 낙관적인 사람들에 해당합니다. 낙관적인 사람들은 항상 좋은 방향으로 나아갈 것이라고 생각합니다. 🎜🎜화장실에 구덩이가 하나뿐이라고 가정해 보겠습니다. Optimistic Lock은 다음과 같이 생각합니다. 이 황야에는 사람이 많지 않아 내가 문을 닫고 잠글 때마다 내 구덩이를 빼앗을 사람은 아무도 없습니다. 시간 낭비이므로 잠그지 않는 것이 좋습니다. 아시다시피, 낙관적인 자물쇠는 낙관적으로 태어났습니다! 🎜🎜코드 세계로 돌아가서 낙관적 잠금은 데이터를 작동할 때 잠기지 않습니다. 업데이트할 때 이 기간 동안 다른 스레드가 데이터를 업데이트할지 여부를 결정합니다. 🎜🎜Java 잠금을 완전히 마스터합니다(그래픽 및 텍스트 분석).🎜🎜낙관적 잠금은 버전 번호 메커니즘CAS 알고리즘을 사용하여 구현할 수 있습니다. Java 언어에서 java.util.concurrent.atomic 패키지 아래의 원자 클래스는 CAS 낙관적 잠금을 사용하여 구현됩니다. 🎜🎜🎜두 가지 잠금 유형의 사용 시나리오🎜🎜🎜비관적 잠금과 낙관적 잠금은 더 좋고 나쁘지 않고 고유한 적합한 시나리오가 있습니다. 🎜🎜낙관적 잠금은 쓰기 횟수가 상대적으로 적은(상대적으로 작은 충돌) 시나리오에 적합합니다. 잠금을 잠그거나 해제할 필요가 없기 때문에 잠금 오버헤드가 제거되어 처리량이 향상됩니다. 🎜🎜쓰기가 많고 읽기가 적은 시나리오, 즉 충돌이 심각하고 스레드 간의 경쟁이 자극되는 경우 낙관적 잠금을 사용하면 스레드가 계속 재시도하게 되어 이 시나리오에서도 성능이 저하될 수 있습니다. 비관적 잠금을 사용하는 것이 더 적절합니다. 🎜🎜배타적 잠금 및 공유 잠금🎜🎜🎜배타적 잠금🎜🎜🎜배타적 잠금은 잠금이 한 번에 하나의 스레드만 보유할 수 있음을 의미합니다. 스레드가 데이터에 배타적 잠금을 추가하면 다른 스레드는 더 이상 데이터에 어떤 유형의 잠금도 추가할 수 없습니다. 배타적 잠금을 획득한 스레드는 데이터를 읽고 수정할 수 있습니다. 🎜🎜Java 잠금을 완전히 마스터합니다(그래픽 및 텍스트 분석).🎜🎜JDK의 synchronizedjava.util.concurrent(JUC) 패키지에 있는 Lock 구현 클래스는 배타적 잠금입니다. 🎜🎜🎜공유 잠금🎜🎜🎜공유 잠금은 잠금이 여러 스레드에 의해 유지될 수 있음을 의미합니다. 스레드가 데이터에 공유 잠금을 추가하면 다른 스레드는 데이터에 공유 잠금만 추가할 수 있고 배타적 잠금은 추가할 수 없습니다. 공유 잠금을 획득한 스레드는 데이터를 읽을 수만 있고 데이터를 수정할 수는 없습니다. 🎜🎜Java 잠금을 완전히 마스터합니다(그래픽 및 텍스트 분석).🎜🎜JDK에서 ReentrantReadWriteLock은 일종의 공유 잠금입니다. 🎜🎜Mutex 잠금 및 읽기-쓰기 잠금🎜🎜🎜Mutex 잠금🎜🎜🎜Mutex 잠금은 배타적 잠금의 일반적인 구현입니다. 즉, 특정 리소스는 한 방문자만 동시에 액세스할 수 있도록 허용합니다. 시간 액세스는 독특하고 배타적입니다. 🎜🎜Java 잠금을 완전히 마스터합니다(그래픽 및 텍스트 분석).🎜🎜뮤텍스 잠금 한 번에 하나의 스레드만 뮤텍스 잠금을 소유할 수 있으며, 다른 스레드는 대기만 할 수 있습니다. 🎜🎜🎜읽기-쓰기 잠금🎜🎜🎜읽기-쓰기 잠금은 공유 잠금의 특정 구현입니다. 읽기-쓰기 잠금은 일련의 잠금을 관리합니다. 하나는 읽기 전용 잠금이고 다른 하나는 쓰기 잠금입니다. 🎜🎜쓰기 잠금이 없을 때 여러 스레드가 동시에 읽기 잠금을 보유할 수 있으며 쓰기 잠금은 배타적입니다. 쓰기 잠금의 우선순위는 읽기 잠금의 우선순위보다 높습니다. 읽기 잠금을 획득한 스레드는 이전에 해제된 쓰기 잠금에 의해 업데이트된 내용을 볼 수 있어야 합니다. 🎜

읽기-쓰기 잠금은 뮤텍스 잠금보다 동시성이 더 높습니다. 한 번에 하나의 쓰기 스레드만 있지만 동시에 여러 스레드를 읽을 수 있습니다.

Java 잠금을 완전히 마스터합니다(그래픽 및 텍스트 분석).

은 JDK에서 읽기-쓰기 잠금 인터페이스를 정의합니다. ReadWriteLockReadWriteLock

public interface ReadWriteLock {
    /**
     * 获取读锁
     */
    Lock readLock();

    /**
     * 获取写锁
     */
    Lock writeLock();
}

ReentrantReadWriteLock 实现了ReadWriteLock接口,具体实现这里不展开,后续会深入源码解析。

公平锁和非公平锁

公平锁

公平锁是指多个线程按照申请锁的顺序来获取锁,这里类似排队买票,先来的人先买,后来的人在队尾排着,这是公平的。

Java 잠금을 완전히 마스터합니다(그래픽 및 텍스트 분석).

在 java 中可以通过构造函数初始化公平锁

/**
* 创建一个可重入锁,true 表示公平锁,false 表示非公平锁。默认非公平锁
*/
Lock lock = new ReentrantLock(true);

非公平锁

非公平锁是指多个线程获取锁的顺序并不是按照申请锁的顺序,有可能后申请的线程比先申请的线程优先获取锁,在高并发环境下,有可能造成优先级翻转,或者饥饿的状态(某个线程一直得不到锁)。

Java 잠금을 완전히 마스터합니다(그래픽 및 텍스트 분석).

在 java 中 synchronized 关键字是非公平锁,ReentrantLock默认也是非公平锁。

/**
* 创建一个可重入锁,true 表示公平锁,false 表示非公平锁。默认非公平锁
*/
Lock lock = new ReentrantLock(false);

可重入锁

可重入锁又称之为递归锁,是指同一个线程在外层方法获取了锁,在进入内层方法会自动获取锁。

Java 잠금을 완전히 마스터합니다(그래픽 및 텍스트 분석).

对于Java ReentrantLock而言, 他的名字就可以看出是一个可重入锁。对于Synchronized而言,也是一个可重入锁。

敲黑板:可重入锁的一个好处是可一定程度避免死锁。

以 synchronized 为例,看一下下面的代码:

public synchronized void mehtodA() throws Exception{
 // Do some magic tings
 mehtodB();
}

public synchronized void mehtodB() throws Exception{
 // Do some magic tings
}

上面的代码中 methodA 调用 methodB,如果一个线程调用methodA 已经获取了锁再去调用 methodB 就不需要再次获取锁了,这就是可重入锁的特性。如果不是可重入锁的话,mehtodB 可能不会被当前线程执行,可能造成死锁。

自旋锁

自旋锁是指线程在没有获得锁时不是被直接挂起,而是执行一个忙循环,这个忙循环就是所谓的自旋。

Java 잠금을 완전히 마스터합니다(그래픽 및 텍스트 분석).

自旋锁的目的是为了减少线程被挂起的几率,因为线程的挂起和唤醒也都是耗资源的操作。

如果锁被另一个线程占用的时间比较长,即使自旋了之后当前线程还是会被挂起,忙循环就会变成浪费系统资源的操作,反而降低了整体性能。因此自旋锁是不适应锁占用时间长的并发情况的。

在 Java 中,AtomicInteger 类有自旋的操作,我们看一下代码:

public final int getAndAddInt(Object o, long offset, int delta) {
    int v;
    do {
        v = getIntVolatile(o, offset);
    } while (!compareAndSwapInt(o, offset, v, v + delta));
    return v;
}

CAS 操作如果失败就会一直循环获取当前 value 值然后重试。

另外自适应自旋锁也需要了解一下。

在JDK1.6又引入了自适应自旋,这个就比较智能了,自旋时间不再固定,由前一次在同一个锁上的自旋时间以及锁的拥有者的状态来决定。如果虚拟机认为这次自旋也很有可能再次成功那就会次序较多的时间,如果自旋很少成功,那以后可能就直接省略掉自旋过程,避免浪费处理器资源。

分段锁

分段锁 是一种锁的设计,并不是具体的一种锁。

分段锁设计目的是将锁的粒度进一步细化,当操作不需要更新整个数组的时候,就仅仅针对数组中的一项进行加锁操作。

Java 잠금을 완전히 마스터합니다(그래픽 및 텍스트 분석).

在 Java 语言中 CurrentHashMap 底层就用了分段锁,使用Segment,就可以进行并发使用了。

锁升级(无锁|偏向锁|轻量级锁|重量级锁)

JDK1.6 为了提升性能减少获得锁和释放锁所带来的消耗,引入了4种锁的状态:无锁偏向锁轻量级锁重量级锁,它会随着多线程的竞争情况逐渐升级,但不能降级。

无锁

无锁

private static final Object LOCK = new Object();

for(int i = 0;i <code>ReentrantReadWriteLock</code>은 <code>ReadWriteLock</code> 인터페이스를 구현합니다. 특정 구현을 이겼습니다. 여기서는 더 이상 설명하지 말고 나중에 심층적인 소스 코드 분석을 다루겠습니다. <p></p><h2>공정한 잠금과 불공정한 잠금</h2><strong></strong>공정한 잠금🎜🎜<code>공정한 잠금</code>은 여러 스레드가 잠금을 신청한 순서대로 잠금을 획득한다는 의미입니다. 구매를 위해 대기하는 것과 비슷합니다. 티켓은 먼저 오는 사람이 먼저 사고, 나중에 오는 사람이 줄을 서서 기다리는 것이 공평하다. 🎜🎜<img alt="Java 잠금을 완전히 마스터합니다(그래픽 및 텍스트 분석)." src="https://img.php.cn/upload/article/000/000/067/fd2651f075dc10341872ce27dedd5291-6.png">🎜🎜Java에서는 생성자를 통해 공정한 잠금을 초기화할 수 있습니다🎜<pre class="brush:php;toolbar:false"> synchronized(LOCK){
     for(int i = 0;i 🎜🎜Unfair lock🎜🎜🎜<code>Unfair lock</code>은 여러 스레드가 잠금을 획득하는 순서가 다음 순서와 다르다는 것을 의미합니다. 잠금을 적용하면 나중에 적용한 스레드가 먼저 적용한 스레드보다 먼저 잠금을 획득할 수 있습니다. 동시성이 높은 환경에서는 우선순위 뒤집기 또는 기아 상태가 발생할 수 있습니다(특정 스레드는 잠금을 얻지 못함). 🎜🎜<img alt="Java 잠금을 완전히 마스터합니다(그래픽 및 텍스트 분석)." src="https://img.php.cn/upload/article/000/000/067/2937d6a341e4758dd354c9c1529754bd-7.png">🎜🎜자바에서는 동기화 키워드가 불공정 잠금이고, ReentrantLock도 기본적으로 불공정 잠금입니다. 🎜<pre class="brush:php;toolbar:false">public String test(String s1, String s2){
    StringBuffer stringBuffer = new StringBuffer();
    stringBuffer.append(s1);
    stringBuffer.append(s2);
    return stringBuffer.toString();
}

재진입 잠금

🎜재진입 잠금재귀 잠금이라고도 하는데, 이는 동일한 스레드가 외부 메서드에서 잠금을 획득한다는 의미입니다. 내부 메소드 진입 시 자동으로 획득됩니다. 🎜🎜Java 잠금을 완전히 마스터합니다(그래픽 및 텍스트 분석).🎜🎜Java ReentrantLock의 경우 이름을 보면 재진입 잠금임을 알 수 있습니다. 동기화의 경우 재진입 잠금이기도 합니다. 🎜🎜칠판을 두드리세요: 재진입 잠금의 한 가지 이점은 교착 상태를 어느 정도 피할 수 있다는 것입니다. 🎜🎜동기화를 예로 들어 다음 코드를 살펴보세요. 🎜
StringBuffer.class

// append 是同步方法
public synchronized StringBuffer append(String str) {
    toStringCache = null;
    super.append(str);
    return this;
}
🎜위 코드에서 methodA는 methodB를 호출합니다. 스레드가 methodA를 호출하고 잠금을 획득한 다음 methodB를 호출하면 잠금을 다시 획득할 필요가 없습니다. . 이것은 재진입 특성입니다. 재진입 잠금이 아닌 경우 현재 스레드에서 mehtodB가 실행되지 않아 교착 상태가 발생할 수 있습니다. 🎜

스핀 잠금

🎜스핀 잠금은 스레드가 잠금을 획득하지 못한 경우 직접 일시 중단되지 않고 바쁜 루프를 실행한다는 의미입니다. 자동 잠금이라고 합니다. 🎜🎜Java 잠금을 완전히 마스터합니다(그래픽 및 텍스트 분석).🎜🎜스핀 잠금의 목적은 스레드가 일시 중단될 가능성을 줄이는 것입니다. 스레드 일시 중단 및 깨우기 역시 리소스를 소비하는 작업이기 때문입니다. 🎜🎜다른 스레드가 오랫동안 잠금을 점유하고 있는 경우 현재 스레드는 회전 후에도 여전히 일시 중지되며 사용 중인 루프는 시스템 리소스를 낭비하게 되어 실제로 전체 성능을 저하시킵니다. 따라서 스핀 잠금은 잠금에 오랜 시간이 걸리는 동시성 상황에는 적합하지 않습니다. 🎜🎜Java에서 AtomicInteger 클래스에는 스핀 작업이 있습니다. 코드를 살펴보겠습니다. 🎜rrreee🎜 CAS 작업이 실패하면 현재 값을 얻기 위해 계속 루프를 돌다가 다시 시도합니다. . 🎜🎜또한 적응형 스핀 잠금 장치에 대해서도 알아야 합니다. 🎜🎜적응형 스핀은 더욱 지능적인 JDK1.6에 도입되었습니다. 스핀 시간은 더 이상 고정되지 않지만 동일한 잠금의 이전 스핀 시간과 잠금 소유자의 상태에 따라 결정됩니다. 가상 머신이 이 스핀이 다시 성공할 가능성이 있다고 생각하면 시간이 더 걸릴 것입니다. 스핀이 거의 성공하지 못하는 경우에는 프로세서 리소스 낭비를 피하기 위해 향후 스핀 프로세스를 직접 생략할 수도 있습니다. 🎜

분할 자물쇠

🎜분할 자물쇠는 특정 자물쇠가 아닌 자물쇠 디자인입니다. 🎜🎜세그먼트 잠금의 설계 목적은 잠금의 세분성을 더욱 구체화하는 것입니다. 작업에서 전체 배열을 업데이트할 필요가 없으면 배열에서 하나의 항목만 잠글 수 있습니다. 🎜🎜Java 잠금을 완전히 마스터합니다(그래픽 및 텍스트 분석).🎜🎜Java 언어에서는 CurrentHashMap의 기본 레이어가 세그먼트화 잠금을 사용하므로 동시에 사용할 수 있습니다. 🎜

잠금 업그레이드(no lock|biased lock|lightweight lock|heavyweight lock)

🎜JDK1.6 성능을 개선하고 잠금 획득 및 해제로 인한 소비를 줄이기 위해 4가지 유형의 잠금이 도입됩니다. : 잠금 없음, 바이어스 잠금, 경량 잠금무거운 잠금은 다중 스레드 경쟁 조건을 따릅니다. 점차적으로 확대되지만 저하될 수는 없습니다. 🎜🎜🎜Lock-free🎜🎜🎜Lock-free 상태는 실제로 위에서 언급한 낙관적 잠금이므로 여기서는 설명하지 않습니다. 🎜🎜🎜바이어스 잠금🎜🎜

Java偏向锁(Biased Locking)是指它会偏向于第一个访问锁的线程,如果在运行过程中,只有一个线程访问加锁的资源,不存在多线程竞争的情况,那么线程是不需要重复获取锁的,这种情况下,就会给线程加一个偏向锁。

偏向锁的实现是通过控制对象Mark Word的标志位来实现的,如果当前是可偏向状态,需要进一步判断对象头存储的线程 ID 是否与当前线程 ID 一致,如果一致直接进入。

轻量级锁

当线程竞争变得比较激烈时,偏向锁就会升级为轻量级锁,轻量级锁认为虽然竞争是存在的,但是理想情况下竞争的程度很低,通过自旋方式等待上一个线程释放锁。

重量级锁

如果线程并发进一步加剧,线程的自旋超过了一定次数,或者一个线程持有锁,一个线程在自旋,又来了第三个线程访问时(反正就是竞争继续加大了),轻量级锁就会膨胀为重量级锁,重量级锁会使除了此时拥有锁的线程以外的线程都阻塞。

升级到重量级锁其实就是互斥锁了,一个线程拿到锁,其余线程都会处于阻塞等待状态。

在 Java 中,synchronized 关键字内部实现原理就是锁升级的过程:无锁 --> 偏向锁 --> 轻量级锁 --> 重量级锁。这一过程在后续讲解 synchronized 关键字的原理时会详细介绍。

锁优化技术(锁粗化、锁消除)

锁粗化

锁粗化就是将多个同步块的数量减少,并将单个同步块的作用范围扩大,本质上就是将多次上锁、解锁的请求合并为一次同步请求。

举个例子,一个循环体中有一个代码同步块,每次循环都会执行加锁解锁操作。

private static final Object LOCK = new Object();

for(int i = 0;i <p>经过<code>锁粗化</code>后就变成下面这个样子了:</p><pre class="brush:php;toolbar:false"> synchronized(LOCK){
     for(int i = 0;i <p><strong>锁消除</strong></p><p><code>锁消除</code>是指虚拟机编译器在运行时检测到了共享数据没有竞争的锁,从而将这些锁进行消除。</p><p>举个例子让大家更好理解。</p><pre class="brush:php;toolbar:false">public String test(String s1, String s2){
    StringBuffer stringBuffer = new StringBuffer();
    stringBuffer.append(s1);
    stringBuffer.append(s2);
    return stringBuffer.toString();
}

上面代码中有一个 test 方法,主要作用是将字符串 s1 和字符串 s2 串联起来。

test 方法中三个变量s1, s2, stringBuffer, 它们都是局部变量,局部变量是在栈上的,栈是线程私有的,所以就算有多个线程访问 test 方法也是线程安全的。

我们都知道 StringBuffer 是线程安全的类,append 方法是同步方法,但是 test 方法本来就是线程安全的,为了提升效率,虚拟机帮我们消除了这些同步锁,这个过程就被称为锁消除

StringBuffer.class

// append 是同步方法
public synchronized StringBuffer append(String str) {
    toStringCache = null;
    super.append(str);
    return this;
}

一张图总结:

前面讲了 Java 语言中各种各种的锁,最后再通过六个问题统一总结一下:

Java 잠금을 완전히 마스터합니다(그래픽 및 텍스트 분석).

推荐学习:《java视频教程

위 내용은 Java 잠금을 완전히 마스터합니다(그래픽 및 텍스트 분석).의 상세 내용입니다. 자세한 내용은 PHP 중국어 웹사이트의 기타 관련 기사를 참조하세요!

성명:
이 기사는 csdn.net에서 복제됩니다. 침해가 있는 경우 admin@php.cn으로 문의하시기 바랍니다. 삭제