文章简介

上一篇文章【「阿里面试系列」搞懂并发编程,轻松应对80%的面试场景】我们了解了进程和线程的发展历史、线程的生命周期、线程的优势和使用场景,这一篇,我们从Java层面更进一步了解线程的使用。关注我的技术公众号【架构师修炼宝典】一周出产1-2篇技术文章。Q群725219329分享并发编程,分布式,微服务架构,性能优化,源码,设计模式,高并发,高可用,Spring,Netty,tomcat,JVM等技术视频。

内容导航

  1. 并发编程的挑战
  2. 线程在Java中的使用

并发编程的挑战

引入多线程的目的在第一篇提到过,就是为了充分利用CPU是的程序运行得更快,当然并不是说启动的线程越多越好。在实际使用多线程的时候,会面临非常多的挑战

线程安全问题

线程安全问题值的是当多个线程访问同一个对象时,如果不考虑这些运行时环境采用的调度方式或者这些线程将如何交替执行,并且在代码中不需要任何同步操作的情况下,这个类都能够表现出正确的行为,那么这个类就是线程安全的
比如下面的代码是一个单例模式,在代码的注释出,如果多个线程并发访问,则会出现多个实例。导致无法实现单例的效果

public class SingletonDemo {
private static SingletonDemo singletonDemo=null;
private SingletonDemo(){}
public static SingletonDemo getInstance(){
if(singletonDemo==null){/***线程安全问题***/
singletonDemo=new SingletonDemo();
}
return singletonDemo;
}
}

通常来说,我们把多线程编程中的线程安全问题归类成如下三个,至于每一个问题的本质,在后续的文章中我们会单独讲解

  1. 原子性
  2. 可见性
  3. 有序性

上下文切换问题

在单核心CPU架构中,对于多线程的运行是基于CPU时间片切换来实现的伪并行。由于时间片非常短导致用户以为是多个线程并行执行。而一次上下文切换,实际就是当前线程执行一个时间片之后切换到另外一个线程,并且保存当前线程执行的状态这个过程。上下文切换会影响到线程的执行速度,对于系统来说意味着会消耗大量的CPU时间

减少上下文切换的方式

  1. 无锁并发编程,在多线程竞争锁时,会导致大量的上下文切换。避免使用锁去解决并发问题可以减少上下文切换
  2. CAS算法,CAS是一种乐观锁机制,不需要加锁
  3. 使用与硬件资源匹配合适的线程数

死锁

在解决线程安全问题的场景中,我们会比较多的考虑使用锁,因为它使用比较简单。但是锁的使用如果不恰当,则会引发死锁的可能性,一旦产生死锁,就会造成比较严重的问题:产生死锁的线程会一直占用锁资源,导致其他尝试获取锁的线程也发生死锁,造成系统崩溃

以下是死锁的简单案例

public class DeadLockDemo {
//定义锁对象
private final Object lockA = new Object();
private final Object lockB = new Object();
private void deadLock(){
new Thread(()->{
synchronized (lockA){
try {
Thread.sleep(4000);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (lockB){
System.out.println("Lock B");
}
}
}).start();
new Thread(()->{
synchronized (lockB){
synchronized (lockA){
System.out.println("Lock A");
}
}
}).start();
}
public static void main(String[] args) {
new DeadLockDemo().deadLock();
}
}

通过jstack分析死锁

1.首先通过jps获取当前运行的进程的pid

6628 Jps
17588 RemoteMavenServer
19220 Launcher
19004 DeadLockDemo

2.jstack打印堆栈信息,输入 jstack19004, 会打印如下日志,可以很明显看到死锁的信息提示

Found one Java-level deadlock:
=============================
"Thread-1":
waiting to lock monitor 0x000000001d461e68 (object 0x000000076b310df8, a java.lang.Object),
which is held by "Thread-0"
"Thread-0":
waiting to lock monitor 0x000000001d463258 (object 0x000000076b310e08, a java.lang.Object),
which is held by "Thread-1"

解决死锁的手段
1.保证多个线程按照相同的顺序获取锁
2.设置获取锁的超时时间,超过设定时间以后自动释放
3.死锁检测

资源限制

资源限制主要指的是硬件资源和软件资源,在开发多线程应用时,程序的执行速度受限于这两个资源。硬件的资源限制无非就是磁盘、CPU、内存、网络;软件资源的限制有很多,比如数据库连接数、计算机能够支持的最大连接数等
资源限制导致的问题最直观的体现就是前面说的上下文切换,也就是CPU资源和线程资源的严重不均衡导致频繁上下文切换,反而会造成程序的运行速度下降

资源限制的主要解决方案,就是缺啥补啥。CPU不够用,可以增加CPU核心数;一台机器的资源有限,则增加多台机器来做集群。

线程在Java中的使用

在Java中实现多线程的方式比较简单,因为Java中提供了非常方便的API来实现多线程。
1.继承Thread类实现多线程
2.实现Runnable接口
3.实现Callable接口通过Future包装器来创建Thread线程,这种是带返回值的线程
4.使用线程池ExecutorService

关注我的技术公众号【架构师修炼宝典】一周出产1-2篇技术文章。Q群725219329分享并发编程,分布式,微服务架构,性能优化,源码,设计模式,高并发,高可用,Spring,Netty,tomcat,JVM等技术视频。

继承Thread类

继承Thread类,然后重写run方法,在run方法中编写当前线程需要执行的逻辑。最后通过线程实例的start方法来启动一个线程

public class ThreadDemo extends Thread{
@Override
public void run() {
//重写run方法,提供当前线程执行的逻辑
System.out.println("Hello world");
}
public static void main(String[] args) {
ThreadDemo threadDemo=new ThreadDemo();
threadDemo.start();
}
}

Thread类其实是实现了Runnable接口,因此Thread自己也是一个线程实例,但是我们不能直接用 newThread().start()去启动一个线程,原因很简单,Thread类中的run方法是没有实际意义的,只是一个调用通过构造函数传递寄来的另一个Runnable实现类的run方法,这块的具体演示会在Runnable接口的代码中看到

public
class Thread implements Runnable {
/* What will be run. */
private Runnable target;
...
@Override
public void run() {
if (target != null) {
target.run();
}
}
...

实现Runnable接口

如果需要使用线程的类已经继承了其他的类,那么按照Java的单一继承原则,无法再继承Thread类来实现线程,所以可以通过实现Runnable接口来实现多线程

public class RunnableDemo implements Runnable{
@Override
public void run() {
//重写run方法,提供当前线程执行的逻辑
System.out.println("Hello world");
}
public static void main(String[] args) {
RunnableDemo runnableDemo=new RunnableDemo();
new Thread(runnableDemo).start();
}
}

上面的代码中,实现了Runnable接口,重写了run方法;接着为了能够启动RunnableDemo这个线程,必须要实例化一个Thread类,通过构造方法传递一个Runnable接口实现类去启动,Thread的run方法就会调用target.run来运行当前线程,代码在上面.

实现Callable接口

在有些多线程使用的场景中,我们有时候需要获取异步线程执行完毕以后的反馈结果,也许是主线程需要拿到子线程的执行结果来处理其他业务逻辑,也许是需要知道线程执行的状态。那么Callable接口可以很好的实现这个功能

public class CallableDemo implements Callable<String>{
@Override
public String call() throws Exception {
return "hello world";
}
public static void main(String[] args) throws ExecutionException, InterruptedException {
Callable<String> callable=new CallableDemo();
FutureTask<String> task=new FutureTask<>(callable);
new Thread(task).start();
System.out.println(task.get());//获取线程的返回值
}
}

在上面代码案例中的最后一行 task.get()就是获取线程的返回值,这个过程是阻塞的,当子线程还没有执行完的时候,主线程会一直阻塞直到结果返回

使用线程池

为了减少频繁创建线程和销毁线程带来的性能开销,在实际使用的时候我们会采用线程池来创建线程,在这里我不打算展开多线程的好处和原理,我会在后续的文章中单独说明。

public class ExecutorServiceDemo {
public static void main(String[] args) throws ExecutionException, InterruptedException {
//创建一个固定线程数的线程池
ExecutorService pool = Executors.newFixedThreadPool(1);
Future future=pool.submit(new CallableDemo());
System.out.println(future.get());
}
}

pool.submit有几个重载方法,可以传递带返回值的线程实例,也可以传递不带返回值的线程实例,源代码如下

/*01*/Future<?> submit(Runnable task);
/*02*/<T> Future<T> submit(Runnable task, T result);
/*03*/<T> Future<T> submit(Callable<T> task);

关注我的技术公众号【架构师修炼宝典】一周出产1-2篇技术文章。Q群725219329分享并发编程,分布式,微服务架构,性能优化,源码,设计模式,高并发,高可用,Spring,Netty,tomcat,JVM等技术视频。

【阿里面试系列】Java线程的应用及挑战的更多相关文章

  1. [不得不知道系列]Java线程面试你不得不知道的基础知识一

    Java内存管理面试指南一 Java基础面试指南一 Java基础面试指南二 Java基础面试指南三 Java基础面试指南四 Java线程面试指南一 Java线程面试指南二 Redis面试指南一 Kaf ...

  2. 阿里面试经历JAVA总结

