Thread

 

 

1、进程与线程

         即是一个正在执行中的程序;

         进程可以同时开启;在某一个时刻,cpu只能执行某一个程序,只是cpu在快速切换;所以才感觉,多个程序在同时进行;一个进程当中有可能会出现多条执行路径;

         线程是进程中的内容;每一个应用程序当中都至少有一个线程,因为线程是程序当中的控制单元或者执行路径;

         总结:

         每一个进程执行都有一个执行顺序,该顺序是一个执行路径;或者叫一个控制单元

         进程就是用来定义和标示内存里的空间,分配内存里的地址,用来封装里面的一些控制单元;而线程才是进程中的一个独立的控制单元,在控制着进程的执行;一个进程中至少有一个线程;多个线程就是多个独立的控制单元在执行多块代码;彼此互相没有关系,除了共用cpu外,相互独立执行;

         应用程序在执行时,代码要先被加载进入内存,才能被执行,才会有很多运算,而负责代码执行的就是线程;就是进程中的一个线程;

         注意:进程结束,内存分配空间就没有了,这时候肯定就没有线程了,进程结束就是把程序关闭了。

         Java的jvm启动时会有一个进程java.exe,该进程中至少有一个线程在负责java程序的执行,而且这个线程运行的代码存在于main方法中;该线程称之为主线程;

        

         扩展:其实更细节说,jvm在启动时,就是一个多线程的进程;jvm启动不只一个线程;比如还有垃圾回收的线程;

         有多条执行路径的程序,我们称之为多线程执行程序;

 

2、多线程的意义:

         可以让程序中的部分产生同时出现的效果,还可以提高一些效率,

         重点在于可以使多个代码部分同时执行;产生多条路径,使程序更加优化;

         (注意是因为可以多代码同时进行,才会有提高效率的应用,而不是多线程的目的是为了提高效率,别把主末倒置了;)

 

3、线程的创建方式:

         创建一些线程去控制某些代码;自定义的去创建一些线程,让某些代码能够同时进行;

 

         如何在自定义的代码中自定义一个控制单元或者自定义一个线程?

 

         线程已经被jvm封装为对象了,其实这些线程真正的不是我们创建的,而是存在于某一个进程当中的,而这个进程是由系统帮助创建的,所以进程中的线程也应该由系统完成的,所以jvm只需要调用系统中的内容就能完成创建线程了,所以jvm应该已经帮我提供好了对这个事物的对象体现了;

java. lang包 是核心包;

 

通过对api的查找,java已经提供了对线程这类事物的描述,即Thread类;

 

创建新执行线程有两种方法。一种方法是将类声明为 Thread 的子类。该子类应重写 Thread 类的 run 方法。接下来可以分配并启动该子类的实例。

 

建立好了一个继承Thread的子类的对象,就是建立好了一个线程;

 

步骤:

(1)、定义类继承Thread;

(2)、重写Thread类中的run方法;

(目的:将自定义代码存储在run方法中,让线程运行)

(3)、调用线程的start方法,该方法两个作用,启动线程,调用run方法;

start()方法是Thread类的一个启动线程并去执行run()的方法;这种做法就叫做,沿袭父类的功能,重写父类的内容;start()方法调用的时候,运行的是父类的方法,而调用的是子类重写的内容;)

 

注意:只有Thread类的start方法才是开启一个新线程的方法;run方法没有任何的开启线程的作用;当然,start开启方法,就需要Thread的子类去创建一个线程的实例才有线程可以开启;start()只有开启已建新线程的功能,不能创建新线程,新线程要靠创建实例去创造;所以调用run(),而没有调用start()的区别是:一个没有开启新线程,一个开启新线程了;并非是一个多了一个线程,一个没多线程,线程是一样的,只是一个没有开启而已;不然就无法调用了;要有对象才能调用的;

 

         windows是多任务操作系统,在任何一个时刻,cpu是不可能同时执行两个线程的;只是因为cpu切换非常块,所以才以为是多任务的;每一个进程的多个线程,不同进程中的多个线程都在被cpu不停的切换着执行;

         cpu是不能控制的,可以学习多核编程去了解;多核之后,内存就是瓶颈;

 

         发现,建立新的线程之后,每次运行结果都不同;因为多个线程都获取cpu的执行权,cpu执行到谁,谁就运行;,明确一点,在某一个时刻,cpu只能有一个程序在运行,(多核除外)cpu在做着快速的切换,以达到看上去是同时运行的效果;

我们可以形象的把多线程的运行行为在互相抢夺cpu的执行权,这就是多线程的一个特性,随机性,谁抢到,谁执行,至于执行多长,cpu说了算;

         cpu一直在为了优化资源,就不停在切换,不可能会把某一个线程执行完了再执行另一个,除非有控制;cpu一直在若干可执行的线程间切换类切换去;这是多线程出现问题的根本原因;极有可能就是执行半句话后,就执行别的线程去了;

 

