skip to content

What are SelectionKey interest ops, and how do interest sets and ready sets differ?

level: middleimportance: should knowfreq 48%

answer

  1. Four ops: ACCEPT, CONNECT, READ, WRITE (powers of two, OR-combinable)
  2. Interest = what you subscribed to; ready = what's happening now
  3. ready set is a subset of interest set
  4. isReadable/isWritable/isAcceptable/isConnectable test the ready set
  5. Add OP_WRITE only when a write was partial; remove when drained

basics

~20 s

Interest 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 s

When 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
java
// 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

for a junior

Names the four interest ops and knows they say what events to watch a channel for.

for a middle

Distinguishes interest set from ready set, uses isReadable()/isWritable() correctly, and changes interest with interestOps().

for a senior

Explains the OP_WRITE-spin trap and demand-driven write interest, uses attachments for per-connection state, and handles cancelled keys.

for a principal

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

context