读Java编程艺术之笔记(多线程)(二)

线程协调

利用多线程进行并行处理可以提高程序的运行效率。但在代码中必须提供对各线程的控制,协调它们之间的工作,尤其在个线程共享资源或数据的情况下,更须如此。
Java提供了一系列技术,进行线程间的协调控制:使用关键字volatile来保证多线程访问共享数据的一致性;使用关键字synchronized保证多线程访问共享资源或方法时的协作和有序;应用wait()方法迫使当前正在执行的线程必须等待,直到其他线程调用notify()或notifyAll结束等待状态;应用notify()或notifyAll()方法通知其他等待线程可以进入共享资源进行更新和操作。

volatile
cache技术的应用提高了访问数据的速度和效率。但在某个瞬间,一个数据储存在主存储器和暂留在cache中的值可能不同。尤其在多线程中,某个线程访问的共享数据可能是cache中的值,而不是主存储器的值。应用关键字volatile,可以使线程越过cache,而直接访问主储存器的数据,保证了数据的一致性。注意:volatile只能应用于基本类型变量;只保证线程访问主存储器中的变量,以此保证共享数据的一致性。

synchronized
利用monitor和lock技术,可以用它来定义一段程序块,或者整个方法,用来协调多线程对这个程序块或方法的有序访问。Monitor和lock技术保证只有一个线程访问某个指定的程序块或方法,其他线程必须等待,直到当前线程结束对这个程序块或方法的操作为止。当当前线程访问完毕,lock将被打开,线程调度指定的下一个线程则获得对这段程序块或方法的方完全,达到多线程协调目的。关键字synchronized和volatile经常同时使用,以保证对数据和操作的协调。

wait
wait()和notify()或notifyAll()应当在synchronized的程序块或方法中配合使用,使多线程在共享资源和数据得到进一步的保障。wait()抛出检查型异常InterruptedException。在一个synchronized的代码中调用wait()必须提供这个异常处理机制。wait()导致其他试图进入这个synchronized代码中的线程放弃monitor和lock,保证只有当前线程执行这段协调代码。实际上,放弃锁定的其他所有线程都进入等待状态,直到某个在monitor中运行的线程调用notify()或notifyAll(),所有进入等待状态的线程将有机会重新进入monitor并锁定、执行。由于线程的执行优先权以及操作系统对线程等待的调度方式不同,等待时间最长的不一定最先获得monitor并lock然后执行。推荐调用notifyAll()来唤醒所有等待的线程,由线程调度器决定哪个线程将被最先执行。PS. wait()、notify()、notifyAll()都属于Object类。wait(long timeout)是对wait()的重载。

notify()/notifyAll()
notify()或notifyAll()必须和wait()配合使用。notify()只是唤醒一个正在等待的线层。如果代码中只有一个线程处于等待状态,调用notify()应该不存在什么问题,由于系统调度器处理线程调度的不透明性,唤醒哪个线程是不确定的。所以不要根据代码分析和假设来判断,notifyAll()被经常使用,以增强代码的可靠性。

import java.util.Scanner;

public class ProducerConsumerExample {
    public static void start() {
        ProductMarket market = new ProductMarket();
        Thread healthProducer = new Producer(market, "health");
        Thread timeProducer = new Producer(market, "time");
        Thread consumer = new Consumer(market);
        healthProducer.start();
        timeProducer.start();
        consumer.start();
        Scanner sc = new Scanner(System.in);
        String command = "";
        while (!command.equals("stop")) {
            command = sc.next();
        }
        healthProducer.interrupt();
        timeProducer.interrupt();
        consumer.interrupt();
    }
}

class Producer extends Thread {
    private ProductMarket market;
    private int product_id;
    private String product_name;

    public Producer(ProductMarket market, String product_name) {
        // TODO Auto-generated constructor stub
        this.market = market;
        this.product_id = 0;
        this.product_name = product_name;
    }

    @Override
    public void run() {
        // TODO Auto-generated method stub
        try {
            while (!this.isInterrupted()) {
                ++product_id;
                Product item = new Product();
                item.setId(product_id);
                item.setName(product_name);
                market.insert(item);
                System.out.println("Producer "+product_name+" product: " + product_name + " "
                        + product_id);
                Thread.sleep(2000);
            }
        } catch (InterruptedException e) {
            // TODO: handle exception
            //e.printStackTrace();
        }
    }
}

class Consumer extends Thread {
    private ProductMarket market;

    public Consumer(ProductMarket market) {
        // TODO Auto-generated constructor stub
        this.market = market;
    }

    @Override
    public void run() {
        // TODO Auto-generated method stub
        try {
            while (!this.isInterrupted()) {
                Product item = new Product();
                item = market.remove();
                System.out.println("Consumer consume: " + item.getName()+" "+item.getId());
                Thread.sleep(2000);
            }
        } catch (InterruptedException e) {
            // TODO: handle exception
            //e.printStackTrace();
        }
    }
}

interface ProductPool {
    void insert(Product item);

    Product remove();
}

class ProductMarket implements ProductPool {
    private static final int SIZE = 9;
    private int count;
    private int in;
    private int out;
    private volatile Product[] market;

    public ProductMarket() {
        // TODO Auto-generated constructor stub
        count = 0;
        in = 0;
        out = 0;
        market = new Product[SIZE];
    }

    @Override
    public Product remove() {
        // TODO Auto-generated method stub
        Product item;
        while (count == 0) {
            ;
        }
        --count;
        item = market[out];
        out = (out + 1) % SIZE;

        return item;
    }

    @Override
    public synchronized void insert(Product item) {
        // TODO Auto-generated method stub
        while (count == SIZE) {
            ;
        }
        ++count;
        market[in] = item;
        in = (in + 1) % SIZE;
    }

}

class Product {
    private int id;
    private String name;

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public int getId() {
        return id;
    }

    public void setId(int id) {
        this.id = id;
    }
}

运行结果:

Producer health product: health 1
Producer time product: time 1
Consumer consume: health 1
Producer health product: health 2
Producer time product: time 2
Consumer consume: time 1
Producer health product: health 3
Producer time product: time 3
Consumer consume: health 2
stop

关于wait和notify的实例,可以看这篇文章http://zhangjunhd.blog.51cto.com/113473/71387/

关于monitor和lock的讨论,详细出处参考Java并行(2): Monitor

    Monitor其实是一种同步工具,也可以说是一种同步机制,它通常被描述为一个对象,主要特点是:

  • 对象的所有方法都被“互斥”的执行。好比一个Monitor只有一个运行“许可”,任一个线程进入任何一个方法都需要获得这个“许可”,离开时把许可归还。
  • 通常提供singal机制:允许正持有“许可”的线程暂时放弃“许可”,等待某个谓词成真(条件变量),而条件成立后,当前进程可以“通知”正在等待这个条件变量的线程,让他可以重新去获得运行许可。

    Monitor对象可以被多线程安全地访问。关于“互斥”与“为什么要互斥”,我就不傻X兮兮解释了;而关于Monitor的singal机制,历史上曾经出现过两大门派,分别是Hoare派和Mesa派(上过海波老师OS课的SS同学应该对这个有印象),我还是用我的理解通俗地庸俗地解释一下:

  • Hoare派的singal机制江湖又称“Blocking condition variable”,特点是,当“发通知”的线程发出通知后,立即失去许可,并“亲手”交给等待者,等待者运行完毕后再将许可交还通知者。在这种机制里,可以等待者拿到许可后,谓词肯定为真——也就是说等待者不必再次检查条件成立与否,所以对条件的判断可以使用“if”,不必“while”
  • Mesa派的signal机制又称“Non-Blocking condition variable”, 与Hoare不同,通知者发出通知后,并不立即失去许可,而是把闻风前来等待者安排在ready queue里,等到schedule时有机会去拿到“许可”。这种机制里,等待者拿到许可后不能确定在这个时间差里是否有别的等待者进入过Monitor,因此不能保证谓词一定为真,所以对条件的判断必须使用“while”

    这两种方案可以说各有利弊,但Mesa派在后来的盟主争夺中渐渐占了上风,被大多数实现所采用,有人给这种signal另外起了个别名叫“notify”,想必你也知道,Java采取的就是这个机制。

    在Java的设计中,每一个对象自打娘胎里出来,就带了一把看不见的锁,通常我们叫“内部锁”,或者“Monitor锁”,或者“Intrinsic lock”。为了装逼起见,我们就叫它Intrinsic lock吧。

  Java采取了wait/notify机制来作为intrinsic lock 相关的条件变量,表示为等待某一条件成立的条件队列——说到这里顺带插一段,条件队列必然与某个锁相关,并且语义上关联某个谓词(条件队列、锁、条件谓词就是吉祥的一家)。所以,在使用wait/notify方法时,必然是已经获得相关锁了的,在进一步说,一个推论就是“wait/notify  方法只能出现在相应的同步块中”。

    public void deposit(int amount){
        balance +=amount;
        notify();
    }

    //或者这样:
    public void deposit(int amount){
        synchronized (lock) {
            balance +=amount;
            notify();
        }
    }

