Java编程思想第21章并发读书笔记(下)

1. 共享受限资源

当两个或者多个线程同时操作一个共享数据时,很可能引起冲突,就会出现问题。例如,两个线程同时尝试访问同一个银行账户,或向同一个打印机发出打印的请求,改变同一个值

1.1 不正确的访问资源

简单示例,模拟两个窗口卖5张车篇:

public class TicketRunnable implements Runnable {
    private int num = 5;

    @Override
    public void run() {
        while (num > 0) {
            try {
                TimeUnit.MILLISECONDS.sleep(100);
                System.out.println(Thread.currentThread().getName() + " --> " + num--);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    public static void main(String[] args) {
        TicketRunnable ticket = new TicketRunnable();
        ExecutorService executorService = Executors.newCachedThreadPool();
        executorService.execute(ticket);
        executorService.execute(ticket);
        executorService.shutdown();
    }
}

运行结果:

pool-1-thread-2 --> 5
pool-1-thread-1 --> 5
pool-1-thread-2 --> 4
pool-1-thread-1 --> 3
pool-1-thread-2 --> 2
pool-1-thread-1 --> 1
pool-1-thread-2 --> 0

run()方法中,加入sleep(100),问题就是5号票被卖了两份

Java中,递增和递减的任务在执行过程中都有可能会被线程机制挂起,递增和递减都不是原子性的操作。即使是单一的递增或者递减也不是安全的

Java中使用线程的基本问题:永远不知道一个线程在何时运行


1.2 解决共享资源竞争

防止任务后冲突的方法就是当资源被一个任务使用时,在资源上加锁。第一个访问某项资源的任务锁定这个资源,其他任务在资源被解锁前,无法访问

基本上所有的并发模式在解决线程冲突的问题时,都是采用序列化访问共享资源的方案。这意味着在一个特定的时刻只允许一个任务来访问共享资源。通常时通过在代码前加上一条锁定语句来实现,这样在给定的一段时间内只可以有一个任务运行这段代码。因为锁语句产生了一种互斥的效果,这种机制也便常常被称为互斥量(mutex)


1.2.1 synchronized关键字

Java提供了关键字synchronized,作用便是防止资源冲突。当任务要执行的代码被synchronized关键字保护的代码片段的时候,这个关键字将会检查锁是否可以用,然后获取锁,执行代码,释放锁

共享资源一般是以对象形式存在于内存片段,也可以是文件、输入/输出端口,或者是打印机。要控制对共享资源的访问时,得先把资源包装进一个对象。然后把所有要访问这个资源的方法标记为sycnchronized。如果某个任务处于一个标记为syncronized的方法调用中,在这个线程从该方法返回之前,其他要调用类中任何被标记为synchronized方法的线程都会被阻塞

方法上加上了synchronized关键字后,对象就会自动含有单一的锁(监视器)

当在对象上调用任意synchronized方法时,此对象就会被加锁,这个时候此对象上的其他synchronized方法只有等待到前一个方法调用完毕并释放了锁之后才可以调用。

synchronized void f(){ ... };
synchronized void g(){ ... };

如果某一个任务调用了某个对象of()方法,对于o来说,就只能等到f()调用结束并释放了锁之后,其他的任务才能调用f()g()

对于某个特定的对象来说,所有的synchronized方法共享一个锁


简单使用,修改TicketRunnable代码:

@Override
public void run() {
   synchronized (TicketRunnable.this){//加入锁
      while (num > 0) {
         try {
              TimeUnit.MILLISECONDS.sleep(100);
              System.out.println(Thread.currentThread().getName() + " --> " + num--);
         } catch (InterruptedException e) {
              e.printStackTrace();
         }
      }
   }
}

针对每一个类,也有一个锁,直接使用synchronized (Object.class),所以synchronized static方法可以在类的范围内防止对static数据的并发访问

同步规则:如果一个变量正在一个线程中进行写入操作,接下来可能被另外一个线程读取,或者一个线程正在读取一个被另一个线程进行写入操作过的变量,必须考虑使用同步,并且,读和写的线程必须使用同一个锁来进行同步

每个访问临界共享资源的方法都必须被同步 ,如果类中有超过一个方法在处理临界数据,必须要同步所有的方法。如果只同步一个方法,其他的方法将可能随意地忽略这个对象锁,并在无任何限制的条件下被调用

临界区也就是某个特定时刻只能有一个线程访问的代码区,也被称为同步代码块,通常使用synchronized来建立

synchronized(syncObject) { 
    //critical section
   //by only one task a time
}

一个线程在进入此代码块前,必须要先得到syncObject对象的锁,如果其他的线程已经持有了该syncObject对象的锁,则必须等待锁被释放

也可以使用Lock来建立


1.2.2 使用显式的Lock对象

Lock是显式的互斥机制。使用时,Lock对象必须被显式地创建、锁定和释放

简单使用,修改TicketRunnable代码:

 //显式创建Lock对象
 private Lock lock = new ReentrantLock();

    @Override
    public void run() {
        lock.lock();
        try {
            while (num > 0) {
                try {
                    TimeUnit.MILLISECONDS.sleep(100);
                    System.out.println(Thread.currentThread().getName() + " --> " + num--);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        } finally {
            lock.unlock();
        }

    }

Lock用的是import java.util.concurrent.locks.Lock包下的,创建时,通过new ReentrantLock()

一般会把lock.unlock()放置在finally子句中,如果方法需要返回值,把需要返回的值try子句中return,以确保unLock()不会过早发生,导致将数据暴露给了第二个任务


通常情况下,都是使用synchronized关键字,有些比较特殊的情况下使用LockLock可以做到:尝试获取锁但并不去持有锁,只是进行查看是否拥有权利去持有锁;还可以指定尝试的时间,然后放弃尝试

public class AttemptLocking {
    private ReentrantLock lock = new ReentrantLock();

    public static void main(String[] args) {
        final AttemptLocking al = new AttemptLocking();
        al.untimed();//true 此时锁可见 并且 当前线程可以上锁
        al.timed();//true 此时锁可见 并且 当前线程可以上锁
        Thread t2 = new Thread() {
            //构造代码块
            {
                setDaemon(true);
            }

            @Override
            public void run() {
                al.lock.lock();//上锁
                System.out.println("t2 has acquired");
            }
        };
        t2.start();
        Thread.yield();//让步  给t2一次机会
        al.untimed();//false 此时锁已经被t2持有
        al.timed();//false 此时锁已经被t2持有
    }

    /**
     * 查看锁对象是否被持有
     */
    private void untimed() {
        //如果锁没有被其他线程持有或者锁被当前线程持有,返回true
        boolean capture = lock.tryLock();
        try {
            System.out.println("tryLock() --> " + capture);
        } finally {
            if (capture)
                lock.unlock();
        }
    }
    /**
     * 2秒后查看锁对象是否被持有
     */
    private void timed() {
        boolean captured = false;
        try {
            captured = lock.tryLock(2, TimeUnit.SECONDS);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        try {
            System.out.println("tryLock(2,TimeUnit.SECONDS) --> " + captured);
        } finally {
            if (captured)
                lock.unlock();
        }
    }
}

运行结果:

tryLock() --> true
tryLock(2,TimeUnit.SECONDS) --> true
t2 has acquired
tryLock() --> false
tryLock(2,TimeUnit.SECONDS) --> false

tryLock()方法个人感觉只是去查看是否能有权利去持有锁。当别的线程已经持有了锁,可以使用这个方法进行查看,若发现锁已经被持有了,就放弃锁,然后可以去执行其他的操作而不是一直等待锁被释放

显式的Lock对象在加锁和释放锁方面,比内建的synchronized锁拥有更细力度的控制力。例如,用于遍历链接列表中的节点的节节传递加锁机制(锁耦合),这种遍历必须在释放当前节点的锁之前捕获下一个节点的锁


1.3 原子性和可见性

原子操作:

一个操作不能线程调度机制中断操作,操作一旦开始就不会被打断,要么就不进行操作

原子是世界上的最小单位,具有不可分割性。比如 a=0;(a非long和double类型) 这个操作是不可分割的,那么我们说这个操作时原子操作。再比如:a++; 这个操作实际是a = a + 1;是可分割的,所以他不是一个原子操作。非原子操作都会存在线程安全问题,需要我们使用同步技术(sychronized)来让它变成一个原子操作。一个操作是原子操作,那么我们称它具有原子性。Java的concurrent包下提供了一些原子类,我们可以通过阅读API来了解这些原子类的用法。比如:AtomicInteger、AtomicLong、AtomicReference等。

可见性:

可见性,是指线程之间的可见性,一个线程修改的状态对另一个线程是可见的。也就是一个线程修改的结果。另一个线程马上就能看到。比如:用volatile修饰的变量,就会具有可见性。volatile修饰的变量不允许线程内部缓存和重排序,即直接修改内存。所以对其他线程是可见的。但是这里需要注意一个问题,volatile只能让被他修饰内容具有可见性,但不能保证它具有原子性。比如 volatile int a = 0;之后有一个操作 a++;这个变量a具有可见性,但是a++ 依然是一个非原子操作,也就这这个操作同样存在线程安全问题

以上摘自 java并发之原子性与可见性


2 终结任务

2.1突然终结一个任务

在一个任务Runnablerun()方法中,通常是一个while循环,修改循环的条件就可以终结一个任务

public class StopRunnableDemo {
    public static void main(String[] args) throws InterruptedException {
        StopRunnable runnable = new StopRunnable();
        Thread thread = new Thread(runnable);
        thread.start();
        //main() 的线程休眠 1 毫秒
        TimeUnit.MILLISECONDS.sleep(1);
        runnable.stop();
    }
}

class StopRunnable implements Runnable {
    private Random random = new Random(47);
    private boolean isRunning = true;

    private boolean runningState() {
        return isRunning;
    }

    public void stop() {
        this.isRunning = false;
    }

    @Override
    public void run() {
        while (runningState()) {//根据需求来设置循环条件
            System.out.println(random.nextInt(10));
        }
        System.out.println("任务已结束");
    }
}

书上的案例:
统计每天通过多个大门进入花园的总人数

public class OrnamentalGarden {

    public static void main(String[] args) throws InterruptedException {
        ExecutorService executorService = Executors.newCachedThreadPool();
        for (int i = 0; i < 5; i++) {
            executorService.execute(new Entrance(i));
        }

        TimeUnit.SECONDS.sleep(3);
        Entrance.cancel();
        executorService.shutdown();
        if (!executorService.awaitTermination(250, TimeUnit.MILLISECONDS)) {
            System.out.println("Some tasks were not terminated");
        }
        System.out.println("Total --> " + Entrance.getTotalCount());
        System.out.println("Sum of Entrances --> " + Entrance.sumEntrances());
    }
}

class Count {
    private int count = 0;
    private Random random = new Random(47);

    public synchronized int increment() {
        int temp = count;
        if (random.nextBoolean()) {
            Thread.yield();
        }
        return (count = ++temp);
    }

    public synchronized int value() {
        return count;
    }
}

class Entrance implements Runnable {
    private static Count count = new Count();
    private static List<Entrance> entrances = new ArrayList<>();
    private int num = 0;
    private final int id;
    private static volatile boolean canceled = false;

    public static void cancel() {
        canceled = true;
    }

    public Entrance(int id) {
        this.id = id;
        entrances.add(this);
    }

    @Override
    public void run() {
        while (!canceled) {
            synchronized (this) {
                ++num;
            }
            System.out.println(this + "Total: -->" + count.increment());
            try {
                TimeUnit.MILLISECONDS.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("Stopping --> " + this);
        }
    }

    public synchronized int getValue() {
        return num;
    }

    @Override
    public String toString() {
        return "Entrance --> " + id + " : " + getValue();
    }

    public static int getTotalCount() {
        return count.value();
    }

    public static int sumEntrances() {
        int sum = 0;
        for (Entrance entrance : entrances) {
            sum += entrance.getValue();
        }
        return sum;
    }
}

单个Count对象用来跟踪记录花园每个大门的参观人数,当作Entrance类中的一个静态域来存储。其中Countincrement()value()方法都是synchronized同步方法,用来控制对Count域的访问。在increment()内,通过使用random.nextBoolean()在人数递增过程中,有大约一半时间产生让步

每个Entrance内都有一个本地值num,包含通过某个特定入口的进入的参观者数量,提供了对Count对象的双重检查,以确保Count对象记录的参观者数量是正确的。Entrance.run()只是递增numCount对象,然后休眠100毫秒

Entrance.canceled是一个volatile标记的布尔值,canceled只会被读取和赋值,并不会与其他的域组合一起被读取,并不需要同步对其的访问

main()中,经过了3秒钟后,Entrance.cancel(),对任务进行取消,然后关闭线程池

executorService.awaitTermination(250, TimeUnit.MILLISECONDS)

等待任务结束,第一个参数是等待的时间,第二个参数是时间单位。当所有的任务在设置的超时限制时间之前就结束,就会返回true,否则就false,表示并不是所有的任务都已经结束

当任务终止时,Entrance对象仍旧是有效的,因为在构造方法中,Entrance对象存进了List集合中

整个dome中,关于多线程来操作共享数据的主要思路就是:使用互斥同步来对Count进行访问,共享数据操作


2.2 在任务阻塞时终结

sleep()方法将线程由执行状态变为阻塞状态

线程状态:

  • 新建(new):当线程被创建时,线程会短暂地处于这种状态。此时,线程已经被分配了必需的系统资源,并执行了初始化。此时线程已经有资格获得CPU的时间,此时只是有能力去争取CPU的时间。之后 ,调度器将把这个线程转变为可就绪或者阻塞状态
  • 就绪(Runnable):在这种状态下,线程调度器只要将时间片分配给线程,线程就可以运行。在任意的时刻,线程可以运行也可以不运行,就看调度器是否分配时间片
  • 阻塞(Blocked):线程能够运行,但某个条件阻碍了运行。当一个线程t处于阻塞状态下,调度器就会忽略线程,不会再分配给t任何的CPU时间。只有当t重新进入就绪状态,t才有可能被分给CPU时间片,才可能执行操作
  • 死亡(Dead):处于死亡或者终止状态的线程将不再拥有是可调度的。并且也不可能再有资格去得到CPU时间片,线程的任务已经结束。任务死亡的通常做法就是run()执行结束返回

进入阻塞状态的可能原因:

  1. 调用了sleep()方法,任务进入休眠。此时,线程在指定的时间内不会运行
  2. 调用了wait()方法使线程挂起。直到线程得到notify(),notifyAll(),线程才会进入就绪状态
  3. 线程在等个某个输入输出完成
  4. 线程试图在某个对象上调用其同步方法,但此时对象锁被另一个线程持有不可用

对于处于阻塞状态的线程,必须强制这个线程跳出阻塞状态后,才可以让线程主动地终止


2.3 中断

Thread类又一个interrupt()方法,可以用来终止一个被阻塞的任务。线程调用interrupt()方法后,将被设置为中断状态,若此时线程正好处于休眠,阻塞或者挂起,就会抛出InterruptedException异常,然后中断状态便会被清除

public class InterruptDemo {
    public static void main(String[] args) throws InterruptedException {
        Thread t = new Thread(new Runnable() {
            @Override
            public void run() {
                    for (int i = 0 ; i < 5;i ++ ){
                        System.out.println(i);
                    }
                    System.out.println("t进入休眠");
                    try {
                        TimeUnit.SECONDS.sleep(10);
                    } catch (InterruptedException e) {
                        System.out.println("中断异常");
                    }
                }
        });
        t.start();
        TimeUnit.SECONDS.sleep(3);
        t.interrupt();
    }
}

运行结果:

0
1
2
3
4
t进入休眠
中断异常
任务结束

书上这一小节,看的很尴尬,暂时也不知道这个中断机制有啥用,先不管了


3 线程间协作

通常使用多个线程来同时执行多个任务时,可以使用锁来同步两个任务的行为,从而使得一个任务不会干涉另一个任务的资源。也就是说,如果有两个任务正在交替着使用某个共享资源(一般是内存),可以使用互斥使得任意时刻只有一个任务有权利访问这个资源

当想要有多个任务一起工作解决某个问题时,必须要考虑线程间的协作。在解决问题时,有的任务需要并发执行,而某些任务需要其他所有的任务执行完毕后才能开动

当任务协作时,关键问题是这些任务的握手。为了实现握手,使用相同的特性:互斥。互斥能够保证只有一个任务可以响应某个信号,这样就可以任何可能竞争条件。在互斥之上,为任务添加一种的途径,可以将自身挂起,直至外界条件发生变化,标志着任务可以向前开动了为止。任务间的握手问题,可以使用Objectwait()notify()来安全地实现,Java SE5的并发库也提供了具有await()signal()方法的Condition对象


3.1 wait()和 notifyAll()

wait()可以使线程等待某个条件发生变化,通常这个条件所需要的能力超出了当前任务的权力,一般是由另一个任务来改变。在任务中,不断进行空循环,也称为忙等待,是一种不良的CPU使用方式。wait()会在检测外界条件变化的过程中,将任务挂起,并且只有在notify()或者notifyAll()方法被调用时,被挂起的任务才会被唤醒并去检查所产生的变化。因此,wait()提供了一种任务间对活动同步的方式

调用sleep()yield()方法时,锁并没有被释放。但当一个任务在执行时,线程对象调用了wait()方法,线程被挂起,线程对象上持有的锁也将会被释放掉,wait()方法会释放锁,意味着另一个线程可以获取这个锁。不然线程被挂起了却依然持有锁,别的线程将无法去持有锁,而无法正常进行任务。在一个线程wait()期间,锁对象中的其他synchronized方法可以被其他的任务调用。

当调用了wait()时,就是声明:

我刚刚已经做完了力所能及的事情,现在我要在这里等待,但其他的synchronized操作在条件适合的情况下能够执行

wait() 与 sleep()区别:

  • wait()方法释放锁
  • 没有限时时间的wait()方法会一直等待下去,直到线程调用notify()或者notifyAll()方法
  • wait()方法不属于Thread类的方法,而是Object的方法
  • wait()方法只能在同步控制方法或者同步控制块里调用,而sleep()可以在非同步控制方法中调用

wait(),notify(),notifyAll()方法都是基类Object中的方法。这3个方法都涉及操作到锁对象,锁对象是不确定的,把wait()方法放在Object基类里,这样可以把wait()放在任何的Runnbale或者Thread同步控制的方法。调用wait(),notify(),notifyAll()方法的任务必须要获取对象锁,否则将有IllegalMonitorStateException异常,并伴随一些比较含糊的消息,例如当前线程不是锁拥有者

可以让另一个对象执行某种操作以维护自己的锁,要这么做首先必须得到对象的锁。例如,如果要向对象x发送notifyAll()

synchronized(x){
   x.notifyAll();
   ...
}

具体实例:

WaxOMAtic.java有两个任务:一个在Car上涂蜡,一个抛光

public class WaxOMatic {

    public static void main(String[] args) throws InterruptedException {
        Car car = new Car();
        ExecutorService executorService = Executors.newCachedThreadPool();
        executorService.execute(new WaxOff(car));
        executorService.execute(new WaxOn(car));
        TimeUnit.SECONDS.sleep(1);
        executorService.shutdownNow();//中断全部线程
    }
}

class Car{

    private boolean waxOn = false;//用来表示打蜡-抛光的处理状态

    /**
     *  打蜡完成
     */
    public  synchronized void waxed(){
        waxOn = true;//准备进行buffed抛光
        notifyAll();
    }

    /**
     * 抛光完成
     */
    public synchronized void buffed(){
        waxOn = false;// 准备为另一个 进行 waxed 打蜡
        notifyAll();
    }
    
    /**
     * 正在抛光,等待着准备打蜡
     * @throws InterruptedException
     */
    public synchronized void waitForWaxing() throws InterruptedException {
        while (!waxOn){
            wait();
        }
    }

    /**
     * 正在打蜡,等待着准备抛光
     * @throws InterruptedException
     */
    public synchronized void waitForBuffing() throws InterruptedException {
        while (waxOn){
            wait();
        }
    }
}

class WaxOn implements Runnable{
    private  Car car;

    public WaxOn(Car car) {
        this.car = car;
    }

    @Override
    public void run() {
        try
        {
            while (!Thread.interrupted()){
                System.out.println("开始打蜡");
                TimeUnit.MILLISECONDS.sleep(200);//模拟打蜡用的时间
                car.waxed();//打蜡完成
                car.waitForBuffing();
            }
        }catch (InterruptedException e){
            System.out.println("打蜡 --> 中断异常");
        }
        System.out.println("完成打蜡");
    }
}

class WaxOff implements Runnable{
    private Car car;

    public WaxOff(Car car) {
        this.car = car;
    }

    @Override
    public void run() {
        try
        {
            while (!Thread.interrupted()){
                car.waitForWaxing();
                System.out.println("开始抛光");
                TimeUnit.MILLISECONDS.sleep(200);//模拟抛光时间
                car.buffed();//抛光完成                
            }
        }catch (InterruptedException e){
            System.out.println("抛光 --> 中断异常");
        }
        System.out.println("完成抛光");
    }
}

运行结果:

开始打蜡
开始抛光
开始打蜡
开始抛光
开始打蜡
抛光 --> 中断异常
完成抛光
打蜡 --> 中断异常
完成打蜡

waitForWaxing()中检查waxOn标记,如果正在抛光,就将打蜡任务挂起。在waxed()中,先讲处理标记修改后,就使用notifyAll()唤醒所有被wait()挂起的任务。wait()所处的方法都是synchronized方法,调用了wait()方法时,线程被挂起,而锁被释放

WaxOn.run()表示为Car打蜡的第一个步骤,调用sleep()是模拟打蜡过程所用的时间,然后告诉Car打蜡结束,并调用waitForBuffed(),里面有一个wait()方法挂起打蜡任务,直到WaxOff任务调用了buffed(),改变处理状态后,notifyAll()

WaxOff.run()会立即调用waitForWaxing()方法,然后被抛光任务被挂起,直到WaxOn调用waxed()

两个步骤在1秒钟内重复执行,然后运行executorService.shutdownNow(),调用所有由executorService控制的线程的interrup()方法

notifyAll()只会唤醒等待特定的锁对象的任务


3.2 错失信号

两个线程间,不合理的使用判断线程挂起标志时,会造成错失信号

缺陷版本:

T1:
synchronized(sharedMonitor){
    <setup condition for T2>
    someCondition = false ;
    sharedMonitor.notify();
}

T2:
while(someCondition){
   //Potion 1
   synchronized(sharedMonitor){
       sharedMoitor.wait();
   }
}

<setup condition for T2>这里就是为了修改someCondition的标志值,不再让T2调用wait()

假设某个时刻,T2执行while()后,判断someConditiontrue。然后执行while()
内部,当执行到Potion 1这一瞬间,线程调度器切换到了T1,而T1修改了someConditionfalse后调用了sharedMonitor.notify(),发出唤醒信号。此时T2Potion1处继续执行,没有意识到someCondition值已经发生改变,盲目调用wait()被挂起。此时,notify()会丢失,T2无限等待并没有接收到已经发过的唤醒信号,而导致死锁

解决的思路便是:防止someCondition产生竞争条件


修改T2代码:

synchronized(shareMontior){
    while(someCondition){
       sharedMoitor.wait();
    }
}

如果是T1先执行,由于同步锁,T2便不会执行,T1修改过someCondition值后,并唤醒了T2,此时根据改变的someCondition值而不会再调用wait()

如果T2先执行,线程被挂起后,会等待T1的唤醒,不会丢失唤醒信号


3.3 生产者和消费者

有一个餐馆:一个厨师一个服务员。当厨师做好一份饭时,通知服务员,服务员通过餐窗取走食物,给客人端过去,然后服务员等待下次通知,一天只卖10份

class Restaurant {
    Meal meal;
    ExecutorService executorService = Executors.newCachedThreadPool();
    final Chef chef = new Chef(this);
    final WaitPerson waitPerson = new WaitPerson(this);

    public Restaurant() {
        executorService.execute(chef);
        executorService.execute(waitPerson);
    }

    public static void main(String[] args) {
        new Restaurant();
    }
}

class WaitPerson implements Runnable {
    private Restaurant restaurant;

    public WaitPerson(Restaurant restaurant) {
        this.restaurant = restaurant;
    }

    @Override
    public void run() {
        try {
            while (!Thread.interrupted()) {
                synchronized (this) {
                    while (null == restaurant.meal) {
                        wait();
                    }
                }
                System.out.println("服务员 --> " + restaurant.meal);
                synchronized (restaurant.chef) {
                    restaurant.meal = null;
                    restaurant.chef.notifyAll();
                }
            }
        } catch (InterruptedException e) {
            System.out.println("服务员 -->  InterruptedException");
        }
    }
}

class Chef implements Runnable {

    private Restaurant restaurant;
    private int count = 0;

    public Chef(Restaurant restaurant) {
        this.restaurant = restaurant;
    }

    @Override
    public void run() {
        try {
            while (!Thread.interrupted()) {
                synchronized (this) {
                    while (null != restaurant.meal) {
                        wait();
                    }
                }
                if (count++ == 10) {
                    System.out.println("卖完了,关门");
                    restaurant.executorService.shutdownNow();
                }
                System.out.print("端菜啦!  ...    ");

                synchronized (restaurant.waitPerson) {
                    restaurant.meal = new Meal(count);
                    restaurant.waitPerson.notifyAll();
                }
                TimeUnit.MILLISECONDS.sleep(100);
            }

        } catch (InterruptedException e) {
            System.out.println("厨师 -->  InterruptedException");
        }
    }
}

class Meal {
    private final int olderNum;

    public Meal(int olderNum) {
        this.olderNum = olderNum;
    }

    @Override
    public String toString() {
        return "回锅肉 " + olderNum;
    }
}

运行结果:

端菜啦!  ...    服务员 --> 回锅肉 1
端菜啦!  ...    服务员 --> 回锅肉 2
端菜啦!  ...    服务员 --> 回锅肉 3
端菜啦!  ...    服务员 --> 回锅肉 4
端菜啦!  ...    服务员 --> 回锅肉 5
端菜啦!  ...    服务员 --> 回锅肉 6
端菜啦!  ...    服务员 --> 回锅肉 7
端菜啦!  ...    服务员 --> 回锅肉 8
端菜啦!  ...    服务员 --> 回锅肉 9
端菜啦!  ...    服务员 --> 回锅肉 10
卖完了,关门
端菜啦!  ...    厨师 -->  InterruptedException
服务员 -->  InterruptedException

RestaurantWaitPersonChef的焦点,他们在一家餐馆工作,通过一个餐窗来打交道,以便放置或者取走食物restaurant.meal

WaitPersonrun()方法中,当食物没有做好的时候,就会调用wait()方法,停止任务,等待Chef来唤醒。当Chef做好食物,通知WaitPerson来取走食物,然后就挂起,等待WaitPerson通知有下一个订单

wait()被包装在一个while语句中,while会不断地进行判断测试判断查看测试的条件。在Chef.run()中,调用notifyAll()前必须要获取waitPerson锁,而在WaitPerson.run()中,一旦没有了食物后,就会调用wait(),会自动释放WaitPerson对象锁。同理,WaitPerson需要调用notifyAll()时,情况也一样


4.死锁

死锁,个人理解:任务1在等待任务2,而任务2却又在等待任务1结束释放锁,这样一直相互等待,互相持有对象锁

简单死锁案例:

public class DeadLockDemo {
    public static void main(String[] args) throws InterruptedException {
        DeadLockRunnable runnable = new DeadLockRunnable();
        ExecutorService executorService = Executors.newCachedThreadPool();
        executorService.execute(runnable);
        TimeUnit.MILLISECONDS.sleep(100);
        runnable.flag = false;
        executorService.execute(runnable);
        executorService.shutdown();
    }
}


class DeadLockRunnable implements Runnable {
    private int tick = 1000;
    private Object object = new Object();
    boolean flag = true;

    @Override
    public void run() {
        if (flag) {
            while (true) {
                synchronized (object) {
                    if (tick > 0) {
                        show();
                    }
                }
            }
        } else {
            while (true) {
                show();
            }
        }
    }

    private synchronized void show() {
        synchronized (object) {
            if (tick > 0) {
                try {
                    TimeUnit.MILLISECONDS.sleep(100);

                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread().getName() + " --> " + tick--);
            }
        }
    }
}

哲学家就餐问题:有5个哲学家,当他们思考人生时,并不需要共享资源。当他们思考结束,就餐时,需要使用筷子,而现在筷子有限,每个哲学家只有一个筷子

哲学家就餐

图来自尝试解决哲学家进餐问题

筷子代码:

public class Chopstick {
    private boolean taken = false;

    /**
     * 拿起筷子
     * @throws InterruptedException
     */
    public synchronized void take() throws InterruptedException {
        while (taken){
            wait();
        }
        taken = true;
    }

    /**
     *  放下筷子
     */
    public synchronized void drop(){
        taken = false;
        notifyAll();
    }
}

哲学家代码:

public class Philosopher implements Runnable {
    private Chopstick left;
    private Chopstick right;
    private final int id;
    private final int ponderFactor;
    private Random random = new Random(47);

    public Philosopher(Chopstick left, Chopstick right, int id, int ponderFactor) {
        this.left = left;
        this.right = right;
        this.id = id;
        this.ponderFactor = ponderFactor;
    }

    private void pause() throws InterruptedException {
        if (ponderFactor == 0) return;
        TimeUnit.MILLISECONDS.sleep(random.nextInt(ponderFactor * 250));
    }

    @Override
    public void run() {
        try {
            while (!Thread.interrupted()) {
                System.out.println(this + " --> " + "思考人生");
                pause();
                System.out.println(this + " --> " + "饿了");
                System.out.println(this + " --> " + "拿起右边的筷子");
                right.take();
                System.out.println(this + " --> " + "拿起左边的筷子");
                left.take();
                System.out.println(this + " --> " + "开始吃");
                pause();
                right.drop();
                left.drop();
                System.out.println(this + " --> " + "吃完了");
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

    }

    @Override
    public String toString() {
        return "哲学家 " + id;
    }
}

可能会产生死锁的代码:

public class DeadlockingDiningPhilosopher {
    public static void main(String[] args) throws Exception {
        int ponder = 5;
        int size = 5;
        if (args.length > 0){
            ponder = Integer.parseInt(args[0]);
        }
        if (args.length > 1){
            size = Integer.parseInt(args[1]);
        }
        ExecutorService executorService = Executors.newCachedThreadPool();
        Chopstick[] chopsticks = new Chopstick[5];
        for (int i = 0; i < size; i++) {
            chopsticks[i] = new Chopstick();
        }
        for (int i = 0; i < size; i++) {
            executorService.execute(new Philosopher(chopsticks[i], chopsticks[(i + 1) % size], i, ponder));
        }
        if (args.length == 3 && args[2].equals("timeout")) {
            TimeUnit.SECONDS.sleep(5);
        } else {
            System.out.println("点击Enter键退出");
            System.in.read();
        }
        executorService.shutdownNow();
    }
}

然而,我试了半天也没出来一次死锁


修复代码:

 class FixedDiningPhilosopher {
    public static void main(String[] args) throws Exception {
        int ponder = 5;
        int size = 5;
        if (args.length > 0){
            ponder = Integer.parseInt(args[0]);
        }
        if (args.length > 1){
            size = Integer.parseInt(args[1]);
        }
        ExecutorService executorService = Executors.newCachedThreadPool();
        Chopstick[] chopsticks = new Chopstick[5];
        for (int i = 0; i < size; i++) {
            chopsticks[i] = new Chopstick();
        }
        for (int i = 0; i < size; i++) {
            if ( i < (size - 1)){
                executorService.execute(new Philosopher(chopsticks[i], chopsticks[(i + 1)], i, ponder));
            }else{//最后一个哲学家
                executorService.execute(new Philosopher(chopsticks[0], chopsticks[(i)], i, ponder));
            }
        }
        if (args.length == 3 && args[2].equals("timeout")) {
            TimeUnit.SECONDS.sleep(5);
        } else {
            System.out.println("点击Enter键退出");
            System.in.read();
        }
        executorService.shutdownNow();
    }
}

死锁的条件:

  • 互斥条件。任务正在使用的资源中,至少有一个是不能共享的。案例中,一根Chopstick一次就只能被一个Philosoopher使用
  • 至少有一个任务,它必须持有一个资源并且正在等待获取一个正在被别的任务持有的资源。Philosoopher拿着一根筷子,等另一根筷子
  • 资源不能被任务抢占,任务班必须把释放资源当作普通事件。哲学家们都是绅士,不会从别人手里抢筷子
  • 必须有循环等待,一个任务等待其他的任务所持有的资源,后者又在等待另一个任务所持有的资源,彼此拥有对方需要的资源,而都在等待里一个任务释放资源,使得都被锁住。

DeadlockingDiningPhilosopher中,每个哲学家都试图先得到右边的筷子,然后再得到左边的筷子,所以发生了循环等待,防止死锁最简单的思路就是防止循环等待。

最后一个哲学家,被初始化成先拿左边的,再拿右边的,这位哲学家也就不会阻止他右边的其他的哲学家们


5.最后

上,下两篇记录了记录一部分书上个人感觉比较重要的基础知识点。

整本书,也差不多看完了。前面一些比较熟悉的知识点章节看的快一些,也没写博客记录,效果果然不行,感觉记住的东西很少。然而无论如何不用再受这本书折磨了,很多案例总感觉有点绕,并不算特别容易接受理解

本人很菜,有错误请指出

共勉 :)

推荐阅读更多精彩内容

  • 1.解决信号量丢失和假唤醒 public class MyWaitNotify3{ MonitorObject m...
    Q罗阅读 565评论 0 1
  • 本文出自 Eddy Wiki ,转载请注明出处:http://eddy.wiki/interview-java.h...
    eddy_wiki阅读 1,101评论 0 14
  • 出来混迟早要还的。终于深刻的体会到了这句话的含义。白天敲代码,晚上码字。这几天忙碌的工作和奔波的生活,让脑...
    陌颜苒seven阅读 94评论 0 0
  • 儿子初升高的一模比整个初中阶段最差的那次还下降了一百多名。当知道结果时的我心里堵得慌,理性告知我,责怪也无用,...
    2731836b20c7阅读 71评论 0 2
  • 秋天 有菊 有酒 有茶 有诗 有相思 秋天是离愁的季节 花开不能太妖艳 漂泊的游子 会焦躁不安 误以为春还没完 秋...
    y幽清阅读 315评论 6 3