注意一点:

         创建的新线程是执行的是Thread子类的run方法里的代码;        

         main线程执行的是main方法里的代码;

 

         为什么要覆盖run方法呢?

         Thread类用于描述线程;该类就定义了一个功能,用于存储线程要运行的代码,该存储功能就是run方法;也就是Thread类中的run方法,用于存储线程要运行的代码?

         主线程里的代码就是存放在main方法;自定义线程就定义在run方法当中了。

         见上面的创建线程步骤的红色字体;

 

 

4、线程的几种状态;

         Thread类是描述事物的;这个类的方法肯定也创建对象了,即在调用底层创建线程,java是不会创建线程的,都是调用windows创建的线程方法;

         new方法创建新城,start激活线程,此时线程并没有被cpu执行或者运行;即进入了可运行的状态,当线程被激活后,就一直出在可运行状态(冻结)和正在运行状态之间切换,人工无法控制;即不可控的,即随时可能会处于可运行状态,也随时会正在运行;主要看cpu在执行什么,看什么时候赋予运行权,一般情况下,线程被激活后首先都是进入可运行状态,然后等待调配进行运行;

        

         临时状态,阻塞状态;可运行状态,具备运行资格;

         线程结束的方法:run方法结束;stop语句;

         线程可调控的状态:sleep();wait();此时线程没有资格获得运行,即不可运行状态或者说冻结状态;

 

5、如何取得线程的名称:

要想找线程的名称,首先肯定是要找线程对象的方法,因为线程的名称应该定义在线程事物里面,只有这个对象最熟悉,直接找线程对象就可以了;

 

线程都有自己默认的名称,即Thread-编号;该编号从0开始;

 

自定义线程的名称的方式:

static Thread currentThread():获取当前线程对象;相当于this关键字;静态的,可以用类调用,也可以直接用父类调用。

getName();获取线程名称;

设置线程名称:

setName或者构造函数;

总结:从以上名称的方式可以推测,Thread类的的线程名称应该是定义在构造函数里面的;其实很简单,把setName()方法定义在构造函数里就可以了;

 

6、线程的第二种定义方式:

 

需求:简单的买票程序:多个窗口同时买票;

 

静态可以让所有对象共享资源;但是静态的生命周期太长,浪费内存;各建立一个对象就有四百张票了;

已经在运行的状态是不能再开启一次,否则会出现线程状态异常;

即new一个Thread类的子类,调用了start方法多次;中间报错一次,并且最后的结果肯定是只有一个线程在进行;

局部的变量在每个线程代码区域都会有一份;主要只run方法;???成员变量不一样么???我觉得成员变量是一样的;肯定是一样的;

 

 

创建线程的第二种方式:实现Runnable接口;

步骤:

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

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

目的:将线程要运行的代码存放在该run方法中;

(3)通过Thread类建立线程对象;

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

为什么要将Runnable接口的子类对象传递给Thread类的构造函数,因为自定义的run方法所属的对象是Runnable接口的子类对象;所以要让线程指定对象的run方法,就必须要明确该run方法所属的对象;

(5)调用thread类的start方法开启线程,并调用Runnable接口子类的run方法;

 

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

当一个类已经继承了一个父类,但是里面有需要多线程运行的代码,所以这时候这是就出现了一个接口,使得类具有了功能性的扩展;使其继承了父类,同时通过实现实现了功能的扩展;

主要区别

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

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

 

并且实现方式还可以使得资源可以产生共享;;

 

两种方式的区别还有:

继承Thread线程代码存放Thread子类run方法中;

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

 

当然如果没有其他继承,用继承的方式也都差不多;只是实现方式比较常用;

 

 

其实Thread自己也实现了Runnable,因为这个接口就是为了确定线程要实现的内容代码所存储的地方,即run方法;

 

看Thread的构造函数Thread(Runnable target)其实就是利用的多态的形式用接口类来定义实现类的类型;

 

7、多线程的安全问题:

 

覆盖的接口的方法,出现错误就不能抛,因为接口是不会抛的,所以在重写方法时,只能够进行try catch;(135视频的10分钟);

 

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

多线程最怕的就是安全问题,所以写多线程的时候一定要注意安全问题;

尽量让代码更简单,不会出现在多句话中出现线程切换而发生错误;

或者尽量不共享数据也行;

 

产生安全问题的原因:

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

 

解决办法:

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

 

java对于多线程的安全问题提供了专业的解决方式:

就是同步代码块:

格式是:Synchronized(对象){需要同步的代码}

哪些代码需要同步,就看哪些代码事都在操作共享数据;

