> Java > Java베이스 > 본문

Java 동시 프로그래밍에 대한 자세한 설명

풀어 주다: 2019-12-25 17:12:11
앞으로
2604명이 탐색했습니다.

Java 동시 프로그래밍에 대한 자세한 설명

1. 동기화의 결함

동기화는 Java의 키워드로, Java 언어에 내장된 기능임을 의미합니다. 그렇다면 자물쇠는 왜 나타나는 걸까요?

코드 블록이 동기화되면 스레드가 해당 잠금을 획득하고 코드 블록을 실행할 때 다른 스레드는 잠금을 획득한 스레드가 잠금을 해제할 때까지만 기다릴 수 있으며 여기서 잠금을 획득한 스레드는 잠금을 해제합니다. 잠금에는 두 가지 상황만 있습니다.

1) 잠금을 획득한 스레드는 코드 블록 실행을 마친 다음 잠금 소유를 해제합니다.

2) 스레드 실행 중에 예외가 발생합니다. JVM이 스레드가 자동으로 잠금을 해제하도록 허용하는 시간입니다.

권장: 기본 Java 자습서

그런 다음 잠금을 획득한 스레드가 IO 대기 또는 기타 이유(예: sleep 메소드 호출)로 인해 차단되었지만 잠금이 해제되지 않은 경우 다른 스레드는 가만히 기다릴 수만 있습니다. . 이것이 프로그램 실행 효율성에 얼마나 영향을 미치는지 상상해 보십시오.

따라서 대기 중인 스레드가 무한정 대기하는 것을 방지하는 메커니즘(예: 특정 시간만 기다리거나 인터럽트에 응답할 수 있는 기능)이 필요하며 이는 Lock을 통해 수행될 수 있습니다.

또 다른 예: 파일을 읽고 쓰는 스레드가 여러 개 있는 경우 읽기 작업과 쓰기 작업이 충돌하고 쓰기 작업은 쓰기 작업과 충돌하지만 읽기 작업과 읽기 작업은 충돌하지 않습니다.

그러나 동기화를 달성하기 위해 동기화된 키워드를 사용하면 문제가 발생합니다.

여러 스레드가 읽기 작업만 수행하는 경우, 즉 한 스레드가 읽기 작업을 수행하면 다른 스레드는 대기만 할 수 있고 읽기 작업을 수행할 수 없습니다.

따라서 여러 스레드가 읽기 작업만 수행하는 경우 스레드 간의 충돌을 방지하는 메커니즘이 필요합니다. 이는 잠금을 통해 달성할 수 있습니다.

또한 Lock을 통해 스레드가 성공적으로 잠금을 획득했는지 여부를 알 수 있습니다. 이것은 동기화가 할 수 없는 일입니다.

요약하자면 잠금은 동기화보다 더 많은 기능을 제공합니다. 하지만 다음 사항에 주의하세요.

1) 잠금은 Java 언어에 내장되어 있지 않습니다. 동기화는 Java 언어의 키워드이므로 내장된 기능입니다. Lock은 동기식 액세스를 달성할 수 있는 클래스입니다.

2) 동기화를 사용하면 동기화된 메서드나 동기화된 코드 블록이 실행될 때 사용자가 수동으로 잠금을 해제할 필요가 없습니다. , 시스템은 자동으로 스레드가 잠금을 해제하도록 허용합니다. 잠금이 사용자에게 수동으로 잠금을 해제하도록 요구하면 교착 상태가 발생할 수 있습니다.

2. java.util.concurrent.locks 패키지에서 일반적으로 사용되는 클래스

java.util.concurrent.locks 패키지에서 일반적으로 사용되는 클래스와 인터페이스에 대해 논의해 보겠습니다.

1.Lock

가장 먼저 설명할 것은 Lock입니다. Lock의 소스 코드를 보면 Lock이 인터페이스라는 것을 알 수 있습니다.

public interface Lock {
    void lock();
    void lockInterruptibly() throws InterruptedException;
    boolean tryLock();
    boolean tryLock(long time, TimeUnit unit) throws InterruptedException;
    void unlock();
    Condition newCondition();
}
로그인 후 복사

