多线程

1.多线程概述

概述:多线程是java特点之一,掌握多线程技术,可以充分利用cpu资源,解决实际中的问题,多线程技术广泛应用于与网络有关的程序设计中,掌握多线程对学习网络至关重要。

多线程:指一个应用程序有多条并发执行的线索,每一条线索我们称为线程,他们交替执行,彼此间进行通信

并非:一段时间内,多个线程同时运行。本质是cpu的高速切换

 

 

1.1进程

简介:在一个操作系统中,每个独立运行的程序都可以称为一个进程,即“正在运行的程序”

补充 : 鼠标右键点击任务栏, 选择 `任务管理器` 选项, 可以打开任务管理器面板. 在窗口的 `进程` 选项卡中可以看到当前正在执行的程序.

在多任务操作系统中,表面上看似支持进程并发执行的,例如可以一边听音乐一边聊天.但实际上这些进程并不是同时运行的.在计算机中, 所有的应用程序都是由CPU执行的, 对于一个CPU而言,在某个时间点只能运行一个程序,也就是说只能执行一个进程.操作系统会为每一个进程分配一段有限的CPU使用时间, CPU在这段时间中执行某个进程,然后在下一段时间切换到另一个进程中执行,由于CPU运行速度很快,能在极短的时间内在不同的进程之间进行切换,所以给人一种同时执行多个程序的感觉.

 

1.2什么是线程

简介:线程不是进程,但其行为很像进程。线程是比进程更小的执行单位,一个进程在其执行过程中,可以产生多个线程,每个线程都有它自身产生,存在和消亡的过程。和进程可以共享操作系统的资源类似,线程间也可共享进程中的某些内存单元(代码和数据),并利用这些共享单元来实现数据交换,实现通信和必要的同步操作,但与进程不同,线程的中断与恢复可以更加节省系统的开销,具有多个线程的进程能更好的表达和解决现实世界中的具体问题。

概况:没有进程就不会有线程,就像没有操作系统就没有进程一样

问题 : 线程是不是越多越好呢?

答:对于一个CPU而言,同一时刻,只能执行一个任务。同一时间段内,如果线程过多,每个线程被切到的时间就变少了。因此, 线程并不是越多越好。(例如: 迅雷下载, 喂宝宝吃东西)

 

1.3主线程

Java语言的一大特性就是内置了对多线程的支持

每个运行的程序都是一个进程,当一个Java程序启动时,就会产生一个进程, 该进程会默认创建一个线程. 我们已经知道, Java应用程序总是从主类的main方法开始执行. 当JVM加载代码, 发现main方法之后, 就会启动一个线程, 这个线程被称为 `主线程`. 该线程负责执行 main 方法, 那么, 在main方法的执行中再次创建的线程, 就称为程序中的其它线程.如果main方法中没有创建其它的线程,那么当main方法执行完最后一条语句,即main方法返回时,JVM就会结束我们的Java应用程序.如果main方法中又创建了其它线程,那么JVM就要在主线程和其它线程之间轮流切换,保证每个线程都有机会使用CPU资源,main方法即使执行完最后的语句, JVM也不会结束Java的应用程序, JVM一直要等到Java应用程序中的所有线程都结束之后,才结束Java应用程序.

说明:多条线程,看似同时执行,其实不然,和进程一样都是cpu高速切换的轮流执行的。

 

1.4main方法单线程运行图解

执行结果:

1.5体验线程

总结:使用多线程技术实现在一个java程序中同时执行两个或两个以上无限循环

2.线程的创建

java提供了两种多线程实现的方式

1.继承java.lang包下的Thread类,重写Thread类的run方法,在run方法中实现运行在新线程上的代码。

2.实现java.lang.Runnable接口,同样在run方法中实现运行在新线程上的代码。

 

2.1继承Thread类

2.1.1Thread类介绍

并发地运行多个执行线程的一种方式是:将类声明为Thread的子类,该子类应重写Thread类的run方法,接下来可以分配并启动该子类的实例

 

2.1.2实现步骤

疑问解答:

问1:为什么不直接调用run方法,而调用start方法?

答1:因为run方法中仅仅封装了线程的任务,该任务无法调用操作系统资源,从而来启动线程

在Thread类中,提供了一个start方法用于调用系统资源,并启动新线程,线程启动后,系统会自动调用run方法,我们直接调用run方法,此时执行的只是普通方法,并没有在内存中开启新的内存空间运行任务代码,只有调用start方法才会开启新的空间,并在新的空间中自动去运行run方法

