(java多线程并发)控制并发线程数的Semaphore、ScheduledThreadPoolExcutor、BlockingQueue、ReadWriteLock...

控制并发线程数的Semaphore

1.简介
信号量(Semaphore),有时被称为信号灯,是在多线程环境下使用的一种设施, 它负责协调各个线程, 以保证它们能够正确、合理的使用公共资源。

2.概念
Semaphore分为单值和多值两种,前者只能被一个线程获得,后者可以被若干个线程获得。

以一个停车场运作为例。为了简单起见,假设停车场只有三个车位,一开始三个车位都是空的。这时如果同时来了五辆车,看门人允许其中三辆不受阻碍的进入,然后放下车拦,剩下的车则必须在入口等待,此后来的车也都不得不在入口处等待。这时,有一辆车离开停车场,看门人得知后,打开车拦,放入一辆,如果又离开两辆,则又可以放入两辆,如此往复。

在这个停车场系统中,车位是公共资源,每辆车好比一个线程,看门人起的就是信号量的作用。

更进一步,信号量的特性如下:信号量是一个非负整数(车位数),所有通过它的线程(车辆)都会将该整数减一(通过它当然是为了使用资源),当该整数值为零时,所有试图通过它的线程都将处于等待状态。在信号量上我们定义两种操作: Wait(等待) 和 Release(释放)。 当一个线程调用Wait(等待)操作时,它要么通过然后将信号量减一,要么一直等下去,直到信号量大于一或超时。Release(释放)实际上是在信号量上执行加操作,对应于车辆离开停车场,该操作之所以叫做“释放”是因为加操作实际上是释放了由信号量守护的资源。

在java中,还可以设置该信号量是否采用公平模式,如果以公平方式执行,则线程将会按到达的顺序(FIFO)执行,如果是非公平,则可以后请求的有可能排在队列的头部。
JDK中定义如下:
Semaphore(int permits, boolean fair)
  创建具有给定的许可数和给定的公平设置的Semaphore。

Semaphore当前在多线程环境下被扩放使用,操作系统的信号量是个很重要的概念,在进程控制方面都有应用。Java并发库Semaphore 可以很轻松完成信号量控制,Semaphore可以控制某个资源可被同时访问的个数,通过 acquire() 获取一个许可,如果没有就等待,而 release() 释放一个许可。比如在Windows下可以设置共享文件的最大客户端访问个数。

Semaphore实现的功能就类似厕所有5个坑,假如有10个人要上厕所,那么同时只能有多少个人去上厕所呢?同时只能有5个人能够占用,当5个人中 的任何一个人让开后,其中等待的另外5个人中又有一个人可以占用了。另外等待的5个人中可以是随机获得优先机会,也可以是按照先来后到的顺序获得机会,这取决于构造Semaphore对象时传入的参数选项。单个信号量的Semaphore对象可以实现互斥锁的功能,并且可以是由一个线程获得了“锁”,再由另一个线程释放“锁”,这可应用于死锁恢复的一些场合。

3.案例一:

  1. packageSemaPhore;
  2. importjava.util.Random;
  3. importjava.util.concurrent.*;
  4. publicclassTest{
  5. publicstaticvoidmain(String[]args){
  6. //线程池
  7. ExecutorServiceexecutor=Executors.newCachedThreadPool();
  8. //定义信号量,只能5个线程同时访问
  9. finalSemaphoresemaphore=newSemaphore(5);
  10. //模拟20个线程同时访问
  11. for(inti=0;i<20;i++){
  12. finalintNO=i;
  13. Runnablerunnable=newRunnable(){
  14. publicvoidrun(){
  15. try{
  16. //获取许可
  17. semaphore.acquire();
  18. //availablePermits()指的是当前信号灯库中有多少个可以被使用
  19. System.out.println("线程"+Thread.currentThread().getName()+"进入,当前已有"+(5-semaphore.availablePermits())+"个并发");
  20. System.out.println("index:"+NO);
  21. Thread.sleep(newRandom().nextInt(1000)*10);
  22. System.out.println("线程"+Thread.currentThread().getName()+"即将离开");
  23. //访问完后,释放
  24. semaphore.release();
  25. }catch(Exceptione){
  26. e.printStackTrace();
  27. }
  28. }
  29. };
  30. executor.execute(runnable);
  31. }
  32. //退出线程池
  33. executor.shutdown();
  34. }
  35. }


