Java互斥锁理解和应用实例

前阵子用Java写了一个俄罗斯方块游戏,里面一些功能一开始用了非常浪费算力的纯while+判断条件方法。后来使用了多线程缓解了CPU使用率,但是仍然居高不下,这时,我发现了新大陆——互斥锁。当然了,互斥锁我在学习单片机的时候已经接触过,但是当时没有足够理解。现在使用了互斥锁后,发现可以干很多事情,而且能代替浪费资源的用法。

互斥锁的本意是为了防止多线程之间改写同一个空间时造成冲突的情况,这就有点像数据库里面的乐观锁,能够同时读取但不允许同时写入。利用这个特性,我们可以让某个线程处于“睡眠”的状态,而其他线程则正常工作,当我们需要这个线程工作时,再将它“唤醒”。本文基于互斥特性做了一个“暂停”的操作,至于其他的用法,可以自行研究。

Java中有一个关键字:synchronized,它用于监控一个被加锁对象,这个对象会被赋予wait、notify方法,wait方法用于等待被加锁对象被“唤醒”,notify方法用于唤醒被加锁对象。举个例子:

class MyThread extends Thread {
    @Override
    public void run() {
        synchronized (this) {
            try {
                sleep(1000);
                notify();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

public class WaitTest {
    private static final MyThread thread = new MyThread();

    public static void main(String[] args) throws InterruptedException {
        synchronized (thread) {
            thread.start();
            System.out.println("线程已启动");
            thread.wait();
            System.out.println("等待完成");
        }
    }
}

在此例子中,我们看到thread是作为被加锁对象存在的,线程启动后,main方法“暂停”了,1秒后线程发送通知,main方法才继续进行下去。然而,如果我们注释掉 thread.wait(),会发现main方法没有“暂停”。可见,wait的作用就是等待被加锁对象发送通知。然而,使用线程本身作为被加锁对象有很大的局限性,例如我们需要重新创建一个进程,那就会使得thread不是final属性,也就不能使用互斥锁。此时,我们可以选择在其中一个类中定义一个final属性的对象,这个对象可以是空对象,也可以是有意义的对象,使用上没有影响。例如:

public final Object mutex = new Object();

我们将mutex对象设置为被加锁对象,代码改为:

package second;

class MyThread extends Thread {
    public final Object mutex = new Object();

    @Override
    public void run() {
        synchronized (mutex) {
            try {
                sleep(1000);
                mutex.notify();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

public class WaitTest {

    public static void main(String[] args) throws InterruptedException {
        MyThread thread = new MyThread();
        for (int i=0;i<10;i++) {
            synchronized (thread.mutex) {
                thread.start();
                System.out.println("线程已启动");
                thread.mutex.wait();
                System.out.println("等待完成");
                thread = new MyThread();
                System.out.println("重新创建进程");
            }
        }
    }
}

从上面的代码,我们知道wait方法并不是等待线程响应,而是等待被加锁方法通知。代码每1秒重新创建一个对象,没有发送冲突,而如果我们将thread作为被加锁对象,则会报错。

由此可见,Java的互斥锁在多线程应用中非常方便,可以说是一个“神器”。

最后,给大家看一下我用到互斥锁的俄罗斯方块代码片段,跟上面的例子是基本一样的:

// 唤醒
public void run() {
        synchronized (mutex) {
            try {
                Block[] blocks = generateBlock();
                jBlock.addBlocks(blocks);
                while (game.isPause() || moveBlock(0,1))
                    sleep(mode.getSpeed()); // 下落速度
                mutex.notify();
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }

// 等待唤醒
while (true) {
            synchronized (nowBC.mutex) {
                try {
                    nowBC.start();
                    nowBC.mutex.wait();
                    score();
                    if (nowBC.reachTop()) break;
                    nowBC = next_bc;
                    next_bc = newBlocks();
                    nextBlock.drawNew(next_bc.generateBlock(true));
                } catch (InterruptedException e) {
                    break;
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        }

而我原先的等待操作,竟然是在主循环中使用判断是否下落完成,未完成则continue,CPU占用率高达20%;使用互斥锁后,CPU占用率仅在1-2%浮动,完美节省资源。暂停时,CPU占用率是0%,之前则是跟下落时的20%相同。当然了,仅仅是这样的优化,也不算非常厉害,毕竟这种小游戏理应就不可能占用太多资源。不过,通过这次的应用,让我对互斥锁的理解和用法更加深入,将来如果还用Java写项目,应该还会再用到。

发表评论