Putting a breakpoint in a non reachable thread forces it to run

As I expected, since boolean test is not volatile Thread1 uses local cache value of test and when Thread2 changes it to true Thread1 won't do anything.

Your expectation is incorrect.

According to the Java Language Specification, if one thread updates a non-volatile shared variable and another thread subsequently reads it without appropriate synchronization then the second thread may see the new value, or it may see an earlier value.

So what you are seeing is allowed by the JLS.

In practice, when a debug agent is attached to a JVM, it will typically cause the JIT compiler to recompile some or all methods at a lower optimization level ... or possibly even execute them using the bytecode interpreter. This is likely to happen for methods with breakpoints set in them, and when you are single-stepping1. This may result in different behavior for code that uses shared variables without proper synchronization when you debug it.

This is one of the reasons that debugging problems caused by inadequate synchronization is difficult.

As far as I know breakpoints change the instructions of code by adding a special trap called INT 3. So what's really going on?

That is what happens when you debug C / C++. It is not specified how a JVM handles this, but a typical JVM has other options for implementing breakpoints ... because of bytecodes and JIT compilation.

When I put a sleep(1) in the Thread1 before if statement it will also print the line. Is there a same thing happening by adding a sleep?

The sleep will cause the current thread to be suspended. What happens at the implementation level is not specified. However, it is likely that the native thread mechanisms will flush any outstanding writes (i.e. dirty cache entries) for the suspended thread to memory ... as part of the process of performing a thread context switch.

Similarly, if you use print statements, a typical I/O stack has internal synchronization that can trigger cache flushes, etc. This can also alter the behavior of the code that you are trying to debug.

However, I should stress that these behaviors are not specified.

1 - A JIT optimizer is allowed to reorder assignments provided that this doesn't alter single-threaded behavior. But, if you are debugging a method and observing that values of variables, the effects of the reordering are visible (to the programmer). De-optimizing / interpreting avoids this. Fortunately, a modern JVM / debug agent can do this "on the fly" as required.

Warning: this answer is based mostly on how .Net debuggers work but I expect similar behavior between two runtimes. I expect JVM to allow per-method re-JIT-ing at run time as it already can replace method with HotSpot JIT.

There is some existing articles and posts about what optimizations are turned off for debugging like AMD: perf when debugging enabled, Side Effects of running the JVM in debug mode, Will Java app slow down by presence of -Xdebug or only when stepping through code?. They hint that at least when there is an exception code takes significantly different code path under debugger which may be how breakpoints are implemented.

Many debuggers turn off optimizations (compile time if you allow to recompile code and JIT time if you debugging existing code) when you debug the code. In .Net world impact is global - when debugger is attached it can switch all future JIT compilations to non-optimized path, I expect Java/JVM to support more granular control to allow de-optimizing only methods that may need to stop in debugger. This is done to allow you to clearly see all values of all variables. Otherwise half of the information sometimes including method calls and local/member variables is not available.

"uses local cache value of test" is optimization (likely at JIT time) - so when you start debugging the code (or enable some sort of step-through with breakpoints) it will switch off optimization and read from memory every time thus essentially making variable close to volatile (still not necessary to behave that way all the time but close).

Depending on debugger you use you may disable such behavior (but debugging will be much harder).