The issue has nothing to do with thread safety, really. There’s a simple, straightforward answer to why instance variables can always be captured: this is always effectively final. That is, there is always one known fixed object at the time of the creation of a lambda accessing an instance variable. Remember that an instance variable named foo is always effectively equivalent to this.foo.
So
class MyClass {
private int foo;
public void doThingWithLambda() {
doThing(() -> { System.out.println(foo); })
}
}
can have the lambda rewritten as doThing(() -> System.out.println(this.foo); }) and is therefore equivalent to
class MyClass {
private int foo;
public void doThingWithLambda() {
final MyClass me = this;
doThing(() -> { System.out.println(me.foo); })
}
}
…except this is already final and doesn’t need to be copied to another local variable (though the lambda will capture the reference).
All of the normal thread-safety caveats apply, of course. If your lambdas get passed to multiple threads and modify variables, then exactly the same things would happen if lambdas weren’t used, and no extra thread-safety applies beyond the thread safety of your variables (e.g. if they are volatile) or if your lambdas use other mechanisms to safely access the variables. Lambdas do nothing special about thread-safety at all, and they don’t do anything special with instance variables, either; they just capture a reference to this instead of to the instance variable.