多线程和并发问题

多线程

多线程就是指一个进程中同时有多个线程正在执行。多线程只是在同/异步角度上解决高并发问题的其中的一个方法手段,是在同一时刻利用计算机闲置资源的一种方式。多线程在高并发问题中的作用就是充分利用计算机资源,使计算机的资源在每一时刻都能达到最大的利用率,不至于浪费计算机资源使其闲置。
多线程出现的原因:

  • 为了解决负载均衡问题,充分利用CPU资源.
  • 为了提高CPU的使用率,采用多线程的方式去同时完成几件事情而不互相干扰.
  • 在一个程序中,有很多的操作是非常耗时的,如数据库读写操作,IO操作等,如果使用单线程,那么程序就必须等待这些操作执行完成之后才能执行其他操作。使用多线程,可以在将耗时任务放在后台继续执行的同时,同时执行其他操作。

多线程的好处:

  1. 充分利用系统的处理能力,提高系统的资源利用率。使用线程可以把占据时间长的程序中的任务放到后台去处理

  2. 提高系统响应性,即线程可以在运行现有任务的情况下立即开始处理新的任务。在一些等待的任务实现上,如用户输入,文件读取和网络收发数据等.

  3. 提高程序的运行性能

多线程的缺点:

  1. 如果有大量的线程,会影响性能,因为操作系统需要在它们之间切换.

  2. 更多的线程需要更多的内存空间

  3. 线程中止需要考虑对程序运行的影响.

  4. 通常块模型数据是在多个线程间共享的,需要防止线程死锁情况的发生

总结:多线程是异步的,但这不代表多线程真的是几个线程是在同时进行,实际上是系统不断地在各个线程之间来回的切换(因为系统切换的速度非常的快,所以给我们在同时运行的错觉)。

多线程通信:
多线程之间需要进行通信,线程的通信依赖共享内存线程方法的调用来实现。Java内存模型分为主内存和工作内存,通过内存之间的数据交换实现线程之间的通信;主动调用线程的wait()notify()方法也可以实现线程之间的通信。

多线程并发执行引发的问题:

  • 安全性问题:在单线程系统上正常运行的代码,在多线程环境中可能会出现意料之外的结果。
  • 活跃性问题:不正确的加锁、解锁方式可能会导致死锁or活锁问题。
  • 性能问题:多线程并发即多个线程切换运行,线程切换会有一定的消耗并且不正确的加锁。

补充: 如果你是写服务器端应用的,其实在现在的网络服务模型下,创建进程的开销是可以忽略不计的,因为现在一般流行的是按照 CPU 核心数量开进程或者线程,开完之后在数量上一直保持,进程与线程内部使用协程或者异步通信来处理多个并发连接,因而开进程与开线程的开销可以忽略了。
另外一种新的开销被提上日程:核心切换开销。 现代的体系,一般 CPU 会有多个核心,而多个核心可以同时运行多个不同的线程或者进程。当每个 CPU 核心运行一个进程的时候,由于每个进程的资源都独立,所以 CPU 核心之间切换的时候无需考虑上下文。 当每个 CPU 核心运行一个线程的时候,由于每个线程需要共享资源,所以这些资源必须从 CPU 的一个核心被复制到另外一个核心,才能继续运算,这占用了额外的开销。换句话说,在 CPU 为多核的情况下,多线程在性能上不如多进程。
因而,当前面向多核的服务器端编程中,需要习惯多进程而非多线程。

并发问题(安全性问题)

要编写线程安全的代码,核心在于对状态访问操作进行管理。特别是对共享的和可变的状态的访问

解决思路
当发生安全性问题时。有三种解决问题的角度:

  1. 不在线程之间共享变量
  2. 将状态变量修改为不可变
  3. 访问状态变量时使用同步机制。

前两种方式从根本上避免了多线程并发问题的原因:对共享和可变状态的访问。

1. 不在线程之间共享变量

即限制变量只能在单个线程中访问

实现方式:

  • 线程封闭:保证变量只能被一个线程可以访问到。可以通过Executors.newSingleThreadExecutor()实现。

  • 栈封闭:栈封闭即使用局部变量。局部变量只会存在于本地方法栈中,不能被其他线程访问,因此也就不会出现并发问题。所以如果可以使用局部变量就优先使用局部变量。

  • ThreadLocal封闭:ThreadLocal是Java提供的实现线程封闭的一种方式,ThreadLocal内部维护了一个Map,Map的key是各个线程,而Map的值就是要封闭的对象。每个线程中的对象都对应着Map中一个值,也就是ThreadLocal利用Map实现了对象的线程封闭。

2. 将状态变量修改为不可变

即使用不可变对象。

不可变对象:当一个对象构造完成后,其状态就不再变化,我们称这样的对象为不可变对象(Immutable Object),这些对象关联的类为不可变类(Immutable Class)。

比如Java中的String、Integer、Double、Long等所有原生类型的包装器类型,都是不可变的。

大多数时候,线程间是通过使用共享资源实现通信的。如果该共享资源诞生之后就完全不再变更(犹如一个常量),多线程间共同并发读取该共享资源是不会产生线程冲突的,因为所有线程无论何时读取该共享资源,总是能获取到一致的、完整的资源状态,这样也能规避多线程冲突。不可变对象就是这样一种诞生之后就完全不再变更的对象,该类对象天生支持在多线程间共享。

3. 使用同步机制

关注一个并发问题,有3个基本的关注点:

  1. 原子性,即一个操作或者多个操作 要么全部执行并且执行的过程不会被任何因素打断,要么就都不执行。(synchronized关键字/其他互斥锁——保证同一时间只能有一个线程访问加锁区域中的代码,即保证了原子性。)

  2. 可见性,当多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即看得到修改的值。(voliate关键字/synchronized关键字——每次修改变量后,立即将变量写会主内存;每次使用变量时,必须从主内存中同步变量的值。)

  3. 有序性,有序性指的是数据不相关的变量在并发的情况下,实际执行的结果和单线程的执行结果是一样的,不会因为重排序的问题导致结果不可预知。(保证多线程执行的串行顺序;volatile, final, synchronized,显式锁都可以保证有序性)

所有的并发问题的都可以从这三个点进行分析并针对性的进行解决。

补充:
活跃性问题包括但不限于死锁、活锁、饥饿等。

死锁:死锁发生在一个线程需要获取多个资源的时候,这时由于两个线程互相等待对方的资源而被阻塞,死锁是最常见的活跃性问题。

活锁:活锁指的是线程不断重复执行相同的操作,但每次操作的结果都是失败的。尽管这个问题不会阻塞线程,但是程序也无法继续执行。

饥饿:饥饿指的线程无法访问到它需要的资源而不能继续执行时,引发饥饿最常见资源就是CPU时钟周期。解决饥饿问题的最简单策略是FCFS资源分配策略(先来先服务)

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值