首页 > 代码库 > 09、多线程(一) -- 基本概念

09、多线程(一) -- 基本概念

1.1、多线程基本使用

1、线程的创建方式 

多线程的创建有两种方式,分别如下:

继承

  • 继承Thread类,并重写run方法,将需要多线程的代码放入run方法中。
  • 通过Thread的子类的引用调用start()方法来开启线程。

实现

  • 定义类实现Runnable接口,覆盖Runnable接口中的run方法。
  • 通过Thread类建立线程对象。
  • 将Runnable接口的子类对象作为实际参数传递给Thread类的构造函数。
  • 调用Thread类的star方法开启线程并调用Runnable接口子类的run方法。

继承的方式:

public class ThreadDemo {    public static void main(String[] args) {        new MyThread().start();    }}class MyThread extends Thread{    @Override    public void run() {        // 多线程代码        System.out.println("多线程开启了");    }}

实现的方式:

public class ThreadDemo {    public static void main(String[] args) {        // 第一种写法/*      MyThread t1 = new MyThread();        Thread thread = new Thread(t1);        thread.start();*/                // 开发中写法        new Thread(){            @Override            public void run() {                System.out.println("线程开启了");            }        }.start();    }}class MyThread implements Runnable{    public void run() {        System.out.println("线程开启了");    }}

 线程都有自己默认的名称,通过getName和setName来进行获取和设置。同时,还可以通过Thread.currentThread()来获取当前线程对象。

2、 线程的运行状态

被创建-------------------->运行-------------------------->消亡(stop(已过时)或run方法结束)

                                            |

                                          阻塞-->(阻塞状态:具备运行资格,但不具备cpu执行权。)

                                            |

                                         冻结(sleep和wait):不具备运行资格,也不具备cpu执行权。

睡眠状态和等待状态:不具备运行资格,也没有执行权。

  • 睡眠状态:sleep(time),当线程遇到sleep会进入睡眠状态,当睡眠时间到达后可能是运行状态,也可能进入临时状态(阻塞状态)。
  • 等待状态:wait(),当线程遇到wait会进入等待状态,这时需要notify()来唤醒,唤醒后可能是运行状态,也可能是临时状态(阻塞状态)。

消亡的两种方式:通过stop()命令强行结束线程或run()方法执行结束。

3、 多线程售票实例

  火车站有100张票,分别在t1、t2、t3、t4等4个窗口进行出售。

由此首先我们考虑到的是将100张票定义为静态共享变量,并开启四个线程来出售,那么代码如下:

public class ThreadDemo {    public static void main(String[] args) {        Ticket ticket = new Ticket();        Thread t1 = new Thread(ticket);// 售票窗1        Thread t2 = new Thread(ticket);// 售票窗2        Thread t3 = new Thread(ticket);// 售票窗3        Thread t4 = new Thread(ticket);// 售票窗4        t1.start();        t2.start();        t3.start();        t4.start();    }}// 售票厅class Ticket extends Thread {    private static int ticks = 100;// 出售票的方法    public void sale() {        System.out.println(Thread.currentThread().getName() + "sale:" + ticks--);    }    @Override    public void run() {        while (true) {            if (ticks > 0) {                sale();            }        }    }}

输出结果:

Thread-0sale:3
Thread-0sale:2
Thread-0sale:1
Thread-2sale:93
Thread-1sale:89
Thread-3sale:91

从结果中我们可以看出,售卖过程中我们发现出售的顺序已经错乱,这是因为线程太快导致,我们可以让线程在输出时进行睡眠20毫秒:

// 售票厅class Ticket extends Thread{    private static int ticks = 100;    // 出售票的方法    public void sale(){        System.out.println(Thread.currentThread().getName() + "sale:" + ticks--);    }    @Override    public void run() {        while(true){            if(ticks > 0){
try{Thread.sleep(20);}catch(Exception e){} sale(); }
} }}

输出结果:

Thread-2sale:77Thread-3sale:77Thread-0sale:77Thread-1sale:77

从结果中,我们看到出现售卖相同的票,那么线程的安全隐患就暴露出来了,那么我们如何解决这个问题呢?

我们只能通过线程同步来解决该问题,来保住每次被并发执行的代码中只能有一个线程在操作,以此解决线程的安全问题。

4、 多线程同步

由上面代码的示例我们知道,直接开启四个线程进行售卖是存在安全隐患的,我们必须通过同步来解决线程安全问题。

同步的前提:

  • 必须要有两个或者两个以上的线程。
  • 必须是多个线程使用同一个锁。必须保证同步中只能有一个线程在运行。

线程的同步解决了多线程的安全问题,但是多个线程都需要判断锁,较为耗费资源。

a) 如果需要同步的代码只是部分,则可以使用同步代码块,同步代码块的锁对象可以是任意的对象:

// 售票厅class Ticket extends Thread {    private static int ticks = 100;    Object obj = new Object();    // 出售票的方法    public void sale() {        System.out.println(Thread.currentThread().getName() + "sale:" + ticks--);    }    @Override    public void run() {        while (true) {            synchronized(obj){                if (ticks > 0) {                    try{Thread.sleep(20);}catch(Exception e){}                    sale();                }            }        }    }}

输出结果:

Thread-1sale:100Thread-1sale:99Thread-1sale:98...Thread-2sale:3Thread-2sale:2Thread-2sale:1

b) 函数需要被对象调用。那么函数都有一个所属对象引用,就是this,所以同步函数使用的锁是this

