Threads Zwei Threads, aber doppelte Ausgabe verhindern (synchronized)

Diskutiere Zwei Threads, aber doppelte Ausgabe verhindern (synchronized) im Java Basics - Anfänger-Themen Bereich.
T

Tobias-nrw

Mir wurde ein Programm zugesendet und meine Frage wär jetzt, wie ich die doppelte Ausgabe verhindere...

Java:
import java.util.Arrays;
import java.util.Scanner;

public class Unittest {
  private static Scanner s1;
  private static String p1, p2;

  public static void main(String[] args) {
    s1 = new Scanner(System.in);
    System.out.println("Adresse 1: ");
    p1 = s1.nextLine();
    System.out.println("Adresse 2: ");
    p2 = s1.nextLine();
    byte[] ip1 = ipv4ToBytes(p1);
    byte[] ip2 = ipv4ToBytes(p2);

    // Thread
    Runnable r1 = new Runnable() {
      @Override
      public void run() {
        byte[] a = ip1;
        while (true) {
          String s = bytesToIpv4(a);
          System.out.println("Die Adresse Thread 1 ist " + s);
          synchronized (s1) {
            if (isNotEquals(a, ip2)) {
              count(a);
            } else {
              break;
            }
          }
        }
      }
    };
    Runnable r2 = new Runnable() {
      @Override
      public void run() {
        byte[] a = ip2;
        while (true) {
          String s = bytesToIpv4(a);
          System.out.println("Die Adresse Thread 2 ist " + s);
          synchronized (s1) {
            if (isNotEquals(a, ip1)) {
              count2(a);
            } else {
              break;
            }
          }
        }
      }
    };

    new Thread(r1).start();
    new Thread(r2).start();
  }

  public static boolean isNotEquals(byte[] a, byte[] b) {
    return !Arrays.equals(a, b);
  }

  public static byte[] ipv4ToBytes(String ip) {
    String[] parts = ip.split("\\.");
    byte[] result = new byte[4];

    for (int i = 0; i < result.length; i++) {

      result[i] = (byte) Integer.parseInt(parts[i]);
    }
    return result;
  }

  public static void count(byte[] value) {

    int i = value.length;

    do {

      i--;
      value[i]++;
    } while (value[i] == 0);
  }

  public static void count2(byte[] value) {

    int i = value.length;

    do {

      i--;
      value[i]--;
    } while ((value[i] & 0xFF) == 255);
  }

  public static String bytesToIpv4(byte[] value) {

    return String.format("%d.%d.%d.%d", value[0] & 0xff, value[1] & 0xff, value[2] & 0xff, value[3] & 0xff);
  }

}

Code:
Adresse 1: 
255.255.254.240
Adresse 2: 
255.255.255.5
Die Adresse Thread 1 ist 255.255.254.240
Die Adresse Thread 2 ist 255.255.255.5
Die Adresse Thread 2 ist 255.255.255.4
Die Adresse Thread 1 ist 255.255.254.241
Die Adresse Thread 2 ist 255.255.255.3
Die Adresse Thread 1 ist 255.255.254.242
Die Adresse Thread 2 ist 255.255.255.2
Die Adresse Thread 1 ist 255.255.254.243
Die Adresse Thread 2 ist 255.255.255.1
Die Adresse Thread 2 ist 255.255.255.0
Die Adresse Thread 2 ist 255.255.254.255
Die Adresse Thread 2 ist 255.255.254.254
Die Adresse Thread 2 ist 255.255.254.253
Die Adresse Thread 2 ist 255.255.254.252
Die Adresse Thread 2 ist 255.255.254.251
Die Adresse Thread 2 ist 255.255.254.250
Die Adresse Thread 2 ist 255.255.254.249
Die Adresse Thread 2 ist 255.255.254.248
Die Adresse Thread 2 ist 255.255.254.247
Die Adresse Thread 1 ist 255.255.254.244
Die Adresse Thread 1 ist 255.255.254.245
Die Adresse Thread 1 ist 255.255.254.246
Die Adresse Thread 2 ist 255.255.254.246

Zusatzfrage: Wird das Programm immer anhalten? (bitte begründen)
 
Blender3D

Blender3D

Der Code ist mit Fehlern behaftet.
z.B.
Java:
public static void count2(byte[] value) {
    int i = value.length;
    do {
      i--;
      value[i]--;
    } while ((value[i] & 0xFF) == 255);
  }
