写代码啦
多线程初步
回复数(0) 浏览数(25)
{{topic.upvote_count || 0}} 编辑 回复

线程

线程是操作系统能够进行运算调度的最小单位,它被包含在进程之中,是进程中的实际运作单位。

线程属性

线程如果看作是人的话,也有自己的名字等等。
* ID: 线程ID是唯一身份标识,用于JVM执行过程中,识别线程。独一无二的,可类比为人的指纹。方法栈中可以看到,也可以使用getId()获取。
* 线程名:不同的线程可以有相同的名字,和人的名字一样你可以叫张三,我也可以,大家都可以。设置线程名的方法有两种:
* 在创建新线程的时候赋值,不设置有默认名称
* 使用setName()赋值
* 线程优先级:线程的优先级分为1-10,10为最高级别,1为最低级别,5为默认级别。学习好的同学比较容易得到老师的青睐。可以使用set get方法进行操作。
* 新建的线程优先级和当前线程优先级相同
* 当有多个线程就绪时,优先级高的线程先获得处理器资源,与执行顺序无关
* 守护线程:守护线程又称为后台线程,精灵线程,典型的守护线程就是垃圾回收线程,如果进程中没有非守护线程了,则守护线程自动销毁。守护线程的存在就是服务于其他线程。

线程状态和生命周期

传统线程模型的有3种或5种状态两种,最新的线程模型有6种状态。本质上区别不大,只是拆解和组合方式不太一样。不同的状态连在一起就是其生命周期:
1. new : 创建新线程,还未调用start()
2. runnable: java中将就绪(ready)和运行中(running)统称为“运行”
线程对象被创建后,其他线程调用其start()方法后,其就位于可运行线程池中,等待被线程调度选取,获取CPU的使用权,此时处于就绪(ready)状态。ready状态的线程在获得CPU时间片后变为运行中状态(running)
3. block :通常与锁有关,可能是获取锁控制权“未遂”
4. waiting : 该线程需要其他线程做出一些特定动作(通知notify(), notifyall()/或者中断jion等),不然就保持等待状态
5. timed_waiting: 可以规定指定时间后自行返回,结束wait()动作
6. teminated: 该线程已执行完毕

顺带一提的是线程的方法栈和main的方法栈有所不同,main的起点就是main的方法,但是线程起点是thread中的run()方法。
只有调用了start()方法,才会表现出多线程的特性,不同线程的run()方法里面的代码交替执行。如果只是调用run()方法,必须等待一个线程的run()执行完毕之后,才开始其他进程。

 线程六种状态 线程六种状态

为什么需要多线程

在默认情况下,都是在main单线程运行不同的java任务,顺序执行,选择,循环等。单线程的逻辑就像普通人使用惯用手去写字一样,自然且容易理解。
但是和人脑不一样的是,CPU(多核)不仅能够左手画圆右手画方,而且写字的速度远高于提供纸的速度。
CPU处理的速度基准远远快于其他处理数据的单位,包括内存,磁盘,网络,并且现在CPU大多是多核的。
运行的阻塞,导致CPU性能被大大的浪费。

多线程的优势

  1. 线程在程序中是独立的,并发的执行流,且线程间的隔离程度小于进程之间。它们共享内存,文件句柄和其他每个进程应有的状态
  2. Java语言内置多线程功能支持,而不是单纯地作为底层操作系统的调度方式,从而简化了Java的多线程编程
  3. 现代CPU有多个核心,理论上本身就具有多任务同时工作的天赋
  4. 多线程便于建模,合理拆解分块大任务,再分配到不同线程有利于建立处理模型

多线程的适用场景

多线程当然不是万能的,对于不同的应用场景作用效果也不同。JAVA抽象作业大致可分为两种:
* CPU密集型: 像是渲染解压,编解码等,不停的进行运算。不适用多线程,使用多线程对其提升有限。
* IO密集型:一是网络IO型,像数据库操作,浏览等,二是文件IO,包括读写文件等。这两种都涉及数据流在元件间的传递,其时间对于CPU来说是极其漫长的,可以使用多线程提升效率。

多线程带来的问题

多线程在带来便利和性能提升时也带来了新问题。而其中大部分问题都是由共享变量引起的。

单核CPU多线程处理问题 单核CPU多线程处理问题
单核CPU看起来是多个线程同步进行,其实是像闪电侠一样在不同线程上穿梭,每个线程被占用的时间是指定的,到确定时间后就终止当前线程换成另一个线程工作。多核CPU可以真正实现同时执行多个线程的作业。
在多线程中,某一时刻只能被一个线程操作的事情称为原子操作。但是对非原子共享变量的多线程会造成数据返回的混乱。
像是i++,if-then-to都可能造成线程的不安全性。因为取值,判断,操作,返回是分步的。
竞争条件带来的数据错误 竞争条件带来的数据错误

线程安全

多线程的安全体现在代码在多线程和单线程情况下永远得到相同的结果。为了保证线程安全,可以参考以下几个方面:
1. 不可变类的应用
String、Integer、Long这些,都是final类型的类,任何一个线程都改变不了它们的值,要改变除非新创建一个,因此这些不可变对象不需要任何同步手段就可以直接在多线程环境下使用
2. synchronized
任何对象都可以作为锁,使用同步块可以获取锁控制权的资源,保证同一时刻只有一个进程在运行这段代码
3. 使用JUC包
位于java.util.concurrent,其中的模型都是满足线程安全的
4. ThreadLocal
在每个Thread里面维护了一个以开地址法实现的ThreadLocal.ThreadLocalMap,把数据进行隔离,数据不共享,没有线程安全问题

对于所有用到的类,都可以在源码或网络上找到是否线程安全。非强调的常用类都不是线程安全的,但是也有应对方法。
Collection可以使用Collection.synchronized使其线程安全,原理就是对list,map其中的所有方法使用同步块包裹使其所有方法调用起来同步。

为什么需要线程池

线程是昂贵的,这是java线程模型的缺陷,完全依赖于操作系统的调度,每个线程都要占有操作系统一定资源。
使用线程池避免频繁地创建和销毁线程,达到线程对象的重用。另外,使用线程池还可以根据项目灵活地控制并发的数目。

Callable/Future

多线程相比较于单线程是复杂的,某线程是否执行,执行到什么位置,执行了多久等等,都只能等待线程执行完毕获取结果。而Callable/Future相比较于Runnable可以在多线程运行的时候获取异步运行的结果。

小结

多线程真的很难,我个人认为最难的部分在于思维上的反直觉。因为我们的思维是单线的,即使知道了这些逻辑上的原理,也无法一开始就能习惯于其逻辑,也就不免踩坑。
这些可能连多线程体系的冰山一角都没有,一方面是知识储备问题,另一方面写在一篇博客中不能展示其树状的框架。

{{topic.upvote_count || 0}}

线程

线程是操作系统能够进行运算调度的最小单位,它被包含在进程之中,是进程中的实际运作单位。

线程属性

线程如果看作是人的话,也有自己的名字等等。
* ID: 线程ID是唯一身份标识,用于JVM执行过程中,识别线程。独一无二的,可类比为人的指纹。方法栈中可以看到,也可以使用getId()获取。
* 线程名:不同的线程可以有相同的名字,和人的名字一样你可以叫张三,我也可以,大家都可以。设置线程名的方法有两种:
* 在创建新线程的时候赋值,不设置有默认名称
* 使用setName()赋值
* 线程优先级:线程的优先级分为1-10,10为最高级别,1为最低级别,5为默认级别。学习好的同学比较容易得到老师的青睐。可以使用set get方法进行操作。
* 新建的线程优先级和当前线程优先级相同
* 当有多个线程就绪时,优先级高的线程先获得处理器资源,与执行顺序无关
* 守护线程:守护线程又称为后台线程,精灵线程,典型的守护线程就是垃圾回收线程,如果进程中没有非守护线程了,则守护线程自动销毁。守护线程的存在就是服务于其他线程。

线程状态和生命周期

传统线程模型的有3种或5种状态两种,最新的线程模型有6种状态。本质上区别不大,只是拆解和组合方式不太一样。不同的状态连在一起就是其生命周期:
1. new : 创建新线程,还未调用start()
2. runnable: java中将就绪(ready)和运行中(running)统称为“运行”
线程对象被创建后,其他线程调用其start()方法后,其就位于可运行线程池中,等待被线程调度选取,获取CPU的使用权,此时处于就绪(ready)状态。ready状态的线程在获得CPU时间片后变为运行中状态(running)
3. block :通常与锁有关,可能是获取锁控制权“未遂”
4. waiting : 该线程需要其他线程做出一些特定动作(通知notify(), notifyall()/或者中断jion等),不然就保持等待状态
5. timed_waiting: 可以规定指定时间后自行返回,结束wait()动作
6. teminated: 该线程已执行完毕

