What are SelectionKey interest ops, and how do interest sets and ready sets differ?
answer
- Four ops: ACCEPT, CONNECT, READ, WRITE (powers of two, OR-combinable)
- Interest = what you subscribed to; ready = what's happening now
- ready set is a subset of interest set
- isReadable/isWritable/isAcceptable/isConnectable test the ready set
- Add OP_WRITE only when a write was partial; remove when drained
basics
~20 sInterest ops (OP_ACCEPT, OP_CONNECT, OP_READ, OP_WRITE) say which events you want a channel watched for. The interest set is what you asked for; the ready set is which of those are actually ready right now after select().
solid answer
~40 sWhen you register a channel you get a SelectionKey carrying an interest set — a bitmask of the four ops you care about: OP_ACCEPT (server has a pending connection), OP_CONNECT (client handshake finished), OP_READ (bytes to read), OP_WRITE (room to write). After select(), each key also has a ready set (key.readyOps()) — the subset of interest ops the channel is currently ready for; helpers like isReadable()/isWritable()/isAcceptable()/isConnectable() test it. You can change interest dynamically with key.interestOps(...). A common pattern: keep OP_READ as the steady interest, and only add OP_WRITE when you have buffered data that didn't fully write, then remove OP_WRITE once drained — otherwise the channel is almost always writable and select() would spin returning OP_WRITE continuously.
code
java · 9 lines// Demand-driven write interest
int written = channel.write(pending);
if (pending.hasRemaining()) {
// partial write: ask to be told when writable
key.interestOps(key.interestOps() | SelectionKey.OP_WRITE);
} else {
// fully drained: stop watching for writability
key.interestOps(key.interestOps() & ~SelectionKey.OP_WRITE);
}go deeper
Names the four interest ops and knows they say what events to watch a channel for.
Distinguishes interest set from ready set, uses isReadable()/isWritable() correctly, and changes interest with interestOps().
Explains the OP_WRITE-spin trap and demand-driven write interest, uses attachments for per-connection state, and handles cancelled keys.
Designs the read/write interest state machine for back-pressure and fairness, and reasons about edge- vs level-triggered semantics versus the underlying epoll/kqueue.
## Recap: the SelectionKey When you call `channel.register(selector, ops)`, you get back a **`SelectionKey`** — the object that ties one channel to one selector. It holds two bitmasks of operations: - the **interest set** — what you *want* to be notified about, and - the **ready set** — what the channel is *currently ready* for. Both are expressed using four integer flag constants on `SelectionKey`. ## The four interest ops | Constant | Meaning | Valid on | |---|---|---| | `OP_ACCEPT` | a new incoming client connection is waiting to be accepted | `ServerSocketChannel` | | `OP_CONNECT` | a non-blocking `connect()` has completed its handshake | client `SocketChannel` | | `OP_READ` | there are bytes available to read | `SocketChannel`, `DatagramChannel` | | `OP_WRITE` | the channel can accept more bytes (send buffer has room) | `SocketChannel`, `DatagramChannel` | They are powers of two, so you combine them with bitwise OR: `OP_READ | OP_WRITE`. A channel may only declare interest in ops it actually supports — a `ServerSocketChannel` supports only `OP_ACCEPT`. ## Interest set vs ready set - **Interest set** (`key.interestOps()`): the standing subscription. You set it at registration and can change it later with `key.interestOps(newOps)`. - **Ready set** (`key.readyOps()`): after `select()` returns, this is the subset of the interest set the channel is *actually* ready for *right now*. You typically don't read the bitmask directly; you use the convenience predicates: - `key.isAcceptable()` → ready set contains `OP_ACCEPT` - `key.isConnectable()` → contains `OP_CONNECT` - `key.isReadable()` → contains `OP_READ` - `key.isWritable()` → contains `OP_WRITE` Think of the interest set as *"tell me about these"* and the ready set as *"these are happening now."* The ready set is always a subset of the interest set. ## The OP_WRITE gotcha (very common interview point) A connected socket is **writable almost all the time** — its send buffer usually has room. So if you leave `OP_WRITE` permanently in the interest set, `select()` will return that key on nearly every cycle even when you have nothing to send, burning CPU in a tight loop. The correct pattern is **demand-driven write interest**: 1. Normally register interest in just `OP_READ`. 2. Try to `write()` directly when you have data. If `write()` does a **partial write** (returns fewer bytes than the buffer held), there's leftover data the socket couldn't take. 3. Only *then* add `OP_WRITE` (`key.interestOps(key.interestOps() | OP_WRITE)`) so the loop wakes you when the socket can take more. 4. When the buffer is fully drained, **remove** `OP_WRITE` again (`key.interestOps(key.interestOps() & ~OP_WRITE)`). `OP_READ` does not have this problem because a socket is only readable when bytes have actually arrived. ## Attachments A key can carry an **attachment** — any object via `key.attach(obj)` / `key.attachment()`. This is where you store per-connection state: the partially read message, the pending write buffer, a protocol decoder, etc. It's how the single event-loop thread keeps each connection's context straight. ## Cancelling `key.cancel()` removes the registration; the key becomes invalid (`key.isValid()` returns false) and is purged on the next `select()`. Closing a channel cancels all its keys automatically. Calling almost any method on an invalid key throws `CancelledKeyException`.
- Why is registering permanent OP_WRITE interest usually a bug?A socket is writable almost all the time, so select() returns it on nearly every cycle, spinning the CPU. You should only enable OP_WRITE when you have unsent buffered data and disable it once drained.
- Where do you store the per-connection buffer/state when one thread handles many channels?In the SelectionKey's attachment via key.attach(state) / key.attachment().
saying these in an interview costs you the question
- Leaving OP_WRITE permanently in the interest set -> busy spin
- Confusing interestOps() with readyOps()
- Declaring OP_ACCEPT on a SocketChannel or OP_READ on a ServerSocketChannel
- Using a key after cancel()/close() -> CancelledKeyException
- Forgetting attachments as the place to keep per-connection state