Die while Schleife macht hier keinen Sinn, da die Bedingung maximal 1 mal erfüllt sein kann. Wenn value[i] i == 256 --> value[i]-- --> value[i] -- --> Bedinung nicht mehr erfüllt.
Da der Name der Funktion count2 nicht aussagekräftig ist, kann man hier nur vermuten was die Funktion eigentlich machen soll.
Meine Annahme ist: Sie soll eine IP4 Adresse um eine Stelle herunterzählen.
Eine Andere Frage an dieser Stelle:
Java:
       synchronized (s1) {
            if (isNotEquals(a, ip1)) {
              count2(a);
            } else {
              break;
            }
s1 ist der Scanner der zum Einlesen der ip4 Strings dient. Warum wird eine Variable synchronisiert, auf die gar nicht mehr zugegriffen wird?
Zusatzfrage: Wird das Programm immer anhalten? (bitte begründen)
Da die Bedingung zum Abbruch des jeweiligen Threads die Gleichheit beider IP4 Adressen ist . Die Countfunktion aber immer nur die Letzte Stelle herauf oder herunter zählt --- > Nein es wird nicht immer abgebrochen.

Hier ein andere Variante.

Java:
public class Unittest {
    public static void main(String[] args) {
        Scanner input = new Scanner(System.in);
        System.out.println("Gib 2 IP4 Adressen ein:\nIP1 wird hinauf- und IP2 wird heruntergezählt.\n");
        System.out.println(
                "Die zählenden Threads stoppen wenn:\na) Beide die selbe IP Adresse haben.\nb) Die Limits erreicht wurden.\n");
        System.out.print("Adresse 1: ");
        String tmp1 = input.nextLine();
        System.out.print("Adresse 2: ");
        String tmp2 = input.nextLine();
        input.close();
        byte[] ip1 = ipv4ToBytes(tmp1);
        byte[] ip2 = ipv4ToBytes(tmp2);

        // Thread
        Runnable r1 = new Runnable() {
            @Override
            public void run() {
                while (true) {
                    String s = bytesToIpv4(ip1);
                    System.out.println("Die Adresse Thread 1 ist " + s);
                    synchronized (ip2) {
                        if (Arrays.equals(ip1, ip2) || !countUp(ip1)) {
                            System.out.println("Thread 1 exit");
                            break;
                        }
                    }
                }
            }
        };
        Runnable r2 = new Runnable() {
            @Override
            public void run() {
                while (true) {
                    String s = bytesToIpv4(ip2);
                    System.out.println("Die Adresse Thread 2 ist " + s);
                    synchronized (ip1) {
                        if (Arrays.equals(ip2, ip1) || !countDown(ip2)) {
                            System.out.println("Thread 2 exit");
                            break;
                        }
                    }
                }
            }
        };
        new Thread(r1).start();
        new Thread(r2).start();
    }

    public static String bytesToIpv4(byte[] value) {
        return String.format("%d.%d.%d.%d", value[0] & 0xff, value[1] & 0xff, value[2] & 0xff, value[3] & 0xff);
    }

    public static boolean countUp(byte[] value) {
        int i = value.length - 1;
        while (true) {
            if ((value[i] & 255) == 255) {
                if (i == 0)
                    return false;
                value[i] = 0;
                i--;
            } else {
                value[i]++;
                return true;
            }
        }
    }

    public static boolean countDown(byte[] value) {
        int i = value.length - 1;
        while (true) {
            if ((value[i] & 255) == 0) {
                if (i == 0)
                    return false;
                value[i] = (byte) 255;
                i--;
            } else {
                value[i]--;
                return true;
            }
        }
    }

    public static byte[] ipv4ToBytes(String ip) {
        String[] parts = ip.split("\\.");
        byte[] result = new byte[4];
        for (int i = 0; i < result.length; i++)
            result[i] = (byte) Integer.parseInt(parts[i]);
        return result;
    }
}
 
Zuletzt bearbeitet von einem Moderator:
T

Tobias-nrw

Ich Frage doch nur stellvertretend und der Fehler ist mir gar nicht aufgefallen, ich denke es wäre besser die mittlere IP herauszufinden und nicht zu synchronisieren...

Btw. Ja, Zugeständnis, ich habe den Code etwas ver(schlimm)bessert bevor ich ihn hier postete.
 
M

mrBrown

Die while Schleife macht hier keinen Sinn, da die Bedingung maximal 1 mal erfüllt sein kann. Wenn value[i] i == 256 --> value[i]-- --> value[i] -- --> Bedinung nicht mehr erfüllt.
Sollte das so sein? (Hab das mal in [PLAIN] gesetzt, damit die [i] da bleibe, sieht allerdings merkwürdig aus...)
 
T

temi

Hier ein andere Variante.
Ist das jetzt wirklich besser? Du synchronisierst jetzt auf ip1, bzw. ip2, aber es greift sowieso nur jeweils ein Thread auf diese Variable zu.

Meine schlichte Herangehenweise wäre, entweder wie schon @Tobias-nrw angeregt hat, jedem Thread seinen eigenen Bereich an IP-Adressen zur Bearbeitung zu geben.

Oder eine Liste von IP-Adressen zu erstellen und darauf zu synchronisieren. Dann können die Threads sich immer die nächste zu prüfende IP holen. Eine Alternative dazu wäre ein Set auf dem synchronisiert wird, in das jeder Thread die IP-Adressen schreibt, die er gerade prüft. Damit kann vor jeder neuen Prüfung geschaut werden, ob die Adresse bereits bearbeitet wurde.

Edit:

Der ganze Code ist (wie auch schon in einem anderen Thema bereits angedeutet) wenig objektorientiert. Ich würde eine Klasse "IPv4Range" vorsehen, die über eine threadsichere Methode "next()" die nächste Adresse der Range liefert. Diese kann dann von beliebig vielen Threads abgerufen und getestet werden.
 
Zuletzt bearbeitet:
M

mrBrown

Die while Schleife macht hier keinen Sinn, da die Bedingung maximal 1 mal erfüllt sein kann. Wenn value[i] i == 256 --> value[i]-- --> value[i] -- --> Bedinung nicht mehr erfüllt.
Nein, die Schleife kann durchaus mehrfach durchlaufen werden, am einfachsten zu sehen mit {0,0,0,0}.

Fängt mit Index 3 an, 0 - 1 = -1 , -1 & 0xFF = 255, Bedingung ist also wahr.
Dann das gleiche mit Index 2, genau gleiche Rechnung, Bedingung ist weiterhin wahr.
Dann das gleiche mit Index 1, usw...


Da die Bedingung zum Abbruch des jeweiligen Threads die Gleichheit beider IP4 Adressen ist . Die Countfunktion aber immer nur die Letzte Stelle herauf oder herunter zählt --- > Nein es wird nicht immer abgebrochen.
Zählt wie gesagt nicht nur die letzte Stelle ;)


s1 ist der Scanner der zum Einlesen der ip4 Strings dient. Warum wird eine Variable synchronisiert, auf die gar nicht mehr zugegriffen wird?
Ob drauf zugegriffen wird ist egal, wichtig ist in dem Fall nur, dass alle Threads ein gemeinsames Objekt zum synchronisieren nutzen.
Für die Nutzung des Scanners gibts keinen Grund, es funktioniert allerdings, anders als deine Variante:

Ist das jetzt wirklich besser? Du synchronisierst jetzt auf ip1, bzw. ip2, aber es greift sowieso nur jeweils ein Thread auf diese Variable zu.
Eher deutlich schlechter - wenn jeder Thread eine eigenes Objekt zum synchronisieren hat, ist das sinnlos.
In diesem Fall verändert Thread1 ip1, während Thread2 lesend darauf zugreift.
 
T

temi

Für die Nutzung des Scanners gibts keinen Grund, es funktioniert allerdings
Naja, korrekter wäre: Die Synchronisation auf den Scanner funktioniert zwar, aber sie bewirkt nicht das, was damit bezweckt wurde. Es wird ja nicht verhindert, dass zwei Threads die gleiche IP (bzw. eine bereits getestete IP) testen.
 
Blender3D

Blender3D

Eher deutlich schlechter - wenn jeder Thread eine eigenes Objekt zum synchronisieren hat, ist das sinnlos.
In diesem Fall verändert Thread1 ip1, während Thread2 lesend darauf zugreift.
Oder Thread1 vergleicht ip1 und ip2 und verbietet währen dessen den Schreibzugriff auf ip2, um zu verhindern dass eine Veränderung während des Vergleichs stattfindet.
 