4.案例二:

下面是模拟一个连接池,控制同一时间最多只能有50个线程访问。

  1. importjava.util.UUID;
  2. importjava.util.concurrent.Semaphore;
  3. importjava.util.concurrent.TimeUnit;
  4. publicclassTestSemaphoreextendsThread{
  5. publicstaticvoidmain(String[]args){
  6. inti=0;
  7. while(i<500){
  8. i++;
  9. newTestSemaphore().start();
  10. try{
  11. Thread.sleep(1);
  12. }catch(InterruptedExceptione){
  13. e.printStackTrace();
  14. }
  15. }
  16. }
  17. /**
  18. *控制某资源同时被访问的个数的类控制同一时间最后只能有50个访问
  19. */
  20. staticSemaphoresemaphore=newSemaphore(50);
  21. staticinttimeout=500;
  22. publicvoidrun(){
  23. try{
  24. Objectconnec=getConnection();
  25. System.out.println("获得一个连接"+connec);
  26. Thread.sleep(300);
  27. releaseConnection(connec);
  28. }catch(InterruptedExceptione){
  29. e.printStackTrace();
  30. }
  31. }
  32. publicvoidreleaseConnection(Objectconnec){
  33. /*释放许可*/
  34. semaphore.release();
  35. System.out.println("释放一个连接"+connec);
  36. }
  37. publicObjectgetConnection(){
  38. try{/*获取许可*/
  39. booleangetAccquire=semaphore.tryAcquire(timeout,TimeUnit.MILLISECONDS);
  40. if(getAccquire){
  41. returnUUID.randomUUID().toString();
  42. }
  43. }catch(InterruptedExceptione){
  44. e.printStackTrace();
  45. }
  46. thrownewIllegalArgumentException("timeout");
  47. }
  48. }




Timer与ScheduledThreadPoolExcutor

1.用timer缺点非常大

Timer 的优点在于简单易用,但由于所有任务都是由同一个线程来调度因此所有任务都是串行执行的,同一时间只能有一个任务在执行前一个任务的延迟或异常都将会影响到之后的任务。


我们关于定时/周期操作都是通过Timer来实现的。但是Timer有以下几种危险

a. Timer是基于绝对时间的。容易受系统时钟的影响。
b. Timer只新建了一个线程来执行所有的TimeTask。所有TimeTask可能会相关影响
c. Timer不会捕获TimerTask的异常,只是简单地停止。这样势必会影响其他TimeTask的执行。


2.ScheduledThreadPoolExecutor

鉴于 Timer 的上述缺陷,Java 5 推出了基于线程池设计的ScheduledThreadPoolExecutor。其设计思想是,每一个被调度的任务都会由线程池中一个线程去执行,因此任务是并发执行的,相互之间不会受到干扰。需 要注意的是,只有当任务的执行时间到来时,ScheduedExecutor 才会真正启动一个线程,其余时间 ScheduledExecutor 都是在轮询任务的状态。


有以下四个调度器的方法:

public ScheduledFuture<?> schedule(Runnable command,
				       long delay, TimeUnit unit);

public <V> ScheduledFuture<V> schedule(Callable<V> callable,
					   long delay, TimeUnit unit);

public ScheduledFuture<?> scheduleAtFixedRate(Runnable command,
						  long initialDelay,
						  long period,
						  TimeUnit unit);

public ScheduledFuture<?> scheduleWithFixedDelay(Runnable command,
						     long initialDelay,
						     long delay,
						     TimeUnit unit);

那么这四个方法有什么区别呢?其实第一个和第二个区别不大,一个是Runnable、一个是Callable,内部包装后是一样的效果;所以把头两个方法几乎当成一种调度,那么三种情况分别是:

1、 进行一次延迟调度:延迟delay这么长时间,单位为:TimeUnit传入的的一个基本单位,例如:TimeUnit.SECONDS属于提供好的枚举信息;(适合于方法1和方法2)。

