1. 锁介绍

  java中锁是个很重要的概念,当然这里的前提是你会涉及并发编程。

  除了语言提供的锁关键字 synchronized和volatile之外,jdk还有其他多种实用的锁。

  不过这些锁大多都是基于AQS队列同步器。ReadWriteLock 读写锁就是其中一个。

  读写锁的含义是,将读锁与写锁分开对待,读锁可以任意个一起读,因为读并不涉及数据变更,而遇到写锁后,所有后续的读写都将被阻塞。这特性有什么用呢?比如我们有一个缓存,我们可以用它来提高访问速度,但是当数据变更时,怎样能保证能读到准确的数据?

  在没有读写锁之前,我们可以使用wait/notify机制,我们可以以写锁作为一个同步介质,当写锁被占用时,读只能等待,写操作完成后,通知所有读继续。这看起来不那么好实现!

  当有了读写锁后,我们就不需要这么麻烦了,只需要读操作使用读锁,写操作获取写锁操作。大家可能会想,既然都要获取锁,那和其他锁有什么差别呢,一般看到锁咱们都会想到串行,阻塞。但其实读写锁不是这样的。看起来你是每次都获取读锁,但其实单纯的读锁并不会阻塞线程,所以同样是并行无阻,读锁只有在一种情况下会阻塞,那就是写锁被某线程占用时。因为写锁被占用则意味着,数据可能马上发生变化,如果此再允许读操作任意进行的话,多半可能读到写了一半或者是老数据,而这简直太糟了。而写锁则只每次都会真正进行后续操作的阻塞动作,使写操作保证强一致性。

  好了,以上就是咱们从概念上来理解读写锁。

  而实际上呢?ReadWriteLock只是一个接口,而其实现则可能是n多的。我们就以jdk实现的 ReentrantReadWriteLock 为契机,看一下读写锁的实现吧。

  在介绍 ReetrantReadWriteLock 之前,我们要先简单说下 ReentrantLock 重入锁,从字面意思理解,就是可重新进入的锁。那么,到底是什么意思呢?我们想一下,如果我们有2个资源锁可用,那么,如果我在本线程上上锁两次,是不是资源就没有了呢,那第三次进行锁获取的时候,是不是就把自己给锁死了呢?想想应该是这样的,但是为啥平时咱们都遇不到这种情况呢?原因就在于可重入性。可重入的意思是说,如果当前线程进行多次加锁操作,那么无论如何它自己都是可以进入的。简单从实现来说就是,锁会排除当前线程,从而避免自身阻塞。这些需求看起来很理所当然,但是咱们自己实现的时候可能会因为场景不一样,从而不一定需要这种特性呢。syncronized也是一种重入锁。好了,说了这么多,还是没有看到 ReetrantLock是怎么实现的!

用个不恰当的图描绘下:(该锁是读写分离的,读多于写的场景能够在保证线程安全的同时提供尽可能大的并发能力)

2. 简单没获取

我们来看下源码就一目了然了。

        /**
* Fair version of tryAcquire
*/
protected final boolean tryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
if (!hasQueuedPredecessors() &&
compareAndSetState(0, acquires)) {
// 第一次进入获取到锁后,标记获得锁的线程,后续判定重入
setExclusiveOwnerThread(current);
return true;
}
}
// 重入锁判定,否则失败
else if (current == getExclusiveOwnerThread()) {
// 最多可重入 int 次
int nextc = c + acquires;
if (nextc < 0)
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}
}

  重入锁介绍完后,咱们可以安心的来说说 ReentrantReadWriteLock了。该读写锁也是一种可重入锁。它要实现的特性就是,读读锁无阻塞,写锁必阻塞(包括写读锁/写写锁),读写锁阻塞(需等待读锁释放后才能获取写锁从而保证无脏读)。

  从上面可以看出,读和写是两个锁,但是他们的状态却是互相关联的,那怎样设计其数据结构呢?用两个变量去推导往往不太可行,因为其本身就是锁,如果再用两个变量去判定锁状态,那么又如何保证变量自身的可靠性呢?ReentrantReadWriteLock 是通过一个状态变量来控制的,具体为 高16位保存读锁状态,低16位保存写锁状态,而在改变状态时,使用cas保证写入的可靠性。(其实这里可以看出,锁个数不应该超过16位即65536个,这种锁数量已经完全被忽略掉了)。有了数据结构,咱们再看下怎么控制读写互联。读锁的获取,写锁没被占用时,即低位为0时,高位大于0即可代表获取了读锁,所以,读锁是n个可用的。而写锁的获取,则要依赖高低位判定了,高位大于0,即代表还有读锁存在,不能进入,如果高位为0,也不一定可进入,低位不为0则代表有写锁在占用,所以只有高低位都为0时,写锁才可用。

  下面,来看下读写锁的具体实现!