M

mrBrown

Naja, korrekter wäre: Die Synchronisation auf den Scanner funktioniert zwar, aber sie bewirkt nicht das, was damit bezweckt wurde. Es wird ja nicht verhindert, dass zwei Threads die gleiche IP (bzw. eine bereits getestete IP) testen.
Sie verhindert, das ein Thread ein Objekt verändert, während ein andere es liest. Das sie verhindert, dass eine IP mehrmals getestet wird, wurde doch von niemandem erwartet?
 
M

mrBrown

Oder Thread1 vergleicht ip1 und ip2 und verbietet währen dessen den Schreibzugriff auf ip2, um zu verhindern dass eine Veränderung während des Vergleichs stattfindet.
Nein, lies dir noch mal durch, was synchronized bewirkt ;)

Auch wenn Thread 1 über ip2 synchronisiert, kann Thread 2 munter ip2 verändern.

Hier, ein ganz reduziertes Beispiel dazu, zwei Threads, einer synchronisiert über der Variable, der andere verändert:
Java:
public static void main(String[] args) {
    int[] ints = new int[1];

    new Thread() {
        @Override
        public void run() {
            synchronized (ints) {
                while (true) {
                    System.out.println("Thread1: " + ints[0]);
                }
            }
        }
    }.start();

    while (true) {
        ints[0]++;
    }

}
 
Blender3D

Blender3D

Fängt mit Index 3 an, 0 - 1 = -1 , -1 & 0xFF = 255, Bedingung ist also wahr.
Dann das gleiche mit Index 2, genau gleiche Rechnung, Bedingung ist weiterhin wahr.
Dann das gleiche mit Index 1, usw...
Das sehe ich nicht so!
Java:
int i = value.lenght;   // i ist hier 4
i--; // i = 3
value[i]++;// Zugriff auf value[3] // hier werden die letzten 8 Bit verändert
while( (value[i] & 0xFF) == 255 ); // nur solange die letzen 8 Bit gesetzt sind
 
M

mrBrown

Ich habe das mal so dem Titel entnommen und da ich im Code nichts anderes erkenne, was das bewerkstelligen könnte, nahm ich einfach an, dass es damit geplant gewesen ist.
Hm, stimmt, in Verbindung mit dem Titel könnte man das so verstehen, wenn das so gemeint war hast du natürlich recht und das synchronized ist dafür Unsinn :) Nötig ist es allerdings trotzdem, um gleichzeitige zugriffe zu verhindern.
 
M

mrBrown

Das sehe ich nicht so!
Java:
int i = value.lenght;   // i ist hier 4
i--; // i = 3
value[i]++;// Zugriff auf value[3] // hier werden die letzten 8 Bit verändert
while( (value[i] & 0xFF) == 255 ); // nur solange die letzen 8 Bit gesetzt sind
Du hast jetzt aber nicht übersehen, dass das eine do-while-Schleife ist?

Probier es einfach aus, wenn dir mein Vorrechnen nicht reicht...
 
Zuletzt bearbeitet:
Blender3D

Blender3D

Nein, lies dir noch mal durch, was synchronized bewirkt ;)

Auch wenn Thread 1 über ip2 synchronisiert, kann Thread 2 munter ip2 verändern.
14.5.7 Synchronisieren mit »synchronized« Zur nächsten ÜberschriftZur vorigen Überschrift
Schon seit Java 1.0 können kritische Abschnitte mit synchronized geschützt werden. Im einfachsten Fall markiert der Modifizierer synchronized die gesamte Methode. Ein betretender Thread setzt bei Objektmethoden den Monitor des this-Objekts und bei statischen Methoden den Lock des dazugehörigen Class-Objekts.

Betritt ein Thread A eine synchronisierte Methode eines Objekts O und versucht anschließend Thread B eine synchronisierte Methode des gleichen Objekts O aufzurufen, muss der nachfolgende Thread B so lange warten, bis A wieder aus dem synchronisierten Teil austritt. Das geschieht, wenn der erste Thread A die Methode verlässt, denn mit dem Verlassen einer Methode – oder auch einer Ausnahme – gibt die JVM automatisch den Lock frei. Die Dauer eines Locks hängt folglich mit der Dauer des Methodenaufrufs zusammen, was zur Konsequenz hat, dass längere kritische Abschnitte die Parallelität einschränken und zu längeren Wartezeiten führen. Eine Endlosschleife in der synchronisierten Methode gäbe den Lock niemals frei.
 
Blender3D

Blender3D

Du hast jetzt aber nicht übersehen, dass das eine do-while-Schleife ist?
Java:
int i = value.lenght;   // i ist hier 4
i--; // i = 3
value[i]++;// Zugriff auf value[3] // hier werden die letzten 8 Bit verändert 
// wenn hier die letzten Bits davor gesetzt waren sind sie es jetzt nicht mehr
// einziger Fall die letzten 8 Bits waren 1111 1110 -> 1111 1111 dann wird die Schleife wieder holt
while( (value[i] & 0xFF) == 255 ); // nur solange die letzen 8 Bit gesetzt sind
Würde die Schleife hier öfters als 3 mal Durchlaufen werden --> dann ist i = -1 und es folgt eine IndexOutofBounds Exception
value[-1] geht nicht.
Das geschieht aber nicht, da ein Mehrfachdurchlauf nicht stattfindet. Habe ich laufen lassen.
 
M

mrBrown

14.5.7 Synchronisieren mit »synchronized« Zur nächsten ÜberschriftZur vorigen Überschrift
Schon seit Java 1.0 können kritische Abschnitte mit synchronized geschützt werden. Im einfachsten Fall markiert der Modifizierer synchronized die gesamte Methode. Ein betretender Thread setzt bei Objektmethoden den Monitor des this-Objekts und bei statischen Methoden den Lock des dazugehörigen Class-Objekts.

Betritt ein Thread A eine synchronisierte Methode eines Objekts O und versucht anschließend Thread B eine synchronisierte Methode des gleichen Objekts O aufzurufen, muss der nachfolgende Thread B so lange warten, bis A wieder aus dem synchronisierten Teil austritt. Das geschieht, wenn der erste Thread A die Methode verlässt, denn mit dem Verlassen einer Methode – oder auch einer Ausnahme – gibt die JVM automatisch den Lock frei. Die Dauer eines Locks hängt folglich mit der Dauer des Methodenaufrufs zusammen, was zur Konsequenz hat, dass längere kritische Abschnitte die Parallelität einschränken und zu längeren Wartezeiten führen. Eine Endlosschleife in der synchronisierten Methode gäbe den Lock niemals frei.
Richtig, genau so funktioniert synchronized (wobei es in diesem Thread allerdings um Blöcke und nicht Methoden geht).

In deinem Code gibt es Thread 1 und Thread 2.
Thread 1 hat einen Block, der ip2 zur Synchronisierung nutzt, und Thread 2 nutzt ip1.

Solange sich Thread 1 in dem Block befindet, kann kein andere Thread sich in einem Block befinde, der auch ip2 zur Synchronisierung nutzt. Thread 2 nutzt allerdings ip1, die beiden synchronized-Blöcke sind also unabhängig voneinander und können gleichzeitig ausgeführt werden.
Und insbesondere können sie nicht nur gleichzeitig ausgeführt werden, Thread 1 kann auch ip2 verändern, während Thread 2 ip2 list (und andersrum mit ip1).

Damit sie nicht gleichzeitig ein Objekte schreiben und lesen, müssen beide Blöcke das selbe Objekt zum synchronisieren nutzen.

(Feinheiten wie wait mal außer acht gelassen)
 
M

mrBrown

Würde die Schleife hier öfters als 3 mal Durchlaufen werden --> dann ist i = -1 und es folgt eine IndexOutofBounds Exception
value[-1] geht nicht.
Das geschieht aber nicht, da ein Mehrfachdurchlauf nicht stattfindet. Habe ich laufen lassen.
Keine Ahnung was du hast laufen lassen, aber ich bekomme bei passenden Werten jedes mal wie erwartete eine IndexOutOfBoundsException:

Code:
Exception in thread "main" java.lang.ArrayIndexOutOfBoundsException: Index -1 out of bounds for length 4

Java:
public static void main(String[] args) {
    byte[] bs =new byte[]{0,0,0,0};
    count2(bs);
}

public static void count2(byte[] value) {

    int i = value.length;

    do {

      i--;
      value[i]--;
    } while ((value[i] & 0xFF) == 255);
  }
 
Zuletzt bearbeitet:
M

mrBrown

Ich hab keine Ahnung, was du damit sagen willst? (Bei dem Zitat oben auch schon nicht).

Der von dir kritisieret Code nutzt mit dem Scanner ein Objekt für beide Threads - damit funktioniert synchronized.
Dein Code nutzt verschiedene Objekte für beide Threads - damit ist synchronized effektiv "ausgeschaltet".
 
Blender3D

Blender3D

Keine Ahnung was du hast laufen lassen, aber ich bekomme bei passenden Werten jedes mal wie erwartete eine IndexOutOfBoundsException:
Jetzt habe ich verstanden, was Du meinst. Im Fall 0 haben wir -1 und die letzten 8 Bits bleiben gesetzt.
Stimmt.

Trotzdem ist die Schleife an dieser Stelle sinnfrei.
 
M

mrBrown

Jetzt habe ich verstanden, was Du meinst. Im Fall 0 haben wir -1 und die letzten 8 Bits bleiben gesetzt.
Stimmt.

Trotzdem ist die Schleife an dieser Stelle sinnfrei.
Nein, ist sie nicht!

Erst durch diese Schleife wird korrekt rückwärts gezählt, sodass zB nach "127.127.127.0" "127.127.126.255" kommt.
 
T

Tobias-nrw

Mmm, ich komme bezüglich der doppelten Ausgabe auf keinen Grünen Zweig:
Java:
  private static volatile Boolean runs = true;
// ...
    Runnable r1 = new Runnable() {
      @Override
      public void run() {
        byte[] a = ip1;
        while (runs) {
          String s = bytesToIpv4(a);
          System.out.println("Die Adresse Thread 1 ist " + s);
          synchronized (runs) {
            if (runs) {
              if (isNotEquals(a, ip2)) {
                count(a);
              } else {
                runs = false;
              }
            }
          }
        }
      }
    };
 
M

mrBrown

a) Speicher alle bereits ausgegebenen IPs und check jeweils vor der Ausgabe
b) Statt sich von zwei Seiten dem Mittelwert anzunähern, für einen expliziten "IpCounter" ein, aus dem sich alle Threads die nächste IP holen. Threads müssen dann nicht synchronisiert sein, nur der Counter muss das intern regeln
c) Variante b mit Queue und explizitem Generator-Thread, dieser legt IPs in eine Queue, Worker-Threads holen die sich da raus und testen sie
d) Den gesamten Bereich direkt aufteilen, jeder Thread kümmert sich nur um seinen Bereich



BTW: der übliche Weg für synchronized ist ein explizites "lock"-Object, was einfach nur zum synchronisieren genutzt wird. Das macht den Code meist einfacher lesbar, als wenn man irgendein beliebiges Objekt dafür "missbraucht".
 
T

temi

b) Statt sich von zwei Seiten dem Mittelwert anzunähern, für einen expliziten "IpCounter" ein, aus dem sich alle Threads die nächste IP holen. Threads müssen dann nicht synchronisiert sein, nur der Counter muss das intern regeln
Ich hab es ja weiter oben schon geschrieben; die Möglichkeit b) wäre mein persönlicher Favorit, weil die Verarbeitung bei Bedarf sehr simpel auf zwei, drei oder noch mehr Threads verteilt werden kann.
 
M

mrBrown

Ich hab es ja weiter oben schon geschrieben; die Möglichkeit b) wäre mein persönlicher Favorit, weil die Verarbeitung bei Bedarf sehr simpel auf zwei, drei oder noch mehr Threads verteilt werden kann.
Ja, sind im wesentlichen deine vier Varianten, nur anders formuliert.

Für beliebig viele Threads dürften sich alle vier Varianten nutzen lassen, wobei Variante d) da am unschönsten sein dürfte.
 
T

temi

Just for fun und ohne Anspruch auf die Ideallösung:

Intern wird für die IP ein long verwendet, dann kann man einfach addieren um die nächste Adresse zu erhalten. Wer mag kann auch gerne noch ein paar zusätzliche Threads verwenden.

Java:
public final class Ipv4 {

    private static final int MAX_SHIFT = 24;
    private static final int PART_SHIFT = 8;