(这里可以理解一下  在run块中的区域,每个线程都会分配一个代码区域;但是某一个区域可能都共享一个区域;)这点有点自己把自己搞复杂,其实很简单,就是没这个意思即可;

Synchronized里的参数是对象,因此可以随意定义一个对象放一个对象在参数里面;

可以直接new一个Object的对象,放在里面即可;

其实同步锁的含义是锁的对某一个对象的访问权,锁住了之后,就只能一次有一个人可以访问;故用任意一个对象皆可;但为什么函数上面的锁是锁的this,因为很简单,函数本来就是属于本类的this,他封装的都是this,而锁作为修饰词,肯定也是锁的this了;静态的也是同理;目的就是为了让别人没有访问权限;

 

 

 

8、多线程同步代码块;

同步锁,

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

 

可以解决多线程的安全问题;

 

同步的前提:

必须要有两个以上的线程;必须是多个线程使用同一个锁,才能同步;

(必须要保证是访问的是同一个对象才有意义;)

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

 

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

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

 

双核常会出现情况,就是有的地方运行完了,前面一个还没有打印;

 

练习:

需求:银行有一个金库,有两个储户分别存300元,分三次,每次一百;

目的:该程序是否有安全问题,如果有,如何解决;

如何找问题:

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

2)明确共享数据

3)明确多线程运行代码中哪些语句是操作共享数据的;(即影响到结果的关键步骤语句也就是会产生安全问题的语句)

一般的,分析对结果产生影响的语句一定是非常近,因为太远了,cpu的切换不会持续这么久,cpu切换是非常快速的;

只需要同步同时操作共享数据的多条语句就可以解决安全问题了;

 

注意在Runnable和Thread的子类中的例外只能try不能throws,因为他们throws就抛给虚拟机了;

 

同步代码块封装代码唯一的区别就是它具有局部的特性;

 

9、函数的同步

 

直接把同步锁Synchronized关键字作为修饰符放在函数上面即可

 

同步有两种表现形式,第一个是同步代码块,第二个是同步函数;

 

把需要同步的块,单独封装成一个函数,然后在上面加同步锁,另外建立一个方法引用即可;

如果直接把同步加载一个大函数上面,那么就会出现如果里面有循环,就很只有一个线程在里面;

 

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

函数需要被对象调用,那么函数都有一个所属对象引用,就是this所以同步函数使用的锁就是this;同步块当中的锁,可以是任意的对象都可以;

 

可以用一个小程序验证:

使用两个线程来买票,一个线程在同步代码块中,一个线程在同步函数中,都在执行买票动作;

 

10、静态同步函数的锁调用的是谁:

如果把线程共享的数据设置为静态,发现又出现了安全问题,所以肯定是两个线程不是同一个锁;可以说明静态的属性的锁不是this;

 

静态进内存时,内存中没有本类的对象,但是一定有该类对应的字节码文件对象,即类名点class,该对象的类型是Class。

 

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

即静态的对象不能是this,因为静态的根本就没有this而言;

就是类里面的成员属性是静态的;要锁住它,就必须要锁住类字节码文件,因为每一个人都可以调用,只有把字节码锁住了,别人就调不了了;但静态的类是不能实例化的,所以只能调用类的文件作为对象;

 

如果对象是类文件,即Ticket.class,这个和this就是同一个效果;因为访问的都是同一个数据;

 

11、饿汉式和懒汉式的故事:第140个视频后本部分,重点中的重点

懒汉式在多线程访问时,就容易产生安全隐患;

所以懒汉式加同步会比较低效,每次都要同步比较低效;

把产生问题的语句,进行同步代码块封装一下,比直接在函数上的效率稍微高一些;

用双重判断形式,稍微提高一点效率;

一般单例模式就用饿汉式。

这里面的知识点必须要会,代码什么的都要记住;

 

12、死锁

         通常死锁的出现就是,同步中嵌套同步,而锁却不同;

         要求,必须要会写死锁程序,防止面试;

 

 

13、线程间通信;

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

 

送煤车和拉煤车的示例(存取的实例)

 

在各线程之间,类的字节码文件肯定永远是唯一的,如果找不到一个共同的对象来同步,可以用类的字节码文件;

只是如果不是该代码块的文件,就要让线程多走一个弯路,就是去访问另外一个文件;消耗性能;所以尽量用同步代码块中的对象,比较节约;

 

如何保证存取是有序的存一个取一个,而不是多次重复的;

可以定一个一个布尔型的标签;来进行区别;

 

等待唤醒机制——在开发中比较常见;操作局部数据线程间没有任何的关系,但是操作共享数据时候,就要注意;

 