问2:为什么要继承Thread类?

答2:只有继承了Thread类,才具备操作线程的各种方法(开启线程,关闭线程等),即线程类,同时就可以重写run方法,将线程要执行的任务放到run中执行,

问3:为什么要重写run方法?

答3:在Thread类中,由于target是空的,因此系统什么都不会做,只有当子类重写了该方法,那么程序执行时,就会调用子类重写的方法

设计Thread这个API的人,在设计的时候,只设计了如何启动线程,至于线程要执行什么任务,他并不知道。所以,如此设计. 让start启动线程之后,JVM来自动的调用run方法执行线程任务代码。

问4:线程是否可以多次启动?

答4:多次启动一个线程是非法的

面试题:start方法和run方法的区别?

run:封装线程任务,不调用系统资源,开启新线程

start:先调用系统资源,启动线程,再执行run方法

 

2.1.3  获取和设置线程名称

Thread类的三个和线程名称有关的api

 

2.1.4 继承Thread优点小结:

使用Thread子类创建线程的优点是:可以在子类中增加新的成员变量,使线程具有某种属性,也可以在子类中新增方法,使线程具备某种功能,但是java不支持多继承,Thread类的子类不能再扩展到其他类

 

2.2 实现Runnable接口

2.2.1 Runnable接口介绍

我们发现Thread类中的run方法就是实现自Runnable接口,run方法是用来封装线程任务的。在Runnable接口中,只有一个run方法,因此,这个接口就是专门用来封装线程任务的接口,因此,实现该接口的类,称为线程任务类。

 

2.2.2 实现步骤

注意:getName()和setName()方法都是Thread类的方法,类实现了Runnable接口时,是不能用此方法,所以我们得先获取此线程对象,再获取此线程的名称

 

2.2.3 实现原理

方式1:继承Thread类,重写run方法,调用start启动线程,会自动调用线程类中的run方法。

方式2:实现Runnable接口,将任务对象传给Thread对象,调用Thread对象的start方法...(同上)

分析 :

1. 在我们创建 Thread 类对象时,我们已经将 task 任务类的对象作为参数传递给了线程类对象.在其内容就会将 task 赋值给内部属性 target 进行存储.

2. 当我们调用 Thread 对象的 start 方法启动线程时,肯定会执行 Thread 类的 run 方法.而 run 方法的实现如下 :

3. 在 Thread 的 run 方法中, 会先判断 target 是否为 null. 这个target就是我们创建 Thread 对象时传入的任务类对象,所以 target 不为 null, 因此就会执行 target 的 run 方法. 也就是任务类的 run 方法.

 

2.3 两种实现多线程方式的对比分析

请问:既然继承Thread类和实现Runnable接口都能实现多线程,那么两种方式又有什么区别?

需求:某航空公司有三个窗口发售当日航班100张票,100张票作为共享资源,三个售票窗口开启三个线程,使用代码模拟

注意:不是每个售票窗口都是100张票,而是一共100张票

2.3.1 继承Thread类

分析:从结果来看,每张票都被打印了三次,出现这种情况是因为三个线程没有共享100张票,而是各自出售100张票,再程序中创建了三个TicketWindow对象,就等于创建三个售票程序,每个程序都有自己的100张票,每个线程都在独立处理各自的资源

试想:如果将属性改为静态属性呢?

很明显是存在问题的,第98张门票不可能再第99张门票出售之前,那为什么会造成这样问题呢?因为三个线程共用了一个属性,但此属性并没有被加锁,所以当对此属性进行修改操作时,比如会出现线程问题

 

2.3.2 实现Runnable接口

说明:为了保证资源的共享,再程序中只能创建一个售票对象,然后开启多线程去运行同一个售票对象的售票方法,简单来说就是三个线程运行同一个售票程序

原理:线程执行的是线程任务类中run方法,三个线程执行了同一个线程任务类中的run方法(有点类似于单例模式)

 

2.3.3 使用接口完成多线程的好处

1.避免了java单继承的局限性(可以通过多实现来扩展方法)

2.把线程代码和任务代码分离,完全解耦

3.适合多个相同程序代码的线程去处理同一个资源的情况,更灵活的实现数据的共享

 

3.线程同步

说明:多线程的并发执行可以提高程序的效率,但是,当多个线程访问同一个资源时,也会引发一些安全问题,当多个线程同时访问同一个变量,且一些线程需要去修改这个变量,程序应该怎么处理?