2、 多次调度,每次依照上一次预计调度时间进行调度,例如:延迟2s开始,5s一次,那么就是2、7、12、17,如果中间由于某种原因导致线程不够用,没有得到调度机会,那么接下来计算的时间会优先计算进去,因为他的排序会被排在前面,有点类似Timer中的:scheduleAtFixedRate方法,只是这里是多线程的,它的方法名也叫:scheduleAtFixedRate,所以这个是比较好记忆的(适合方法3)

3、 多次调度,每次按照上一次实际执行的时间进行计算下一次时间,同上,如果在第7秒没有被得到调度,而是第9s才得到调度,那么计算下一次调度时间就不是12秒,而是9+5=14s,如果再次延迟,就会延迟一个周期以上,也就会出现少调用的情况(适合于方法3);

4、 最后补充execute方法是一次调度,期望被立即调度,时间为空

  1. publicclassScheduledThreadPoolExecutorDemo{
  2. publicstaticvoidmain(String[]args){
  3. ScheduledThreadPoolExecutorscheduledExecutor=newScheduledThreadPoolExecutor(1);
  4. /**
  5. *newtimeTaskForException()要执行的任务线程
  6. *1000:延迟多长时间执行
  7. *2000:每隔多少长时间执行一次
  8. *TimeUnit.MILLISECONDS:时间单位
  9. */
  10. scheduledExecutor.scheduleAtFixedRate(newtimeTaskForException(),1000,2000,TimeUnit.MILLISECONDS);
  11. scheduledExecutor.scheduleAtFixedRate(newtimeTaskForPrintSYSTime(),1000,3000,TimeUnit.MILLISECONDS);
  12. }
  13. staticclasstimeTaskForExceptionimplementsRunnable{
  14. publicvoidrun(){
  15. thrownewRuntimeException();
  16. }
  17. }
  18. staticclasstimeTaskForPrintSYSTimeimplementsRunnable{
  19. publicvoidrun(){
  20. System.out.println(System.nanoTime());
  21. }
  22. }
  23. }

(java多线程与并发)java并发库中的阻塞队列--BlockingQueue

1.阻塞队列的概念

阻塞队列与普通队列的区别在于,当队列是空的时,从队列中获取元素的操作将会被阻塞,或者当队列是满时,往队列里添加元素的操作会被阻塞。试图从空的阻塞队列中获取元素的线程将会被阻塞,直到其他的线程往空的队列插入新的元素。同样,试图往已满的阻塞队列中添加新元素的线程同样也会被阻塞,直到其他的线程使队列重新变得空闲起来,如从队列中移除一个或者多个元素,或者完全清空队列,下图展示了如何通过阻塞队列来合作:


(java多线程并发)控制并发线程数的Semaphore、ScheduledThreadPoolExcutor、BlockingQueue、ReadWriteLock...


线程1往阻塞队列中添加元素,而线程2从阻塞队列中移除元素

从刚才的描述可以看出,发生阻塞起码得满足下面至少一个条件: (前提:队列是有界的)

1.从队列里取元素时,如果队列为空,则代码一直等在这里(即阻塞),直到队列里有东西了,拿到元素了,后面的代码才能继续

2.向队列里放元素时,如果队列满了(即放不下更多元素),则代码也会卡住,直到队列里的东西被取走了(即:有空位可以放新元素了),后面的代码才能继续


2.生产者消费者模型用阻塞队列实现和原来的区别

