多线程
2020-01-07 08:55:01 15 举报
AI智能生成
登录查看完整内容
深入浅出描述多线程
作者其他创作
大纲/内容
多线程程序设计思路
(1). 多线程需求单线程化
无论多线程的需求是什么样子的,一定要先设计成单线程的代码。
(2). 提取需要“同时运行”的代码块
再次根据这段设计出来的单线程代码中哪些地方需要同时运行,就需要把这个需要同时运行的部分封装到多线程需要自己定义的run方法中。
[1].如果抽取出来的代码块执行的是同一个内容,就封装为一个自定义的线程类或者Runnable接口实现类中的run方法。
[2].如果抽取出来的代码块不是执行的同一个内容,那么执行的内容有几种就封装到几个Thread实现子类的run中或者几个Runnable实现子类的run方法中。
【一句话】执行了几种不同的内容就要有几个不同的Thread子类或者Runnable实现子类与之对应。
(4). 单线程中“共享数据”的处理
如果在提取多个需要“同时运行的代码块”的步骤结束之后,如果发现这几个代码块共同都对原来的单线程程序中的某个变量进行了使用,那就要对这个单线程的变量进行封装,封装到Thread子类中或者Runnable接口实现类中的作为共享数据。
{1}. 如果将原有的单线程变量封装到Thread子类中,为了仍然保持多线程对这个变量数据的共享,就要把这个变量设计为Thread子类的静态成员变量,Thread子类通过该变量共享。
{2}. 如果将原有的单线程变量封装到Runnable接口的实现子类中,为了仍然保持多线程对这个变量数据的共享,就要把这个变量设计为Runnable实现子类的普通成员变量。 因为运行的是同一个Runnable子实现类中的run()方法。
(5). 在被抽取出来的代码地方实例化线程并启动
在这些线程代码被抽取出来之后,在原来的位置要创建这个线程的实例,并启动这些线程即可实现多线程。
多线程中的数据安全问题
描述
[1]. 多线程中的数据安全问题不一定就会发生
举例:上面运行了六次代码,三次运行正确,三次运行错误。
[2]. 把多线程中的数据安全问题不一定就会发生的情况称为数据安全隐患问题
数据安全问题出现的原因
{1}.多条语句共同操作了同一个共享数据
{2}. 这几条操作同一个数据的语句又会被多个线程实例执行
1. 这时候某个线程进入run方法并且操作了线程间的共享数据,但是没有执行完run方法中所有关于线程间共享数据的语句就交出了CPU的执行权。
2. 此时下一个线程进来之后对共享数据进行了修改。等上次没有执行完run方法的线程又被CPU执行的时候,就会发生这个线程执行同一个run方法的时候,前后两次共享数据内容不一致的错误。
多线程中数据安全问题分析的步骤
[1]. 首先明确哪些代码是多线程要运行的代码
[2]. 其次明确多线程之间的共享数据
[3]. 再次明确多线程运行的代码中哪些语句是操作共享数据的
[4]. 使用中断CPU执行权的方式对存在的数据安全问题进行分析
{4.1}. 在第一次操作到共享数据的语句处做一个标记并假设有多个线程处于该语句处并等待执行。
{4.2}. 假设某一个线程执行了第一条操作了共享数据有关的语句之后,CPU立刻切换了其他的线程。
{4.3}. 假设另一个线程再次进入这个方法并修改了相应的共享数据之后,再次被CPU中断了执行。
使用Thread.sleep(xxx)强行切换CPU执行的线程【模拟】
【提醒】如果一个线程进入run方法之后把所有有关共享数据的代码一下子全部都执行完成,那么多线程的数据安全问题是不会发生的。
同步代码块基本用法和原理
基本用法
(1). 同步代码块基本格式
synchronized(对象)
【注意】synchronized后面的()中的“对象”是任意类型都可以。但是这个对象也必须以多线程之间的共享数据的身份出现,也就是多个线程之间共享这一个对象。
(2). 对run方法中代码加同步的判定方法
run方法中哪些语句操作了多线程间的共享数据,哪些语句就放置到同步代码块中。
同步代码块原理-锁机制
对象的锁
每个对象都有一个标志位,取值为0的时候表示“上锁”;取值为1的时候表示“开锁”。这个对象的标志位称为对象的锁。
持有锁的线程
当一个线程进入同步代码块的时候,如果此时同步代码块上的obj的锁的值是1(也就是开锁),就把这个线程称为持有锁的线程。
同步代码块的原理
{1}. 持有锁的线程可以在同步代码块中运行。
{2}. 不持有锁的线程即使获取了CPU的执行权,也不能进入同步代码块执行。
{3}. 具体执行过程
1. 对同步代码块上锁
当一个线程进入同步代码块的时候,如果发现同步代码块的obj的锁的值是1,在执行同步代码块的内容之前先把obj锁的值变成0(也就是给同步代码块上锁),同时这个线程也被称为持有锁的线程。
2. 对同步代码块开锁
当持有锁的线程执行完同步代码块的时候,会将同步代码块的锁的值变为1,表示对同步代码块开锁,这样别的线程就有机会运行这个同步代码块中的代码。同时原来持有锁的线程也变成了非持有锁的线程。
(2). 解决了多线程中共享数据的安全问题
[1]. 假设这个时候这个持有锁的线程失去了CPU的执行权,别的线程想运行这个同步代码块中的时候,发现同步代码块已经被上锁,所以没有办法进入到同步代码块中执行同步代码。
[2]. 这一点保证了如果某个持有锁的线程即使没有一下子操作完所有和共享数据有关的代码,别的非持有锁的线程也没有机会在同步代码块被持有锁的线程上锁之后执行同步代码块中和共享数据有关的代码。
[3]. 因此失去CPU执行权的持有锁的线程再次获得CPU执行权的时候,线程间的共享数据的值仍然是这个持有锁的线程被CPU中断执行时候的值,并没有发生改变。
对操作共享数据的语句加锁,不要对操作非共享数据的语句加锁。
数据安全扩展
【注意】对run方法中对自身引用类型的共享数据变量调用了其中的方法之后,一定要展开这个属于多线程共享数据的自身引用数据类型的方法。
只要多线程间共享的数据类型的是引用数据类型并且在run方法中对这个引用数据类型的方法进行了调用,就必须展开分析其中的代码。
同步函数基本用法和原理
同步函数的基本用法
(1). 同步函数的基本格式
直接在要同步的方法前使用synchronized关键字修饰这个方法即可。这样同步函数里面的函数体就是需要同步的代码块。
(2). 同步函数的应用场合
[1]. 对Runnable实现类中的run方法中的共享代码块,抽取成同步方法
[2]. 如果一个方法内部的整个方法体都被同步代码块包围,那么此时就可以直接去掉这个同步代码块并在这个方法前面加上synchronized关键字修饰这个方法。
2). 同步函数原理-锁机制
(1). 非静态同步函数的锁
非静态同步函数的锁对象就是调用这个非静态方法的this对象(对象锁)
同步函数和同步代码块的同步原理都是锁机制,即使用多线程之间共享对象的锁标识位。
(2). 非静态同步函数的工作原理
[1]. 清楚了非静态同步方法的锁对象之后,非静态同步函数的工作原理和同步代码块的同步工作原理是一样的
[2]. 同步代码块的同步工作原理就是持有锁的线程对同步代码块的上锁和开锁的过程。一旦同步代码块被上锁,非持有锁的线程就不可以执行同步代码块中的代码。
[3]. 非静态同步函数的同步工作原理也是持有锁的线程对同步函数体的上锁和开锁的过程。
特别的地方就是非静态同步函数的锁对象是调用这个非静态同步函数的this对象。
一旦非静态同步函数体被上锁,非持有锁的线程同样也是不可以执行同步代码块中的代码。
3). 验证非静态同步函数的锁对象
验证思想
{1}.让同步代码块和同步函数执行相同的功能并操作多线程间同一个共享数据。
{2}.同步代码块的锁对象是Runnable实现子类对象的成员变量(objLock),同步函数的锁对象就是调用这个函数的this对象(this)。
验证结论
1. 如果最后共享数据没有出现数据安全问题,说明同步代码块的锁对象和同步函数的锁对象是同一个
2. 如果最后共享数据出现了数据安全问题,说明同步代码块和同步函数的锁对象不是同一个
结论: 数据安全问题出现。所以证明同步代码块和同步函数使用的锁对象不是同一个。
结论2:如果要实现数据安全,同步代码块的锁就不能再使用Runnable实现子类对象的成员变量(objLock)了,而要使用run方法的调用者this
线程两种创建方式下的同步
使用Thread子类创建线程中的同步
(1). 在Thread子类中的共享数据
Thread子类中的共享数据必须以Thread子类的静态成员变量的方式才能被多个线程对象进行共享。
(2). 在Thread子类中使用同步代码块
[1]. 在Thread子类的run方法中使用同步代码块进行线程间的同步
在Thread子类中使用静态成员对象做同步代码块的锁
在Thread子类中使用普通成员对象做同步代码块的锁,用一个对象作为子线程构造函数的参数,传入多个线程中
(3). 在Thread子类中使用同步函数
{1}. 直接在Thread子类中定义同步函数并在run方法中直接进行调用。这种方法一定是不可行的。
{2}. 采用非静态方式解决办法:借鉴Runnable的实现子类的方式
{2}1.自定义一个类用来存放同步方法。(封装)
{2}2.为Thread子类自定义一个刚才提到的自定义类的普通成员属性和以这个自定义类为参数的构造方法。
{2}3.在main方法中实例化自定义类的对象并在Thread子类是实例化的时候传入这个自定义类对象。
{3}. 注意无法采用静态同步函数的方式来解决问题。因为run方法是非静态方法。
使用Runnable实现子类对象创建线程中的同步
概述
直接在Runnable实现子类中定义普通的成员变量就可以达到这个成员变量因为Runnable实现子类对象被多线程对象共享而共享。
{1}. 创建线程实例的时候,向所有线程类构造方法的传入的Runnable实例对象一定是线程实例之间最大的共享对象
{2}. Runnable实例对象的所有成员属性同样也是线程实例之间的共享对象
在Runnable实现子类中使用同步代码块
此时就仅仅在Runnable实现子类中定义额外的非静态成员变量作为同步代码块的锁对象即可。
(3). 在Runnable实现子类中使用同步函数
{1}.非静态同步函数的锁对象就是调用这个同步函数的对象,即这个this指向的对象。
{2}.根据锁对象的性质:调用同步函数的this对象要求必须是多个线程之间共享的对象。
{3}. Runnable实例对象本身是线程实例间最大的共享数据和Runnable实例对象本身的所有成员属性也是线程实例间的共享数据的结论
【依据以上三点】可以把同步函数直接定义在Runnable实现子类中,也可以把同步函数封装到Runnable实现子类的某个引用类型的成员属性上
同步代码块和同步函数的关系
Runnable实现子类
(1). 同步代码块的自定义锁向this锁转换
[1]. Runnable实现子类对象本身就是共享数据,所以不必额外在Runnable实现子类中定义额外的共享对象来用作锁对象。直接使用this作为锁对象即可。
(2). 同步代码块的this锁对象到同步函数
[1]. 将使用this对象作为锁对象所构成的同步代码块直接封装到Runnable实现子类的自定义同步方法中。
[2]. 在这个run方法中直接调用本类中的同步方法的时候,就通过this指针。
(3). Runnable实现子类本身的同步函数封装到统一的自定义类中
[1]. 将Runnable实现子类的自定义同步方法和用到的共享数据一同封装到自定义的类中。
[2]. 在Runnable实现子类直接增加封装了同步方法和所用的共享数据的类对象作为Runnable本身的成员变量。
[3]. 这样封装的好处 --增强了代码的维护性
多线程
基本概念/创建方式
线程的基本概念
进程
正在运行中的程序
JVM启动的时候,会产生一个进程java.exe与其对应。
进程和线程都是由本地操作系统OS创建的。而JVM仅仅是起到调用OS中相应功能的作用
线程
就是进程中的一个独立控制单元
一个进程至少有一个线程
主线程
进程java.exe中至少有一个线程负责Java程序的运行,这个线程就是主线程
主线程运行代码的存放位置:存放在main函数中
JVM的多线程启动
执行应用程序中的main函数中的代码
垃圾回收线程
负责清理主线程运行过程中在堆内存中产生的垃圾对象
创建线程
继承Thread类
JVM创建线程依赖于Windows/Linux。JVM要调用这些OS的内容方可完成线程的创建。Java将处理上述事情封装成一个类,这个类就是Thread类
创建线程的第一种方式:创建Thread类的子类
【step 1】继承Thread类,创建Thread类的子类
【step 2】重写Thread子类中的run方法
【step 3】在主线程中,调用Thread类的子类对象的start()方法来启动线程
线程的运行状态
run()和start()关系
run()
Thread类的run()的作用:用于存储非主线程线程代码
子类重写run方法的目的:将需要自定义线程运行代码存放在run()方法中,等待自定义启动之后,运行run()中的内容。
如果主线程仅仅调用子类的run()方法的结果就是主线程直接执行线程子类的run()方法的代码,没有启动另一个进程/程序控制单元。整个程序/进程仍然是单线程执行。
start()
[1]. Thread类的start()的作用:用于开启线程并执行定义在run()中的代码
[2]. 主线程必须调用线程实例的start()的原因:调用run()之后,成个程序仍然是单线程、调用start()方法之后,start中的本地native方法start0()方法会调用操作系统的底层的资源创建相关的线程。这个时候,这个的进程才是多个线程来执行。
线程运行状态转换
(1). 调用start()方法
调用start()方法会使得线程从新建状态发生状态跳转。最有可能跳转到临时阻塞状态,同时也有很小的可能使得该线程立刻被执行,处于运行状态。
(2). 调用sleep(时间)方法或者wait()方法
线程如果碰到sleep(时间)方法或者wait()方法,就会立刻变成冻结状态(挂起状态)。放弃CPU的执行权。
(3). sleep()运行时间到或者调用notify()方法
当sleep()运行时间到或者调用了notify()方法,处于冻结状态的线程会激活。很有可能跳转到临时阻塞状态。有很小的可能回到运行状态。
(4). stop()或者run()方法运行完成
当调用stop()方法或者run()方法被线程运行完成,线程就从运行状态走向了消亡状态。
(5). 临时阻塞状态和运行状态
这两个状态之间的转换完全是看相应的线程有没有CPU执行权。如果有,就执行。没有,就等待。
获取线程对象的名称
默认的线程的名称:“Thread-编号”(编号从0开始)
通过构造函数的初始化方式为线程对象设置对应的线程名称
通过name属性的setter:setName()方法为线程改变线程名称
通过Thread类的静态方法 currentThread()方法来获取当前被CPU执行的线程对象
创建线程两种方法的对比
Runnable接口
当我们将实现了Runnable接口的类的实例,作为Thread构造函数的参数时,我们就不再使用Thread类里面的run()方法了,而使用传入参数的run()方法
Thread类本身实现了Runnable接口,并且有个Runnable成员变量,用来接收构造函数中传入的参数值
直接new Thread();是没有实际意义的。要么是Thread子类,要么传实现了Runnable接口的参数
【结论】线程对象一定是Thread类或者Thread子类的对象。与Thread类无关的类的实例对象都不是线程对象。
Thread类和Runnable接口
继承Thread的缺点
[1]. 缺点1 ----生命周期过长
静态变量的生命周期和这个类的Class对象的生命周期一样长
[2]. 缺点2 ----是所有类的实例的共享数据,数据不安全
(2). 改进Thread继承类
提取共享数据到自定子类,作为构造函数传入线程的成员变量中
自定义类的对象以共享数据的身份被相应的多线程对象进行操作,那么这个自定义对象自身的成员属性也属于多线程对象的共享数据。
缺点
缺点I:增加了Thread子类对锁对象的访问的难度
缺点II:增加了Thread子类对共享操作数据的访问的难度
(3). 改进Thread子类+ 自定义SynClassI类的优化 -抽取run()方法
Thread子类的run方法调用自定义类的run方法(委托)
[1]. 优化I:将Thread子类中的run方法也移植到自定义类SynClassI类中
[2]. 优化II:在Thread子类中直接通过自定义类SynClass类的synClassObj对象直接调用SynClassObj封装好的run方法即可。
【优点】为自定义SynClass类增加了run方法实际上是格式化了Thread类的代码
(4)自定义类向上抽取成公共接口
自定义SynClass类的进一步优化 ---- 向上抽取成通用接口run
综述:Java设计者在设计Thread类和Runnable的时候就遵循了在1中提到的优化过程。
0 条评论
回复 删除
下一页