# JList + DefaultListModel = Race Condition :(



## hdi (21. Okt 2008)

Hey...

mir ist grad mehr durch Zufall etwas aufgefallen, worüber ich einfach mal sagen würde, es ist 
ne Race Condition (tritt manchmal auf, manchmal nicht).

Um was geht's:

Ich habe eine JList und ein JListModel. Das Model verändere ich während der Laufzeit, ich füge
Nachrichten hinzu (das Model ist eine Art Logger).

Das tue ich mittels


```
Log.say("Meine Nachricht");  // Log ist dieses Model (extends DefaultListModel)
```

Klappt auch soweit, die JList ist an ein JFrame gekoppelt und zeigt diese Nachrichten dann an.

Nur wenn ich ganz schnell hintereinander zweimal diese say()-Methode aufrufe, passiert es manchmal (!),
dass der komplette Fensterinhalt, also alle Log-Nachrichten, verschwinden -> das Fenster (die Liste??) ist leer...

Ich kann mir das überhaupt nicht erklären, scheinbar ist ihm das einfach "zu schnell".
Ich habe alle Methoden, die meiner Meinung nach mit diesem ganzen Observern-Pattern zu tun haben,
mit dem Schlüsselwort synchronized versehen -> Kein Effekt 

Ich glaube ich kann es euch nicht ersparen, diesen Teil meines Programms zu posten...


```
public class Log {

	private static LogListModel model = new LogListModel(new LogConsole());

	public static synchronized void say(String message) {
		model.addElement(message);
	}
}
```


```
public class LogListModel extends DefaultListModel {

	private ArrayList<LogEntry> entries;
	private LogConsole myConsole;

	public LogListModel(LogConsole console) {

		entries = new ArrayList<LogEntry>();
		myConsole = console;  // das ist einfach das JFrame indem alles angezeigt wird.
		myConsole.getList().setModel(this);  // hier wird der JList eben dieses Model übergeben!
		myConsole.setVisible(true);
	}

	@Override
	public synchronized Object getElementAt(int index) {
		return entries.get(index).getMessage();

	}

	public synchronized String getMoreFrom(int index) {
		return entries.get(index).getMore();
	}

	@Override
	public synchronized void addElement(Object o) {
		super.addElement(entries.add(new LogEntry((String) o)));
	}
}
```

An der JList selbst hab ich nix gemacht, is ne ganz stinknormale new JList(), die eben als Model
mein LogListModel bekommen hat... Keine Ahnung, warum dieser Effekt manchmal auftritt.
Wie gesagt nur, wenn ich zwei mal sofort hintereinander mittels Log.say() etwas logge...

Was vllt auch noch eine hilfreiche Info sein könnte: Die JList wiederum liegt auf einem JScrollPane, das in 
das Fenster gelegt wurde:


```
public class LogConsole extends JFrame {

	private JList list;
	private JScrollPane pane;

	public LogConsole() {

		list = new LogConsoleList();
		pane = new LogConsoleScrollPane(list);

		getContentPane().add(pane);
		pack();
	}

        // ...
}
```


Hat jemand eine Idee, was da los ist?  ???:L


----------



## Gast (21. Okt 2008)

Ist das ein externer Thread der die Nachrichten einfügt (Log.say())?


----------



## hdi (21. Okt 2008)

ja das ist ein extra dafür gestarteter Thread. (ich weiss jetz nich so genau was du mit "extern" meinst, also 
ich hab schon eine neue Instanz eines Threads mittels new und start() erzeugt)
in seiner run() Methode ruft der dann diese say-Methoden auf...


----------



## Beni (21. Okt 2008)

Das ist nicht gut, auf die GUI sollte man nur im EventDispatcherThread (EDT) zugreiffen. Swing ist nicht darauf ausgelegt mit mehreren Threads zu arbeiten, deshalb kommt deine Liste vermutlich durcheinander wenn da ein extra Thread reinschreibt.

Du kannst das machen mit:

```
Runnable run = new Runnable(){
  public void run(){
    model.addElement( ... );
  }
};
EventQueue.invokeLater( run );
```


----------



## hdi (22. Okt 2008)

Oh  :shock:  Das ist sehr, sehr schlecht 

Ich hab nämlich später im Programm mehrere Threads, die da alles mögliche loggen...

Und ich hab mir jetzt gedacht, nagut dann starte ich halt ab jetzt jeden Thread so:


```
EventQueue.invokeLater ( new EinThreadDenIchStartenWill() );
```
und rufe das "start()" dann im Konstruktor des Threads auf...
Aber was passiert: NIX! Mein Fenster, das der Thread erstellen sollte, wird nicht erstellt...

Ich habe es auch so probiert:


```
EinThread = new EinThread();
EventQueue.invokeLater ( EinThread );
EinThread.start();
```

...sowie das gleiche nochmal aber das start() vor dem invokeLater() aufgerufen. 
Ergebnis in beiden Fällen:
Das Fenster erscheint zwar diesmal ansatzweise, aber total verbuggt, ohne Inhalt und nix passiert.

Heisst das jetzt, mein komplettes LogSystem woran ich schon seit Tagen rumtu, kann ich wegwerfen,
weil nich mehrere Threads das benutzen können wenn ich da Swing Komponenten hab ?

Das wär bitter :autsch: Wie macht man sowas denn? Ich meine... es gibt doch viele Anwendungen (eig. doch 
so ziemlich alle?) die mit GUI's arbeiten und trotzdem mehrere Threads haben die irgendwas da verändern etc...


----------



## Gast (22. Okt 2008)

Nein, du musst nur wie Beni geschrieben hat den Aufruf am Model in ein Runnable verpacken und an invokeLater übergeben.
Dann wird das vom Event Dispatcher Thread für dich ausgeführt.


----------



## hdi (22. Okt 2008)

Das geht nicht... Das Fenster baut sich halb auf und bleibt dann hängen mit einer verbuggten Anzeigen und es ist Schluss... es liegt wohl am sleep() hab ich grad rausgefunden...Das hängt total alles.. :


```
import log.Log;

public class LogDemo implements Runnable {
	
	public void run() {
		
		Log.say("WILLKOMMEN ZUR LOG-DEMO !");
		sleep(2000);
		Log.say("Los geht's...");
		sleep(1000);
		Log.say("Das ganze funktioniert in Realtime...");
		sleep(3000);
		Log.say("...und es wird automatisch mitgescrollt:");
		sleep(2500);
		
		for(int i = 0; i<15; i++) {
			Log.say("neue nachricht kommt...");
			sleep(200);
		}
		Log.say("Falls eine Nachricht zu lang ist, wird eine horizontale Scrollbar eingeblendet. (Normalerweise sollte der Platz aber für 'echte' Log-Nachrichten reichen)");
		sleep(9000);
		Log.say("Achso...");
		sleep(2000);
		Log.say("Es gibt auch eine Rollover Funktion.");
		sleep(3000);
		Log.say("Die zeigt genaueres über eine Nachricht");
		sleep(3000);
		Log.say("Einfach mal ausprobieren!");
		Log.say("Das war's!");
	}
	
	public static void sleep(int millis) {
		try {
			Thread.sleep(millis);
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
	}
}
```

Hab ich das Sleep falsch überschrieben? Wenn ich von Thread erbe, dann funktioniert das... Wieso geht es jetzt
nicht mehr? Ich meine es ist ja ein Thread auch wenn ich das Interface Runnable implementier, is doch eig. das gleiche oder nicht. 
Wenn ich nur eine sleep-Anweisung hab geht zB alles, aber er kommt scheinbar mit sovielen sleep-anweisungen nicht zurecht. woran liegt das?


----------



## Marco13 (22. Okt 2008)

```
EventQueue.invokeLater ( new EinThreadDenIchStartenWill() );
```