    为记录阿里的电面经历,特与大家分享,岗位是JAVA研发工程师. 一面主要问题如下: 1)首先自我介绍 2)数据结构算法的基本问题,如排序算法,二叉树遍历,后序遍历非递归,图的最短路径问题 3)对一个数 ...

  3. (Java 多线程系列)Java 线程池(Executor)

    线程池简介 线程池是指管理同一组同构工作线程的资源池,线程池是与工作队列(Work Queue)密切相关的,其中在工作队列中保存了所有等待执行的任务.工作线程(Worker Thread)的任务很简单 ...

  4. Java面试系列--java基础

    Java基础总结 JAVA中的几种基本数据类型是什么,各自占用多少字节. 八大基本数据类型,byte:8位,short:16位,int:32位,long:64位,float:32位,double:64 ...

  5. 面试系列<3>——java并发

    面试系列--java并发 一.使用线程 有三种使用线程的方法: 实现Runnable接口 实现Callable接口 继承Thread类 实现 Runnable 和 Callable 接口的类只能当做一 ...

  6. 程序员面试系列之Java单例模式的攻击与防御

    我写的程序员面试系列 Java面试系列-webapp文件夹和WebContent文件夹的区别? 程序员面试系列:Spring MVC能响应HTTP请求的原因? Java程序员面试系列-什么是Java ...

  7. 当阿里面试官问我:Java创建线程有几种方式?我就知道问题没那么简单

    这是最新的大厂面试系列,还原真实场景,提炼出知识点分享给大家. 点赞再看,养成习惯~ 微信搜索[武哥聊编程],关注这个 Java 菜鸟. 昨天有个小伙伴去阿里面试实习生岗位,面试官问他了一个老生常谈的 ...

  8. 干货,阿里P8浅谈对java线程池的理解(面试必备)

    线程池的概念 线程池由任务队列和工作线程组成,它可以重用线程来避免线程创建的开销,在任务过多时通过排队避免创建过多线程来减少系统资源消耗和竞争,确保任务有序完成:ThreadPoolExecutor ...

  9. 阿里面试回来,想和Java程序员谈一谈(转载)

    引言 其实本来真的没打算写这篇文章,主要是LZ得记忆力不是很好,不像一些记忆力强的人,面试完以后,几乎能把自己和面试官的对话都给记下来.LZ自己当初面试完以后,除了记住一些聊过的知识点以外,具体的内容 ...

随机推荐

  1. James Munkres Topology: Sec 22 Exer 3

    Exercise 22.3 Let \(\pi_1: \mathbb{R} \times \mathbb{R} \rightarrow \mathbb{R}\) be projection on th ...

  2. 关于定时脚本crontab的坑

    需求: 每分钟执行一次程序,将处理后的数据写入mongodb 最初做法: 1):写crontab没有响应,于是打算通过shell脚本的while true来执行 当时sb,没控制时间内,而且我还是用n ...

  3. nginx反向代理+tomcat域名绑定

    今天在用nginx做反向代理时,由于一个tomcat下有多个应用,因此要在tomcat做域名绑定.tomcat启动后,通过域名+端口是可以访问到页面的,但是通过nginx转发后就不能访问了,因此tom ...

  4. 第六届Code+程序设计网络挑战赛

    弹指能算(easy)模式只做出一道签到题,还WA了一发,我好菜啊啊啊啊...虽然我菜,但是比赛体验一点都不好,服务器老是崩. 比赛链接:https://moocoder.xuetangx.com/de ...

  5. 动态规划——Best Time to Buy and Sell Stock IV

    这是这个系列题目的第四个,题目大意和之前的差不多,但是这次提供最多k次的操作,操作还是不能同时操作即必须结束前一个操作才能进行后一个操作. 状态比较好理解,就是题目要求的缩小版,dp[k][i]表示进 ...

  6. CI 框架 隐藏index.php 入口文件 和 设置访问application下子目录

    1.隐藏根目录下 index.php, 在根目录下创建 .htaccess文件 内容如下: <IfModule mod_rewrite.c> RewriteEngine on Rewrit ...

  7. python爬虫实践(二)——爬取张艺谋导演的电影《影》的豆瓣影评并进行简单分析

    学了爬虫之后,都只是爬取一些简单的小页面,觉得没意思,所以我现在准备爬取一下豆瓣上张艺谋导演的“影”的短评,存入数据库,并进行简单的分析和数据可视化,因为用到的只是比较多,所以写一篇博客当做笔记. 第 ...

  8. 一. 优化小程序自身的Storage

    小程序中的存储只有 Storage ,特性如下: 上限为 10MB 以用户纬度隔离,同一个设备,A 无法访问 B 用户的数据. 持久缓存,只有在用户关掉小程序才会删除,如果空间不足,会进行 LRU , ...

  9. javascript 插入DOM节点

    1.使用appendChild,把一个子节点添加到父节点的最后一个子节点,.innerText插入的是内容 HTML <!-- HTML结构 --> <p id="js&q ...

  10. 关于Promise层层嵌套可读性差问题

    Promise 是异步编程的一种解决方案,比传统的解决方案——回调函数和事件——更合理和更强大.它由社区最早提出和实现,ES6 将其写进了语言标准,统一了用法,原生提供了Promise对象 ES6 规 ...