다음은 Lock에서 각 메소드의 사용법을 설명합니다. 인터페이스를 하나씩 사용하여 lock(), tryLock(), tryLock(긴 시간, TimeUnit 단위) 및 lockInterruptible()을 사용하여 잠금을 획득합니다. unLock() 메서드는 잠금을 해제하는 데 사용됩니다. newCondition() 메서드는 당분간 여기서 설명하지 않지만 스레드 공동 작업에 대한 이후 문서에서 설명합니다.

Lock에는 잠금을 얻기 위해 네 가지 메소드가 선언되어 있는데, 이 네 가지 메소드의 차이점은 무엇인가요?

우선, lock() 메서드는 잠금을 얻는 데 사용되는 가장 일반적으로 사용되는 메서드입니다. 다른 스레드가 잠금을 획득한 경우 기다리십시오.

앞서 말씀드린 것처럼 Lock을 사용할 경우 적극적으로 Lock을 해제해야 하며, 예외가 발생하더라도 자동으로 Lock이 해제되지는 않습니다. 따라서 일반적으로 Lock의 사용은 try{}catch{} 블록에서 수행되어야 하며, 잠금을 해제하는 작업은 finally 블록에서 수행되어야 잠금이 해제되어 발생을 방지할 수 있습니다. 교착 상태. Lock이 일반적으로 동기화에 사용되는 경우 다음과 같은 형식으로 사용됩니다.

Lock lock = ...;
lock.lock();
try{
    //处理任务
}catch(Exception ex){
     
}finally{
    lock.unlock();   //释放锁
}
로그인 후 복사

tryLock() 메서드에는 반환 값이 있는데, 이는 잠금 획득을 시도하는 데 사용된다는 의미입니다. 획득에 실패하면(즉, 다른 스레드에서 잠금을 획득한 경우) false를 반환합니다. 즉, 이 메서드는 무슨 일이 있어도 즉시 반환됩니다. 자물쇠를 얻을 수 없을 때 거기에서 기다리지 않을 것입니다.

tryLock(장시간, TimeUnit 단위) 메서드는 tryLock() 메서드와 유사하지만 잠금을 얻을 수 없는 경우 일정 시간 동안 대기한다는 차이점이 있습니다. 시간 제한이 있으면 그냥 false를 반환하세요. 잠금이 처음에 획득되었거나 대기 기간 동안 획득된 경우 true를 반환합니다.

따라서 일반적인 상황에서는 tryLock을 통해 잠금을 획득할 때 다음과 같이 사용됩니다.

Lock lock = ...;
if(lock.tryLock()) {
     try{
         //处理任务
     }catch(Exception ex){
         
     }finally{
         lock.unlock();   //释放锁
     } 
}else {
    //如果不能获取锁,则直接做其他事情
}
로그인 후 복사

lockInterruptously() 메서드는 특별합니다. 이 메서드를 통해 잠금을 획득할 때 스레드가 잠금 획득을 기다리고 있으면 스레드는 인터럽트, 즉 인터럽트 스레드의 대기 상태에 응답할 수 있습니다.

즉, 두 스레드가 lock.lockInterruptible()을 통해 동시에 잠금을 획득하려고 할 때 스레드 A가 이때 잠금을 획득하고 스레드 B가 기다리고만 있다면 threadB.interrupt()는 스레드 B에서 호출됩니다. 이 메서드는 스레드 B의 대기 프로세스를 중단할 수 있습니다.

由于lockInterruptibly()的声明中抛出了异常,所以lock.lockInterruptibly()必须放在try块中或者在调用lockInterruptibly()的方法外声明抛出InterruptedException。

因此lockInterruptibly()一般的使用形式如下:

public void method() throws InterruptedException {
    lock.lockInterruptibly();
    try {  
     //.....
    }
    finally {
        lock.unlock();
    }  
}
로그인 후 복사

注意,当一个线程获取了锁之后,是不会被interrupt()方法中断的。因为本身在前面的文章中讲过单独调用interrupt()方法不能中断正在运行过程中的线程,只能中断阻塞过程中的线程。

因此当通过lockInterruptibly()方法获取某个锁时,如果不能获取到,只有进行等待的情况下,是可以响应中断的。

而用synchronized修饰的话,当一个线程处于等待某个锁的状态,是无法被中断的,只有一直等待下去。

2.ReentrantLock

ReentrantLock,意思是“可重入锁”,关于可重入锁的概念在下一节讲述。ReentrantLock是唯一实现了Lock接口的类,并且ReentrantLock提供了更多的方法。下面通过一些实例看具体看一下如何使用ReentrantLock。