Nee - eigentlich kann man der Methode eh keinen Thread übergeben, sondern nur ein Runnable, aber das wäre ja auch murks: Die Methode führt das, was ihr übergeben wird, ja im Event-Dispatch-Thread aus - d.h. dann würde der EDT alles machen, was _eigentlich_ deine anderen Threads erledigen sollten.

Auch hier gilt: Du brauchst (eigentlich, wenn du dalles sauber getrennt hast/hättest) nirgendwo irgendwas an deinem eigentlichen Log-System zu ändern - NUR der Aufruf 

```
void say(String s)
{
    LogEntry logEntry = new LogEntry(s);
    listModel.addElement(logEntry);
}
```

muss geändert werden, so wie Beni geschrieben hat, bzw in

```
void say(String s)
{
    final LogEntry logEntry = new LogEntry(s);
    SwingUtilities.invokeLater(new Runnable()
    {
        public void run()
        {
            listModel.addElement(logEntry);
        }
    });
}
```


----------



## hdi (22. Okt 2008)

Ah okay, hatte das auf die Threads selbst bezogen, hab's jetzt geändert.
bekomme allerdings ein Problem:

Wenn ich so ein neues LogEntry erstelle, wird darin folgendes aufgerufen:


```
StackTraceElement ste = Thread.currentThread().getStackTrace()[4];
```

Bisher waren die Infos eben an Index 4, dadurch bedingt wie halt meine Log-Klassen
zusammenhängen.

Jetzt bekomme ich outOfBounds. Wohl deshalb, da ich einen neuen Thread über
SwingUtilities starte, und Thread.currentThread() mir nun diesen liefert.

Problem: Verringere ich den Index, bekomme ich zwar keine OutOfBound Nachricht mehr,
aber in einer Endlosschleife sowas hier:



> Exception in thread "AWT-EventQueue-0" java.lang.ClassCastException: log.LogEntry cannot be cast to java.lang.String
> at log.LogListModel.addElement(LogListModel.java:31)
> at log.Log$1.run(Log.java:14)
> at java.awt.event.InvocationEvent.dispatch(Unknown Source)
> ...



Das passiert, egal welchen Index <4 ich ihm übergebe. Warum?

Und das führt mich zu einer weiteren Frage: Bekomme ich aus dem LogEntry heraus dann so
überhaupt noch die Information, welche Methode denn das say() wirklich aufgerufen hat?

Oder muss ich die o.g. Zeile komplett ändern und nix mit currentThread machen, sondern sonst
irgendwie wieder auf meine ursprünglich das say() aufrufende Methode kommen?


----------



## Marco13 (22. Okt 2008)

Poste am besten was, was man mit Copy&Paste direkt rauskopieren und compilieren kann.


----------



## hdi (22. Okt 2008)

okay hier is mein komplettes programm in einer klasse, kann man so ausführen.
der Fehler tritt in der Klasse LogEntry auf, beim StackTrace...


