If you're going to face the deep intricacies of language memory models with volatile and such, you may want to go beyond a basic understanding of the rules (as we currently know them at least, given that the Rust memory model isn't fully finalized yet).
The compiler's optimizer can almost freely add, remove, split, merge and reorder non-volatile accesses (including with volatile accesses), if it can adjust the surrounding code accordingly so that the change is not visible to the current thread. The only thing that it's not allowed to do is to add writes, if there is any chance that they could be observed by other threads in the same process (e.g. when the data is accessed via a raw pointer of unknown origin).
What volatile gives you on top of that is that volatile accesses will not be removed by the compiler (nor added, as long as you don't give the compiler permission to add more by using non-atomic accesses or constructing Rust references to the data), and are dispatched to the CPU in the order where they are listed in the code, possibly with some tearing if the volatile accesses are too large for native CPU load/store instructions.
This is why you usually don't want to mix volatile and non-volatile accesses.
There are data wrappers out there which force every access to be volatile, like
VolatileCell. They can be useful when you want to avoid easy mistakes like taking an
&mut T to a "volatile" memory location. Rust references come with very strong invariants, and the mere action of constructing one allows the compiler to do and assume things which are inappropriate in memory-mapped I/O use cases, which are what volatile is about.
If your use case doesn't involve some form of memory-mapped I/O, you likely want atomics, not volatile, as they 1/provide other guarantees (non-tearing, memory barriers...) that are vital for thread synchronization, which are the reason why non-atomic concurrent access to memory is UB; and 2/reduce the degree to which the compiler is forced to pessimize memory accesses by assuming that something somewhere may concurrently and unpredictably access the target memory location.
Volatile is not a superset of atomic and it's a common mistake to use volatile for cross-thread synchronization, which is why @matklad thought it could be worth pointing out.