例子1,lock()的正确使用方法

public class Test {
    private ArrayList<Integer> arrayList = new ArrayList<Integer>();
    public static void main(String[] args)  {
        final Test test = new Test();
         
        new Thread(){
            public void run() {
                test.insert(Thread.currentThread());
            };
        }.start();
         
        new Thread(){
            public void run() {
                test.insert(Thread.currentThread());
            };
        }.start();
    }  
     
    public void insert(Thread thread) {
        Lock lock = new ReentrantLock();    //注意这个地方
        lock.lock();
        try {
            System.out.println(thread.getName()+"得到了锁");
            for(int i=0;i<5;i++) {
                arrayList.add(i);
            }
        } catch (Exception e) {
            // TODO: handle exception
        }finally {
            System.out.println(thread.getName()+"释放了锁");
            lock.unlock();
        }
    }
}
로그인 후 복사

输出结果:

Thread-0得到了锁
Thread-1得到了锁
Thread-0释放了锁
Thread-1释放了锁

在insert方法中的lock变量是局部变量,每个线程执行该方法时都会保存一个副本,那么理所当然每个线程执行到lock.lock()处获取的是不同的锁,所以就不会发生冲突。

知道了原因改起来就比较容易了,只需要将lock声明为类的属性即可。

public class Test {
    private ArrayList<Integer> arrayList = new ArrayList<Integer>();
    private Lock lock = new ReentrantLock();    //注意这个地方
    public static void main(String[] args)  {
        final Test test = new Test();
         
        new Thread(){
            public void run() {
                test.insert(Thread.currentThread());
            };
        }.start();
         
        new Thread(){
            public void run() {
                test.insert(Thread.currentThread());
            };
        }.start();
    }  
     
    public void insert(Thread thread) {
        lock.lock();
        try {
            System.out.println(thread.getName()+"得到了锁");
            for(int i=0;i<5;i++) {
                arrayList.add(i);
            }
        } catch (Exception e) {
            // TODO: handle exception
        }finally {
            System.out.println(thread.getName()+"释放了锁");
            lock.unlock();
        }
    }
}
로그인 후 복사

这样就是正确地使用Lock的方法了。

例子2,tryLock()的使用方法

public class Test {
    private ArrayList<Integer> arrayList = new ArrayList<Integer>();
    private Lock lock = new ReentrantLock();    //注意这个地方
    public static void main(String[] args)  {
        final Test test = new Test();
         
        new Thread(){
            public void run() {
                test.insert(Thread.currentThread());
            };
        }.start();
         
        new Thread(){
            public void run() {
                test.insert(Thread.currentThread());
            };
        }.start();
    }  
     
    public void insert(Thread thread) {
        if(lock.tryLock()) {
            try {
                System.out.println(thread.getName()+"得到了锁");
                for(int i=0;i<5;i++) {
                    arrayList.add(i);
                }
            } catch (Exception e) {
                // TODO: handle exception
            }finally {
                System.out.println(thread.getName()+"释放了锁");
                lock.unlock();
            }
        } else {
            System.out.println(thread.getName()+"获取锁失败");
        }
    }
}
로그인 후 복사

输出结果:

Thread-0得到了锁
Thread-1获取锁失败
Thread-0释放了锁

例子3,lockInterruptibly()响应中断的使用方法:

