Power by GeekHades

Java多线程(二)——同步与互斥

0x1 前言

(Java多线程(一)——多线程基础)文章中,我们给出了一个火车售票系统的雏形方案,但是不尽如人意的是这个方案好像并不能参与实际工作。那么我们该采取什么方法来改善我们这个方案呢?

首先在分析我们的系统之前,我们有必要了解线程的两个重要的概念:同步互斥


0x2 同步与互斥

这里我们引入几个与并发相关的几个知识点:

  • 原子操作:一个或多个指令的序列,对外是不可分的;即没有其他进程可以看到中间状态或终端此操作。举个例子:假设你要去厕所上厕所,上厕所就包含了很多个操作步骤(多个指令序列),比如脱裤子...啥的,在这个过程中有一个坑位让你上,你就必须把门关紧,别的人不可以看见你在做什么,也不可能说你弄到一半把你揪出来,并且你也不会说你先拉一半然后再让门外的人拉一半。要么不做,要么全做。

  • 临界区:是一段代码,在这段代码中进程将访问共享资源,当另外一个进程已经在这段代码中运行时,其他进程不能进入这段代码运行。

  • 互斥:当一个进程在临界区访问共享资源时,其他进程不能进入临界区访问任何共享资源。

  • 同步:在互斥的基础上,实现进程之间的有序访问。例如:上完厕所后你必须是先擦干净再提上裤子。

那么现在我们就利用上面提到的来分析以下我们的火车售票系统。

窗口1售出一张票,总票数还剩:99
窗口1售出一张票,总票数还剩:95
窗口1售出一张票,总票数还剩:93
窗口1售出一张票,总票数还剩:91
窗口1售出一张票,总票数还剩:90
窗口3售出一张票,总票数还剩:97
窗口4售出一张票,总票数还剩:96
窗口9售出一张票,总票数还剩:85
窗口2售出一张票,总票数还剩:98
...

我们发现窗口1的前五条连续售票记录中,他的剩余票数并非连续的,也就是意味着他在售票的过程中被打断了,那么就属于:非原子性操作。也就导致了后面的最后一张票已经售出的情况下,居然还有窗口在售票。

所以,我们之前的程序犯了根本性的错误。我们以为计算机的线程会按顺序执行,但其实不然,因为存在竞争,所以就会出现错误的结果。

那我们该如何改进呢?显而易见的是,我们的10个窗口共享100张票,那么这100张票就是临界区,售票的过程就是互斥的,然后每个窗口要协同合作把100张票卖完。

那么Java中是如何实现同步与互斥机制的呢?


0x3 synchronized

synchronized关键字,是Java提供给我们保护临界区的方式,有两种方法使用synchronized:修饰方法和修饰代码块。

public class A{

    public synchronized void func1() {
        // do something
    }

    public void func2() {
        synchronized (锁对象) {
            // do something
        }
    }
}

只要被synchronized修饰的地方,在运行的时候都会被检测所持有的锁对象,synchronized方法的锁对象是当前对象,也就是this,而synchronized代码块的锁对象就是括号中的对象,可以选择传入this或其他对象,只要保证这个对象在所有线程执行时是唯一的。例如下面的做法就是错误的:

public void run() {
    String lock = new String();
    synchronized (lock) {
        // do somethind
    }
}

因为每个进程如果运行到这里,都是新建一个lock,那么这个锁就失效了,无法保护临界资源。


0x4 改进方案

好了,现在我们知道了需要用synchronized去给我们的售票过程添上一把锁,锁我们就可以让售票库存对象ticket来充当,因为它就是在那一次运行之中所有窗口共享的而且唯一的。但是现在问题又来了,我们这把锁应该添加在哪里?

@Override
public void run() {
    while (ticket.getTickets() > 0) {
        ticket.setTickets( ticket.getTickets() - 1 );
        System.out.println(name + "售出一张票,总票数还剩:" + ticket.getTickets());
    }
}

上面就是我们前面实现的代码,锁应该添加在哪里呢?是在while内部,还是while之外?

  • 假设我们放在while内部是代表什么意思呢?意味着我们是要先保证票数够的情况下,再来确保只有一个窗口进行售票。

  • 那么放在while之外呢?就是要先保证某一时刻只有一个窗口进行售票,然后再检测票数是否足够。

好像都挺有道理,甚至乍一看两者没有任何分别。那么我们就都试试,毕竟实践是检验真理的唯一标准。

锁添加在while内部:

@Override
public void run() {
    while (ticket.getTickets() > 0) {
        synchronized (ticket) {
            ticket.setTickets( ticket.getTickets() - 1 );
            System.out.println(name + "售出一张票,总票数还剩:" + ticket.getTickets());
        }
    }
}

运行结果:

窗口1售出一张票,总票数还剩:99
窗口4售出一张票,总票数还剩:98
窗口4售出一张票,总票数还剩:97
窗口4售出一张票,总票数还剩:96
窗口4售出一张票,总票数还剩:95
...
窗口2售出一张票,总票数还剩:0
窗口3售出一张票,总票数还剩:-1
窗口4售出一张票,总票数还剩:-2
窗口10售出一张票,总票数还剩:-3
窗口9售出一张票,总票数还剩:-4
窗口8售出一张票,总票数还剩:-5
窗口7售出一张票,总票数还剩:-6
窗口6售出一张票,总票数还剩:-7
窗口5售出一张票,总票数还剩:-8
窗口1售出一张票,总票数还剩:-9

从结果来看,的确是保证了原子操作,但是总票数为负是什么鬼?

我们来试着分析一下出现负数的原因。因为我们的锁添加在while内部,我们试想一下,当票数还剩下最后一张的时候,此时所有的线程都可以通过while的检测,但是只有一个线程能够修改票数,所以其他线程只能等待,等进入临界区的进程退出来之后,会释放ticket这把锁。其他线程就可以再次进行竞争。然后等所有在等待池等待的线程都得到了一次锁之后,再次运行while就会不通过。请留心观察我们负数的结果0->(-9),刚好10个窗口都售了一次票。

那么我们在将锁添加在while外部看看

@Override
public void run() {
    synchronized (ticket) {
        while (ticket.getTickets() > 0) {
            ticket.setTickets(ticket.getTickets() - 1);
            System.out.println(name + "售出一张票,总票数还剩:" + ticket.getTickets());
        }
    }
}

运行结果

窗口1售出一张票,总票数还剩:99
窗口1售出一张票,总票数还剩:98
窗口1售出一张票,总票数还剩:97
窗口1售出一张票,总票数还剩:96
...
窗口1售出一张票,总票数还剩:2
窗口1售出一张票,总票数还剩:1
窗口1售出一张票,总票数还剩:0

Process finished with exit code 0

从结果上来看,的确能正常运行,但是存在一个问题是不管那次运行,都是只有某一个窗口在售票,并且会售完100张为止。原因是因为我们锁在while外面,只要某个线程一拿到锁,就一直执行,直到while循环退出。如果还想改的更完善一点就有很多种方法了,读者可以尝试一下。

这里我给出我的一个方案,只是能勉强解决问题。如果有更好的方案请联系我。感谢

@Override
public void run() {
    while (ticket.getTickets() > 0 ) {
        synchronized (ticket) {
            if ( ticket.getTickets() > 0 ) {
                ticket.setTickets(ticket.getTickets() - 1);
                System.out.println(name + "售出一张票,总票数还剩:" + ticket.getTickets());
            }
        }
    }
}

0x5 小结

学到这里,我们已经能利用同步与互斥相关知识解决一些比较简单的问题了。但是如果要说深入研究,那我们就要去研究一些模型,比如我们下一次会讲的Java多线程(三)——生产者/消费者模型,理发师问题等等。本章虽然内容不多,但是知识点都是经过浓缩的,还是需要读者多花功夫去更深入的了解并发线程的相关知识。

最后祝大家身体健康、学业进步和工作顺利。



* 如果你对文章有任何意见或建议请发 邮件 给我!
* if you have any suggestion that you could send a E-mail to me, Please!