    public Ipv4(String value) {
        String[] parts = value.split("\\.");

        if (parts.length != 4) {
            throw new IllegalArgumentException("IP string has no valid length");
        }

        long temp = 0;

        for (int part = 0; part < parts.length; part++) {
            temp += Long.parseLong(parts[part]) << (MAX_SHIFT - PART_SHIFT * part);
        }

        this.value = temp;
    }

    public Ipv4(long value) {
        this.value = value;
    }

    public long getValue() {
        return value;
    }

    @Override
    public String toString() {
        return "Ipv4 {" +
                (value >> 24 & 0xFF) + "." +
                (value >> 16 & 0xFF) + "." +
                (value >>  8 & 0xFF) + "." +
                (value       & 0xFF) +
                "}";
    }

    private final long value;
}

Java:
public final class Ipv4Range {

    public Ipv4Range(final Ipv4 from, final Ipv4 to) {
        this.from = from.getValue();
        this.to = to.getValue();
        first();
    }

    public synchronized Ipv4 first() {
        return new Ipv4(current = from);
    }

    public synchronized Ipv4 next() {
        if (current <= to) {
            return new Ipv4(current++);
        }

        throw new NoSuchElementException("There is no next IP");
    }

    public synchronized boolean hasNext() {
        return current <= to;
    }

    private final long from, to;
    private long current;
}

Java:
public class Main {

    public static void main(String[] args) {

        Ipv4 from = new Ipv4("192.168.0.1");
        Ipv4 to = new Ipv4("192.168.0.25");

        Ipv4Range range = new Ipv4Range(from, to);

        // Thread 1
        Runnable r1 = new Runnable() {
            @Override
            public void run() {
                while (range.hasNext()) {
                    System.out.println("Die Adresse Thread 1 ist " + range.next());
                }
            }
        };

        // Thread 2
        Runnable r2 = new Runnable() {
            @Override
            public void run() {
                while (range.hasNext()) {
                    System.out.println("Die Adresse Thread 2 ist " + range.next());
                }
            }
        };

        new Thread(r1).start();
        new Thread(r2).start();
    }
}
Code:
Die Adresse Thread 2 ist Ipv4 {192.168.0.2}
Die Adresse Thread 1 ist Ipv4 {192.168.0.1}
Die Adresse Thread 2 ist Ipv4 {192.168.0.3}
Die Adresse Thread 1 ist Ipv4 {192.168.0.4}
Die Adresse Thread 2 ist Ipv4 {192.168.0.5}
Die Adresse Thread 1 ist Ipv4 {192.168.0.6}
Die Adresse Thread 2 ist Ipv4 {192.168.0.7}
Die Adresse Thread 1 ist Ipv4 {192.168.0.8}
Die Adresse Thread 2 ist Ipv4 {192.168.0.9}
Die Adresse Thread 1 ist Ipv4 {192.168.0.10}
Die Adresse Thread 2 ist Ipv4 {192.168.0.11}
Die Adresse Thread 1 ist Ipv4 {192.168.0.12}
Die Adresse Thread 2 ist Ipv4 {192.168.0.13}
Die Adresse Thread 1 ist Ipv4 {192.168.0.14}
Die Adresse Thread 2 ist Ipv4 {192.168.0.15}
Die Adresse Thread 1 ist Ipv4 {192.168.0.16}
Die Adresse Thread 2 ist Ipv4 {192.168.0.17}
Die Adresse Thread 1 ist Ipv4 {192.168.0.18}
Die Adresse Thread 2 ist Ipv4 {192.168.0.19}
Die Adresse Thread 1 ist Ipv4 {192.168.0.20}
Die Adresse Thread 2 ist Ipv4 {192.168.0.21}
Die Adresse Thread 1 ist Ipv4 {192.168.0.22}
Die Adresse Thread 2 ist Ipv4 {192.168.0.23}
Die Adresse Thread 1 ist Ipv4 {192.168.0.24}
Die Adresse Thread 2 ist Ipv4 {192.168.0.25}
 
Zuletzt bearbeitet:
M

mrBrown

Kleine Anmerkung zur Synchronisierung; zwischen hasNext und next können sich die beiden Threads in die Quere kommen. Wenn nur noch ein Element "vorhanden ist", bekommen beide bei hasNext true, next wirft aber dann bei einem eine Exception.

Man könnte da mit Optional statt der Exception arbeiten oder in Richtung Spliterator gehen.
 
T

Tobias-nrw

Wenn ich es richtig sehe wird es nicht immer anhalten bzw eine NoSuchElementException geben weil hasNext( und next( zwar einzeln synchronisiert sind, allerdings nicht zusammen, wodurch hasNext true zurückgeben kann aber zwischenzeitlich ein anderer Thread auch next aufrufen kann.
 
T

temi

Ist es nicht so, dass durch das "synchronized" auf der Methodenebene auf "this" gelockt wird?

Damit kann keine der synchronisierten Methoden betreten werden, solange ein Thread innerhalb einer der synchronisierten Methoden ist, oder?
 
M

mrBrown

Was soll sich denn dann ändern?
Man spart sich hasNext und es kann keine Exception fliegen.

Ist es nicht so, dass durch das "synchronized" an der Methode auf "this" gelockt wird?

Damit kann auch keine synchronisierte Methode betreten werden, solange ein Thread innerhalb einer der synchronisierten Methoden ist, oder?
Ja, aber bei zwei Methoden schlägt das fehl:

Thread 1 ruft hasNext auf und bekommt true
Thread 2 wartete bis Thread 1 zurückkehrt aus hasNext und ruft danach hasNext auf und bekommt true
Thread 1 wartete bis Thread 2 mit hasNext fertig ist und führt danach next aus, bekommt den Wert
Thread 2 wartete bis Thread 1 mit next fertig ist, ruft danach next auf und bekommt die Exception
 
T

temi

Ihr dürft das gerne korrigieren ;)

Wir gehen jetzt erst mal in die Sauna :)
 
M

mrBrown

- IPv4 gibt nach außen nicht den long preis, stattdessen bekommt sie 'ne Methode um die IP zu inkrementieren.
- IPRange bekommt statt hasNext und next eine getNext, die ein Optional zurück gibt
- außerdem zusätzlich tryAdvance und forEachRemaining wie zB Spliterator das hat

Alle drei Varianten sollten sich parallel nutzen lassen, falls jemand Fehler sieht gerne drauf hinweisen :)

Java:
public class IPv4 implements Comparable<IPv4> {

