04.关于线程你必须知道的8个问题(下)

简介: 大家好,我是王有志。今天是Java面试中线程问题的最后一部分内容,包括我们来聊同步与互斥,线程的本质,调度,死锁以及线程的优缺点等问题。

大家好,我是王有志,欢迎和我聊技术,聊漂泊在外的生活。快来加入我们的Java提桶跑路群:共同富裕的Java人

今天我们来学习线程中最后4个问题:

  • 线程的同步与互斥

  • 线程的本质与调度

  • 死锁的产生与解决

  • 多线程的是与非

通过本篇文章,你可以了解到计算机中经典的同步机制--管程Java线程的本质与调度方式,如何解决死锁问题,以及为什么要使用多线程。

线程的同步与互斥

首先来看线程同步线程互斥的概念,这里引用百度百科中的定义:

线程同步:

即当有一个线程在对内存进行操作时,其他线程都不可以对这个内存地址进行操作,直到该线程完成操作, 其他线程才能对该内存地址进行操作,而其他线程又处于等待状态,实现线程同步的方法有很多,临界区对象就是其中一种。

线程互斥:

线程互斥是指某一资源同时只允许一个访问者对其进行访问,具有唯一性和排它性。但互斥无法限制访问者对资源的访问顺序,即访问是无序的。

线程同步关注的是线程间的执行顺序,强调线程t2必须在线程t1执行完成后执行,是串行方式

线程互斥,关注的是不同线程对共享资源的使用方式,同一时间只允许一个线程访问共享资源,在共享资源的访问上是串行方式,而其它处理过程可以并发执行。

实现同步与互斥的方式有很多,比如:互斥锁,信号量和管程。Java 1.5前只提供了基于MESA管程思想实现的synchronized。之后,提供了JUC工具包,包含信号量,互斥锁等同步工具。

管程的思想

管程是由Hoare和Hansen提出的,最早用于解决操作系统进程间同步问题。Hansen首次在Pascal上实现了管程,Hoare证明了管程与信号量是等价的

管程的发展历史上,先后出现了3种管程模型:

  • Hansen管程,Hansen提出;
  • Hoare管程,Hoare提出;
  • MESA管程,施乐公司在MESA语言中实现。

这里不过多的涉及管程的内容,只举一个通俗的例子解释下管程的实现原理。最近大家都有好好的做核酸吧?

首先,大家(线程)从四面八方赶到核酸亭(并发执行),随后进入排队区(入口队列,串行执行),紧接着是身份识别(检查条件变量),最后进行核酸监测(操作共享变量),当下一个人看到你完成了核酸监测后,开始进行核酸检测(唤醒)。

图1:通俗的管程.png

Java中synchronized的底层正是借鉴了MESA管程的实现思想。应用层面,使用synchronizedObject.wait方法,来实现的同步机制也是管程的实现。这些会在synchronized的部分中详细解释。

线程的本质与调度

关于线程你必须知道的8个问题(中)中,我们看到了thread.cpp创建操作系统层面的线程,不过碍于篇幅没有继续往下追,今天我们来看下os_linux.cpp中是如何创建线程的:

bool os::create_thread(Thread* thread, ThreadType thr_type, size_t req_stack_size) {
  int ret = pthread_create(&tid, &attr, (void* (*)(void*)) thread_native_entry, thread);
  return true;
}

可以看到,是通过调用pthread_create来创建线程的,该方法是Linux的thread.h库中创建线程的方法,用来创建操作Linux的线程。

到这里你可能会有疑问,或者听到过这样的问题,Java的线程是用户线程还是内核线程?

早期Linux并不支持线程,但可以通过编程语言模拟实现“线程”,本质还是调用进程,这时创建的线程就是用户线程

2003年RedHat初步完成了NPTL(Native POSIX Thread Library)项目,通过轻量级进程实现了符合POSIX标准的线程,这时创建的线程就是内核线程

因此,如果不是跑在古董服务器上的项目的话,使用的Java线程都会映射到一个内核线程上

好了,你已经知道现代Java线程的本质是操作系统的内核线程,并且也知道了操作系统内核线程是通过轻量级进程实现的。所以,我们可以得到:Java线程≈操作系统内核线程≈操作系统轻量级进程。那么对于Java线程的调度方式来说就有:Java线程的调度方式≈操作系统进程的调度方式

恰好,Linux中使用了抢占式进程调度方式。因此,并不是JVM中实现了抢占式线程调度方式,而是Java使用了Linux的进程调度方式,Linux选择了抢占式进程调度方式

死锁的产生与解决

我们随便写个例子:

public static void main(String[] args) {
  String lock_a = "lock-a";
  String lock_b = "lock-b";
  ShareData lock_a_shareData = new ShareData(lock_a, lock_b);
  ShareData lock_b_shareData = new ShareData(lock_b, lock_a);
  new Thread(lock_a_shareData, "lock-a-thread").start();
  new Thread(lock_b_shareData, "lock-b-thread").start();
}

static class ShareData implements Runnable {
  private final String holdLock;
  private final String requestLock;
  public ShareData(String holdLock, String requestLock) {
    this.holdLock = holdLock;
    this.requestLock = requestLock;
  }

  @SneakyThrows
  @Override
  public void run() {
    synchronized (holdLock) { // 1
      System.out.println("线程:" + Thread.currentThread().getName() + ",持有:" + this.holdLock + ",尝试获取:" + this.requestLock);
      TimeUnit.SECONDS.sleep(3);
      synchronized (requestLock) { // 2
        System.out.println("成功获取!");
      }
    }
  }
}

lock_a_shareData持有lock_a,尝试请求lock_b,相反的lock_b_shareData持有lock_b,尝试请求lock_a,在它们互相都不放手的情况下,谁也无法请求成功,因此双双阻塞在那里。

通过上面的例子我们可以总结出死锁产生的4个条件:

  1. 代码1和代码2处添加了synchronized,保证只有持有对应锁的线程可以进入,这是互斥条件,锁只能被一个线程持有
  2. 代码1处持有锁不释放,并且在代码2处请求锁,这是保持和请求条件,保持自己的锁,并请求其它的锁
  3. 线程lock-a-thread和线程lock-b-thread只是在那里不断请求,并没有谁要求其它线程放弃,这是不剥夺条件,不抢夺其它线程已获取的锁,只能由其主动释放
  4. 线程lock-a-thread和线程lock-b-thread的持有与互相请求锁形成了一个环路,这是循环等待条件,多个线程间的资源请求形成了环路

知道了死锁产生的条件,那么解决的办法也就显而易见了。首先互斥条件是无法被打破的,因为本身的目的就是在此处形成互斥,避免并发造成的“意外”。那么我们可以尝试打破剩余的3个条件:

  • 通过一次性申请所有资源来打破保持和请求条件,增加Admin角色去统一管理资源的申请和释放;
  • 通过主动释放资源来打破不剥夺条件,既然不能主动抢,那主动释放总归是可以的吧?
  • 通过按照资源顺序申请来打破循环等待条件,每个资源由小到大依次编号,只有申请到编号较小的资源后才可以申请编号较大的资源。

Java中定位死锁

涉及到多线程的问题,往往具有难排查的特点,不过好在我们可以借助Java提供的工具。首先是通过jps,ps或者它工具确定Java程序的进程ID:

# Linux平台
jps -1

# window平台
.\jps

然后通过jstack查看线程的堆栈信息,确定“事故”:

# Linux平台
jstack <进程ID>

# window平台
.\stack <进程ID>

得到大致如下的信息(省略了非常多):

图2:线程堆栈信息.png

这个输出信息就非常明显了吧?虽然实际工作中,情况可能会更加复杂,但是大致思路是一样的:程序阻塞 -> 查看线程状态 -> 查看持有与等待情况 -> 查看问题代码

预防死锁

通常快速定位解决死锁问题,会在程序员中获得“技术大牛”的称赞,但质量效能部门会记一个大大的事故。为了避免这种情况,我们还是要多做预防工作。

首先是尽量避免使用多个锁,避免这种持有与请求的情况发生,如果必须要用多个锁,请保证多个锁的使用至少满足以下一种:

  • 线程按照特定顺序获取锁
  • 为每把锁添加超时时间,当然synchronized是没办法做到的。

另外也可以借助工具在上线前发现死锁问题,比如:FindBugs™

多线程的是与非

使用多线程的目的是什么?

无论是说多核处理器时代不用多线程就是浪费资源,还是说程序既要处理数据,又有IO操作,多线程可以在IO期间处理数据保证CPU的利用率,归根结底就是要提速

通常意义上,多线程确实会快于单线程。

Tips:《Java并发编程的艺术》中在章节“1.1.1 多线程一定快吗”给出了一个反例。我提供了这本书的电子版,有兴趣的可以去阅读。

我经常会和小伙伴聊到,引入一种技术,有利就会有弊,无论是技术选型还是架构设计,都是一门权衡的艺术。

那么引入多线程会带来什么问题?

显而易见的是编程难度的提升,人的思维是线性的,因此编程过程中也总是倾向于线性处理流程,在程序中编写代码的难度可想而知。

另外,《Java并发编程的艺术》中提到了上下文切换,死锁,以及资源限制的问题,这些大家都耳熟能详了,就不过多赘述了。

