热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

各种锁策略,JUC里面的信号量、计数器、循环屏障,HashMap总结

一、锁策略:1.乐观锁乐观锁认为一般情况下不会出现问题,所以在使用的时候不会加锁,只有在数据修改的时候才会判断有没有锁竞争,如果没有就会直接修改数据,如果有则会返回失败信息给用户自




一、锁策略:

1.乐观锁

乐观锁认为一般情况下不会出现问题,所以在使用的时候不会加锁,只有在数据修改的时候才会判断有没有锁竞争,如果没有就会直接修改数据,如果有则会返回失败信息给用户自行处理。
乐观锁经典使用场景:CAS(Compare And Swap)对比并且替换

CAS实现 V[内存中的值], A[预期的旧值], B[新值]
V == A ? V修改为B : 不能修改
对比失败之后不是直接返回给用户错误信息,而是先进行自旋

面试题:CAS底层实现的原理是什么?
在这里插入图片描述
答:CAS在java中通过Unsafe实现的,Unsafe 是本地类或者本地方法,它是C或C++实现的原生方法,最终通过调用操作系统的Atomic::cmpxchg(原子指令)来实现的(实现对比和替换)



CAS在java中的应用:AtomicInteger/Atomic*(都是通过乐观锁实现的)

在多线程中实现i++、i-- 保证线程安全的方法:


  1. 加锁
  2. ThreadLocal
  3. AtomicInteger

AtomicInteger 保证线程安全的演示

import java.util.concurrent.atomic.AtomicInteger;
public class ThreadDemo91 {
private static AtomicInteger count = new AtomicInteger(0);
private static final int MAXSIZE = 100000;
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
for (int i = 0; i count.getAndIncrement(); //表示i++
//count.incrementAndGet(); //++i
}
}
});
t1.start();
Thread t2 = new Thread(new Runnable() {
@Override
public void run() {
for (int i = 0; i count.getAndDecrement(); //i--
//count.decrementAndGet(); //--i
}
}
});
t2.start();
t1.join();
t2.join();
System.out.println("最终的结果:"+count);
}
}

最终的结果:0

但是!!!!!
乐观锁存在一个问题:ABA问题

在这里插入图片描述

面试题:ABA如何处理?
答:使用版本号,每次修改的时候判断预期的旧值和版本号,每次修改成功后更改版本号,这样即使预期的值和V值相等,因为版本号不同,所以也不能进行修改,从而解决了ABA问题。

java中处理ABA: AtomicStampedReference

转账代码进行演示:正常的情况:如下:

import java.util.concurrent.atomic.AtomicReference;
/**
* 使用代码的方式演示ABA
*/
public class ThreadDemo92 {
private static AtomicReference mOney= new AtomicReference(100); //100是内存种的旧值
public static void main(String[] args) throws InterruptedException {
//转账 -100
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
boolean result = money.compareAndSet(100,0); //预期值 修改值
System.out.println("线程1执行转账"+result);
}
});
t1.start();
Thread t2 = new Thread(new Runnable() {
@Override
public void run() {
boolean result = money.compareAndSet(100,0); //预期值 修改值
System.out.println("线程1执行转账"+result);
}
});
t2.start();
t1.join();
t2.join();
}
}

线程1执行转账true
线程1执行转账false

产生ABA的代码:

import java.util.concurrent.atomic.AtomicReference;
/**
* 使用代码的方式演示ABA
*/
public class ThreadDemo92 {
private static AtomicReference mOney= new AtomicReference(100);
public static void main(String[] args) throws InterruptedException {
//转账 -100
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
boolean result = money.compareAndSet(100,0); //预期值 修改值
System.out.println("线程1执行转账"+result);
}
});
t1.start();
t1.join();
//中途账户多了一笔钱
Thread t3 = new Thread(new Runnable() {
@Override
public void run() {
boolean result = money.compareAndSet(0,100);
System.out.println("线程3转入了100元"+result);
}
});
t3.start();
t1.join();
t3.join();
//不小心点错了
//转账 -100
Thread t2 = new Thread(new Runnable() {
@Override
public void run() {
boolean result = money.compareAndSet(100,0); //预期值 修改值
System.out.println("线程1执行转账"+result);
}
});
t2.start();
}
}

线程1执行转账true
线程3转入了100元true
线程1执行转账true

解决ABA的代码:

import java.util.concurrent.atomic.AtomicStampedReference;
/**
* 使用代码的方式演示ABA
*/
public class ThreadDemo93 {
private static AtomicStampedReference mOney= new AtomicStampedReference(100,1); //初始化值V 版本号
public static void main(String[] args) throws InterruptedException {
//转账 -100
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
/**
* 需要传递四个值:
* expectedReference 预期旧值
* newReference 新值
* expectedStamp 旧版本号
* newStamp 新版本号
*/
boolean result = money.compareAndSet(100,0,1,2);
System.out.println("线程1执行转账 "+result);
}
});
t1.start();
t1.join();
//中途账户多了一笔钱
Thread t3 = new Thread(new Runnable() {
@Override
public void run() {
boolean result = money.compareAndSet(0,100,2,3);
System.out.println("线程3转入了100元 "+result);
}
});
t3.start();
t1.join();
t3.join();
//不小心点错了
//转账 -100
Thread t2 = new Thread(new Runnable() {
@Override
public void run() {
boolean result = money.compareAndSet(100,0,1,2); //预期值 修改值
System.out.println("线程1执行转账 "+result);
}
});
t2.start();
}
}

线程1执行转账 true
线程3转入了100元 true
线程1执行转账 false

注意事项:AtomicStampedReference【可以解决ABA,有版本号】 和 AtomicReference 【有ABA问题】



当把金额全部改为1000的时候:

import java.util.concurrent.atomic.AtomicStampedReference;
public class ThreadDemo94 {
private static AtomicStampedReference mOney= new AtomicStampedReference(1000,1); //初始化值V 版本号
public static void main(String[] args) throws InterruptedException {
//转账 -100
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
/**
* 需要传递四个值:
* expectedReference 旧值
* newReference 新值
* expectedStamp 旧版本号
* newStamp 新版本号
*/
boolean result = money.compareAndSet(1000,0,1,2);
System.out.println("线程1执行转账 "+result);
}
});
t1.start();
t1.join();
//中途账户多了一笔钱
Thread t3 = new Thread(new Runnable() {
@Override
public void run() {
boolean result = money.compareAndSet(0,1000,2,3);
System.out.println("线程3转入了100元 "+result);
}
});
t3.start();
t1.join();
t3.join();
//不小心点错了
//转账 -100
Thread t2 = new Thread(new Runnable() {
@Override
public void run() {
boolean result = money.compareAndSet(1000,0,1,2); //预期值 修改值
System.out.println("线程1执行转账 "+result);
}
});
t2.start();
}
}

线程1执行转账 false
线程3转入了100元 false
线程1执行转账 false

AtomicStampedReference 注意事项:里面的旧值它对比的是引用,并不是实际的值。

Integer 高速缓存:-128~127(当取值为此范围的时候会使用缓存值,不会重新new对象,超过这个范围就是引用)
在这里插入图片描述
解决方案:将缓存的边界设置的大一点
在这里插入图片描述

import java.util.concurrent.atomic.AtomicStampedReference;
/**
* 使用代码的方式演示ABA
*/
public class ThreadDemo94 {
private static AtomicStampedReference mOney= new AtomicStampedReference(1000,1); //初始化值V 版本号
public static void main(String[] args) throws InterruptedException {
//转账 -100
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
/**
* 需要传递四个值:
* expectedReference 旧值
* newReference 新值
* expectedStamp 旧版本号
* newStamp 新版本号
*/
boolean result = money.compareAndSet(1000,0,1,2);
System.out.println("线程1执行转账 "+result);
}
});
t1.start();
t1.join();
//中途账户多了一笔钱
Thread t3 = new Thread(new Runnable() {
@Override
public void run() {
boolean result = money.compareAndSet(0,1000,2,3);
System.out.println("线程3转入了100元 "+result);
}
});
t3.start();
t1.join();
t3.join();
//不小心点错了
//转账 -100
Thread t2 = new Thread(new Runnable() {
@Override
public void run() {
boolean result = money.compareAndSet(1000,0,1,2); //预期值 修改值
System.out.println("线程1执行转账 "+result);
}
});
t2.start();
}
}

线程1执行转账 true
线程3转入了100元 true
线程1执行转账 false

2.悲观锁

悲观锁认为只要执行多线程,就会出现问题,所以在进入方法之后会直接加锁

悲观锁的实现:synchronized