    private static final int MAX_SHIFT = 24;

    private static final int PART_SHIFT = 8;

    private final long value;

    public IPv4(String value) {
        String[] parts = value.split("\\.");

        if (parts.length != 4) {
            throw new IllegalArgumentException("IP string has no valid length");
        }

        long temp = 0;

        for (int part = 0; part < parts.length; part++) {
            temp += Long.parseLong(parts[part]) << (MAX_SHIFT - PART_SHIFT * part);
        }

        this.value = temp;
    }

    private IPv4(long value) {
        this.value = value;
    }

    public IPv4 increment() {
        return new IPv4(this.value+1);
    }

    @Override
    public String toString() {
        return "" +
               (value >> 24 & 0xFF) + "." +
               (value >> 16 & 0xFF) + "." +
               (value >> 8 & 0xFF) + "." +
               (value & 0xFF);
    }

    @Override
    public int compareTo(final IPv4 that) {
        return Long.compare(this.value, that.value);
    }

    @Override
    public boolean equals(final Object o) {
        if (this == o) { return true; }
        if (o == null || getClass() != o.getClass()) { return false; }
        final IPv4 that = (IPv4) o;
        return this.value == that.value;
    }

    @Override
    public int hashCode() {
        return Objects.hash(value);
    }

}

Java:
public class IPv4Range {

    private final IPv4 from;

    private final IPv4 to;

    private IPv4 current;

    public IPv4Range(final IPv4 from, final IPv4 to) {
        this.from = from;
        this.to = to;
        this.current = this.from;
    }

    void forEachRemaining(Consumer<? super IPv4> action) {
        while (this.tryAdvance(ip -> System.out.printf("%s: %s%n", Thread.currentThread().getName(), ip)));
    }

    /**
     * @see Spliterator#tryAdvance(Consumer)
     */
    boolean tryAdvance(Consumer<? super IPv4> action) {
        Optional<IPv4> ip = getNext();
        if (ip.isEmpty()) {
            return false;
        }
        action.accept(ip.get());
        return true;

    }

    public synchronized Optional<IPv4> getNext() {
        if (!hasNext()) {
            return Optional.empty();
        }
        return Optional.of(next());
    }

    private IPv4 next() {
        if (hasNext()) {
            IPv4 next = current;
            current = current.increment();
            return next;
        }
        throw new NoSuchElementException("There is no next IP");
    }

    private boolean hasNext() {
        return current.compareTo(to) <= 0;
    }

}

Java:
public class Main {

    public static void main(String[] args) {

        IPv4 from = new IPv4("192.168.0.1");
        IPv4 to = new IPv4("192.168.1.0");

        IPv4Range range = new IPv4Range(from, to);

        Runnable withOptional = new Runnable() {
            @Override
            public void run() {
                for (Optional<IPv4> ip = range.getNext(); ip.isPresent(); ip = range.getNext()) {
                    System.out.printf("%s: %s%n", Thread.currentThread().getName(), ip.get());
                }
            }
        };

        Runnable withTryAdvance = new Runnable() {
            @Override
            public void run() {
                while (range.tryAdvance(ip -> System.out.printf("%s: %s%n", Thread.currentThread().getName(), ip))) {

                }
            }
        };
        Runnable withForEachRemaining = new Runnable() {
            @Override
            public void run() {
                range.forEachRemaining(ip -> System.out.printf("%s: %s%n", Thread.currentThread().getName(), ip));
            }
        };

        new Thread(withOptional).start();
        new Thread(withTryAdvance).start();
        new Thread(withForEachRemaining).start();
    }

}
 
H

httpdigest

Hier eine Lösung mit einer einzigen next() Methode und AtomicReference (man könnte auch AtomicInteger nehmen, aber ich wollte nicht den int der Ipv4 nach außen geben - ach ja, es muss kein long sein, ein int reicht; sind ja nur 4 Bytes) plus noch ein paar Hilfsmethoden:
Java:
import java.util.NoSuchElementException;
public class Ipv4 implements Comparable<Ipv4> {
  private final int value;
  public Ipv4(String value) {
    String[] parts = value.split("\\.");
    if (parts.length != 4) {
      throw new IllegalArgumentException("IP string has no valid length");
    }
    int temp = 0;
    for (String part : parts) {
      int parti = Integer.parseInt(part);
      if (parti < 0 || parti > 255)
        throw new IllegalArgumentException("IP part has invalid value: " + part);
      temp = (temp << 8) + parti;
    }
    this.value = temp;
  }
  public Ipv4(int value) {
    this.value = value;
  }
  public Ipv4Range to(Ipv4 end) {
    return new Ipv4Range(this, end);
  }
  public Ipv4 next() {
    if (value == -1)
      throw new NoSuchElementException();
    return new Ipv4(value + 1);
  }
  @Override
  public String toString() {
    return "Ipv4 {" + (value >> 24 & 0xFF)
          + "." + (value >> 16 & 0xFF)
          + "." + (value >> 8  & 0xFF)
          + "." + (value       & 0xFF) + "}";
  }
  @Override
  public int compareTo(Ipv4 o) {
    return Integer.compare(value, o.value);
  }
}
Java:
import java.util.Optional;
import java.util.concurrent.atomic.AtomicReference;
public class Ipv4Range {
  private final Ipv4 to;
  private final AtomicReference<Ipv4> current;
  public Ipv4Range(Ipv4 from, Ipv4 to) {
    this.to = to;
    this.current = new AtomicReference<Ipv4>(from);
  }
  /**
   * @return the next {@link Ipv4} in this range;
   *         or <code>empty</code> if all Ipv4 instances
   *         of this range have been returned
   */
  public Optional<Ipv4> next() {
    Ipv4 curr, next;
    do {
      if ((curr = current.get()).compareTo(to) >= 0)
        return Optional.empty();
    } while (!current.compareAndSet(curr, next = curr.next()));
    return Optional.of(next);
  }
}
Java:
public class Main {
  public static void main(String[] args) {
    Ipv4Range range = new Ipv4("192.168.0.1").to(new Ipv4("192.168.4.25"));
    Runnable r1 = () -> {
      Ipv4 next = null;
      while ((next = range.next()) != null) {
        System.out.println(Thread.currentThread() + " -> " + next);
      }
    };
    for (int i = 0; i < 8; i++)
      new Thread(r1).start();
  }
}
 
Zuletzt bearbeitet:
K

kneitzel

