百度360必应搜狗淘宝本站头条
当前位置:网站首页 > 技术分类 > 正文

Java 的多线程和并发库——面试题(高新必问)

ztj100 2024-12-14 16:11 19 浏览 0 评论

对于 Java 对程序员来说,多线程在工作中的使用场景还是比较常见的,而仅仅掌握了 Java 中的传统多线程机制,

还是不够的。在 JDK5.0 之后,Java 增加的并发库中提供了很多优秀的 API,在实际开发中用的比较多。因此在看具体

的面试题之前我们有必要对这部分知识做一个全面的了解。

(一)多线程基础知识--传统线程机制的回顾

( 1 ) 传统使用类 Thread 和接口 Runnable 实现

1.在 Thread 子类覆盖的 run 方法中编写运行代码

方式一

new Thread(){

@Override

public void run(){

while(true){

try {

Thread.sleep(2000);

} catch (InterruptedException e) {

e.printStackTrace();

} }

}

}.start();

2.在传递给 Thread 对象的 Runnable 对象的 run 方法中编写代码

new Thread(new Runnable(){

public void run(){

while(true){

try {

Thread.sleep(2000);

} catch (InterruptedException e) {

e.printStackTrace();

}

System.out.println(Thread.currentThread().getName());

}

}

}).start();

3.总结

查看 Thread 类的 run()方法的源代码,可以看到其实这两种方式都是在调用 Thread 对象的 run 方法,如果 Thread

类的 run 方法没有被覆盖,并且为该 Thread 对象设置了一个 Runnable 对象,该 run 方法会调用 Runnable 对象的

run 方法

/**

* If this thread was constructed using a separate

* <code>Runnable</code> run object, then that

* <code>Runnable</code> object's <code>run</code> method is called;

* otherwise, this method does nothing and returns.

* <p>

* Subclasses of <code>Thread</code> should override this method.

*

* @see #start()

* @see #stop()

* @see #Thread(ThreadGroup, Runnable, String)

*/

@Override

public void run() {

if (target != null) {

target.run();

}

( 2 ) 定时现时器 Timer 和 TimerTask

Timer 在实际开发中应用场景不多,一般来说都会用其他第三方库来实现。但有时会在一些面试题中出现。

下面我们就针对一道面试题来使用 Timer 定时类。

1.请模拟写出双重定时器(面试题)

要求:使用定时器,间隔 4 秒执行一次,再间隔 2 秒执行一次,以此类推执行。

class TimerTastCus extends TimerTask{

@Override

public void run() {

count = (count +1)%2;

System.err.println("Boob boom ");

new Timer().schedule(new TimerTastCus(), 2000+2000*count);

}

}

Timer timer = new Timer();

timer.schedule(new TimerTastCus(), 2000+2000*count);

while (true) {

System.out.println(new Date().getSeconds());

try {

Thread.sleep(1000);

} catch (InterruptedException e) {

// TODO Auto-generated catch block

e.printStackTrace();

}

}

//PS:下面的代码中的 count 变量中

//此参数要使用在你匿名的内部类中,使用 final 修饰就无法对其值进行修改,

//只能改为静态变量

private static volatile int count = 0;

( 3 ) 线程互斥与同步

在引入多线程后,由于线程执行的异步性,会给系统造成混乱,特别是在急用临界资源时,如多个线程急用同一台

打印机,会使打印结果交织在一起,难于区分。当多个线程急用共享变量,表格,链表时,可能会导致数据处理出错,

因此线程同步的主要任务是使并发执行的各线程之间能够有效地共享资源和相互合作,从而使程序的执行具有可再现性。

当线程并发执行时,由于资源共享和线程协作,使用线程之间会存在以下两种制约关系。

1. 间接相互制约。一个系统中的多个线程必然要共享某种系统资源,比如共享 CPU,共享 I/O 设备,所谓间接相

互相制约即源于这种资源共享,打印机就是最好的例子,线程 A 在使用打印机时,其它线程都要等待。

2. 直接相互制约。这种制约主要是因为线程之间的合作,如果有线程 A 将计算结果提供给线程 B 作进一步处理,

那么线程 B 在线程 A 将数据送达之前都将处于阻塞状态。

间接相互制约可以称为互斥,直接相互制约可以称为同步,对于互斥可以这样理解,线程 A 和线程 B 互斥访问某

要么资源则它们之间就会产生个顺序问题——要么线程 A 等待线程 B 操作完毕,要么线程 B 等待线程操作完毕,这其实就是

实现线程的同步了。因此同步包括互斥,互斥其实是一种特殊的同步。

下面我们通过一道面试题来体会线程的交互。

要求:子线程运行执行 10 次后,主线程再运行 5 次。这样交替执行三遍

public static void main(String[] args) {

final Bussiness bussiness = new Bussiness();

//子线程

new Thread(new Runnable() {

@Override

public void run() {

for (int i = 0; i < 3; i++) {

bussiness.subMethod();

}

}

}).start();

//主线程

for (int i = 0; i < 3; i++) {

bussiness.mainMethod();

}

}

}

class Bussiness {private boolean subFlag = true;

public synchronized void mainMethod() {

while (subFlag) {

try {

wait();

} catch (InterruptedException e) {

e.printStackTrace();

}

}

for (int i = 0; i < 5; i++) {

System.out.println(Thread.currentThread().getName()

+ " : main thread running loop count -- " + i);

try {

Thread.sleep(1000);

} catch (InterruptedException e) {

e.printStackTrace();

}

}

subFlag = true;

notify();

}

public synchronized void subMethod() {

while (!subFlag) {

try {

wait();

} catch (InterruptedException e) {

e.printStackTrace();

}

}

for (int i = 0; i < 10; i++) {

System.err.println(Thread.currentThread().getName()

+ " : sub thread running loop count -- " + i);

try {

Thread.sleep(1000);

} catch (InterruptedException e) {

e.printStackTrace();

}

}

subFlag = false;

notify();

}

}

( 4 ) 线程局部变量 ThreadLocal

? ThreadLocal 的作用和目的:用于实现线程内的数据共享,即对于相同的程序代码,多个模块在同一个

线程中运行时要共享一份数据,而在另外线程中运行时又共享另外一份数据。

? 每个线程调用全局 ThreadLocal 对象的 set 方法,在 set 方法中,首先根据当前线程获取当前线程的

ThreadLocalMap 对象,然后往这个 map 中插入一条记录,key 其实是 ThreadLocal 对象,value 是各自的 set

方法传进去的值。也就是每个线程其实都有一份自己独享的 ThreadLocalMap对象,该对象的 Key 是 ThreadLocal

对象,值是用户设置的具体值。在线程结束时可以调用 ThreadLocal.remove()方法,这样会更快释放内存,不调

用也可以,因为线程结束后也可以自动释放相关的 ThreadLocal 变量。

? ThreadLocal 的应用场景:

? 订单处理包含一系列操作:减少库存量、增加一条流水台账、修改总账,这几个操作要在同一个

事务中完成,通常也即同一个线程中进行处理,如果累加公司应收款的操作失败了,则应该把前面

的操作回滚,否则,提交所有操作,这要求这些操作使用相同的数据库连接对象,而这些操作的代码

分别位于不同的模块类中。

? 银行转账包含一系列操作: 把转出帐户的余额减少,把转入帐户的余额增加,这两个操作要在

同一个事务中完成,它们必须使用相同的数据库连接对象,转入和转出操作的代码分别是两个不同

的帐户对象的方法。

? 例如 Strut2 的 ActionContext,同一段代码被不同的线程调用运行时,该代码操作的数据是每

个线程各自的状态和数据,对于不同的线程来说,getContext 方法拿到的对象都不相同,对同一个

线程来说,不管调用 getContext 方法多少次和在哪个模块中 getContext 方法,拿到的都是同一

个。

1. ThreadLocal 的使用方式

(1) 在关联数据类中创建 private static ThreadLocal

在下面的类型中,私有静态 ThreadLocal 实例(serialNum)为调用该类的静态 SerialNum.get() 方法的每个

线程维护了一个“序列号”,该方法将返回当前线程的序列号。(线程的序列号是在第一次调用 SerialNum.get() 时

分配的,并在后续调用中不会更改。)

public class SerialNum {

// The next serial number to be assigned

private static int nextSerialNum = 0;

private static ThreadLocal serialNum = new ThreadLocal() {

protected synchronized Object initialValue() {

return new Integer(nextSerialNum++);

}

};

public static int get() {

return ((Integer) (serialNum.get())).intValue();

}

}

另一个例子,也是私有静态 ThreadLocal 实例:

public class ThreadContext {

private String userId;

private Long transactionId;

private static ThreadLocal threadLocal = new ThreadLocal(){

@Override

protected ThreadContext initialValue() {

return new ThreadContext();

}

};

public static ThreadContext get() {

return threadLocal.get();

}

public String getUserId() {

return userId;

}

public void setUserId(String userId) {

this.userId = userId;

}

public Long getTransactionId() {

return transactionId;

}

public void setTransactionId(Long transactionId) {

this.transactionId = transactionId;

}

}

2. 在 Util 类中创建 ThreadLocal

这是上面用法的扩展,即把 ThreadLocal 的创建放到工具类中。

public class HibernateUtil {

private static Log log = LogFactory.getLog(HibernateUtil.class);

private static final SessionFactory sessionFactory; //定义 SessionFactory

static {

try {

// 通过默认配置文件 hibernate.cfg.xml 创建 SessionFactory

sessionFactory = new Configuration().configure().buildSessionFactory();

} catch (Throwable ex) {

log.error("初始化 SessionFactory 失败!", ex);

throw new ExceptionInInitializerError(ex);

}

}

//创建线程局部变量 session,用来保存 Hibernate 的 Session

public static final ThreadLocal session = new ThreadLocal();

/**

* 获取当前线程中的 Session

* @return Session

* @throws HibernateException

*/

public static Session currentSession() throws HibernateException {

Session s = (Session) session.get();

// 如果 Session 还没有打开,则新开一个 Session

if (s == null) {

s = sessionFactory.openSession();

session.set(s); //将新开的 Session 保存到线程局部变量中

}

return s;

}

public static void closeSession() throws HibernateException {

//获取线程局部变量,并强制转换为 Session 类型

Session s = (Session) session.get();

session.set(null);

if (s != null)

s.close();

}

}

3. 在 Runnable 中创建 ThreadLocal

在线程类内部创建 ThreadLocal,基本步骤如下:

①、在多线程的类(如 ThreadDemo 类)中,创建一个 ThreadLocal 对象 threadXxx,用来保存线程间

需要隔离处理的对象 xxx。

②、在 ThreadDemo 类中,创建一个获取要隔离访问的数据的方法 getXxx(),在方法中判断,若

ThreadLocal 对象为 null 时候,应该 new()一个隔离访问类型的对象,并强制转换为要应用的类型

③、在 ThreadDemo 类的 run()方法中,通过调用 getXxx()方法获取要操作的数据,这样可以保证每个线

程对应一个数据对象,在任何时刻都操作的是这个对象。

public class ThreadLocalTest implements Runnable{

ThreadLocal<Studen> studenThreadLocal = new ThreadLocal<Studen>();

@Override

public void run() {

String currentThreadName = Thread.currentThread().getName();

System.out.println(currentThreadName + " is running...");

Random random = new Random();

int age = random.nextInt(100);

System.out.println(currentThreadName + " is set age: " + age);

Studen studen = getStudent(); //通过这个方法,为每个线程都独立的 new 一个 student 对象,每个线程的的

student 对象都可以设置不同的值

studen.setAge(age);

System.out.println(currentThreadName + " is first get age: " + studen.getAge());

try {

Thread.sleep(500);

} catch (InterruptedException e) {

e.printStackTrace();

}

System.out.println( currentThreadName + " is second get age: " + studen.getAge());

}

private Studen getStudent() {

Studen studen = studenThreadLocal.get();

if (null == studen) {

studen = new Studen();

studenThreadLocal.set(studen);

}

return studen;

}

public static void main(String[] args) {

ThreadLocalTest t = new ThreadLocalTest();

Thread t1 = new Thread(t,"Thread A");

Thread t2 = new Thread(t,"Thread B");

t1.start();

t2.start();

}

}

class Studen{

int age;

public int getAge() {

return age;

}

public void setAge(int age) {

this.age = age;

}

( 5 ) 多线程共享数据

在 Java 传统线程机制中的共享数据方式,大致可以简单分两种情况:

? 多个线程行为一致,共同操作一个数据源。也就是每个线程执行的代码相同,可以使用同一个 Runnable 对

象,这个 Runnable 对象中有那个共享数据,例如,卖票系统就可以这么做。

? 多个线程行为不一致,共同操作一个数据源。也就是每个线程执行的代码不同,这时候需要用不同的

Runnable 对象。例如,银行存款。

下面我们通过两个示例代码来分别说明这两种方式。

1. 多个线程行为一致共同操作一个数据

如果每个线程执行的代码相同,就可以使用同一个 Runnable 对象,这个 Runnable 对象中有那个共享数据,例如,

买票系统就可以这么做。

/**

*共享数据类

**/

class ShareData{

private int num = 10 ;

public synchronized void inc() {

num++;

System.out.println(Thread.currentThread().getName()+": invoke inc method num =" + num);

try {

Thread.sleep(1000);

} catch (InterruptedException e) {

e.printStackTrace();

}

}

}

/**

*多线程类

**/

class RunnableCusToInc implements Runnable{

private ShareData shareData;

public RunnableCusToInc(ShareData data) {

this.shareData = data;

}

@Override

public void run() {

for (int i = 0; i < 5; i++) {

shareData.inc();

}

}

}

/**

*测试方法

**/

public static void main(String[] args) {

ShareData shareData = new ShareData();

for (int i = 0; i < 4; i++) {

new Thread(new RunnableCusToInc(shareData),"Thread "+ i).start();

}

}

}

2. 多个线程行为不一致共同操作一个数据

如果每个线程执行的代码不同,这时候需要用不同的 Runnable 对象,有如下两种方式来实现这些 Runnable 对

项之间的数据共享:

1) 将共享数据封装在另外一个对象中,然后将这个对象逐一传递给各个 Runnable 对象。每个线程对共享

数据的操作方法也分配到那个对象身上去完成,这样容易实现针对该数据进行的各个操作的互斥和通信。

public static void main(String[] args) {

ShareData shareData = new ShareData();

for (int i = 0; i < 4; i++) {

if(i%2 == 0){

new Thread(new RunnableCusToInc(shareData),"Thread "+ i).start();

}else{

new Thread(new RunnableCusToDec(shareData),"Thread "+ i).start();

}

}

}

//封装共享数据类

class RunnableCusToInc implements Runnable{

//封装共享数据

private ShareData shareData;

public RunnableCusToInc(ShareData data) {

this.shareData = data;

}

@Override

public void run() {

for (int i = 0; i < 5; i++) {

shareData.inc();

}

}

}

//封装共享数据类

class RunnableCusToDec implements Runnable{

//封装共享数据

private ShareData shareData;

public RunnableCusToDec(ShareData data) {

this.shareData = data;

}

@Override

public void run() {

for (int i = 0; i < 5; i++) {

shareData.dec();

}

}

}

/**

*共享数据类

**/

class ShareData{

private int num = 10 ;

public synchronized void inc() {

num++;

System.out.println(Thread.currentThread().getName()+": invoke inc method num =" + num);

try {

Thread.sleep(1000);

} catch (InterruptedException e) {

e.printStackTrace();

}

}

}

2) 将这些 Runnable 对象作作为某一个类中的内部类,共享数据作为这个外部类中的成员变量,每个线程对

共享数据的操作方法也分配给外部类,以便实现对共享数据进行的各个操作的互斥和通信,作为内部类的各个

Runnable 对象调用外部类的这些方法。

public static void main(String[] args) {

//公共数据

final ShareData shareData = new ShareData();

for (int i = 0; i < 4; i++) {

if(i%2 == 0){

new Thread(new Runnable() {

@Override

public void run() {

for (int i = 0; i < 5; i++) {

shareData.inc();

}

}

},"Thread "+ i).start();

}else{

new Thread(new Runnable() {

@Override

public void run() {

for (int i = 0; i < 5; i++) {

shareData.dec();

}

}

},"Thread "+ i).start();

}

}

}

class ShareData{

private int num = 10 ;

public synchronized void inc() {

num++;

System.out.println(Thread.currentThread().getName()+": invoke inc method num =" + num);

try {

Thread.sleep(1000);

} catch (InterruptedException e) {

e.printStackTrace();

}

}

public synchronized void dec() {

num--;

System.err.println(Thread.currentThread().getName()+": invoke dec method num =" + num);

try {

Thread.sleep(1000);

} catch (InterruptedException e) {

e.printStackTrace();

}

}

}

相关推荐

