黑马程序员_Java_多线程

8.多线程

8.1、多线程概述

  • 进程:是一个正在执行中的程序。每一个进程执行都有一个执行顺序。该顺序是一个执行路径,或者叫一个控制单元。
  • 线程(例:FlashGet):就是进程中一个独立的控制单元。线程在控制着进程的执行。一个进程中至少有一个线程。
    •   Java VM启动的时候,会有一个进程java.exe。该进程中至少一个线程负责Java程序的执行。而且这个线程运行的代码存在于main方法中。该线程称之为主线程。
    • 扩展:其实更细节说明jvm,jvm启动不止一个线程,还有负责垃圾回收机制的线程。
  • 多线程存在的意义
    •   一个采用了多线程技术的应用程序可以更好地利用系统资源。其主要优势在于充分利用了CPU的空闲时间片,可以用尽可能少的时间来对用户的要求做出响应,使得进程的整体运行效率得到较大提高,同时增强了应用程序的灵活性。更为重要的是,由于同一进程的所有线程是共享同一内存,所以不需要特殊的数据传送机制,不需要建立共享存储区或共享文件,从而使得不同任务之间的协调操作与运行、数据的交互、资源的分配等问题更加易于解决。

8.2、创建线程一(继承Thread类)

  • 线程的创建方式(如何在自定义的代码中,自定义一个线程呢?)
    •   通过对api的查找,Java已经提供了对线程这类事物的描述,就是Thread类。
    • 创建新执行线程有两种方法。
      •    方法一:一种方法是将类声明为 Thread 的子类。该子类应重写 Thread 类的 run 方法。并调用start方法。//该方法两个作用:启动线程,调用run方法。建立好一个对象就是创建好一个线程。

start:使该线程开始执行;Java 虚拟机调用该线程的 run 方法。

发现运行结果每一次都不同,因为多个线程都获取cpu的执行权。CPU执行到谁,谁就执行。明确一点,在某一时刻只能有一个程序在运行。(多核除外)

CPU在做着快速的切换,以达到看上去是同时运行的效果。我们可以形象地把多线程的运行行为在互相抢夺CPU的执行权。

为什么要覆盖run方法呢?

Thread类用于描述线程。该类就定义了一个功能,用于存储线程要执行的代码,该储存功能就是run方法。(创建线程的目的就是为了让线程去执行一些代码,那么线程在描述过程中需要定义代码所存放的位置,这个存储空间就是run方法,也就是说Thread类中的run方法,用于存储线程要运行的代码。)

  • 多线程的特性

多线程的一个特性:随机性。谁抢到谁执行,至于执行多长,CPU说了算。

8.3、线程的运行状态

1、被创建

2、临时状态(阻塞):线程被start以后,不一定运行。当开启了多个线程时,CPU在某一时刻只能执行一个线程,在等待CPU的执行权。(具备执行资格,但没有执行权。)

3、运行(既有资格又有执行权):创建后通过调用start方法运行。

4、冻结(放弃了执行资格):一、在运行状态下通过sleep(time)方法进行冻结。当自定义的time过后,线程复活了,从冻结状态转为临时状态,即具备执行资格,当争取到执行权时便运行。

二、在运行状态下通过wait()方法将线程进行冻结,通过notify()方法将线程复活,,从冻结状态转为临时状态,即具备执行资格,当争取到执行权时便运行。

三、线程在冻结状态时进程并没有结束。

5、消亡:通过stop()方法将线程结束,run方法结束。

8.4、获取线程对象以及名称

1、线程都有自己默认的名称:Thread-编号  该编号从0开始。

2、调用getName() 方法获取。

3、调用setName()方法改变线程名称。但通常用父类的构造方法进行自定义名称。(即子类的构造方法调用父类已经定义好的构造方法,super(name);)

4、currentThread():返回对当前正在执行的线程对象的引用。

5、每一个线程的创建都有自己独立的运行空间。

8.5、创建线程二(实现Runnable接口)最为常用

通过售票的例子,继承Thread类,创建多个Thread类子类对象是不行的,因为ticket数据不共享。可以通过static进行共享,但一般不建议这么做,因为生命周期太长。接着创建一个对象进行多次start,出现了无效线程状态异常,只有Thread-0在跑。因为线程已经从创建状态到运行状态,再次开启start是没有意义的。所以,该怎么解决这个问题呢?第一种创建方式不行了。

方法二:创建线程的另一种方法是声明实现 Runnable 接口的类。该类然后实现 run 方法。然后可以分配该类的实例,在创建 Thread 时作为一个参数来传递并启动。(此时ticket被共享了)

