最近复习Java多线程时,看到”生产者消费者问题“—— 这是个多线程并发访问的经典案列,操作系统知识中也讲到过,详细内容就不在此列出了,如果有不明的,可以参考我的另一篇文章”多线程经典案例——生产者/消费者问题的Java实现与详解“。
在这个问题中,为了输出更加友好的信息,我按”生产日期+生产总数"对产品进行了编号。实现这一策略的思路是:生产者每生产出一个Product,记录Product总量的静态计数器都会加1,由于是多线程并发,编号就存在重复的风险,所以必须对计数器进行锁定。但任一线程的产出的Product对象都是不同的,所以只能用类锁来进行同步。这是一种比较通用的解决方法:
/** * 产品类 */ class Product { //已生产的产品总数 private static Integer totalProduct = 0; //产品id(生产日期+生产总数) private String id = null; public Product() { this.id = generateId(); } private String generateId(){ //类锁 由于所有线程的Product对象不同,故只能用类锁使任何使用该类对象的线程在此处进行同步 synchronized(Product.class){ ++totalProduct; String genId = new Timestamp(System.currentTimeMillis()).toString().replaceAll( "[-,:, ,.]", "") + "-" + totalProduct; return genId; } } public String getId() { return id; } }
以上编号策略还是比较好实现的,不过,现在请大家考虑一下另一种编号策略:这一策略要求产品按“生产者编号+该生产者生产的产品总数”来编号,当然,也可以在前面加个生产日期,但这并不是重点。对于这个问题,由于是初学者,我花了不少时间思考,走了不少弯路,最后才找到自认为比较有效的方法,下面是我的分享:
这个问题可以分为如下几个子问题:
- 首先,生产者的编号如何产生?
- 其次,单个生产者生产的产品总数如何产生?(最关键的)
- 最后,合并成最终的编号。
对于第一个问题,如果你有看过我之前的文章,你应该知道怎么解决了吧。实际上,解决方法非常简单,先给每个生产者线程命名,像这样:
//建立一个存储区 ProductStack ss = new ProductStack(); //添加生产者并为线程命名 Producer p1 = new Producer(ss); p1.setName("NO.1P"); Producer p2 = new Producer(ss); p2.setName("NO.2P");
要注意的是:
setName方法是Producer的父类Thread的方法,如果你要在子类重写该方法,那么,也必须调用该父类方法,否者线程将采用默认的如Thread-0、Thread-1这样的命名,显然,这是非常不友好的,且很难管理。
再来看看生产者类:
/** * 生产者 (如要多继承的话,可以实现Runnable接口) */ class Producer extends Thread{ //持有一个存储区(前面的文章中有该存储区的实现代码) ProductStack ss; public Producer(ProductStack ss) { this.ss = ss; } /** * 生产产品 */ public void run() { //存储区开放时一直生产 while (ss.isStackOpen()) { try { //模拟生产一个产品的所需的时间 Thread.sleep(10); } catch (InterruptedException e) { e.printStackTrace(); } Product pt = new Product(); //将生产的产品放到存储区中 ss.push(pt); } } }
接下来,我们只要在generateId中调用Thread.currentThread.getName()就可以获得生产者的编号了,像这样:
private String generateId(){ ... String ProducerId = Thread.currentThread().getName(); ... } }
解决了第一个问题以后,让我们来看看第二个:
首先,为了获得单个生产者所生产的产品总数,我们必须要为该生产者维护一个静态的计数器,每当该生产者(线程)new一个产品时,该计数器加1。但基于同一个类的线程的静态成员并不属于单个线程,而是被所有同类线程所共享,也就是说,这个静态计数器不能作为Producer这个类的静态成员,因为所有生产者线程都是基于该类创建的,它只存在一个实例化的对象,并不是某个线程所私有的。
那我们该如何为基于同一类的每个线程建立私有的变量呢?
事实上,Java为我们提供一个有效的解决方法——使用ThreadLocal!
什么是ThreadLocal?
在博主麦田的这篇博客中对ThreadLocal有很好的解释,他推荐的一些关于ThreadLocal的资料也非常好,尤其是这篇《理解 ThreadLocal》,大家可以看看。
以下是从该博客中摘抄的一些内容:
- 一个线程局部变量(ThreadLocal variables)为每个线程方便地提供了一个单独的变量副本,每个线程修改副本时不影响其它线程对象的副本。
- ThreadLocal 实例通常作为静态的私有的(private static)字段出现在一个类中,这个类用来关联一个线程。当多个线程访问 ThreadLocal 实例时,每个线程维护 ThreadLocal 提供的独立的变量副本。
从上面的解释可以看出,第一,ThreadLocal变量可以每个线程提供一个私有的变量,这当然也适用于基于同一个类的所有线程,第二,ThreadLocal 实例通常作为静态的私有的字段出现在一个类中,为每个线程提供了私有的变量,也就是说,这个类是每个线程都要访问的,所以对于我们的问题而言,这个要建立ThreadLacal变量的类就是Produce类,因为它被每个Producer线程所访问。下面是具体的实现:
/** * 产品类 * */ class Production { /* * ThreadLocal变量为每个访问该类的线程维护一个Integer类型的变量副本, * ThreadLocal类中有一个Map,用于存储每一个线程的变量副本,Map中元素的键为线程对象,而值对应线程的变量副本。 * 通过匿名内部类覆盖ThreadLocal的initialValue()方法,指定初始值。 */ private static ThreadLocal<Integer> sequenceNum = new ThreadLocal<Integer>(){ @Override protected Integer initialValue() { return 0; } }; //产品id(生产者编号+该生产者生产的产品总数) private String id = null; public Production() { this.id = generateId(); } /* * 生成产品编号 */ private String generateId(){ //设置当前线程的线程局部变量的值,即当前生产者(线程)生产的产品的总数 sequenceNum.set(sequenceNum.get() + 1); //获得生产者编号 String ProducerId = Thread.currentThread().getName(); //返回当前线程所对应的线程局部变量,即当前生产者(线程)生产的产品的总数。 String nextSeqNum = String.valueOf(sequenceNum.get()); //返回合成的编号 return ProducerId + " " + nextSeqNum; } }这样,通过以上方法我们就很好的解决的第二个编号生成策略问题。