The entire Spring framework is IMO an elaborate construction built so that engineers could use global variables without their managers finding out. There is little to no difference between carefully using global variables and Spring dependency injection except syntax.
The best solution I have ever seen to global variables is definitely parameterize with Racket (https://docs.racket-lang.org/guide/parameterize.html). I don't think Racket was the first language to come up with this, but it was the first one I am aware of. The basic idea is that you define some global with a default value. However, you can call parameterize to change the value for the duration of some input function. It is made thread safe by using thread local memory. It then resets the parameter back to the default value at the end of the function.
On the other end of the spectrum, I think Rust also has a very good implementation of globals. It will let you use global variables, but you have to declare it as mutable, use some form of locking, or use an UnsafeCell. Additionally, you have to mark your code as unsafe any time you try to read or change this global variable.