步骤:

1、定义类实现Runnable接口。

2、覆盖Runnable接口中的run方法。(将线程要运行的代码存放在该run方法中)

3、通过Thread类建立线程对象。

4、将Runnable的子类对象作为实际参数传递给Thread类的构造函数。

为什么要将Runnable接口的子类对象传递给Thread类的构造函数呢?因为自定义的run方法所属的对象是Runnable接口的子类对象。所以要线程去执行制定对象的run方法。就必须明确该run方法所属的对象。

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

实现方式和继承方式有什么区别呢?

实现方式好处:避免了单继承的局限性。

在定义线程时,建议使用实现方式。

两种方式区别:

继续Thread:线程代码存放在Thread子类的run方法中。

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

8.6、多线程的安全问题

sleep(long millils):在指定的毫秒数内让当前正在执行的线程休眠(暂停执行),此操作受到系统计时器和调度程序精度和准确性的影响。

该方法抛出InterruptedException中断异常:如果任何线程中断了当前线程。当抛出该异常时,当前线程的中断状态 被清除。

通过分析发现,打印出0、-1、-2等错票。多线程的运行出现了安全问题。

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

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

8.7、多线程同步代码块

Java对于多线程的安全问题提供了专业的解决方式。就是同步代码块。synchronized(对象){需要被同步的代码;}

对象如同锁,持有锁的线程可以在同步中执行。没有持有锁的线程即使过去CPU的执行权,也进不去,因为没有获取锁。(火车上的卫生间——经典。)

同步的前提:

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

2,必须是多个线程使用同一个锁。(通常把资源对象作为锁。)

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

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

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

8.8、多线程同步函数

如何在代码中找问题?

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

2,明确共享数据。

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

把synchronized作为修饰符放在函数上,即为同步函数。

不能把run方法随便的加同步,因为要哪些代码需要被同步,而哪些不需要。把需要同步的代码用函数封装起来,再调用此方法即可。

同步函数用的是哪一个锁呢?

函数需要被对象调用,那么函数都有一个所属对象引用。就是this。所以同步函数使用的锁是this。

8.9、静态同步函数

如果同步函数被静态修饰后,使用的锁是什么呢?通过验证,发现不再是this,因为静态方法中不可以定义this。静态进内存时,内存中没有本类对象,但是一定有该类对应的字节码文件对象。

类名.class  该对象的类型是Class。

静态的同步方法,使用的锁是该方法所在类的字节码文件对象。

8.10、多线程-单力设计模式-懒汉式

懒汉式的特点在于实例的延迟加载。当多线程访问时会出现安全问题。可以加同步来解决。用同步函数会比较低效,用同步代码块和双重判断的形式会更加高效,解决效率的问题。加同步时使用的锁是该类所属字节码文件对象。

8.11、多线程-死锁

死锁:同步中嵌套同步。

发生死锁的原因一般是两个对象的锁相互等待造成的。

那么为什么会产生死锁呢?
1.因为系统资源不足。
2.进程运行推进的顺序不合适。    
3.资源分配不当。
             
学过操作系统的朋友都知道:产生死锁的条件有四个:
1.互斥条件:所谓互斥就是进程在某一时间内独占资源。
2.请求与保持条件:一个进程因请求资源而阻塞时,对已获得的资源保持不放。
3.不剥夺条件:进程已获得资源,在末使用完之前,不能强行剥夺。
4.循环等待条件:若干进程之间形成一种头尾相接的循环等待资源关系。

8.12、多线程间的通信问题

思考 1、wait(),notify(),notifyAll()用来操作线程为什么定义在了Object中?

  1. 这些方法存在于同步中。
  2. 使用这些方法时必须要标识所属的同步得到锁。
  3. 锁可以是任意对象,所以任意对象锁调用的方法一定定义Object类中。

思考2、wait(),sleep()有什么区别呢?

  1. wait():释放资源,释放锁。
  2. sleep():释放资源,不释放锁。

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

解决安全问题:

将run方法中操作同一资源的代码加上同步,并使用同一个锁。通常将资源对象作为锁。因为资源是唯一的。

等待唤醒机制:

使用原因:当输入线程跳出同步时,输入线程和输出线程都可能抢到CPU的执行权,所以输入线程有可能再次抢到执行权,再次进行输入信息,把前面的覆盖掉。当输出线程抢到执行权时也不可能只输出一个。这是CPU的切换造成的。