下面先使用Object.wait()和Object.notify()、非阻塞队列实现生产者-消费者模式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
publicclassTest {
privateintqueueSize =10;
privatePriorityQueue<Integer> queue =newPriorityQueue<Integer>(queueSize);
publicstaticvoidmain(String[] args) {
Test test =newTest();
Producer producer = test.newProducer();
Consumer consumer = test.newConsumer();
producer.start();
consumer.start();
}
classConsumerextendsThread{
@Override
publicvoidrun() {
consume();
}
privatevoidconsume() {
while(true){
synchronized(queue) {
while(queue.size() ==0){
try{
System.out.println("队列空,等待数据");
queue.wait();
}catch(InterruptedException e) {
e.printStackTrace();
queue.notify();
}
}
queue.poll();//每次移走队首元素
queue.notify();
System.out.println("从队列取走一个元素,队列剩余"+queue.size()+"个元素");
}
}
}
}
classProducerextendsThread{
@Override
publicvoidrun() {
produce();
}
privatevoidproduce() {
while(true){
synchronized(queue) {
while(queue.size() == queueSize){
try{
System.out.println("队列满,等待有空余空间");
queue.wait();
}catch(InterruptedException e) {
e.printStackTrace();
queue.notify();
}
}
queue.offer(1);//每次插入一个元素
queue.notify();
System.out.println("向队列取中插入一个元素,队列剩余空间:"+(queueSize-queue.size()));
}
}
}
}
}

  这个是经典的生产者-消费者模式,通过阻塞队列和Object.wait()和Object.notify()实现,wait()和notify()主要用来实现线程间通信。

  具体的线程间通信方式(wait和notify的使用)在后续问章中会讲述到。

  下面是使用阻塞队列实现的生产者-消费者模式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
publicclassTest {
privateintqueueSize =10;
privateArrayBlockingQueue<Integer> queue =newArrayBlockingQueue<Integer>(queueSize);
publicstaticvoidmain(String[] args) {
Test test =newTest();
Producer producer = test.newProducer();
Consumer consumer = test.newConsumer();
producer.start();
consumer.start();
}
classConsumerextendsThread{
@Override
publicvoidrun() {
consume();
}
privatevoidconsume() {
while(true){
try{
queue.take();
System.out.println("从队列取走一个元素,队列剩余"+queue.size()+"个元素");
}catch(InterruptedException e) {
e.printStackTrace();
}
}
}
}
classProducerextendsThread{
@Override
publicvoidrun() {
produce();
}
privatevoidproduce() {
while(true){
try{
queue.put(1);
System.out.println("向队列取中插入一个元素,队列剩余空间:"+(queueSize-queue.size()));
}catch(InterruptedException e) {
e.printStackTrace();
}
}
}
}
}

  有没有发现,使用阻塞队列代码要简单得多,不需要再单独考虑同步和线程间通信的问题。

  在并发编程中,一般推荐使用阻塞队列,这样实现可以尽量地避免程序出现意外的错误。

  阻塞队列使用最经典的场景就是socket客户端数据的读取和解析,读取数据的线程不断将数据放入队列,然后解析线程不断从队列取数据解析。还有其他类似的场景,只要符合生产者-消费者模型的都可以使用阻塞队列。



3.实现原理:

这里只贴几段主要的代码,体会一下思想:

1
2
3
4
5
6
7
8
/** Main lock guarding all access */
finalReentrantLock lock;
/** Condition for waiting takes */
privatefinalCondition notEmpty;
/** Condition for waiting puts */
privatefinalCondition notFull;

这3个变量很重要,ReentrantLock重入锁,notEmpty检查不为空的Condition 以及 notFull用来检查队列未满的Condition

Condition是一个接口,里面有二个重要的方法:
await() : Causes the current thread to wait until it is signalled or interrupted. 即阻塞当前线程,直到被通知(唤醒)或中断

singal(): Wakes up one waiting thread. 唤醒阻塞的线程

再来看put方法:(jdk 1.8)

1
2
3
4
5
6
7
8
9
10
11
12
publicvoidput(E e)throwsInterruptedException {
checkNotNull(e);
finalReentrantLock lock =this.lock;
lock.lockInterruptibly();
try{
while(count == items.length)
notFull.await();
enqueue(e);
}finally{
lock.unlock();
}
}

1.先获取锁

2.然后用while循环检测元素个数是否等于items长度,如果相等,表示队列满了,调用notFull的await()方法阻塞线程

3.否则调用enqueue()方法添加元素

4.最后解锁

1
2
3
4
5
6
7
8
9
10
privatevoidenqueue(E x) {
// assert lock.getHoldCount() == 1;
// assert items[putIndex] == null;
finalObject[] items =this.items;
items[putIndex] = x;
if(++putIndex == items.length)
putIndex =0;
count++;
notEmpty.signal();
}

这是添加元素的代码(jdk 1.8),注意最后一行notEmpty.signal()方法,表示添加完元素后,调用singal()通知等待(从队列中取元素)的线程,队列不空(有值)啦,可以来取东西了。