```
import javax.swing.SwingUtilities;
import java.util.ArrayList;
import javax.swing.DefaultListModel;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.awt.Component;
import javax.swing.JScrollPane;
import java.awt.Color;
import java.awt.Font;
import javax.swing.JList;
import java.awt.Dimension;
import java.awt.GraphicsEnvironment;
import java.awt.Rectangle;
import javax.swing.JFrame;
import javax.swing.JList;
import javax.swing.JScrollPane;

public class Main {
	public static void main(String[] args) {

		new LogDemo().start();
	}
}
class LogDemo extends Thread {
	
	public void run() {
		
		Log.say("erste nachricht");
		sleep(200);
		Log.say("zweite...");
		Log.say("..und dritte");

	}
	
	public static void sleep(int millis) {
		try {
			Thread.sleep(millis);
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
	}
}
class LogListModel extends DefaultListModel {

	private ArrayList<LogEntry> entries;
	private LogConsole myConsole;

	public LogListModel(LogConsole console) {

		entries = new ArrayList<LogEntry>();
		myConsole = console;
		myConsole.getList().setModel(this);
		myConsole.setVisible(true);
	}

	@Override
	public synchronized Object getElementAt(int index) {
		return entries.get(index).getMessage();

	}

	public synchronized String getMoreFrom(int index) {
		return entries.get(index).getMore();
	}

	@Override
	public synchronized void addElement(Object o) {
		super.addElement(entries.add(new LogEntry((String) o)));
	}
}
class LogEntry {

	private final String MESSAGE;
	private final String MOREINFO;

	public LogEntry(String message) {

		this.MESSAGE = message;

		StackTraceElement ste = Thread.currentThread().getStackTrace()[1];
		String caller = ste.getClassName().split("\\.")[1];
		String method = ste.getMethodName();
		int line = ste.getLineNumber();
		SimpleDateFormat formatter = new SimpleDateFormat("HH:mm:ss");
		String time = formatter.format(new Date());

		this.MOREINFO = time + " > " + caller + " : " + method + " (" + line
				+ ")";
	}

	public String getMessage() {
		return MESSAGE;
	}

	public String getMore() {
		return MOREINFO;
	}
}
class LogConsoleScrollPane extends JScrollPane {

	public LogConsoleScrollPane(Component list) {

		setViewportView(list);
		setVerticalScrollBarPolicy(VERTICAL_SCROLLBAR_ALWAYS);
		setHorizontalScrollBarPolicy(HORIZONTAL_SCROLLBAR_NEVER);

		// die methode finden, die die liste synchronisiert, und
		// die scrollbar nachziehen in dieser methode, und immer die
		// horizontale ganz nach links setzen!
	}
}
class LogConsoleList extends JList {

	public LogConsoleList() {
		setBackground(Color.lightGray);
		setForeground(Color.black);
		setFont(new Font("Arial", Font.PLAIN, 10));
	}

//	@Override
//	public synchronized String getToolTipText(MouseEvent evt) {
//
//		int index = locationToIndex(evt.getPoint());
//		String item = ((LogListModel) getModel()).getMoreFrom(index);
//		return item;
//	}
	
	// ToolTips anpassen !!!
}
class LogConsole extends JFrame {

	private final int X_DIM = 220;
	private final int Y_DIM = 200;
	private JList list;
	private JScrollPane pane;

	public LogConsole() {

		setTitle("LogConsole");
		setPreferredSize(new Dimension(X_DIM, Y_DIM));
		setAlwaysOnTop(true);
		setResizable(false);
		setDefaultCloseOperation(DO_NOTHING_ON_CLOSE);
		list = new LogConsoleList();
		pane = new LogConsoleScrollPane(list);
		getContentPane().add(pane);
		pack();
		setLocationByTray();
	}

	private void setLocationByTray() {
		int width, height;
		Rectangle visibleDesktop = GraphicsEnvironment
				.getLocalGraphicsEnvironment().getMaximumWindowBounds();
		width = (int) visibleDesktop.getWidth();
		height = (int) visibleDesktop.getHeight();
		this.setLocation(width - X_DIM, height - Y_DIM);
	}

	public JList getList() {
		return list;
	}
}
class Log {

	private static LogListModel model = new LogListModel(new LogConsole());

	public static synchronized void say(String message) {
		{
			final LogEntry logEntry = new LogEntry(message);
			SwingUtilities.invokeLater(new Runnable() {
				public void run() {
					model.addElement(logEntry);
				}
			});
		}
	}
}
```


----------



## Lim_Dul (22. Okt 2008)

Zeile 69:

```
super.addElement(entries.add(new LogEntry((String) o)));
```
Warum erzeugt du da eines Neues LogEntry Objekt? Du bekommst doch ein LogEntry Objekt rein. Du kannst das einfach hinzufügen.


----------



## hdi (22. Okt 2008)

ah, hatte ich jetzt vergessen zu ändern nachdem ich die say() methode geändert hatte...

okay jetzt geht's, danke


----------

