Race Conditions in Java Multithreading

Video Statistics and Information

Video
Captions Word Cloud
Reddit Comments
Captions
race conditions is a situation where two or more threads access the same variables or data in a way where the final result stored in the variable depends on how threat access to the variables is scheduled race conditions occur when two or more threads read and write the same variables or dates are concurrently and the threads access the variables using either of these patterns check then act read modify write and where the modified value depends on the previously read value and the thread access to the variables or data is not atomic i will explain this in more detail throughout the rest of this video i will start with an example of the read modify write problem and i have prepared an example and let me just resize the editor here so you can see what's going on in this example i create a single counter here which is shared by two different runnables i create two different runnables by calling git runnable and as you can see the counter is passed as parameter to each of them and here's just a text for printing out some results and then the threads here are started um so yeah one counter two threads with two runnables sharing the counter and then this threads start and what you can see happening inside the runnable here is that each runnable which then means each thread will loop for 1 million iterations through this for loop and call this ink and get method here on the counter and then print out the final value that the counter has counted to before i run this example to show you the result let me just show you the implementation of the counter as you can see it has a single private long member variable here initialized to the varia to the value 0 and then you can see the ink and get method here simply increments the count value the count member variable here and then returns the value of the count member variable at that time and as you can see the get method simply returns the value of the count variable if we quickly jump back to the example here that runs the um the counters here that calls the counters here then if we have two threads sharing a counter and each thread counts to one million and increments the counter by one for each iteration here or iterates one million times here then that means you have two threads each iterating one million times which should result in two million iterations in total and thus when the threats print out the final value down here at least one of the threats should have counted to the value 2 million right that is what we expect the first threat to finish will of course not have counted to 2 million because after all one of the threats will finish before the other but the second third we would have expected would have reached 2 million however that is not what happens let's just run the example here and see what actually comes out as you can see the first thread um counts to 1 million and the second threat here has only reached the count of 181.87 million and let's try to run this again and you will see different numbers again there you see they got to the same number and if we run it again you see the numbers vary now let me explain to you why that is to explain that let's have a look at this diagram that i have here so what we expect to happen is that the first thread here will read the value of the counter the value stored inside the counter class here or the counter object into a local cpu register where the thread is running increment the value by one and then write that value back to main memory so that when the second thread here reads the count it reads the value one it by 1 and then writes the value 2 back to main memory however that is not exactly what happens sometimes what might be happening instead instead of sequential axis as shown here is interleaved access which means thread 1 here reads the value 0 from the counter into a cpu register and then thread 2 at the same time reads the value or just just right after it doesn't really matter but it reads the value zero from the counter as well into a local cpu register for thread two now both threads here increment that value that it has stored in its own cpu register and writes it back so thread one will increment from zero to one and write one back thread two will also increment from zero to one and write one back as well now two threads have actually incremented the counter and so we expect the value to be two but it is actually only one because of this interleaved access to the counter here and this is an example of two threads first reading then modifying and writing back the value here and the same here this thread reads modifies and writes back the value to main memory and another condition is also fulfilled which has to be fulfilled it is that the second value here the modified value depends on the previous value so that the new value count here is the old value plus one and it's the same over here right if the value had not depended on whatever the old value was this would have been not a problem but i will get back to that later on this section of code here where the problem happens where the race condition happens is called a critical section and it is a critical section because ready because race conditions can occur within this section here right as you can see first we read the value of the count variable here into a local cpu register this is invisible for you but this happens underneath the surface inside the java virtual machine then the new value is set as the old value plus one so the new value depends on the old value and then we write it back to count again this looks like one instruction here but it is actually multiple instructions inside the virtual machine so this here is a critical section here the way to fix a critical section is to make the critical section atomic and by atomic means that only one thread can execute within the critical section at a given time once you make a critical section atomic you get sequential access to the critical section and then you will force the behavior in the counter that you see here so first one thread gets access to the count value reach it into a cpu register increments it writes it back and then the second thread then can then get access to the count value the variables value read it into the cpu register increment it and write it back in this particular case one way to solve the problem here make this critical section atomic is to simply wrap it in a synchronized block and that means that only one thread can execute within this block here on the same counter synchronized instance at the same time and so now you will not cannot have a situation in which two threads read the value of count and then increment that red value first whatever thread gets in here will read the value incremented and write it back as one atomic operation let me just show you how the behavior of this program looks when we are using a counter synchronized instead of a counter everything else is pretty much the same thing right all the code looks the same but now increment and get is called by two different threads but the the method is now um synchronized or the code in here is wrapped inside of a synchronized block so as you can see the first thread finishes a little bit before the second thread but the second thread now counts to 2 million and it does that even if we run it again run it again you can see it's not the same thread that that is the last thread but whatever thread is last always finishes by you know with a count to 2 million now remember i told you that it is only if multiple threads are both reading and writing the same variable that race conditions occur and let me just show you an example where only one thread is writing to the variable but and and only one thread is reading from it so one thread reads it one thread writes it so as you can see here i have a normal counter here it's not synchronized and i have two threads here and they are being um initialized with different runnables okay so the first runnable here the incrementing runnable is the one that calls ink and get so this will cause the ingan get a method to be called 1 million times and then here i have another runnable here which [Music] only iterates five times sleeps for one millisecond and then it prints out the value found inside the counter um and as you will see it's not super visible but as you will see here let's try to run this example as you can see the thread that counts to 1 million reaches to count that and then the first time here you can see thread 2 doesn't reach the account value before the first third has reached 1 million but then as you can see here it actually the following times has reached 1 million and we can try to run it again and you can see that there is not really any race condition here and the reason is that in a race condition the problem here is that the two threads may overwrite each other's updates or they may not actually see each other's updates and all the the the scheduling of the instructions here will cause the um the two threads to miss each other's updates and that is not the case here because there's only one thread making any updates so the thread the second thread which is only reading the value well it sees all the updates from the other thread no updates are missed well in theory in theory it is actually possible since we're using a non-synchronized counter it is actually possible that the second thread could miss um could miss some values from um that were written to the counter because we have no guarantee about when this value here this incremented value is written back to main memory so that it is visible for the thread that that reads it this is known as a visibility problem so if we want to be absolutely sure then we would have to declare these variables either volatile or you would have to access it from within a synchronized block and i will not get into more detail about all of this i have covered that in extensive detail in my videos about the java synchronized keyword the java volatile keyword java memory model and the java happens before guarantee but just keep in mind that for race conditions to occur there must be at least two threads which update which right to the same variable otherwise you do not have a race condition you may still have a visibility problem but you do not have a race condition let me also just show you a final example here or another example here where i start two threads and each thread is given a runnable and the runnable gets two counters and so the runnable will increment the first counter and only print the value of the second counter right so you can see here it gets counter a and counter b and it will increment the first counter here for 1 million times and then after that it will print out the values of counter a and counter b along with the name of the runnable and i've called the thread 1 and thread 2. now you can see that i have switched the counters here so that thread one will update we'll increment counter one and read counter two and thread two will increment counter two and read counter one now in this case we also do not have a race condition because thread a here the first thread here thread one is the only thread that is writing to counter one and thread two is the only thread writing to counter two so you do not actually have a race condition even if you actually have two threads that are accessing these two counters but the read and write pattern of the two threads is different and therefore we are not talking about a race condition let me just try to run this example and see what we get out here as you can see as we expected both thread 1 and 2 reach one million here because we do not have a race condition they are not overwriting each other's increments right each thread gets to increment one of the counters from zero to one million and you can see again and again and again now again because i'm still using the unsynchronized counter here and inside the counter class i have not declared this variable here volatile um you could actually in in an application that was long running you could risk that once in a while one of the increments was not visible until until at some later point where the latest value would be synchronized to main memory so what you might see is a thread reading stale values but you will never lose an update sooner or later the li the latest correct count value will be written to main memory and then thread two will see it right the other thread will see it you do not lose updates as you do in interleaved access as i have illustrated here right so we also do not have um race conditions in in this case here before i wrap up this video i want to show you a an example of a race condition that is caused by the check then acts behavioral pattern and if you look at the code example here on the left you can see that i have created here a shared map it is a concurrent hash map so this uh concurrent hazmat is hashmap is capable of having multiple threads access it at the same time without the hashmap becoming internally inconsistent then i create two threads here each with a runnable the same runnable actually and not the same instance but the same implementation of the runnable interface and then they get access to as yet to the shared map then the threads are started now let's have a look at what happens down here each of the two runnables that are created will loop 1 million times and for each of these 1 million times it will check if the shared map contains the key key if it does actually contain the key the map it will the thread will remove that value from the map and if that value is null then it will print out that for iteration i the value for the key was null if the map does not contain the key then the thread will insert that key and thus sooner or later there will be some values inserted into the key into the map for the given key here now on the surface it looks like that when we have just checked if the map contains the key then we cannot then after remove the key just after remove the key and then get the value null back especially not when we never insert a null value into the map but it is actually still possible when we have two threads running this runnable here this runnable code against the same shared map and let me just explain to you how that is possible if both of the threads execute the if statement at the same time at a time where the map actually contains the key value pair here so it contains a key with the with the value key and the second thread here executes the exact same statement at the same time then both of these if statements will evaluate to true and thus the instruction here map remove key will be executed in both threads that is this instruction here however the concurrent has map will not allow both threads to remove the key value pair only one of them gets to remove it and the other one gets a null value back and that is why once in a while the value might actually be null because even though we have just determined that the um the map contains the key before this thread gets to execute this statement here the other thread has removed this key here so in between this x instruction and this instruction the key is removed by the other thread and thus we can have the situation here where the value is null now let's try to run this example and see what we get out and as you can see we do get quite a few iterations where the value is not it's not every iteration you can see sometimes there's a bit of a gap between the threads as well but sometimes it happens quite a lot as you can see but it is not consistent it is not every single time that the value is null you can see here it jumps from 29 to 36 before it reaches this situation again this thread the solution to this problem here is of course to make the critical section which is this check here check then act to make this check to make this check atomic and we can do that simply by wrapping it inside of a of a synchronized block here and we synchronize on the shared map it has to be the same object that the two runnables are synchronizing on otherwise the synchronization will not block the other thread and the only shared object these two threads have is the shared map here so now only one um threat can execute the check here with the if remove else insert check then act behavior but since it is not the whole loop that is made synchronized it means that both threads can still make progress both can both threads can still iterate through the loop but only one of them at a time can enter into this block here this critical section which is now atomic now let's run this example again and we should not get any um you can see now from the output we never have a situation where um a thread first checks that the map contains the key and then afterwards removes a key a key removes the key value pair and the value is null for that key that no longer happens anymore because there can no longer be another thread that removes the key in between this instruction and this instruction that's all for this video about race conditions in java remember to check out the description below the video for links to related videos about topics such as java volatile java synchronized etc etc and if you liked the video hit the like button and if you want to see more videos like this subscribe to my channel
Info
Channel: Jakob Jenkov
Views: 9,692
Rating: 4.964179 out of 5
Keywords: Race Conditions, Java Multithreading, Java Concurrency, Java
Id: RMR75VzYoos
Channel Id: undefined
Length: 22min 38sec (1358 seconds)
Published: Tue Oct 27 2020
Related Videos
Note
Please note that this website is currently a work in progress! Lots of interesting data and statistics to come.