3.1.1 观察之前的售票系统

再售票系统中,碰到一系列的意外,比如同一张票被售出了很多次,或者售出的票为0甚至为负数,这些意外都是由于多线程操作共享资源tickets所导致的线程安全问题

  出现同一张票被多次售出的情况

        售出不可能存在的票

原因:多个线程操作共享资源,cpu高速切换

出现的错误票号:

是多个线程在执行售票任务的同时,由于在售票的代码中访问了同一个成员变量Tickets,可是在操作tickets的这些语句中,一个线程操作到其中一部分代码时,cpu高速切换到其他线程中开始执行代码,此时就导致了tickets变量的值被修改的不一致。

上述的这些问题,我们称为多线程运行的安全问题

总结多线程的安全问题发生的原因: 

1.首先必须有多线程

2.多个线程在操作共享的数据,并且对共享的数据有修改(如果只是读,不会产生线程安全问题)

3.本质原因是cpu在处理多个线程数据时,在操作共享数据的多条代码之间进行切换导致的

 

3.2 多线程安全问题的解决方案

3.2.1 同步代码块

说明:要想解决线程安全问题,必须保证用于处理共享资源的代码在任何时刻都只能被一个线程所访问

为了实现这种限制,java中提供了同步机制,当多个线程使用同一个共享资源时,可以将处理共享资源的代码放置在一个代码块中,使用Synchronized关键字来修饰,被称为同步代码块。

问1:锁对象时什么?  答:任意一个对象

问2:哪些代码需要被同步  答:操作共享资源的所有代码

上述解决方案被称为,线程的同步。为保证线程的安全,需要在操作共享数据的地方,加上线程的同步锁。

锁对象的前提条件:必须保证锁对象的唯一性!!!

同步代码块中的锁对象可以时是任意类型的对象,但是多个线程共享的锁对象必须是唯一的,'任意'指的是共享的锁对象的类型,所以,锁对象的创建代码不能放在run()方法中,因为run方法中创建的变量都是局部变量,每个线程运行run()方法都会创建一个新的锁对象,每个线程都有一个不同的锁对象,每个锁都有自己的标识位,线程之间不能产生同步的效果。

注意事项:

1.同步代码块中,只能包含操作资源的代码,不能乱包

2.同步代码块中锁可以是任意的,但必须是唯一的

原理说明:lock锁对象,它是同步代码块的关键,当线程A执行同步代码块时,会先检查锁对象的标识位,默认情况下标识位为1,此时线程开始执行同步代码块中代码,并将锁对象的标识位设置为0,当另一个线程B执行到这段同步代码块时,检测到锁对象的标识位为0,线程B会发生阻塞,等待线程A执行完同步代码块后,释放锁对象,此时锁对象重写设置为1,线程B才能进入同步代码块中执行其中的代码,循环往复,直到共享资源被处理完为止,这个过程好比公共电话亭,出一个进一个

 扩展:关于this作为锁对象的演示和说明:

当实现Runnable时,可以使用此种方式,因为线程任务类对象(this)是独一份的。 

 

3.2.2 同步方法

说明:在方法前面使用synchronized关键字修改,被修改的方法即同步方法,他能实现和同步代码块相同的功能

被synchronized修饰的方法在某一个时刻只允许一个线程访问,访问该对象的其他线程都会发生阻塞,直到当前线程访问完毕后,其他线程才有机会执行方法

问1:同步方法有锁吗?   答:有

问2:同步方法中锁是谁?  答:当前调用该方法的对象,即this指向的对象

这样的好处是,同步方法被所有线程共享,方法所在的对象相对于所有线程来说是唯一的,从而保证了锁的唯一性

注意:

1.如果是一个方法内部,所有代码都要被同步,那么用同步方法

2.同步方法的锁是this

 

4.认识Lock

JDK5提供的Lock接口比JDK5之前的同步更好使用。Lock接口代替JDK5之前的同步代码块. 更加灵活.

注意 : Lock接口的实现类对象不能和 Object类中的 wait(), notify(), notifyAll(), 共同使用, 因为Object类中的 wait(), notify(), notifyAll(), 方法必须要和 同步锁 synchronized 共同使用, 否则报异常!

使用 Lock 接口的 Reentrant 实现类对象实现同步锁的功能 :

 注意:Lock接口的实现类对象必须是唯一的

 

5.匿名内部类方式实现多线程

 

6.线程的运行状态图(生命周期)

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值