This is safe as long as you only invoke f once. There is a happens-before relationship between a thread A that mutates data and a thread B that's started from thread A (the HB-relationship being at Thread.start). Since nobody mutates data after D is started, this is safe.
Some ways to break the thread safety:
- mutate
i again, including by invoking foo again
- read
i from a thread other than D or the one that invoked foo
The reason that you can't mutate i again, even from the thread that invoked foo, is that this mutation would have happened after d.start(), and would therefore have no HB edge against that second mutation.
The reason you can't read i from any arbitrary thread is that this thread wouldn't have a well-defined view of the i++ mutation.
It can get a tad more subtle than that, but at a high level, there you go.