The thing to know here is that automatic variables have a lifetime based on an activation scope. They are created and initialized when entering that scope, and disappear when leaving that scope. The key thing to know is the a scope remains active until reaching the procedural end of that scope, or after all child process have completed, whichever comes last.
Cases 2 and 3 are essentially the same. The activation scope for k is the begin/end block inside the fork/join_none. There are actually 3 activations of k, one for each process forked off. None of those processes activate until after the parent initial process reaches the #0 delay, which is after the for-loop has reached its final value j==3. The only difference with case 3 is you initialize each of the k’s to 3 as in case 2, but then the first statement of the process is to assign it to 3 again. The variable k disappears when each process terminates.
In cases 1 and 4, the activation scope for k is entering the fork/join_none. As with cases 2 and 3, there will be 3 activations of k, but this time the activation starts immediately with each iteration of the for-loop. So k gets initialized with the current value of j; 0, 1, and 2. The lifetime of each k continues until the process spawn by the fork/join_none terminates.
The problem with case 4 is there are two processes spawned by each activation of the fork/join_none: the assignment k = j, and the $write(k). The assignment to each k will take the final value of j==3 since the process does not start until the initial process reaches the #0 delay. But now you have a race condition with the process with the $write(k) statement. If the write executes first, you will see the same results as case 1: the values 1, 2, and 3. But if the assignment statement executes first, k gets the final value of j==3, and you get the same results as case 3.