Thread基础之多线程和同步与互斥(锁)

6 篇文章 0 订阅

多进程和多线程

1. 多进程

现代化操作系统都是采用『多任务』和『分时』设计,从而诞生了『进程』的概念,而后又进一步诞生了『线程』的概念。

提示
从历史的时间线来看,先诞生的是『进程』概念,后诞生的『线程』概念。

通俗地说,『程序』是死的,『进程』是活的。进程就是活着的程序

  • 程序是在硬盘上,而进程是程序运行起来后的、在操作系统中的所有的相关内存数据和资源。

  • 电脑关机后,程序还在,但是进程就没了。

进程』是程序的一次动态执行的过程,它对应了从代码加载、执行至执行完毕的一个完整过程。这个过程也是进程从产生、发展至消亡的过程。

在操作系统的管控下,多个进程『轮流』使用 CPU 资源(和其他公共资源)

提示
通俗地说,站在 CPU 的角度而言,根本不存在所谓的同时。这一点很重要!

进程的特点:

  • 进程是操作系统运行程序的基本单元;

  • 每一个进程都有自己独立的一块内存空间和一组系统资源;

  • 每一个进程的内部数据和状态是完全独立的。

2. 进程的状态

进程是有状态的,进程的状态反映了此时此刻是操作系统中的哪个进程在使用 CPU 。

不同的操作系统因为各有特色、各有偏重,导致大家的就能成的状态的数量互有多少。但是所有的操作系统的进程的状态都是围绕如下的、核心三态进行设计、实现的。

thread-status

状态说明
Running
运行中
表示当前正式这个进程使用 CPU ,也就是说,此时此刻,这个程序的代码正在刷刷刷地执行着。对于单核 CPU 而言,一个进程中无论有多少线程,任意时刻有且仅有一个线程是处于该状态。同理,对于多核 CPU 而言,N 核 CPU 有且仅有 N 个线程处于这种状态。
Runnable
可运行
因为 CPU 是被进程轮流使用的,那么当下没有『轮到』的进程都是处于 Runnable 状态。通俗的说,这种状态就是『万事俱备,只欠东风』,这里的『东风』指的就是 CPU 。显而易见,任何时刻,大多数进程都是处于这种状态。
Blocked
阻塞状态
一个正在运行(Running)的进程因为某种原因代码逻辑无法再继续运行下去了,就进入阻塞状态。进入阻塞状态的线程会让出 CPU ,因为即便它强占 CPU 也无法再继续运行下去。当造成该线程阻塞的原因消除后,该线程将又有机会运行。通俗的说,这种状态就是『除了欠东风,还欠别的』,比如,获得用户的输入,但是用户又迟迟没有输入时。

在上述的核心三态之外,你可能还会经常听到、见到 2 种状态:

状态说明
Created
新生态
进程刚被创建出来的一段时间内,就是这种状态。
Zombie
僵尸态
进程在结束执行后的一段时间内,会是这种状态,直到操作系统回收为它分配的资源,将它彻底销毁。

以上就是经典操作系统原理中的 5 种进程状态。

3. 多线程

== 提示==
线程是更现代化的概念和技术(它的诞生晚于进程)

线程是进程中的一个单位,即,一个进程可以有多个线程(至少有一个)。简单来说,进程和线程的数量关系是 一对多 的关系

进程是操作系统分配资源的最小单位,一个进程下的所有线程共享这个进程的所有资源。其中最关键的资源就是『时间片』,即,使用 CPU 的资格和时长。

  • 进程下的某个线程是 running ,进程就是 running ;
  • 进程下的所有线程是 blocked ,进程就是 blocked ;
  • 进程下的 running 线程变成 blocked ,操作系统选择同进程下的 runnable 线程执行;
  • 进程的本轮时间片耗完时,进程的 running 线程会变为 runnable 。操作系统选择其它进程的某个 runnable 线程运行。

通俗地说,每一个线程都有自己的『历史使命』:执行一个方法。线程开始执行,即意味着执行这个方法的第一行代码;方法的最后一行代码结束后,线程则会被销毁。

每一个线程所要执行的那个方法就被称为线程的『执行方法』,也叫『入口方法』。

每一个进程天然、天生、自动拥有一个线程,这个线程以 main 方法作为自己的执行方法,因此,这个线程也被称为 main 线程,即,主线程。

创建并使用线程的过程可以分为 4 个步骤:

  1. 定义一个线程类,同时指明这个线程的执行方法

  2. 创建线程对象

  3. 启动线程

  4. 终止线程

定义一个线程类通常有两种方法:继承 java.lang.Thread 类和实现 java.lang.Runnable 接口。

Thread 类和 Runnable 接口

1. 使用 Thread 类创建线程

Java 提供了 java.lang.Thread 类支持多线程编程,该类提供了大量的方法来控制和操作线程。

#说明
run 方法直接调用线程的执行函数
start 方法启动线程
sleep 方法让线程休眠(即进入阻塞状态)指定 毫秒
getName 方法返回该线程的名称
getPriority 方法返回线程的优先级
setPriority 方法更改线程的优先级
getState 方法返回该线程的状态
isAlive 方法测试线程是否处于活动状态
join 方法等待该线程终止
interrupt 方法中断线程
yield 方法暂停正在执行的线程,并执行其他线程

使用继承 Thread 类的方式创建线程的实现步骤如下:

  1. 定义一个类去继承 Thread 类,重写 Thread#run 方法,在 Thread#run 方法中实现代码逻辑;
  2. 创建线程对象;
  3. 调用 Thread#start 方法启动线程。

2. 使用 Runnable 接口创建线程

由于 Java 只允许单继承,因此一旦一个类已有父类,那么就无法再继承 Thread 类,从而导致上述实现线程的方式无法使用。

使用 Runnable 接口创建线程能解决上述问题。

Runnable 接口声明了一个 Runnable#run 方法。任何一个类都可以通过实现 Runnable 接口并实现其 Runnable#run 方法来完成线程的所有活动。

使用实现 Runnable 接口的方式创建线程的实现步骤如下:

  1. 定义 Xxx 类并实现 java.lang.Runnable 接口,并实现它所声明的 run 方法;

  2. 创建线程对象;

  3. 调用 Runnable#start 方法启动线程。

线程的调度

1. 实现线程调度的方法

线程调度的实现核心思路只有一个:通过各种手段,迫使当前线程(即,Running 线程)进入 Blocked/Runnable 状态,让出 CPU , 从而让其它线程拥有执行机会

  • 手段一:Thread#join 方法

    Thread#join 方法会导致当前线程阻塞(让出 CPU),等待调用该方法的线程(即,Thread 对象所代表的那个线程)结束后再继续执行本线程。

  • 手段二:Thread.sleep 方法

    Thread.sleep 方法会导致当前线程睡眠(本质上也是阻塞,迫使当前线程让出 CPU),在指定时间到期后,重新进入可运行状态。

  • 手段三:Thread.yield 方法

    Thread.yield 方法稍微有点不同,它让当前线程让出 CPU ,但并不是进入阻塞状态,而是直接进入 Runnable 状态。

需要注意的是,当前线程让出 CPU 之后,接下来是哪个线程执行(从 Runnable 状态变为 Running 状态)带有『不确定性』。

2. 线程的同步与互斥

当两个或多个线程需要访问同一资源(或执行同一段代码时),需要某一时刻只能被一个线程使用的方式,称为线程『互斥』。

当两个或多个线程以互斥的方式访问完同一资源(或执行同一段代码)后,『通知』其他线程的方式,称为线程『同步』。

同步与互斥通常总是一起出现的。只出现互斥,不出现同步,意味着代码逻辑是一种极简单的多线程状况。

3. synchronized 关键字

使用 synchronized 关键字修饰的方法控制对类成员变量的访问。每个类实例都对应一把锁,方法一旦执行,就独占该锁,直到方法结束时才将锁释放;此后其它被阻塞的线程才能获得该锁,重新进入可执行状态。

这种机制保证了同一时刻,对于每一个实例,其所声明为 synchronized 的方法只能有一个处于可执行状态,从而有效地避免了类成员变量的访问冲突。

语法:

访问修饰符 synchronized 返回类型 方法名 () {
    ...
} 

synchronized 访问修饰符 返回类型 方法名 () {
    ...
} 

synchronized 方法的缺陷在于:如果将一个耗时的方法声明为 synchronized 将会使其它线程阻塞时间过长,从而影响系统执行效率和用户体验。

同步代码块是同步方法的缺陷的解决方案,它『锁住』的不是整个方法,而是方法中的一个代码片段。

语法:

synchronized (一个对象) {
    ...
}

这里的『一个对象』通常是一个字符串常量对象。

Java 并发 API 对『』提供了支持。锁是一些对象,它们为 synchronized 提供了替代方案。
(Lock) 不仅能实现 synchronized 的互斥功能,还能进一步实现线程间的同步功能。

锁的工作原理如下:在访问共享资源之前,申请用于保护资源的锁;当资源访问结束完成时,释放锁。当某个线程正在使用锁时,如果另一个线程尝试申请锁,那么后者将会阻塞等待,直到锁被前者释放位置。

提示
锁的作用,逻辑上,就是一个令牌、通行证。

持有这个令牌、通行证的线程才能继续执行。没有这个令牌、通行证的线程,无法继续执行,直到令牌的持有者放下令牌,而被它拿到后,它才能继续执行。

所有的锁都要实现 Lock 接口,最常用的 Lock 接口的实现类是 ReentrantLock

             lock(): 进行等待,直到可以获得锁为止      
lockInterruptibly(): 除非被中断,否则进行等待,直到可以获得锁为止   
     newCondition(): 返回与调用锁关联的 Condition 对象 
          tryLock(): 尝试获得锁,如果锁不可获得,立即返回 false;如果可获得,返回 true  
tryLock(wait, unit): 在指定时间内,尝试获得锁。如果超出时间后仍无法获得,则返回 false;
                     如果可获得,则返回 true 
           unlock(): 释放锁    

ReentrantLock 实现了一种可重入锁,当前持有锁的线程能够重复进入这种锁。当然,对于线程重入锁而言,所有 Lock#lock() 调用必须有相同数量的 Lock#unlock() 调用进行抵消。

更多关于锁、同步、互斥、线程安全的内容参看《Java 线程安全》章节。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值