面试题:讲一下 synchronized ?synchronized是如何实现的(也可以是实现原理)


  1. 在java层面,将锁标识放在对象头(每个对象都会有对象头)里面,然后每次去判断对象头的标识和归属判断锁是属于谁的,判断有没有加锁,如果没有加锁,直接使用,并且将标识改为已有锁,加上归属人
  2. 在JVM层面,是监视器锁
  3. 在操作系统层面,是互斥锁

面试题:synchronized在1.7的时候有什么优化(synchronized做了哪些优化)(也可以是实现原理)
在1.7之前都是重量级锁
无锁 -> 偏向锁 -> 轻量级锁(自旋) -> 重量级锁

面试题:什么是偏向锁
在线程初次访问的时候,将线程的id放到对象头中的偏向锁id的字段中,每次线程访问时,判断一下线程的id是否等于对象头中的偏向锁id,如果相等则表明这个线程拥有此锁,就可以正常执行代码了,否则就表明线程不拥有此锁,通过自旋的方式尝试获取锁。


3.公平锁和非公平锁(Java锁默认的策略时非公平锁)


  • 公平锁:获取锁的顺序按照线程访问的先后顺序获取 new ReentrantLock(true) -> 公平锁
  • 非公平锁:不会按照线程的先后访问顺序按序获取锁 (java里面的默认策略,性能比较高)

4. 独占锁和共享锁


  • 独占锁:指的是这一把锁只能被一个线程拥有 synchronized,Lock
  • 共享锁:指的是一把锁可以被多个线程同时拥有 ReadWriteLock 读写锁,读锁就是共享的(读的时候不会发生线程安全的问题)
    读写锁的优势:将锁的粒度更加的细化,从而提高锁的性能

读锁是共享锁,写锁是独占锁,读锁和写锁之间是互斥的

代码演示:

import java.util.Date;
import java.util.concurrent.LinkedBlockingDeque;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.ReentrantReadWriteLock;
/**
* 读写锁示例
*/
public class ThreadDemo95 {
public static void main(String[] args) {
//创建读写锁 可重入的读写锁 可以设置是否为公平锁,默认是非公平锁
ReentrantReadWriteLock readWriteLock = new ReentrantReadWriteLock();
//得到读锁
ReentrantReadWriteLock.ReadLock readLock = readWriteLock.readLock();
//得到写锁
ReentrantReadWriteLock.WriteLock writeLock = readWriteLock.writeLock();
//创建线程池
ThreadPoolExecutor executor = new ThreadPoolExecutor(10,10,
0, TimeUnit.SECONDS,new LinkedBlockingDeque<>(1000));
//任务1 读锁演示
executor.execute(new Runnable() {
@Override
public void run() {
readLock.lock(); //加锁
try {
System.out.println(Thread.currentThread().getName()+" 线程进入了读锁,时间:"+new Date());
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
readLock.unlock(); //释放锁
}
}
});
//任务2 读锁演示
executor.execute(new Runnable() {
@Override
public void run() {
readLock.lock(); //加锁
try {
System.out.println(Thread.currentThread().getName()+" 线程进入了读锁,时间:"+new Date());
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
readLock.unlock(); //释放锁
}
}
});
//如果两个锁的打印时间间隔不超过1秒,说明读锁是可共享的,否则是不可共享的
//任务3 写锁
executor.execute(new Runnable() {
@Override
public void run() {
writeLock.lock();
try {
System.out.println(Thread.currentThread().getName()+" 执行了写锁,时间:"+new Date());
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
writeLock.unlock();
}
}
});
//任务4 写锁
executor.execute(new Runnable() {
@Override
public void run() {
writeLock.lock();
try {
System.out.println(Thread.currentThread().getName()+" 执行了写锁,时间:"+new Date());
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
writeLock.unlock();
}
}
});
//如果两个锁的打印时间间隔不超过1秒,说明写锁是可共享的,否则是不可共享的(独占锁)
}
}

pool-1-thread-1 线程进入了读锁,时间:Fri May 28 22:29:53 CST 2021
pool-1-thread-2 线程进入了读锁,时间:Fri May 28 22:29:53 CST 2021
pool-1-thread-3 执行了写锁,时间:Fri May 28 22:29:56 CST 2021
pool-1-thread-4 执行了写锁,时间:Fri May 28 22:29:57 CST 2021

5. 可重入锁

可重入锁指的是一个线程在拥有了一把锁之后,可以重复的进入( synchronized)

/**
* 可重入锁演示
*/
public class ThreadDemo96 {
//声明锁
private static Object object = new Object();
public static void main(String[] args) {
synchronized (object) {
System.out.println("进入了主方法");
synchronized (object) {
System.out.println("重复进入了方法");
}
}
}
}