类似的take()与dequeue()方法则相当于逆过程(注:同样都是jdk 1.8)

1
2
3
4
5
6
7
8
9
10
11
publicE take()throwsInterruptedException {
finalReentrantLock lock =this.lock;
lock.lockInterruptibly();
try{
while(count ==0)
notEmpty.await();
returndequeue();
}finally{
lock.unlock();
}
}

类似的:

1. 先加锁

2. 如果元素个数为空,表示队列已空,调用notEmpty的await()阻塞线程,直接队列里又有新元素加入为止

3. 然后调用dequeue 从队列里删除元素

4. 解锁

dequeue方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
privateE dequeue() {
// assert lock.getHoldCount() == 1;
// assert items[takeIndex] != null;
finalObject[] items =this.items;
@SuppressWarnings("unchecked")
E x = (E) items[takeIndex];
items[takeIndex] =null;
if(++takeIndex == items.length)
takeIndex =0;
count--;
if(itrs !=null)
itrs.elementDequeued();
notFull.signal();
returnx;
}

倒数第2行,元素移除后,调用notFull.singnal唤醒等待(向队列添加元素的)线程,队列有空位了,可以向里面添加元素了。




java中读写锁ReadWriteLock

1.排他锁(互斥锁)的概念:

synchronized,ReentrantLock这些锁都是排他锁,这些锁同一时刻只允许一个线程进行访问。

2.读写锁的概念:

分为读锁和写锁,多个读锁不互斥,读锁和写锁互斥,写锁与写锁互斥。

3.读写锁的好处:

为了提高性能,Java提供了读写锁,在读的地方使用读锁,在写的地方使用写锁,灵活控制,如果没有写锁的情况下,读是无阻塞的,在一定程度上提高了程序的执行效率。

原来使用的互斥锁只能同时间有一个线程在运行,现在的读写锁同一时刻可以多个读锁同时运行,这样的效率比原来的排他锁(互斥锁)效率高

4.读写锁的原理分析:

Java中读写锁有个接口java.util.concurrent.locks.ReadWriteLock,也有具体的实现ReentrantReadWriteLock,

lock方法 是基于CAS 来实现的

(java多线程并发)控制并发线程数的Semaphore、ScheduledThreadPoolExcutor、BlockingQueue、ReadWriteLock...

源码:

  1. publicinterfaceReadWriteLock{
  2. /**
  3. *Returnsthelockusedforreading.
  4. *
  5. *@returnthelockusedforreading.
  6. */
  7. LockreadLock();
  8. /**
  9. *Returnsthelockusedforwriting.
  10. *
  11. *@returnthelockusedforwriting.
  12. */
  13. LockwriteLock();
  14. }

5.案例一:

  1. packageWriteReadLock;
  2. importjava.util.Random;
  3. importjava.util.concurrent.locks.Lock;
  4. importjava.util.concurrent.locks.ReadWriteLock;
  5. importjava.util.concurrent.locks.ReentrantReadWriteLock;
  6. publicclassReadWriterLockTest{
  7. publicstaticvoidmain(String[]args){
  8. finalQueueq3=newQueue();
  9. for(inti=0;i<3;i++)
  10. {
  11. newThread(){
  12. publicvoidrun(){
  13. while(true){
  14. q3.get();
  15. }
  16. }
  17. }.start();
  18. }
  19. for(inti=0;i<3;i++)
  20. {
  21. newThread(){
  22. publicvoidrun(){
  23. while(true){
  24. q3.put(newRandom().nextInt(10000));
  25. }
  26. }
  27. }.start();
  28. }
  29. }
  30. }
  31. classQueue{
  32. //共享数据,只能有一个线程能写该数据,但可以有多个线程同时读该数据。
  33. privateObjectdata=null;
  34. //得到读写锁
  35. ReadWriteLockreadWriteLock=newReentrantReadWriteLock();
  36. /**
  37. *将用于读的get()和写的put()放在同一个类中这样是为了对同一个资源data进行操作,形成互斥
  38. */
  39. /**
  40. *进行读操作
  41. *可以多个读线程同时进入,写线程不能执行
  42. */
  43. publicvoidget(){
  44. //获取读锁,并加锁
  45. LockreadLock=readWriteLock.readLock();
  46. readLock.lock();
  47. try{
  48. System.out.println(Thread.currentThread().getName()+"bereadytoreaddata!");
  49. Thread.sleep((long)(Math.random()*1000));
  50. System.out.println(Thread.currentThread().getName()+"havereaddata:"+data);
  51. }catch(InterruptedExceptione){
  52. e.printStackTrace();
  53. }finally{
  54. //!!!!!!注意:锁的释放一定要在trycatch的finally中,因为如果前面程序出现异常,锁就不能释放了
  55. //释放读锁
  56. readLock.unlock();
  57. }
  58. }
  59. /**
  60. *进行写操作
  61. *只能一个写线程进入,读线程不能执行
  62. */
  63. publicvoidput(Objectdata){
  64. //获取写锁,并加锁
  65. LockwriteLock=readWriteLock.writeLock();
  66. writeLock.lock();
  67. try{
  68. System.out.println(Thread.currentThread().getName()+"bereadytowritedata!");
  69. Thread.sleep((long)(Math.random()*1000));
  70. this.data=data;
  71. System.out.println(Thread.currentThread().getName()+"havewritedata:"+data);
  72. }catch(InterruptedExceptione){
  73. e.printStackTrace();
  74. }finally{
  75. //释放写锁
  76. writeLock.unlock();
  77. }
  78. }
  79. }