public class Test {
    private Lock lock = new ReentrantLock();   
    public static void main(String[] args)  {
        Test test = new Test();
        MyThread thread1 = new MyThread(test);
        MyThread thread2 = new MyThread(test);
        thread1.start();
        thread2.start();
         
        try {
            Thread.sleep(2000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        thread2.interrupt();
    }  
     
    public void insert(Thread thread) throws InterruptedException{
        lock.lockInterruptibly();   //注意,如果需要正确中断等待锁的线程,必须将获取锁放在外面,然后将InterruptedException抛出
        try {  
            System.out.println(thread.getName()+"得到了锁");
            long startTime = System.currentTimeMillis();
            for(    ;     ;) {
                if(System.currentTimeMillis() - startTime >= Integer.MAX_VALUE)
                    break;
                //插入数据
            }
        }
        finally {
            System.out.println(Thread.currentThread().getName()+"执行finally");
            lock.unlock();
            System.out.println(thread.getName()+"释放了锁");
        }  
    }
}
 
class MyThread extends Thread {
    private Test test = null;
    public MyThread(Test test) {
        this.test = test;
    }
    @Override
    public void run() {
         
        try {
            test.insert(Thread.currentThread());
        } catch (InterruptedException e) {
            System.out.println(Thread.currentThread().getName()+"被中断");
        }
    }
}
로그인 후 복사

运行之后,发现thread2能够被正确中断。

3.ReadWriteLock

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();
}
로그인 후 복사

一个用来获取读锁,一个用来获取写锁。也就是说将文件的读写操作分开,分成2个锁来分配给线程,从而使得多个线程可以同时进行读操作。下面的ReentrantReadWriteLock实现了ReadWriteLock接口。

4.ReentrantReadWriteLock

ReentrantReadWriteLock里面提供了很多丰富的方法,不过最主要的有两个方法:readLock()和writeLock()用来获取读锁和写锁。

下面通过几个例子来看一下ReentrantReadWriteLock具体用法。

假如有多个线程要同时进行读操作的话,先看一下synchronized达到的效果:

public class Test {
    private ReentrantReadWriteLock rwl = new ReentrantReadWriteLock();
     
    public static void main(String[] args)  {
        final Test test = new Test();
         
        new Thread(){
            public void run() {
                test.get(Thread.currentThread());
            };
        }.start();
         
        new Thread(){
            public void run() {
                test.get(Thread.currentThread());
            };
        }.start();
         
    }  
     
    public synchronized void get(Thread thread) {
        long start = System.currentTimeMillis();
        while(System.currentTimeMillis() - start <= 1) {
            System.out.println(thread.getName()+"正在进行读操作");
        }
        System.out.println(thread.getName()+"读操作完毕");
    }
}
로그인 후 복사

这段程序的输出结果会是,直到thread1执行完读操作之后,才会打印thread2执行读操作的信息。

Thread-0正在进行读操作

Thread-0正在进行读操作

Thread-0正在进行读操作

Thread-0正在进行读操作

Thread-0正在进行读操作

Thread-0正在进行读操作

Thread-0正在进行读操作

Thread-0正在进行读操作

Thread-0正在进行读操作

Thread-0正在进行读操作

Thread-0正在进行读操作

Thread-0正在进行读操作

Thread-0正在进行读操作

Thread-0正在进行读操作

Thread-0正在进行读操作

Thread-0正在进行读操作

Thread-0正在进行读操作

Thread-0正在进行读操作

Thread-0正在进行读操作

Thread-0正在进行读操作

Thread-0正在进行读操作

Thread-0正在进行读操作

Thread-0正在进行读操作

Thread-0正在进行读操作

Thread-0正在进行读操作

Thread-0正在进行读操作

Thread-0正在进行读操作

Thread-0正在进行读操作

Thread-0读操作完毕

Thread-1正在进行读操作

Thread-1正在进行读操作

Thread-1正在进行读操作

Thread-1正在进行读操作

Thread-1正在进行读操作

Thread-1正在进行读操作

Thread-1正在进行读操作

Thread-1正在进行读操作

Thread-1正在进行读操作

Thread-1正在进行读操作

Thread-1正在进行读操作

Thread-1正在进行读操作

Thread-1正在进行读操作

Thread-1正在进行读操作

Thread-1正在进行读操作

Thread-1正在进行读操作

Thread-1正在进行读操作

Thread-1正在进行读操作

Thread-1正在进行读操作

Thread-1正在进行读操作

Thread-1正在进行读操作

Thread-1正在进行读操作

Thread-1正在进行读操作

Thread-1正在进行读操作

Thread-1正在进行读操作

Thread-1正在进行读操作

Thread-1正在进行读操作

Thread-1正在进行读操作

Thread-1正在进行读操作

Thread-1正在进行读操作

Thread-1正在进行读操作

Thread-1正在进行读操作

Thread-1正在进行读操作

Thread-1正在进行读操作

Thread-1正在进行读操作

Thread-1正在进行读操作

Thread-1正在进行读操作

Thread-1正在进行读操作

Thread-1正在进行读操作

Thread-1正在进行读操作

Thread-1正在进行读操作

Thread-1正在进行读操作

Thread-1正在进行读操作

Thread-1读操作完毕

而改成用读写锁的话:

public class Test {
    private ReentrantReadWriteLock rwl = new ReentrantReadWriteLock();
     
