黑马程序员——Java中多线程技术

来源:互联网 发布:明道办公软件登陆 编辑:程序博客网 时间:2024/05/18 15:23
------Java培训、Android培训、期待与您交流! -------



一、多线程概念

    学习多线程首先需要掌握一些基本概念:

 

1、进程
    进程是指一个正在运行的程序,它经历了从代码加载、执行到执行完毕的整个过程,每个进程都有自己独立的一块内存空间,多进程操作系统可以同时运行多个进程。

2、线程

线程是指进程中一个负责程序执行的控制单元(执行路径)。一个进程中可以运行多个线程,多个线程可共享数据。

3、多进程

     多线程是指一个进程中可以有多条执行路径,即一个进程中可以运行多个线程,实现多个程序的并发运行,这样每个线程都可以运行自己的内容,这个内容成为线程执行的任务。

     多线程的好处:解决了多任务同时运行的问题。

     多线程的弊端:线程太多造成运行效率降低。

4、多线程的内在原理

    单核CPU在某个瞬间只能运行一个线程,而实现多线程是CPU在不同的线程间进行非常快速的切换,虽然看起来程序在同时运行,但是每一次的结果不一样,这是因为多线程具有随机性,多线程在运行时,各个线程在抢CPU资源。

 二、创建线程的方式

 1、继承Thread类

    步骤:

a)       定义一个类继承Thread类;

b)       覆盖Thread类中的run方法;

c)       直接创建Thread的子类对象同时创建线程;

d)       调用start方法开启线程并调用run方法执行线程的任务。

     覆盖run方法:Thread类用于描述线程。线程的运行需要有任务,而Thread类中的run方法就是封装自定义线程运行任务的函数,只要继承Thread类,复写run方法,将运行的任务代码定义在run方法中就可以运行。

    run方法和start方法的区别:run方法是在本线程内调用该对象的run()方法,是做了一件事,可以重复多次调用;start方法是启动一个线程,调用该该对象的run()方法,是做了两件事,不能多次启动一个线程。

范例:

//通过继承Thread类创建线程class Demo extends Thread {      privateString name;       // 定义构造函数获取name      Demo(Stringname) {             //super(name);             this.name= name;      }       // 复写run方法      public voidrun() {             // 打印自定义线程运行情况             for(int x = 0; x < 10; x++) {                    System.out.println(name+ "....x=" + x + ".....name="                                  +Thread.currentThread().getName());             }      }} class ThreadDemo {      publicstatic void main(String[] args) {             // 创建自定义线程             Demod1 = new Demo("my");             Demod2 = new Demo("your");              // 开启线程,调用run方法             d1.start();             d2.start();              // 打印主线程的运行情况             for(int x = 0; x < 5; x++) {                    System.out.println(x+ "...." + Thread.currentThread().getName());             }      }}


输出结果:

0....main

1....main

2....main

3....main

4....main

your....x=0.....name=Thread-1

your....x=1.....name=Thread-1

your....x=2.....name=Thread-1

your....x=3.....name=Thread-1

your....x=4.....name=Thread-1

my....x=0.....name=Thread-0

my....x=1.....name=Thread-0

my....x=2.....name=Thread-0

my....x=3.....name=Thread-0

your....x=5.....name=Thread-1

your....x=6.....name=Thread-1

your....x=7.....name=Thread-1

your....x=8.....name=Thread-1

your....x=9.....name=Thread-1

my....x=4.....name=Thread-0

my....x=5.....name=Thread-0

my....x=6.....name=Thread-0

my....x=7.....name=Thread-0

my....x=8.....name=Thread-0

my....x=9.....name=Thread-0

    由结果可知, 可以通过Thread的getName获取线程的名称:Thread-编号(从0开始).主线程的名字是main。每一个线程是执行是随机、交替执行的,每一次运行的结果都会不同。

2、实现Runnable接口

    步骤:

a)       定义类实现Runnable接口;

b)       覆盖接口中的run方法,将线程的任务代码封装到run方法中;

c)       通过Thread类创建线程对象,并将Runnable接口的子类对象作为Thread类的构造函数的参数进行传递;

d)       调用线程对象的start方法开启线程。

    这种创建线程方式的好处:将线程的任务进行单独封装;避免了Java单继承的局限性。因此,这种方式比较常用。

范例:

//实现Runnable接口创建线程class Demo1 implements Runnable {      privateString name;       // 定义构造函数获取name      Demo1(Stringname) {             //super(name);             this.name= name;      }       // 复写run方法      public voidrun() {             // 打印自定义线程运行情况             for(int x = 0; x < 10; x++) {                    System.out.println(name+ "....x=" + x + ".....name="                                  +Thread.currentThread().getName());             }      }} class ThreadDemo1 {      publicstatic void main(String[] args) {             // 创建自定义线程             Demo1d1 = new Demo1("my");             Demo1d2 = new Demo1("your");             Threadt1 = new Thread(d1);             Threadt2 = new Thread(d2);              // 开启线程,调用run方法             t1.start();             t2.start();              // 打印主线程的运行情况             for(int x = 0; x < 5; x++) {                    System.out.println(x+ "...." + Thread.currentThread().getName());             }      }}

输出结果为:

0....main

1....main

2....main

3....main

4....main

my....x=0.....name=Thread-0

my....x=1.....name=Thread-0

my....x=2.....name=Thread-0

my....x=3.....name=Thread-0

my....x=4.....name=Thread-0

my....x=5.....name=Thread-0

my....x=6.....name=Thread-0

my....x=7.....name=Thread-0

my....x=8.....name=Thread-0

my....x=9.....name=Thread-0

your....x=0.....name=Thread-1

your....x=1.....name=Thread-1

your....x=2.....name=Thread-1

your....x=3.....name=Thread-1

your....x=4.....name=Thread-1

your....x=5.....name=Thread-1

your....x=6.....name=Thread-1

your....x=7.....name=Thread-1

your....x=8.....name=Thread-1

your....x=9.....name=Thread-1

 

三、线程的状态

   

线程的五种状态:创建、运行、阻塞、冻结、消亡。

   

四、线程安全问题分析

 

1、产生原因

    多个线程在运行时,它们操作的数据是相互共享的,并且操作共享数据的线程代码有多条。这时当一个线程在执这些代码过程中,其他线程若参与执行,就会导致线程安全问题的产生。(多线程访问延迟、线程随机性)

   2、解决办法

    若要避免线程安全问题的产生,就需要将多条操作共享数据的代码封装起来,当有线程执行这些代码时,不让其他线程参与执行。

Java中用“同步”就可以解决这个问题。

 

五、同步

 

  1、同步的前提和利弊

    当多个线程同时存在并且在使用同一个锁时才能使用同步。同步能够解决线程的安全问题,但是由于要判断锁,降低了效率。

2、同步代码块

    代码格式:

    synchronized(对象)

    {需要被同步的代码}

    同步代码块的锁为任意对象,建议使用同步代码块。

范例:

//同步代码块演示//实现Runnable接口创建多线程class TestThreadimplements Runnable {   private int tickets = 20;    //覆盖run方法   public void run() {          while (true) {//无限循环保证程序一直运行                 synchronized (this) {//同步代码块,this代表本类对象                        if (tickets > 0) {                               //调用Thread.sleep()方法实现线程的切换                               try {                                      Thread.sleep(100);                               } catch(Exception e) {                               }                               System.out.println(Thread.currentThread().getName()+ "出售票"                                             +tickets--);                        }                 }          }   }} public classSynchroBlockDemo {   public static void main(String[] args) {          TestThread t = new TestThread();          // 启动了四个线程,实现了资源共享的目的          new Thread(t).start();          new Thread(t).start();          new Thread(t).start();          new Thread(t).start();   }}


输出结果为:

Thread-1出售票20

Thread-1出售票19

Thread-1出售票18

Thread-1出售票17

Thread-1出售票16

Thread-1出售票15

Thread-2出售票14

Thread-0出售票13

Thread-3出售票12

Thread-3出售票11

Thread-3出售票10

Thread-0出售票9

Thread-0出售票8

Thread-2出售票7

Thread-2出售票6

Thread-1出售票5

Thread-1出售票4

Thread-1出售票3

Thread-1出售票2

Thread-1出售票1

 

3、同步函数

   同步函数的定义只需在需要同步的函数定义前加上synchronized关键字即可。

   同步函数的锁为固定的this。

范例:

//同步函数演示//实现Runnable接口创建多线程class MyThread implements Runnable {      privateint tickets = 20;       //覆盖run方法      publicvoid run() {             while(true) {// 无限循环保证程序一直运行                    sale();             }      }       //定义同步函数      publicsynchronized void sale() {             if(tickets > 0) {                    //调用Thread.sleep()方法实现线程的切换                    try{                           Thread.sleep(100);                    }catch (Exception e) {                    }                    System.out.println(Thread.currentThread().getName()+ "出售票"                                  +tickets--);             }      }} public class SynchroFunctionDemo {      publicstatic void main(String[] args) {             MyThreadt = new MyThread();             //启动了四个线程,实现了资源共享的目的             newThread(t).start();             newThread(t).start();             newThread(t).start();             newThread(t).start();      }}


输出结果:

Thread-0出售票20

Thread-2出售票19

Thread-2出售票18

Thread-3出售票17

Thread-1出售票16

Thread-3出售票15

Thread-3出售票14

Thread-3出售票13

Thread-3出售票12

Thread-3出售票11

Thread-3出售票10

Thread-2出售票9

Thread-2出售票8

Thread-2出售票7

Thread-0出售票6

Thread-0出售票5

Thread-2出售票4

Thread-2出售票3

Thread-3出售票2

Thread-1出售票1

 

4、静态同步函数

    静态的同步函数使用的锁是该函数所属字节码文件对象,可以用 getClass方法

获取,也可以用当前类名.class 表示。

     范例:

   

 //验证静态同步函数的锁class Ticket implements Runnable {privatestatic int num = 10;booleanflag = true;// 标志用于同步代码快和同步函数的切换 // 覆盖run方法publicvoid run() {        //同步代码块执行        if(flag) {               while(true) {                      //将同步代码块的锁设置为Ticket.class,用于验证静态同步函数的锁                      synchronized(Ticket.class)// 该锁也可用this.getClass()获取                      {                             if(num > 0) {                                    try{                                           Thread.sleep(10);                                    }catch (InterruptedException e) {                                    }                                    System.out.println(Thread.currentThread().getName()                                                  +".....block...." + num--);                             }                      }               }        }else {               //同步函数执行               while(true)                      Ticket.show();        }} // 静态同步函数publicstatic synchronized void show() {        if(num > 0) {               try{                      Thread.sleep(10);               }catch (InterruptedException e) {               }                System.out.println(Thread.currentThread().getName()                             +".....function...." + num--);        }}} class StaticSynchroFunctionDemo {publicstatic void main(String[] args) {        Tickett = new Ticket();         //创建并启动线程        Threadt1 = new Thread(t);        Threadt2 = new Thread(t);         t1.start();        //切换同步函数执行        try{               Thread.sleep(10);        }catch (InterruptedException e) {        }        t.flag= false;        t2.start();}}

输出结果:

Thread-0.....block....10

Thread-0.....block....9

Thread-1.....function....8

Thread-1.....function....7

Thread-1.....function....6

Thread-1.....function....5

Thread-1.....function....4

Thread-1.....function....3

Thread-1.....function....2

Thread-1.....function....1

 

  5、死锁情况

    多个进程争夺多个锁的访问权时就有可能发生死锁。即同步的嵌套。最常见的形式是当线程1持有对象A上的锁,而正在等待对象B上的锁;而线程2持有对象B上的锁,却正在等待对象A上的锁。这样,两个线程都不会获得锁或者释放锁,会永远等待。

范例:

//死锁情况演示class Testimplements Runnable {       private boolean flag;        Test(boolean flag) {              this.flag = flag;       }        // 复写run方法       public void run() {               // 同步嵌套              if (flag) {                     while (true)                            // 锁locka                            synchronized(MyLock.locka) {                                   System.out.println(Thread.currentThread().getName()                                                 +"..if   locka....");                                   // 锁lockb                                   synchronized(MyLock.lockb) {                                           System.out.println(Thread.currentThread().getName()                                                        +"..if   lockb....");                                   }                            }              } else {                     while (true)                            // lockb锁                            synchronized(MyLock.lockb) {                                   System.out.println(Thread.currentThread().getName()                                                 +"..else  lockb....");                                   // locka锁                                   synchronized(MyLock.locka) {                                          System.out.println(Thread.currentThread().getName()                                                        +"..else   locka....");                                   }                            }              }        } } // 设置同步代码块的锁class MyLock {       public static final Object locka = newObject();       public static final Object lockb = newObject();} classDeadLockDemo {       public static void main(String[] args) {              Test a = new Test(true);              Test b = new Test(false);               // 创建并启动线程              Thread t1 = new Thread(a);              Thread t2 = new Thread(b);              t1.start();              t2.start();       }}


输出结果:

Thread-0..if   locka....

Thread-0..if   lockb....

Thread-0..if   locka....

Thread-0..if   lockb....