从IDEA开始,迈进GO语言之门(idea got)

前言笔者在学习GO语言编程的时候,GO语言在国内还没有像JAVA/Php/Python那样普及,绕了不少的弯路,要开始入门学习一门编程语言,最好就先从选择一个好的编程语言的开发环境开始,有了这个开发环...

基于SpringBoot+MyBatis的私人影院java网上购票jsp源代码Mysql

本项目为前几天收费帮学妹做的一个项目,JavaEEJSP项目,在工作环境中基本使用不到,但是很多学校把这个当作编程入门的项目来做,故分享出本项目供初学者参考。一、项目介绍基于SpringBoot...

基于springboot的个人服装管理系统java网上商城jsp源代码mysql

本项目为前几天收费帮学妹做的一个项目,JavaEEJSP项目,在工作环境中基本使用不到,但是很多学校把这个当作编程入门的项目来做,故分享出本项目供初学者参考。一、项目介绍基于springboot...

基于springboot的美食网站Java食品销售jsp源代码Mysql

本项目为前几天收费帮学妹做的一个项目,JavaEEJSP项目,在工作环境中基本使用不到,但是很多学校把这个当作编程入门的项目来做,故分享出本项目供初学者参考。一、项目介绍基于springboot...

贸易管理进销存springboot云管货管账分析java jsp源代码mysql

本项目为前几天收费帮学妹做的一个项目,JavaEEJSP项目,在工作环境中基本使用不到,但是很多学校把这个当作编程入门的项目来做,故分享出本项目供初学者参考。一、项目描述贸易管理进销存spring...

SpringBoot+VUE员工信息管理系统Java人员管理jsp源代码Mysql

本项目为前几天收费帮学妹做的一个项目,JavaEEJSP项目,在工作环境中基本使用不到,但是很多学校把这个当作编程入门的项目来做,故分享出本项目供初学者参考。一、项目介绍SpringBoot+V...

目前见过最牛的一个SpringBoot商城项目(附源码)还有人没用过吗

帮粉丝找了一个基于SpringBoot的天猫商城项目,快速部署运行,所用技术:MySQL,Druid,Log4j2,Maven,Echarts,Bootstrap...免费给大家分享出来前台演示...

SpringBoot+Mysql实现的手机商城附带源码演示导入视频

今天为大家带来的是基于SpringBoot+JPA+Thymeleaf框架的手机商城管理系统,商城系统分为前台和后台、前台用的是Bootstrap框架后台用的是SpringBoot+JPA都是现在主...

全网首发!马士兵内部共享—1658页《Java面试突击核心讲》

又是一年一度的“金九银十”秋招大热门,为助力广大程序员朋友“面试造火箭”,小编今天给大家分享的便是这份马士兵内部的面试神技——1658页《Java面试突击核心讲》!...

SpringBoot数据库操作的应用(springboot与数据库交互)

1.JDBC+HikariDataSource...

SpringBoot 整合 Flink 实时同步 MySQL

1、需求在Flink发布SpringBoot打包的jar包能够实时同步MySQL表,做到原表进行新增、修改、删除的时候目标表都能对应同步。...

SpringBoot + Mybatis + Shiro + mysql + redis智能平台源码分享

后端技术栈基于SpringBoot+Mybatis+Shiro+mysql+redis构建的智慧云智能教育平台基于数据驱动视图的理念封装element-ui,即使没有vue的使...

Springboot+Mysql舞蹈课程在线预约系统源码附带视频运行教程

今天发布的是由【猿来入此】的优秀学员独立做的一个基于springboot脚手架的Springboot+Mysql舞蹈课程在线预约系统,系统项目源代码在【猿来入此】获取!https://www.yuan...

SpringBoot+Mysql在线众筹系统源码+讲解视频+开发文档(参考论文

今天发布的是由【猿来入此】的优秀学员独立做的一个基于springboot脚手架的在线众筹管理系统,主要实现了普通用户在线参与众筹基本操作流程的全部功能,系统分普通用户、超级管理员等角色,除基础脚手架外...

Docker一键部署 SpringBoot 应用的方法,贼快贼好用

这两天发现个Gradle插件,支持一键打包、推送Docker镜像。今天我们来讲讲这个插件,希望对大家有所帮助!GradleDockerPlugin简介...

取消回复欢迎 发表评论: