За да разберете мотивацията зад обхватните стойности, трябва да имате разбиране за Виртуални нишки и Структурирана паралелност.

Мотивация

В по-ранните дни на Java, когато беше необходимо да се споделят данни в целия код, изпълняван като част от една нишка, щеше да се използва екземпляр „ThreadLocal“. Локалните променливи на нишките бяха ефективно решение по това време, тъй като броят на нишките беше ограничен и стилът на програмиране изискваше променливи данни. С локалните променливи на нишката човек може да осъществява достъп и да променя информацията в цялата верига на извикване на нишка, ако е необходимо.

Въпреки това, въвеждането на VirtualThread значително промени този пейзаж. Сега разработчиците вече не са ограничени от броя на нишките, които могат да създадат, позволявайки потенциално милиони нишки. Въпреки че променливите ThreadLocal все още могат да се използват за достъп до данни по време на изпълнение на нишка и дори да споделят тези данни с дъщерни нишки, създадени от тези нишки чрез InheritableThreadLocal, може да има значителни разходи за производителност, когато броят на дъщерните нишки е голям, както е разрешено от Виртуални нишки.

Недостатъците на използването на променливи ThreadLocal с виртуални нишки се простират отвъд проблемите с производителността. Променливите ThreadLocal са по своята същност променливи, което означава, че всеки код, изпълняван като част от нишката, може да променя данните, съхранявани от променлива ThreadLocal. Тази променливост може да направи кода по-труден за разбиране и отстраняване на грешки.

Въведение

Стойност с обхват (JEP-429) е стойност, която е зададена веднъж и след това е достъпна за четене за ограничен период на изпълнение от нишка. ScopedValue позволява безопасно и ефективно споделяне на данни за ограничен период на изпълнение, без да предава данните като аргументи на метода.

Подвързване

Помислете за този пример:

private static final ScopedValue<String> USER = ScopedValue.newInstance();

ScopedValue.where(USER, "Tim Nadella", () -> doSomething());

Кодът, изпълнен в doSomething(), който извиква USER.get(), ще прочете стойността „Tim Nadella“. Стойността с обхват е обвързана, докато се изпълнява doSomething() и става необвързана, когато doSomething()завърши (нормално или с изключение).

ScopedValue дефинира метода where(ScopedValue, Object, Runnable) за задаване на стойността на ScopedValue за ограничения период на изпълнение. Изпълнението на методите, изпълнявани от Runnable, определя динамичен обхват. Стойността с обхват е обвързана, докато се изпълнява в динамичния обхват, тя се връща към необвързана, когато методът за изпълнение завърши (нормално или с изключение). Кодът, изпълняван в динамичния обхват, използва метода ScopedValue get, за да прочете своята стойност.

Помислете за този пример

public static ScopedValue<String> scopedValue = ScopedValue.newInstance();

public static void main(String[] args) {
  ScopedValueTest instance = new ScopedValueTest();
  ScopedValue.where(scopedValue, "Tim Nadella", () -> {
    System.out.println("Value is: " + scopedValue.get());
    instance.doSomething();
  });
}

public void doSomething() {
  System.out.println("Doing something while accessing scoped value: " + scopedValue.get());
  doSomethingAgain();
}

public void doSomethingAgain() {
  System.out.println("Doing something again while accessing scoped value: " + scopedValue.get());
}

Резултатът от горния пример е

Value is: Tim Nadella
Doing something while accessing scoped value: Tim Nadella
Doing something again while accessing scoped value: Tim Nadella

В допълнение към метода where, който изпълнява метод за изпълнение, ScopedValue дефинира метода where(ScopedValue, Object, Callable) за изпълнение на метод, който връща резултат.

Наследяване със StructuredTaskScope

ScopedValue става още по-мощна функция, когато се използва с „StructuredTaskScope“.

ScopedValue поддържа споделяне на данни между нишки. Това споделяне е ограничено до структурирани случаи, при които дъщерни нишки се стартират и завършват в рамките на ограничения период на изпълнение от родителска нишка. По-конкретно, когато се използва StructuredTaskScope, свързванията на стойности с обхват се улавят при създаване на StructuredTaskScope и се наследяват от всички нишки, стартирани в този обхват с метода fork.

Помислете за този пример

interface WeatherService {
  
  String getWeather();
}

class SunnyWeatherService implements WeatherService {
  
  @Override
  public String getWeather() {
    if (WeatherServiceTest.location.isBound()) { //Check whether the value is available
      return "Weather for " + WeatherServiceTest.location.get() + " Sunny";
    }
    throw new RuntimeException("Location not specified");
  }
}

class CloudyWeatherService implements WeatherService {

  @Override
  public String getWeather() {
    if (WeatherServiceTest.location.isBound()) { //Check whether the value is available
      return "Weather for " + WeatherServiceTest.location.get() + " Cloudy";
    }
    throw new RuntimeException("Location not specified");
  }
}