    public static void main(String[] args)  {
        final Test test = new Test();
         
        new Thread(){
            public void run() {
                test.get(Thread.currentThread());
            };
        }.start();
         
        new Thread(){
            public void run() {
                test.get(Thread.currentThread());
            };
        }.start();
         
    }  
     
    public void get(Thread thread) {
        rwl.readLock().lock();
        try {
            long start = System.currentTimeMillis();
             
            while(System.currentTimeMillis() - start <= 1) {
                System.out.println(thread.getName()+"正在进行读操作");
            }
            System.out.println(thread.getName()+"读操作完毕");
        } finally {
            rwl.readLock().unlock();
        }
    }
}
로그인 후 복사

此时打印的结果为:

Thread-0正在进行读操作

Thread-0正在进行读操作

Thread-1正在进行读操作

Thread-0正在进行读操作

Thread-1正在进行读操作

Thread-0正在进行读操作

Thread-1正在进行读操作

Thread-1正在进行读操作

Thread-1正在进行读操作

Thread-1正在进行读操作

Thread-1正在进行读操作

Thread-1正在进行读操作

Thread-0正在进行读操作

Thread-0正在进行读操作

Thread-0正在进行读操作

Thread-0正在进行读操作

Thread-1正在进行读操作

Thread-1正在进行读操作

Thread-1正在进行读操作

Thread-1正在进行读操作

Thread-0正在进行读操作

Thread-1正在进行读操作

Thread-1正在进行读操作

Thread-0正在进行读操作

Thread-1正在进行读操作

Thread-1正在进行读操作

Thread-0正在进行读操作

Thread-1正在进行读操作

Thread-1正在进行读操作

Thread-1正在进行读操作

Thread-0正在进行读操作

Thread-1正在进行读操作

Thread-1正在进行读操作

Thread-0正在进行读操作

Thread-1正在进行读操作

Thread-0正在进行读操作

Thread-1正在进行读操作

Thread-0正在进行读操作

Thread-1正在进行读操作

Thread-0正在进行读操作

Thread-1正在进行读操作

Thread-0正在进行读操作

Thread-1正在进行读操作

Thread-0正在进行读操作

Thread-1正在进行读操作

Thread-0正在进行读操作

Thread-1正在进行读操作

Thread-0读操作完毕

Thread-1读操作完毕

说明thread1和thread2在同时进行读操作。

这样就大大提升了读操作的效率。

不过要注意的是,如果有一个线程已经占用了读锁,则此时其他线程如果要申请写锁,则申请写锁的线程会一直等待释放读锁。

如果有一个线程已经占用了写锁,则此时其他线程如果申请写锁或者读锁,则申请的线程会一直等待释放写锁。

关于ReentrantReadWriteLock类中的其他方法感兴趣的朋友可以自行查阅API文档。

5.Lock和synchronized的选择

总结来说,Lock和synchronized有以下几点不同:

1)Lock是一个接口,而synchronized是Java中的关键字,synchronized是内置的语言实现;

2)synchronized在发生异常时,会自动释放线程占有的锁,因此不会导致死锁现象发生;而Lock在发生异常时,如果没有主动通过unLock()去释放锁,则很可能造成死锁现象,因此使用Lock时需要在finally块中释放锁;

3)Lock可以让等待锁的线程响应中断,而synchronized却不行,使用synchronized时,等待的线程会一直等待下去,不能够响应中断;

4)通过Lock可以知道有没有成功获取锁,而synchronized却无法办到。

5)Lock可以提高多个线程进行读操作的效率。

在性能上来说,如果竞争资源不激烈,两者的性能是差不多的,而当竞争资源非常激烈时(即有大量线程同时竞争),此时Lock的性能要远远优于synchronized。所以说,在具体使用时要根据适当情况选择。

三.锁的相关概念介绍

在前面介绍了Lock的基本使用,这一节来介绍一下与锁相关的几个概念。

1.可重入锁

如果锁具备可重入性,则称作为可重入锁。像synchronized和ReentrantLock都是可重入锁,可重入性在我看来实际上表明了锁的分配机制:基于线程的分配,而不是基于方法调用的分配。举个简单的例子,当一个线程执行到某个synchronized方法时,比如说method1,而在method1中会调用另外一个synchronized方法method2,此时线程不必重新去申请锁,而是可以直接执行方法method2。

