分享

徐葳【2019版最新】40小时掌握Java语言之05多线程

 大数据徐葳 2019-03-30

1章:多线程

1.1 多线程简介

java也是支持多线程的语言。

什么是线程呢?

在说线程之前先说一下什么是进程

进程:指当前正在执行的程序,代表一个应用程序在内存中的执行区域。

如图1.1所示。

1.1 进程信息

看下面图1.1所示,里面是有很多的应用程序的,每个应用程序都使用一块内存区域,这个内存区域可以称为一个进程,内存区域中是需要执行代码的,具体执行代码就是线程去执行的。

注意:进程只是负责开辟内存空间的,线程才是负责执行代码逻辑的执行单元。

1.2 进程-线程

线程:是进程中的一个执行控制单元,执行路径。

一个进程中至少有一个线程在负责控制程序的执行。

一个进程中如果只有一个执行路径,这个程序称为单线程程序。

一个进程中如果有多个执行路径时,这个程序就称为多线程程序。

单线程和多线程有什么区别呢?

举一个火车站卖票的例子。

一个窗口卖票的时候效率就太低了,如果同时有上百个窗口卖票,这个时候效率就高了。

多线程最明显的效率就是提高执行效率。

多线程的出现可以有多条执行路径,让多部分代码可以同时执行,来提高效率。

1.2 jvm中的多线程

Java中的jvm虚拟机是单线程还是多线程呢?

如图1.3中这个例子,在执行里面的代码的时候会在堆内存中产生很多垃圾,如果是单线程处理,并且后面有很多代码的话,就可能会造成内存溢出,因为单线程是需要这个代码执行完才会去调用内存回收机制的,所以这样就不合理了。

我们想实现这样的功能,一个线程负责执行主程序,另一个线程负责垃圾的回收。

也就是在程序运行的同时,也进行垃圾回收,其实java就是这样做的,

所以java虚拟机也是多线程的。

1.3 代码

2章:线程的创建

2.1 线程创建的方式一-继承thread

如图2.1中显示的这个异常发生在主线程上。

2.1 代码

再看下面这个例子中的代码,如图2.2所示。

这个代码执行的结果是先打印one,最后再打印two

2.2 代码

在图2.2这个例子中,只有一个主线程在控制代码执行的流程,当d1.show();没有执行完的时候,d2.show()是不可能执行的,如果d1执行时,遇到了较多的运算,那么d2就只能等d1结束。

这两个函数之间也没有什么依赖关系,可不可以实现让d1d2同时执行呢?

可以!这时就需要由一个线程控制d1,另一个线程控制d2,那如何创建一个线程呢?

其实java中对线程这类事物已经进行了封装,并提供了相对应的对象,这个对象就是Thread

查看API文档中Thread的介绍:如图2.3所示。

2.3 线程的介绍

Demo3类,继承Thread类,覆盖run方法。代码实现如图2.4所示。

2.4 线程代码

为什么要继承thread类,覆盖run方法呢?

其实直接建立Thread类对象,并开启线程执行就可以了,但是虽然线程执行了,可是执行的代码是Thread类里面run方法中默认的代码。

可是我们定义线程的目的是为了执行自定义的代码,而线程运行的代码必须是在run方法中的,所以只有覆盖run方法,才可以运行自定义的内容,想要覆盖run方法,必须先要继承Thread类。

注意:主线程运行的代码都在main函数中,自定义线程运行的代码都在对应的run方法中。

如何调用Demo3这个线程子类中的代码去执行呢,如图2.5所示。

2.5代码

这样执行的时候,会发现打印的结果和之前没有改为多线程的时候一样

这个程序,其实还是只有一个主线程真正执行。

如果直接调用该对象的run方法,这时,底层资源并没有完成线程的创建和执行,仅仅是简单的对象调用方法的过程,所以这时执行控制流程的只有主线程。

如果想要真正开启线程,需要去调用Thread类中的另一个方法来完成。

start方法:

该方法做了两件事情:

1. 开启线程

2. 调用了线程的run方法

修改下面的代码执行,这个执行效果就是多个线程同时执行。如图2.6所示。

2.6 多线程代码

2.2 线程运行的随机性

当创建了两个对象d1,d2后,这时程序就有了3个线程在同时执行(d1,d2,main)

当主函数执行完d1.start()d2.start()后,这时三个线程同时打印,结果比较杂乱,这时因为线程的随机性造成的。

随机性的原理是:

windows中的多任务同时执行,其实就是多个应用程序在同时执行,而每一个应用程序都由线程来负责控制的,所以windows就是一个多线程的操作系统,CPU是负责提供程序运算的设备。

CPU的特点:在某一个时刻,一个CPU,只能执行一个程序,所以多个程序同时执行其实并不是真正的同时执行,其实就是CPU在做着快速的切换完成的,只是我们感觉上是同时而已。

能不能真正意义上的同时执行呢?

可以的,就是需要多个CPU,也就是现在所见的多核cpu

如图2.7所示。

2.7 CPU执行

再把代码改一下,看一下多个线程的名称,在这我们还没学习到如何获取线程的名称,所以我们通过其他方式来查看线程的名称,如图2.8所示。

可以看到打印的错误信息mainThread0Thread1

2.8线程信息

2.3 线程对象的获取和名称的定义

如果我想通过代码获取线程的名称该怎么获取呢?

查看API文档可以发现 Thread类有一个方法叫getName,可以获取当前线程的名称。

看下面这个例子,如图2.9所示。

2.9线程信息

注意:因为Demo3这个类是Thread的子类,所以可以直接使用Thread类中的getName()方法,获取当前线程的名字。

多线程的名称默认是以Thread-开头,后面的编号是从0开始的。

我们知道main函数也是由一个主线程执行的,那我在这也使用getName()能不能获取到主线程的名称呢?

不可以的,因为这个类并不是Thread的子类,所以无法使用这个方法。

这个主线程是虚拟机创建的。

但是我们可以通过Thead类中的currentThread方法获取当前线程对象,再通过当前线程对象来调用getName方法获取当前线程的名称。
下面这个代码执行完之后就可以看到主线程的名称就是main

如图2.10所示。

2.10 获取线程对象

线程默认的名称不容易识别,所以就想给线程起名字

API发现还有一个setName方法,如图2.11所示。

2.11 设置线程名称

查看API文档发现这个Thread对象的名称还可以在创建线程的时候通过构造函数传递过去。

但是我们之前也传递了参数,线程的名称也没有发生变化

那是因为我们自定义的子类没有这个功能,所以需要在子类的有参构造函数中调用父类的有参构造函数。

改成下面这样就行了,如图2.12所示。

2.12线程代码

总结下刚才我们说的那几个方法

static Thread currentThread():获取当前线程对象

String getName():获取线程名称

void setname():设置线程的名称

Thread(String name):构造函数,在建立线程对象的时候指定名称

2.4 线程运行状态图例

画图分析一下线程运行时的不同状态。如图2.13所示。
有时候cpu在执行这个线程,有时候CPU不在执行这个线程

线程首先被创建,再运行,被创建的线程如何到运行状态呢?

调用start方法就可以了

还有一种状态,冻结状态。

还有一种状态,消亡状态

运行状态到消亡状态需要调用stop方法,或者run方法运行结束。

运行状态怎么到冻结状态呢?

有时候我们需要让线程在执行的时候暂时停一会,让别的线程去执行。

通过冻结状态控制线程的执行。

让正在运行的线程调用sleep(time)可以让当前线程睡一会。

具体睡多久,我们通过参数来执行,单位是毫秒。

如何从冻结状态恢复到运行状态呢?

sleep(time)时间到的话线程就会自动恢复到运行状态了。

通过调用wait()方法也可以让运行的线程进入到冻结状态,但是wait不用指定时间,而sleep必须要指定时间。如何恢复呢?这个时候就需要让另外一个线程调用notify方法(唤醒的意思)

还有一个最重要的状态,临时阻塞状态

这个状态怎么来的呢?

假设我有三个线程,A线程和B线程还有C线程,当这3个线程都调用了start方法后,叫做3个线程具备了执行资格,处于临时阻塞状态。当某一个线程A正在被CPU执行,说明A线程处于运行状态,即具备了执行资格,也具备了CPU的执行权。

B,C处于临时阻塞状态,当CPU切换到B线程时,B就具备了执行权,这时AC就处理临时阻塞状态,只具备执行资格,不具备执行权。

所以,临时阻塞状态:该状态中的线程,具备执行资格的,但是不具备执行权。

临时阻塞状态时由CPU控制的,冻结状态是人为控制的。

2.13 线程的四种运行状态

2.5 线程创建的方式二-实现runnable接口

需求:

火车站售票,一共100章,通过4个窗口卖完。

因为4个窗口售票动作被同时执行,所以需要用到多线程技术。代码如图2.14所示。

开启四个窗口卖票:

2.14 卖票代码

本来100张票,现在却卖出了400张票,这样就出大事了。

现在我创建了4个对象,每个对象都有一个ticket变量,所以就是400张了。

这个时候把这个变量设置为静态的就可以了。这样再执行,打印结果就正常了。

如图2.15所示。

2.15 代码

但是我们不建议使用静态,因为加上静态之后,对象的生命周期变得过长,我们还有其他解决方案来解决多线程数据共享的问题,所以把static关键字取消掉。

main函数中new一个线程,调用4start行吗?

注意:这样是会报错的。因为多次启动一个线程是会报错的。

线程已经开启了,再调用开启是不合适的,如图2.16所示。

2.16 代码

如果你要处理的资源和你的动作封装到一起了,可以怎么做呢?

继承搞不定的话我们就使用另外一种方式来搞定

创建线程的另一种方法是实现runnable接口,然后实现run方法,在创建Thread时作为一个参数来传递并启动

API文档中查看一下runnable接口

把之前的代码改造成这样的,如图2.17所示。

2.17 线程代码

这个代码执行的时候是没有任何输出的,因为默认Threadrun方法什么都没做。

可是我开启多线程的目的是为了让他执行我指定的run方法

所说义在这我们要首先明确run方法所属的对象。

我只要在线程类建立对象的同时,把要执行run方法的对象传进去即可。

这样Thread线程在开启的时候就有了明确的run方法。

把这个ticket对象传给四个线程对象,如图2.18所示。

2.18 多线程代码

在这执行的时候如果想要获取线程的名称,就不能在TicketWin类中直接使用getName了,因为现在这个类不是线程的子类了,

在这我们使用线程的currentThread方法获取线程名称,如图2.19所示。

2.19线程代码

那么这一种和第一种比到底有什么好处呢?

第一种方式都继承子类之后会造成资源不共享,

第二种的话,就很方便了,实现一个接口,让多个线程去运行即可。这样就可以实现资源的共享了。

2.6 线程两种创建方式的区别

 一:继承Thread类。

       步骤:

1.定义类继承Thread

2.覆盖Thread类中的run方法,run方法用于存储多线程要运行的代码。

3.创建Thread类的子类对象创建线程。

4.调用Thread类中的start方法开启线程,并执行子类中的run方法。

       特点:

1.当类去描述事物,事物中有属性和行为。

              如果行为中有部分代码需要被多线程所执行,同时还在操作属性。

              就需要该类继承Thread类,产生该类的对象作为线程对象。

              可是这样做会导致每一个对象中都存储一份属性数据。

              无法在多个线程中共享该数据。加上静态,虽然实现了共享但是生命周期过长。

2.如果一个类明确了自己的父类,那么很遗憾,它就不可以在继承Thread

              因为java不允许类的多继承。

二:实现Runnable接口:

       步骤:

1.定义类实现Runnable接口。

2.覆盖接口中的run方法,将多线程要运行的代码定义在方法中。

3.通过Thread类创建线程对象,并将实现了Runnable接口的子类对象

                     作为实际参数传递给Thread类的构造函数。

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

              是因为线程对象在建立时,必须要明确自己要运行的run方法,而这个run方法

              定义在了Runnable接口的子类中,所以要将该run方法所属的对象传递给Thread类的构造函数。

              让线程对象一建立,就知道运行哪个run方法。

4.调用Thread类中的start方法,开启线程,并执行Runanble接口子类中的run方法。

       特点:

1.描述事物的类中封装了属性和行为,如果有部分代码需要被多线程所执行。

              同时还在操作属性。那么可以通过实现Runnable接口的方式。

              因为该方式是定义一个Runnable接口的子类对象,可以被多个线程所操作

              实现了数据的共享。

2.实现了Runnable接口的好处,避免了单继承的局限性。

              也就说,一个类如果已经有了自己的父类是不可以继承Thread类的。

              但是该类中还有需要被多线程执行的代码。这时就可以通过在该类上功能扩展的形式。

              实现一个Runnable接口。

所以在创建线程时,建议使用第二种方式。

3章:线程安全问题

3.1线程安全问题出现的原因

线程安全问题:因为线程执行的随机性,有可能会导致多线程在操作数据时发生数据错误的情况产生。

分析下面这个代码,理论上是存在线程安全的问题的,卖出去的票可能大于100张,代码如图3.1所示。

3.1 线程代码

如果电脑开的程序比较多,出现问题的概率就比较大,我们现在开的程序少,还没出现这个现象

下面模拟一下,如图3.2所示。

在代码中让程序睡一会。调用sleep,因为sleep抛出的有异常,所以需要在这进行处理,只能try catch,不能throws,因为我们这个类实现了runnable接口,runnable接口中的run方法并没有向外抛出异常。

3.2 线程代码

这时就出现了线程安全的问题。打印出来的票号有0和负数,并且票的张数也超过了100张。

线程安全问题产生的原因:

当线程中多条代码在操作同一个共享数据时,一个线程将部分代码执行完,还没有基础执行其他代码时,被另一个线程获取到了CPU执行权,这时,共享数据操作就有可能出现数据错误。

简答说:多条操作数据的代码被多个线程分来执行造成的。

在我们这个案例里面就是判断和--操作被多个线程分开执行了,

安全问题涉及的内容:

1. 共享数据

2. 是否被多条语句操作

这也是判断多线程程序是否存在安全隐患的依据。

注意:下面这两个操作没有被一个线程执行完,而是被多线程分开来执行了,这样就容易引发线程安全问题。如图3.3所示。

3.3 代码

3.2 同步代码块-synchronized

如何解决这个线程安全问题呢?

java中提供了一个同步机制,解决的原理是让多条操作共享数据的代码在某一时间段,被一个线程执行完,在执行过程中,其他线程不可以参与运算。

同步的格式看下面,这个代码块可以保证一次只有一个线程在里面执行。

里面需要一个对象,这个对象可以是任意对象,就算是new 一个类也可以,但是我还需要定义这个类,比较麻烦,所以可以直接在这个类里面new一个Object

同步的格式(同步代码块)

synchronized(对象){ // 该对象可以是任意对象

需要被同步的代码;

}

代码案例如图3.4所示。

3.4 代码

这样改完之后,再执行,就不会出现负号票了。

3.3 线程同步的原理

同步代码块到底是如何解决线程安全问题的呢?

看这段代码,如图3.5所示,假设有四个线程会执行,第一个线程过来之后,执行到synchronized代码,在这里我们为了方便理解,可以吧obj认为是只有01的两个状态,当第一个线程过来的时候,判断obj的值,如果是1,则向下执行,在向下执行的时候会把这个值改为0,这样其他线程过来的话就进不来这个代码块了,这样我的第一个线程就继续向下执行,当执行到最后的时候再把obj的值从0改为1,这个时候其他线程才可以进入这个代码块。

3.5 代码

举个例子,火车上的卫生间。

你去上厕所的时候会看一下里面是否有人,如果没人,直接进去把门反锁上,这样就显示厕所有人了,其他人就进不来了。

其实刚才我们说的obj就相当于是一把锁。

谁执行到这个同步,就持有这把锁,谁执行完了就释放这个锁。

同步的原理:

通过一个对象锁,将多条操作共享数据的代码进行了封装并加锁。这样只有持有这个锁的线程才能操作同步中的代码

在这个线程执行期间,即使其他线程获得了执行权,因为没有获得锁,就只能在同步代码块外面等

只有当同步中的线程执行完同步代码块中的代码,才会释放这个锁,这个时候其他线程才有机会去获取这个锁

并只能有一个线程获取到锁而且进入到同步中。

同步的好处:

同步的出现解决了多线程的安全问题。

同步的弊端:

因为多个线程每次都要判断这个锁,所以效率会降低。

以后我们在写同步代码的时候会发现一个问题,如果出现了安全问题,加入了同步,安全问题依然存在,因为同步是有前提的,一定要确认是哪块的问题;

同步前提:

1. 同步需要两个或者两个以上的线程

2. 多个线程使用的是同一个锁

未满足这两个条件,不能称其为同步。

如果出现了加上同步代码 安全问题依然存在的情况,就按照这两个前提来排查问题。

注意:

同步前提里面的1:如果单线程也使用同步的话,这样既不存在安全性,效率还低。

同步前提里面的2:如果一个线程使用A锁,一个线程使用B锁,这样的话和不使用锁没什么区别

注意:这种写法是错误的,相当于给每一个线程都使用一个不同的锁,所以输出结果还是有负数,如图3.6所示。

3.6 代码

3.4 线程同步的另一种体现-同步函数

看这个例子

有两个储户,到同一个银行存钱,每次存100,存3次,两个储户是随机存入的。

银行有一个金库,提供一个存钱的功能。

代码实现如图3.7所示。

3.7  银行存款

这样实现的话,打印的是100 200 300 ,没有出现600.

因为new Bank是在run方法内部调用的,这样两次调用就会创建两个bank对象,所以需要把这个对象提到run方法外面,和run方法平级。

Cus类改成这样,如图3.8所示。

3.8 代码

改过之后看看代码有没有线程安全问题。

根据线程安全的判断原则,有共享变量,有多个线程操作。所以是存在的,

在这个代码的位置添加sleep,演示下效果。如图3.9所示。

3.9 代码

执行的效果如下图3.10所示。

3.10 执行的结果

分析下为什么没打印100,因为线程1过来的时候sum变成了100,线程1休息一会,线程2过来,sum就变成了200,最后线程1和线程2都打印的是200

所以,我们发现sum是共享数据,有两条语句在操作这个共享数据,如果这两条语句被多个线程分开执行,也就是一个线程没有执行完,其他线程就参与执行了,就容易发生线程安全问题。

解决办法:加入同步机制,将需要被一个线程一次执行完的代码存储到同步代码块中。

那么使用前面学习的synchroized代码块,代码如图3.11所示。

3.11 代码

注意:Cus类中的for循环中的x是不涉及线程安全问题的,他是一个局部变量。

在这里我们发现,同步代码块是用于封装代码的,而函数也是用来封装代码的,所不同之处是同步带有锁机制。那么如果让函数具备同步的特性,不就可以取代同步代码块了吗

怎么让函数具备同步性呢?

其实很简单,只要在函数上加上一个同步关键字修饰即可,这就是同步的另一个体现形式,同步函数。代码如图3.12所示。

3.12 同步函数

3.5 同步函数使用的锁

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

修改前面卖票的代码,如图3.13所示。

发现卖的票重复了,因为现在用的锁不是同一个。

3.13 代码

把同步代码块的锁换成this,验证一下效果。如图3.14所示。

3.14 代码

将同步代码块的锁换成this.发现同步安全问题解决了,所以可以确认同步函数使用的同步锁是this

同步函数和同步代码块的区别:

同步代码块使用的锁可以是任意对象

同步函数使用的锁是固定对象 this

所以一般定义同步时,建议使用同步代码块。如果锁对象可以使用this,那么就可以使用同步函数。

3.6 单例设计模式之懒汉式的多线程操作

单例模式我们前面讲了有两种实现形式,一种是饿汉式、一种是懒汉式,如图3.15所示。

3.15 单例模式

针对第二种懒汉式这种形式,当多个线程并发执行getInstance方法时,容易发生线程安全问题,因为s是共享数据,有多条语句在操作共享数据。

解决方式很简单,只要让getInstance方法具备同步性即可,如图3.16所示。

3.16 懒汉式

这样虽然解决了线程安全问题,但是多个线程每一次获取该实例都要调用这个方法,这样效率会比较低。为了保证安全,同时提高效率,可以通过双重判断的形式来完成,其实就是减少线程判断锁的次数。如图3.17所示。

通过双重判断来提高效率,当后续执行到第一个判断语句的时候,就会发现s!=null,这个时候就不需要再判断同步锁的代码了。

3.17 懒汉式代码

4章:线程池

4.1 线程池简介

多线程的异步执行方式,虽然能够最大限度发挥多核计算机的计算能力,但是如果不加控制,反而会对系统造成负担。线程本身也要占用内存空间,大量的线程会占用内存资源并且可能会导致Out of Memory。即便没有这样的情况,大量的线程回收也会给GC带来很大的压力。

为了避免重复的创建线程,线程池的出现可以让线程进行复用。通俗点讲,当有工作来,就会向线程池拿一个线程,当工作完成后,并不是直接关闭线程,而是将这个线程归还给线程池供其他任务使用。

4.2 常用的线程池

java中提供的线程池大致有下面这4种:

1. newFixedThreadPool

2. newSingleThreadExecutor

3. newCachedThreadPool

4. newScheduledThreadPool

其中常用的是newFixedThreadPool,在这里我们就以这个为例进行分析演示。

固定大小的线程池,可以指定线程池的大小,该线程池中的线程数量始终不变,当有新任务提交时,线程池中有空闲线程则会立即执行,如果没有,则会暂存到阻塞队列。对于固定大小的线程池,不存在线程数量的变化。缺点是在线程池空闲时,即线程池中没有可运行任务时,它也不会释放工作线程,还会占用一定的系统资源。

代码案例如图4.1所示。

4.1 线程池代码

4.3 如何选择线程池数量

线程池的大小决定着系统的性能,过大或者过小的线程池数量都无法发挥最优的系统性能。

当然线程池的大小也不需要做的太过于精确,只需要避免过大和过小的情况。一般来说,确定线程池的大小需要考虑CPU的数量,内存大小,任务是计算密集型还是IO密集型等因素

NCPU = CPU的数量

UCPU = 期望对CPU的使用率 0 UCPU 1

W/C = 等待时间与计算时间的比率

如果希望处理器达到理想的使用率,那么线程池的最优大小为:

线程池大小:Nthreads=Ncpu * Ucpu * (1+W/C)

下面分析一下IO密集型的任务下线程池大小的设置:

一般情况下,如果存在IO,那么肯定w/c>1(阻塞耗时一般都是计算耗时的很多倍),但是需要考虑系统内存有限(每开启一个线程都需要内存空间),这里需要上服务器测试具体多少个线程数适合(CPU占比、线程数、总耗时、内存消耗)。如果不想去测试,保守点取1即,Nthreads=Ncpu*1*(1+1)=2Ncpu。这样设置一般都OK

针对计算密集型的任务下线程池大小的设置:

假设没有等待,w=0,则W/C=0. Nthreads=Ncpu

总结:

IO密集型=2Ncpu(可以测试后自己控制大小,2Ncpu一般没问题,其实在实际中可以把这个值适当调大一些)(常出现于线程中:数据库数据交互、网络数据传输、文件处理、网络爬虫等等)

计算密集型=Ncpu(常出现于线程中:复杂算法)

对于计算密集型的任务,在拥有N个处理器的系统上,当线程池的大小为N+1时,通常能实现最优的效率。即使当计算密集型的线程偶尔由于缺失故障或者其他原因而暂停时,这个额外的线程也能确保CPU的时钟周期不会被浪费。

java中:int Ncpu = Runtime.getRuntime().availableProcessors();

    转藏 分享 献花(0

    0条评论

    发表

    请遵守用户 评论公约

    类似文章 更多