Also die Lösungen sind doch alle schon recht schön aufbereitet und dem kann ich nur wenig beisteuern.

Eine Lösung, die mir durch den Kopf geht und ich kurz anmerken möchte, gerade weil sie einfach und schnell ohne diese hohe Komplexität zu schreiben ist: Nutzung von Stream.
- Man will etwas für jede IP aus einerm IP-Bereich machen: Hier kann man die IP Adressen schön als Integer Werte sehen, so dass man ein einfachen Stream von Ints hat
- Diesen Stream kann man nun parallel abarbeiten dank .parallel()
- Die eigentliche Aktion landet dann in .foreach verarbeiten.

Eine solche Lösung bietet sich hier durchaus an, da der eigentliche Task relativ einfach ist und es zwischen den Tasks keine Abhängigkeiten / Wechselwirkungen gibt.

So wird aus der Thematik ein relativ simpler, verständlicher Code, der aus wenigen Methoden und eben der Stream Lösung besteht.

Bezüglich dieser Lösung würde ich einfach einmal folgenden Link anführen (Angelika Langer hat aber diesbezüglich deutlich mehr geschrieben, aber das ist ein interessanter Artikel von Ihr. Generell findet sich bei Ihr aber sehr viel Interessantes.
http://www.angelikalanger.com/Articles/EffectiveJava/82.Java8.Performance-Model-of-Streams/82.Java8.Performance-Model-of-Streams.html

Viele Grüße,

Konrad
 
T

Tobias-nrw

zufrieden bin ich mit dem alle dem noch nicht, da Synchronisierung und Streams Zeit kosten und durch Parallelisierung eigentlich diese eingespart werden sollte. Jetzt ist die eigentliche Rechenoperation nur minimal (nur eine Ausgabe), aber es wär durchaus ja möglich, auch intensivere Operationen parallelisieren zu müssen...

Bearbeitung: Ich bin jetzt Kuchenessen....
 
M

mrBrown

- Man will etwas für jede IP aus einerm IP-Bereich machen: Hier kann man die IP Adressen schön als Integer Werte sehen, so dass man ein einfachen Stream von Ints hat
Wobei man es ja eigentlich vermeiden will, IPs nur als int zu repräsentieren (zumindest ich), einen Stream von ints würde ich daher als Nachteil sehen
 
M

mrBrown

zufrieden bin ich mit dem alle dem noch nicht, da Synchronisierung und Streams Zeit kosten und durch Parallelisierung eigentlich diese eingespart werden sollte. Jetzt ist die eigentliche Rechenoperation nur minimal (nur eine Ausgabe), aber es wär durchaus ja möglich, auch intensivere Operationen parallelisieren zu müssen...

Bearbeitung: Ich bin jetzt Kuchenessen....
Wenn du keinerlei synchronisierung willst, Teil das vorher in passende Bereiche für jeden Thread auf, dann kann jeder unabhängig von den anderen arbeiten.

(Hat allerdings auch Nachteile, falls ein Thread signifikant länger braucht, alle anderen sind dann fertig und müssen auf diesen einen warten)
 
K

kneitzel

Wobei man es ja eigentlich vermeiden will, IPs nur als int zu repräsentieren (zumindest ich), einen Stream von ints würde ich daher als Nachteil sehen
Es geht ja hier nicht darum, wie es ausgegeben wird. Ausgeben würde ich eine IP auch immer rein die typischen 4 Zählen, getrennt durch Punkte. Aber intern sind und bleiben es eben genau die 4 Bytes und mit diesen lässt sich nun einmal sehr gut rechnen.

Aber natürlich: ich würde Code so nicht wirklich stehen lassen. Daten, die etwas bedeuten, werden gekapselt und dann entsprechend benutzt.

Aber generell würde ich bei so Themen halt eine Standard Lösung bevorzugen. Abarbeitung eines Vorrats ist halt eine Thematik, bei der z.B. TheadPools und so durchaus brauchbar sind. Aber bezüglich diverser möglicher Pattern wurde ja schon viel geschrieben, so dass ich da nicht weiter drauf eingehen möchte. Aber die Parallelen Streams sind halt eine einfache, unkomplizierte schnelle Lösung für sowas.
 
T

Tobias-nrw

So hätte ich das mit Streams gemacht:
Java:
import java.net.InetAddress;
import java.net.UnknownHostException;
import java.util.Optional;
import java.util.Scanner;
import java.util.stream.IntStream;

public class Unittest {
  private static Scanner s1;
  private static String p1, p2, p3;

  public static int ipToInt(String ip) throws UnknownHostException {
    byte[] a = InetAddress.getByName(ip).getAddress();
    return (a[0] << 24) | (a[1] << 16) | (a[2] << 8) | (a[3] << 0);
  }

  public static String ipToString(int ip) {
    return String.format("%d.%d.%d.%d", (ip >>> 24) & 0xFF, (ip >>> 16) & 0xFF, (ip >>> 8) & 0xFF, (ip >>> 0) & 0xFF);
  }

  public static synchronized Optional<String> getNext() {
    if (p3.equals(p2)) {
      return Optional.empty();
    }
    try {
      p3 = ipToString(ipToInt(p3) + 1);
    } catch (UnknownHostException e) {
      e.printStackTrace();
    }
    return Optional.of(p3);
  }

  public static void main(String[] args) throws UnknownHostException {
    s1 = new Scanner(System.in);
    System.out.println("Adresse 1: ");
    p1 = ipToString(ipToInt(s1.nextLine()) - 1);
    System.out.println("Adresse 2: ");
    p2 = s1.nextLine();
    p3 = p1;

    IntStream.rangeClosed(0, ipToInt(p2) - ipToInt(p1)).mapToObj(e -> getNext()).filter(e -> e.isPresent())
        .forEach(System.out::println);

    p3 = p1;
    IntStream.rangeClosed(0, ipToInt(p2) - ipToInt(p1)).parallel().mapToObj(e -> getNext()).filter(e -> e.isPresent())
        .forEach(System.out::println);
  }
}

Code:
Adresse 1: 
255.255.255.250
Adresse 2: 
255.255.255.255
Optional[255.255.255.250]
Optional[255.255.255.251]
Optional[255.255.255.252]
Optional[255.255.255.253]
Optional[255.255.255.254]
Optional[255.255.255.255]
Optional[255.255.255.250]
Optional[255.255.255.251]
Optional[255.255.255.252]
Optional[255.255.255.253]
Optional[255.255.255.254]
Optional[255.255.255.255]

Jetzt kann System.out::println eine sehr lange Operation sein wie zB Thread.sleep(100); dann würd sequential 600ms brauchen und parallel würd theoretisch nur 100ms brauchen... Richtig?
 
M

mrBrown

Jetzt kann System.out::println eine sehr lange Operation sein wie zB Thread.sleep(100); dann würd sequential 600ms brauchen und parallel würd theoretisch nur 100ms brauchen... Richtig?
War bei den anderen Varianten hier auch schon so :)


