JavaSE学习笔记(三十二)—— 多线程(下)

一、电影院卖票问题

1.1 最初版 

  某电影院目前正在上映贺岁大片(红高粱,少林寺传奇藏经阁),共有100张票,而它有3个售票窗口售票,请设计一个程序模拟该电影院售票。

【继承Thread类来实现】

public class SellTicket extends Thread {
    // 定义100张票
    // private int tickets = 100;
    // 为了让多个线程对象共享这100张票,我们其实应该用静态修饰
    private static int tickets = 100;

    @Override
    public void run() {
        // 定义100张票
        // 每个线程进来都会走这里,这样的话,每个线程对象相当于买的是自己的那100张票,这不合理,所以应该定义到外面
        // int tickets = 100;

        // 为了模拟一直有票
        while (true) {
            if (tickets > 0) {
                System.out.println(getName() + "正在出售第" + (tickets--) + "张票");
            }
        }
    }
}
public class SellTicketDemo {
    public static void main(String[] args) {
        // 创建三个线程对象
        SellTicket st1 = new SellTicket();
        SellTicket st2 = new SellTicket();
        SellTicket st3 = new SellTicket();

        // 给线程对象起名字
        st1.setName("窗口1");
        st2.setName("窗口2");
        st3.setName("窗口3");

        // 启动线程
        st1.start();
        st2.start();
        st3.start();
    }
}

【实现Runnable接口来实现】 

public class SellTicket implements Runnable {
    // 定义100张票
    private int tickets = 100;

    @Override
    public void run() {
        while (true) {
            if (tickets > 0) {
                System.out.println(Thread.currentThread().getName() + "正在出售第"
                        + (tickets--) + "张票");
            }
        }
    }
}
public class SellTicketDemo {
    public static void main(String[] args) {
        // 创建资源对象
        SellTicket st = new SellTicket();

        // 创建三个线程对象
        Thread t1 = new Thread(st, "窗口1");
        Thread t2 = new Thread(st, "窗口2");
        Thread t3 = new Thread(st, "窗口3");

        // 启动线程
        t1.start();
        t2.start();
        t3.start();
    }
}

1.2 改进版及问题分析

  上面的电影院售票程序,从表面上看不出什么问题,但是在真实生活中,售票时网络是不能实时传输的,总是存在延迟的情况,所以,在出售一张票以后,需要一点时间的延迟。

  接下来改实现接口方式的卖票程序,每次卖票延迟100毫秒。

 1 public class SellTicket implements Runnable {
 2     // 定义100张票
 3     private int tickets = 100;
 4 
 5     @Override
 6     public void run() {
 7         while (true) {
 8             if (tickets > 0) {
 9                 // 为了模拟更真实的场景,我们稍作休息
10                 try {
11                     Thread.sleep(100);
12                 } catch (InterruptedException e) {
13                     e.printStackTrace();
14                 }
15                 System.out.println(Thread.currentThread().getName() + "正在出售第"
16                         + (tickets--) + "张票");
17             }
18         }
19     }
20

  加入延迟后,再次运行SellTicketDemo,出现了以下问题:

  1. 相同的票卖了多次
  2. 出现了负数票

  下面分析为什么会出现”相同的票卖了多次“

  t1、t2、t3三个线程,假设t1和t2都进到11行稍作休息了,理想状态是:窗口1正在出售第100张票;窗口2正在出售第99张票。

  但是有一种情况是,t1来到16行的时,tickets--,此时是先记录以前的值100,即窗口1正在出售第100张票。但是在t1输出以前的值(100)时,t2刚好来了,这时tickets还没有完tickets--操作,即t2读取到的数据和t1读取到的数据是一样的,都是100,所以就会出现”相同的票卖了多次“的现象。归根结底,原因就是:CPU的每一次执行必须是一个原子性(最简单基本的)的操作

  那”出现负数票“又是什么原因呢?

  t1、t2、t3三个线程,假设tickets=1,t1、t2、t3都进到11行并休息了。这时就会有:窗口1正在出售第1张票,tickets=0;窗口2正在出售第0张票,tickets=-1;窗口3正在出售第-1张票,tickets=-2。三个线程最坏情况下能卖到-1张票,4个窗口(四个线程)最坏能卖到-2张票,并且tickets=-3。原因就是:随机性和延迟导致的。

二、线程安全问题

  线程安全问题在理想状态下,不容易出现,但一旦出现对软件的影响是非常大的。那么如何解决线程安全问题呢?

  要想解决问题,就要知道哪些原因会导致出问题:(而且这些原因也是以后我们判断一个程序是否会有线程安全问题的标准):

  • A:是否是多线程环境
  • B:是否有共享数据
  • C:是否有多条语句操作共享数据

  我们来回想一下我们的卖票程序有没有上面的问题呢?都有!卖票程序种出现了t1、t2、t3三个线程,属于多线程环境;定义的100张票(tickets)属于共享数据;而第8行对tickets的判断操作,第15、16行对tickets的自减操作以及对tickets的输出操作,说明了有多条语句操作了共享数据。

  由此可见我们的程序出现问题是正常的,因为它满足出问题的条件。接下来我们要想想如何解决问题呢?A和B的问题我们改变不了,我们只能想办法去把C改变一下。

  解决这个问题的思想是:把多条语句操作共享数据的代码给包成一个整体,让某个线程在执行的时候,别人不能来执行

  问题是怎么包呢?Java给我们提供了:同步机制。

2.1 同步代码块解决线程安全问题

【同步代码块】

synchronized(对象){
    需要同步的代码;
}

   对象是什么呢?可以是任意对象,该对象如同锁的功能,多个线程必须是同一把锁

   需要同步的代码是哪些呢?把多条语句操作共享数据的代码的部分给包起来

public class SellTicket implements Runnable {
    // 定义100张票
    private int tickets = 100;
    // 创建锁对象
    private Object obj = new Object();

    @Override
    public void run() {
        while (true) {
            synchronized (obj) {
                if (tickets > 0) {
                    try {
                        Thread.sleep(100);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println(Thread.currentThread().getName() + "正在出售第"
                            + (tickets--) + "张票");
                }
            }
        }
    }
}
public class SellTicketDemo {
    public static void main(String[] args) {
        // 创建资源对象
        SellTicket st = new SellTicket();

        // 创建三个线程对象
        Thread t1 = new Thread(st, "窗口1");
        Thread t2 = new Thread(st, "窗口2");
        Thread t3 = new Thread(st, "窗口3");

        // 启动线程
        t1.start();
        t2.start();
        t3.start();
    }
}

  注意:同步可以解决安全问题的根本原因就在那个对象上。该对象如同锁的功能。

  举例:火车上厕所

【同步的前提】

  • 多个线程
  • 多个线程使用的是同一个锁对象

【同步的好处】

  同步的出现解决了多线程的安全问题。

【同步的弊端】

  当线程相当多时,因为每个线程都会去判断同步上的锁,这是很耗费资源的,无形中会降低程序的运行效率。

2.2 同步方法解决线程安全问题

  同步方法:就是把同步关键字(synchronized)加到方法上。

  同步方法的锁对象是谁呢?this

public class SellTicket implements Runnable {
    // 定义100张票
    private int tickets = 100;
    // 创建锁对象
    private Object obj = new Object();
    private int x = 0;

    @Override
    public void run() {
        while (true) {
            if (x % 2 == 0) {
                synchronized (this) {
                    if (tickets > 0) {
                        try {
                            Thread.sleep(100);
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                        System.out.println(Thread.currentThread().getName()
                                + "正在出售第" + (tickets--) + "张票 ");
                    }
                }
            } else {
                sellTicket();
            }
            x++;
        }
    }

    private synchronized void sellTicket() {
        if (tickets > 0) {
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName() + "正在出售第"
                    + (tickets--) + "张票");
        }
    }
}

  如果是静态方法,同步方法的锁对象又是什么呢?类的字节码文件对象。

public class SellTicket implements Runnable {
    // 定义100张票
    private static int tickets = 100;
    // 创建锁对象
    private Object obj = new Object();
    private int x = 0;

    @Override
    public void run() {
        while (true) {
            if (x % 2 == 0) {
                synchronized (SellTicket.class) {
                    if (tickets > 0) {
                        try {
                            Thread.sleep(100);
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                        System.out.println(Thread.currentThread().getName()
                                + "正在出售第" + (tickets--) + "张票 ");
                    }
                }
            } else {
                sellTicket();
            }
            x++;
        }
    }

    private static synchronized void sellTicket() {
        if (tickets > 0) {
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName() + "正在出售第"
                    + (tickets--) + "张票");
        }
    }
}

  那么,我们到底使用谁?
  如果锁对象是this,就可以考虑使用同步方法。
  否则能使用同步代码块的尽量使用同步代码块。

2.3 回顾已学的线程安全的类

  StringBuffer:

  

  Vector:

  

  Hashtable:

  

  Vector是线程安全的时候才去考虑使用的,但是我还说过即使要安全,我也不用你。那么到底用谁呢?

  public static <T> List<T> synchronizedList(List<T> list)

List<String> list1 = new ArrayList<String>();// 线程不安全
List<String> list2 = Collections.synchronizedList(new ArrayList<String>()); // 线程安全

三、Lock锁的使用

  虽然我们可以理解同步代码块和同步方法的锁对象问题,但是我们并没有直接看到在哪里加上了锁,在哪里释放了锁,为了更清晰的表达如何加锁和释放锁,JDK5以后提供了一个新的锁对象Lock。

  Lock是一个接口对象,其主要方法有:

void lock(): 获取锁。
void unlock():释放锁。

  ReentrantLock是Lock的实现类。

  下面使用Lock锁来实现电影票的卖票功能:

public class SellTicket implements Runnable {

    // 定义票
    private int tickets = 100;

    // 定义锁对象
    private Lock lock = new ReentrantLock();

    @Override
    public void run() {
        while (true) {
            try {
                // 加锁
                lock.lock();
                if (tickets > 0) {
                    try {
                        Thread.sleep(100);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println(Thread.currentThread().getName()
                            + "正在出售第" + (tickets--) + "张票");
                }
            } finally {
                // 释放锁
                lock.unlock();
            }
        }
    }
}
public class SellTicketDemo {
    public static void main(String[] args) {
        // 创建资源对象
        SellTicket st = new SellTicket();

        // 创建三个窗口
        Thread t1 = new Thread(st, "窗口1");
        Thread t2 = new Thread(st, "窗口2");
        Thread t3 = new Thread(st, "窗口3");

        // 启动线程
        t1.start();
        t2.start();
        t3.start();
    }
}

四、死锁问题

【同步的弊端】

  1. 效率低
  2. 容易产生死锁

【死锁】

  两个或两个以上的线程在争夺资源的过程中,发生的一种相互等待的现象。

【举例】

  中国人,美国人吃饭案例。
  正常情况:
    中国人:筷子两支
    美国人:刀和叉
  现在:
    中国人:筷子1支,刀一把
    美国人:筷子1支,叉一把

【实现死锁的程序】

  先创建两把锁对象

public class MyLock {
    // 创建两把锁对象
    public static final Object objA = new Object();
    public static final Object objB = new Object();
}

  实现多线程

 1 public class DieLock extends Thread {
 2 
 3     private boolean flag;
 4 
 5     public DieLock(boolean flag) {
 6         this.flag = flag;
 7     }
 8 
 9     @Override
10     public void run() {
11         if (flag) {
12             synchronized (MyLock.objA) {
13                 System.out.println("if objA");
14                 synchronized (MyLock.objB) {
15                     System.out.println("if objB");
16                 }
17             }
18         } else {
19             synchronized (MyLock.objB) {
20                 System.out.println("else objB");
21                 synchronized (MyLock.objA) {
22                     System.out.println("else objA");
23                 }
24             }
25         }
26     }
27 }

  测试类

public class DieLockDemo {
    public static void main(String[] args) {
        DieLock dl1 = new DieLock(true);
        DieLock dl2 = new DieLock(false);

        dl1.start();
        dl2.start();
    }
}

  理想状态下, A线程依次走完12-15行代码时,B线程抢到了CPU的执行权,依次执行19-22行的代码,这时的输出结果就是:

  

  非理想状态时,A线程刚走到第13行时,B线程抢到了执行权,然后B线程走到了第20行,B再往下走时,就要用到A的锁,而A锁在第12行还没放出来,所以用不了,B就在第21行等着,B就抢不到资源了,换作是A抢到了。然后A重新回到第13行继续往下走,到14行时要用到B的锁,而B的锁也没释放,所以A就在第14行等着。A和B都在等着对方释放自己的锁,谁也走不了下一步,所以就形成了死锁。这时的输出结果就是:

  

五、生产者消费者问题

5.1 线程通信

  回顾我们的电影院卖票程序:3个窗口一起卖100张票

  

  其实在真实生活中,电影票不只是售出,还能进来的,比如一部电影售完票了,还会有一部新的电影进来,新电影接着就可以卖票了。而之前我们的程序都只有卖票的线程,真实生活中应该还有进票的线程。所以这个程序并不是特别符合真实情况。

  再举个例子:

  一个卖包子的店铺,顾客消费包子,商家生产包子。这个就是生产者/消费者模式。

  

  这个例子中,既有生产包子的线程,又有消费包子的线程,这就产生了线程间通信问题:不同种类的线程针对同一个资源的操作。

5.2 案例演示

【初始版】

  资源类:Student。按照标准的做法,应该在属性前面加上private修饰符,并提供对应的get/set方法,但这里为了方便后面的操作,先这么写:

public class Student {
    String name;
    int age;
}

  设置学生数据:SetThread(生产者)

public class SetThread implements Runnable{
    @Override
    public void run() {
        Student s = new Student();
        s.name = "张三";
        s.age = 27;
    }
}

  获取学生数据:GetThread(消费者)

public class GetThread implements Runnable {
    @Override
    public void run() {
        Student s = new Student();
        System.out.println(s.name + "---" + s.age);
    }
}

   测试类:StudentDemo

public class StudentDemo {
    public static void main(String[] args) {
        SetThread st = new SetThread();
        GetThread gt = new GetThread();

        Thread t1 = new Thread(st);
        Thread t2 = new Thread(gt);

        t1.start();
        t2.start();
    }
}

  运行测试类时,控制台的打印结果是

  问题就来了,按照思路写代码,结果却发现数据每次都是:null---0。为什么会这样呢?

  原因就是,我们在每个线程中都创建了新的资源,而我们要求的时候设置和获取线程的资源应该是同一个。

  如何实现呢?在外界把这个数据创建出来,通过构造方法传递给其他的类。

  修改StudentDemo、SetThread、GetThread

public class StudentDemo {
    public static void main(String[] args) {
        // 创建资源
        Student s = new Student();

        // 设置和获取的类
        SetThread st = new SetThread(s);
        GetThread gt = new GetThread(s);

        // 线程类
        Thread t1 = new Thread(st);
        Thread t2 = new Thread(gt);

        // 启动线程
        t1.start();
        t2.start();
    }
}
public class SetThread implements Runnable {
    private Student s;

    public SetThread(Student s) {
        this.s = s;
    }

    @Override
    public void run() {
        // Student s = new Student();
        s.name = "张三";
        s.age = 27;
    }
}
public class GetThread implements Runnable {
    private Student s;

    public GetThread(Student s) {
        this.s = s;
    }

    @Override
    public void run() {
        // Student s = new Student();
        System.out.println(s.name + "---" + s.age);
    }
}

  但这里存在一个问题:这里的2个线程中,只有t1先抢到资源,设置值以后,t2抢到资源时才能获取到值。否则,如果t2比t1先抢到了资源,那么输出的结果将会是null。

  所以上面的代码还需要进行改进。

【改进版】

  为了数据的效果好一些,我加入了循环和判断,给出不同的值

 1 public class SetThread implements Runnable {
 2     private Student s;
 3     private int x = 0;
 4 
 5     public SetThread(Student s) {
 6         this.s = s;
 7     }
 8 
 9     @Override
10     public void run() {
11         while (true) {
12             if (x % 2 == 0) {
13                 s.name = "张三";
14                 s.age = 27;
15             } else {
16                 s.name = "李四";
17                 s.age = 30;
18             }
19             x++;
20         }
21     }
22 }
public class GetThread implements Runnable {
    private Student s;
    private int x = 0;

    public GetThread(Student s) {
        this.s = s;
    }

    @Override
    public void run() {
        while (true) {
            System.out.println(s.name + "---" + s.age);
        }
    }
}

  这时候运行StudentDemo测试类时,却发现了这样的问题:同一个数据出现多次,姓名和年龄不匹配。

  

  “同一个数据出现多次”的原因是:CPU的一点点时间片的执行权,就足够你执行很多次。

  “姓名和年龄不匹配”的原因是:线程运行的随机性。分析如下:

  一开始x=0,程序进入if的逻辑,此时name=张三,age=27,x++;x=1,这时来到了15行的else的逻辑,走到16行时,name的值变成了“李四”。可是刚走到第16行,赋值完毕这一刻,执行权就被别人抢到了。这时内存中的name是“李四”,因为原本的age并没改,所以age还是27。这就产生了“姓名和年龄不匹配”的问题。同理,如果走到第13行时,执行权被别人抢走了,就会出现name=“张三”,age=30的问题。这是由于线程运行的随机性导致的。

  所以”姓名和年龄不匹配“其实就属于线程安全问题。 它符合产生线程安全问题的条件:多线程环境;有共享数据;并且有多条语句操作共享数据。

  怎么解决呢?加锁!

  注意:不同种类的线程都要加锁。而且,不同种类的线程加的锁必须是同一把。

public class SetThread implements Runnable {
    private Student s;
    private int x = 0;

    public SetThread(Student s) {
        this.s = s;
    }

    @Override
    public void run() {
        while (true) {
            synchronized (s) {
                if (x % 2 == 0) {
                    s.name = "张三";
                    s.age = 27;
                } else {
                    s.name = "李四";
                    s.age = 30;
                }
                x++;
            }
        }
    }
}
public class GetThread implements Runnable {
    private Student s;
    private int x = 0;

    public GetThread(Student s) {
        this.s = s;
    }

    @Override
    public void run() {
        while (true) {
            synchronized (s) {
                System.out.println(s.name + "---" + s.age);
            }
        }
    }
}

  这时候就不会出现”姓名和年龄不匹配“的问题了”

  

5.3 等待唤醒机制

   还是上面的案例:

  线程安全是解决了,但是打印的结果是一次一大片的,不好看,存在着如下的问题:

  • 如果消费者先抢到了CPU的执行权,就会去消费数据,但是现在的数据是默认值,没有意义。应该等着数据有意义后,再去消费。
  • 如果生产者先抢到CPU的执行权,就会去生产数据。但是,它生产完数据后,还继续拥有执行权,它又继续生产数据。这是有问题的,你应该等着消费者把数据消费掉,然后再生产。

  怎样才能依次的一次一个输出?正常的思路应该是:

  • 生产者:先看是否有数据,有就等待;没有就生产,生产完之后通知消费者来消费数据。
  • 消费者:先看是否有数据,有就消费;没有就等待,并且通知生产者生产数据。

  为了处理这样的问题,Java就提供了一种机制:等待唤醒机制。

  Object类中提供了三个方法:

wait():等待
notify():唤醒单个线程
notifyAll():唤醒所有线程

  为什么这些方法不定义在Thread类中呢?

    因为这些方法的调用是依赖于锁对象的,而同步代码块的锁对象是任意锁。所以,这些方法必须定义在Object类中。

【代码改进】

public class Student {
    String name;
    int age;
    boolean flag;//默认情况是没有数据,如果是true,说明有数据
}
 1 public class SetThread implements Runnable {
 2     private Student s;
 3     private int x = 0;
 4 
 5     public SetThread(Student s) {
 6         this.s = s;
 7     }
 8 
 9     @Override
10     public void run() {
11         while (true) {
12             synchronized (s) {
13                 if (s.flag) {
14                     // 如果有数据,就等待
15                     try {
16                         s.wait();
17                     } catch (InterruptedException e) {
18                         e.printStackTrace();
19                     }
20                 }
21                 if (x % 2 == 0) {
22                     s.name = "张三";
23                     s.age = 27;
24                 } else {
25                     s.name = "李四";
26                     s.age = 30;
27                 }
28                 x++;
29 
30                 // 修改标记
31                 s.flag = true;
32                 // 唤醒线程
33                 s.notify();
34             }
35         }
36     }
37 }
 1 public class GetThread implements Runnable {
 2     private Student s;
 3     private int x = 0;
 4 
 5     public GetThread(Student s) {
 6         this.s = s;
 7     }
 8 
 9     @Override
10     public void run() {
11         while (true) {
12             synchronized (s) {
13                 if (!s.flag) {
14                     // 如果没有数据,就等待
15                     try {
16                         s.wait();
17                     } catch (InterruptedException e) {
18                         e.printStackTrace();
19                     }
20                 }
21 
22                 // 如果有就消费
23                 System.out.println(s.name + "---" + s.age);
24 
25                 // 消费完后就没有了,修改标记
26                 s.flag = false;
27                 // 唤醒
28                 s.notify();
29             }
30         }
31     }
32 }

  这时候数据就是依次输出的了:

  

  下面来分析一下刚才的代码:

  假设t2先抢到了资源,t2是GetThread,程序来到GetThread类的第13行,flag的默认值是false,所以就进到if里面的代码,来到16行,t2就等待(因为此时没数据),然后立即释放锁(wait的特点),将来醒过来的时候是从16行醒过来的,“哪里跌倒就从哪里爬起来”。

  由于t2等待了,所以t1就抢到了执行权,程序来到了SetThread类的第13行,因为flag是false,所以进不了if语句,开始执行第21行以后的代码。初始时x=0,所以name=张三,age=27。然后x变为1,有数据后修改flag为true,然后唤醒t2,需要注意的是,虽然t2被唤醒了,却不表示你能立马执行,必须还得抢CPU的执行权。

  如果还是t1抢到了执行权,来到SetThread类的第13行,这时候flag已经是true了,所以进入if语句,t1等待,这时t2就抢到了执行权,t2从GetThread的第16行醒来,并继续往下执行。

  根据上面的执行流程,就能保证数据依次输出了。

【最终版代码】

  把Student的成员变量给私有的了。
  把设置和获取的操作给封装成了功能,并加了同步。
  设置或者获取的线程里面只需要调用方法即可。

public class Student {
    private String name;
    private int age;
    private boolean flag; // 默认情况是没有数据,如果是true,说明有数据

    public synchronized void set(String name, int age) {
        // 如果有数据,就等待
        if (this.flag) {
            try {
                this.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }

        // 设置数据
        this.name = name;
        this.age = age;

        // 修改标记
        this.flag = true;
        this.notify();
    }

    public synchronized void get() {
        // 如果没有数据,就等待
        if (!this.flag) {
            try {
                this.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }

        // 获取数据
        System.out.println(this.name + "---" + this.age);

        // 修改标记
        this.flag = false;
        this.notify();
    }
}
public class SetThread implements Runnable {

    private Student s;
    private int x = 0;

    public SetThread(Student s) {
        this.s = s;
    }

    @Override
    public void run() {
        while (true) {
            if (x % 2 == 0) {
                s.set("林青霞", 27);
            } else {
                s.set("刘意", 30);
            }
            x++;
        }
    }
}
public class GetThread implements Runnable {
    private Student s;

    public GetThread(Student s) {
        this.s = s;
    }

    @Override
    public void run() {
        while (true) {
            s.get();
        }
    }
}
public class StudentDemo {
    public static void main(String[] args) {
        //创建资源
        Student s = new Student();
        
        //设置和获取的类
        SetThread st = new SetThread(s);
        GetThread gt = new GetThread(s);

        //线程类
        Thread t1 = new Thread(st);
        Thread t2 = new Thread(gt);

        //启动线程
        t1.start();
        t2.start();
    }
}

六、线程的状态转换图

  常见的情况有:

  1. 新建——就绪——运行——死亡
  2. 新建——就绪——运行——就绪——运行——死亡
  3. 新建——就绪——运行——其他阻塞——就绪——运行——死亡
  4. 新建——就绪——运行——同步阻塞——就绪——运行——死亡
  5. 新建——就绪——运行——等待阻塞——同步阻塞——就绪——运行——死亡

七、线程组

  Java中使用ThreadGroup来表示线程组,它可以对一批线程进行分类管理,Java允许程序直接对线程组进行控制。

  默认情况下,所有的线程都属于主线程组。
  public final ThreadGroup getThreadGroup()

public class MyRunnable implements Runnable {
    @Override
    public void run() {
        for (int x = 0; x < 100; x++) {
            System.out.println(Thread.currentThread().getName() + ":" + x);
        }
    }
}
public class ThreadGroupDemo {
    public static void main(String[] args) {
        MyRunnable my = new MyRunnable();
        Thread t1 = new Thread(my, "张三");
        Thread t2 = new Thread(my, "李四");

        // 我不知道他们属于那个线程组,我想知道,怎么办
        // 线程类里面的方法:public final ThreadGroup getThreadGroup()
        ThreadGroup tg1 = t1.getThreadGroup();
        ThreadGroup tg2 = t2.getThreadGroup();

        // 线程组里面的方法:public final String getName()
        String name1 = tg1.getName();
        String name2 = tg2.getName();
        System.out.println(name1);// main
        System.out.println(name2);// main

        // 通过结果我们知道了:线程默认情况下属于main线程组
        // 通过下面的测试,你应该能够看到,默任情况下,所有的线程都属于同一个组
        System.out.println(Thread.currentThread().getThreadGroup().getName());// main
    }
}  

  我们如何修改线程所在的组呢?

  可以创建一个线程组,创建其他线程的时候,把其他线程的组指定为我们自己新建线程组。

public class ThreadGroupDemo {
    public static void main(String[] args) {
        ThreadGroup tg = new ThreadGroup("这是一个新的组");

        MyRunnable my = new MyRunnable();
        // Thread(ThreadGroup group, Runnable target, String name)
        Thread t1 = new Thread(tg, my, "张三");
        Thread t2 = new Thread(tg, my, "李四");

        System.out.println(t1.getThreadGroup().getName());//这是一个新的组
        System.out.println(t2.getThreadGroup().getName());//这是一个新的组

        //通过组名称设置后台线程,表示该组的线程都是后台线程
        tg.setDaemon(true);
    }
}

八、线程池 

  程序启动一个新线程成本是比较高的,因为它涉及到要与操作系统进行交互。而使用线程池可以很好的提高性能,尤其是当程序中要创建大量生存期很短的线程时,更应该考虑使用线程池。

【线程池的好处】
  线程池里的每一个线程代码结束后,并不会死亡,而是再次回到线程池中成为空闲状态,等待下一个对象来使用。

8.1 如何实现线程池

  在JDK5之前,我们必须手动实现自己的线程池,从JDK5开始,Java内置支持线程池。

  JDK5新增了一个Executors工厂类来产生线程池,其中有个方法为:

public static ExecutorService newFixedThreadPool(int nThreads)

  它表示创建一个线程池对象,控制要创建几个线程对象。  

  这些方法的返回值是ExecutorService对象,该对象表示一个线程池,可以执行Runnable对象或者Callable对象代表的线程。它提供了如下方法

Future<?> submit(Runnable task)
<T> Future<T> submit(Callable<T> task)

  如果需要关闭线程池,可以调用ExecutorService对象的shutdown()方法

public class MyRunnable implements Runnable {

    @Override
    public void run() {
        for (int x = 0; x < 100; x++) {
            System.out.println(Thread.currentThread().getName() + ":" + x);
        }
    }

}
public class ExecutorsDemo {
    public static void main(String[] args) {
        // 创建一个线程池对象,控制要创建几个线程对象。
        // public static ExecutorService newFixedThreadPool(int nThreads)
        ExecutorService pool = Executors.newFixedThreadPool(2);

        // 可以执行Runnable对象或者Callable对象代表的线程
        pool.submit(new MyRunnable());
        pool.submit(new MyRunnable());

        //结束线程池
        pool.shutdown();
    }
}

8.2 多线程的实现方案3

  前面已经介绍过多线程的2中实现方案了,其实多线程还有第三种实现方案,这种方式做了解即可。

  这种方式要实现Callable接口,而且它是基于线程池的基础上扩展的方式,不能分离线程池来使用。

//Callable:是带泛型的接口。
//这里指定的泛型其实是call()方法的返回值类型。
public class MyCallable implements Callable {

    @Override
    public Object call() throws Exception {
        for (int x = 0; x < 100; x++) {
            System.out.println(Thread.currentThread().getName() + ":" + x);
        }
        return null;
    }
}
public class CallableDemo {
    public static void main(String[] args) {
        //创建线程池对象
        ExecutorService pool = Executors.newFixedThreadPool(2);
        
        //可以执行Runnable对象或者Callable对象代表的线程
        pool.submit(new MyCallable());
        pool.submit(new MyCallable());
        
        //结束
        pool.shutdown();
    }
}

【练习】  

  实现多线程求和案例:

public class MyCallable implements Callable<Integer> {

    private int number;

    public MyCallable(int number) {
        this.number = number;
    }

    @Override
    public Integer call() throws Exception {
        int sum = 0;
        for (int x = 1; x <= number; x++) {
            sum += x;
        }
        return sum;
    }

}
public class CallableDemo {
    public static void main(String[] args) throws InterruptedException, ExecutionException {
        // 创建线程池对象
        ExecutorService pool = Executors.newFixedThreadPool(2);

        // 可以执行Runnable对象或者Callable对象代表的线程
        // Future 表示异步计算的结果
        Future<Integer> f1 = pool.submit(new MyCallable(100));
        Future<Integer> f2 = pool.submit(new MyCallable(200));

        // V get() 如有必要,等待计算完成,然后获取其结果。
        Integer i1 = f1.get();
        Integer i2 = f2.get();

        System.out.println(i1);
        System.out.println(i2);

        // 结束
        pool.shutdown();
    }
}

九、匿名内部类实现多线程

  首先回顾一下匿名内部类的格式:

new 类名或者接口名() {
    重写方法;
};

  匿名内部类的本质是:是该类或者接口的子类对象。

public class ThreadDemo {
    public static void main(String[] args) {
        // 继承Thread类来实现多线程
        new Thread() {
            @Override
            public void run() {
                for (int x = 0; x < 100; x++) {
                    System.out.println(Thread.currentThread().getName() + ":" + x);
                }
            }
        }.start();

        // 实现Runnable接口来实现多线程
        new Thread(new Runnable() {
            @Override
            public void run() {
                for (int x = 0; x < 100; x++) {
                    System.out.println(Thread.currentThread().getName() + ":"
                            + x);
                }
            }
        }).start();
    }
}

十、多线程常见问题总结

  1. 多线程有几种实现方案,分别是哪几种?
    两种。 继承Thread类、实现Runnable接口
  2. 同步有几种方式,分别是什么?
    两种。同步代码块和同步方法
  3. 启动一个线程是run()还是start()?它们的区别?
    start();
    run():封装了被线程执行的代码,直接调用仅仅是普通方法的调用
    start():启动线程,并由JVM自动调用run()方法
  4. sleep()和wait()方法的区别
    sleep():必须指定时间;不释放锁。
    wait():可以不指定时间,也可以指定时间;释放锁。
  5. 为什么wait(),notify(),notifyAll()等方法都定义在Object类中
    因为这些方法的调用是依赖于锁对象的,而同步代码块的锁对象是任意锁。
  6. 线程的生命周期图

  

 

posted @ 2019-05-24 19:36  yi0123  阅读(331)  评论(0编辑  收藏  举报