为了能输入一个就取出一个,在资源上加入一个标记(boolean flag )。当flag为false时,输入线程往资源里面存值,并将标记修改,反之,把值输出。但当输入线程再次抢到cup的执行权时,不能再存了。这时就让输入线程先等着,由于时间不确定,所以不能用sleep,这时就要用wait。所以当输出线程把值输出后,在输出线程冻结前将其他线程唤醒notify。同时注意要标识出wait、notify、notifyAll所属的锁。

生产者和消费者

在实际开发中,代码时由多个线程去运行的,多个线程负责生产多个线程负责消费。由于生产线程在生产前没有再次判断标记,所以会出现生产两个消费一个的情况。if只判断一次,while会判断多次,所以将if改为while。当生产线程被唤醒后,会再次判断标记,就不会出现再次生产的情况。但是线程全都锁死了,全都挂着(全都等待,非死锁)。原因是notify唤醒的时线程池中最早等待的那一个,不一定是对方线程。所以将notify改为notifyAll,将线程池中的线程全部唤醒。

注意:当出现了多个生产者和消费者时,必须要用while循环和notifyAll。而if和notify只能用于生产者一个消费者一个的情况。

笔记:对于多个生产者和消费者,为什么要定义while判断标记?

愿意:让被唤醒的线程再次判断标记。

为什么定义notifyAll?因为需要唤醒对方线程,因为只用notify,容易出现只唤醒对方线程的情况,导致程序中的所有线程都等待。

8.13、生产者和消费者jdk5.0升级版

我们希望只唤醒对方,不唤醒本方,该怎么做呢?Java提供了接口Lock。

Lock 实现提供了比使用 synchronized 方法和语句可获得的更广泛的锁定操作。此实现允许更灵活的结构,可以具有差别很大的属性,可以支持多个相关的 Condition 对象。

以前的加锁和解锁都是隐式的。用Lock后,代码中就为显式的。

ConditionObject 监视器方法(waitnotifynotifyAll)分解成截然不同的对象,以便通过将这些对象与任意 Lock 实现组合使用,为每个对象提供多个等待 set(wait-set)。其中,Lock 替代了 synchronized 方法和语句的使用,Condition 替代了 Object 监视器方法的使用。

  1. lock():加锁。
  2. unlock():解锁。
  3. await():替代了wait()。
  4. signal():替代了notify()、
  5. signalAll():替代了notifyAll()。

当线程拿到锁后,加锁的代码中可能导致程序跳转,下面的解锁代码执行不到,但是解锁的代码是必须执行的,所以用finally()。

Lock支持多个相关的Condition对象,意思是锁里面能创建多个Condition对象,Condition_con.wait()只能被Condition_con.signal或Condition_con.signalAll唤醒。这就能起到只唤醒对方的作用。指定唤醒等待中的线程。

JDK1.5中提供了多线程升级解决方案。将同步Synchronnized替换成现实Lock操作。将Object的wait,norify,notifyAll,替换了Condition对象。该对象可以通过Lock锁的newCondition()方法进行获取。在该实例中实现了只唤醒对方的操作。

8.14、停止线程

1、定义循环结束标记。因为线程运行代码一般都是循环,只要控制了循环即可。

2、使用interrupt(中断)方法。该方法是结束线程的冻结状态,使线程回到运行状态上来。

注意:stop方法已经过时不在使用。如何停止线程?只有一种方法就是,run方法结束。

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

特殊状态:当线程处于了冻结状态,就不会读取到标记,那么线程就不会结束。

当没有指定的方式让冻结的线程回复到运行状态是时,这时需要对冻结进行清除。强制让线程回复到运行状态上来。这样就可以操作标记让线程结束。Thread类提供该方法interrupt();

8.15、join方法

join():等待该线程终止。意思是调用该方法的线程在抢夺CPU的执行权。当A线程执行到B线程的join方法时,A就会等待,等B线程都执行完,A才会执行。

join可以用来临时加入线程执行。

8.16、优先级&yield方法

线程的优先级一共是有10级,默认是5。

setPriority():更改线程的优先级。

MAX_PRIORITY:线程可以具有的最高优先级。(10)

MIM_PRIORITY:线程可以具有的最低优先级。(5)

NORM_PRIORITY:分配给线程的默认优先级。(1)

yield():暂停当前正在执行的线程对象,并执行其他线程。强制放弃执行权。

上一篇:Python的Django框架完成一个完整的论坛(源码以及思路)


下一篇:框架-spring源码分析(一)