由不同编号生成策略产生的多线程问题及解决

最近复习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》,大家可以看看。


以下是从该博客中摘抄的一些内容:

  1. 一个线程局部变量(ThreadLocal variables)为每个线程方便地提供了一个单独的变量副本,每个线程修改副本时不影响其它线程对象的副本
  2. 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;
		}
	}	
这样,通过以上方法我们就很好的解决的第二个编号生成策略问题。

由不同编号生成策略产生的多线程问题及解决,布布扣,bubuko.com

由不同编号生成策略产生的多线程问题及解决

上一篇:关于Java中传递参数的若干问题


下一篇:“NASA”计划背后,阿里巴巴大数据系统架构概述