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

------ 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培训、期待与您交流! -------
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值