[Canonical page](<https://docs.dhtmlx.com/planning-engine/java/how-to/share-a-project-between-threads/>)
[Full documentation index](<https://docs.dhtmlx.com/planning-engine/llms.txt>)


# Share a project between threads

Use one solved project from several threads without corrupting its state: run what-if edits in parallel, or let many request threads edit the same plan.

A [`Project`](<https://docs.dhtmlx.com/planning-engine/java/api/project/project.md>) isn't synchronized, and neither is [`JsonProject`](<https://docs.dhtmlx.com/planning-engine/java/api/json/json-project.md>). Every call can write internal state, even [`copy()`](<https://docs.dhtmlx.com/planning-engine/java/api/project/project.md#copy>), which marks the data as shared on both sides. Two patterns are safe:

- Give each thread its own copy, made on the thread that owns the project.
- Route every call on one shared project through a lock.

## Prerequisites

- A solved `Project`: [`solve(data, options)`](<https://docs.dhtmlx.com/planning-engine/java/api/project-solve.md>) has run.

## Steps

### Run what-ifs on per-thread copies

1. Make one copy per task on the thread that owns the project, before you hand any work to the pool. `copy()` is O(1): the copies share data until one of them is edited.

   ```java
   List<Project> drafts = new ArrayList<>();
   for (int i = 0; i < options.length; i++) {
       drafts.add(project.copy());
   }
   ```

2. Give each worker exactly one copy. The worker can edit and solve it freely. Its first edit deep-copies the data, so the other copies and the original don't change.

   ```java
   Project draft = drafts.get(i);
   pool.submit(() -> {
       draft.apply(stretch("build", duration));
       return draft.solve(null, new SolveOptions());
   });
   ```

3. Don't call anything on the original while the workers run. If you need the original, take the lock from the next section.

4. Treat every [`ScheduleResult`](<https://docs.dhtmlx.com/planning-engine/java/api/results/schedule-result.md>) as read-only. A copy shares the cached result with its original until one of them is edited, so two threads can hold the same object. Call [`deepClone()`](<https://docs.dhtmlx.com/planning-engine/java/api/results/schedule-result.md#deepclone>) before you change one.

### Guard one shared project with a lock

1. Wrap the project in a class and take one lock for every call, reads included. `solve(null, …)` stores its cached result and `copy()` writes sharing flags, so no call counts as a pure read.

   ```java
   static class SharedPlan {
       private final Project project = new Project();

       synchronized ScheduleResult load(ProjectData data) {
           return project.solve(data, new SolveOptions());
       }

       synchronized ScheduleResult edit(SchedulingChanges changes) {
           project.apply(changes);
           return project.solve(null, new SolveOptions());
       }

       synchronized Project snapshot() {
           return project.copy();
       }
   }
   ```

2. Keep `apply` and the `solve(null, …)` that follows it in the same locked block. Otherwise another thread can apply its own edit in between, and the result covers both edits.

3. Hand out `snapshot()` copies for long work such as [`level`](<https://docs.dhtmlx.com/planning-engine/java/api/project-level.md>) or [`suggestAssignments`](<https://docs.dhtmlx.com/planning-engine/java/api/project-suggest-assignments.md>), so the lock isn't held while it runs.

The full example loads a two-task plan into a `SharedPlan`, takes three snapshots, and stretches `build` to a different length on each one in a thread pool:

```java
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import org.dhtmlx.plen.*;

public class ShareProject {
  // One project shared by many threads: every call goes through the lock.
  static class SharedPlan {
    private final Project project = new Project();

    synchronized ScheduleResult load(ProjectData data) {
      return project.solve(data, new SolveOptions());
    }

    synchronized ScheduleResult edit(SchedulingChanges changes) {
      project.apply(changes);
      return project.solve(null, new SolveOptions());
    }

    synchronized Project snapshot() {
      return project.copy();
    }
  }

  static SchedulingChanges stretch(String task, double duration) {
    var changes = new SchedulingChanges();
    changes.tasks.update.add(new TaskUpdate(Id.str(task)).withDuration(Patch.of(duration)));
    return changes;
  }

  public static void main(String[] args) throws Exception {
    var monday = Moment.fromYmdHm(2026, 8, 10, 0, 0);
    var data = new ProjectData()
        .withConfig(new SchedulingConfig().withUseCalendars(false))
        .withTasks(List.of(
            new TaskData(Id.str("design")).withStartDate(monday).withDuration(2.0),
            new TaskData(Id.str("build")).withStartDate(monday).withDuration(3.0)))
        .withLinks(List.of(new LinkData()
            .withId(Id.str("l1")).withSource(Id.str("design")).withTarget(Id.str("build"))));

    var plan = new SharedPlan();
    plan.load(data);

    double[] options = {4.0, 6.0, 8.0};
    List<Project> drafts = new ArrayList<>();
    for (int i = 0; i < options.length; i++) {
      drafts.add(plan.snapshot());
    }

    ExecutorService pool = Executors.newFixedThreadPool(options.length);
    try {
      List<Future<String>> results = new ArrayList<>();
      for (int i = 0; i < options.length; i++) {
        Project draft = drafts.get(i);
        double duration = options[i];
        results.add(pool.submit(() -> {
          draft.apply(stretch("build", duration));
          ScheduleResult result = draft.solve(null, new SolveOptions());
          Mutation build = result.updates.stream()
              .filter(u -> u.id.equals(Id.str("build")))
              .findFirst().orElseThrow();
          return "build = " + duration + " days: ends " + build.endDate;
        }));
      }
      for (Future<String> result : results) {
        System.out.println(result.get());
      }
    } finally {
      pool.shutdown();
    }

    ScheduleResult accepted = plan.edit(stretch("build", 4.0));
    System.out.println("shared project updates: " + accepted.updates.size());
  }
}
```

## Result

Each worker reports its own finish date for `build`, and the shared project is still at the loaded state until `edit` runs:

```text
build = 4.0 days: ends 2026-08-16 00:00
build = 6.0 days: ends 2026-08-18 00:00
build = 8.0 days: ends 2026-08-20 00:00
shared project updates: 1
```

`design` ends on August 12, so `build` starts there in every copy. The last line comes from the shared project. Its baseline still has `build` at 3 days, which is why the 4-day edit reports one update.

To score many change sets in parallel, you don't need your own pool: [`evaluateWith(candidates, options, Execution.parallel(threads))`](<https://docs.dhtmlx.com/planning-engine/java/api/project-evaluate.md#evaluatewith>) scores them on isolated copies and blocks until all are done. See [`Execution`](<https://docs.dhtmlx.com/planning-engine/java/api/evaluate/execution.md>). To keep long calls off a request thread, see [Call from async code](<https://docs.dhtmlx.com/planning-engine/java/how-to/call-from-async-code.md>).
