黑马程序员 java基础回顾---多线程

来源:互联网 发布:php数组按id排序 编辑:程序博客网 时间:2024/05/19 22:28

---------------------- ASP.Net+Android+IOS开发、 .Net培训、期待与您交流! ----------------------

一、概述:


1、线程是什么呢?

我们先来说一说比较熟悉的进程吧,之后就比较容易理解线程了。所谓进程,就是一个正在执行(进行)中的程序。每一个进程的执行都有一个执行顺序,或者说是一个控制单元。简单来说,就是你做一件事所要进行的一套流程。线程,就是进程中的一个独立的控制单元;也就是说,线程是爱控制着进程的执行。一个进程至少有一个线程,并且线程的出现使得程序要有效率。打个比方说,在仓库搬运货物,一个人搬运和五个人搬运效率是不一样的,搬运货物的整个程序,就是进程;每一个人搬运货物的过程,就是线程。

2、java中的线程:

在java中,JVM虚拟机启动时,会有一个进程为java.exe,该程序中至少有一个线程负责java程序的执行;而且该程序运行的代码存在于main方法中,该线程称之为主线程。其实,JVM启动时不止有一个线程(主线程),由于java是具有垃圾回收机制的,所以,在进程中,还有负责垃圾回收机制的线程。

3、多线程的意义:

透过上面的例子,可以看出,多线程有两方面的意义:

1)提高效率。   2)清除垃圾,解决内存不足的问题。

二、自定义线程:

线程有如此的好处,那要如何才能通过代码自定义一个线程呢?其实,线程是通过系统创建和分配的,java是不能独立创建线程的;但是,java是可以通过调用系统,来实现对进程的创建和分配的。java作为一种面向对象的编程语言,是可以将任何事物描述为对象,从而进行操作的,进程也不例外。我们通过查阅API文档,知道java提供了对线程这类事物的描述,即Thread类。创建新执行线程有两种方法:

一)创建线程方式一:继承Thread类。

1、步骤:

第一、定义类继承Thread。

第二、复写Thread类中的run方法。

第三、调用线程的start方法。分配并启动该子类的实例。

          start方法的作用:启动线程,并调用run方法。

2、运行特点:

A.并发性:我们看到的程序(或线程)并发执行,其实是一种假象。有一点需要明确:;在某一时刻,只有一个程序在运行(多核除外),此时cpu是在进行快速的切换,以达到看上去是同时运行的效果。由于切换时间是非常短的,所以我们可以认为是在并发进行。
B.随机性:在运行时,每次的结果不同。由于多个线程都在获取cpu的执行权,cpu执行到哪个线程,哪个线程就会执行。可以将多线程运行的行为形象的称为互相抢夺cpu的执行权。这就是多线程的特点,随机性。执行到哪个程序并不确定。


3、覆盖run方法的原因:
1)Thread类用于描述线程。该类定义了一个功能:用于存储线程要运行的代码,该存储功能即为run方法。也就是说,Thread类中的run方法用于存储线程要运行的代码,就如同main方法存放的代码一样。
2)复写run的目的:将自定义代码存储在run方法中,让线程运行要执行的代码。直接调用run,就是对象在调用方法。调用start(),开启线程并执行该线程的run方法。如果直接调用run方法,只是将线程创建了,但未运行。

示例:

class Demo extends Thread{public void run(){for(int x = 0;x<60;x++){System.out.println("demo run----"+x);}}}class  ThreadDemo{public static void main(String[] args) {Demo d = new Demo();d.start();for(int i = 0;i<60;i++){System.out.println("Hello World!"+i);}}}


二)创建线程方式二:实现Runnable接口

1、步骤:
第一、定义类实现Runnable接口。
第二、覆盖Runnable接口中的run方法。
第三、通过Thread类建立线程对象。要运行几个线程,就创建几个对象。
第四、将Runnable接口的子类对象作为参数传递给Thread类的构造函数。
第五、调用Thread类的start方法开启线程,并调用Runnable接口子类的run方法。

2、说明:

A.步骤2覆盖run方法:将线程要运行的代码存放在该run方法中。

B.步骤4:为何将Runnable接口的子类对象传给Thread构造函数。因为自定义的run方法所属对象为Runnable接口的子类对象,所以让线程指定对象的run方法,就必须明确该run方法所属的对象。

示例:

class Demo implements Runnable{public void run(){for(int x = 0;x<60;x++){System.out.println("demo run----"+x);}}}class  ThreadDemo{public static void main(String[] args) {Demo d = new Demo();Thread t = new Thread(d);t.start();for(int i = 0;i<60;i++){System.out.println("Hello World!"+i);}}}


三)实现方式与继承方式有何区别:

1、实现方式:避免了单继承的局限性。
             在定义线程时,建议使用实现方式。

2区别:

继承Thread:线程代码存放在Thread子类的run方法中。
实现Runnable:线程代码存在接口的子类run方法中。
需要注意的是:局部变量在每一个线程中都独有一份。

四)多线程的同步

多线程的运行出现了安全问题。

问题的原因:

    当多条语句在操作同一个线程共享数据时,一个线程对多条语句只执行了一部分,还没有执行完,另一个线程参与进来进行执行,导致共享数据的错误。

解决办法:

    对多条操作共享数据的语句,只能让一个线程都执行完,在执行的过程中,其他线程不可以参与执行。

同步的前提:

    1、必须有两个或者两个以上的线程;

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

同步的两种方法:

    1、同步代码块

格式:

synchronized(obj){...}

其中obj是一个对象,这个对象可以是任意的,在同步功能中充当一个锁。

    2、同步函数

格式:

public synchronized void Test(...){...}

注意:同步函数的锁是 this 如果是静态同步函数,那么锁是该静态函数所在类的Class对象

多线程同步的应用--单例设计模式(懒汉式)

class Single{private static Single s = null;private Single(){}public static Single getInstance(){if(s==null){synchronized(Single.class)//静态函数里面不能用this{if(s==null)s = new Single();}}return s;}}

注意:懒汉式加同步锁比较低效,因为线程较多时每个调用的都要判断锁,这个问题可以用两个if 判断语句来解决。

3、死锁

死锁产生的原因:同步中嵌套同步,两个同步代码中都在等待对方的锁造成的。

在编码过程中我们要尽量避免死锁。

五)线程间通信

    多线程间通信是线程之间进行交互的方式,简单说就是存储资源和获取资源。比如说仓库中的货物,有进货的,有出货的。还比如生产者和消费者的例子。这些都可以作为线程通信的实例。那么如何更好地实现通信呢?先看下面的代码:

class Rec{private String name;private String sex;public boolean flag;public void setName(String name){this.name = name;}public void setSex(String sex){this.sex = sex;}public String getName(){return this.name;}public String getSex(){return this.sex;}}class Input implements Runnable{private Rec r;public Input(Rec r){this.r = r;}public void run(){int i = 0;while(true){synchronized(r){if(i == 0){r.setName("mark");r.setSex("male");}else{r.setName("丽丽");r.setSex("女");}i = (i+1)%2;}}}}class Output implements Runnable{private Rec r;public Output(Rec r){this.r = r;}public void run(){while(true){synchronized(r){System.out.println(r.getName()+"..."+r.getSex());}}}}class InputOutputDemo {public static void main(String[] args) {Rec r = new Rec();Thread input = new Thread(new Input(r));Thread output = new Thread(new Output(r));input.start();output.start();}}

在编写过程中遇到了一些问题:打印出了一些名字和性别不相符的记录;

原因:取数据和存数据两个线程同步没有用相同的锁;

解决办法:在主函数中传入相同的锁 r 。

等待唤醒机制

1、显式锁机制和等待唤醒机制:

在JDK 1.5中,提供了改进synchronized的升级解决方案。将同步synchronized替换为显式的Lock操作,将Object中的wait,notify,notifyAll替换成Condition对象,该对象可对Lock锁进行获取。这就实现了本方唤醒对方的操作。在这里说明几点:

1)、对于wait,notify和notifyAll这些方法都是用在同步中,也就是等待唤醒机制,这是因为要对持有监视器(锁)的线程操作。所以要使用在同步中,因为只有同步才具有锁。

2)、而这些方法都定义在Object中,是因为这些方法操作同步中的线程时,都必须表示自己所操作的线程的锁,就是说,等待和唤醒的必须是同一把锁。不可对不同锁中的线程进行唤醒。所以这就使得程序是不良的,因此,通过对锁机制的改良,使得程序得到优化。

