赞
踩
同步和异步通常用来形容一次方法调用。
调用一旦开始,调用者必须等到方法调用返回后,才能继续后续的行为。
目的:都是为了解决多线程中的对同一资源的访问冲突;
比如银行的转账系统,对数据库的保存操作等等,都会使用同步交互操作。
调用更像一个消息传递,一旦开始,方法调用就会立即返回,调用者就可以继续后续的操作。而异步方法通常会在另外一个线程中,“真实”地执行着。整个过程,不会阻碍调用者的工作。
在多核情况下,每条线程可能运行于不同的CPU 中,因此每个线程
运行时有自己的高速缓存(对单核CPU 来说,其实也会出现这种问题,只不过
是以线程调度的形式来分别执行的)。为了保留在这些高速缓存中的共享资源,保持数据一致性的机制,叫做缓存一致性。
为了解决缓存不一致性问题,通常来说有以下2 种解决方法:
CPU 和其他部件进行通信都是通过总线来进行的,如果对总线加LOCK#锁的话,就阻塞了其他CPU 对其他部件访问(如内存),从而使得只能有一个CPU 能使用这个变量的内存。这样做的缺点是:在锁住总线期间,其他CPU 无法访问内存,导致效率低下。
该协议保证了每个缓存中使用的共享变量的副本是一致的。
核心思想:当CPU 向内存写入数据时,如果发现操作的变量是共享变量,即在其他CPU 中也存在该变量的副本,会发出信号通知其他CPU 将该变量的缓存行置为无效状态,因此当其他CPU 需要读取这个变量时,发现自己缓存中缓存该变量的缓存行是无效的,那么它就会从内存重新读取。
单CPU的多进程其实是多任务的快速轮转调度,不是并发执行;
只有多CPU才能真正的并发运行。
指令重排导致了有序性;线程切换导致了原子性;缓存导致了可见性 。
了解概念即可,实际应用场景中线程的使用均是通过线程池来实现的。
java5引入了java.util.concurrent.Executor
,spring Framework 2.0引入了interface TaskExecutor extends Executor
。
TaskExecutor 常用实现:
Spring 3.0之后提供了一个@Async注解。
原理:AOP,根据切入点创建代理,调用@Async注解标注的方法时,会调用代理,执行切入点处理器invoke方法,将方法的执行提交给线程池,实现异步执行。
注解 | 场景说明 |
---|---|
@EnableAsync | 配置类中通过此注解开启对异步任务的支持; |
@Async | 在实际执行的bean方法使用该注解来声明其是一个异步任务 |
使用时要注意:
@EnableAsync
@Async("name")
指定线程池。/** * 线程池的配置 */ @Configuration @EnableAsync public class AsyncConfig { private static final int MAX_POOL_SIZE = 50; private static final int CORE_POOL_SIZE = 20; @Bean("asyncTaskExecutor") public AsyncTaskExecutor asyncTaskExecutor() { ThreadPoolTaskExecutor asyncTaskExecutor = new ThreadPoolTaskExecutor(); // 指定最大线程数 asyncTaskExecutor.setMaxPoolSize(MAX_POOL_SIZE); // 核心线程数目 asyncTaskExecutor.setCorePoolSize(CORE_POOL_SIZE); // 队列中最大的数目 asyncTaskExecutor.setQueueCapacity(20); // 线程空闲后的最大存活时间 asyncTaskExecutor.setKeepAliveSeconds(60); // 线程名称前缀 asyncTaskExecutor.setThreadNamePrefix("async-task-thread-pool-"); // 等待任务在关机时完成-表明等待所有线程执行完 asyncTaskExecutor.setWaitForTasksToCompleteOnShutdown(true); // 等待时间(默认为0,此时立即停止) asyncTaskExecutor.setAwaitTerminationSeconds(60); //拒绝策略 asyncTaskExecutor.setRejectedExecutionHandler(new ThreadPoolExecutor.AbortPolicy()); asyncTaskExecutor.initialize(); return asyncTaskExecutor; } } //其它类异步调用的时候,@Async会默认从线程池获取线程,当然也可以显式的指定 @Async("asyncTaskExecutor")
除了使用@Async注解来开启异步任务,也可以使用线程池对象,来开启异步任务:
@Autowired
AsyncTaskExecutor asyncTaskExecutor;//注入线程池对象
//通过线程池对象提交异步任务
asyncTaskExecutor.submit(() -> {
log.info("异步任务开始");
//省略异步任务业务逻辑...
log.info("异步任务结束");
});
Future表示异步计算的结果,提供了用于检查计算是否完成、等待计算完成、以及检索计算结果的方法。
fork-join框架就是实现了Future接口。核心方法:
方法 | 说明 |
---|---|
get() | 等待任务完成,获取执行结果,如果任务取消会抛出异常; |
get(long timeout, TimeUnit unit) | 指定等待任务完成的时间,等待超时会抛出异常; |
isDone() | 判断任务是否完成; |
isCancelled() | 判断任务是否被取消; |
cancel(boolean mayInterruptIfRunning) | 尝试取消此任务的执行,如果任务已经完成、已经取消或由于其他原因无法取消,则此尝试将失败; |
@Slf4j
public class FutureTask {
@Async
public Future<String> taskOne() throws Exception {
//执行内容同上,省略
return new AsyncResult<>("1完成");
}
}
Future<String> taskOne = futureTask.taskOne();
if (taskOne.isDone()) {
log.info("任务1返回结果={}", taskOne.get());
}
Spring 3.0还引用了任务调度接口 TaskScheduler。
定时任务:
指定某个特定时间执行,或者多次重复执行的任务。
public interface TaskScheduler { //推荐! //通过触发器(Trigger )来决定task是否执行(更加灵活) ScheduledFuture schedule(Runnable task, Trigger trigger); //在starttime的时候执行一次 ScheduledFuture schedule(Runnable task, Date startTime); //从starttime开始每个period时间段执行一次task ScheduledFuture scheduleAtFixedRate(Runnable task, Date startTime, long period); //每隔period执行一次 ScheduledFuture scheduleAtFixedRate(Runnable task, long period); //从startTime开始每隔delay长时间执行一次 ScheduledFuture scheduleWithFixedDelay(Runnable task, Date startTime, long delay); //每隔delay时间执行一次 ScheduledFuture scheduleWithFixedDelay(Runnable task, long delay); }
模式 | 说明 | 备注 |
---|---|---|
fixedRate | 两次任务开始时间之间间隔指定时长 | 默认 |
fixedDelay | 上一次任务的结束时间与下一次任务开始时间``间隔指定时长 |
2)CronTrigger
通过Cron表达式来生成调度计划。比如:
//工作日的9点到17点,每个小时的15分执行一次
scheduler.schedule(task, new CronTrigger("0 15 9-17 * * MON-FRI"));
注解 | 场景说明 | 备注 |
---|---|---|
@EnableScheduling | 在配置类上使用,开启计划任务的支持 | 类上 |
@Scheduled | 来申明这是一个任务,包括cron,fixDelay,fixRate等类型。方法上,需先开启计划任务的支持 | @Scheduled(cron="0 0/30 * * * ?") cron表达式至少6位(也有7位),由空格分隔,按顺序依次表示:秒(0~59 )分时(0~23 )天(0~31 )月(0~11 ) |
注意:
调度仅支持当前物理机调度,不适用分布式架构。
保证多个线程访问同一个共享资源时,数据是一致的。
其实就是满足程序访问共享资源时的原子性、有序性、可见性。
同步方法的常量池中会有一个ACC_SYNCHRONIZED标识,当其他线程访问某个方法时,如果有ACC_SYNCHRONIZED设置则需要先获得监视器(Monitor,每个对象都有一个Monitor,它用于保证同一时间只能有一个线程访问当前对象)锁才能执行方法,执行之后再释放监视器锁。如果方法发生异常,抛异常之前会释放监视器锁。
1.6之前,synchronized加锁时会调用对象的objectMonitor的enter方法,解锁时会调用exit方法。锁是重量级锁:java线程需要映射到os的原生线程之上,如果阻塞或者唤醒一个线程就需要os的帮忙,进行用户态到核心态的切换,花费很多cpu时间。
1.6之后优化了锁:自旋锁(1.4推出,默认关闭。1.6默认打开)
锁状态 | 对象头-是否偏向锁 | 锁标志位 | 其它 |
---|---|---|---|
无锁 | 0 | 01 | hashCode |
偏向锁 | 1 | 01 | ThreadId… |
轻量级锁 | 00 | 指向线程栈中锁记录的指针 | |
重量级锁 | 10 | 指向等待队列的指针 | |
GC标记 | 11 | 空 |
尽量减小锁的粒度。JIT会自动粗化锁:一系列操作反复加锁、解锁,会将加锁范围扩散(粗化)到整个操作序列的外部。
JIT经过逃逸分析之后发现并无线程安全问题的话会自动做锁消除。
什么是原子性?
原子操作即:不可分割的单一的操作。
如 i++可以分为3步原子操作:① 读取变量 i 当前值 ②拿 i 的当前值,和1做加法运算 ③将加法结果赋值给 i 变量。
非原子操作在cpu的时间片切换的影响下可能会受到其他线程的干扰。
synchronized关键字实现操作的原子性,本质是通过该关键字所包括的临界区的排他性保证在任何一个时刻只有一个线程能够执行临界区中的代码。
什么是内存的可见性?
CPU在执行代码时,为减少变量访问时间将其缓存到CPU的缓存区。如此再次访问变量,是从cache中读取而不是从内存读取。同样也未写入内存。每个CPU有自己的缓存区,其内容对其他CPU不可见。
synchronized在解锁之前会把数据重新写回内存。
保证一个线程执行临界区中的代码时,所修改的变量值对于稍后执行该临界区的线程来说是可见的。
synchronized保证了单线程的执行,单线程下指令重排会进行限制(实际上还是有重排)。
同步常用的有三种:
用的锁是obj。
demo
package Thread; import javax.swing.plaf.SliderUI; public class MyThread implements Runnable { private int i = 4; Object obj = new Object(); @Override public void run() { while (true) { /* * 对象如同锁,持有锁才可以在同步中执行。 * 没有锁的即使获取cpu的执行权,也无法进入同步块。 * 要等待当前进程释放锁,其他进程才能抢占对象锁。 */ synchronized (obj) { if (i > 0) { try { Thread.sleep(10); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName() + "--this is MyThread:" + i--); } } } } }
用的锁是this。
/** *线程安全的计数器 */ public class ThreadSafeCounter{ private int counter = 0; public void increment(){ synchronized(this){ counter++; } } public int get(){ synchronized(this){ return counter; } } }
用的锁是该类。
因为静态方法中不能定义this。静态进内存时,内存中没有本类对象,但是一定有该类对应的字节码文件对象。静态同步方法,使用的锁是该方法所在类的字节码文件对象:通过【类名.class】,获取。
public synchronized static void doSth(){
}
相当于:
synchronized(A.class){
}
轻量级的synchronized,在多处理器编程中保证共享变量的统一性。
双重校验锁DCL(double checked locking)–使用volatile 的场景之一。
由于有些时候对 volatile的操作,不会被保存,说明不会造成阻塞。不可用与多线程环境下的计数器。
是一组处理器指令,解决禁止指令重排序和内存可见性的问题。
1.先于这个内存屏障的指令必须先执行,后于这个内存屏障的指令必须后执行。
2.使得内存可见性。
所以,如果你的字段是volatile,在读指令前插入读屏障,可以让高速缓存中的数据失效,重新从主内存加载数据。在写指令之后插入写屏障,能让写入缓存的最新数据写回到主内存。
1.volatile 是变量修饰符,而synchronized 则作用于代码块或方法。
2.volatile 不会对变量加锁,不会造成线程的阻塞;synchronized 会对变量加锁,可能会造成线程的阻塞。
3.volatile 仅能实现变量的修改可见性,并不能保证原子性;而synchronized 则可以保证变量的修改可见性和原子性。
(synchronized 有两个重要含义:它确保了一次只有一个线程可以执行代码的受保护部分(互斥),而且它确保了一个线程更改的数据对于其它线程是可见的(更改的可见性),在释放锁之前会将对变量的修改刷新到主存中)。
4.volatile 标记的变量不会被编译器优化,禁止指令重排序;synchronized 标记的变量可以被编译器优化。
只需要保证共享资源的可见性的时候可以使用volatile 替代,
synchronized 保证可操作的原子性、一致性和可见性。
线程安全:
指当多线程访问时,采用了加锁的机制;即当一个线程访问该类的某个数据时,会对这个数据进行保护,其他线程不能对其访问,直到该线程读取完之后,其他线程才可以使用。防止出现数据不一致或者数据被污染等意外情况。
线程不安全:
就是不提供数据访问时的数据保护,多个线程能够同时操作某个数据,从而出现数据不一致或者数据污染等意外情况。
常见的线程不安全的集合:
ArrayList(多个线程操作会抛异常:ConcurrentModificationException);
常见的线程安全的集合:
CopyOnWriteArrayList(写时复制)
Vector
AtomicLong是作用是对长整形进行原子操作。
在32位操作系统中,64位的long 和 double 变量由于会被JVM当作两个分离的32位来进行操作,所以不具有原子性。而使用AtomicLong能让long的操作保持原子型。
常见用法:
//0、创建具有初始值 0 的新 AtomicLong。 AtomicLong atomicLong = new AtomicLong(); System.out.println("Value:" + atomicLong.get()); //0 //1、设置值 atomicLong.set(10); System.out.println("Value:" + atomicLong.get()); //10 //2、创建具有给定初始值的新 AtomicLong。 AtomicLong atomicLong1 = new AtomicLong(10); System.out.println("Value:" + atomicLong1.get()); //10 //3、以原子方式将给定值添加到当前值,先加上特定的值 相当于++5 System.out.println("Value:" + atomicLong1.addAndGet(5)); //15 //4、先获取当前值再加上特定的值 相当于5++ System.out.println("Value:" + atomicLong1.getAndAdd(5)); //15 System.out.println("Value:" + atomicLong1); //20 //5、如果当前值 == 预期值,则以原子方式将该值设置为给定的更新值。 compareAndSet(long expect, long update) System.out.println("Value:" + atomicLong1.compareAndSet(20,15));//true System.out.println("Value:" + atomicLong1);//15 //6、 以原子方式将当前值减 1,先减去1再获取值 atomicLong1.decrementAndGet() //7、先获取当前值再减1 getAndDecrement() //8、先获取当前值再加1 getAndIncrement() //9、先加1再获取当前值 incrementAndGet() //10、先获取当前值再设置新的值 getAndSet()
AtomicReference类提供了对象引用的非阻塞原子性读写操作。
针对对象引用的修改包含了如下两个步骤:获取该引用和改变该引用(每一个步骤都是原子性的操作,但组合起来就无法保证原子性了)。即使用volatile修饰也不能保证原子性。
synchronized是一种阻塞式的解决方案,同一时刻只能有一个线程真正在工作,其他线程都将陷入阻塞,效率低。利用AtomicReference的非阻塞原子性解决方案能更加高效一点。
说明 | 说明 | 举例 | 备注 |
---|---|---|---|
无参构造函数 | AtomicReference() | 需要再次调用set()方法为AtomicReference内部的value指定初始值 | |
有参构造函数 | AtomicReference(V initialValue); | private static AtomicReference<DebitCard> debitCardRef = new AtomicReference<>(new DebitCard("zhangsan", 0)); | 已经指定了初始值 |
原子性地更新AtomicReference内部的value值 | boolean compareAndSet(V expect, V update) | expect:当前值,update:要更新的新值。当expect和AtomicReference的当前值不相等时,修改会失败,返回值为false | |
原子性地更新AtomicReference内部的value值,并且返回AtomicReference的旧值。 | getAndSet(V newValue) | ||
原子性地更新value值,并且返回AtomicReference的旧值,该方法需要传入一个Function接口。 | getAndUpdate(UnaryOperator<V> updateFunction) | ||
原子性地更新value值,并且返回AtomicReference更新后的新值,该方法需要传入一个Function接口。 | updateAndGet(UnaryOperator<V> updateFunction) | ||
原子性地更新value值,并且返回AtomicReference更新前的旧值。 | getAndAccumulate(V x, BinaryOperator<V> accumulatorFunction) | 第一个参数是更新后的新值,第二个是BinaryOperator接口。 | |
原子性地更新value值,并且返回AtomicReference更新后的值。 | accumulateAndGet(V x, BinaryOperator<V> accumulatorFunction) | 第一个参数是更新的新值,第二个是BinaryOperator接口。 | |
获取AtomicReference的当前对象引用值 | get() | ||
设置AtomicReference最新的对象引用值,该新值的更新对其他线程立即可见。 | set(V newValue) | ||
设置AtomicReference的对象引用值。 | lazySet(V newValue) |
使用Lock 必须在try-catch-finally 块中进行,并且将释放锁的操作放在finally 块中进行,以保证锁一定被释放,防止死锁的发生。通常使用Lock 来进行同步的话,是以下面这种形式去使用的:
Lock lock = ...;
lock.lock();
try{
//处理任务
}catch(Exception ex){
}finally{
lock.unlock(); //释放锁
}
1)Lock 是一个接口,而synchronized 是Java 中的关键字,synchronized 是内置的语言实现;
2)synchronized 在发生异常时,会自动释放线程占有的锁,因此不会导致死锁现象发生;而Lock 在发生异常时,如果没有主动通过unLock()去释放锁,则很可能造成死锁现象,因此使用Lock 时需要在finally 块中释放锁;
3)Lock 可以让等待锁的线程响应中断(可中断锁),而synchronized却不行,使用synchronized 时,等待的线程会一直等待下去,不能够响应中断(不可中断锁);
4)通过Lock 可以知道有没有成功获取锁(tryLock()方法:如果获取了锁,则返回true;否则返回false,也就说这个方法无论如何都会立即返回。在拿不到锁时不会一直在那等待),而synchronized 却无法办到。
5)Lock 可以提高多个线程进行读操作的效率(读写锁)。
6)Lock 可以实现公平锁,synchronized 不保证公平性。
在性能上来说,如果线程竞争资源不激烈时,两者的性能是差不多的,而当竞争资源非常激烈时(即有大量线程同时竞争),此时Lock 的性能要远远优于synchronized。所以说,在具体使用时要根据适当情况选择。
Java7 的一个用于并行执行的任务的框架(分治法)。
final ForkJoinPool.ForkJoinWorkerThreadFactory factory = pool -> {
final ForkJoinWorkerThread worker = ForkJoinPool.defaultForkJoinWorkerThreadFactory.newThread(pool);
worker.setName("my-thread" + worker.getPoolIndex());
return worker;
};
//参数:
//1. 最大线程数(默认值为cpu核数)
//2. 创建线程的工厂(默认:ForkJoinWorkerThreadFactory defaultForkJoinWorkerThreadFactory)
//3. 异常处理器(默认为 null):在线程执行任务时对由于某些无法预料的错误而导致任务线程中断时,该处理器会进行一些处理。
//4. 表示工作线程内的任务队列是采用何种方式进行调度。true:FIFO;fasle: LIFO(默认)。
private final ForkJoinPool forkJoinPool = new ForkJoinPool(5, factory, null, false);
submit提交任务并开始执行。
//GetBatchTask extends RecursiveTask<List>
GetBatchTask getBatchTask = new GetBatchTask();
ForkJoinTask<List> submit = forkJoinPool.submit(getBatchTask);
task.fork();
将任务加入线程队列,并手动保存当前所有子任务;task.join()
,手动获取所有子任务结果并归并所有结果。if(task.isCompletedAbnormally()) {
System.out.println(task.getException());
}
RecurisiveTask(有返回结果);
RecursiveAction(没有返回结果)
需求是:计算1+2+3+4的结果.
package cn.day8; import java.util.concurrent.ExecutionException; import java.util.concurrent.ForkJoinPool; import java.util.concurrent.Future; import java.util.concurrent.RecursiveTask; public class CountTask extends RecursiveTask<Integer> { private static final int THRESHOLD = 2; // 阈值 private int start; private int end; public CountTask(int start, int end) { this.start = start; this.end = end; } @Override protected Integer compute() { // TODO Auto-generated method stub int sum = 0; // 如果任务足够小就计算任务 boolean canCompute = (end - start) <= TH`RES`HOLD; if (canCompute) { for (int i = start; i <= end; i++) { sum += i; } } else { // 如果任务大于阈值,就分裂成两个子任务计算 int middle = (start + end) / 2; CountTask leftTask = new CountTask(start, middle); CountTask rightTask = new CountTask(middle + 1, end); // 执行子任务 leftTask.fork(); rightTask.fork(); // 等待子任务执行完,并得到其结果 int leftResult = leftTask.join(); int rightResult = rightTask.join(); // 合并子任务 sum = leftResult + rightResult; } return sum; } public static void main(String[] args) { final ForkJoinPool.ForkJoinWorkerThreadFactory factory = pool -> { final ForkJoinWorkerThread worker = ForkJoinPool.defaultForkJoinWorkerThreadFactory.newThread(pool); worker.setName("my-thread" + worker.getPoolIndex()); return worker; }; //创建分治任务线程池,可以追踪到线程名称。线程池一定要作为公共的进行管理,并且要限制初始化的大小和队列的长度,否则会一直吃资源。 ForkJoinPool forkJoinPool = new ForkJoinPool(4, factory, null, false); // 生成一个计算任务,负责计算1+2+3+4 CountTask task = new CountTask(1, 4); // 执行一个任务 Future<Integer> result = forkJoinPool.submit(task); try { System.out.println(result.get()); } catch (Exception e) { // TODO Auto-generated catch block e.printStackTrace(); } } }
访问频率较高但内容变动较小,使用网站HTML 静态化方案来优化访问速度。将社区内的帖子、文章进行实时的静态化,有更新的时候再重新静态化也是大量使用的策略。
优势:
一、减轻服务器负担。
二、加快页面打开速度,静态页面无需访问数据库,打开速度较动态页面有明显提高;
三、很多搜索引擎都会优先收录静态页面,不仅被收录的快,还收录的全,容易被搜索引擎找到;
四、HTML 静态页面不会受程序相关漏洞的影响,减少攻击,提高安全性。
现在很多的网站上都会用到大量的图片,而图片是网页传输中占主要的数据量,也是影响网站性能的主要因素。因此很多网站都会将图片存储从网站中分离出来,另外架构一个或多个服务器来存储图片,将图片放到一个虚拟目录中,而网页上的图片都用一个URL 地址来指向这些服务器上的图片的地址,这样的话网站的性能就明显提高了。
优势:
一、分担Web 服务器的I/O 负载-将耗费资源的图片服务分离出来,提高服务器的性能和稳定性。
二、能够专门对图片服务器进行优化-为图片服务设置有针对性的缓存方案,减少带宽成本,提高访问速度。
三、提高网站的可扩展性-通过增加图片服务器,提高图片吞吐能力。
见“数据库部分的—如果有一个特别大的访问量到数据库上,怎么做优化?”==>缓存
尽量使用缓存,包括用户缓存,信息缓存等,多花点内存来做缓存,可以大量减少与数据库的交互,提高性能。假如我们能减少数据库频繁的访问,那对系统肯定大大有利的。比如一个电子商务系统的商品搜索,如果某个关键字的商品经常被搜,那就可以考虑这部分商品列表存放到缓存(内存中去),这样不用每次访问数据库,性能大大增加。
镜像是冗余的一种类型,一个磁盘上的数据在另一个磁盘上存在一个完全相同的副本即为镜像。
在网站高并发访问的场景下,使用负载均衡技术(负载均衡服务器)为一个应用构建一个由多台服务器组成的服务器集群,将并发访问请求分发到多台服务器上处理,避免单一服务器因负载压力过大而响应缓慢,使用户请求具有更好的响应延迟特性。
加锁,如乐观锁和悲观锁。
如:订票系统,某车次只有一张火车票,假定有1w 个人同时打开12306 网站来订票,如何解决并发问题?(可扩展
到任何高并发网站要考虑的并发读写问题)。
不但要保证1w 个人能同时看到有票(数据的可读性),还要保证最终只能由一个人买到票(数据的排他性)。使用数据库层面的并发访问控制机制。采用乐观锁即可解决此问题。乐观锁意思是不锁定表的情况下,利用业务的控制来解决并发问题,这样既保证数据的并发可读性,又保证保存数据的排他性,保证性能的同时解决了并发带来的脏数据问题。hibernate 中实现乐观锁。
通过mq 一个一个排队方式,跟12306 一样。
300个用户在一个客户端上,会占用客户机更多的资源,而影响测试的结果。
线程之间可能发生干扰,而产生一些异常。
300个用户在一个客户端上,需要更大的带宽。
IP地址的问题,可能需要使用IP Spoof来绕过服务器对于单一IP地址最大连接数的限制。
所有用户在一个客户端上,不必考虑分布式管理的问题;而用户分布在不同的客户端上,需要考虑使用控制器来整体调配不同客户机上的用户。同时,还需要给予相应的权限配置和防火墙设置。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。