进入了主方法
重复进入了方法

典型使用场景:synchronized、ReentrantLock


6. 自旋锁

自旋锁相当于死循环,一直循环尝试获取锁。(synchronized,在偏向锁升级为轻量级锁的过程中)

下面的代码时伪代码,不可执行,只是想演示一下什么是自旋锁

public class ThreadDemo96 {
//声明锁
private static Object object = new Object();
public static void main(String[] args) {
while (true) {
if (尝试获取锁) {
break;
}
}
for (;;) {
if (尝试获取锁) {
break;
}
}
}
}

自旋锁的实现场景:synchronized




二、JUC(Java并发包)

在这里插入图片描述
JUC包下的所有类都是线程安全的,JUC下有:


  1. ReentrantLock 可重入锁

    a)lock()一定要放在try之前
    b)在finally里面一定要释放锁

  2. Semaphore 信号量,可以用来实现限流功能

  3. CyclicBarrier 循环屏障

  4. CountDownLatch 计数器 (没有公平还是非公平之说的,等待所有的线程进入某个步骤之后在统一执行某个流程,就好像田径比赛,当所有的选手都到达重点之后,再来宣布成绩)


1.信号量代码演示

import java.util.concurrent.LinkedBlockingDeque;
import java.util.concurrent.Semaphore;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
public class ThreadDemo97 {
public static void main(String[] args) {
//创建信号量
Semaphore semaphore = new Semaphore(2); //2 代表停车场里面有两个位置,即最多执行两个任务,剩下的任务只能等到前面的任务执行结束才能进行执行
//创建线程池
//每一个任务就是一辆车
ThreadPoolExecutor executor = new ThreadPoolExecutor(10,10,
0, TimeUnit.SECONDS,new LinkedBlockingDeque<>(1000));
executor.execute(new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName()+" 到达停车场门口");
try {
Thread.sleep(1000);

//尝试获取锁
semaphore.acquire();
} catch (InterruptedException e) {
e.printStackTrace();
}
//执行到这行代码代表已经获取到了停车位
System.out.println(Thread.currentThread().getName()+" 进入停车场");
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+" 离开停车场");
//释放锁
semaphore.release();
}
});
//执行任务2
executor.execute(new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName()+" 到达停车场门口");
try {
Thread.sleep(1000);
//尝试获取锁
semaphore.acquire();
} catch (InterruptedException e) {
e.printStackTrace();
}
//执行到这行代码代表已经获取到了停车位
System.out.println(Thread.currentThread().getName()+" 进入停车场");
try {
Thread.sleep(200);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+" 离开停车场");
//释放锁
semaphore.release();
}
});
//执行任务3
executor.execute(new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName()+" 到达停车场门口");
try {
Thread.sleep(1000);
//尝试获取锁
semaphore.acquire();
} catch (InterruptedException e) {
e.printStackTrace();
}
//执行到这行代码代表已经获取到了停车位
System.out.println(Thread.currentThread().getName()+" 进入停车场");
try {
Thread.sleep(1500);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+" 离开停车场");
//释放锁
semaphore.release();
}
});
//执行任务4
executor.execute(new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName()+" 到达停车场门口");
try {
Thread.sleep(1000);
//尝试获取锁
semaphore.acquire();
} catch (InterruptedException e) {
e.printStackTrace();
}
//执行到这行代码代表已经获取到了停车位
System.out.println(Thread.currentThread().getName()+" 进入停车场");
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+" 离开停车场");
//释放锁
semaphore.release();
}
});
}
}

pool-1-thread-1 到达停车场门口
pool-1-thread-2 到达停车场门口
pool-1-thread-3 到达停车场门口
pool-1-thread-4 到达停车场门口
pool-1-thread-3 进入停车场
pool-1-thread-1 进入停车场
pool-1-thread-1 离开停车场
pool-1-thread-4 进入停车场
pool-1-thread-3 离开停车场
pool-1-thread-2 进入停车场
pool-1-thread-2 离开停车场
pool-1-thread-4 离开停车场

2. 计数器代码演示

执行原理就是内部有一个计数器,当执行了CoubtDown 之后计数器-1,直到减到0那么这个计数器就使用完毕了,就可以执行await()之后的代码了

