> ## Content Index
> Fetch the complete content index at: https://www.sfeir.dev/llms.txt
> Use this file to discover other available public pages before exploring further.

# Threads virtuels et gestion de fichiers
- URL: https://www.sfeir.dev/back/threads-virtuels-et-gestion-de-fichiers/
- Published: 2026-04-03T08:51:31.000Z
- Updated: 2026-04-03T08:51:31.000Z
- Description: Découvrez comment exploiter les threads virtuels de Java (Project Loom) dans une application Spring Boot pour optimiser le transfert et l’organisation concurrente de fichiers via SFTP. Un guide pratique illustré, entre simplicité d’implémentation et gains en performance.
- Author: Erwan Le Tutour
- Tags: Back, Java, Spring Boot, Loom

Depuis toujours, la gestion de la concurrence en [Java](https://www.sfeir.dev/back/il-etait-une-fois-java/) repose sur les threads traditionnels du système d’exploitation. Ces derniers, bien que puissants, sont relativement coûteux en mémoire et en ressources lorsqu’il s’agit de gérer un grand nombre de tâches simultanées.  
Avec l’arrivée de [**Java 21**](https://www.sfeir.dev/back/quoi-de-neuf-dans-lapi-java-21/) et la finalisation du [**projet Loom**](https://www.sfeir.dev/back/revolutionnez-votre-programmation-asynchrone-avec-java-21-et-loom/), une nouvelle possibilité s’offre à nous : les **threads virtuels**.

Nous allons voir comment mettre en œuvre ces threads virtuels dans une application [**Spring Boot**](https://www.sfeir.dev/back/back-spring-boot/), illustrée par un cas concret d’intégration avec un serveur SFTP.

## Présentation des threads virtuels et du projet Loom

Le **projet Loom** est une initiative de longue haleine de l’équipe Java visant à simplifier l’écriture d’applications concurrentes et massivement parallèles. Son objectif principal est d’introduire des **threads légers** gérés directement par la JVM et non plus par le système d’exploitation.

- **Threads traditionnels (OS threads)** : chaque thread correspond à une ressource du système, limitée et coûteuse.
- **Threads virtuels** : extrêmement légers, créés et gérés par la JVM, ils permettent de lancer des milliers, voire des millions de tâches concurrentes sans saturer les ressources.

En pratique, un thread virtuel se comporte comme un thread classique (même API `java.lang.Thread`), mais il est planifié différemment, ce qui rend son utilisation transparente pour le développeur.

Pour en savoir plus, c'est par ici :

[Révolutionnez votre programmation asynchrone avec Java 21 et LoomDepuis sa version 19, Java a introduit le projet Loom, et avec lui, la promesse d’une programmation asynchrone non bloquante, tout en restant dans un paradigme impératif.![](https://storage.ghost.io/c/0b/31/0b31478e-a66a-46f1-8c34-d44388e21192/content/images/icon/plume-point-243.png)sfeir.dev - Le média incontournable pour les passionnés de tech et d'intelligence artificielleYves Dautremay![](https://storage.ghost.io/c/0b/31/0b31478e-a66a-46f1-8c34-d44388e21192/content/images/thumbnail/loom-2.png)](https://www.sfeir.dev/back/revolutionnez-votre-programmation-asynchrone-avec-java-21-et-loom/)

## Avantages et inconvénients

### Avantages

- **Scalabilité accrue** : on peut gérer un grand nombre de connexions simultanées (ex. : requêtes HTTP, échanges SFTP).
- **Modèle de programmation inchangé** : pas besoin de changer de paradigme comme avec les APIs réactives (Reactor, RxJava, etc.).
- **Lisibilité** : on conserve un code séquentiel, facile à maintenir, tout en profitant de la concurrence.
- **Intégration transparente** : [Spring Boot 3](https://www.sfeir.dev/back/back-pourquoi-utiliser-spring-boot/) et Java 21 offrent déjà une compatibilité native avec les threads virtuels.

### Inconvénients

- **Jeunesse de la technologie** : bien que stabilisée, l’écosystème autour des threads virtuels est encore en évolution.
- **Interopérabilité** : certaines bibliothèques très bas niveau peuvent ne pas encore être totalement optimisées.
- **Debugging** : les outils d’analyse de performance et de suivi des threads doivent encore s’adapter pleinement à ce nouveau modèle.

## Exemple concret : upload et organisation de fichiers SFTP avec threads virtuels

Prenons une application [Spring Boot](https://www.sfeir.dev/back/creer-son-projet-spring-boot-de-zero/) qui gère l’upload et l’organisation de fichiers sur un serveur **SFTP**. Sans threads virtuels, nous devions utiliser un **pool de threads** limité pour exécuter les tâches en parallèle. Avec les threads virtuels, la gestion devient bien plus flexible.

### Configuration du support des threads virtuels

Il est possible d’activer les threads virtuels simplement par une propriété :

```properties
spring.threads.virtual.enabled=true
```

Cette configuration indique à Spring d’utiliser un **`AsyncTaskExecutor` basé sur des threads virtuels**. Ainsi, chaque tâche soumise sera exécutée dans un thread virtuel.

### Définition de la configuration SFTP

On configure l’accès au serveur SFTP via Spring Integration.

Pour ce faire nous ajoutons les dépendances suivantes dans notre fichier `pom.xml`

```xml
<dependencies>
  <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-integration</artifactId>
  </dependency>

  <dependency>
      <groupId>org.springframework.integration</groupId>
      <artifactId>spring-integration-sftp</artifactId>
  </dependency>
</dependencies>
```

Et créons une classe de configuration qui s'occupera de gérer les accès à notre serveur SFTP

```java
@Configuration
@EnableIntegration
public class SftpConfig {

    @Value("${sftp.host}")
    private String sftpHost;

    @Value("${sftp.port}")
    private int sftpPort;

    @Value("${sftp.user}")
    private String sftpUser;

    @Value("${sftp.password}")
    private String sftpPassword;

    @Value("${sftp.remote.dir}")
    private String sftpRemoteDir;

    @Bean
    public SessionFactory<SftpClient.DirEntry> sftpSessionFactory() {
        DefaultSftpSessionFactory factory = new DefaultSftpSessionFactory(true);
        factory.setHost(sftpHost);
        factory.setPort(sftpPort);
        factory.setUser(sftpUser);
        factory.setPassword(sftpPassword);
        factory.setAllowUnknownKeys(true);
        return new CachingSessionFactory<>(factory);
    }

    @Bean
    @ServiceActivator(inputChannel = "toSftpChannel")
    public MessageHandler sftpMessageHandler() {
        SftpMessageHandler handler = new SftpMessageHandler(sftpSessionFactory());
        handler.setRemoteDirectoryExpression(new LiteralExpression(sftpRemoteDir));
        handler.setAutoCreateDirectory(true);
        return handler;
    }
}
```

Ici, rien n’est spécifique aux threads virtuels. Mais le choix du `taskExecutor` utilisé dans le service changera tout.

### Service utilisant l’`AsyncTaskExecutor` basé sur des threads virtuels

Le cœur de notre application réside dans le **service SFTP**, qui utilise :

- un **`SftpGateway`** pour déléguer les transferts,
- un **`AsyncTaskExecutor`** pour exécuter les tâches en parallèle,
- et la **`SessionFactory`** pour interagir avec le serveur SFTP.

#### Déclaration du service

```java
@Service
public class SftpService {

    private static final Logger log = LoggerFactory.getLogger(SftpService.class);
    private final SftpGateway sftpGateway;
    private final AsyncTaskExecutor taskExecutor;
    private final SessionFactory<SftpClient.DirEntry> sftpSessionFactory;

    @Value("${sftp.remote.dir}")
    private String sftpRemoteDir;

    public SftpService(SftpGateway sftpGateway,
                       AsyncTaskExecutor taskExecutor,
                       SessionFactory<SftpClient.DirEntry> sftpSessionFactory) {
        this.sftpGateway = sftpGateway;
        this.taskExecutor = taskExecutor;
        this.sftpSessionFactory = sftpSessionFactory;
    }

```

Ici, `taskExecutor` est injecté automatiquement par Spring. Grâce à la propriété  
`spring.threads.virtual.enabled=true`, il est basé sur des **threads virtuels**.

#### Upload de fichiers avec exécution concurrente

```java
public void uploadFiles(List<MultipartFile> files) {
    Path tempDir;
    try {
        tempDir = Files.createTempDirectory("upload-");
    } catch (IOException e) {
        throw new RuntimeException(e);
    }

    files.forEach(file -> taskExecutor.submit(() -> {
        String filename = StringUtils.cleanPath(Objects.requireNonNull(file.getOriginalFilename()));
        if (filename.isEmpty()) {
            log.warn("Skipping file with empty name");
            return;
        }
        try {
            Path tempFile = tempDir.resolve(filename);
            file.transferTo(tempFile);
            log.info("Uploading file {} to SFTP server", tempFile.toFile().getName());
            sftpGateway.upload(tempFile.toFile());
            Files.delete(tempFile);
            log.info("File {} transferred with success", filename);
        } catch (Exception e) {
            log.error("Error processing file {}", filename, e);
        }
    }));
}

```

- Chaque fichier est transféré **dans un thread virtuel distinct** grâce au `taskExecutor.submit(...)`.
- Cela permet de gérer **des centaines de fichiers en parallèle** sans épuiser les ressources de la machine.
- Les logs permettent de suivre finement la progression de chaque upload.

#### Organisation concurrente des fichiers

```java
public void organizeFiles() {
    long startTime = System.currentTimeMillis();
    try (var session = sftpSessionFactory.getSession()) {
        var files = session.list(sftpRemoteDir);
        log.info("Found {} entries in {}", files.length, sftpRemoteDir);

        for (var file : files) {
            String filename = file.getFilename();
            if (file.getAttributes().isDirectory() || filename.equals(".") || filename.equals("..")) {
                continue;
            }

            taskExecutor.submit(() -> {
                try {
                    if (filename.length() < 14 || filename.charAt(8) != '_') {
                        log.warn("File '{}' does not match expected format yyyyMMdd_HHmm_name, skipping.", filename);
                        return;
                    }

                    String year = filename.substring(0, 4);
                    String month = filename.substring(4, 6);
                    String day = filename.substring(6, 8);
                    String targetDir = sftpRemoteDir + "/" + year + "/" + month + "/" + day;

                    // création récursive des répertoires
                    String[] dirs = targetDir.replace(sftpRemoteDir + "/", "").split("/");
                    String currentPath = sftpRemoteDir;
                    for (String dir : dirs) {
                        currentPath = currentPath + "/" + dir;
                        try {
                            session.mkdir(currentPath);
                            log.info("Created directory: {}", currentPath);
                        } catch (IOException e) {
                            if (!session.exists(currentPath)) throw e;
                        }
                    }

                    String fromPath = sftpRemoteDir + "/" + filename;
                    String toPath = targetDir + "/" + filename;

                    log.info("Moving file from {} to {}", fromPath, toPath);
                    session.rename(fromPath, toPath);
                    log.info("Successfully moved file {}", filename);
                } catch (Exception e) {
                    log.error("Error processing file {}: {}", filename, e.getMessage(), e);
                }
            });
        }
    } catch (IOException e) {
        log.error("Failed to list or move files on SFTP server", e);
        throw new RuntimeException(e);
    }
    long endTime = System.currentTimeMillis();
    log.info("Organize files concurrently took {} ms", (endTime - startTime));
}
```

Examinons maintenant en détail le fonctionnement :

L’objectif est simple : nous disposons d’un répertoire SFTP rempli de fichiers nommés selon une convention précise : `yyyyMMdd_HHmm_nom.extension`

```bash
20250910_1430_facture.pdf
20250911_0830_rapport.docx
```

Ces fichiers doivent être automatiquement classés dans des sous-dossiers correspondant à leur date :

```bash
/remoteDir/2025/09/10/20250910_1430_facture.pdf
/remoteDir/2025/09/11/20250911_0830_rapport.docx
```

**Étape 1 - Récupération des fichiers**  
La méthode commence par ouvrir une session SFTP et récupérer la liste des fichiers dans le répertoire cible :

```java
try (var session = sftpSessionFactory.getSession()) {
    var files = session.list(sftpRemoteDir);
    log.info("Found {} entries in {}", files.length, sftpRemoteDir);
```

Ici, `session.list(...)` retourne l’ensemble des fichiers et sous-dossiers. On logge également le nombre d’entrées trouvées pour suivre le traitement.

**Étape 2 - Filtrage des fichiers valides**  
On élimine les entrées qui ne correspondent pas à des fichiers à déplacer :

```java
String filename = file.getFilename();
if (file.getAttributes().isDirectory() || filename.equals(".") || filename.equals("..")) {
    continue;
}
```

Les répertoires et les pseudo-entrées (`.` et `..`) sont ignorés. Seuls les vrais fichiers sont pris en compte.

**Étape 3 - Lancement concurrent de tâches**  
C’est ici que les **threads virtuels** entrent en scène. Pour chaque fichier valide, on soumet une tâche asynchrone au `taskExecutor` :

```java
taskExecutor.submit(() -> {
    try {
        if (filename.length() < 14 || filename.charAt(8) != '_') {
            log.warn("File '{}' does not match expected format yyyyMMdd_HHmm_name, skipping.", filename);
            return;
        }

        String year = filename.substring(0, 4);
        String month = filename.substring(4, 6);
        String day = filename.substring(6, 8);

        String targetDir = sftpRemoteDir + "/" + year + "/" + month + "/" + day;

        // Ensure target directory exists, handling concurrency
        String[] dirs = targetDir.replace(sftpRemoteDir + "/", "").split("/");
        String currentPath = sftpRemoteDir;
        for (String dir : dirs) {
            currentPath = currentPath + "/" + dir;
            try {
                session.mkdir(currentPath);
                log.info("Created directory: {}", currentPath);
            } catch (IOException e) {
                // It's possible another thread created the directory.
                // We check if it exists now. If it does, we can ignore the exception.
                if (!session.exists(currentPath)) {
                    // If it still doesn't exist, the error was real.
                    throw e;
                }
            }
        }

        String fromPath = sftpRemoteDir + "/" + filename;
        String toPath = targetDir + "/" + filename;

        log.info("Moving file from {} to {}", fromPath, toPath);
        session.rename(fromPath, toPath);
        log.info("Successfully moved file {}", filename);
    } catch (Exception e) {
        log.error("Error processing file {}: {}", filename, e.getMessage(), e);
    }
});
```

Chaque tâche sera exécutée dans un **thread virtuel distinct**.  
Contrairement aux threads classiques, on peut en lancer **des centaines ou des milliers** sans crainte de saturer la JVM.

- Avant de déplacer un fichier, on s’assure qu’il correspond bien au format attendu `yyyyMMdd_HHmm_nom.extension`, les fichiers mal nommés sont ignorés, évitant des erreurs lors du calcul des répertoires cibles.
- On extrait l’année, le mois et le jour directement du nom de fichier, cela permet de construire dynamiquement la hiérarchie de dossiers dans laquelle le fichier sera déplacé.
- Le plus délicat est de gérer la création concurrente des répertoires. Plusieurs threads peuvent tenter de créer le même dossier en même temps (par ex. `2025/09/10`).
  - Chaque thread crée récursivement les sous-répertoires nécessaires.
  - Si un autre thread a déjà créé le répertoire, une exception est levée, mais on la neutralise si le répertoire existe bel et bien.
  - Cette approche évite les collisions entre tâches concurrentes.
- Une fois la structure prête, on peut déplacer le fichier, chaque thread gère son fichier indépendamment, ce qui permet un **traitement parallèle massif**.

## Comparaison avec un traitement séquentiel

Dans mon service, j'ai une méthode qui fait exactement la même chose que celle dont je vous ai détaillé le fonctionnement, mais sans utiliser les threads virtuel.

Ceci afin de vous démontrer la différence dans les temps de traitement 

```log
Organize files concurrently took 19 ms
Organize files sequentially took 122 ms
```

Dans le cadre de mon exemple, je n'ai utilisé que 32 fichiers, mais on peut constater une large différence dans le temps pris par les 2   
processus.

## Conclusion

Les **threads virtuels** du projet Loom ouvrent une nouvelle ère pour les applications Java concurrentes.  
Ils combinent la **simplicité du modèle de programmation synchrone** avec la **puissance de la scalabilité** propre aux solutions réactives.

Dans notre exemple, nous avons vu qu’une application Spring Boot pouvait, sans modification majeure du code, **tirer parti des threads virtuels** pour exécuter en parallèle des transferts et des réorganisations de fichiers sur un serveur SFTP.

En définitive, l’adoption des threads virtuels est une évolution naturelle pour les projets Java modernes :

- elle réduit la complexité du code,
- elle améliore la performance,
- et elle prépare nos applications à répondre aux besoins croissants de concurrence et de scalabilité.

---

Tout le code relatif à cet article est disponible ici :

[GitHub - ErwanLT/springboot-demo: Demo project for spring-boot possibilityDemo project for spring-boot possibility. Contribute to ErwanLT/springboot-demo development by creating an account on GitHub.![](https://storage.ghost.io/c/0b/31/0b31478e-a66a-46f1-8c34-d44388e21192/content/images/icon/pinned-octocat-093da3e6fa40-99.svg)GitHubErwanLT![](https://storage.ghost.io/c/0b/31/0b31478e-a66a-46f1-8c34-d44388e21192/content/images/thumbnail/1c442eb6-968b-4033-9509-b77aaa738390-43)](https://github.com/ErwanLT/springboot-demo?ref=sfeir.dev)