没有使用锁之前:读和写交叉在一起

(java多线程并发)控制并发线程数的Semaphore、ScheduledThreadPoolExcutor、BlockingQueue、ReadWriteLock...


在加入读写锁之后:读的过程中,不会有写

(java多线程并发)控制并发线程数的Semaphore、ScheduledThreadPoolExcutor、BlockingQueue、ReadWriteLock...


6.案例二:

  1. packageWriteReadLock;
  2. importjava.util.HashMap;
  3. importjava.util.Map;
  4. importjava.util.Random;
  5. importjava.util.concurrent.locks.Lock;
  6. importjava.util.concurrent.locks.ReadWriteLock;
  7. importjava.util.concurrent.locks.ReentrantReadWriteLock;
  8. publicclassCacheDemo{
  9. //用map来模拟缓存
  10. Map<String,Object>cache=newHashMap<String,Object>();
  11. ReadWriteLockreadWriteLock=newReentrantReadWriteLock();
  12. publicstaticvoidmain(String[]args){
  13. finalCacheDemocacheDemo=newCacheDemo();
  14. for(inti=0;i<6;i++)
  15. {
  16. newThread(){
  17. publicvoidrun(){
  18. while(true){
  19. System.out.println(cacheDemo.getData("key1").toString());
  20. }
  21. }
  22. }.start();
  23. }
  24. }
  25. LockreadLock=readWriteLock.readLock();
  26. LockwriteLock=readWriteLock.writeLock();
  27. //这里必须要用volatie当一个写线程设置value="aaaabbbb",一定要让其他的线程知道vlue的变化,这样就不会被重复写
  28. volatileObjectvalue;
  29. publicObjectgetData(Stringkey){
  30. readLock.lock();
  31. try{
  32. Thread.sleep(300);
  33. System.out.println("read");
  34. value=cache.get(key);
  35. if(value==null){
  36. //这里已经加了读锁,读锁中写是不能允许的,所以要把这个锁释放掉
  37. readLock.unlock();
  38. writeLock.lock();
  39. //防止,当多个写者进程在等待,前面的写进程已经赋值了,value已经不为空了后面的等着的写进程仍然继续赋值
  40. if(value==null){
  41. System.out.println("findnull");
  42. value="aaaabbbb";
  43. cache.put(key,value);
  44. System.out.println("write");
  45. }
  46. writeLock.unlock();
  47. //从新加上读锁
  48. readLock.lock();
  49. }
  50. returnvalue;
  51. }catch(Exceptione){
  52. e.printStackTrace();
  53. }finally{
  54. readLock.unlock();
  55. }
  56. returnnull;
  57. }
  58. }
(java多线程并发)控制并发线程数的Semaphore、ScheduledThreadPoolExcutor、BlockingQueue、ReadWriteLock...