顺带一提的是线程的方法栈和main的方法栈有所不同,main的起点就是main的方法,但是线程起点是thread中的run()方法。
只有调用了start()方法,才会表现出多线程的特性,不同线程的run()方法里面的代码交替执行。如果只是调用run()方法,必须等待一个线程的run()执行完毕之后,才开始其他进程。

 线程六种状态 线程六种状态

为什么需要多线程

在默认情况下,都是在main单线程运行不同的java任务,顺序执行,选择,循环等。单线程的逻辑就像普通人使用惯用手去写字一样,自然且容易理解。
但是和人脑不一样的是,CPU(多核)不仅能够左手画圆右手画方,而且写字的速度远高于提供纸的速度。
CPU处理的速度基准远远快于其他处理数据的单位,包括内存,磁盘,网络,并且现在CPU大多是多核的。
运行的阻塞,导致CPU性能被大大的浪费。

多线程的优势

  1. 线程在程序中是独立的,并发的执行流,且线程间的隔离程度小于进程之间。它们共享内存,文件句柄和其他每个进程应有的状态
  2. Java语言内置多线程功能支持,而不是单纯地作为底层操作系统的调度方式,从而简化了Java的多线程编程
  3. 现代CPU有多个核心,理论上本身就具有多任务同时工作的天赋
  4. 多线程便于建模,合理拆解分块大任务,再分配到不同线程有利于建立处理模型

多线程的适用场景

多线程当然不是万能的,对于不同的应用场景作用效果也不同。JAVA抽象作业大致可分为两种:
* CPU密集型: 像是渲染解压,编解码等,不停的进行运算。不适用多线程,使用多线程对其提升有限。
* IO密集型:一是网络IO型,像数据库操作,浏览等,二是文件IO,包括读写文件等。这两种都涉及数据流在元件间的传递,其时间对于CPU来说是极其漫长的,可以使用多线程提升效率。

多线程带来的问题

多线程在带来便利和性能提升时也带来了新问题。而其中大部分问题都是由共享变量引起的。

单核CPU多线程处理问题 单核CPU多线程处理问题
单核CPU看起来是多个线程同步进行,其实是像闪电侠一样在不同线程上穿梭,每个线程被占用的时间是指定的,到确定时间后就终止当前线程换成另一个线程工作。多核CPU可以真正实现同时执行多个线程的作业。
在多线程中,某一时刻只能被一个线程操作的事情称为原子操作。但是对非原子共享变量的多线程会造成数据返回的混乱。
像是i++,if-then-to都可能造成线程的不安全性。因为取值,判断,操作,返回是分步的。
竞争条件带来的数据错误 竞争条件带来的数据错误

线程安全

多线程的安全体现在代码在多线程和单线程情况下永远得到相同的结果。为了保证线程安全,可以参考以下几个方面:
1. 不可变类的应用
String、Integer、Long这些,都是final类型的类,任何一个线程都改变不了它们的值,要改变除非新创建一个,因此这些不可变对象不需要任何同步手段就可以直接在多线程环境下使用
2. synchronized
任何对象都可以作为锁,使用同步块可以获取锁控制权的资源,保证同一时刻只有一个进程在运行这段代码
3. 使用JUC包
位于java.util.concurrent,其中的模型都是满足线程安全的
4. ThreadLocal
在每个Thread里面维护了一个以开地址法实现的ThreadLocal.ThreadLocalMap,把数据进行隔离,数据不共享,没有线程安全问题

对于所有用到的类,都可以在源码或网络上找到是否线程安全。非强调的常用类都不是线程安全的,但是也有应对方法。
Collection可以使用Collection.synchronized使其线程安全,原理就是对list,map其中的所有方法使用同步块包裹使其所有方法调用起来同步。

为什么需要线程池

线程是昂贵的,这是java线程模型的缺陷,完全依赖于操作系统的调度,每个线程都要占有操作系统一定资源。
使用线程池避免频繁地创建和销毁线程,达到线程对象的重用。另外,使用线程池还可以根据项目灵活地控制并发的数目。

Callable/Future

多线程相比较于单线程是复杂的,某线程是否执行,执行到什么位置,执行了多久等等,都只能等待线程执行完毕获取结果。而Callable/Future相比较于Runnable可以在多线程运行的时候获取异步运行的结果。

小结

多线程真的很难,我个人认为最难的部分在于思维上的反直觉。因为我们的思维是单线的,即使知道了这些逻辑上的原理,也无法一开始就能习惯于其逻辑,也就不免踩坑。
这些可能连多线程体系的冰山一角都没有,一方面是知识储备问题,另一方面写在一篇博客中不能展示其树状的框架。

25
回复 编辑