class RainyWeatherService implements WeatherService {

  @Override
  public String getWeather() {
    if (WeatherServiceTest.location.isBound()) { //Check whether the value is available
      return "Weather for " + WeatherServiceTest.location.get() + " Rainy";
    }
    throw new RuntimeException("Location not specified");
  }
}

public class WeatherServiceTest {

  public static ScopedValue<String> location = ScopedValue.newInstance();

  public static void main(String[] args) {
    ScopedValue.where(location, "New York", WeatherServiceTest::getWeather);
  }

  public static void getWeather() {
    try (var scope = new StructuredTaskScope.ShutdownOnSuccess<String>()) {
      Future<String> res1 = scope.fork(() -> new SunnyWeatherService().getWeather());
      Future<String> res2 = scope.fork(() -> new CloudyWeatherService().getWeather());
      Future<String> res3 = scope.fork(() -> new RainyWeatherService().getWeather());
      scope.join();
      System.out.println(scope.result());
    } catch (Exception e) {
      throw new RuntimeException(e);
    }
  }

}

В горния пример дефинирахме WeatherService с три реализации, всяка от реализациите на метеорологичната услуга очаква местоположението да присъства в ScopedValue.

Сега, да кажем, че за да увеличим производителността и надеждността на нашето приложение, решихме да използваме и трите реализации на WeatherService и да се върнем веднага щом някоя от тях върне времето. Това е перфектен случай за използване на StructuredConcurrency, където искаме да третираме едновременните операции като едно цяло.

С помощта на ShutdownOnSuccessна StructuredTaskScope ние стартираме нов обхват и извикваме всички реализации на метеорологичната услуга едновременно с метода fork, който вътрешно използва VirtualThreads. Всички операции, разклонени от StructuredTaskScopeвече имат достъп до ScopedValueкоято е зададена от родителския контекст.

Като най-добра практика винаги трябва да проверяваме дали ScopedValue съществува, преди да осъществим достъп до него с помощта на метода isBound.

Стойностите с обхват не са обвързани с Thread, а само VirtualThreads, разклонени от StructuredTaskScope.

Повторно обвързване

ScopedValue API позволява да се установи ново свързване за вложени динамични обхвати. Това е известно като повторно свързване. ScopedValue, която е обвързана с някаква стойност, може да бъде обвързана с нова стойност за ограниченото изпълнение на някакъв метод. Разгръщащото се изпълнение на код, изпълняван от този метод, дефинира вложения динамичен обхват. Когато методът завърши (обикновено или по изключение), стойността на ScopedValue се връща към предишната си стойност.

Помислете за този пример

public class ScopedValueTest {

  public static ScopedValue<String> scopedValue = ScopedValue.newInstance();

  public static void main(String[] args) {
    ScopedValueTest instance = new ScopedValueTest();
    ScopedValue.where(scopedValue, "Tim Nadella", () -> {
      System.out.println("Value is: " + scopedValue.get());
      instance.doSomething();
    });
  }

  public void doSomething() {
    System.out.println("Doing something while accessing scoped value: " + scopedValue.get());
    ScopedValue.where(scopedValue, "Satya Cook", () -> {
      System.out.println("Value is: " + scopedValue.get());
      doSomethingAgain();
    });
  }

  public void doSomethingAgain() {
    System.out.println("Doing something again while accessing scoped value: " + scopedValue.get());
  }
}

В горния пример, когато методът doSomethingизвиква този метод doSomethingAgain, той го прави след повторно обвързване на променливата scopedValueс друга стойност и по този начин целият код, изпълнен в рамките на тази верига на извикване, вижда актуализираната стойност на променливата ScopedValue.

Резултатът от горния код е

Value is: Tim Nadella
Doing something while accessing scoped value: Tim Nadella
Value is: Satya Cook
Doing something again while accessing scoped value: Satya Cook

Ограничения

Стойностите с обхват са предназначени за използване в относително малки количества. Първоначално методът „get“ търси в обхващащи обхвати, за да намери най-вътрешното обвързване за обхватна стойност и кешира резултата в малък локален кеш на нишка. Това прави последващите извиквания на „get“ за стойността с обхват значително по-бързи. Въпреки това, ако една програма циклично използва многобройни стойности с обхват, процентът на попадения в кеша ще бъде нисък, което ще доведе до лоша производителност.

Този дизайн гарантира бързо наследяване на обхватна стойност от StructuredTaskScope нишки, като по същество не изисква повече от копиране на указател. По подобен начин оставянето на обвързване на обхватна стойност изисква минимални усилия, като например актуализиране на указател.

Тъй като кешът с обхват на стойност на нишка е ограничен по размер, за потребителите е от решаващо значение да минимизират броя на използваните обвързани стойности с обхват. Например, ако множество стойности трябва да бъдат предадени по този начин, е по-ефективно да се създаде клас на запис, който да съхранява тези стойности и да обвърже една ScopedValue към екземпляр на този запис.