import java.util.Random;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.LinkedBlockingDeque;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
/**
* 计数器示例
*/
public class ThreadDemo98 {
public static void main(String[] args) throws InterruptedException {
//创建计数器为3
CountDownLatch countDownLatch = new CountDownLatch(3); //当线程数到达3个之后再去执行其他的任务
ThreadPoolExecutor executor = new ThreadPoolExecutor(10,10,
0, TimeUnit.SECONDS,new LinkedBlockingDeque<>(100));
//执行任务
executor.execute(new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName()+" 开跑");
int num = new Random().nextInt(5);
num+=1;
try {
Thread.sleep(num*1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+" 也到达了终点");
//计数器-1
countDownLatch.countDown();
}
});
//执行任务
executor.execute(new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName()+" 开跑");
int num = new Random().nextInt(5);
num+=1;
try {
Thread.sleep(num*1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+" 也到达了终点");
//计数器-1
countDownLatch.countDown();
}
});
//执行任务
executor.execute(new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName()+" 开跑");
int num = new Random().nextInt(5);
num+=1;
try {
Thread.sleep(num*1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+" 也到达了终点");
//计数器-1
countDownLatch.countDown();
}
});
//执行任务
/*
executor.execute(new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName()+" 开跑");
int num = new Random().nextInt(5);
num+=1;
try {
Thread.sleep(num*1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+" 也到达了终点");
//计数器-1
countDownLatch.countDown();
}
});
*/
//等待所有的选手都到达终点,等待计数器为0
countDownLatch.await();
System.out.println("比赛结束,宣布成绩");
}
}

pool-1-thread-1 开跑
pool-1-thread-2 开跑
pool-1-thread-3 开跑
pool-1-thread-1 也到达了终点
pool-1-thread-3 也到达了终点
pool-1-thread-2 也到达了终点
比赛结束,宣布成绩

但是上面的计数器(countDownLatch )只能使用一次,当使用结束以后就不能继续使用了。所以有了循环屏障


3. 循环屏障

循环屏障可以循环使用

代码演示:

import java.util.concurrent.*;
/**
* 循环屏障
*/
public class ThreadDemo99 {
public static void main(String[] args) {
CyclicBarrier cyclicBarrier = new CyclicBarrier(2, new Runnable() {
@Override
public void run() {
System.out.println("----------到达了屏障------------");
}
});
ThreadPoolExecutor executor = new ThreadPoolExecutor(10,10,
0, TimeUnit.SECONDS,new LinkedBlockingDeque<>(1000));
for (int i = 0; i <4; i++) {
final int finalI = i;
executor.execute(new Runnable() {
@Override
public void run() {
try {
Thread.sleep(finalI*1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+" 进入了任务");
try {
//等待其他线程执行
cyclicBarrier.await();
} catch (InterruptedException e) {
e.printStackTrace();
} catch (BrokenBarrierException e) {
e.printStackTrace();
}
//System.out.println(Thread.currentThread().getName()+" 执行结束");
}
});
}
}
}

pool-1-thread-1 进入了任务
pool-1-thread-2 进入了任务
----------到达了屏障------------
pool-1-thread-3 进入了任务
pool-1-thread-4 进入了任务
----------到达了屏障------------

循环屏障执行原理:内部有一个计数器,每次线程执行到await方法的时候,计数器+1,直到计数器个数等于创建时声明的格式的时候,就会突破屏障,执行之后的代码,在突破屏障之后计时器清零可以进行新一轮的执行了。

面试题:CyclicBarrier 和CountDownLauch 的区别?
答:CountDownLauch 他的计数器只能使用一次 CyclicBarrier可以反复使用




三、HashMap:非安全的容器
  1. 多线程下的问题:
    a) JDK1.7 头插法 -> 死循环
    b) JDK1.8 尾插法 -> 数据覆盖

  2. 解决上面两个问题的方法:
    1.加锁
    2.JDK提供HashMap安全容器 ConcurrentHashMap(分段锁) Hashtable(put整体加锁)

  3. HashMap的安全版本:ConcurrentHashMap

ConcurrentHashMap实现线程安全的原理是在修改操作的时候(put),会在进入方法之后加锁,并且会在操作完成之后释放锁,所以不会有线程安全的问题

ConcurrentHashMap优化:将HashMap分成多个sengment对每个 sengment 分别进行加锁,这样就可以保障多线程如果操作的不是同一个sengment就不需要进行排队处理了,从而提高了程序的执行效率。

HashMap是非线程安全的
Hashtable是线程安全的



推荐阅读
author-avatar
Snape吾爱
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有