3. 来个例子先:

public class ReadWriteLockTest {

    private ReentrantReadWriteLock reentrantReadWriteLock = new ReentrantReadWriteLock();
/**
* 读锁
*/
private Lock r = reentrantReadWriteLock.readLock(); /**
* 写锁
*/
private Lock w = reentrantReadWriteLock.writeLock(); /**
* 执行线程池
*/
private ExecutorService executorService = Executors.newCachedThreadPool(); @Test
public void testReadLock() {
for (int i = 0; i < 10; i++) {
Thread readWorker = new ReadWorker();
executorService.submit(readWorker);
}
waitForExecutorFinish();
} @Test
public void testWriteLock() {
for (int i = 0; i < 10; i++) {
Thread writeWorker = new WriteWorker();
executorService.submit(writeWorker);
}
waitForExecutorFinish();
} @Test
public void testReadWriteLock() {
for (int i = 0; i < 10; i++) {
Thread readWorker = new ReadWorker();
Thread writeWorker = new WriteWorker();
executorService.submit(readWorker);
executorService.submit(writeWorker);
}
waitForExecutorFinish();
} /**
* 线程模拟完成后,关闭线程池
*/
private void waitForExecutorFinish() {
executorService.shutdown();
try {
executorService.awaitTermination(100, TimeUnit.SECONDS);
} catch (InterruptedException e) {
e.printStackTrace();
}
} private final class ReadWorker extends Thread {
@Override
public void run() {
r.lock();
try {
SleepUtils.second(1);
System.out.println(System.currentTimeMillis() + ": " + Thread.currentThread().getName() + " reading...");
SleepUtils.second(1);
}
finally {
r.unlock();
}
}
} private final class WriteWorker extends Thread {
@Override
public void run() {
w.lock();
try {
SleepUtils.second(1);
System.out.println(System.currentTimeMillis() + ": " + Thread.currentThread().getName() + " writing...");
SleepUtils.second(1);
}
finally {
w.unlock();
}
}
} }

  可以看到 testReadLock(), 无阻塞,立即完成10个读任务!

  而 testWriteLock(),则是全部阻塞执行,20秒完成串行10个任务!

  而 testReadWriteLock(), 则是 读锁与写锁交替执行,在执行写锁时,所有锁等待,在执行读锁时,可能存在多个锁同时运行!执行结果样例如下:

: pool--thread- reading...
: pool--thread- writing...
: pool--thread- writing...
: pool--thread- writing...
: pool--thread- writing...
: pool--thread- writing...
: pool--thread- reading...
: pool--thread- reading...
: pool--thread- writing...
: pool--thread- writing...
: pool--thread- reading...
: pool--thread- reading...
: pool--thread- writing...
: pool--thread- writing...
: pool--thread- writing...
: pool--thread- reading...
: pool--thread- reading...
: pool--thread- reading...
: pool--thread- reading...
: pool--thread- reading...

  ok, 现象已经展示了,是时候透过现象看本质了!