// 售票厅class Ticket extends Thread {    private static int ticks = 100;    Object obj = new Object();    // 出售票的方法    public void sale() {        System.out.println(Thread.currentThread().getName() + "sale:" + ticks--);    }    @Override    public void run() {        while (true) {            synSal();        }    }    // 同步函数    public synchronized void synSal(){        if (ticks > 0) {            try{Thread.sleep(20);}catch(Exception e){}            sale();        }    }}

输出结果:

Thread-1sale:100Thread-1sale:99Thread-1sale:98...Thread-2sale:3Thread-2sale:2Thread-2sale:1

c) 当同步函数被static(静态)所修饰的时候,使用的锁是所在类的字节码文件(类名.class)。

// 售票厅class Ticket extends Thread {    private static int ticks = 100;    Object obj = new Object();    // 出售票的方法    public static void sale() {        System.out.println(Thread.currentThread().getName() + "sale:" + ticks--);    }    @Override    public void run() {        while (true) {            synSal();        }    }    // 同步函数    public static synchronized void synSal(){        if (ticks > 0) {            try{Thread.sleep(20);}catch(Exception e){}            sale();        }    }}

输出结果:

Thread-1sale:100Thread-1sale:99Thread-1sale:98...Thread-2sale:3Thread-2sale:2Thread-2sale:1

d) 多线程--死锁,同步中嵌套同步,而锁却不同就会造成死锁,从而导致程序无法继续向下运行。

class DeadLock implements Runnable{    //定义一个标记    private boolean flag;    DeadLock(boolean flag){        this.flag = flag;    }    //重写run方法    public void run(){        if(flag){            synchronized(MyLock.locka){                synchronized(MyLock.lockb){                    System.out.println("if locka");                }            }        }        else{            synchronized(MyLock.lockb){                synchronized(MyLock.locka){                    System.out.println("else lockb");                }            }        }    }}//定义两个锁class MyLock{    static Object locka = new Object();    static Object lockb = new Object();}public class DeadLockDemo {    public static void main(String[] args) {        new Thread(new DeadLock(true)).start();        new Thread(new DeadLock(false)).start();    }}

5、多线程之间的通信

a) 等待唤醒机制

  • notify():激活线程池中 wait的线程。(谁先进去就唤醒谁)
  • notifyAll():激活线程池中所有wait的线程。
  • notify,notifyAll和wait等方法必须用在同步中,也就是说同步是前提。
  • 而且这几种方法都是继承自Object类,出现异常,只能try而不能抛。
  • 等待和唤醒必须是同一个锁,锁可以是任意对象,所以方法定义在Object类中。
  • 都使用在同步中,因为要持有监视器(锁)的线程操作。
  • 所以要使用在同步中,因为只有同步才具有锁。

b) 生产者和消费者实例

  我们来看一个生产者和消费者的实例,在生产商品的同时将商品销售或消费掉。

class ProducerConsumerDemo {    public static void main(String[] args) {        Resource res = new Resource();              Producer pro = new Producer(res);        Consumer con = new Consumer(res);        Thread t1 = new Thread(pro);        Thread t2 = new Thread(pro);        Thread t3 = new Thread(con);        Thread t4 = new Thread(con);        t1.start();        t2.start();        t3.start();        t4.start();    }}
class Resource { private String name; private int count = 1; private boolean flag = false; public synchronized void set(String name) { while (flag) try { this.wait(); } catch (Exception e) { } this.name = name + "--" + count++; System.out.println(Thread.currentThread().getName() + "...生产者.." + this.name); flag = true; this.notifyAll(); } public synchronized void out() { while (!flag) try { wait(); } catch (Exception e) { } System.out.println(Thread.currentThread().getName() + "...消费者........." + this.name); flag = false; this.notifyAll(); }}class Producer implements Runnable { private Resource res; Producer(Resource res) { this.res = res; } public void run() { while (true) { res.set("+商品+"); } }}class Consumer implements Runnable { private Resource res; Consumer(Resource res) { this.res = res; } public void run() { while (true) { res.out(); } }}

为什么要定义while判断标记?

  对于多个生产者和消费者需要用while判断标记,来让被唤醒的线程再一次判断标记。

为什么要定义notifyAll()?

  因为在本线程进入冻结或睡眠状态时,需要唤醒对方线程,如果用notify(),容易出现只唤醒本方线程的情况,导致程序中所有线程都进入等待状态。

我们可以看下简单的总结:

  一个生产线程对应一个消费线程,我们可以用if(flag)来判断标记,唤醒必须用notify()。

if(flag)    -->    notify()

  多个生产线程对应多个消费线程,我们必须用while(flag)来判断标记,唤醒必须用notifyAll()。(while循环下唤醒是notify()的话会导致全部等待)

while(flag)    -->    notifyAll();

死锁和全部等待:死锁是争抢执行权而导致程序停止,全部等待是所有线程都进入冻结状态。

针对唤醒本方的同时也唤醒对方的问题,Jdk1.5对该问题进行了针对性的处理,请参考下面的新特性。

09、多线程(一) -- 基本概念