这个Java线程锁定概念不好吗?

时间:2019-03-16 16:26:41

标签: java multithreading locking executorservice

我将尝试通过一个示例简要解释我提出的线程锁定概念。考虑以下示例程序。

public class Main {
    public static void main(String[] args) {
        Data data = new Data();

        while (true) {
            doStuff();
            doStuff();

            for (int i = 0; i < 256; i++) {
                System.out.println("Data " + i + ": " + data.get(i));
            }

            doStuff();
            doStuff();

            for (int i = 0; i < 256; i++) {
                data.set(i, (byte) (data.get(i) + 1));
            }

            doStuff();
            doStuff();
        }
    }

    public static void doStuff() {
        try {
            Thread.sleep(10);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

public class Data {
    private final byte[] data = new byte[256];

    public byte get(int i) {
        return data[i];
    }

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

重要的是只有主线程才能修改data。现在,我要使打印data的循环是异步的。

    public static void main(String[] args) {
        ExecutorService executorService = Executors.newSingleThreadExecutor();
        Data data = new Data();

        while (true) {
            doStuff();
            doStuff();

            executorService.submit(new Runnable() {
                @Override
                public void run() {
                    for (int i = 0; i < 256; i++) {
                        System.out.println("Data " + i + ": " + data.get(i));
                    }
                }
            });

            doStuff();
            doStuff();

            for (int i = 0; i < 256; i++) {
                data.set(i, (byte) (data.get(i) + 1));
            }

            doStuff();
            doStuff();
        }
    }

在将任务提交到executorService之后,主线程现在可以根据需要继续工作。问题是,主线程可能会到达在打印之前修改data的地步,但是在提交时应打印data的状态。

我知道在这种情况下,我可以在提交打印之前创建data的副本,但这实际上不是我想要的。请记住,这只是一个示例,在实际代码中复制可能是一项昂贵的操作。

这是我针对此问题提出的解决方案。

    public static void main(String[] args) {
        ExecutorService executorService = Executors.newSingleThreadExecutor();
        Data data = new Data();
        Lock lock = new Lock(); // <---------------

        while (true) {
            doStuff();
            doStuff();

            lock.lock(); // <---------------
            executorService.submit(new Runnable() {
                @Override
                public void run() {
                    for (int i = 0; i < 256; i++) {
                        System.out.println("Data " + i + ": " + data.get(i));
                    }

                    lock.unlock(); // <---------------
                }
            });

            doStuff();
            doStuff();

            lock.waitUntilUnlock(); // <---------------
            for (int i = 0; i < 256; i++) {
                data.set(i, (byte) (data.get(i) + 1));
            }

            doStuff();
            doStuff();
        }
    }

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

    public void lock() {
        lockCount.incrementAndGet();
    }

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

    public synchronized void waitUntilUnlock() {
        while (lockCount.get() > 0) {
            try {
                wait();
            } catch (InterruptedException e) {

            }
        }
    }
}

现在,提交data之后,主线程可以继续处理其他内容。至少它可以直到修改data为止。

问题:这是好设计还是坏设计?还是针对此问题有更好的(已经存在的)实现?

请注意,ReentrantLock在这种情况下不起作用。在提交主线程之前,我必须先锁定,然后在执行者线程上释放该锁。

4 个答案:

答案 0 :(得分:3)

Java具有更高级别的同步抽象。通常,您应该避免使用wait()和notifyAll(),它们太低级且太复杂,无法正确使用和读取。

在这种情况下,您可以只在两个线程之间使用共享的阻塞队列(synchronous queue对我来说合适)

    ExecutorService executorService = Executors.newSingleThreadExecutor();
    Data data = new Data();
    SynchronousQueue queue = new SynchronousQueue();

    while (true) {
        doStuff();
        doStuff();

        executorService.submit(new Runnable() {
            @Override
            public void run() {
                for (int i = 0; i < 256; i++) {
                    System.out.println("Data " + i + ": " + data.get(i));
                }
                queue.put(data);
            }
        });

        doStuff();
        doStuff();

        data = queue.take();
        for (int i = 0; i < 256; i++) {
            data.set(i, (byte) (data.get(i) + 1));
        }

        doStuff();
        doStuff();
    }

答案 1 :(得分:1)

您希望此Data事物异步生成,并且主线程希望能够继续进行到特定点,然后需要获取完成的对象。这就是期货的用途,它为您提供了可能尚未完成的计算的参考。

将异步部分重写为Callable,以便其返回数据作为结果。

Callable<Integer> task = () -> {
    Data data = new Data();
    for (int i = 0; i < 256; i++) {
        System.out.println("Data " + i + ": " + data.get(i));
    }
    return data;
};

ExecutorService executor = Executors.newFixedThreadPool(1);
Future<Data> future = executor.submit(task);
doStuff();
// ... main thread goes about its business

// when you get to a point where you need data, 
// you can block here until the computation is done
Data data = future.get(); 

通过这种方式,Future可以确保确保Data对象在线程间可见。

答案 2 :(得分:0)

我找到了解决该问题的另一种方法,该方法也回答了@JB Nizet的回答。 ExecutorService#submit(Runnable)返回Future<?>,该Queue<Future<?>> queue可用于等待任务准备就绪。如果应该进行多个提交,则可以只创建一个Future<?>,始终将ExecutorService#submit(Runnable)返回的queue提供给#poll().get(),并且在主线程应该等待的时候queue整个 // if enemy runs into player will return true public static boolean Collision (EntityEnemy enemyShips, EntityPlayer entP ) { if (enemyShips.getBounds().intersects(entP.getBounds())) { return true; } return false; } // returning two enemies colliding with the player public static boolean doubleCollision (EntityEnemy enemyShips, EntityPlayer entP ) { if (enemyShips.getBounds().intersects(entP.getBounds())) { count ++; if (count == 2) { return true; } } return false; }

编辑:我也在这里找到了相关的答案:https://stackoverflow.com/a/20496115/3882565

答案 3 :(得分:-1)

基本同步似乎已经很好地覆盖了这一点:doStuff希望能够唯一地访问数据,大概是这样,以便它可以安全地整体修改数据。同时,doPrint期望对稳定数据进行操作。在这两种情况下,数据是受访问控制的状态单元,并且适当的锁定技术是在要访问的数据实例上进行同步。

public void runningInThread1() {
    Data someData = getData(); // Obtain the data which this thread is using
    doStuff(someData); // Update the data
    // Looping and such omitted.
}

public void runningInThread2() {
    Data someData = getData();
    doPrint(someData); // Display the data
}

public void doStuff(Data data) {
    synchronized ( data ) {
        // Do some stuff to the data
    }
}

public void doPrint(Data data) {
    synchronized ( data ) {
        // Display the data
    }
}

或者,如果将doStuffdoPrint作为Data的实例方法来实现,则可以通过在实例方法中添加synchronized关键字来实现同步。