最近复习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;
}
}
这样,通过以上方法我们就很好的解决的第二个编号生成策略问题。