Why we using volatile? Because sometimes we have to make sure that (1) the variable is loaded from memory each time it is accessed instead of copying from registers, or (2) do the math on run time. Otherwise, the result is not what we expected because of the optimization done by compilers.
Basically embedded engineers know volatile better than software engineers, because they have to deal with hardware register, interrupt, etc. As a embedded engineers, I often use volatile for (1) pointers which point to hardware peripheral registers and (2) variables referenced in interrupt service routine.
One day one of my colleagues told me that I should use volatile for shared variables in multi-threaded program as well. I was confused and wondered why I have not used volatile in the multi-threaded code before.
But I knew he is right and I decided to take this code as a practice.
If we compile the code with -O3, then the thread2 will enter a infinite loop, because i is loaded from memory only once in the beginning.
Adding volatile can solve this problem. It become more clearer by reading assembly code.