以上的问题我们都有解决办法或者可以忽略,并发编程中最大的挑战其实是线程安全问题带来的数据错误,比如,前公司的同事曾经使用了有状态的Spring单例Bean。

最后是额外的一点,如无必要,勿增实体,在可预见的未来(大约3年),如果业务发展并没有使用多线程的必要,那就遵循奥卡姆剃刀原理,选择最简单的解决方案。

结语

今天的内容其实都可以在操作系统的发展史中找到它们的影子,与其说是线程的问题不如说是多任务处理的问题。

文章中涉及到了一些操作系统的内容,尤其是在线程的同步与互斥线程的本质与调度中,最早写了3种管程模型,但写完发现文章奔着上万字去了,于是就删掉了这部分内容,尽量做到简短准确的表达。

关于线程的问题到这里就告一段落了,希望这3篇文章能够给你带来帮助。接下来我们从synchronizedvolatilefinal开始。


好了,今天就到这里了,Bye~~

目录
相关文章
|
Go 数据库
Golang 语言编写 gRPC 实战项目
Golang 语言编写 gRPC 实战项目
274 0
|
机器学习/深度学习 人工智能 算法
探索软件测试的未来:自动化与AI的融合之路移动应用开发的新纪元:从原生到跨平台
【8月更文挑战第27天】在软件开发的世界中,测试是确保产品质量的关键步骤。随着技术的不断进步,传统的手动测试方法正逐渐被自动化和人工智能(AI)技术所取代。本文将探讨自动化测试的现状与挑战,并展望未来AI如何重塑软件测试领域,同时提供实用的代码示例,引领读者一窥自动化测试的未来趋势。
|
缓存 安全 算法
Java面试题:如何通过JVM参数调整GC行为以优化应用性能?如何使用synchronized和volatile关键字解决并发问题?如何使用ConcurrentHashMap实现线程安全的缓存?
Java面试题:如何通过JVM参数调整GC行为以优化应用性能?如何使用synchronized和volatile关键字解决并发问题?如何使用ConcurrentHashMap实现线程安全的缓存?
178 0
|
存储 缓存 安全
大厂面试高频:ConcurrentHashMap 的实现原理( 超详细 )
本文详细解析ConcurrentHashMap的实现原理,大厂高频面试,必知必备。关注【mikechen的互联网架构】,10年+BAT架构经验倾囊相授。
大厂面试高频:ConcurrentHashMap 的实现原理( 超详细 )
|
Java API 数据库
深研Java异步编程:CompletableFuture与反应式编程范式的融合实践
【6月更文挑战第30天】Java 8的CompletableFuture革新了异步编程,提供如thenApply等流畅接口,而Java 9后的反应式编程(如Reactor)强调数据流和变化传播,以事件驱动应对高并发。两者并非竞争关系,而是互补,通过Flow API和第三方库结合,如将CompletableFuture转换为Mono进行反应式处理,实现更高效、响应式的系统设计。开发者可根据需求灵活选用,提升现代Java应用的并发性能。
232 1
|
12月前
|
Java 开发者
Java 中的锁是什么意思,有哪些分类?
在Java多线程编程中,锁用于控制多个线程对共享资源的访问,确保数据一致性和正确性。本文探讨锁的概念、作用及分类,包括乐观锁与悲观锁、自旋锁与适应性自旋锁、公平锁与非公平锁、可重入锁和读写锁,同时提供使用锁时的注意事项,帮助开发者提高程序性能和稳定性。
496 3
|
设计模式 Java 关系型数据库
【Java笔记+踩坑汇总】Java基础+JavaWeb+SSM+SpringBoot+SpringCloud+瑞吉外卖/谷粒商城/学成在线+设计模式+面试题汇总+性能调优/架构设计+源码解析
本文是“Java学习路线”专栏的导航文章,目标是为Java初学者和初中高级工程师提供一套完整的Java学习路线。
868 37
|
NoSQL Java Redis
面试官:项目中如何实现分布式锁?
面试官:项目中如何实现分布式锁?
326 7
面试官:项目中如何实现分布式锁?
|
存储 缓存 Java
大厂面试高频:Volatile 的实现原理 ( 图文详解 )
本文详解Volatile的实现原理(大厂面试高频,建议收藏),涵盖Java内存模型、可见性和有序性,以及Volatile的工作机制和源码案例。关注【mikechen的互联网架构】,10年+BAT架构经验倾囊相授。
大厂面试高频:Volatile 的实现原理 ( 图文详解 )
|
存储 NoSQL Java
Java调度任务如何使用分布式锁保证相同任务在一个周期里只执行一次?
【10月更文挑战第29天】Java调度任务如何使用分布式锁保证相同任务在一个周期里只执行一次?
381 1