这两段都是错的,第一段没有在同步块里,而第二段拿到的是lock的内部锁,调用的却是this.notify(),让人遗憾。运行时他们都会抛IllegalMonitorStateException异常——唉,想前一阵我参加一次笔试的时候,有一道题就是这个,让你选所给代码会抛什么异常,我当时就傻了,想这考得也太偏了吧,现在看看,确实是很基本的概念,当初被虐是压根没有理解wait/notify机制的缘故。那怎么写是对的呢?

    public void deposit(int amount){
        synchronized (lock) {
            balance +=amount;
            lock.notify();
        }
    }
//或者(取决于你采用的锁):
    synchronized public void deposit(int amount){
        balance +=amount;
        notify();
    }

看上去,Java的内部锁和wait/notify机制已经可以满足任何同步需求了,不是吗?em…可以这么说,但也可以说,不那么完美。有两个问题:

  • 锁不够用

    有时候,我们的类里不止有一个状态,这些状态是相互独立的,如果只用同一个内部锁来维护他们全部,未免显得过于笨拙,会严重影响吞吐量。你马上会说,你刚才不是演示了用任意一个Object来做锁吗?我们多整几个Object分别加锁不就行了吗?没错,是可行的。但这样可能显得有些丑陋,而且Object来做锁本身就有语义不明确的缺点。

  • 条件变量不够用

    Java用wait/notify机制实际上默认给一个内部锁绑定了一个条件队列,但是,有时候,针对一个状态(锁),我们的程序需要两个或以上的条件队列,比如,刚才的Account例子,如果某个2B银行有这样的规定“一个账户存款不得多于10000元”,这个时候,我们的存钱需要满足“余额+要存的数目不大于10000,否则等待,直到满足这个限制”,取钱需要满足“余额足够,否则等待,直到有钱为止”,这里需要两个条件队列,一个等待“存款不溢出”,一个等待“存款足够”,这时,一个默认的条件队列够用么?你可能又说,够用,我们可以模仿network里的“多路复用”,一个队列就能当多个来使,像这样:

public class Account {
    public static final int BOUND = 10000;
    private int balance;
    
    public Account(int balance) {
        this.balance = balance;
    }
    
    synchronized public boolean withdraw(int amount) throws InterruptedException{
            while(balance<amount)
                wait();// no money, wait
            balance -= amount;
            notifyAll();// not full, notify
            return true;
    }
    
    synchronized public void deposit(int amount) throws InterruptedException{
            while(balance+amount >BOUND)
                wait();//full, wait
            balance +=amount;
            notifyAll();// has money, notify
    }
}

  不是挺好吗?恩,没错,是可以。但是,仍然存在性能上的缺陷:每次都有多个线程被唤醒,而实际只有一个会运行,频繁的上下文切换和锁请求是件很废的事情。我们能不能不要notifyAll,而每次只用notify(只唤醒一个)呢?不好意思,想要“多路复用”,就必须notifyAll,否则会有丢失信号之虞(不解释了)。只有满足下面两个条件,才能使用notify:
  一,只有一个条件谓词与条件队列相关,每个线程从wait返回执行相同的逻辑。
  二,一进一出:一个对条件变量的通知,语义上至多只激活一个线程。

关于monitor lock的讨论,详细出处参考http://blog.163.com/hsh8523@126/blog/static/218935592011214114257822/

在JVM的规范中,有这么一些话:  “在JVM中,每个对象和类在逻辑上都是和一个监视器相关联的,为了实现监视器的排他性监视能力,JVM为每一个对象和类都关联一个锁,锁住了一个对象,就是获得对象相关联的监视器”
监视器好比一做建筑,它有一个很特别的房间,房间里有一些数据,而且在同一时间只能被一个线程占据,进入这个建筑叫做"进入监视器",进入建筑中的那个特别的房间叫做"获得监视器",占据房间叫做"持有监视器",离开房间叫做"释放监视器",离开建筑叫做"退出监视器". 而一个锁就像一种任何时候只允许一个线程拥有的特权. 一个线程可以允许多次对同一对象上锁.对于每一个对象来说,java虚拟机维护一个计数器,记录对象被加了多少次锁,没被锁的对象的计数器是0,线程每加锁一次,计数器就加1,每释放一次,计数器就减1.当计数器跳到0的时候,锁就被完全释放了.
Java提供了synchronized关键字来支持内在锁。Synchronized关键字可以放在方法的前面、对象的前面、类的前面。

posted on 2013-04-11 11:04  夜月升  阅读(273)  评论(0)    收藏  举报

导航