Thread-0..if   locka....

Thread-1..else  lockb....

    此时,程序形成死锁,等待但不会向下执行。

 

六、线程间通信

 

 1、输入输出两个线程操作同一资源问题

    假设资源区有存储着人的姓名和性别两个属性,并有输入线程对其赋值,输出线程对其取值,这时由于CPU切换线程是随机的,就可能会出现人的姓名和性别不对应、赋值和取值不是交替进行的情况。那么该怎么解决呢?

范例:

//多线程通信class Resource {       private String name;       private String sex;       private boolean flag = false;// 标志用于线程切换        // 设置姓名和性别,使用同步函数解决线程安全问题       public synchronized void set(String name,String sex) {              if (flag)                     try {                            this.wait();// 线程冻结                     } catch(InterruptedException e) {                     }              this.name = name;              this.sex = sex;              flag = true;              this.notify();// 唤醒线程       }        // 输出姓名和性别,使用同步函数解决线程安全问题       public synchronized void out() {              if (!flag)                     try {                            this.wait();// 线程冻结                     } catch(InterruptedException e) {                     }              System.out.println(name +"---->" + sex);              flag = false;              notify();// 唤醒线程       }} // 输入线程class Inputimplements Runnable {       Resource r;        Input(Resource r) {              this.r = r;       }        // 复写run方法       public void run() {              int x = 0;// 用于 切换赋值              while (true) {                     if (x == 0) {                            r.set("小明", "男");                     } else {                            r.set("小红", "女");                     }                     x = (x + 1) % 2;              }       }} // 输出线程class Outputimplements Runnable {        Resource r;        Output(Resource r) {              this.r = r;       }        public void run() {              while (true) {                     r.out();              }       }} classThreadCommunation {       public static void main(String[] args) {              // 创建资源              Resource r = new Resource();              // 创建任务              Input in = new Input(r);              Output out = new Output(r);              // 创建线程              Thread t1 = new Thread(in);              Thread t2 = new Thread(out);              // 开启线程              t1.start();              t2.start();       }}


部分输出结果:

小明---->男

小红---->女

小明---->男

小红---->女

小明---->男

小红---->女

 

说明:

    等待唤醒机制:

    wait(): 让线程处于冻结状态,被wait的线程会被存储到线程池中。

    notify():唤醒线程池中一个线程(任意)。

    notifyAll():唤醒线程池中的所有线程,高优先级的线程被首先唤醒。

    这些方法都必须定义在同步中。因为这些方法是用于操作线程状态的方法。

    必须要明确到底操作的是哪个锁上的线程。为什么操作线程的方法wait notify notifyAll定义在了Object类中? 因为这些方法是监视器的方法。监视器其实就是锁。锁可以是任意的对象,任意的对象调用的方式一定定义在Object类中。

    另外,wait 和 sleep 有什么区别呢?

    wait:Object类中的方法,使线程处于“不可运行”状态,可以指定时间也可以不指定,用在同步中时释放CPU执行权,释放锁。

    sleep:Thread类中的方法,使线程处于“非运行”状态,必须指定时间,用在同步中时释放CPU执行权,不释放锁。

 

  2、多生产者多消费者问题


    多生产者,多消费者,即多个输入线程,多个输出线程操作资源,若还是按照上述程序使用if判断标记,结果只判断一次,会导致不该运行的线程运行,出现操作资源数据错误的。如果用while判断标记,就解决了这种情况,这时线程获取执行权后,一定会运行!

另外,同样若继续使用notify方法,只能唤醒一个线程,对于多消费者多生产者情况,就会出现本方线程唤醒了本方的情况,没有意义。而while判断标记加notify方法会导致死锁。因此需要用notifyAll方法,这时本方线程一定会唤醒对方线程。

范例:

//多生产者多消费者情况演示class Resource {    privateString name;    privateint count = 1;    privateboolean flag = false;// 标志用于多线程切换     //多生产者同步函数    publicsynchronized void set(String name) {           while(flag)                  //while判断标记                  try{                         this.wait();                  }catch (InterruptedException e) {                  }            this.name= name + count;// 不同生产者生产不同产品           count++;           System.out.println(Thread.currentThread().getName()+ "...生产者..."                         +this.name);           flag= true;           notifyAll();//唤醒所有线程    }     //多消费者同步函数    publicsynchronized void out() {           while(!flag)                  try{                         this.wait();                  }catch (InterruptedException e) {                  }           System.out.println(Thread.currentThread().getName()+ "...消费者........"                         +this.name);           flag= false;           notifyAll();    }} // 生产者线程类class Producer implements Runnable {    privateResource r;     Producer(Resourcer) {           this.r= r;    }     //复写run方法    publicvoid run() {           while(true) {                  r.set("产品");// 生产产品           }    }} // 消费者线程类class Consumer implements Runnable {    privateResource r;     Consumer(Resourcer) {           this.r= r;    }     publicvoid run() {           while(true) {                  r.out();//消费产品           }    }} class ProducerConsumerDemo {    publicstatic void main(String[] args) {           //创建资源           Resourcer = new Resource();            //创建任务           Producerpro = new Producer(r);           Consumercon = new Consumer(r);            //创建线程           Threadt0 = new Thread(pro);           Threadt1 = new Thread(pro);           Threadt2 = new Thread(con);           Threadt3 = new Thread(con);            //开启线程           t0.start();           t1.start();           t2.start();           t3.start();     }}


部分输出结果:

Thread-3...消费者........产品36031

Thread-0...生产者...产品36032

Thread-3...消费者........产品36032

Thread-1...生产者...产品36033

Thread-2...消费者........产品36033

Thread-1...生产者...产品36034

Thread-3...消费者........产品36034

Thread-0...生产者...产品36035

Thread-3...消费者........产品36035

Thread-1...生产者...产品36036

Thread-2...消费者........产品36036

Thread-1...生产者...产品36037

Thread-3...消费者........产品36037

 

  3、JDK1.5版本多生产多消费问题解决办法

    JDK1.5以后将同步和锁封装成了对象,并将操作锁的隐式方式定义到了该对象中,将隐式动作变成了显示动作。

    Lock接口:替代了同步代码块或者同步函数,将同步的隐式锁操作变成现实锁操作,更为灵活,可以一个锁上加上多组监视器。

    lock():获取锁。

    unlock():释放锁,通常需要定义finally代码块中。

    Condition接口:出现替代了Object中的wait、notify、notifyAll方法,为await()、signal()、signalAll()。将这些监视器方法单独进行了封装,变成Condition监视器对象,可以任意锁进行组合。

上个例子中的部分程序可以修改如下:

//JDK1.5版本多线程新特性import java.util.concurrent.locks.*; class Resource {      privateString name;      privateint count = 1;      privateboolean flag = false;       //创建一个锁对象。      Locklock = new ReentrantLock();       //通过已有的锁获取两组监视器,一组监视生产者,一组监视消费者。      Conditionproducer_con = lock.newCondition();      Conditionconsumer_con = lock.newCondition();       //生产者      publicvoid set(String name) {             lock.lock();//获取锁             try{                    while(flag)                           //try{lock.wait();}catch(InterruptedException e){}                           try{                                  producer_con.await();//await方法相当于wait方法                           }catch (InterruptedException e) {                           }                     this.name= name + count;                    count++;                    System.out.println(Thread.currentThread().getName()                                  +"...生产者5.0..." + this.name);                    flag= true;                    //notifyAll();                    //con.signalAll();                    consumer_con.signal();             }finally {                    lock.unlock();//释放锁             }       }       //消费者      publicvoid out() {             lock.lock();             try{                    while(!flag)                           //try{this.wait();}catch(InterruptedException e){} //t2 t3                           try{                                  consumer_con.await();                           }catch (InterruptedException e) {                           }                    System.out.println(Thread.currentThread().getName()                                  +"...消费者.5.0......." + this.name);                    flag= false;                    //notifyAll();                    //con.signalAll();                    producer_con.signal();             }finally {                    lock.unlock();             }       }}


七、多线程其他内容总结

1、停止线程:stop()方法结束和run()方法结束。一般使用后者,因为stop()方法会导致数据的不完整。用run()方法结束线程,可以通过控制任务中的循环来结束任务。如果线程处于冻结状态,可以使用interrupt()方法使线程强制恢复运行状态,再停止线程,不过需要处理InterruptException。

2、Thread.currentThread.getName():获取当前线程的名称

3、setPriority():设置线程的优先级。MAX_PRIORITY最高优先级10。MIN_PRIORITY最低优先级1。NORM_PRIORITY 分配给线程的默认优先级。

4、join():临时加入一个线程。

5、isAlive():判断线程是否启动。

6、setDaemon(true);设置线程为后台线程(守护线程)。

 


------Java培训、Android培训、期待与您交流! -------
0 0
原创粉丝点击