4. 读锁的获取过程 r.lock(), 其实现为 ReadLock!

        public void lock() {
// 调用 AQS 的 acquireShared() 方法,进行统一调度
sync.acquireShared(1);
}
// AQS 获取共享读锁
public final void acquireShared(int arg) {
// 调用 ReentrantReadWriteLock.Sync.tryAcquireShared(), 定义锁获取方式
if (tryAcquireShared(arg) < 0)
doAcquireShared(arg);
} // 获取读锁,unused 传参未使用,直接使用内置的高位加1方式处理
protected final int tryAcquireShared(int unused) {
/*
* Walkthrough:
* 1. If write lock held by another thread, fail.
* 2. Otherwise, this thread is eligible for
* lock wrt state, so ask if it should block
* because of queue policy. If not, try
* to grant by CASing state and updating count.
* Note that step does not check for reentrant
* acquires, which is postponed to full version
* to avoid having to check hold count in
* the more typical non-reentrant case.
* 3. If step 2 fails either because thread
* apparently not eligible or CAS fails or count
* saturated, chain to version with full retry loop.
*/
Thread current = Thread.currentThread();
int c = getState();
// 写锁使用中,则直接获取失败
if (exclusiveCount(c) != 0 &&
getExclusiveOwnerThread() != current)
return -1;
int r = sharedCount(c);
// 读锁任意获取,除了超过最大限制
if (!readerShouldBlock() &&
r < MAX_COUNT &&
compareAndSetState(c, c + SHARED_UNIT)) {
if (r == 0) {
firstReader = current;
firstReaderHoldCount = 1;
} else if (firstReader == current) {
firstReaderHoldCount++;
} else {
HoldCounter rh = cachedHoldCounter;
if (rh == null || rh.tid != getThreadId(current))
cachedHoldCounter = rh = readHolds.get();
else if (rh.count == 0)
readHolds.set(rh);
rh.count++;
}
return 1;
}
// 对读锁阻塞情况,进行处理
return fullTryAcquireShared(current);
} // 获取低位数,即写锁状态值
static int exclusiveCount(int c) {
return c & EXCLUSIVE_MASK;
}
// 获取高位数,即读锁状态值
static int sharedCount(int c) {
return c >>> SHARED_SHIFT;
} /**
* Full version of acquire for reads, that handles CAS misses
* and reentrant reads not dealt with in tryAcquireShared.
*/
final int fullTryAcquireShared(Thread current) {
/*
* This code is in part redundant with that in
* tryAcquireShared but is simpler overall by not
* complicating tryAcquireShared with interactions between
* retries and lazily reading hold counts.
*/
HoldCounter rh = null;
for (;;) {
int c = getState();
if (exclusiveCount(c) != 0) {
if (getExclusiveOwnerThread() != current)
return -1;
// else we hold the exclusive lock; blocking here
// would cause deadlock.
} else if (readerShouldBlock()) {
// Make sure we're not acquiring read lock reentrantly
if (firstReader == current) {
// assert firstReaderHoldCount > 0;
} else {
if (rh == null) {
rh = cachedHoldCounter;
if (rh == null || rh.tid != getThreadId(current)) {
rh = readHolds.get();
if (rh.count == 0)
readHolds.remove();
}
}
if (rh.count == 0)
return -1;
}
}
if (sharedCount(c) == MAX_COUNT)
throw new Error("Maximum lock count exceeded");
// 验证通过,cas更新锁状态,使用 SHARED_UNIT 进行高位加1
if (compareAndSetState(c, c + SHARED_UNIT)) {
if (sharedCount(c) == 0) {
firstReader = current;
firstReaderHoldCount = 1;
} else if (firstReader == current) {
firstReaderHoldCount++;
} else {
if (rh == null)
rh = cachedHoldCounter;
if (rh == null || rh.tid != getThreadId(current))
rh = readHolds.get();
else if (rh.count == 0)
readHolds.set(rh);
rh.count++;
cachedHoldCounter = rh; // cache for release
}
return 1;
}
}
}

  以上是获取读锁的过程,其实际控制很简单,只是多了很多的状态统计,所以看起来复杂!

