简体   繁体   中英

Java thread safe locking

I am modifying a Java server software. The whole application is single threaded. One of my changes takes much time, so I have decided doing it asynchronously to avoid freezing the main-thread.

This is an example of the original code (not the real code, just an example):

public class Packet {
    private final byte[] data = new byte[1024];

    public void setData(int index, byte data) {
        this.data[index] = data;
    }

    public byte getData(int index) {
        return data[index];
    }

    public void sendPacket(ClientConnection clientConnection) {
        clientConnection.sendPacket(data);
    }
}

Currently this is my code (look at the comments):

public class Packet {
    private final byte[] data = new byte[1024];

    public void setData(int index, byte data) {
        synchronized (this) {
            this.data[index] = data;
        }
    }

    public byte getData(int index) {
        return data[index];
    }

    public void sendPacket(final ClientConnection clientConnection) {
        //This state of data should be sent
        new Thread(new Runnable() {
            @Override
            public void run() {
                //The thread is now running
                //The main-thread can move on
                //The main-thread can also modify data now because we are not inside the synchronized block
                //But it should not because the state of data when the method sendPacket was called should be sent
                synchronized (Packet.this) {
                    thisTakesMuchTime(data);
                    clientConnection.sendPacket(data);
                }
            }
        }).start();
    }
}

What I am actually looking for is something like that:

public class Packet {
    private final byte[] data = new byte[1024];

    public void setData(int index, byte data) {
        //wait for unlock
        this.data[index] = data;
    }

    public byte getData(int index) {
        return data[index];
    }

    public void sendPacket(final ClientConnection clientConnection) {
        //lock
        new Thread(new Runnable() {
            @Override
            public void run() {
                thisTakesMuchTime(data);
                clientConnection.sendPacket(data);
                //unlock
            }
        }).start();
    }
}

The question: What is the best implementation of such a lock in Java? Should I do it myself with an AtomicInteger for example.

Edit: Look at my answer for my current implementation.

You can make a copy of your data, and send the copy, to avoid concurrency.

public class Packet {
    private final byte[] data = new byte[1024];

    public void setData(final int index, final byte data) {
        this.data[index] = data;
    }

    public byte getData(final int index) {
        return data[index];
    }

    public void sendPacket(final ClientConnection clientConnection) {
        byte[] dataToSend = new byte[1024];
        System.arraycopy(data, 0, dataToSend, 0, 1024);
        new Thread(new Runnable() {
            @Override public void run() {
                clientConnection.sendPacket(dataToSend);
            }
        }).start();
    }
}

Using CopyOnWriteArrayList is analogous to the code bellow, which also avoids concurrency but is not as efficient (given you'll be calling setData more often than sendPacket ):

public class Packet {
    private byte[] data = new byte[1024];

    public void setData(final int index, final byte data) {
        byte[] newData = new byte[1024];
        System.arraycopy(data, 0, newData, 0, 1024);
        newData[index] = data;
        this.data = newData;
    }

    public byte getData(final int index) {
        return data[index];
    }

    public void sendPacket(final ClientConnection clientConnection) {
        new Thread(new Runnable() {
            @Override public void run() {
                clientConnection.sendPacket(data);
            }
        }).start();
    }
}

The simplest lock you can use is the Reentrant Lock , reentrant meaning that, if you attempt to acquire the lock when you already have it, the operation will succeed.

In your code, to achieve the threading you desire, you will also have to use wait() and notify() to block the main thread until your child thread has acquired the lock:

public class Packet {
    private final ReentrantLock lock = new ReentrantLock();
    private final byte[] data = new byte[1024];

    public void setData(int index, byte data) {
        lock.lock(); //wait for unlock
        try {
            this.data[index] = data;
        } finally {
            lock.unlock();
        }
    }

    public byte getData(int index) {
        return data[index];
    }

    public void sendPacket(final ClientConnection clientConnection) {
        Thread thread = new Thread(new Runnable() {
            @Override
            public void run() {
                lock.lock(); //lock
                try {
                    synchronized(this) {
                        this.notify();
                    }

                    thisTakesMuchTime(data);
                    clientConnection.sendPacket(data);
                } finally { 
                    lock.unlock(); //unlock
                }
            }
        }).start();

        synchronized(thread) {
            try {
                thread.wait();
            } catch (InterruptedException e) {
                //handle
            }
        }
    }
}

Also consider using an ExecutorService and not creating raw Thread objects.

What is the best implementation of such a lock in Java? Should I do it myself with an AtomicInteger for example.

I think @ericbn's answer would work ok. Grabbing a copy of the data using the main thread but still inside of Packet is fine.

However you are worried about the 1k buffer? The real expense here is not creating a copy of the data in the main thread, it's forking the thread each time you go to send the packet. This is tremendously expensive compared to object creation. I'd use a thread-pool and submit packet jobs to it.

// you might want this to be bounded so you don't queue up too many packets
private final ExecutorService threadPool = Executors.newSingleThreadExecutor();
...

public void sendPacket(ClientConnection clientConnection) {
    byte[] dataToWrite = new byte[data.length];
    System.arraycopy(data, 0, dataToWrite, 0, dataToWrite.length);
    threadPool.submit(new PacketToWrite(dataToWrite, clientConnection));
    // you can clear or reset the `data` array now
}

private static class PacketToWrite implements Runnable {
    private final byte[] dataToWrite;
    private final ClientConnection clientConnection;
    public PacketToWrite(byte[] dataToWrite, ClientConnection clientConnection) {
        this.dataToWrite = dataToWrite;
        this.clientConnection = clientConnection;
    }
    public void run() {
        thisTakesMuchTime(data);
        clientConnection.sendPacket(data);
    }
}

You are sending data over the network so the extra object bandwidth is nothing compared to the network lag.

My current implementation:

Packet:

public class Packet {
    private final Lock lock = new Lock();
    private final byte[] data = new byte[1024];

    public void setData(int index, byte data) {
        lock.waitUntilUnlock();
        this.data[index] = data;
    }

    public byte getData(int index) {
        return data[index];
    }

    public void sendPacket(final ClientConnection clientConnection) {
        lock.lock();
        new Thread(new Runnable() { // I use an ExecutorService
            @Override
                public void run() {
                thisTakesMuchTime(data);
                clientConnection.sendPacket(data);
                lock.unlock();
            }
        }).start();
    }
}

Lock:

public class Lock {
    private final AtomicInteger lockCount = new AtomicInteger();

    public void lock() { // Main thread
        lockCount.incrementAndGet();
    }

    public synchronized void unlock() {
        lockCount.decrementAndGet();
        notifyAll();
    }

    public synchronized void waitUntilUnlock() { // Main thread
        try {
            while (lockCount.get() > 0) {
                wait();
            }
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        }
    }
}

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM