多线程简单总结

来源:互联网 发布:数据恢复高级技术pdf 编辑:程序博客网 时间:2024/06/15 15:06

.JAVA多线程实现方式主要有三种:继承Thread类、实现Runnable接口、使用ExecutorService、Callable、Future实现有返回结果的多线程。其中前两种方式线程执行完后都没有返回值,只有最后一种是带返回值的。

   

二.

  • 多线程:指的是这个程序(一个进程)运行时产生了不止一个线程,是程序执行的最小单元
  • 并行与并发:
    • 并行:多个cpu实例或者多台机器同时执行一段处理逻辑,是真正的同时。
    • 并发:通过cpu调度算法,让用户看上去同时执行,实际上从cpu操作层面不是真正的同时。并发往往在场景中有公用的资源,那么针对这个公用的资源往往产生瓶颈,我们会用TPS或者QPS来反应这个系统的处理能力。
                                
   并发与并行
  • 线程安全:经常用来描绘一段代码。指在并发的情况之下,该代码经过多线程使用,线程的调度顺序不影响任何结果。这个时候使用多线程,我们只需要关注系统的内存,cpu是不是够用即可。反过来,线程不安全就意味着线程的调度顺序会影响最终结果,如不加事务的转账代码:
    void transferMoney(User from, User to, float amount){  to.setMoney(to.getBalance() + amount);  from.setMoney(from.getBalance() - amount);}
  • 同步:Java中的同步指的是通过人为的控制和调度,保证共享资源的多线程访问成为线程安全,来保证结果的准确。如上面的代码简单加入@synchronized关键字。在保证结果准确的同时,提高性能,才是优秀的程序。线程安全的优先级高于性能。

 多线程优势:

1)某个操作可能会陷入长时间的等待。采用多线程,当一个线程等待的时候,可执行其他线程,充分利用cpu。

2)某操作(如计算)可能会消耗大量时间,而导致和用户之间的交互中断。多线程可让一个线程负责计算,另一线程负责交互。

3)软件本身就要求并发操作,如多端下载软件

4)多核cpu,本身就具备同时执行多个线程的能力

线程访问权限

 一般来说线程能访问进程内存中的所有数据,但实际应用中线程也有自己的空间
1)栈(可能被其他进程访问,但仍可认为是私有数据)

2)线程局部存储,一般只有很小容量

3)寄存器(包括PC寄存器)


线程状态

线程状态转换


各种状态一目了然,值得一提的是"blocked"这个状态:
线程在Running的过程中可能会遇到阻塞(Blocked)情况

  1. 调用join()和sleep()方法,sleep()时间结束或被打断,join()中断,IO完成都会回到Runnable状态,等待JVM的调度。
  2. 调用wait(),使该线程处于等待池(wait blocked pool),直到notify()/notifyAll(),线程被唤醒被放到锁定池(lock blocked pool ),释放同步锁使线程回到可运行状态(Runnable)
  3. 对Running状态的线程加同步锁(Synchronized)使其进入(lock blocked pool ),同步锁被释放进入可运行状态(Runnable)。

此外,在runnable状态的线程是处于被调度的线程,此时的调度顺序是不一定的。Thread类中的yield方法可以让一个running状态的线程转入runnable。


三. 线程安全

多线程并发时,在访问数据方面会出现一些问题。特别是当多个线程访问同一个变量的时候。

下面将用一个例子来说明可能出现的问题:

线程A、线程B都对变量X进行操作,操作顺序如下:

1)线程A对X赋值

2)线程A对X自加

3)线程B使用X的值(比如说把它赋值给另一个变量)

代码经过编译之后,在处理器中执行代码时,通常一个很简单的运算(如自家运算)都会被分为多个步骤执行(指令流水)。

比如当线程A对X自加运算的时候,编译后的自加运算共分为三步,当没有执行完这三步的时候,可能线程A就会被切出(比如说有需要即时响应的操作发生)。也就是说线程A在对数据还没处理完全的时候被切出了,这样当线程B执行的时候,使用的X值将不是我们期望的值,显然,发生了错误。

解决策略:

上面问题的出现本质上是因为一个不应该被打断的操作被强行中断了,那么有一种解决的办法就是设置一种规定一些操作,在执行的时候不能被中断。这样就避免了操作还没完成就被换出的情况。把这些简单的操作称为原子的操作。windows中对于这种操作也有支持。

但是这种策略只适用于简单的情况。对于复杂的情况,我们用一种称为同步与锁的机制来实现。

同步与锁

简单的说,就是在一个线程对数据访问结束之前,其他线程不能对这个数据进行访问。这样的话,对数据的访问就原子化了。

这种机制的实现也很简单:每个线程对数据访问的时候都会尝试获取锁,当访问结束后释放。在获取锁的时候如果有线程在访问数据,就会获取失败,这时候线程会等待, 直到访问数据的那个线程释放锁。

二元信号量

是最简单的一种锁,它只有两个状态:占用与非占用。它用于只能被一个线程访问的资源。只有资源状态为非占用 的时候,才能被线程获取,获取之后修改资源状态为占用,访问结束后修改资源状态为非占用。

信号量

稍微复杂一些,他适用于可以被多个线程同时访问的资源。一个初始值为N=n的信号量能被n个线程同时访问。当要访问数据的时候,先查看N值,(N的值代表还有多少个线程能访问资源)如果N值大于0,该线程能访问资源,线程进入后把N值减一。当访问结束后N值+1.

如果信号量的值小于0,则进入等待状态。

互斥量

和二元信号相似,资源只能被一个线程访问,但是同一个信号量只能被获取该信号量的线程释放,也就是说对于二元信号量,同一个互斥量可以被别的(任意)线程释放。相对二元信号量来说更严格了。

临界区

是比互斥量更严格的同步手段。临界区和上面的区别在于,互斥量和信号量在任何进程都是可见的,也就是说,一个进程创建了互斥量和信号量,在其他进程都是可见的,而临界区的作用范围仅限于本进程。

读写锁

读写锁用于更加通用的场合。对于同一个数据,多个进程同时读是没问题的,但是如果有线程要对数据进行修改,就要使用同步手段来避免出错。对于同一个读写锁,有两种获取方式:

1、共享的,对数据只进行读操作,可以多个线程同时进行

2、独占的。会修改数据,在修改完成之前,不能有其他线程操作数据

当锁处于自由状态时,以任何一种方式获取锁都会成功,并将锁至于相应的状态。

如果锁处于共享状态,那么其他与以共享方式获取锁的线程都能成功,共同读数据。

对于独占式获取的,则要等到以共享方式获取的所有线程释放后(锁重新回到自由状态)才能获取。并且对于以独占方式获取的锁,其他任何对锁的请求都不会成功。

条件变量

条件变量类似于一个发令枪,可以有多个线程等待枪响,枪响的时候,这些等待枪响的线程会同时恢复执行。发令枪何时响也可由线程来决定。

也就是说,条件变量可以让多个线程等待某件事的发生,当时间发生时(条件变量被唤醒),所有的线程可以一起恢复执行。


四.线程调度

最好的情况是:当处理器数量大于要处理的线程数目的时候,所有线程都可以同时执行。实现真正意义上的并发。

而这种情况在现实中基本不可能。现实中的并发只是一种模拟出来的状态,特别是在单核处理对于多线程的时候。它通过让多个线程交替执行,每个线程执行很短时间,从表面上看,这些线程同时执行,实现并发。

每个线程都想被执行,但是每次执行的线程数量是有限的,所以就要有一种方法来从众多的线程中选出要执行的线程,现在讨论下单核的情况,多核的类似。

在操作系统中有专门的线程调度算法来实现,下面列几个简单的“调度算法”

1、“先进先出”策略。所有的线程组成一个队列,新生的线程加入到队列末尾,每次取队头执行。有一个缺陷就是,如果新生成的线程是紧急操作,需要操作系统尽快相应,这种调度方法就不能满足了。

2、按优先级调度。每个线程都有自己的优先级,并且是可以被操作系统修改的,调度时候每次选取优先级最高的执行。这种方法弥补了上一种方法的缺陷。对于需要及时相应的紧急事件,可以给他一个高优先级,这样就能在下次被调度。然而这种方法也有一个问题,也就是所谓的饥饿。如果某线程“看似”无关紧要,被给予一个低得优先级,以后每次产生的线程优先级都比他高,那么这个线程会一直得不到执行,成为饿死。一个解决的办法是随之事件的推移而提升线程的优先级。这样只要事件足够长,低优先级的线程也会获得高优先级而被执行。

从上面的分析来开,线程似乎有两个状态:执行和不执行(等待)。其实操作系统中的每个线程都对应三个状态。