5. 下面,来看写锁的获取过程,WriteLock.lock()

        public void lock() {
// AQS获取独占锁
sync.acquire(1);
} // AQS 锁调度
public final void acquire(int arg) {
// 如果获取锁失败,则加入到等待队列中
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
} // ReentrantReadWriteLock.Sync.tryAcquire(), 写锁获取过程
protected final boolean tryAcquire(int acquires) {
/*
* Walkthrough:
* 1. If read count nonzero or write count nonzero
* and owner is a different thread, fail.
* 2. If count would saturate, fail. (This can only
* happen if count is already nonzero.)
* 3. Otherwise, this thread is eligible for lock if
* it is either a reentrant acquire or
* queue policy allows it. If so, update state
* and set owner.
*/
Thread current = Thread.currentThread();
int c = getState();
int w = exclusiveCount(c);
// 如果是0,则说明不存在读写锁,直接成功
// 否则分有读锁和有写锁两种情况判断
if (c != 0) {
// (Note: if c != 0 and w == 0 then shared count != 0)
// 存在读锁,或者不是当前线程(重入),则直接失败
if (w == 0 || current != getExclusiveOwnerThread())
return false;
if (w + exclusiveCount(acquires) > MAX_COUNT)
throw new Error("Maximum lock count exceeded");
// Reentrant acquire
setState(c + acquires);
return true;
}
// cas 更新 state
if (writerShouldBlock() ||
!compareAndSetState(c, c + acquires))
return false;
setExclusiveOwnerThread(current);
return true;
} /**
* Creates and enqueues node for current thread and given mode.
*
* @param mode Node.EXCLUSIVE for exclusive, Node.SHARED for shared
* @return the new node
*/
private Node addWaiter(Node mode) {
Node node = new Node(Thread.currentThread(), mode);
// Try the fast path of enq; backup to full enq on failure
Node pred = tail;
if (pred != null) {
node.prev = pred;
if (compareAndSetTail(pred, node)) {
pred.next = node;
return node;
}
}
enq(node);
return node;
} // AQS 的锁入队列操,从队列中进行锁获取,如果获取失败,则产线一个中断标志
final boolean acquireQueued(final Node node, int arg) {
boolean failed = true;
try {
boolean interrupted = false;
for (;;) {
final Node p = node.predecessor();
// 这里是公平锁的实现方式,只会从队列头获取锁
if (p == head && tryAcquire(arg)) {
setHead(node);
p.next = null; // help GC
failed = false;
return interrupted;
}
// 阻塞判定,响应中断
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}

  ok, 读写锁的获取已经完成,再来看一下释放的过程!

5. 读锁的释放 ReadLock.unlock()

        public void unlock() {
// AQS 的释放控制
sync.releaseShared(1);
} // AQS 释放锁
public final boolean releaseShared(int arg) {
if (tryReleaseShared(arg)) {
doReleaseShared();
return true;
}
return false;
}
// ReentrantReadWriteLock.Sync.tryReleaseShared() 自定义释放
protected final boolean tryReleaseShared(int unused) {
Thread current = Thread.currentThread();
if (firstReader == current) {
// assert firstReaderHoldCount > 0;
if (firstReaderHoldCount == 1)
firstReader = null;
else
firstReaderHoldCount--;
} else {
HoldCounter rh = cachedHoldCounter;
if (rh == null || rh.tid != getThreadId(current))
rh = readHolds.get();
int count = rh.count;
if (count <= 1) {
readHolds.remove();
if (count <= 0)
throw unmatchedUnlockException();
}
--rh.count;
}
for (;;) {
int c = getState();
int nextc = c - SHARED_UNIT;
// cas更新状态,每次减1,直到为0,锁才算真正释放
if (compareAndSetState(c, nextc))
// Releasing the read lock has no effect on readers,
// but it may allow waiting writers to proceed if
// both read and write locks are now free.
return nextc == 0;
}
} /**
* Release action for shared mode -- signals successor and ensures
* propagation. (Note: For exclusive mode, release just amounts
* to calling unparkSuccessor of head if it needs signal.)
*/
private void doReleaseShared() {
/*
* Ensure that a release propagates, even if there are other
* in-progress acquires/releases. This proceeds in the usual
* way of trying to unparkSuccessor of head if it needs
* signal. But if it does not, status is set to PROPAGATE to
* ensure that upon release, propagation continues.
* Additionally, we must loop in case a new node is added
* while we are doing this. Also, unlike other uses of
* unparkSuccessor, we need to know if CAS to reset status
* fails, if so rechecking.
*/
for (;;) {
Node h = head;
if (h != null && h != tail) {
int ws = h.waitStatus;
if (ws == Node.SIGNAL) {
if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))
continue; // loop to recheck cases
unparkSuccessor(h);
}
else if (ws == 0 &&
!compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
continue; // loop on failed CAS
}
if (h == head) // loop if head changed
break;
}
}

6. 读锁的释放, WriteLock.unlock()

        public void unlock() {
// AQS 释放控制
sync.release(1);
}
// AQS
public final boolean release(int arg) {
if (tryRelease(arg)) {
Node h = head;
// 释放锁
if (h != null && h.waitStatus != 0)
unparkSuccessor(h);
return true;
}
return false;
}
// Sync.tryRelease()
protected final boolean tryRelease(int releases) {
if (!isHeldExclusively())
throw new IllegalMonitorStateException();
int nextc = getState() - releases;
// 如果写锁状态为0,则意味着当前线程完全释放锁,将 owner 线各设置为null
boolean free = exclusiveCount(nextc) == 0;
if (free)
setExclusiveOwnerThread(null);
setState(nextc);
return free;
} /**
* Wakes up node's successor, if one exists.
*
* @param node the node
*/
private void unparkSuccessor(Node node) {
/*
* If status is negative (i.e., possibly needing signal) try
* to clear in anticipation of signalling. It is OK if this
* fails or if status is changed by waiting thread.
*/
int ws = node.waitStatus;
if (ws < 0)
compareAndSetWaitStatus(node, ws, 0); /*
* Thread to unpark is held in successor, which is normally
* just the next node. But if cancelled or apparently null,
* traverse backwards from tail to find the actual
* non-cancelled successor.
*/
Node s = node.next;
if (s == null || s.waitStatus > 0) {
s = null;
for (Node t = tail; t != null && t != node; t = t.prev)
if (t.waitStatus <= 0)
s = t;
}
// 调用 LockSupport 释放锁
if (s != null)
LockSupport.unpark(s.thread);
}

7. 锁降级

锁降级是指把写锁降级为读锁。如果当前线程,然后将其释放,最后再获取读锁,最后再获取读锁,这种不能称为锁降级!

锁降级是指把持住当前的写锁,再获取到读锁,随后释放写锁的过程;

锁降级的两个重要问题:

1. 为什么拥有了写锁,还要去再获取读锁?

2. 既然已经被写锁占有了,还能获取读锁吗?

回答完上面两个问题,才算真正明白锁降级的意义所在!

1. 再次想获取读锁的目的在于,读和写模块是分开的,而更新操作则可能在读的时候触发的。比如在读的时候发现数据过期了,这时就要调用写操作,而此时读锁又不能释放,所以需要在安全的情况下,释放和重新获取读锁;

2. 在写锁已经被获取的情况下,当前线程的读锁是可重入的,所以读锁对当前线程是开放的。而且,当前线程重新获取读锁后,其他线程的写锁将会被延迟获取,从而更高效地保证了当前线程的运行效率;

  综上,读写锁的简要解析就算完成了。 其主要使用 AQS 的基础组件,进行锁调度! 使用CAS进行状态的安全设置! 而锁的阻塞,则是使用 LockSupport 工具组件进行实际阻塞!

ReentrantReadWriteLock 读写锁解析的更多相关文章

  1. Java并发包源码学习系列:ReentrantReadWriteLock读写锁解析

    目录 ReadWriteLock读写锁概述 读写锁案例 ReentrantReadWriteLock架构总览 Sync重要字段及内部类表示 写锁的获取 void lock() boolean writ ...

  2. ReentrantReadWriteLock读写锁的使用

    Lock比传统线程模型中的synchronized方式更加面向对象,与生活中的锁类似,锁本身也应该是一个对象.两个线程执行的代码片段要实现同步互斥的效果,它们必须用同一个Lock对象. 读写锁:分为读 ...

  3. ReentrantReadWriteLock读写锁的使用2

    本文可作为传智播客<张孝祥-Java多线程与并发库高级应用>的学习笔记. 这一节我们做一个缓存系统. 在读本节前 请先阅读 ReentrantReadWriteLock读写锁的使用1 第一 ...

  4. 锁对象-Lock: 同步问题更完美的处理方式 (ReentrantReadWriteLock读写锁的使用/源码分析)

    Lock是java.util.concurrent.locks包下的接口,Lock 实现提供了比使用synchronized 方法和语句可获得的更广泛的锁定操作,它能以更优雅的方式处理线程同步问题,我 ...

  5. ReentrantReadWriteLock读写锁简单原理案例证明

    ReentrantReadWriteLock存在原因? 我们知道List的实现类ArrayList,LinkedList都是非线程安全的,Vector类通过用synchronized修饰方法保证了Li ...

  6. ReentrantReadWriteLock读写锁详解

    一.读写锁简介 现实中有这样一种场景:对共享资源有读和写的操作,且写操作没有读操作那么频繁.在没有写操作的时候,多个线程同时读一个资源没有任何问题,所以应该允许多个线程同时读取共享资源:但是如果一个线 ...

  7. java多线程:并发包中ReentrantReadWriteLock读写锁的锁降级模板

    写锁降级为读锁,但读锁不可升级或降级为写锁. 锁降级是为了让当前线程感知到数据的变化. //读写锁 private ReentrantReadWriteLock lock=new ReentrantR ...

  8. java中ReentrantReadWriteLock读写锁的使用

    Lock比传统线程模型中的synchronized方式更加面向对象,与生活中的锁类似,锁本身也应该是一个对象.两个线程执行的代码片段要实现同步互斥的效果,它们必须用同一个Lock对象. 读写锁:分为读 ...

  9. java多线程:ReentrantReadWriteLock读写锁使用

    Lock比传统的线程模型synchronized更多的面向对象的方式.锁和生活似,应该是一个对象.两个线程运行的代码片段要实现同步相互排斥的效果.它们必须用同一个Lock对象. 读写锁:分为读锁和写锁 ...

随机推荐

  1. 利用mybatis generator实现数据库之间的表同步

    项目背景: 项目需要对两个服务器上的表进行同步,表的结构可能不一样.比如服务器A上的表i同步数据到服务器B上的表j,i和j的结构可能不一样,当然大部分字段是一样的.项目看起来很简单,网上一搜也是很多, ...

  2. python 12 模块与包

    一.不知道什么原则 python文件下面只写方法,所有的可直接执行的代码,都放在条件下 原因.该文件有可能被其他文件调用 二.eval() 将字符串转为相应的数据格式可以想象json转为map 三.模 ...

  3. Winform 利用 Oracle.ManagedDataAccess访问Oracle数据库

    Winform 利用 Oracle.ManagedDataAccess访问Oracle数据库时出现以下错误: Message = "每个配置文件中只允许存在一个 <configSect ...

  4. MySQL远程连接问题解决方法

    问题:Host 'XXX' is not allowed to connect to this MySQL server. 原因分析: 1.登录到mysql: 在开始目录下管理员身份运行[MySQL ...

  5. list常用方法

    1.切片: ①.顾头不顾尾,从头开始取,但不包括最后一个. ②.从左向右数为正,从零开始,从右开始为负,从-1开始 如: names=['1','2','3'] ames[-1]与names[2]效果 ...

  6. APP界面设计 大概总结

    APP界面设计大概总结 首先,你得有个Android Studio 其次,你得学会有耐心的对它 最后,要适应它习惯它了解它 来看看APP的基本步骤 先有资源 再是界面布局 下来承载布局Activity ...

  7. final,finally,finalize

    final:可以修饰属性,可以修饰方法(方法不能被重写,可以继承),可以修饰类(该类不能被继承,不能产生子类) finally:无论什么情况,都会执行 finalize:垃圾回收时,调用此方法

  8. Java————迷宫问题

    它表示一个迷宫,其中的1表示墙壁,0表示可以走的路,只能横着走或竖着走,不能斜着走,要求编程序找出从左上角到右下角的最短路线. package algorithm_java; import java. ...

  9. 记录mysql安装过程遇到问题

    1. 远程连接授权 登陆mysql数据库    (如果安装在系统盘可以直接命令, 否则要切换到安装目录..bin/) mysql -u root -p mysql> use mysql;   - ...

  10. 82、iOS 基本算法

    “冒泡排序.选择排序.快速排序.归并排序.逆序.二分查找.求两个整数的最大公约数和最小公倍数.” 一.冒泡排序 1.比较相邻的元素.如果第一个比第二个大,就交换他们两个. 2.对每一对相邻元素作同样的 ...