`

多线程-实战篇

    博客分类:
  • Java
阅读更多

在进入实战篇以前,我们简单说一下多线程编程的一般原则。

  [安全性]是多线程编程的首要原则,如果两个以上的线程访问同一对象时,一个线程会损坏另一个线程的数据,这就是违反了安全性原则,这样的程序是不能进入实际应用的。

  安全性的保证可以通过设计安全的类和程序员的手工控制。如果多个线程对同一对象访问不会危及安全性,这样的类就是线程安全的类,在JAVA中比如String类就被设计为线程安全的类。而如果不是线程安全的类,那么就需要程序员在访问这些类的实例时手工控制它的安全性。

  [可行性]是多线程编程的另一个重要原则,如果仅仅实现了安全性,程序却在某一点后不能继续执行或者多个线程发生死锁,那么这样的程序也不能作为真正的多线程程序来应用。

  相对而言安全性和可行性是相互抵触的,安全性越高的程序,可性行会越低。要综合平衡。

  [高性能] 多线程的目的本来就是为了增加程序运行的性能,如果一个多线程完成的工作还不如单线程完成得快。那就不要应用多线程了。

  高性能程序主要有以下几个方面的因素:

  数据吞吐率,在一定的时间内所能完成的处理能力。

  响应速度,从发出请求到收到响应的时间。

  容量,指同时处理雅致同任务的数量。

  安全性和可行性是必要条件,如果达到不这两个原则那就不能称为真正的多线程程序。而高性是多线程编程的目的,也可以说是充要条件。否则,为什么采用多线程编程呢?

 

[生产者与消费者模式]

  首先以一个生产者和消费者模式来进入实战篇的第一节。

  生产者和消费者模式中保护的是谁?

  多线程编程都在保护着某些对象,这些个对象是"紧俏资源",要被最大限度地利用,这也是采用多线程方式的理由。在生产者消费者模式中,我们要保护的是"仓库",在我下面的这个例子中,

就是桌子(table)。

  我这个例子的模式完全是生产者-消费者模式,但我换了个名字。厨师-食客模式,这个食堂中只有1张桌子,同时最多放10个盘子,现在有4个厨师做菜,每做好一盘就往桌子上放(生产者将产品往仓库中放),而有6个食客不停地吃(消费者消费产品,为了说明问题,他们的食量是无限的)。

  一般而言,厨师200-400ms做出一盘菜,而食客要400-600ms吃完一盘。当桌子上放满了10个盘子后,所有厨师都不能再往桌子上放,而当桌子是没有盘子时,所有的食客都只好等待。

  下面我们来设计这个程序:

  因为我们不知道具体是什么菜,所以叫它food:

 class Food{}

  然后是桌子,因为它要有序地放而且要有序地取(不能两个食客同时争取第三盘菜),所以我们扩展LinkedList,或者你用聚合把一个LinkedList作为属性也能达到同样的目的,例子中我是用

继承,从构造方法中传入一个可以放置的最大值。

class Table extends java.util.LinkedList{
  int maxSize;
  public Table(int maxSize){
    this.maxSize = maxSize;
  }
}

现在我们要为它加两个方法,一是厨师往上面放菜的方法,一是食客从桌子上拿菜的方法。

放菜:因为一张桌子由多个厨师放菜,所以厨师放菜的要被同步,如果桌子上已经有十盘菜了。所有厨师就要等待:

 public synchronized void putFood(Food f){
    while(this.size() >= this.maxSize){
      try{
        this.wait();
      }catch(Exception e){}
    }
    this.add(f);
    notifyAll();
  }

拿菜:同上面,如果桌子上一盘菜也没有,所有食客都要等待:

 public synchronized Food getFood(){
    while(this.size() <= 0){
      try{
        this.wait();
      }catch(Exception e){}
    }
    Food f = (Food)this.removeFirst();
    notifyAll();
    return f;
  }

厨师类:

  由于多个厨师要往一张桌子上放菜,所以他们要操作的桌子应该是同一个对象,我们从构造方法中将桌子对象传进去以便控制在主线程中只产生一张桌子。

厨师做菜要用一定的时候,我用在make方法中用sleep表示他要消耗和时候,用200加上200的随机数保证时间有200-400ms中。做好后就要往桌子上放。

这里有一个非常重要的问题一定要注意,就是对什么范围同步的问题,因为产生竞争的是桌子,所以所有putFood是同步的,而我们不能把厨师自己做菜的时间也放在同步中,因为做菜是各自做的。同样食客吃菜的时候也不应该同步,只有从桌子中取菜的时候是竞争的,而具体吃的时候是各自在吃。所以厨师类的代码如下:

 class Chef extends Thread{
  Table t;
  Random r = new Random(12345);
  public Chef(Table t){
    this.t = t;
  }
  public void run(){
    while(true){
      Food f = make();
      t.putFood(f);
    }
  }
  private Food make(){

    try{
      Thread.sleep(200+r.nextInt(200));
    }catch(Exception e){}
    return new Food();
  }
}

同理我们产生食客类的代码如下:

class Eater extends Thread{
  Table t;
  Random r = new Random(54321);
  public Eater(Table t){
    this.t = t;
  }
  public void run(){
    while(true){
      Food f = t.getFood();
      eat(f);
    }
  }
  private void eat(Food f){
    
    try{
      Thread.sleep(400+r.nextInt(200));
    }catch(Exception e){}
  }
}

完整的程序在这儿:

package debug;
import java.util.regex.*;
import java.util.*;

class Food{}

class Table extends LinkedList{
  int maxSize;
  public Table(int maxSize){
    this.maxSize = maxSize;
  }
  public synchronized void putFood(Food f){
    while(this.size() >= this.maxSize){
      try{
        this.wait();
      }catch(Exception e){}
    }
    this.add(f);
    notifyAll();
  }
  
  public synchronized Food getFood(){
    while(this.size() <= 0){
      try{
        this.wait();
      }catch(Exception e){}
    }
    Food f = (Food)this.removeFirst();
    notifyAll();
    return f;
  }
}

class Chef extends Thread{
  Table t;
  String name;
  Random r = new Random(12345);
  public Chef(String name,Table t){
    this.t = t;
    this.name = name;
  }
  public void run(){
    while(true){
      Food f = make();
      System.out.println(name+" put a Food:"+f);
      t.putFood(f);
    }
  }
  private Food make(){
    try{
      Thread.sleep(200+r.nextInt(200));
    }catch(Exception e){}
    return new Food();
  }
}

class Eater extends Thread{
  Table t;
  String name;
  Random r = new Random(54321);
  public Eater(String name,Table t){
    this.t = t;
    this.name = name;
  }
  public void run(){
    while(true){
      Food f = t.getFood();
      System.out.println(name+" get a Food:"+f);
      eat(f);
      
    }
  }
  private void eat(Food f){
    
    try{
      Thread.sleep(400+r.nextInt(200));
    }catch(Exception e){}
  }
}

public class Test {
    public static void main(String[] args) throws Exception{
      Table t = new Table(10);
      new Chef("Chef1",t).start();
      new Chef("Chef2",t).start();
      new Chef("Chef3",t).start();
      new Chef("Chef4",t).start();
      new Eater("Eater1",t).start();
      new Eater("Eater2",t).start();
      new Eater("Eater3",t).start();
      new Eater("Eater4",t).start();
      new Eater("Eater5",t).start();
      new Eater("Eater6",t).start();

    }
}

 

这一个例子中,我们主要关注以下几个方面:

  1.同步方法要保护的对象,本例中是保护桌子,不能同时往上放菜或同时取菜。

  假如我们把putFood方法和getFood方法在厨师类和食客类中实现,那么我们应该如此:

(以putFood为例)

class Chef extends Thread{
  Table t;
  String name;
  public Chef(String name,Table t){
    this.t = t;
    this.name = name;
  }
  public void run(){
    while(true){
      Food f = make();
      System.out.println(name+" put a Food:"+f);
      putFood(f);
    }
  }
  private Food make(){
    Random r = new Random(200);
    try{
      Thread.sleep(200+r.nextInt());
    }catch(Exception e){}
    return new Food();
  }
  public void putFood(Food f){//方法本身不能同步,因为它同步的是this.即Chef的实例

    synchronized (t) {//要保护的是t
      while (t.size() >= t.maxSize) {
        try {
          t.wait();
        }
        catch (Exception e) {}
      }
      t.add(f);
      t.notifyAll();
    }
  }
}

  2.同步的范围,在本例中是放和取两个方法,不能把做菜和吃菜这种各自不相干的工作放在受保护的范围中。

  3.参与者与容积比

   对于生产者和消费者的比例,以及桌子所能放置最多菜的数量三者之间的关系是影响性能的重要因素,如果是过多的生产者在等待,则要增加消费者或减少生产者的数据,反之则增加生产者或减少消费者的数量。

  另外如果桌子有足够的容量可以很大程序提升性能,这种情况下可以同时提高生产者和消费者的数量,但足够大的容时往往你要有足够大的物理内存。 

 

[一个线程在进入对象的休息室(调用该对象的wait()方法)后会释放对该对象的锁],基于这个原因。在同步中,除非必要,否则你不应用使用Thread.sleep(long l)方法,因为sleep方法并不释放对象的锁。

  这是一个极其恶劣的品德,你自己什么事也不干,进入sleep状态,却抓住竞争对象的监视锁不让其它需要该对象监视锁的线程运行,简单说是极端自私的一种行为。但我看到过很多程序员仍然有在同步方法中调用sleep的代码。

  看下面的例子:

package debug;

class SleepTest{

  public synchronized void wantSleep(){
    try{
      Thread.sleep(1000*60);

    }catch(Exception e){}
    System.out.println("111");
  }
  
  public synchronized void say(){
    System.out.println("123");
  }
}

class T1 extends Thread{
  SleepTest st;
  public T1(SleepTest st){
    this.st = st;
  }
  public void run(){
    st.wantSleep();
  }
}

class T2 extends Thread{
  SleepTest st;
  public T2(SleepTest st){
    this.st = st;
  }
  public void run(){
    st.say();
  }
}

public class Test {
    public static void main(String[] args) throws Exception{
      SleepTest st = new SleepTest();
      new T1(st).start();
      new T2(st).start();
    }
}

  我们看到,线程T1的实例运行后,当前线程抓住了st实例的锁,然后进入了sleep。直到它睡满60秒后才运行到System.out.println("111");然后run方法运行完成释放了对st的监视锁,线程T2的实例才得到运行的机会。

  而如果我们把wantSleep方法改成:

  public synchronized void wantSleep(){
    try{
      //Thread.sleep(1000*60);
      this.wait(1000*60);
    }catch(Exception e){}
    System.out.println("111");
  }

  我们看到,T2的实例所在的线程立即就得到了运行机会,首先打印了123,而T1的实例所在的线程仍然等待,直到等待60秒后运行到System.out.println("111");方法。

  所以,调用wait(long l)方法不仅达到了阻塞当前线程规定时间内不运行,而且让其它有竞争需求的线程有了运行机会,这种利人不损己的方法,何乐而不为?这也是一个有良心的程序员应该遵循的原则。

  当一个线程调用wait(long l)方法后,线程如果继续运行,你无法知道它是等待时间完成了还是在wait时被其它线程唤醒了,如果你非常在意它一定要等待足够的时间才执行某任务,而不希望是中途被唤醒,这里有一个不是非常准确的方法:

 long l = System.System.currentTimeMillis();

wait(1000);//准备让当前线程等待1秒
while((System.System.currentTimeMillis() - l) < 1000)//执行到这里说明它还没有等待到1秒
                             //是让其它线程给闹醒了

    wait(1000-(System.System.currentTimeMillis()-l));//继续等待余下的时间.

  这种方法不是很准确,但基本上能达到目的。

  所以在同步方法中,除非你明确知道自己在干什么,非要这么做的话,你没有理由使用sleep,wait方法足够达到你想要的目的。而如果你是一个很保守的人,看到上面这段话后,你对sleep方法深恶痛绝,坚决不用sleep了,那么在非同步的方法中(没有和其它线程竞争的对象),你想让当前线程阻塞一定时间后再运行,应该如何做呢?(这完全是一种卖弄,在非同步的方法中你就应该合理地应用sleep嘛,但如果你坚决不用sleep,那就这样来做吧)

    public static mySleep(long l){
        Object o = new Object();
        synchronized(o){
            try{
                o.wait(l);    
            }catch(Exception e){}
        }
    }

  放心吧,没有人能在这个方法外调用o.notify[All],所以o.wait(l)会一直等到设定的时间才会运行完成。

[虚拟锁的使用]

  虚拟锁简单说就是不要调用synchronized方法(它等同于synchronized(this))和不要调用synchronized(this),这样所有调用在这个实例上的所有同步方法的线程只能有一个线程可以运行。也就是说:

  如果一个类有两个同步方法 m1,m2,那么不仅是两个以上线调用m1方法的线程只有一个能运行,就是两个分别调用m1,m2的线程也只有一个能运行。当然非同步方法不存在任何竞争,在一个线程获取该对象的监视锁后这个对象的非同步方法可以被任何线程调用。

  而大多数时候,我们可能会出现这种情况,多个线程调用m1时需要保护一种资源,而多个线程调用M2时要保护的是另一种资源,如果我们把m1,m2都设成同步方法。两个分别调用这两个方法的线程其实并不产生冲突,但它们都要获取这个实例的锁(同步方法是同步this)而产生了不必要竞争。

  所以这里应该采用虚拟锁。

  即将m1和m2方法中各自保护的对象作为属性a1,a2传进来,然后将同步方法改为方法的同步块分别以a1,a2为参数,这样到少是不同线程调用这两个不同方法时不会产生竞争,当然如果m1,m2方法都操作同一受保护对象则两个方法还是应该作为同步方法。这也是应该将方法同步还是采用同步块的理由之一。

package debug;

class SleepTest{

  public synchronized void m1(){

    System.out.println("111");
    try{
      Thread.sleep(10000);
    }catch(Exception e){}
  }
  
  public synchronized void m2(){
    System.out.println("123");
    
  }
}

class T1 extends Thread{
  SleepTest st;
  public T1(SleepTest st){
    this.st = st;
  }
  public void run(){
    st.m1();
  }
}

class T2 extends Thread{
  SleepTest st;
  public T2(SleepTest st){
    this.st = st;
  }
  public void run(){
    st.m2();
  }
}

public class Test {
    public static void main(String[] args) throws Exception{
      SleepTest st = new SleepTest();
      new T1(st).start();
      new T2(st).start();
    }
}

  这个例子可以看到两个线程分别调用st实例的m1和m2方法却因为都要获取st的监视锁而产生了竞争。T2实例要在T1运行完成后才能运行(间隔了10秒)。而假设m1方法要操作操作一个文件 f1,m2方法要操作一个文件f2,当然我们可以在方法中分别同步f1,f2,但现在还不知道f2,f2是否存在,如果不存在我们就同步了一个null对象,那么我们可以使用虚拟锁:

package debug;

class SleepTest{

  String vLock1 = "vLock1";
  String vLock2 = "vLock2";
  public void m1(){
    synchronized(vLock1){
      System.out.println("111");
      try {
        Thread.sleep(10000);
      }
      catch (Exception e) {}
      //操作f1
    }
  }
  
  public void m2(){
     synchronized(vLock2){
       System.out.println("123");
       //操作f2
     }    
  }
}

class T1 extends Thread{
  SleepTest st;
  public T1(SleepTest st){
    this.st = st;
  }
  public void run(){
    st.m1();
  }
}

class T2 extends Thread{
  SleepTest st;
  public T2(SleepTest st){
    this.st = st;
  }
  public void run(){
    st.m2();
  }
}

public class Test {
    public static void main(String[] args) throws Exception{
      SleepTest st = new SleepTest();
      new T1(st).start();
      new T2(st).start();
    }
}

  我们看到两个分别调用m1和m2的线程由于它们获取不同对象的监视锁,它们没有任何竞争就正常运行,只有这两个线程同时调用m1或m2才会产生阻塞。 

 

转载自dev2dev网友axman的go deep into java专栏。

分享到:
评论

相关推荐

    Java多线程编程实战指南-核心篇

    《Java多线程编程实战指南-核心篇》是一本深入探讨Java并发编程的书籍,旨在帮助读者掌握在Java环境中创建、管理和同步线程的核心技术。Java的多线程能力是其强大之处,使得开发者能够在同一时间执行多个任务,提高...

    多线程编程实战指南-核心篇

    《多线程编程实战指南-核心篇》是针对Java开发者深入理解并掌握多线程编程的一本实战性书籍。在当今的并发计算环境中,多线程技术是必不可少的知识点,它能够有效地利用多核处理器资源,提高程序的执行效率。本书以...

    Java多线程编程实战指南(核心篇)

    Java多线程编程实战指南(核心篇) 高清pdf带目录 随着现代处理器的生产工艺从提升处理器主频频率转向多核化,即在一块芯片上集成多个处理器内核(Core),多核处理器(Multicore Processor)离我们越来越近了――如今...

    java 多线程编程实战指南(核心 + 设计模式 完整版)

    《Java多线程编程实战指南》这本书深入浅出地讲解了Java多线程的核心概念和实战技巧,分为核心篇和设计模式篇,旨在帮助开发者掌握并应用多线程技术。 1. **线程基础** - **线程的创建**:Java提供了两种创建线程...

    java多线程编程实战指南 核心篇 代码

    《Java多线程编程实战指南(核心篇)》以基本概念、原理与方法为主线,辅以丰富的实战案例和生活化实例,并从Java虚拟机、操作系统和硬件多个层次与角度出发,循序渐进、系统地介绍Java平台下的多线程编程核心技术及...

    Java-jdk10-最新最全多线程编程实战指南-核心篇

    《Java-jdk10-最新最全多线程编程实战指南-核心篇》是一本深入探讨Java多线程编程的专著,针对Java 10版本进行了全面的更新和优化。这本书聚焦于Java多线程的核心概念和技术,旨在帮助开发者理解和掌握如何在并发...

    多线程编程——实战篇

    ### 多线程编程实战篇知识点详解 #### 一、多线程编程的基本原则 多线程编程在软件开发中占据着重要的地位,特别是在需要高效利用计算机资源的应用场景下。多线程编程的核心在于如何有效地管理和协调多个线程之间...

    Java多线程编程实战指南-设计模式篇

    《Java多线程编程实战指南(设计模式篇)》采用Java(JDK1.6)语言和UML 为描述语言,并结合作者多年工作经历的相关实战案例,介绍了多线程环境下常用设计模式的来龙去脉:各个设计模式是什么样的及其典型的实际应用...

    Java多线程编程实战指南(设计模式篇)

    《Java多线程编程实战指南(设计模式篇)》由黄文海撰写,是一本深入探讨Java多线程编程和设计模式的专业书籍。书中详细介绍了如何在Java环境中利用多线程来实现高效的并发处理,同时结合设计模式,帮助开发者更好地...

    Java多线程编程实战指南 设计模式篇.rar

    在Java编程中,多线程是一项关键技能,它允许程序同时执行多个任务,极大地提高了程序的...通过阅读"Java多线程编程实战指南 设计模式篇.pdf",你将获得更深入的理论知识和实践技巧,为你的编程事业奠定坚实的基础。

    Java多线程编程实战指南核新篇&设计篇&以及和核新篇的案例代码

    《Java多线程编程实战指南》是一本深入探讨Java并发编程的书籍,涵盖了核心篇与设计模式篇。这本书旨在帮助开发者理解和掌握Java平台上的多线程编程,提升系统性能和可扩展性。在Java世界中,多线程是实现并发处理、...

    Python应用实战:python多线程-多线程安全问题&lock与rlock.zip

    本篇文章将深入探讨Python中的多线程安全问题以及如何使用锁(Lock)和可重入锁(RLock)来解决这些问题。 首先,我们要理解什么是线程安全。线程安全是指在多线程环境下,一个函数或方法被多个线程调用时,不会...

    Java多线程编程实战指南+设计模式篇

    本资源“Java多线程编程实战指南+设计模式篇”深入探讨了这两个主题,旨在帮助开发者提升其在并发编程和软件设计上的技能。 **一、Java多线程** 1. **线程基础**:Java中的线程是程序执行的最小单位。Java通过`...

    Java多线程编程实战指南+设计模式篇(全部)

    本资源“Java多线程编程实战指南+设计模式篇(全部)”提供了全面的学习材料,帮助开发者深入理解并熟练应用这两个主题。 首先,我们来探讨Java多线程。多线程是Java中并发处理的关键特性,允许程序同时执行多个任务...

    Java多线程编程实战指南:设计模式篇

    本书以理论结合示例的方式介绍了多线程常见设计模式。

    Java多线程编程实战指南 设计模式篇

    《Java多线程编程实战指南 设计模式篇》是一本深度探讨Java并发编程与设计模式融合的书籍。在Java编程中,多线程是提升系统性能、实现并行计算的关键技术,而设计模式则是解决常见问题的最佳实践。本书旨在帮助...

    QT/C++学习指南 - 实战篇

    QT/C++学习指南实战篇是针对想要深入理解QT框架和C++编程语言的开发者们的一份宝贵资源。这个指南通过一系列生动有趣的实例项目,帮助学习者掌握QT库的应用和C++编程技巧。以下是对每个示例项目的详细解读: 1. **...

    Java多线程编程实战指南设计模式篇.mobi

    Java多线程编程实战指南设计模式篇.mobi

    读书笔记:Java多线程编程实战指南核心篇.zip

    读书笔记:Java多线程编程实战指南核心篇

Global site tag (gtag.js) - Google Analytics