2 Stimmen

Sperren einer Datei für einen einzelnen Thread innerhalb einer VM

Ich habe mehrere Threads, die meine 'Data'-Objekte in Dateien serialisieren. Der Dateiname basiert auf 2 Feldern des Objekts

  class Data {
    org.joda.DateTime time;
    String title;

    public String getFilename() {
      return time.toString() + '\_' + title + ".xml";
    }

Es ist möglich, dass 2 Datenobjekte die gleiche "Zeit" und den gleichen "Titel" und damit den gleichen Dateinamen haben.

Das ist akzeptabel, und ich bin froh, wenn einer von beiden gerettet wird. (Wahrscheinlich handelt es sich sowieso um dasselbe Datenobjekt, wenn es dasselbe ist)

Mein Problem ist, dass zwei (oder mehr) Threads in eine Datei GLEICHZEITIG schreiben, was zu fehlerhaftem XML führt.

Ich hatte einen Blick auf java.nio.channels.FileLock, aber es ist für VM-Wide Sperren, und speziell NICHT geeignet für Intra-Thread-Sperren.

Ich könnte auf DataIO.class synchronisieren (aber das wird einen RIESIGEN Overhead verursachen, da ich wirklich nur auf die einzelne Datei synchronisieren möchte).

Eine Synchronisierung mit dem File-Objekt ist nutzlos, da mehrere File-Objekte dieselbe System-Datei darstellen können.

Code Folgt:

class DataIO {
  public void writeArticleToFile(Article article, String filename, boolean overwrite) throws IOException {
    File file = new File(filename);
    writeArticleToFile(article, file, overwrite);
  }

  public void writeDataToFile(Data data, File file, boolean overwrite) throws IOException {
    if (file.exists()) {
      if (overwrite) {
        if (!file.delete()) {
          throw new IOException("Failed to delete the file, for overwriting: " + file);
        }
      } else {
        throw new IOException("File " + file + " already exists, and overwrite flag is set to false.");
      }
    }

    File parentFile = file.getParentFile();
    if (parentFile != null) {
      file.getParentFile().mkdirs();
    }

    file.createNewFile();

    if (!file.canWrite()) {
      throw new IOException("You do not have permission to write to the file: " + file);
    }

    FileOutputStream fos = new FileOutputStream(file, false);
    try {
      writeDataToStream(data, fos);
      logger.debug("Successfully wrote Article to file: " + file.getAbsolutePath());
    } finally {
      fos.close();
    }
  }
}

2voto

John Vint Punkte 38604

Wenn ich das richtig verstehe, haben Sie ein Datenobjekt, das eine einzelne Datei darstellt.

Sie können in Erwägung ziehen, einen Striped Set auf der Grundlage des Data-Objekts zu erstellen. Möglicherweise mit einer ConcurrentHashMap von

ConcurrentMap<Data,Lock> lockMap = new ConcurrentHashMap<Data,Lock>();

Nein, wenn Sie in dieses Objekt schreiben wollen, können Sie das tun:

Lock lock = lockMap.get(someMyDataObject);
lock.lock();
try{
   //write object here
}finally{
   lock.unlock();
}

Denken Sie daran, dass Sie die HashCode- und Equals-Methode auf der Grundlage von Titel und DateTime schreiben müssen

1voto

Adrian Regan Punkte 2240

Sie können die Zeichenkette, die den Dateinamen darstellt, intern() eingeben. Dann synchronisieren Sie auf die internierte Zeichenkette.

class DataIO {
  public void writeArticleToFile(Article article, String filename, boolean overwrite) throws IOException {
    synchronized(filename.intern()) {
       File file = new File(filename);
       writeArticleToFile(article, file, overwrite);
    }
  }

0 Stimmen

Das ist mit Sicherheit die einfachste Lösung. Nicht ohne Risiko, da die Objekte, die Sie sperren, am besten nicht öffentlich zugänglich sein sollten. (und eine internierte Zeichenfolge ist immer und überall verfügbar)

0 Stimmen

Das ist richtig und eine gute Praxis. Sie sperren jedoch nicht die Zeichenfolge an sich, sondern verwenden die Zeichenfolge, um die Grundlage für eine Sperre in Ihrer DataIO-Klasse zu bilden. Siehe geänderten Beitrag.

0 Stimmen

Das ist wirklich eine sehr saubere Lösung, aber wie Kirk Woll sagt, könnte ich den String auch an anderer Stelle brauchen (insbesondere zum Lesen der Dateien usw.). Wenn ich jedoch dem Dateinamen eine ungewöhnliche, verworrene Zeichenkette voranstelle (vielleicht den voll qualifizierten Klassennamen) und dann auf den intern() dieser Zeichenkette sperre, ist die Wahrscheinlichkeit, dass ich genau dieses Zeichenkettenobjekt benötige, fast null, und das ist für mich akzeptabel.

0voto

Kirk Woll Punkte 72923

Ich stimme zu, dass die Synchronisierung die Technik ist, die Sie verwenden sollten. Was Sie brauchen, ist ein eigenes Objekt für jede Datei-Permutation, und noch wichtiger, jedes Mal das gleiche Objekt. Eine Option könnte sein, eine Klasse namens FileLock zu erstellen:

public class FileLock {
    DateTime time;
    String title;

    public FileLock(DateTime time, String title) {
        this.time = time;
        this.title = title;
    }

    override equals/hashCode based on those two properties

    static Hashtable<FileLock, FileLock> unqiueLocks = new Hashtable<FileLock, FileLock>();
    static lockObject = new Object();

    public static FileLock getLock(DateTime time, String title) {
        synchronized (lockObject) {
            FileLock lock = new FileLock(time, title);
            if (unqiueLocks.ContainsKey(lock)) {
                return unqiueLocks.get(lock);
            }
            else {
                unqiueLocks.put(lock, lock);
                return lock;
            }
        }
    }
}

Dann würden die Anrufer es so verwenden:

synchronized (FileLock.getLock(time, title)) {
    ...
}

Beachten Sie, dass dies zu einem Speicherleck führt, da die Hashtabelle mit neuen Datei-/Zeit-Permutationen weiter wächst. Bei Bedarf können Sie diese Technik so abändern, dass die Aufrufer von getLock auch eine releaseLock-Methode aufrufen, die Sie verwenden, um die Hashtable sauber zu halten.

CodeJaeger.com

CodeJaeger ist eine Gemeinschaft für Programmierer, die täglich Hilfe erhalten..
Wir haben viele Inhalte, und Sie können auch Ihre eigenen Fragen stellen oder die Fragen anderer Leute lösen.

Powered by:

X