void main(String [] args){
Thread addOne = new AddOneThread();
Thread addTwo = new AddTwoThread();
addOne.start(); addTwo.start();
}
}
----------------------
Listing 1.1
----------------------
public class AddOneThread extends Thread {
@Override
public void run(){
Main.value += 1;
System.out.println(Main.value + " from AddOneThread")
}
}
----------------------
Listing 1.2
----------------------
public class AddTwoThread extends Thread {
@Override
public void run(){
Main.value += 2;
System.out.println(Main.value + " from AddTwoThread")
}
}
----------------------
Listing 1.3
I created two classes (Listings 1.2 -1.3) , that extend Theard class, the classes mentioned above can access and modify concurrently a static variable of the Main class named value, via their run methods, I was expecting the following outputs:
1 from AddOne Thread
3 from AddTwo Thread
But every time I run this code I also get these different outputs:
3 from AddOne Thread
3 from AddTwo Thread
3 from AddTwo Thread
3 from AddOne Thread
3 from AddTwo Thread
1 from AddOne Thread
Can anyone help me understand what's actually happening? and how did the later outputs came to be?
Why do you expect exactly that output? I mean, it feels that you expect some specific order and timing, and what is it that you expect?
I expected those threads to behave sequentially(one at a time), at some point, and when they did I got the expected outputs. But when they didn't act sequentially, but instead concurrently(which is the idea behind threads), I got the other confusing results. So I wanted to know how threads actually executed?, but I kinda figure it out, threads are executed on bytecode instruction level(or maybe lower than that), so it's impossible for theadrs to not interfere with each other, without proper management of Threads, the results can be tragic, this can be observed for the outputs, the value of variable value, is literally over the place, so it can reference one value or all values simultaneously, this shit is quantum.
> I expected those threads to behave sequentially(one at a time) If that was the case for threads, then what would be the point of using threads at all? If that was the case, you could simply write it this way to achieve the same result: class Main { static int value = 0; public static void main(String[] args) { Main.value += 1; Main.value += 2; } } This code will produce the result you want without any threads. If threads were behaving as you expect, why would people invent them at all?)) You could just write simple single-threaded code as above to achieve your goal. > when they did I got the expected outputs What's expected for you, is not what's expected by design. And it's normal, these things are quite hard to grasp. Read more about threads and how they work. I usually use the following analogy that makes things much easier to understand for newcomers. Imagine that CPU cores are independent people who can do the work with their hands. And there's the manager (OS scheduler) who decides which work those people should do at any specific moment. And there are many different goals (analogy for thread) that need to be done, where detailed goal steps are described in method "run". I specifically don't use word "task" here, which is better from grammatical and semantic standpoint, to avoid confusion with the term "task" used everywhere in programming. That's a different thing. That's a very real world analogy. Think of yourself, you're a person with single pair of hands who can do only one thing at a time. But you're doing many many goals during the day (sounds ugly, but I don't use "task" intentionally). And most always, you don't do them sequentially. For example, you want to make some tea. You fill it with water first. While you was about to turn it on, your door bell rang. You decide to put away the kettle and go open the door. You switched the current goal, while you still can continue the "tea goal" later. It's your friend came to visit you, you invited him, and then went back and turned on the kettle. While it's heating, you go talk to your friend. And so on. You can do only one thing at a time, but you switch to complete multiple goals faster. Similar thing happens with CPUs, threads and OS scheduler. The only difference that is a little bit hard to grasp, is that in the above example you're both the worker (CPU) and the one who decides what to do (scheduler). But imagine that it's not you who decides what to do, but rather a separate abstract entity - "the manager" (OS scheduler). "The manager" can stop you at any point in time and say "stop doing this, and switch to doing that". And the key point to understand is that all the cicrumstances (current active goals and hardware behavior) are unpredictable. And this manager is fair but unpredictable too. The same way as you can't have two perfectly identical days in your life, you can't have two perfectly identical runs of the same program. For example there are ~1000 threads (different "goals") running on my machine right now. Now, specifically to your code. The results you observe are because the timings of threads (goals) are unpredictable. Code for those goals has 2 (actually much more) steps where the manager can stop you and tell to switch to another goal: Main.value += 2; // --- person can be stopped here pirntln(Main.value + " .."); Note that at the step 2 you're making another "read" operation - Main.value + .... At step 2 you may already have been interrupted, and manager told you to switch to another goal, where you executed Main.value += 1;. And then you returned back.
Your analogy is clear and honestly fun, the one key concept I was missing is the "Scheduler", of course threads can't perform "goals" on their own, there got be some of administrative body, to tell them what to do, and when to do it, it's pretty much like a Conductor, the music sheet tells the musicians what to do, and the Conductor (on a basic level) makes sure that everybody is playing on the same tempo and time. Well I can't say I fully understand threads after this, there still much much more to dig, but at least, I can now say I see the bigger picture, so once again.. Thank you very much @dburyak
You're welcome. The key part of the analogy is that a human can SWITCH between goals. At any point of the goal. Hence, one human (one cpu core) can do many goals (many threads) "simultaneously" by switching between them - do a little bit of this, then do that, then go back to first thing and continue, and so on. Yeah, scheduler is important, but the key part is this switching. If you understand this switching and it's unpredictability, that will be the major first step in understanding concurrency. In context of your question it's important that scheduler is the main source of unpredictability: when, in what sequence, and by which human (if you have multi-core CPU) each instruction in the code will be executed. You can try simple exercise. "Execute" your code yourself. Imagine that you're the CPU, and you execute your code. You have 3 threads (3 goals) - main one, thread-1 and thread-2. And try to switch between goals in different timings. Pick a pencil and paper, and try to visualize how you execute it, note what value is stored. Also one important thing that I now realize I forgot to mention. Note that "someThread.start()" doesn't run that thread right away. It only creates underlying thread on the OS level, and signals scheduler that "it's ready to do the job, include it to your schedule". When exactly it will start executing the code is totally unpredictable. Practically impossible, but in theory it can start executing the code even 10s later after you called "thread.start()".
"Also one important thing.." So the unpredictability of the Scheduler creates a vast space of possibilities, where it is difficult to observe when a thread is actually going to start, execute, pause, retrieve and finish, and the only thing we can do is to let them be. But at the same time we use threads because they get goals done by letting them run simultaneously, while utilizing the capabilities of modern cpus, thus optimization in general, and to achieve this executing threads has to be unpredictable. I think to understand fully Threads, one must dive in the theoretical part, get their hand dirty with tge technical part, and strengthen thier knowledge with experimental work, so I find pencil and paper approach very convenient, and will definitely solidify my understanding about concurrency.
Yep, correct. One tiny clarification about this > while utilizing the capabilities of modern cpus Multithreading was designed long long time ago with "old" cpus. Multithreading is not only for modern multi-core CPUs. Even with a single-core cpu you will achieve "simultaneous" progress of tasks. Just like many tasks can be done "simultaneously" by a single human if he/she will switch often after doing small portion of each task. There still will be simultaneous progress. Yes, you picked the very nicely fitting word simultaneous. Multithreading is for OS-es, not the CPUs actually. If you try to write code for microcontrollers for "bare metal", where there is no OS at all, you won't face any unpredictability. There are no threads, kernel, scheduler and all that. Only your code which gets executed directly starting from the first instruction by the microcontroller when you turn its power on. There is no unpredictability because there are no threads and concurrency. (in simplest cases of course, you can write your own scheduler and organize your code for microcontroller in form of tasks, but even then there will be no same problems as you have with threads in OSes)
Oh man.., wish I focused harder on OS courses 😂, the whole time I thought CPUs have part in this concurrent juggle rather than just executing, but thanks to you, I recall now, that it's the OS who's responsible of scheduling and allocating, and the CPUs just execute.
Yep, exactly. Also CPUs caches data. In 3 layers. Which is a source of huuge performance increase, but also is a source of a lot of troubles in concurrent programming. This is a more advanced topic, but let me lift the veil a little bit. If you remove those sysout statements from your code (they are source of non-obvious side-effects) and put them somewhere else, and run it on multi-core CPU, because of that caching, threads may see data like this: AddOne: 0 -> 1 AddTwo: 0 -> 2 main: 0 I mean, regardless of changes to the value, it's not visible to other cpus because updated value is stored in specific cpu cache only. The analogy with humans is very suitable here. Think of a situation when you start cooking something and you reach out to fridge for eggs because the last time you checked the fridge there were some. But now you open it and there are none. Turns out that your brother/roommate/whoever just ate the last ones. What you think about the fridge state is "your cache". But you've just used all the milk, and your brother still thinks that there is milk in the fridge. It's his "cache". While in the fridge itself (main memory) there are neither eggs, nor milk. It's not perfect analogy but very close one.
Hmm Interesting.., I think it would be so neat, if there is a mechanism to notify me and my brother (CPUs)if any changes occurred in the fridge(Memory), that way we both can be constantly updated, but I anticipate such mechanism will come with a cost.
Обсуждают сегодня