3)、等待唤醒机制中,等待的线程处于冻结状态,是被放在线程池中,线程池中的线程已经放弃了执行资格,需要被唤醒后,才有被执行的资格。

代码示例:

import java.util.concurrent.locks.*;    class ProducerConsumerDemo{      public static void main(String[] args){          Resouse r = new Resouse();          Producer p = new Producer(r);          Consumer c = new Consumer(r);          Thread t1 = new Thread(p);          Thread t2 = new Thread(c);          Thread t3 = new Thread(p);          Thread t4 = new Thread(c);          t1.start();          t2.start();          t3.start();          t4.start();      }  }    class Resouse{      private String name;      private int count = 1;      private boolean flag =  false;       private Lock lock = new ReentrantLock();      private Condition condition_P = lock.newCondition();      private Condition condition_C = lock.newCondition();  //要唤醒全部,否则都可能处于冻结状态,那么程序就会停止。这和死锁有区别的。      public void set(String name)throws InterruptedException{          lock.lock();          try{              while(flag)//循环判断,防止都冻结状态                  condition_P.await();              this.name = name + "--" + count++;              System.out.println(Thread.currentThread().getName() + "..生成者--" + this.name);              flag = true;              condition_C.signal();          }finally{              lock.unlock();//释放锁的机制一定要执行          }             }      public void out()throws InterruptedException{          lock.lock();          try{              while(!flag)//循环判断,防止都冻结状态                  condition_C.await();              System.out.println(Thread.currentThread().getName() + "..消费者." + this.name);              flag = false;              condition_P.signal();//唤醒全部          }finally{              lock.unlock();          }      }  }    class Producer implements Runnable{      private Resouse r;      Producer(Resouse r){          this.r = r;      }      public void run(){          while(true){              try{                  r.set("--商品--");              }catch (InterruptedException e){}          }      }  }    class Consumer implements Runnable{      private Resouse r;      Consumer(Resouse r){          this.r = r;      }      public void run(){          while(true){              try{                  r.out();              }catch (InterruptedException e){}          }      }  }

2、对于上面的程序,有两点要说明:

1)、为何定义while判断标记:

原因是让被唤醒的线程再判断一次。

避免未经判断,线程不知是否应该执行,就执行本方的上一个已经执行的语句。如果用if,消费者在等着,两个生成着一起判断完flag后,cpu切换到其中一个如t1,另一个t3在wait,当t1唤醒冻结中的一个,是t3(因为它先被冻结的,就会先被唤醒),所以t3未经判断,又生产了一个。而没消费。

2)这里使用的是signal方法,而不是signalAll方法。是因为通过Condition的两个对象,分别唤醒对方,这就体现了Lock锁机制的灵活性。可以通过Contidition对象调用Lock接口中的方法,就可以保证多线程间通信的流畅性了。

小结:

       多线程编程在实际应用中十分广泛,因为一个系统不可能每次都只为允许一个用户使用,这样的效率太低;一个好的系统必须允许很多人同时访问,这样就需要使用到多线程,但是多线程又具有安全隐患,当多个人同时访问系统中的数据时会造成数据错乱,产生“脏数据”,这时就需要用到线程同步技术,当把某些代码设置成同步后,每次就只能有一个线程对其进行访问了。所以我们需要掌握线程同步的方法,这里介绍的三种线程同步的方法在开发过程中各有用途,都必须掌握。

---------------------- ASP.Net+Android+IOS开发、 .Net培训、期待与您交流! ----------------------

原创粉丝点击