看下面这段代码就明白了:

class MyClass {
    public synchronized void method1() {
        method2();
    }
     
    public synchronized void method2() {
         
    }
}
로그인 후 복사

上述代码中的两个方法method1和method2都用synchronized修饰了,假如某一时刻,线程A执行到了method1,此时线程A获取了这个对象的锁,而由于method2也是synchronized方法,假如synchronized不具备可重入性,此时线程A需要重新申请锁。但是这就会造成一个问题,因为线程A已经持有了该对象的锁,而又在申请获取该对象的锁,这样就会线程A一直等待永远不会获取到的锁。

而由于synchronized和Lock都具备可重入性,所以不会发生上述现象。

2.可中断锁

可中断锁:顾名思义,就是可以相应中断的锁。

在Java中,synchronized就不是可中断锁,而Lock是可中断锁。

如果某一线程A正在执行锁中的代码,另一线程B正在等待获取该锁,可能由于等待时间过长,线程B不想等待了,想先处理其他事情,我们可以让它中断自己或者在别的线程中中断它,这种就是可中断锁。

在前面演示lockInterruptibly()的用法时已经体现了Lock的可中断性。

3.公平锁

公平锁即尽量以请求锁的顺序来获取锁。比如同是有多个线程在等待一个锁,当这个锁被释放时,等待时间最久的线程(最先请求的线程)会获得该所,这种就是公平锁。

非公平锁即无法保证锁的获取是按照请求锁的顺序进行的。这样就可能导致某个或者一些线程永远获取不到锁。

在Java中,synchronized就是非公平锁,它无法保证等待的线程获取锁的顺序。

而对于ReentrantLock和ReentrantReadWriteLock,它默认情况下是非公平锁,但是可以设置为公平锁。

看一下这2个类的源代码就清楚了:

Java 동시 프로그래밍에 대한 자세한 설명

在ReentrantLock中定义了2个静态内部类,一个是NotFairSync,一个是FairSync,分别用来实现非公平锁和公平锁。

我们可以在创建ReentrantLock对象时,通过以下方式来设置锁的公平性:

ReentrantLock lock = new ReentrantLock(true);
로그인 후 복사

如果参数为true表示为公平锁,为fasle为非公平锁。默认情况下,如果使用无参构造器,则是非公平锁。

Java 동시 프로그래밍에 대한 자세한 설명

另外在ReentrantLock类中定义了很多方法,比如:

isFair()        //判断锁是否是公平锁

isLocked()    //判断锁是否被任何线程获取了

isHeldByCurrentThread()   //判断锁是否被当前线程获取了

hasQueuedThreads()   //判断是否有线程在等待该锁

在ReentrantReadWriteLock中也有类似的方法,同样也可以设置为公平锁和非公平锁。不过要记住,ReentrantReadWriteLock并未实现Lock接口,它实现的是ReadWriteLock接口。

4.读写锁

读写锁将对一个资源(比如文件)的访问分成了2个锁,一个读锁和一个写锁。

正因为有了读写锁,才使得多个线程之间的读操作不会发生冲突。

ReadWriteLock就是读写锁,它是一个接口,ReentrantReadWriteLock实现了这个接口。

可以通过readLock()获取读锁,通过writeLock()获取写锁。

上面已经演示过了读写锁的使用方法,在此不再赘述。

原文地址:http://www.cnblogs.com/dolphin0520/p/3923167.html

위 내용은 Java 동시 프로그래밍에 대한 자세한 설명의 상세 내용입니다. 자세한 내용은 PHP 중국어 웹사이트의 기타 관련 기사를 참조하세요!

관련 라벨:
원천:cnblogs.com
본 웹사이트의 성명
본 글의 내용은 네티즌들의 자발적인 기여로 작성되었으며, 저작권은 원저작자에게 있습니다. 본 사이트는 이에 상응하는 법적 책임을 지지 않습니다. 표절이나 침해가 의심되는 콘텐츠를 발견한 경우 admin@php.cn으로 문의하세요.
인기 튜토리얼
더>
최신 다운로드
더>
웹 효과
웹사이트 소스 코드
웹사이트 자료
프론트엔드 템플릿