黑马程序员ava学习笔记——多线程

来源:互联网 发布:新浪云平台数据库 编辑:程序博客网 时间:2024/06/11 02:38
------- android培训java培训、期待与您交流! ----------

多线程

    概念

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

    线程:就是进程中一个独立的控制到元,线程在操控着进程的的执行,一个进程中至少有一个线程。 

    多线程:一个程序里边有多条执行路径,就是说由程序是由多个线程组成的,多线程虽然降低了程序运行的效率,但一个程序中线程越多,获取到cpu执行权的概率就越大。

    创建线程的方式

    创建线程的方式有两种,一种是继承Thread类,另一种是实现Runnable接口。

    继承Thread类

    步骤:1,定义一个类继承Thread;

          2,复写Thread类中的run( )方法;

          3,调用线程的start( )方法,开启线程,并调用执行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();//开启线程并执行该线程的run方法。//d.run();仅仅是对象调用方法。而线程创建了,并没有运行。for(int x=0; x<60; x++)System.out.println("Hello World!--"+x);}}

    

    发现运行结果每一次都不同,因为多个线程都在获取cpu的执行权,cpu执行到谁,谁就运行,需要明确的是,在某一时刻,只有一个程序在运行(多核除外),cpu在做着快速的切换,,以达到看上去是同时在运行的结果,我们可以形象的把多线程的运行形容为,多个线程在抢夺cpu的执行权,这就是多线程的一个特性,随机性。谁抢到谁执行,至于执行多长时间,cpu说了算。

    多线程run方法和start方法的区别

    Thread类用于描述线程,该类定义了一个能用于储存线程运行代码的方法,这个方法就是run方法,也就是说Thread类中的run方法用于存储线程要运行的代码,主线程要运行的代码放在main方法中,虚拟机定义的。我们复写run方法的目的就是将自定义的代码放在run方法中,让线程运行。

    d.start():开启线程并执行该线程的run方法;

    d.run():仅仅是对象调用方法,线程创建了却并没有运行。

    实现Runnable接口

    步骤:1,定义一个类实现Rannable接口;

          2,覆盖Rannable接口中的run( )方法;

          3,通过Thread类创建线程对象;

          4,将Rannable接口的子类对象作为实际参数传递给Thread类的构造函数;

          5,调用Thread类的start方法开启线程,调用Runnable接口子类的run方法 。

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

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

    实现方式和继承方式的区别:我们用一个售票的例子来进行说明,用继承Thread类的方法创建线程的话,需要建立四个线程对象,运行四次,等于是卖了400张票,我们可以想到用静态,但是静态的生命周期太长,所以我们可以选择实现Runnable接口的方法来完成,代码如下:

/*需求:简单的卖票程序。多个窗口同时买票。*/class Ticket implements Runnable//extends Thread{private  int tick = 100;public void run(){while(true){if(tick>0){System.out.println(Thread.currentThread().getName()+"....sale : "+ tick--);}}}}class  TicketDemo{public static void main(String[] args) {Ticket t = new Ticket();//只需要建立一个对象,然后将这个对象传递给Thread类的构造函数;Thread t1 = new Thread(t);//创建了一个线程;Thread t2 = new Thread(t);//创建了一个线程;Thread t3 = new Thread(t);//创建了一个线程;Thread t4 = new Thread(t);//创建了一个线程;t1.start();t2.start();t3.start();t4.start();}}


    运行结果是:

      

    实现方式和继承方式的区别

    实现方式好处:避免了单继承的局限性,在定义线程时,建立使用实现方式。
    两种方式区别:继承Thread:线程代码存放Thread子类run方法中。
    实现Runnable,线程代码存在接口的子类的run方法。

    

    线程运行状态

        

    如图所示,线程存在五种状态,分别是创建,运行,临时,冻结和消亡状态。

    获取线程对象及名称

    currentThread:这是一个静态方法,用于获取当前对象;

    getName:获取线程的名称;

    setName:设置线程的名称。

    

    多线程的安全问题

    增加窗口后的运行结果:


        


    通过上边的买票小程序,我们发现当买票的窗口增加后,可能会出现打印0,-1,-2这些错票的情况,为了验证,我们可以让程序在打票之前,用sleep方法让进入if语句的线程停一段时间,我们队if语句中的代码作如下改动,如图:

       

    分析一下出现这种问题的原因:在if语句中有可能,四个线程进来后,都停在sleep这条语句这里,我们假设现在tick=1,某一个线程获取执行权后,向下执行输出语句,执行完tick--,tick=0,这是,还在if语句中的其他线程已经判断过条件了,所以即使tick的值已经不满足if的条件表达式,这些线程还是会向下执行输出语句,所以就会出现打印出错票的情况,线程越多,有可能打印出的错票就越多。

    得出一个结论:当多条语句在执行同一个线程共享数据时,一个线程对象对于语句只执行了一部分,没有执行完,另一个线程参与进来执行,导致共享数据的错误。

    解决办法:对于多条操作共享数据的语句,只能让一个线程都执行完后在执行其他线程,执行过程中,其他线程不能参与执行。

    java对多线程安全问题提供了专门的解决方式:同步代码块和同步函数。

    同步代码块

    synchronized(对象)  //这个对象可以是任意对象;

   {

        要被同步的代码;

    }

    那些代码需要被同步,就看那些代码在操作共享数据。

    对于售票这个例子,加上同步代码快后的代码如下,就只有run方法的代码有变化,其他地方都不变,在这里为了说明问题,我只写run方法中的代码:

public void run(){while(true){synchronized(obj){if(tick>0){try{Thread.sleep(10);}catch(Exception e){}//让进来的线程在这里停一会;System.out.println(Thread.currentThread().getName()+"....sale : "+ tick--);}}}}

    

    对象如同锁,持有锁的线程可以在同步中执行,没有持有锁的线程,及时获取了cpu的执行权,也进不去,因为没有获取锁。

    同步的前提:1,必须要有两个或者两个以上的线程;

                2,必须是多个线程使用同一个锁;

    必须保证同步中只有一个线程在运行。

    同步的好处与弊端

    好处:解决了多线程的安全问题。

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

    同步函数

    同步函数:就是在函数中添加了synchronized修饰符的函数。

    如何找多线程中的安全问题?

     1,明确哪些代码是多线程运行代码;

     2,明确共享数据;

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

    同步代码快的锁是任意对象,那么同步函数的锁又是什么呢?

    对于同步函数的锁,我们可以去验证一下,创建两个线程,一个线程执行同步代码快中的代码,另一个线程执行同步函数中的代码,这个可以通过定义标记来实现,验证的原理就是同步的前提:必须是多个线程使用同一个锁;通过运行结果,我们发现,当同步代码快中的对象是Object类的对象时,还是会发生安全问题,这说明两个锁不一样,换成this后,安全问题就解决了,说明同步函数的锁是this。

    如果同步函数被静态修饰后,使用的锁是什么呢?

    我们也可以用上面的方法进行验证,发现不再是this,因为静态方法中也不可以定义this;静态进内存时,内存中没有本类对象,但是一定有该类对应的字节码文件对象,类名.class  该对象的类型是Class
    静态的同步方法,使用的锁是该方法所在类的字节码文件对象。 类名.class

    单例设计模式中的懒汉式在多线程,处理安全问题的用的就是用的静态同步函数。

    死锁

    同步中嵌套同步,而锁却不同。

/*死锁*/class Dead implements Runnable{private boolean flag;Dead(boolean flag){this.flag = flag;}public void run(){if(flag){synchronized(MyLock.locka)//a锁{System.out.println("if locka");synchronized(MyLock.lockb)//b锁{System.out.println("if lockb");}}}else{synchronized(MyLock.lockb)//b锁{System.out.println("if lockb");synchronized(MyLock.locka)//a锁{System.out.println("if locka");}}}}}class MyLock{static Object locka = new Object();static Object lockb = new Object();}class DeadLockDemo {public static void main(String[] args){Thread t1 = new Thread(new Dead(true));Thread t2 = new Thread(new Dead(false));t1.start();t2.start();}}


出现这种结果:


        

    死锁就相当于吃饭时用筷子,筷子只有一双,两个人每人手里有一根,这时,两人一人一根筷子,但是两人都不愿意让别人先吃,不愿意将筷子送给对方,于是两人都吃不了饭,死锁也是一样,两个线程都想获取对方的锁,如果都这么僵持着,就会出现死锁,死锁就是在这样的情况下造成的。
     

    线程间通信

    线程间通信其实就是多个线程在操作同一个资源,但操作的动作不同。

    我们用一个小程序来提现什么是线程间通信:这个小程序我们只创建两个线程,已解决过安全问题,而且得到的结果是存一个打一个。

/*线程间通信这个小程序,创建了两个线程,一个线程负责设置名字,另一个线程负责打印名字。*/class Resource{private String name,sex;private boolean flag;//定义一个标记;public synchronized void setName(String name,String sex){if(flag)//如果这个标记为真try{this.wait();}catch(Exception e){}//让线程释放执行权;this.name = name;this.sex = sex;flag = true;//传完值后,把标记改为真;this.notify();//唤醒后,再回去判断标记久为真,那么,就会wait;}public synchronized void printName(){if(!flag)//如果这个标记为假try{this.wait();}catch(Exception e){}//让线程释放执行权;System.out.println(name+"............"+sex);//如果为假,就打印flag = false;//打印完,把标记改为假;this.notify();//唤醒另一个线程后,执行完再回去判断标记if中的条件表达式为真,就会wait;}}class Input implements Runnable{private Resource res;Input(Resource res){this.res = res;}public void run(){int x = 0;while(true){if(x==0)res.setName("BaiYun","M M");elseres.setName("黑土","G G");x = (x+1)%2;}}}class Output implements Runnable{private Resource res;Output(Resource res){this.res = res;}public void run(){while(true){res.printName();}}}class InputOutputDemo{public static void main(String[] args){Resource res = new Resource();new Thread(new Input(res)).start();//开启并执行线程new Thread(new Output(res)).start();}}


结果是:


    

  

    在操作同一种资源时,对于不同的处理方式,可以用多线程来完成,为了让两个线程交替执行,可以定义一个标记,在一个线程执行完后,让该线程进入等待状态,同时,唤醒其他在等待中的线程,这就是我理解的等待唤醒机制。

    等待中的线程临时存放在线程池中,当我们notify的时候,唤醒的是线程池中的线程,如果有多个线程在线程池中,通常先唤醒第一个在等待的,如果要唤醒很多,可以用notifyAll()方法。

    wait和notify方法,是从Object中继承的方法,全都用在同步中,这时必须要指出wait所操作的那个线程所属的锁,wait方法会抛出异常。

    为什么这些操作线程的方法都定义在Object类中?

    因为这些方法在操作同步中线程时,都必须要标识他们所操作线程持有的那个锁,只有同一个锁上的被等待县城可以被一个锁上的notify唤醒,也就是说,等待和唤醒必须是同一个锁,而锁可以是任意对象,所以可以被任意对象所调用的方法定义在Object类中。

    生产者消费者

    对于多个生产者,消费者,在多线程运行时,出现的问题,我们通过下面的代码进行说明,网页上的代码写太多注释,有时候会出问题,所以,我把分析写在后边。

class Resource{private String name;private int count = 1;private boolean flag;public synchronized void set(String name){if(flag)//这里用的是if语句来判断标记;try{wait();}catch(Exception e){}//t1,t2this.name = name+"-----"+count++;System.out.println(Thread.currentThread().getName()+"....生产者......"+this.name);flag = true;this.notify();//这里只唤醒了一个线程;}public synchronized void out(){if(!flag)try{wait();}catch(Exception e){}//t3,t4System.out.println(Thread.currentThread().getName()+"..........消费者.........."+this.name);flag = false;this.notify();}}class Pro implements Runnable{private Resource res;Pro(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();}}}class ProCon{public static void main(String[] args){Resource res = new Resource();Pro pro = new Pro(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();}}


运行后出现了这样的问题:


    
    

    出现了生产一个商品却被消费了两次的情况,当然,也有可能出现生产两次,消费一次的情况,这是为什么呢?

    分析:t1、t2是生产者线程,t3、t4是消费者线程,我们假设t1先获取到执行权,这时标记为false,t1向下执行,生产一个商品,然后标记被设置为true,回来再读标记,t1进入等待状态,释放了执行权;现在t2、t3、t4都有可能获取到执行权,假设t2获取到了执行权,标记为true,它也进入等待状态,释放了执行权;接下来t3、t4中有一个获取到执行权,执行后消费了一次,标记被设置为假,t3进入等待状态,释放执行权;然后t3唤醒了t1,t1生产一个商品后,并没有去唤醒消费者线程,把t2唤醒了,这样,虽然标记已经被t1设置为true,t2它已经判断过标记,被唤醒后直接向下执行,所以又会生产一个商品,于是就出现了生产两次,消费一次的情况,原因就是t1在执行完后,唤醒了本方的线程。

    对于这种问题我们应该怎么处理呢?

    发生这个现象就是因为没有循环判断标记,所以,可以用while循环去循环判断标记,但是,新的问题出现了,while循环判断标记可能会造成所有线程都陷入等待状态,如图:

     

    那么,我们就需要在线程释放执行权之前,将其他线程都唤醒,可以使用notifyAll()方法,这样问题就解决了。

    然而,在开发中这么做是比较麻烦的,所以我们就想:能不能每次唤醒只唤醒对方的线程呢?答案是可以,java在JDK1.5之后给我们提供了 一些新特性,在java.util.concurrent.locks包中,有Condition和Lock接口,还有ReenTrantLock类给我们提供了方法,Lock替代了synchronized方法和语句的使用,Condition替代了 Object监视器方法的使用。接下来,我们就来使用这些新特性,将生产者消费者的代码重新体现。

import java.util.concurrent.locks.*;//包就不一个一个导了class Res{private String name;private int count;boolean flag;private Lock lock = new ReentrantLock();//ReentrantLock是非抽象的,要建立它的对象,调用这些方法。private Condition condition_pro = lock.newCondition();//获取condition对象,一个锁上可以有多个相关的condition;private Condition condition_con = lock.newCondition();//获取condition对象public void set(String name)throws InterruptedException{lock.lock();//synchronized同步换成了两个方法,拿锁,释放锁;try{while(flag)condition_pro.await();//wait换成了await方法this.name = name+"--"+count++;System.out.println(Thread.currentThread().getName()+"......生产者......"+this.name);flag = true;condition_con.signal();//notify换成了signal方法;}finally{lock.unlock();//释放锁的动作一定要做;这里没有catch处理,下边是一样的;}}public void out()throws InterruptedException{lock.lock();try{while(!flag)condition_con.await();System.out.println(Thread.currentThread().getName()+"............消费者..........."+this.name);flag = false;condition_pro.signal();}finally{lock.unlock();}}}class Producer implements Runnable{private Res r;Producer(Res r){this.r = r;}public void run(){while(true)try{r.set("+商品+");//run方法里只能try}catch (InterruptedException e){e.printStackTrace();}}}class Consumer implements Runnable{private Res r;Consumer(Res r){this.r = r;}public void run(){while(true)try{r.out();}catch (InterruptedException e){e.printStackTrace();}}}class ProConDemo{public static void main(String[] args){Res r = new Res();Producer pro = new Producer(r);Consumer con = new Consumer(r);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();}}


运行后的结果:


    


    在jdk1.5版本后提供了显示的锁机制,以及显示的锁对象上的等待唤醒操作机制。
   

    Lock:替代了Synchronized。
    lock():上锁;

    unlock():释放锁;

    newCondition():获取Condition对象,一个锁可以对应多个Condition,而以前一个wait只能对应一个notify;
   

    Condition:替代了Object中的wait、notify、notifyAll。
    await():相当于wait;

    signal():相当于notify;

    signalAll():相当于notifyAll。

    

    停止线程

    因为stop方法已过时,那么我们想要停止线程要通过什么方法呢?

    只有一种,那就是等run方法结束,开启线程运行,运行代码通常都是循环结构,只要控制住循环,就可以让run方法结束,也就是线程结束。

    步骤:1,定义循环标记;

          2,使用interrupt中断方法,该方法能结束现成的冻结状态,使线程回到运行状态。

    特殊情况:当线程处于冻结状态,就不会读取到标记,那么线程就不会结束,这是,当没有指定的方式让冻结的线程恢复到运行状态是,需要对冻结状态进行清除,强制让线程恢复到运行状态中来,这样就可以让线程结束,Thread类提供了该方法:interrupt方法,中断线程。

class StopThread implements Runnable{private boolean flag =true;public  synchronized void run(){while(flag){try{wait();}catch (InterruptedException e){System.out.println(Thread.currentThread().getName()+"..........Exception");flag = false;//只要捕捉到异常,说明就有人在强制清除冻结状态;}System.out.println(Thread.currentThread().getName()+"....run");}}public void changeFlag()//这个方法就不再需要了;{flag = false;}}class  StopThreadDemo{public static void main(String[] args) {StopThread st = new StopThread();Thread t1 = new Thread(st);Thread t2 = new Thread(st);t1.start();t2.start();int num = 0;while(true){if(num++ == 60){//st.changeFlag();t1.interrupt();t2.interrupt();break;}System.out.println(Thread.currentThread().getName()+"......."+num);}System.out.println("over");}}


得到的结果是:


    

    注意:interrupt方法只是将冻结状态线程唤醒,如果没有冻结状态的线程,这个方法是没作用的。    

    

    守护线程 

    特点:1,当正在运行的线程都是守护线程时,java虚拟机退出;

          2,该方法必须在县城启动前调用 。

    setDemon(boolean):如果参数是true,则将该线程设置为守护线程。

    当所有的前台线程都结束,后台线程会自动结束,后台依赖于前台,开启运行都和前台线程没区别,结束时有区别。

    应用:比如有一个县城依赖于另一个线程,另外一个线程的数据如果不在运算了,这个线程存在是没意义的,输入线程不输入了,输出线程就不用输出了。

    

    join方法

    等待该线程终止。

    特点:当A线程执行到了B线程的join方法时,那么A线程就会等待,等B线程都执行完,A线程才会执行,join可以用来临时加入线程执行。


    优先级代表抢资源的频率,所有线程的默认优先级是5,包括主线程。

    setPriority(int num) :设置优先级。


    yeild方法:暂停当前正在执行的线程对象,并执行其他线程,就是临时释放一下执行权,稍微减缓一下线程执行的频率。

    










0 0
原创粉丝点击