Java5 多线程(一)--入门篇

来源:互联网 发布:淘宝怎么找小二介入 编辑:程序博客网 时间:2024/06/05 21:57
首先回顾一下JDK1.5之前的线程相关的知识:

1 线程的入门.
        什么是线程,线程就是程序执行的线索,Java是面向对象的语言什么类来表示这样一个东西呢?Thread.
通过start()方法启动它,线程所要执行的任务放在run()方法里面,下面可以看一下run()方法里面的源码

创建线程的两种传统方式(注: Runnable类并不是一个线程,它只是线程一个执行单元):

打开Thread的构造方法,

然后可以跟进看到init()方法具体的实现.其中有一行代码就是对target(Runnable类型)的赋值,因为线程所执行的任务都在run()方法里面,那么在run()方法里面,target就不为null,然后就调用了Runnale的run()方法.因为我们重写了Runnable的run()方法,那么最终执行的就是我们所覆写的run()方法.具体代码如下:

      如果我们同时实现了Thread的run()方法又同时覆盖了Runnable的run()方法.那么到底会执行哪个的run()方法呢?
根据Java的多态,肯定执行的是Thread的run()方法.因为我们覆写了Thread的run()方法,那么所执行的就是我们run()方法,而不是

2 传统的定时器:
        定时器通过Timer这个类来描述,通过schedule()方法来调度,定时执行的任务通过TimerTask来定义.
下面来实现一个简单的定时器,功能如下,每隔2秒执行一次,之后隔4秒执行一次,然后又隔2秒,就这样轮循下去.具体用法可以查看API里面有详细介绍.
[java] view plaincopy
  1. public static void main(String[] args) {  
  2.     new Timer().schedule(new MyTimerTask(), 2000);  
  3.     try {  
  4.         while (true) {  
  5.         System.out.println(new Date().getSeconds());  
  6.         Thread.sleep(1000);  
  7.     }  
  8. catch (InterruptedException e) {  
  9.     e.printStackTrace();  
  10.         }  
  11.     }  
  12.     }  
  13. class MyTimerTask extends TimerTask {  
  14.     static int count = 0;  
  15.     @Override  
  16.     public void run() {  
  17.         count = (count + 1) % 2;//count=0或1  
  18.         System.out.println("boming");  
  19.         Timer timer = new Timer();  
  20.         timer.schedule(new MyTimerTask(), 2000 + (2000) * count);  
  21.     }  

3 线程之间的互斥和同步通信
当两个线程去同时操作一个字符串,那么可能会出现线程安全问题.这样的情况可以用银行转帐来解释.
下面的代码就会出现问题,
[java] view plaincopy
  1. public static void main(String[] args) {  
  2. final Outputer outputer = new Outputer();  
  3. new Thread() {  
  4. @Override  
  5. public void run() {  
  6. while (true) {  
  7. try {  
  8. Thread.sleep(100);  
  9. catch (InterruptedException e) {  
  10. e.printStackTrace();  
  11. }  
  12. outputer.print("zhangsan");  
  13. }  
  14. }  
  15. }.start();  
  16. new Thread() {  
  17. @Override  
  18. public void run() {  
  19. while (true) {  
  20. try {  
  21. Thread.sleep(100);  
  22. catch (InterruptedException e) {  
  23. e.printStackTrace();  
  24. }  
  25. outputer.print("zhangxiaoxiang");  
  26. }  
  27. }  
  28. }.start();  
  29. }  
  30. }  
  31. class Outputer {  
  32. public void print(String name) {  
  33. for (int i = 0; i < name.length(); i++) {  
  34. System.out.print(name.charAt(i));  
  35. }  
  36. System.out.println();// 打印完字符串换行  
  37. }  
  38. }  

我们使用两个线程去调用print(String name)方法,当第一个方法还没有执行完毕,第二个方法来执行,那么打印出来的name就会出现为问题.如下图所示,

现在我们要实现的是,只有当第一个线程执行完毕后,第二个线程才能执行print(String name)方法,这就必须互斥或者说同步.
我们知道实现同步可以使用同步代码块或者同步方法,想到同步(Synchronized)那么自然而然就想到同步监视器.
这是两个很重要的概念.
现在我们来改造上面Outputer的print(String name)方法.
[java] view plaincopy
  1. public void print(String name) {  
  2. //synchronized()里面的参数就是同步监视器  
  3. //然而这里使用name作为同步监视器是不行的,  
  4. //因为要实现原子性(互斥)必须要使用同一个监视器对象  
  5. //当第一个线程来执行该代码块,name对象是一个String对象  
  6. //当第二个线程来执行,name对象又是另一个String对象,  
  7. //这样就不能实现同步  
  8. synchronized (name) {  
  9. for (int i = 0; i < name.length(); i++) {  
  10. System.out.print(name.charAt(i));  
  11. }  
  12. System.out.println();// 打印完字符串换行  
  13. }  
  14. }   
执行结果如下所示:

我们可以通过this关键字作为同步监视器,因为从上面定义两个线程的代码来看,我们只new了一次Outputer对象,所以this代表同一个对象.
 
现在来通过同步方法来实现同步,
[java] view plaincopy
  1. //同步方法也同样也有同步监视器,它是this  
  2. public synchronized void print2(String name) {  
  3.     for (int i = 0; i < name.length(); i++) {  
  4.         System.out.print(name.charAt(i));  
  5.     }  
  6.     System.out.println();// 打印完字符串换行  
  7. }  
把第二个线程改成使用print2(String name)方法.这样的话就需要print2和print这两个方法互斥.这个怎么理解呢?
上面我们是对print()这个一个方法进行互斥,现在呢?需要对两个方法进行互斥.
我们可以这样比喻(对一个方法进行互斥):假设一个茅坑(print(String name)),上面有一把锁(this对象),现在一个人(Thread)来上厕所,它把钥匙放进了口袋,第二个人(Thread2)来上厕所,因为没有钥匙,必须要等第一个人出来,把钥匙放上去,第二个人才能拿着钥匙进去.这是对一个方法进行同步,
(对两个方法或者更多进行同步)),现在有多个茅坑(print(String name),print2(String name)),只有一个钥匙(同步监视器),那么当一个人(Thread)进去后,拿了那仅有的一个钥匙,就算其他人(Thread)想进入的没有人占的茅坑也不行,因为没有钥匙.
        这样的话,打印name的时候就不会出现问题.
现在还有一种情况:
[java] view plaincopy
  1. //静态的同步方法同样也有同步监视器,它是class  
  2. public static synchronized void print3(String name) {  
  3. for (int i = 0; i < name.length(); i++) {  
  4. System.out.print(name.charAt(i));  
  5. }  
  6. System.out.println();// 打印完字符串换行  
  7. }   
这样的话要想互斥就必须把同步监视器改成Outputer.class了,在内存中只有一份.
 
线程之间的同步通信
通过一道面试提来解释.
子线程循环10次,接着主线程循环100,接着又回到子线程循环10次,接着再回到主线程又循环100,如此循环50次,请写出程序。
[java] view plaincopy
  1. //静态的同步方法同样也有同步监视器,它是class  
  2. public static synchronized void print3(String name) {  
  3. for (int i = 0; i < name.length(); i++) {  
  4. System.out.print(name.charAt(i));  
  5. }  
  6. System.out.println();// 打印完字符串换行  
  7. }   
  8. 这样的话要想互斥就必须把同步监视器改成Outputer.class了,在内存中只有一份.  
  9.   
  10. 线程之间的同步通信  
  11. 通过一道面试提来解释.  
  12. 子线程循环10次,接着主线程循环100,接着又回到子线程循环10次,接着再回到主线程又循环100,如此循环50次,请写出程序。  
  13. public static void main(String[] args) {  
  14. new Thread(new Runnable() {  
  15. @Override  
  16. public void run() {  
  17. for (int k = 1; k <= 50; k++) {  
  18. for (int i = 1; i <= 10; i++) {  
  19. System.out.println("sub thread sequence " + i  
  20. " loop of " + k);  
  21. }  
  22. }  
  23. }  
  24. }).start();  
  25. for (int k = 1; k <= 50; k++) {  
  26. for (int i = 1; i <= 100; i++) {  
  27. System.out  
  28. .println("main thread sequence " + i + " loop of " + k);  
  29. }  
  30. }  
  31. }   
[java] view plaincopy
  1.   


这样主要的程序逻辑是实现了,但是执行的次序乱来,子线程执行10次不应该别打断,主线程执行100次也不应该被打断.
所以我们自然就想到了同步,只需要把子循环使用同步代码块,但是用什么作为同步监视器呢?this显然不行的.当然该类的字节码class是可以的,但是这样有2个问题,
第一,虽然实现了同步,但是,不是子线程一次,主线程一次,所以在子/主(线程)次序上还是乱了.
第二,使用class作为同步监视器不好,如果程序逻辑很复杂,需要多组需要互斥,使用class作为同步监视器,那么就成了一组了.所以这也不好.(关于多组互斥可以查看博客http://blog.csdn.net/johnny901114/article/details/7854666)


经验:要用到共同数据(包括同步锁)或共同算法的若干个方法,应该归在同一个类上,这种设计体现了高内聚和程序的健壮性.
比如:


据此,我们可以这样设计
class Business {
publicsynchronizedvoid sub(int k) {
    for (int i = 1; i <= 10; i++) {
        System.out.println("sub thread sequence " + i +" loop of " + k);
    }
}
publicsynchronizedvoid main(int k) {
    for (int i = 1; i <= 100; i++) {
        System.out.println("main thread sequence " + i +" loop of " + k);
    }
}
}
这样就把相关的方法写到一个类里面了.但是这里还是没有解决通信问题. 最终代码如下:
publicstaticvoid main(String[] args) {
    final Business business =new Business();
    new Thread(new Runnable() {
    @Override
    publicvoid run() {
        for (int k = 1; k <= 50; k++) {
            business.sub(k);
         }
    }
    }).start();
    for (int k = 1; k <= 50; k++) {
    business.main(k);
    }
    }
}
class Business {
    //默认子线程先执行
    booleanisShouldSub =true;
publicsynchronizedvoid sub(int k) {
    if(!isShouldSub){//此处用while最好,因为可能出现假唤醒,//用while的话还会重新判断,这样程序更加严谨和健壮
        try {
            this.wait();//this表示同步监视器对象
        } catch (InterruptedException e) {
            e.printStackTrace();
            }
      }
    for (int i = 1; i <= 10; i++) {
            System.out.println("sub thread sequence " + i +" loop of " + k);
     }
    //子线程做完了,把它置为false
    isShouldSub =false;
    //并且唤醒主线程
    this.notify();
}
publicsynchronizedvoid main(int k) {
    if(isShouldSub){){//此处用while最好,因为可能出现假唤醒(API文档里有介绍),//用while的话还会重新判断,这样程序更加严谨和健壮
        try {
            this.wait();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
    for (int i = 1; i <= 100; i++) {
        System.out.println("main thread sequence " + i +" loop of " + k);
    }
    //主线程做完了,把它置为true
    isShouldSub =true;
    //并且唤醒子线程
    this.notify();
    }
}

4,线程范围内共享数据.(ThreadLocal)
下面通过一个简单的示例来描述线程之间非共享数据.
[java] view plaincopy
  1. private static int k = 0;  
  2. public static void main(String[] args) {  
  3. for (int i = 0; i < 2; i++) {  
  4.     new Thread(new Runnable() {  
  5.         @Override  
  6.         public void run() {  
  7.             k = new Random().nextInt();  
  8.                 System.out.println(Thread.currentThread().getName()  
  9.                     + " put value to i " + k);  
  10.             new A().get();  
  11.             new B().get();  
  12.         try {  
  13.                 Thread.sleep(10);  
  14.             } catch (InterruptedException e) {  
  15.                     e.printStackTrace();  
  16.                 }  
  17.             }  
  18.         }).start();  
  19.     }  
  20. }  
  21. //模块A  
  22. static class A {  
  23. public void get() {  
  24.     System.out.println("A from " +Thread.currentThread().getName() + " get     value "+ k);  
  25.     }  
  26. }  
  27. //模块B  
  28. static class B {  
  29.     public void get() {  
  30.         System.out.println("A from " +Thread.currentThread().getName() + " get value "+ k);  
  31.     }  
  32. }   

现在我们需要这样的效果,假设线程0给i赋值为1,那么当线程0取的时候也是1,也就是说线程之间取各自放进去的值.而上面的程序达不到这样的要求. 这就需要线程范围内的数据共享.
那么我们可以这样来实现,这也是线程范围内数据共享的原理.
定义一个Map集合key和value分别为Thread和Integer.
把给i赋值的代码替换为
int k =new Random().nextInt();
map.put(Thread.currentThread(), k);
get()方法内的代码改为
System.out.println("A from " + Thread.currentThread().getName()
" get value " + map.get(Thread.currentThread()));
这样的话就实现了线程范围内的数据共享了,线程取得值是各自放进去的.
这有什么用呢?比如事务,所谓事务的回滚和提交指的是在一个线程上的,如果是在不同的线程上,那么逻辑就乱了.这不是我们想要的,这样的话我们就可以通过线程范围内共享数据,也就是把连接绑定到该线程上,那么在该线程获取的连接是同一个连接.
下面通过ThreadLocal来实现这样的功能.
[java] view plaincopy
  1. public class ThreadLocalTest {   
  2.     public static void main(String[] args) {  
  3.         for (int i = 0; i < 2; i++) {  
  4.             new Thread(new Runnable() {  
  5.                 @Override  
  6.                 public void run() {  
  7.                     int k = new Random().nextInt();  
  8.                     ThreadShareData.getThreadShareData().setAge(k);  
  9.                     ThreadShareData.getThreadShareData().setName("name" + k);  
  10.   
  11.                     System.out.println(Thread.currentThread().getName()  
  12.                             + " put value to i " + k);  
  13.                     new A().get();  
  14.                     new B().get();  
  15.                     try {  
  16.                         Thread.sleep(10);  
  17.                     } catch (InterruptedException e) {  
  18.                         e.printStackTrace();  
  19.                     }  
  20.                 }  
  21.             }).start();  
  22.         }  
  23.     }  
  24.   
  25.     // 模块A  
  26.     static class A {  
  27.         public void get() {  
  28.             ThreadShareData data = ThreadShareData.getThreadShareData();  
  29.             System.out.println("A from " + Thread.currentThread().getName()  
  30.                     + " get value " + data.getName() + "--" + data.getAge());  
  31.         }  
  32.     }  
  33.   
  34.     // 模块B  
  35.     static class B {  
  36.         public void get() {  
  37.             ThreadShareData data = ThreadShareData.getThreadShareData();  
  38.             System.out.println("B from " + Thread.currentThread().getName()  
  39.                     + " get value " + data.getName() + "--" + data.getAge());  
  40.         }  
  41.     }  
  42. }  
  43.   
  44. class ThreadShareData {  
  45.     private static ThreadLocal<ThreadShareData> local = new ThreadLocal<ThreadShareData>();  
  46.     private ThreadShareData() {  
  47.     }  
  48.     public static ThreadShareData getThreadShareData() {  
  49.         ThreadShareData data = local.get();  
  50.         if (data == null) {  
  51.             data = new ThreadShareData();  
  52.             local.set(data);  
  53.         }  
  54.         return data;  
  55.     }  
  56.     private String name;  
  57.     private int age;  
  58.     public String getName() {  
  59.         return name;  
  60.     }  
  61.     public void setName(String name) {  
  62.         this.name = name;  
  63.     }  
  64.     public int getAge() {  
  65.         return age;  
  66.     }  
  67.     public void setAge(int age) {  
  68.         this.age = age;  
  69.     }  
  70. }  

上面的例子,对于线程范围内共享对象是一个比较优雅的设计方案,ThreadShareData 有name和age两个属性,这个类的实例是与每个线程相关的.那么这个设计就交给这个类自己吧,其他用户在任意线程调用我这个类的方法,自然而然就是与线程相关的实例.因为里面我们封装了一个ThreadLocal对象.
        那么我们是否考虑到如果成千上万的线程来访问,那么是不是可能会导致内存溢出呢?
其实当一个线程死亡,那么系统会把该线程在ThreadLocal产生的数据清除掉,


5,多个线程访问共享对象和数据的方式:
1>如果每个线程执行的代码相同,额可以使用相同的Runnable对象,这个Runnable对象中有那个共享数据,例如,买票系统可以这么来实现
[java] view plaincopy
  1. public static void main(String[] args) {   
  2.         MyRunnable myRunnable = new MyRunnable();  
  3.         new Thread(myRunnable).start();  
  4.         new Thread(myRunnable).start();  
  5.         new Thread(myRunnable).start();  
  6.         new Thread(myRunnable).start();  
  7.     }  
  8.     static class MyRunnable implements Runnable {  
  9.         int count = 100;  
  10.         @Override  
  11.         public void run() {  
  12.             synchronized (this) {//同步  
  13.                 while (true) {  
  14.                     if (count > 0) {  
  15.                         try {  
  16.                             //模拟线程安全问题,所以要同步/互斥  
  17.                             Thread.sleep(10);  
  18.                         } catch (InterruptedException e) {  
  19.                             e.printStackTrace();  
  20.                         }  
  21.                         count--;  
  22.                     } else {  
  23.                         break;  
  24.                     }  
  25.                     System.out.println(count);  
  26.                 }  
  27.             }  
  28.         }  
  29.     }  

2>如果每个线程执行的代码不同,比如一个线程对一个整形执行加操作,另一个线程对该整形进行减操作.
这时候需要用不同的Runnable对象,有如下三种方式来实现这些Runnable对象的数据共享.
        ①将共享数据封装在另外一个对象中,然后将这个对象逐一传递给各个Runnable对象,每个线程对共享数据的操作方法也分配到那个对象身上去完成,这样容易实现针对该数据进行各个操作的互斥和通信.
[java] view plaincopy
  1. public static void main(String[] args) {   
  2.        ShareData shareData = new ShareData();  
  3.        new Thread(new MyRunnable(shareData)).start();  
  4.        new Thread(new MyRunnable2(shareData)).start();  
  5.    }  
  6.   
  7.    static class MyRunnable implements Runnable {  
  8.        private ShareData shareData;  
  9.   
  10.        public MyRunnable(ShareData shareData) {  
  11.            this.shareData = shareData;  
  12.        }  
  13.   
  14.        @Override  
  15.        public void run() {  
  16.            shareData.increase();  
  17.        }  
  18.    }  
  19.    static class MyRunnable2 implements Runnable {  
  20.        private ShareData shareData;  
  21.   
  22.        public MyRunnable2(ShareData shareData) {  
  23.            this.shareData = shareData;  
  24.        }  
  25.   
  26.        @Override  
  27.        public void run() {  
  28.            shareData.decrease();  
  29.        }  
  30.    }  
  31.    static class ShareData {  
  32.        int count = 100;  
  33.   
  34.        public void increase() {  
  35.            count++;  
  36.        }  
  37.   
  38.        public void decrease() {  
  39.            count--;  
  40.        }  
  41.    }  

 ②将这些Runnable对象作为某一类中的内部类,共享数据作为这个外部类中的成员变量,每个线程对共享数据的操作方法也分配给外部类,以便实现对共享数据进行各个操作的互斥和通信,作为内部类的各个Runnable对象调用外部类的这些方法.
[java] view plaincopy
  1. static ShareData shareData = new ShareData();   
  2.     public static void main(String[] args) {  
  3.         //final ShareData shareData = new ShareData();   
  4.         new Thread(new Runnable() {  
  5.             @Override  
  6.             public void run() {  
  7.                 shareData.decrease();  
  8.             }  
  9.         }).start();  
  10.         new Thread(new Runnable() {  
  11.             @Override  
  12.             public void run() {  
  13.                 shareData.increase();  
  14.             }  
  15.         }).start();  
  16.     }  
  17.     static class ShareData {  
  18.         int count = 100;  
  19.   
  20.         public void increase() {  
  21.             count++;  
  22.         }  
  23.   
  24.         public void decrease() {  
  25.             count--;  
  26.         }  
  27.     }  

    ③上面两种方式的组合:将共享数据封装在另一个对象中,每个线程对共享数据的操作方法也分配到那个对象身上去完成,对象作为这个外部类中的成员变量或者方法中的局部变量,每个线程的Runnable对象作为外部类中的成员内部类或者局部内部类.
[java] view plaincopy
  1. public class ThreadTest1    
  2.  {   
  3.  private int j;   
  4.  public static void main(String args[]){   
  5.     ThreadTest1 tt=new ThreadTest1();   
  6.     Inc inc=tt.new Inc();   
  7.     Dec dec=tt.new Dec();   
  8.     for(int i=0;i<2;i++){   
  9.         Thread t=new Thread(inc);   
  10.         t.start();   
  11.             t=new Thread(dec);   
  12.         t.start();   
  13.         }   
  14.     }   
  15.  private synchronized void inc(){   
  16.     j++;   
  17.     System.out.println(Thread.currentThread().getName()+"-inc:"+j);   
  18.     }   
  19.  private synchronized void dec(){   
  20.     j--;   
  21.     System.out.println(Thread.currentThread().getName()+"-dec:"+j);   
  22.     }   
  23.  class Inc implements Runnable{   
  24.     public void run(){   
  25.         for(int i=0;i<100;i++){   
  26.         inc();   
  27.         }   
  28.     }   
  29.  }   
  30.  class Dec implements Runnable{   
  31.     public void run(){   
  32.         for(int i=0;i<100;i++){   
  33.         dec();   
  34.         }   
  35.     }   
  36.  }   
  37.  }           

总之,要同步互斥的几段代码最好分别放在几个独立的方法中,这些方法再放在同一个类中,这样比较容易实现他们之间的同步互斥和通信.

转载请注明出处: http://blog.csdn.net/johnny901114/article/details/8695668
原创粉丝点击