zoukankan      html  css  js  c++  java
  • 黑马程序员——【Java基础】——多线程

    ---------- android培训java培训、期待与您交流! ----------

    一、概述

      (一)进程

      正在执行中的程序,每一个进程执行都有一个执行顺序。该顺序是一个执行路径,或者叫一个控制单元。

      (二)线程

      进程中的一个独立的控制单元。线程在控制着进程的执行。一个进程中至少有一个线程。只要进程中有一个线程在执行,进程就不会结束。

      (三)多线程

      在java虚拟机启动的时候会有一个java.exe的执行程序,也就是一个进程。该进程中至少有一个线程负责java程序的执行。而且这个线程运行的代码存在于main方法中。该线程称之为主线程。JVM启动除了执行一个主线程,还有负责垃圾回收机制的线程。这种在一个进程中有多个线程执行的方式,就叫做多线程。

      (四)多线程创建线程的目的和意义

      1、创建目的:开启一条执行路径;运行的代码就是执行路径的任务。JVM创建的主线程的任务都定义在了主函数中。run方法中,定义的代码就是线程要运行的任务代码。

      2、创建的意义:可以让程序产生同时运行效果;可以提高程序执行效率。

    二、创建线程的方式

      创建线程共有两种方式:继承方式和实现方式。

      (一)“继承Thread类”创建多线程

      Java API文档的java.lang包中提供了Thread类,通过继承Thread类,然后覆写其run方法来创建线程。

      1、创建步骤

      (1)定义类,继承Thread。

      (2)覆写Thread类中的run方法。

      (3)调用start方法,启动线程,调用run方法执行线程任务代码。

      2、多线程run和start的特点

      为什么要覆盖run方法呢?Thread类用于描述线程,该类定义了一个功能,用于存储要运行的代码,这些运行的代码就存储在run方法中。

      3、练习:创建两线程,和主线程交替运行。示例代码如下:

     1 //创建线程Test  
     2 class Test extends Thread{
     3     Test(String name){
     4          super(name);
     5     }
     6     //覆写run方法
     7     public void run(){
     8         for(int x=0;x<150;x++)
    9        System.out.println(Thread.currentThread().getName()+"...run..."+x); 10 } 11 } 12 class MultiThread{ 13 public static void main(String[] args){ 14 new Test("Thread one-------").start();//开启第一个线程 15 new Test("Thread two---").start();//开启第二个线程 16 //主线程执行的代码 17 for(int x=0;x<150;x++) 18 System.out.println("Hello World!"); 19 } 20 }

       说明:上述代码每次运行的结果都不一样。因为多个线程都在获取CPU的执行权,CPU执行到谁谁就运行。更明确一点说,在某一时刻,只能有一个程序在运行(多核除外)。CPU是在做着快速的切换,已达到看上去是同时在运行的效果。 

      (二)“实现Runnable接口”创建多线程

      使用Thread类继承方式有一个弊端,即如果该类本来就继承了其他父类,那么就无法通过继承Thread类来创建多线程。这样就有了第二种创建线程的方式:实现Runnable接口,并覆写其中的run方法来创建多线程。

      1、创建步骤

      (1)定义类实现Runnable接口。

      (2)覆盖Runnable接口中的run方法。

      (3)通过Thread类创建线程对象。

      (4)将Runnable接口的子类对象作为实参传递给Thread类的构造方法。

      (5)调用Thread类中start方法启动线程,start方法会自动调用Runnable接口子类的run方法。

      2、细节问题

      (1)为什么要将Runnable接口的子类对象传递给Thread类的构造函数?

      因为自定义的run方法所属的对象是Runnable接口的子类对象,所以要让线程去指定对象的run方法,就必须明确该run方法所属对象。

      (2)实现Runnable的好处:将线程任务从线程的子类中分离出来,进行单独的封装。避免了Java单继承的局限性。在定义线程时,建议使用实现方式。

      (三)“实现方式”和“继承方式”的区别

      1、继承方式:线程代码存放在Thread子类的run方法中。

      2、实现方式:线程代码存在接口的子类run方法中。

      (四)线程运行状态结构图

     

       运行状态结构图说明:

      1、被创建:等待启动,调用start启动。

      2、运行状态:具有执行资格和执行权。

      3、临时状态(阻塞):有执行资格,但是没有执行权。

      4、冻结状态:遇到sleep(time)方法和wait()方法时,失去执行资格和执行权,sleep方法时间到或者调用notify()方法时,获得执行资格,变为临时状态。

      5、消忙状态:stop()方法,或者run方法结束。

      (五)获取线程对象以及名称

      1、原来的线程都有自己默认的名称,命名方式是Thread-编号,编号从0开始。

      2、通过对象this.getName()、super(name),可以获取线程对象的名称。也可以通过Thread.currentThread().getName()标准方式获取线程的名称。说明:① static Thread currentThread():获取当前线程对象。② getName():获取线程名称。

      3、设置线程名称:setName(String name)方法或者构造函数都可以实现。

      4、局部变量在每一个线程区域中都有独立的一份。

      (六)练习:售票例子

     1 /* 简单卖票程序:多窗口同时卖票 */
     2 class Ticket implements Runnable//extends Thread{
     3     private  int tick = 100;
     4     public void run(){
     5         while(true){
     6             if(tick>0)
     7                 System.out.println(Thread.currentThread().getName()+"....sale : "+ tick--);
     8         }
     9     }
    10 }
    11 class TicketDemo{
    12     public static void main(String[] args){
    13         Ticket t = new Ticket();//创建Runnable接口子类的实例对象
    14         //有多个窗口在同时卖票,创建三个线程
    15         Thread t1 = new Thread(t);// 
    16         Thread t2 = new Thread(t);
    17         Thread t3 = new Thread(t);
    18         //启动线程
    19         t1.start();
    20         t2.start();
    21         t3.start();
    22     }
    23 }

    三、多线程的安全问题

      (一)多个线程“安全问题”

      1、问题的原因:当多条线程在操作同一个共享数据时,因为CPU的权限分配而出现一个线程对多条语句只执行一部分,还没有执行完,另外一条线程参与进来执行,导致共享数据的错误。

      2、解决思路:对操作共享数据的多条语句,只能让一个线程都执行完,而其他线程不能执行。

      (二)解决方式

      java对于多线程的安全问题提供了专业的解决方式:为需要操作共享数据的多条语句添加synchronized(同步)修饰符。有两种解决方式:一种是同步代码块,另一种是同步函数。都是利用关键字synchronized来实现。

      1、同步代码块

      (1)格式:synchronized(对象){ 需要被同步的代码块 }

      同步可以解决安全问题的根本原因就在那个对象上。对象如同一把锁,持有锁的线程可以在执行同步代码块,而没有持有锁的线程即使获取CPU执行权,也因为没有获取对象而无法执行同步代码块。

      (2)同步代码块使用的锁是哪一个呢?任意对象。

      (3)同步代码块示例:为多线程卖票程序加上同步代码块,代码如下:

     1 class Ticket implements Runnable {
     2     private int tick = 100;
     3     Object obj = new Object();
     4     public void run() {
     5         while (true) {
     6             // 给程序加同步,即锁
     7             synchronized (obj) {
     8                 if (tick > 0) {
     9                     try {// 使用线程中的sleep方法,模拟线程出现的安全问题
    10                     // 因为sleep方法有异常声明,所以这里要对其进行处理
    11                         Thread.sleep(10);
    12                     } catch (Exception e) {
    13                     }
    14                     System.out.println(Thread.currentThread().getName()+ "..tick..=" + tick--);// 显示线程名及余票数
    15                 }
    16             }
    17         }
    18     }
    19 }

      2、同步函数

      (1)格式:synchronized作为函数的修饰符,添加在函数上,就构成了同步函数。

      (2)同步函数用的是哪一个锁呢?因为函数需要被对象调用,那么函数都持有一个所属对象引用,即this。所以同步函数使用的锁是this。

      (3)同步函数示例

    1 public synchronized void add(int num){
    2     sun = sum + num;
    3     try{
    4         Thread.sleep(10);
    5         }catch(InterruptedException e){
    6         }
    7         System.out.println(“sum = ”+ sum);
    8 }

      3、静态同步函数

      (1)格式:同步函数被static修饰后,就构成了静态同步函数。

      (2)静态同步函数使用的锁是什么呢?Class对象。

      说明:因为静态在进内存时,内存中没有本类对象,但一定有该类对应的字节码文件对象,因此,静态同步函数使用的锁是该函数所属字节码文件对象,即 类名.class,该对象的类型是Class,可以用getClass()方法获取,也可以用 类名.class获取。

      (3)静态同步函数代码示例:

     1 class StaticSynchDemo implements Runnable{
     2     private static int num = 100;
     3     public void run(){ System.out.println(“num = ”+ num); };
     4     public static synchronized void show()    {
     5         if(num>0) {
     6             try{
     7                 Thread.sleep(10);
     8                }catch(InterruptedException e){ }
    9 System.out.println(Thread.currentThread().getName()+".....function...."+num--); 10 } 11 } 12 }

      (三)同步的前提和利弊

      1、同步的前提

      (1)必须要有两个或者两个以上的线程。

      (2)必须是多个线程使用同一个锁。

      (3)必须保证同步中,只能有一个线程在运行

      2、同步的利弊

      利:解决了多线的安全问题。

      弊:多个线程需要判断锁,较为消耗资源。

      (四)如何寻找多线程中的安全问题

      (1)明确哪些代码是多线程运行代码。

      (2)明确共享数据。

      (3)明确多线程运行代码中,哪些语句是操作共享数据的。

    四、死锁

      (一)死锁:常见情景之一是“同步的嵌套”。

      (二)“同步嵌套”出现的死锁示例:

     1 /* 写一个死锁程序 */
     2 class DeadLock implements Runnable {
     3     private boolean flag;
     4     DeadLock(boolean flag) {
     5         this.flag = flag;
     6     }
     7     public void run() {
     8         if (flag){
     9             while (true)
    10                 synchronized (MyLock.locka) {
    11                     System.out.println(Thread.currentThread().getName()+ "..if   locka....");
    12                     synchronized (MyLock.lockb){
    13                         System.out.println(Thread.currentThread().getName()+ "..if   lockb....");
    14                     }
    15                 }
    16         } else {
    17             while (true)
    18                 synchronized (MyLock.lockb) {
    19                     System.out.println(Thread.currentThread().getName()+ "..else  lockb....");
    20                     synchronized (MyLock.locka) {
    21                         System.out.println(Thread.currentThread().getName()+ "..else   locka....");
    22                     }
    23                 }
    24         }
    25     }
    26 }
    27 
    28 class MyLock {
    29     public static final Object locka = new Object();
    30     public static final Object lockb = new Object();
    31 }
    32 
    33 class DeadLockTest {
    34     public static void main(String[] args) {
    35         DeadLock a = new DeadLock(true);
    36         DeadLock b = new DeadLock(false);
    37 
    38         Thread t1 = new Thread(a);
    39         Thread t2 = new Thread(b);
    40         t1.start();
    41         t2.start();
    42     }
    43 }

    五、线程间通信

      (一)线程间通信

      多个线程在操作同一个资源,但是操作的动作不同。

      (二)等待唤醒机制

      1、涉及的方法

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

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

      (3)notifyAll():唤醒线程池中的所有线程。

      2、为什么这些方法都必须定义在同步中?

      因为这些方法是用于操作线程状态的方法,必须要明确到底操作的是哪个锁上的线程。

      3、为什么操作线程的方法wait、notify、notifyAll定义在了Object类中?

      因为这些方法是监视器的方法。监视器其实就是锁,锁可以是任意的对象,任意的对象调用的方式一定定义在Object类中。

      4、wait 和 sleep 区别?

      (1)wait可以指定时间也可以不指定;而sleep必须指定时间。

      (2)在同步中时,各自对CPU的执行权和锁的处理不同:① wait释放执行权,释放锁。② sleep释放执行权,不释放锁。

      5、为什么要定义notifyAll ?

      因为在需要唤醒对方线程时。如果只用notify,容易出现只唤醒本方线程的情况。导致程序中的所有线程都等待。

     1 * 练习:多线程通信示例(生产者——消费者)
     2 class Resource{
     3     private String name;
     4     private int count = 1;
     5     private boolean flag = false;
     6     public synchronized void set(String name){
     7         while(flag)
     8             try{this.wait();}catch(InterruptedException e){}        
     9         this.name = name + count;
    10         count++;
    11         System.out.println(Thread.currentThread().getName()+"...生产者..."+this.name);
    12         flag = true;
    13         notifyAll();
    14     }
    15 
    16     public synchronized void out(){
    17         while(!flag)
    18             try{this.wait();}catch(InterruptedException e){}
    19         System.out.println(Thread.currentThread().getName()+"...消费者........"+this.name);
    20         flag = false;
    21         notifyAll();
    22     }
    23 }
    24 
    25 class Producer implements Runnable{
    26     private Resource r;
    27     Producer(Resource r){
    28         this.r = r;
    29     }
    30     public void run(){
    31         while(true){
    32             r.set("烤鸭");
    33         }
    34     }
    35 }
    36 
    37 class Consumer implements Runnable{
    38     private Resource r;
    39     Consumer(Resource r){
    40         this.r = r;
    41     }
    42     public void run(){
    43         while(true){
    44             r.out();
    45         }
    46     }
    47 }
    48 
    49 class  ProducerConsumerDemo{
    50     public static void main(String[] args){
    51         Resource r = new Resource();
    52         Producer pro = new Producer(r);
    53         Consumer con = new Consumer(r);
    54 
    55         Thread t0 = new Thread(pro);
    56         Thread t1 = new Thread(pro);
    57         Thread t2 = new Thread(con);
    58         Thread t3 = new Thread(con);
    59         t0.start();
    60         t1.start();
    61         t2.start();
    62         t3.start();
    63     }
    64 }

      (三)关于同步和锁的JDK1.5新特性

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

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

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

      4、接口Lock上的一些方法

      (1)lock():获取锁。

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

      5、接口Condition上的一些方法

      (1)await():使当前线程在接到信号或被中断之前一直处于等待状态。

      (2)signal():唤醒一个等待线程。

      (3)signalAll():唤醒所有等待线程。

      1 练习:多生产者多消费者问题(JDK1.5解决办法)
      2 import java.util.concurrent.locks.*;
      3 class Resource2
      4 {
      5     private String name;
      6     private int count = 1;
      7     private boolean flag = false;
      8 
      9     //    创建一个锁对象。
     10     Lock lock = new ReentrantLock();    
     11 
     12     //通过已有的锁获取两组监视器,一组监视生产者,一组监视消费者。
     13     Condition producer_con = lock.newCondition();
     14     Condition consumer_con = lock.newCondition();
     15     
     16     public  void set(String name)
     17     {
     18         lock.lock();
     19         try
     20         {
     21             while(flag)
     22             try{producer_con.await();}catch(InterruptedException e){}
     23         
     24             this.name = name + count;
     25             count++;
     26             System.out.println(Thread.currentThread().getName()+"...生产者5.0..."+this.name);
     27             flag = true;
     28             consumer_con.signal();
     29         }
     30         finally
     31         {
     32             lock.unlock();
     33         }
     34         
     35     }
     36 
     37     public  void out()
     38     {
     39         lock.lock();
     40         try
     41         {
     42             while(!flag)
     43             try{consumer_con.await();}catch(InterruptedException e){}
     44             System.out.println(Thread.currentThread().getName()+"...消费者.5.0......."+this.name);
     45             flag = false;
     46             producer_con.signal();
     47         }
     48         finally
     49         {
     50             lock.unlock();
     51         }
     52         
     53     }
     54 }
     55 
     56 class Producer2 implements Runnable
     57 {
     58     private Resource r;
     59     Producer2(Resource r)
     60     {
     61         this.r = r;
     62     }
     63     public void run()
     64     {
     65         while(true)
     66         {
     67             r.set("烤鸭");
     68         }
     69     }
     70 }
     71 
     72 class Consumer2 implements Runnable
     73 {
     74     private Resource r;
     75     Consumer2(Resource r)
     76     {
     77         this.r = r;
     78     }
     79     public void run()
     80     {
     81         while(true)
     82         {
     83             r.out();
     84         }
     85     }
     86 }
     87 
     88 class  ProducerConsumerDemo2
     89 {
     90     public static void main(String[] args) 
     91     {
     92         Resource r = new Resource();
     93         Producer2 pro = new Producer2(r);
     94         Consumer2 con = new Consumer2(r);
     95 
     96         Thread t0 = new Thread(pro);
     97         Thread t1 = new Thread(pro);
     98         Thread t2 = new Thread(con);
     99         Thread t3 = new Thread(con);
    100         t0.start();
    101         t1.start();
    102         t2.start();
    103         t3.start();
    104     }
    105 }

    六、停止线程

      (一)停止线程的方式

        1、stop方法(已过时)。

        2、run方法结束。

      (二)run()方法结束线程

       1、“定义标记方式”结束线程任务

       怎么控制线程的任务结束呢? 开启多线程运行,运行代码通常都会有循环结构,只要控制住循环就可以让run方法结束,也就是线程结束。控制循环通常就用“定义标记”来完成。

       2、“interrupt()方法”结束线程任务

       如果线程处于了冻结状态,无法读取标记,如何结束呢?可以使用Thread对象的interrupt()方法将线程从冻结状态强制恢复到运行状态中来,让线程具备CPU的执行资格,再通过操作标记,让线程任务结束。但是强制动作会发生InterruptedException,记得要处理

      (三)线程停止示例

     1 class StopThread implements Runnable{
     2     private boolean flag = true;
     3     public synchronized void run(){
     4         while(flag){
     5             try{
     6                 wait();
     7             }catch (InterruptedException e){
     8                 System.out.println(Thread.currentThread().getName()+"....."+e);
     9                 flag = false;
    10             }            
    11             System.out.println(Thread.currentThread().getName()+"......++++");
    12         }
    13     }
    14     public void setFlag(){
    15         flag = false;
    16     }
    17 }
    18 
    19 class StopThreadDemo{
    20     public static void main(String[] args){
    21         StopThread st = new StopThread();
    22        Thread t1 = new Thread(st);
    23        Thread t2 = new Thread(st);
    24        t1.start();
    25        t2.setDaemon(true);//将该线程标记为守护线程(后台线程), 该方法必须在启动线程前调用。
    26        t2.start();
    27 
    28         int num = 1;
    29         for(;;){
    30             if(++num==50){
    31                 t1.interrupt();
    32                 break;
    33             }
    34             System.out.println("main...."+num);
    35         }
    36         System.out.println("over");
    37     }
    38 }

      (四)守护线程(可理解为:后台线程)

      void setDaemon(boolean on) :将该线程标记为守护线程或用户线程,可以理解为后台进程。

      守护线程的特点:

      1、该方法必须在启动线程前调用。

      2、当正在运行的线程都是守护线程时,Java 虚拟机退出。

      3、当所有的前台线程执行结束,后台线程无论处于什么状态,都自动结束。

      (五)线程类的其他方法

      1、setPriority(int newPriority)方法(用来设置线程的优先级)

      (1)参数newPriority的取值有:MAX_PRIORITY(最高优先级10)、MIN_PRIORITY(最低优先级)、NORM_PRIORITY(默认优先级5)

      (2)格式:线程.setPriority(Thread. MAX_PRIORITY)

      2、join()方法

      当A线程执行到了b线程的.join()方法时,A线程就会等待,等B线程都执行完,A线程才会执行。(此时B和其他线程交替运行。)join可以用来临时加入线程执行。

      3、yield()方法

      可以暂停当前线程,释放执行权,让其他线程执行。

      4、toString()方法

      返回该线程的字符串表示形式,包括线程名称、优先级和线程组。

    七、面试题

      (一)试题1:以下代码,如果错误 错误发生在哪一行?  

    1 class Test implements Runnable{
    2       public void run(Thread t)
    3       {
    4 
    5       }
    6 }

       分析:错误在第一行,应该被abstract修饰 

      (二)试题2:以下代码的运行结果是什么?深入分析。 

     1 class ThreadTest{
     2     public static void main(String[] args){
     3         new Thread(new Runnable(){
     4             public void run(){
     5                 System.out.println("runnable run");
     6             }
     7         })
     8         {
     9             public void run(){
    10                 System.out.println("subThread run");
    11             }
    12         }.start();
    13     }
    14 }

       运行结果:subThread run。

      分析:首先当然是以子类为主,所以先执行new Thread(){……..}子类内容。如果子类方法没有内容,再以任务为主,即运行new Runnable(){public void run(){………}}。如果任务都没有,以Thread自己的run方法为主。

     

    ---------- android培训java培训、期待与您交流! ----------

  • 相关阅读:
    洛谷P3811题解
    洛谷P3353在你窗外闪耀的星星-题解
    Map根据value来排序
    java8 groupby count
    Java反射
    maven profile环境切换
    获取nginx代理情况下的真实ip
    获取request里header的name和value
    git 删除iml文件
    java list 排序
  • 原文地址:https://www.cnblogs.com/jianxingjianyuan2014/p/4002723.html
Copyright © 2011-2022 走看看