Dev.to · 12 min read

How I climbed Java's concurrency staircase, one frustration at a time

How I climbed Java's concurrency staircase, one frustration at a time

I bought my first Java book when I was 23. I wasn't trying to "study concurrency," or "learn threads," or check any box. I wanted to make a nonogram — a paint-by-numbers logic puzzle, the kind where number clues along the rows and columns tell you which cells to fill in to reveal a hidden picture. What I'll add up front is the pace. I'd just quit my previous job, so I had nothing but time, and I spent almost all of it studying — every single day. It came to about six months, though I never set that as a target; it's simply when I started wondering whether I could find work in Java. Progress was embarrassingly slow. I'll say this plainly because it matters: nothing came quickly for me. What I had was time, daily repetition, and something I wanted to build. Looking back, that's the whole story. Everything I know about concurrency, I learned because something I was building stopped working, and the only way forward happened to be the next concept up. I never read the threading chapter because it was next in the book. I read it because I wanted a stop button. This is a description of that staircase — the one Java quietly built for me, where each step was pulled into existence by a problem on the step below it. I think the staircase matters more than any single step. So let me walk up it the way I actually climbed it. Step 1: The nonogram that started everything The first version was small. A main method, a GUI, a grid. The user clicks a cell to fill it in or clear it, the state changes. When the grid is complete, a bit of logic checks whether the filled cells match the clues. Nothing here is hard. It's all single-threaded, top to bottom: an event comes in, I change some state, I redraw. There is exactly one thing happening at a time, and I never once had to think about who else might be touching my data, because nobody else was. This was the safe ground floor, though I didn't know to call it that yet. Step 2: Teaching it to solve itself Then I got ambitious. I wanted the program to solve the puzzle on its own. So I wrote a solver — backtracking, the usual thing — and then I spent an embarrassing amount of time optimizing it. Pruning, ordering, little tricks to cut the search space. This was my first real taste of the fact that my own logic is the thing I trust least. I'd be sure the solver was fast, run it, and watch it crawl. But it was still all on the ground floor. One thread, doing one long calculation. And that turned out to be the problem. Step 3: I wanted a stop button The solver was slow on hard boards. And here's the thing that pushed me up the first step: while it was grinding away, the whole window froze. I couldn't click anything. I couldn't cancel. I just had to sit there and watch it think. I wanted a stop button. That's it. That was the entire motivation. Not "I should learn multithreading." Just: I want to be able to click Cancel while it's running. I want to underline this, because it's the pattern that repeats the whole way up. I never went looking for the next concept. The next concept came looking for me, wearing the costume of a small, concrete annoyance. Step 4: Entering threads, because I had no choice To have a stop button, I needed the solver to not be the only thing running. The UI had to stay alive to receive my click while the solver was busy. There was no way around it: I had to learn threads. So I learned the smallest amount I needed. The solver moved into a new Thread. Inside its loop, on each iteration, it checked a volatile boolean stop flag. The UI thread — the one that was now free, because the heavy work had moved off it — could flip that flag when I clicked Cancel. The solver would see it on its next pass and bail out. // roughly what I wrote, 23 years old and delighted volatile boolean stopRequested = false; new Thread(new Runnable() { public void run() { while (!solved && !stopRequested) { stepTheSolver(); } } }).start(); It worked. The window stayed responsive. I could cancel. I felt like a wizard. Step 5: What that actually taught me I would not have said, then, that I "understood threads." I didn't. There were whole categories of things I had no idea about — that the UI events were themselves running on a thread, that there were rules about touching UI components from background threads, all of it invisible to me. But I learned one real thing, and it was the right first thing: if you make another thread, something can happen in parallel, and one can interrupt the other. The heavy work and the responsiveness no longer had to take turns. That single idea — that "at the same time" is now possible — is the doorway into the entire rest of the staircase. And notice how I walked through that doorway. I had to write new Thread. It was an explicit, visible act. There was a clear moment where I crossed from "one thing at a time" into "more than one thing at a time," and because I had to write it down, I knew I'd crossed it. Hold onto that. It's going to matter at the top of the stairs. An aside: the day Java became a friend Somewhere in those six months, after a long stretch of being simply slow — copying examples, half-understanding them, forgetting, trying again — one thing finally clicked: instances. The idea of an object as a thing that exists, that more than one name can point to, that gets shared. It just settled into place one day. I couldn't tell you what triggered it. But after that, Java felt like a friend instead of a stranger. I mention it here, right before the servlet story, because that click is what made the next step possible. When the member variable bit me, I recognized it almost instantly — oh, this instance is being shared — precisely because instances had finally become real to me. The slow months bought me that recognition. Step 6: The member variable that bit me Some time later I bought a book on servlets. I wanted to make things on the web. I did the incantation everyone does — extends HttpServlet, implement doGet — without really understanding what the container was doing underneath. I was building a little web chat — and I should say why, because it's close to the whole reason any of this happened. Back then, the chat rooms everyone hung out in were written in Perl. I'd spent countless hours in them. I wanted, badly, to build one of my own. I wanted to show the logged-in user's name on the page. So I did the obvious-looking thing: I stashed it in a field on the servlet. A member variable. It worked perfectly when I tested it alone. Then more than one person used it at the same time, and the names got crossed. My name showed up on someone else's page. The state was a mess. Here's what I want to be precise about, because it's a thing people still get wrong: a servlet is, by default, a single instance. The container makes one of your servlet objects and runs every request through it. So every request — every thread — is calling doGet on the same instance, sharing the same fields. A member variable being static was never the requirement. An ordinary instance field is already shared across every concurrent request, because the instance itself is shared. (This hasn't changed, by the way. Even today, a Spring @Controller is a singleton by default. Same trap, newer paint.) I figured this out slowly — first a vague hunch that the member variable was the problem, that maybe these doGet calls were stepping on each other. But because my grasp of the language itself was decent by then, the vague hunch hardened into a rule almost immediately. Don't put per-request state on a shared instance. And that's the step where the abstraction climbed. I stopped thinking about the specific bug and started thinking about the principle underneath it: who is this instance shared with? What is its scope? That question — not the patch — is what I carried up to the next step. Step 7: The database, and the things you only learn in production After that, the pace of new things to learn went vertical. A real database — MySQL. What a PreparedStatement is and why string-concatenating SQL is a way to get yourself hurt. Resource management — closing what you open, every time, even when something throws. And one thing I want to call out specifically: connection pools. The importance of a connection pool is, in my experience, something you genuinely cannot learn from a personal project. You learn it in production, when real load arrives and you discover that opening a fresh connection per request falls over. Some lessons are only available on the job — this is one of them. The conditions that teach it, real load and real traffic, simply aren't there in a side project. Underneath all of it, though, the same shape from Step 6 kept showing up: something is shared; who's allowed to touch it, and when? A connection. A pooled resource. The questions rhymed. Step 8: The batch that wouldn't finish — and the anticlimax Then I hit a wall that sent me to the last step I'll describe here. I had a batch job, and it would not finish in reasonable time. It was running on the main thread — which meant it was using exactly one CPU. One core, on a machine that had several, all the rest sitting idle while my one core sweated. This was the Java 1.3 days, before ExecutorService existed, so there was no thread pool to reach for. I built something that barely deserves the name: I'd start a fixed batch of threads — sixteen or so — and each time one finished, the main thread would slot a fresh one into its place. No shared queue, no synchronized blocks. Just the main thread, by hand, keeping a fixed number of threads busy. And here's what surprised me. After all the dread — multithreading had this reputation as the scary, advanced thing — what I actually wrote was almost disappointingly simple. A fixed array of threads, refilled from the main thread as each one finished. That was it. The anticlimax was the lesson: the thing I'd been intimidated by, once I'd climbed the steps below it, was small. I watched the batch server's cores light up — all of them, finally working — and I was completely satisfied. I'd taken a single-core crawl and spread it across the machine, with code I understood line by line, because I'd written every line. The staircase, from where I am now From where I stand now, looking back, it's one continuous staircase, and every step was carved by the step below it: The nonogram solver was slow, so I wanted a stop button. The stop button needed threads, so I learned new Thread. Threads taught me "at the same time" is possible. The servlet member variable got crossed, so I learned scope — who shares this instance. Scope and resources led into the database, pools, resource management. The batch wouldn't finish on one core, so I wrote a thread pool by hand. But the deeper thing — the thing I only saw much later — is that the whole climb was one principle, asked over and over at higher and higher altitude: Who is pointing at this thing? Who shares it? It starts even earlier than threads, actually. The very first version of this question is the one every beginner hits: "Wait — why did this value change? Who changed it?" — and the answer is the method you passed the object into. That's references. Two names pointing at one object. Once you understand that, you can understand "two requests pointing at one servlet instance" (Step 6), and then "two threads pointing at one piece of shared memory" (the data race). References → scope → concurrency. It's the same question, climbing. Each stumble didn't teach me a patch. It taught me the next altitude of that one question. That's what I mean by a staircase: the frustrations were load-bearing. If you're just starting out Looking back, there's something I wish someone had told me. I can still hold threads in my hands today. Not because I'm clever about concurrency — I'd never claim to have fully mastered it. It's because I started with a nonogram stop button at 23, and climbed one frustrated step at a time, and never skipped one. And here's the part I most want you to keep. I am not a gifted programmer. I wasn't fast, and most of what I learned came from repetition rather than flashes of insight. People with real talent for this exist, and I don't try to compete with them on raw ability. What I had was six months, the willingness to show up every day, and the refusal to skip steps. That's the whole secret, and it isn't one. Which means you can do it too — almost certainly faster than I did. You probably have more going for you than I did. Just don't skip the steps. There's one last thing underneath all of it, though — the fuel. I could show up every day for six months because there was something I genuinely wanted to make: my own version of those Perl chat rooms. It was a long, winding detour to get there. But I don't think the discipline works without it. Without something you actually want to build, you won't keep going. So that's the real prerequisite, the one beneath all the others: find a thing you want badly enough to climb a whole staircase for. The frustrations were the staircase. I just had to keep walking up. Built with Claude (Opus).

This is a summary aggregated from Dev.to. Read the complete article on the original site:

Read full article at Dev.to

More AI & Machine Learning News