Wobei halt die Qualität des Codes mal wieder unterirdisch ist...
 
K

kneitzel

Wobei mein Streams Ansatz sogar deutlich weniger umfasst hätte, denn ich hätte da tatsächlich versucht es auf weniger zu konzentrieren. (Sprich: ohne viel drumherum für jede IP einen Consumer starten, Filter & Co würde ich da nicht betrachten. (Da kommen mir irgendwie zu viele Dinge in ein kurzes Stück Code ... ich teile gerne etwas mehr auf.)

Aber über die Sinnhaftigkeit bin ich mir selbst noch nicht ganz im Klaren. Hier im kleinen ist es ein einfacher Weg für die parallele Ausführung ohne viel Code schreiben zu müssen. Aber Java wird ja auch Thread Pools und so kennen und in einer Applikation will man ggf. ja mehr einstellen an Tasks während die anderen noch abgearbeitet werden. Daher fürchte ich, dass mir so eine Lösung evtl. Flexibilität nehmen könnte. Im Kleinen aber durchaus interessant.

Falls ich es heute Abend noch einmal an den Rechner schaffe, dann würde ich mal den Code basteln, wie ich es mir gedacht habe.. so am Tablet ist das gerade nicht wirklich möglich.
 
K

kneitzel

Also um einmal zu skizzieren, was mir so als Idee vorgeschwebt ist:

Java:
import java.net.Inet4Address;
import java.net.UnknownHostException;
import java.nio.ByteBuffer;
import java.util.function.Consumer;
import java.util.stream.IntStream;

public class ParallelInet4Action {

    Inet4Address startIp;
    Inet4Address endIp;

    public ParallelInet4Action(final Inet4Address startIp, final Inet4Address endIp) {
        if (intFromIp(endIp) < intFromIp(startIp)) throw new IllegalArgumentException("Start IP was before End IP!");

        this.startIp = startIp;
        this.endIp = endIp;
    }

    public void doAction(final Consumer<Inet4Address> action) {
        IntStream
                .range(intFromIp(startIp), intFromIp(endIp))
                .parallel()
                .forEach(i -> action.accept(ipFromInt(i)));
    }

    private int intFromIp(final Inet4Address ip) {
        return ByteBuffer.wrap(ip.getAddress()).getInt();
    }

    private Inet4Address ipFromInt(final int ip) {
        try {
            return (Inet4Address) Inet4Address.getByAddress(ByteBuffer.allocate(4).putInt(ip).array());
        } catch (UnknownHostException ex) {
            System.out.println("Unknown Host Exception - given ip address was invalid!");
            throw new IllegalStateException("Given Address was invalid!");
        }
    }

    public static void main (String[] args) throws UnknownHostException {
        Inet4Address start = (Inet4Address) Inet4Address.getByAddress( new byte[]{127, 1, 2, 3});
        Inet4Address end = (Inet4Address) Inet4Address.getByAddress( new byte[]{127, 1, 2, 18});

        ParallelInet4Action action = new ParallelInet4Action(start, end);
        action.doAction(i -> System.out.println(i));
    }
}
Das ist jetzt einfach eine Klasse, dessen Instanz eine Start und eine End IP hat. Und auf dieser IP Range kann dann eine Action ausgeführt werden, welche als Consumer angegeben wird dem eine Inet4Address übergeben wird.

Die main ist nur ein ganz kleines Beispiel - statt der Ausgabe kann da dann halt die Erreichbarkeit geprüft werden mir Ausgabe oder was auch immer gewünscht wird.
 
T

Tobias-nrw

Danke @kneitzel für Deine Bemühungen. Was an #47 unschön ist... Das try catch dürfte dort nicht stehen, potentiell könnte das das Programm nicht anhalten.

Eigentlich bräuchte man 4 Klassen/Interfaces, eine eine IP repräsentierende, einen Producer (intFromIp), einen Consumer (doAction) und eine für die Eingabe.
 
K

kneitzel

Hallo Tobias,

über diese Exception bin ich ja auch gestolpert und ich habe da etwas mehr drüber nachgedacht.

Bei mir ist es noch relativ einfach, da ich mit einem Byte Array heran gehe. Und das Byte Array vom Integer hat immer die Länge 4 und damit kann bei mir die Exception nicht geworfen werden.

Bei Dir ist es aber genau so. Die Eingabe wird geprüft in main - da kann die Exception dann geworfen werden.
Danach hast Du aber generell valide IP Adressen und getByName kann hier keine Exception mehr werfen, denn bei einer ip Adresse wird diese lediglich validiert. Und da Du da ja auch über einen int arbeitest, wirst Du da auch immer 4 Zahlen in der ip haben, die gültig sind.

Daher ist es aus meiner Sicht gar kein wirkliches Problem. Man könnte da aber natürlich auch drüber nachdenken, dass man in dem Fall eben nichts behandeln kann und dann auch eine (unchecked) Exception wirft (So bei einer Veränderung des Codes ein Fehler in den Code kommen sollte, geht dieser Fehlerfall dann nicht verloren).
 
K

kneitzel

Danke für den Hinweis. :)
Gerne, waren halt gestern Abend meine Gedanken dazu. Kann man bestimmt geteilter Meinung zu sein, wie man sowas behandelt.

Eine andere Überlegung wäre hier ggf. ein Error zu loggen um dann ggf.mit einem default weiter zu machen. Also in Deinem Code wäre ein Optional.empty als Rückgabe evtl. denkbar.

Wobei mir in meinem Code (#51) noch aufgefallen ist: Der neuen Exception sollte man natürlich die gefangene Exception als cause mitgeben. Das habe ich da ganz vergessen.
 
Thema: 

Zwei Threads, aber doppelte Ausgabe verhindern (synchronized)

Passende Stellenanzeigen aus deiner Region:
Anzeige

Neue Themen

Anzeige

Anzeige
Oben