代码改变世界

Android多线程全新讲解(Java_JDK1.5)

2012-05-05 10:31  java线程例子  阅读(1169)  评论(0编辑  收藏  举报

前言:

该博文笔者很久以前就写好了,今天和大家分享,希望对大家有帮助,内容来自网上著名IT人士张孝祥老师.视频可以到网上下载.

多线程非常重要,早些年笔者记得在杭州面试的时候对淘宝外包企业,对Java基本功要求很高,当时是电话面试,答的一摊糟,现在特定把自己学到的知识和拙见和大家分享.

期待大家的批评指正...

主要内容:

1,线程的入门;
2 传统的定时器: 
3 线程之间的互斥和同步通信
4,线程范围内共享数据.(ThreadLocal)
5,多个线程访问共享对象和数据的方式
6 Java5线程池
7,Callable和Future的用法
8, Lock和Condition实现线程同步通信,Semaphore实现信号灯
10,JDK1.5 CyclicBarrier同步的工具类
11,JDK1.5 CountDownLatch 同步工具
12,JDK1.5 Exchanger同步工具
13,JDK1.5 ArrayBlockingQueue阻塞队列
14,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里面有详细介绍.
public static void main(String[] args) {
    new Timer().schedule(new MyTimerTask(), 2000);
    try {
        while (true) {
        System.out.println(new Date().getSeconds());
        Thread.sleep(1000);
    }
} catch (InterruptedException e) {
    e.printStackTrace();
        }
    }
    }
class MyTimerTask extends TimerTask {
    static int count = 0;
    @Override
    public void run() {
        count = (count + 1) % 2;//count=0或1
        System.out.println("boming");
        Timer timer = new Timer();
        timer.schedule(new MyTimerTask(), 2000 + (2000) * count);
    }

3 线程之间的互斥和同步通信
当两个线程去同时操作一个字符串,那么可能会出现线程安全问题.这样的情况可以用银行转帐来解释.
下面的代码就会出现问题,
public static void main(String[] args) {
final Outputer outputer = new Outputer();
new Thread() {
@Override
public void run() {
while (true) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
outputer.print("zhangsan");
}
}
}.start();
new Thread() {
@Override
public void run() {
while (true) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
outputer.print("zhangxiaoxiang");
}
}
}.start();
}
}
class Outputer {
public void print(String name) {
for (int i = 0; i < name.length(); i++) {
System.out.print(name.charAt(i));
}
System.out.println();// 打印完字符串换行
}
}

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

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

我们可以通过this关键字作为同步监视器,因为从上面定义两个线程的代码来看,我们只new了一次Outputer对象,所以this代表同一个对象.
 
现在来通过同步方法来实现同步,
//同步方法也同样也有同步监视器,它是this
public synchronized void print2(String name) {
    for (int i = 0; i < name.length(); i++) {
        System.out.print(name.charAt(i));
    }
    System.out.println();// 打印完字符串换行
}
把第二个线程改成使用print2(String name)方法.这样的话就需要print2和print这两个方法互斥.这个怎么理解呢?
上面我们是对print()这个一个方法进行互斥,现在呢?需要对两个方法进行互斥.
我们可以这样比喻(对一个方法进行互斥):假设一个茅坑(print(String name)),上面有一把锁(this对象),现在一个人(Thread)来上厕所,它把钥匙放进了口袋,第二个人(Thread2)来上厕所,因为没有钥匙,必须要等第一个人出来,把钥匙放上去,第二个人才能拿着钥匙进去.这是对一个方法进行同步,
(对两个方法或者更多进行同步)),现在有多个茅坑(print(String name),print2(String name)),只有一个钥匙(同步监视器),那么当一个人(Thread)进去后,拿了那仅有的一个钥匙,就算其他人(Thread)想进入的没有人占的茅坑也不行,因为没有钥匙.
        这样的话,打印name的时候就不会出现问题.
现在还有一种情况:
//静态的同步方法同样也有同步监视器,它是class
public static synchronized void print3(String name) {
for (int i = 0; i < name.length(); i++) {
System.out.print(name.charAt(i));
}
System.out.println();// 打印完字符串换行
} 
这样的话要想互斥就必须把同步监视器改成Outputer.class了,在内存中只有一份.
 
线程之间的同步通信
通过一道面试提来解释.
子线程循环10次,接着主线程循环100,接着又回到子线程循环10次,接着再回到主线程又循环100,如此循环50次,请写出程序。
//静态的同步方法同样也有同步监视器,它是class
public static synchronized void print3(String name) {
for (int i = 0; i < name.length(); i++) {
System.out.print(name.charAt(i));
}
System.out.println();// 打印完字符串换行
} 
这样的话要想互斥就必须把同步监视器改成Outputer.class了,在内存中只有一份.

线程之间的同步通信
通过一道面试提来解释.
子线程循环10次,接着主线程循环100,接着又回到子线程循环10次,接着再回到主线程又循环100,如此循环50次,请写出程序。
public static void main(String[] args) {
new Thread(new Runnable() {
@Override
public void run() {
for (int k = 1; k <= 50; k++) {
for (int i = 1; i <= 10; i++) {
System.out.println("sub thread sequence " + i
+ " loop of " + k);
}
}
}
}).start();
for (int k = 1; k <= 50; k++) {
for (int i = 1; i <= 100; i++) {
System.out
.println("main thread sequence " + i + " loop of " + k);
}
}
} 


这样主要的程序逻辑是实现了,但是执行的次序乱来,子线程执行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)
下面通过一个简单的示例来描述线程之间非共享数据.
private static int k = 0;
public static void main(String[] args) {
for (int i = 0; i < 2; i++) {
    new Thread(new Runnable() {
        @Override
        public void run() {
            k = new Random().nextInt();
                System.out.println(Thread.currentThread().getName()
                    + " put value to i " + k);
            new A().get();
            new B().get();
        try {
                Thread.sleep(10);
            } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }).start();
    }
}
//模块A
static class A {
public void get() {
    System.out.println("A from " +Thread.currentThread().getName() + " get     value "+ k);
    }
}
//模块B
static class B {
    public void get() {
        System.out.println("A from " +Thread.currentThread().getName() + " get value "+ k);
    }
} 

现在我们需要这样的效果,假设线程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来实现这样的功能.
public class ThreadLocalTest { 
    public static void main(String[] args) {
        for (int i = 0; i < 2; i++) {
            new Thread(new Runnable() {
                @Override
                public void run() {
                    int k = new Random().nextInt();
                    ThreadShareData.getThreadShareData().setAge(k);
                    ThreadShareData.getThreadShareData().setName("name" + k);

                    System.out.println(Thread.currentThread().getName()
                            + " put value to i " + k);
                    new A().get();
                    new B().get();
                    try {
                        Thread.sleep(10);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }).start();
        }
    }

    // 模块A
    static class A {
        public void get() {
            ThreadShareData data = ThreadShareData.getThreadShareData();
            System.out.println("A from " + Thread.currentThread().getName()
                    + " get value " + data.getName() + "--" + data.getAge());
        }
    }

    // 模块B
    static class B {
        public void get() {
            ThreadShareData data = ThreadShareData.getThreadShareData();
            System.out.println("B from " + Thread.currentThread().getName()
                    + " get value " + data.getName() + "--" + data.getAge());
        }
    }
}

class ThreadShareData {
    private static ThreadLocal<ThreadShareData> local = new ThreadLocal<ThreadShareData>();
    private ThreadShareData() {
    }
    public static ThreadShareData getThreadShareData() {
        ThreadShareData data = local.get();
        if (data == null) {
            data = new ThreadShareData();
            local.set(data);
        }
        return data;
    }
    private String name;
    private int age;
    public String getName() {
        return name;
    }
    public void setName(String name) {
        this.name = name;
    }
    public int getAge() {
        return age;
    }
    public void setAge(int age) {
        this.age = age;
    }
}

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


5,多个线程访问共享对象和数据的方式:
1>如果每个线程执行的代码相同,额可以使用相同的Runnable对象,这个Runnable对象中有那个共享数据,例如,买票系统可以这么来实现
public static void main(String[] args) { 
        MyRunnable myRunnable = new MyRunnable();
        new Thread(myRunnable).start();
        new Thread(myRunnable).start();
        new Thread(myRunnable).start();
        new Thread(myRunnable).start();
    }
    static class MyRunnable implements Runnable {
        int count = 100;
        @Override
        public void run() {
            synchronized (this) {//同步
                while (true) {
                    if (count > 0) {
                        try {
                            //模拟线程安全问题,所以要同步/互斥
                            Thread.sleep(10);
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                        count--;
                    } else {
                        break;
                    }
                    System.out.println(count);
                }
            }
        }
    }

2>如果每个线程执行的代码不同,比如一个线程对一个整形执行加操作,另一个线程对该整形进行减操作.
这时候需要用不同的Runnable对象,有如下三种方式来实现这些Runnable对象的数据共享.
        ①将共享数据封装在另外一个对象中,然后将这个对象逐一传递给各个Runnable对象,每个线程对共享数据的操作方法也分配到那个对象身上去完成,这样容易实现针对该数据进行各个操作的互斥和通信.
 public static void main(String[] args) { 
        ShareData shareData = new ShareData();
        new Thread(new MyRunnable(shareData)).start();
        new Thread(new MyRunnable2(shareData)).start();
    }

    static class MyRunnable implements Runnable {
        private ShareData shareData;

        public MyRunnable(ShareData shareData) {
            this.shareData = shareData;
        }

        @Override
        public void run() {
            shareData.increase();
        }
    }
    static class MyRunnable2 implements Runnable {
        private ShareData shareData;

        public MyRunnable2(ShareData shareData) {
            this.shareData = shareData;
        }

        @Override
        public void run() {
            shareData.decrease();
        }
    }
    static class ShareData {
        int count = 100;

        public void increase() {
            count++;
        }

        public void decrease() {
            count--;
        }
    }

 ②将这些Runnable对象作为某一类中的内部类,共享数据作为这个外部类中的成员变量,每个线程对共享数据的操作方法也分配给外部类,以便实现对共享数据进行各个操作的互斥和通信,作为内部类的各个Runnable对象调用外部类的这些方法.
static ShareData shareData = new ShareData(); 
    public static void main(String[] args) {
        //final ShareData shareData = new ShareData(); 
        new Thread(new Runnable() {
            @Override
            public void run() {
                shareData.decrease();
            }
        }).start();
        new Thread(new Runnable() {
            @Override
            public void run() {
                shareData.increase();
            }
        }).start();
    }
    static class ShareData {
        int count = 100;

        public void increase() {
            count++;
        }

        public void decrease() {
            count--;
        }
    }

    ③上面两种方式的组合:将共享数据封装在另一个对象中,每个线程对共享数据的操作方法也分配到那个对象身上去完成,对象作为这个外部类中的成员变量或者方法中的局部变量,每个线程的Runnable对象作为外部类中的成员内部类或者局部内部类.
   public class ThreadTest1  
    { 
    private int j; 
    public static void main(String args[]){ 
       ThreadTest1 tt=new ThreadTest1(); 
       Inc inc=tt.new Inc(); 
       Dec dec=tt.new Dec(); 
       for(int i=0;i<2;i++){ 
           Thread t=new Thread(inc); 
           t.start(); 
               t=new Thread(dec); 
           t.start(); 
           } 
       } 
    private synchronized void inc(){ 
       j++; 
       System.out.println(Thread.currentThread().getName()+"-inc:"+j); 
       } 
    private synchronized void dec(){ 
       j--; 
       System.out.println(Thread.currentThread().getName()+"-dec:"+j); 
       } 
    class Inc implements Runnable{ 
       public void run(){ 
           for(int i=0;i<100;i++){ 
           inc(); 
           } 
       } 
    } 
    class Dec implements Runnable{ 
       public void run(){ 
           for(int i=0;i<100;i++){ 
           dec(); 
           } 
       } 
    } 
    }         

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

6 Java5线程池
        线程池的概念与Executors类的应用
首先介绍在TCP服务器编程模型的原理,当一个客户端连接到服务器,服务器要起一个线程为之服,当客户端的回话结束时,线程也就结束了,即每一个客户端连接,服务器 就要为之创建一个新的线程,这好比假设每个报名学员都要通过我来亲自接 待,以便给每个学员一种好的感觉,但每个学员报名手续花费半个小时,对于 50名学生,我一个个接待和为之办手续,显然是不实际的,那么我会怎么做呢
?我会先接待每个学员,打完招呼后,再把他分配给一名工作人员去办理手续 ,这样我就接待了每一名学员.
         如果访问服务器的客户端很多,那么服务器要不断的创建和销毁线程, 这样将严重影响服务器的性能,如果真的来一名学员,我们就安排一名新的 工作人员为之服务,这也是不可能的,那么公司岂不是要招很多工作人员.
应该是一名工作人员服务完一名学员,空闲下来后,一旦有新的学员要服务,
我们安排该工作人员,为之服务.
        线程池的概念于此类似,首先创建一些线程,他们的集合称为线程池,
当服务器接收到一个客户请求后,就从线程池中取出一个空闲的线程为之服
务,服务完成后不关闭该线程,而是将该线程还回到线程池中.
        在线程池的编程模式下,任务是交给整个线程池,而不是直接交给某个线
程,线程池拿到任务偶,他就在内部找有空闲的线程,再把任务交给内部某个
空闲的线程,这就是封装.记住,任务是交给整个线程池,但可以同时向一个线程池中提交多个任务.
创建固定大小的线程池:
ExecutorService threadPool = Executors.newFixedThreadPool(3);
threadPool.execute(Runnable runnable);
 
创建缓存的线程池:
如果线程不够自动创建新的线程满足服务
ExecutorService threadPool = Executors.newCachedThreadPool();
threadPool.execute(Runnable runnable);
 
创建单一线程池.
线程里面只有一个线程,如果该线程意外死亡,那么系统会自动创建一个新的线程来代替.
ExecutorService threadPool = Executors.newSingleThreadExecutor();
threadPool.execute(Runnable runnable);
 
//执行完任务后关闭线程池,
threadPool.shutdown();
//不管任务是否完成都关闭线程池
threadPool.shutdownNow();
 
用线程池启动定时器.
Executors.newScheduledThreadPool(3).schedule(new Runnable(){
@Override
publicvoid run() {
}
}, 20, TimeUnit.SECONDS);


7,Callable和Future的用法
 
通过这两个类可以得到线程执行后返回的结果.
        ExecutorService executorService = Executors.newSingleThreadExecutor();
        //通过submit方法提交任务
        Future<String> future = executorService.submit(new Callable<String>() {
            @Override
            public String call() throws Exception {
                return "hello world";
            }
        });
 
        System.out.println("等待返回的结果");
        try {
            System.out.println("返回的结果,"+future.get());
        } catch (Exception e) {
            e.printStackTrace();
        }
Future取得的结果类型和Callable返回的结果类型必须一致,这是通过泛型来实现的.
submit()方法返回的Future对象可以取消任务.
 
CompletionService用于提交一组Callable任务,其take()方法返回已完成的一个Callable任务对应的Future对象
ExecutorService executorService2 = Executors.newFixedThreadPool(3);
        CompletionService<Integer> service = new ExecutorCompletionService<Integer>(
                executorService2);
        for (int i = 0; i < 10; i++) {
            final int k = i;
                service.submit(new Callable<Integer>() {
                @Override
                public Integer call() throws Exception {
                    Thread.sleep(new Random().nextInt(5000));
                    return k;
                }
            });
        }
 
        try {
            for(int i = 0; i < 10; i++){
                System.out.println(service.take().get());
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
 
8, Lock和Condition实现线程同步通信
1>Lock:
        Lock比传统线程模型中的Synchronied方式更加面向对象,与生活中的锁类似,锁本身也应该是一个对象.两个线程执行的代码段要实现同步互斥的效果,它们必须用同一个Lock对象,锁是在代表要操作的资源的类的内部方法中,而不是线程代码中.
        上面的输出器Outputer类就可以这样改写:
class Outputer2 {
    // 声明一个锁
    Lock lock = new ReentrantLock();
 
    public void print(String name) {
        //把要互斥的代码写在lock()和unlock()方法之间
        lock.lock();
        try {
            for (int i = 0; i < name.length(); i++) {
                System.out.print(name.charAt(i));
            }
            System.out.println();// 打印完字符串换行
        } finally{
            //如果中途抛出异常,那么这把锁就没有被解锁,别人就进不来了
            //所以写在finally里面
            lock.unlock();
        }
    }
}
 
读写锁,分为读锁和写锁,多个读锁不互斥,读锁和写锁互斥,写锁与写锁互斥,这是JVM自己控制的,你只要上好相应的锁即可,如果你的代码只读数据,可以很多人同时读,但不能同时写,那就上读锁;如果你的代码修改数据,只能有一个人在写,且不能同时读取,那就上写锁.总之,读的时候上读锁,写的时候上写锁!
 
看如下程序: 新建6个线程,3个线程用来读,3个线程用来写,
final Queue3 q3 = new Queue3();
        for (int i = 0; i < 3; i++) {
            new Thread() {
                public void run() {
                    while (true) {
                        q3.get();
                    }
                }
            }.start();
            new Thread() {
                public void run() {
                    while (true) {
                        q3.put(new Random().nextInt(10000));
                    }
                }
            }.start();
        }
然后在编写一个类Queue3 里面有一个读方法和写方法:
class Queue3 {
    private Object data = null;// 共享数据,只能有一个线程能写该数据,但可以有多个线程同时读该数据。
    //读写锁
    ReadWriteLock rwl = new ReentrantReadWriteLock();
 
    // 相当于读操作
    public void get() {
        rwl.readLock().lock();
        try {
            System.out.println(Thread.currentThread().getName()
                    + " be ready to read data!");
            Thread.sleep((long) (Math.random() * 1000));
            System.out.println(Thread.currentThread().getName()
                    + "have read data :" + data);
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            rwl.readLock().unlock();
        }
    }
 
    // 相当于写操作
    public void put(Object data) {
        rwl.writeLock().lock();
        try {
            System.out.println(Thread.currentThread().getName()
                    + " be ready to write data!");
            Thread.sleep((long) (Math.random() * 1000));
            this.data = data;
            System.out.println(Thread.currentThread().getName()
                    + " have write data: " + data);
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            rwl.writeLock().unlock();
        }
    }
}
这样可以实现正常的逻辑,如果我们把读写锁相关的代码注释,发现程序正准备写的时候,就有线程读了,发现准备读的时候,有线程去写,这样不符合我们的逻辑
通过Java5的新特新可以很轻松的解决这样的问题.
查看Java API ReentrantReadWriteLock 上面有经典(缓存)的用法.这是上面的伪代码.
class CachedData {
   Object data;
   volatile boolean cacheValid;
   final ReentrantReadWriteLock rwl = new ReentrantReadWriteLock();

   void processCachedData() {
     rwl.readLock().lock();
     if (!cacheValid) {
        // Must release read lock before acquiring write lock
        rwl.readLock().unlock();
        rwl.writeLock().lock();
        try {
          // Recheck state because another thread might have
          // acquired write lock and changed state before we did.

          if (!cacheValid) {
            data = ...
            cacheValid = true;
          }
          // Downgrade by acquiring read lock before releasing write lock
          rwl.readLock().lock();
        } finally  {
          rwl.writeLock().unlock(); // Unlock write, still hold read
        }
     }
     try {
       use(data);
     } finally {
       rwl.readLock().unlock();
     }
   }
}
基于上面的例子,我们可以实现一个缓存系统.
 
Map<String, Object> cache = new HashMap<String, Object>();
ReadWriteLock rrwl = new ReentrantReadWriteLock();
    public Object getData(String key) {
        rrwl.readLock().lock();
        Object value = null;
        try {
            value = cache.get(key);
            if (value == null) {
                rrwl.readLock().unlock();
                rrwl.writeLock().lock();
                try {
                    //假设三个线程同时去获取写锁,我们知道只有第一个线程能够获取
                    //那么其他两个线程只有等了,如果第一个线程按流程执行完后,刚才的两个线程可以得到写锁了,
                    //然后接着就可以修改数据了(赋值).所以加上判断!
                    if (value == null) {//为什么还要在这里判断一次.?
                        value = "hello world";
                    }
                    // 降级,通过释放写锁之前获取读锁
                    rrwl.readLock().lock();
                } finally {
                    rrwl.writeLock().unlock();
                }
            }
        } finally {
            rrwl.readLock().unlock();
        }
        return value;
    }
2>Condition:
        Condition的功能类似于在传统的线程技术中的,Object.wait()和Object.notify()的功能,在等待Condition时,允许发生"虚假唤醒",这通常作为对基础平台语义的让步,对于大多数应用程序,这带来的实际影响很小,因为Condition应该总是在一个循环中被等待,并测试正被等待的状态声明.某个实现可以随意移除可能的虚假唤醒,但是建议程序员总是假定这些虚假唤醒可能发生,因此总是在一个循环中等待.
        一个锁内部可以有多个Condition,即有多路等待和通知,可以参看jdk1.5提供的Lock与Condition实现的可阻塞队列的应用案例,从中要体味算法,还要体味面向对象的封装.在传统的线程机制中,一个监视器对象上只能有一路等待和通知,要想实现多路等待和通知,必须嵌套使用多个同步监视器对象.(如果只用一个Condition,两个放的都在等,一旦一个放进去了,那么它会通知可能会导致另一个放的接着往下走).
        我们也可以通过Lock和Condition来实现上面我们讲的例子:
子线程循环10次,接着主线程循环100,接着又回到子线程循环10次,接着再回到主线程又循环100,如此循环50次,请写出程序。
这里只实现其中的一个方法,因为其他的是一样的.
Lock lock = new ReentrantLock();
    Condition condition = lock.newCondition();
    // 默认子线程先执行
    boolean isShouldSub = true;
    public void sub(int k) {
        lock.lock();//相当于synchronied
        try {
            while(!isShouldSub) {
                try {
                    condition.await();//然后实现通信
                } 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;
            // 并且唤醒主线程
            condition.signal();
        } finally {
            lock.unlock();
        }
    }
 
可以使用Lock和Condition来实现一个缓冲队列(要区别缓冲和缓存的区别),其实jdk api有这样的例子,如下:
class BoundedBuffer {
   final Lock lock = new ReentrantLock();
   final Condition notFull  = lock.newCondition();
   final Condition notEmpty = lock.newCondition();

   final Object[] items = new Object[100];
   int putptr, takeptr, count;

   public void put(Object x) throws InterruptedException {
     lock.lock();//第一步实现互斥
     try {
       while (count == items.length)//如果没有往数组放,线程阻塞
         notFull.await();
       items[putptr] = x;
       if (++putptr == items.length) putptr = 0;//如果putptr已经是数组的最后一个,那么putptr置为0,从第一个开始放
       ++count;//放完后,把总数加一
       notEmpty.signal();//通知其他线程可以取了
     } finally {
       lock.unlock();
     }

   }

   public Object take() throws InterruptedException {
     lock.lock();
     try {

       while (count == 0)
         notEmpty.await();
       Object x = items[takeptr];
       if (++takeptr == items.length) takeptr = 0;
       --count;
       notFull.signal();
       return x;
     } finally {
       lock.unlock();
     }

   }
}
这个逻辑比较好理解,但是我们可以看到上面的程序用了两个Condition呢?用一个Condition似乎也能实现.
   public void put(Object x) throws InterruptedException {
    //如果有5个线程执行到此方法里面,那么只有一个线程获取到锁
     lock.lock(); // 锁住了别的线程就不能进来了,包括下面的take()因为他们用的是同一把锁
     try {
        //如果已经放满
       while (count == items.length)
         notFull.await();//执行到此,锁就释放了,可能这里就有5个线程在此等,其他线程就可以调用take()方法去取了然后调用signal()然而5个线程中,只有一个线程能被唤醒.该被唤醒的线程执行到signal时候,唤醒其他线程.如果用一个Condition,唤醒的可能就是上面的4个线程,而这个4个线程是往里面(put),而应该唤醒的是去取(take())线程.因为已经放满了如果再通知线程去放,那么就出现逻辑错误了.所以这里用到两个Condition的妙处!
       items[putptr] = x;
       if (++putptr == items.length) putptr = 0;
       ++count;
       notEmpty.signal();
     } finally {
       lock.unlock();
     }

   }
        据此,我们可以改变
子线程循环10次,接着主线程循环100,接着又回到子线程循环10次,接着再回到主线程又循环100,如此循环50次,请写出程序。的例子,这个例子是两个线程之间的跳转,那么如果实现三个线程之间的轮循,比如:线程1循环10,线程2循环100,线程3循环20次,然后又是线程1,接着线程2...一直轮循50次.
class Business3 {
    Lock lock = new ReentrantLock();
    Condition sub1 = lock.newCondition();
    Condition sub2 = lock.newCondition();
    Condition sub3 = lock.newCondition();
    //默认线程1执行
    int shouldSub = 1;
    public void sub1(int k) {
        lock.lock();//相当于synchronied
        try {
            while (shouldSub!=1) {
                try {
                    sub1.await();//然后实现通信
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            for (int i = 1; i <= 10; i++) {
                System.out
                        .println("sub1 thread sequence " + i + " loop of " + k);
            }
            // 把值置为2,然线程2可执行
            shouldSub = 2;
            // 线程1做完后,只唤醒线程2
            sub2.signal();
        } finally {
            lock.unlock();
        }
    }
 
    public void sub2(int k) {
        lock.lock();
        try {
            while (shouldSub!=2) {
                try {
                    sub2.await();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            for (int i = 1; i <= 100; i++) {
                System.out.println("sub2 thread sequence " + i + " loop of "
                        + k);
            }
            // 把值置为3,然线程3可执行
            shouldSub = 3;
            // 线程2做完后,只唤醒线程3
            sub3.signal();
        } finally {
            lock.unlock();
        }
    }
    public void sub3(int k) {
        lock.lock();
        try {
            while (shouldSub!=3) {
                try {
                    sub3.await();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            for (int i = 1; i <= 20; i++) {
                System.out.println("sub3 thread sequence " + i + " loop of  "
                        + k);
            }
            // 把值置为1,然线程1可执行
            shouldSub = 1;
            // 线程3做完后,只唤醒线程1
            sub1.signal();
        } finally {
            lock.unlock();
        }
    }

9,Semaphore实现信号灯
        Semaphore可以维护当前访问自身的线程个数,并提供了同步机制,使用Semaphore可以控制同时访问资源的线程数,例如,实现一个文件允许的并发访问数.
        Semaphore实现的功能就类似厕所一共有5个茅坑(new Semaphore(5)),加入有10个人(10个线程)要上厕所,那么同时只能有5个人能够占用,当占用的5个人任何一个让开后(release()方法),其中等待的另外5个人中又有一个可以占用了(acquire()方法).
        另外等待的5个人可以是随机获取优先机会,也可以是按照先来后到的顺序获取机会,这取决于构造方法传入的参数选项.public Semaphore(int permits, boolean fair)
        单个信号量的Semaphore对象可以实现互斥锁的功能,并且可以是由一个线程获得锁,再由另外一个线程释放锁,也就是一个线程可以去释放锁,尽管他没有得到permit,这可以应用死锁恢复的一些场合.
public class SemaphoreTest {
    public static void main(String[] args) {
        ExecutorService service = Executors.newCachedThreadPool();
        final Semaphore sp = new Semaphore(3,true);
        for (int i = 0; i < 10; i++) {
            Runnable runnable = new Runnable() {
                public void run() {
                    try {
                        sp.acquire();
                    } catch (InterruptedException e1) {
                        e1.printStackTrace();
                    }
                    System.out.println("线程" + Thread.currentThread().getName()
                            + "进入,当前已有" + (3 - sp.availablePermits()) + "个并发");
                    try {
                        Thread.sleep((long) (Math.random() * 10000));
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println("线程" + Thread.currentThread().getName()
                            + "即将离开");
                    sp.release();
                    // 下面代码有时候执行不准确,因为其没有和上面的代码合成原子单元
                    System.out.println("线程" + Thread.currentThread().getName()
                            + "已离开,当前已有" + (3 - sp.availablePermits()) + "个并发");
                }
            };
            service.execute(runnable);
        }
    }
 
 
10,JDK1.5 CyclicBarrier同步的工具类,
允许一系列的集合等待彼此,到达一个共同的障碍物点.
表示大家彼此等待,大家集合好后才开始出发,分散活动后又在指定的地点集合碰面,这就好比整个公司的人员利用周末时间集体郊游一样,先各自从家里出发到公司集合后,在同时出发到公园游玩,在指定的地点后再同时开始就餐,...
public static void main(String[] args) {
        ExecutorService service = Executors.newCachedThreadPool();
        final CyclicBarrier cb = new CyclicBarrier(3);
        for (int i = 0; i < 3; i++) {
            Runnable runnable = new Runnable() {
                public void run() {
                    try {
                        Thread.sleep((long) (Math.random() * 10000));
                        System.out.println("线程"
                                + Thread.currentThread().getName()
                                + "即将到达集合地点1,当前已有"
                                + (cb.getNumberWaiting() + 1)
                                + "个已经到达,"
                                + (cb.getNumberWaiting() == 2 ? "都到齐了,继续走啊"
                                        : "正在等候"));
                        cb.await();//只有三个线程都到齐了才往下走
 
                        Thread.sleep((long) (Math.random() * 10000));
                        System.out.println("线程"
                                + Thread.currentThread().getName()
                                + "即将到达集合地点2,当前已有"
                                + (cb.getNumberWaiting() + 1)
                                + "个已经到达,"
                                + (cb.getNumberWaiting() == 2 ? "都到齐了,继续走啊"
                                        : "正在等候"));
 
                        cb.await();
                        Thread.sleep((long) (Math.random() * 10000));
                        System.out.println("线程"
                                + Thread.currentThread().getName()
                                + "即将到达集合地点3,当前已有"
                                + (cb.getNumberWaiting() + 1)
                                + "个已经到达,"
                                + (cb.getNumberWaiting() == 2 ? "都到齐了,继续走啊"
                                        : "正在等候"));
 
                        cb.await();
                    } catch (Exception e) {
                        e.printStackTrace();
                    }
                }
            };
            service.execute(runnable);
        }
        service.shutdown();
    }


在Api有一个使用CyclicBarrier的例子:
class Solver {
   final int N;
   final float[][] data;
   final CyclicBarrier barrier;

   class Worker implements Runnable {
     int myRow;
     Worker(int row) { myRow = row; }
     public void run() {
       while (!done()) {
         processRow(myRow);

         try {
           barrier.await();//处理完一行在此等待,如果N个线程都完成了各自的行,都在此等待,然后
将会执行new CyclicBarrier(N,Runnbale)里面Runnable的代码,也就是合并行mergeRows();
         } catch (InterruptedException ex) {
           return;
         } catch (BrokenBarrierException ex) {
           return;
         }
       }
     }
   }

   public Solver(float[][] matrix) {
     data = matrix;
     N = matrix.length;
     barrier = new CyclicBarrier(N,
                                 new Runnable() {
                                   public void run() {
                                     mergeRows(...);
                                   }
                                 });
     for (int i = 0; i < N; ++i)
       new Thread(new Worker(i)).start();

     waitUntilDone();
   }
}
在这里每个工作者(Worker)线程处理这个矩阵一行,然后在这个障碍物处等待,知道矩阵的所有行都被处理了,当所有的行都被处理了提供的Runnable障碍物被执行并且合并这些行,如果合并者确定了一个合并方案,然后将会返回(return),并且每个worker将终止.

11,JDK1.5  CountDownLatch 同步工具
 
public class CountdownLatchTest {
    public static void main(String[] args) {
        ExecutorService service = Executors.newCachedThreadPool();
        final CountDownLatch cdOrder = new CountDownLatch(1);
        final CountDownLatch cdAnswer = new CountDownLatch(3);
        for (int i = 0; i < 3; i++) {
            Runnable runnable = new Runnable() {
                public void run() {
                    try {
                        System.out.println("线程"
                                + Thread.currentThread().getName() + "正准备接受命令");
 
                        cdOrder.await();//直到cdOrder的计数器归0,才能往下执行
                        //如果把这个程序比喻成一个3个运动员在比赛,还有一个裁判
                        //cdOrder的计数器归0,表明裁判名枪了,运动员可以跑了.
                        System.out.println("线程"
                                + Thread.currentThread().getName() + "已接受命令");
                        Thread.sleep((long) (Math.random() * 10000));
                        System.out.println("线程"
                                        + Thread.currentThread().getName()
                                        + "回应命令处理结果");
                        cdAnswer.countDown();//每一个运动员到此计数器就减一,
                    } catch (Exception e) {
                        e.printStackTrace();
                    }
                }
            };
            service.execute(runnable);
        }
        try {
            Thread.sleep((long) (Math.random() * 10000));
            System.out.println("线程" + Thread.currentThread().getName()
                    + "即将发布命令");
            cdOrder.countDown();//cdOrder的计数器减1,
            System.out.println("线程" + Thread.currentThread().getName()
                    + "已发送命令,正在等待结果");
            cdAnswer.await();//cdAnswer直到计数器为0,也就是上面三个线程执行完毕,
            //裁判就可以公布成绩了
            System.out.println("线程" + Thread.currentThread().getName()
                    + "已收到所有响应结果");
        } catch (Exception e) {
            e.printStackTrace();
        }
        service.shutdown();
    }
}
执行结果如下:


12,JDK1.5 Exchanger同步工具
用于实现两个人之间的数据交换,每一个人在完成一定的事务后想与对方交换交换数据,第一个先拿出数据的人将一直等待第二个人拿着数据到来时,才能彼此交换数据.也就是当两个线程都到达了交换点.才能交换.
public static void main(String[] args) {
        ExecutorService service = Executors.newCachedThreadPool();
        final Exchanger exchanger = new Exchanger();
 
        service.execute(new Runnable() {
            public void run() {
                try {
                    String data1 = "money";
                    System.out.println("线程" + Thread.currentThread().getName()
                            + "正在把数据" + data1 + "换出去");
                    Thread.sleep((long) (Math.random() * 10000));
                    String data2 = (String) exchanger.exchange(data1);
                    System.out.println("线程" + Thread.currentThread().getName()
                            + "换回的数据为" + data2);
                } catch (Exception e) {
 
                }
            }
        });
        service.execute(new Runnable() {
            public void run() {
                try {
                    String data1 = "drugs";
                    System.out.println("线程" + Thread.currentThread().getName()
                            + "正在把数据" + data1 + "换出去");
                    Thread.sleep((long) (Math.random() * 10000));
                    String data2 = (String) exchanger.exchange(data1);
                    System.out.println("线程" + Thread.currentThread().getName()
                            + "换回的数据为" + data2);
                } catch (Exception e) {
                }
            }
        });
    }
运行的结果如下所示:

13,JDK1.5 ArrayBlockingQueue阻塞队列
阻塞队列和非阻塞的区别:如果队列里面已经放满了,如果是阻塞队列那么线程会一直阻塞,而非阻塞对垒则会抛出异常.
队列还包括固定长度的队列和不固定长度的队列.
这个类实现了BlockingQueue
这个接口有如下方法:

拿Insert情况来说,如果队列里面已经满了,使用add方法往里放就会抛出异常,用offer放回false,用put()方法将会阻塞在那里,知道有空间可以放.
下面来看一个例子:
public class BlockingQueueTest {
    public static void main(String[] args) {
        final BlockingQueue<Integer> queue = new ArrayBlockingQueue<Integer>(3);//该队列里面只能放3个Integer
        for(int i=0;i<2;i++){
            new Thread(){
                public void run(){
                    while(true){
                        try {
                            Thread.sleep((long)(Math.random()*1000));
                            System.out.println(Thread.currentThread().getName() + "准备放数据!");                            
                            queue.put(1);//如果放满就会阻塞
                            System.out.println(Thread.currentThread().getName() + "已经放了数据," +                             
                                        "队列目前有" + queue.size() + "个数据");
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    }
                }
            }.start();
        }
        new Thread(){
            public void run(){
                while(true){
                    try {
                        //将此处的睡眠时间分别改为100和1000,观察运行结果
                        Thread.sleep(1000);
                        System.out.println(Thread.currentThread().getName() + "准备取数据!");
                        queue.take();//如果没有了数据,就会阻塞
                        System.out.println(Thread.currentThread().getName() + "已经取走数据," +                             
                                "队列目前有" + queue.size() + "个数据");                    
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
 
        }.start();            
    }
}
运行结果如图:

我们可以通过使用两个都是一个控件的缓冲队列来实现同步通知的功能.
 
static class Business {
        BlockingQueue<Integer> queue1 = new ArrayBlockingQueue<Integer>(1);
        BlockingQueue<Integer> queue2 = new ArrayBlockingQueue<Integer>(1);
 
        {//设置匿名构造方法,他会在实例化对象前执行.
            try {
                //因为要让主线程先执行.
                queue2.put(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        public void sub(int k){//不能加上synchronized,可能会导致死锁,put方法会阻塞但是不会释放锁
            try {
                queue2.put(1);//第一次执行的时候因为已经满了,就阻塞
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            for (int i = 1; i <= 10; i++) {
                System.out.println("sub thread sequence " + i + " loop of " + k);
            }
            try {
                queue1.take();//这样queue1就可以put了
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        public void main(int k) {//不能加上synchronized,可能会导致死锁,put方法会阻塞但是不会释放锁
            try {
                queue1.put(1);//可以放
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            for (int i = 1; i <= 100; i++) {
                System.out.println("main thread sequence " + i + " loop of " + k);
            }
            try {
                queue2.take();//这样queue2就可以put了
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
 
        }
    }
这样也能实现主线程和子线程之间的打印切换.
 
14,JDK1.5 同步集合
在JDK1.5之前没有推出同步集合的时候,可以通过Conllections集合工具类的synchronized+集合名称如:synchronizedSet(Set),现在不需要这种方式了使用ConcurrentHashMap, ConcurrentSkipListMap, ConcurrentSkipListSet等这些集合即可.
        1>线程不安全的集合出现的问题,死循环.
Race Condition(也叫做资源竞争),是多线程编程中比较头疼的问题特别是Java多线程当中,
经常会因为多线程同时访问相同的共享数据,而造成数据的不一致性,为了解决这个问题
通常来说需要加上同步标识synchronized,来保证数据串行的访问,但是synchronized是个性能杀手,
过多的使用会导致性能下降,特别是扩展性下降,使得系统不能使用多个CPU资源,这是性能测试当中
经常遇到的问题.
        然而有一个公司的ERP系统出现了问题,然后就叫Java的高级工程师来解决,该工程师通发现当500个并发用访问的时候,居然把所有的CPU都压得满满的,通过过DTrace for Java工具发现很多CPU都在做同一件事,
那就是不停的执行一条语句(HashMap.get()方法).这是为什么呢?
我们知道遍历Map集合的时候,是这样的情形(下面只是伪代码):
while(hasNext()){
    //每当循环一次cursor加1
    //假设该集合里面有4个元素(count=4),如果循环到最后一次了cursor=4,就在此时另一个线程
//跑来把集合里面的一个元素给删除了(remove()),这时候count=3了,这时候上一个线程接着执行
//它会去判断hasNext(),但是count!=cursor了,而本来是相等的.这样就是死循环了.
 
}
hasNext(){
    if(cursor==count){
        return false;//不需要再循环了
    }
    return true;
}
 
        2>HashSet和HashMap关系
通过查看HashSet源码:
public HashSet() {
map =new HashMap<E,Object>();
} 发现HashSet实际上就是通过HashMap来实现的,不过它只用到了HashMap的key
 
        3>线程不安全集合还有另一个隐患
public static void main(String[] args) {
        //Collection users = new CopyOnWriteArrayList();
        Collection users = new ArrayList();
 
        users.add(new User("张三",28));    
        users.add(new User("李四",25));            
        users.add(new User("王五",31));    
        Iterator itrUsers = users.iterator();
        while(itrUsers.hasNext()){
            System.out.println("aaaa");
            User user = (User)itrUsers.next();
            if("张三".equals(user.getName())){
                users.remove(user);
            } else {
                System.out.println(user);                
            }
        }
    }
运行程序发现出现异常:


这是为什么?
跟进AbstractList 343行:



分析如下:
我们来看一下这个
while(itrUsers.hasNext()){
    User user = (User)itrUsers.next();
    if("张三".equals(user.getName())){
        users.remove(user);
    } else {
        System.out.println(user);
    }
}
查看hasNext源码:

 
如果把 if("张三".equals(user.getName()))改成 if("王五".equals(user.getName()))
输出结果如下:

出现了第一次出现的异常,分析如下:
当第一循环执行到next()时候expectedModCount=3 modCount=3 cursor=1,size()=3
当第二循环执行到next()时候expectedModCount=3 modCount=3 cursor=2,size()=3
当第三循环执行到next()时候expectedModCount=3 modCount=4 cursor=3,size()=2
发现cursor任然不等于size(),循环人仍在继续,最后发现expectedModCount!=modCount抛出异常.所以控制太输出这样的结果.
 
要想迭代的时候同时操作集合可以使用JDK1.5提供的线程安全的集合,
只需要把集合改成如下即可:Collection users = new CopyOnWriteArrayList();


备注 :仅此文章怀念已经逝去的张孝祥老师,虽然没有教我一节课,但是是张老师的Java基础视频把我领进Java的大门.

本文如果有什么讲的不详细的地方或者疏忽之处,尽情大家批评指正,谢谢.

转载请注明出处: http://blog.csdn.net/johnny901114/article/details/7536951

谢谢!