线程运行时候,内存中会存在一个线程池,等待中的线程都是存放在线程池中的;notify唤醒的就是线程池当中的线程;一般的唤醒的是按照进入线程池的先后顺序而进行唤醒的;即唤醒线程池中的第一个;

 

wait() notify()  notifyAll()必须要用到同步锁里面才行;因为要对持有监视器(锁)的线程操作,所以要使用在同步当中,因为同步才具有锁;

 

notify唤醒的其他线程编程可运行运行状态时,当前线程就变成了wait状态;也就是说当前线程和唤醒的线程是同一个锁才行;

 

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

 

锁一定要锁的是资格和条件,即有访问权限才有资格来判断这个条件,不然就容易乱;因为同一个条件下只能一个线程进行操作,而没有把条件锁住,他们都判断条件都符合要求,而条件根本没有发生变化,那么就有多个线程都有资格做下面的事情,所以只有把判断条件的资格给锁住,一次只能一个线程判断资格去完成事情,做完之后改变条件了,再释放锁就解决问题了;

 

 

比较通用的方式是while循环加上notifyAll();

对于多个生产者和消费者,为什么要定义while判断标记呢?因为让被唤醒的线程再一次判断标记;

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

 

 

缓冲区的存储操作,也就是生产者消费者模型  相见apijava.util.concurrent.locks.condition里面有例子,用的是lock关键字,不再是Synchronized了。

异常中finally里面的动作一般都是用来释放资源的;异常的try块中只有finally没有catch,就要到主函数上抛出;

不管最后是否在操作,都要释放锁,用finally块;

 

jdk1.5中提供了多线程升级了生产者消费者的安全解决问题,将Object中的wait,notify,替换了condition对象,该对象可以Lock锁进行获取,并且一个lock锁可以锁多个对象;

可以用来生产者消费者模型中只实现只唤醒对方操作;之前Synchronized只能锁定一个对象,对应一个wait notify,要定义多个wait  notify就要定义多个锁,这样就容易形成嵌套锁,出现死锁。

 

生产者消费者的替代方案,显示的所机制,显示的所对象的等待唤醒的操作机制;可以锁多个对象;一个锁可以有好几个wait notify,可以有几个condition对象,他把wait notify封装成了conditions对象;

 

14、停止线程

 

现在写多线程都要写循环的;否则就没有必要去做线程,单线程就可以搞定了;

 

stop语句,现在已经不用了,过时,因为有些bug,容易出错;

 

如何停止线程;

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

 

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

 

interrupt不是停止线程的意思,而是打断睡眠中的线程,让其变为可运行状态;

即将处于冻结状态的线程强制恢复到运行状态上来,只有运行才能读标记;

interrupt是用于清除线程的冻结状态;可以用于wait和sleep。

 

当没有指定的方式让冻结的线程回复到运行状态时,这是需要对冻结进行清除;

强制让线程恢复到运行状态中来,这样就可以操作标记让线程结束,Thread类中提供了该方法interrupt()方法。

 

15、守护线程

thread类当中的

 

后台线程,前台线程;后台线程和前台线程开启后是一样的,都抢占cpu的进程,但是后台线程有一个特点就是所有的前台线程结束后,后台线程就自动结束;

后台线程必须是在线程开启之前建立;

t。setDeamon(true);传一个真就开始了。

 

16、join方法

停止当前线程,让加入的线程运行直至结束后再运行当前线程;

 

一个线程在其运算过程中,如果满足一个条件可以临时加一个线程来参与运算;

 

join的特点:

当A线程执行到了B线程的join方法时,A就会等待,等B线程都执行完后,A才会执行;

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

如果被加入的线程碰到了wait等方法,会进入死循环,可以让interrupt把主函数唤醒,知只是会发生异常;

 

17、优先级和yield方法

 

线程组,一般是谁开启的哪个线程就是哪个组;

ThreadGroup   线程组开发也用的不多,而且用的比较麻烦;

 

所有的线程,包括主线程,默认的优先级都是5;优先级范围是1-10个等级;

 

优先级,就是抢占cpu的频率的高低;用setPriority 方法设置;高的优先级抢的高些,并不表示一定可以抢得到,只是机会多些;

一般的,只有1、5、10这三个优先级跨度比较明显,其他都差不多多少;

故将其定义为了几个常量;不写数字,因为数字阅读性太差;MAX_PRIORITY等,见api;

数字是固定的,定义为常量,数据共享并静态。

 

yield()方法,临时释放线程,可以稍微减少线程的运行,可以达到线程平均运行的效果。用的效率不高,面试中会用。

 

18、实际开发中如何写多线程:

 

当某些代码需要同时被执行时,就用单独的线程进行封装;

 

可以用匿名内部类来进行Thread类的子类对象的方法;

 

独立运算,相互不相干;

 

 

 

 

 

 

 

 

 

 

 

 

 

 


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值