40道Java多线程问题总结

文章转载自 https://www.cnblogs.com/xrq730/p/5060921.html#!comments


1.多线程有什么用

  • 发挥多核 CPU 的优势
  • 防止阻塞
  • 便于建模

2.创建线程的方式?

  1. 继承Thread类

  2. 实现Runnable接口

后者要好一点,因为实现接口的方式比继承类的方式更灵活,也能减少程序之间的耦合度,面向接口编程 也是设计模式6大原则的核心。

3. start() 方法和 run() 方法的区别

只有调用了 start() 方法,才会表现出多线程的特性,不同线程的 run() 方法里面的代码交替执行。如果只是调用 run() 方法,那么代码还是同步执行的,必须等待一个线程的 run() 方法里面的代码全部执行完毕之后,另外一个线程才可以执行其 run() 方法里面的代码。

4.Runnable 接口和 Callable 接口的区别

Runnable 接口中的 run() 方法的返回值是 void ,它做的事情只是纯粹地执行 run() 方法中的代码而已;Callable 接口中的 call() 方法是有返回值的,是一个泛型,和 Future ,FutureTask 配合可以用来获取异步执行的结果。

这其实是很有用的一个特性,因为 多线程相比单线程更难,更复杂的一个重要原因就是因为多线程充满着未知性,某条线程是否执行了?某条线程执行了多久?某条线程执行的时候我们期望的数据是否已经赋值完毕?无法得知,我们能做的只是等待这条多线程的任务执行完毕而已。而Callable + Future/FutureTask 却可以获取多线程运行的结果,可以在等待时间太长没获取到需要的数据的情况下取消该线程的任务,非常有用

5.CyclicBarrier 和 CountDownLatch 的区别

两个看上去有点像的类,都在 java.util.concurrent 下,都可以用来表示代码运行到某个点上,二者的区别在于:

  1. CyclicBarrier 的某个线程运行到某个点上之后,该线程即停止运行,直到所有的线程都到达了这个点,所有线程才重新运行;CountDownLatch 则不是,某线程运行到某个点上之后,只是给某个数值 -1 而已,该线程继续运行
  2. CyclicBarrier 只能唤起一个任务,CountDownLatch 可以唤起多个任务
  3. CyclicBarrier可重用,CountDownLatch 不可重用,记数值为 0 该 CountDownLatch 就不可重用了

6.volatile 关键字的作用

  1. 多线程主要围绕可见性和原子性两个特性而展开,使用 volatile 关键字修饰的变量,保证了其在多线程之间的可见性,即每次读取到 volatile 变量,一定是最新的数据
  2. 代码底层执行不像我们看到的高级语言 – Java 程序这么简单,它的执行是 Java代码 --> 字节码 --> 根据字节码执行对应的 c/c++ 代码 --> c/c++ 代码被编译成汇编语言 --> 和硬件电路交互,现实中,为了获取更好的性能JVM可能会对指令进行重排序,多线程下可能会出现一些意想不到的问题。使用 volatile 则会对禁止语义重排序,当然这也一定程度上降低了带啊没执行效率

从实践角度而言, volatile的一个重要作用就是和 CAS 结合, 保证了原子性。

什么是线程安全

如果你的代码在多线程下执行和在单线程下执行永远都能获得一样的结果,那么你的代码就是线程安全的。

线程安全的几个级别

  1. 不可变
    像String,Integer,Long这些,都是final类型的类,任何一个线程都改变不了它们的值,要改变除非新创建一个,因此这些不可变对象不需要任何同步手段就可以直接在多线程环境下使用

  2. 绝对线程安全
    不管运行环境如何,调用者都不需要额外的同步措施。要做到这一点通常需要付出许多额外的代价,Java中标注自己是线程安全的类,实际上绝大多数都不是线程安全的,不过绝对线程安全的类,java中也有,比方说CopyOnWriteArrayList,CopyOnWriteArraySet

  3. 相对线程安全
    相对线程安全也就是我们通常意义上说的线程安全,像Vector这种,add,remove方法都是原子操作,不会被打断,但也仅限于此,如果有个线程在遍历某个Vector,有个线程在同时add这个Vector,99%的情况都会出现 ConcurrentModificationException,也就是fail-fast机制

  4. 线程非安全
    ArrayList,LinkedList,HashMap都是线程非安全类

8.Java中如果获取到线程 dump文件

死循环,死锁,阻塞,页面打开慢等问题,打线程 dump 是最好的解决问题的途径。所谓线程 dump 也就是线程堆栈,获取到线程堆栈有两步:

  1. 获取到线程的 pid,可以通过使用jps命令,Linux环境下可以使用 ps -ef|grep java
  2. 打印线程堆栈,可以通过使用 jstack pid 命令,Linux环境可以使用 kill -3 pid

另外,Thread类提供了一个 getStackTrace() 方法也可以用于获取线程堆栈。这是一个实例方法,因此此方法是和具体线程实例绑定的,每次获取到的是具体某个线程当前运行的堆栈。

9.一个线程如果出现了运行时异常会怎样

如果这个异常没有被捕获的话,这个线程就停止执行了。另外重要的一点是:如果这个线程持有某个对象的监视器,那么这个对象监视器会被立即释放

10.如何在两个线程之间共享数据

通过在线程之间共享对象就可以。然后通过 wait/notify/notifyAll,await/signal/signalAll 进行唤起和等待,比方说阻塞队列 BlockingQueue就是为线程之间共享数据而设计的。

11.sleep方法和wait方法有什么区别

sleep 方法和 wait方法都可以用来放弃CPU 一定的时间,不同点在于如果线程持有某个对象的监视器,sleep方法不会放弃这个对象的监视器,wait方法会放弃这个对象的监视器

12.生产者消费者模型的作用是什么

  1. 通过平衡生产者的生产能力和消费者的消费能力来提升整个系统的运行效率,这是生产者消费者模型最重要的作用
  2. 解耦,这是生产者消费者模型附带的作用,解耦意味着生产者和消费者之间的联系少,联系越少越可以独自发展而不需要收到相互的制约

13.ThreadLocal有什么用

简单说 ThreadLocal 就是一种以空间换时间的做法,在每个Thread类里面维护了一个以开地址法实现的 ThreadLocal.ThreadLocalMap,把数据进行隔离,数据不共享,自然就没有线程安全方面的问题了

14.为什么wait() 方法和 notify()/notifyAll() 方法要在同步块中被调用

这是JDK强制的,wait() 方法和 notify()/notifyAll() 方法在调用前都必须先获得对象的锁

15.wait() 方法和 notify()/notifyAll() 方法在放弃对象监视器时有什么区别?

wait() 方法立即释放对象监视器,notify()/notifyAll() 方法则会等待线程剩余代码执行完毕才会放弃对象监视器。

16.为什么要使用线程池

避免频繁的创建和销毁线程,达到线程对象的重用。另外,使用线程池还可以根据项目灵活控制并发的数目。

17.怎么检测一个线程是否持有对象监视器

Thread类提供了一个 holdsLock(Object object)方法,当且仅当对象 object 的监视器被某条线程持有的时候才会返回 true,这是一个 static 方法,意味着某条线程指的是当前线程

18.synchronized 和 ReentrantLock 的区别

synchronized 是和 if,else,for,while 一样的关键字,ReentrantLock是类,这是二者的本质区别。既然ReentrantLock 是类,那么他就提供了比 synchronized 更多更灵活的特性,可以被继承,可以有方法,可以有各种各样的类变量,ReentrantLock 比 synchronized 的扩展性体现在几点上:

  1. ReentrantLock 可以对获取锁的等待时间进行设置,这样就避免了死锁
  2. ReentrantLock 可以获取各种锁的信息
  3. ReentrantLock 可以灵活地实现多路通知

另外,二者的锁机制其实也是不一样的。ReentrantLock 底层调用的是 Unsafe 的 park 方法加锁,synchronized操作的是对象头中 mark word

19.ConcurrentHashMap 的并发度是什么

ConcurrentHashMap 的并发度就是 segment 的大小。默认为16,这意味着最多可以有16条线程操作 ConcurrentHashMap,这也是 ConcurrentHashMap对 HashTable 最大的优势

20.ReadWriteLock 是什么

ReadWriteLock 是一个读写锁接口,ReentrantReadWriteLock 是 ReadWriteLock 接口的一个具体实现,实现的读写的分离,读锁是共享的,写锁是独占的,读和读之间不会互斥,读和写,写和读,写和写之间才会互斥,提升了读写的性能。

21.FutureTask 是什么

FutureTask 表示一个异步运算的任务。FutureTask 里面可以传入一个 Callable 的具体实现类,可以对这个异步运算的任务的结果进行等待获取,判断是否已经完成,取消任务等操作。当然,由于 FutureTask 也是 Runnable 接口的实现类,所以 FutureTask 也可以放入线程池中。

22.Linux环境下如何查看那个线程使用 CPU最长

  1. 获取项目的pid (ps -ef|grep java)
  2. top -H -p pid,顺序不能改变

这样可以打印出当前项目,每条线程占用 CPU 时间的百分比。

23.Java 编程写一个会导致死锁的程序

死锁:线程A 和线程B 互相等待对方持有的锁导致程序无限死循环下去

  1. 两个线程里面分别持有两个Object对象:lock1 和 lock2。这两个lock作为同步代码块的锁
  2. 线程 1 的 run() 方法中同步代码块先获取 lock1 的对象锁,Thread.sleep(xxx),时间不需要太多,50毫秒,然后接着获取 lock2 的对象锁。这么做主要是为了防止线程 1 启动一下子就连续获得了 lock1 和 lock2 两个对象的 对象锁
  3. 线程2 的run() 方法中的同步代码块先获取 lock2 的对象锁,接着获取 lock1 的对象锁,当然这时 lock1 的对象锁 已经被线程1锁持有,线程2 肯定是要等待 线程1 释放 lock1 的对象锁

这样,线程1 睡完,线程2 已经获取了 lock2 的对象锁了,线程1 此时尝试获取 lock2 的对象锁,便被阻塞,此时一个死锁就形成了。

24.怎么唤醒一个阻塞的线程

如果线程是因为调用了 wait(),sleep()或者join() 方法而导致的阻塞,可以中断线程,并且通过抛出 InterruptedException 来唤醒它;

如果线程遇到了 IO 阻塞,无能为力。

25.不可变对象对多线程有什么帮助

不可变对象保证的对象内存的可见性,对不可变对象的读取不需要进行额外的同步手段,提升了代码执行效率。

26.什么是多线程的上下文切换

多线程的上下文切换是指 CPU 控制权由一个已经正在运行的线程切换到另外一个就绪并等待获取 CPU执行权的线程的过程。

27.如果你提交任务时,线程池队列已满,这时会发生什么

  1. 如果使用的是无界队列,LinkedBlockingQueue,继续添加任务到阻塞队列中等待执行,因为LinkedBlockingQueue可以近乎认为是一个无穷大的队列,可以无限存放任务
  2. 如果使用的是有界队列比如 ArrayBlockingQueue,任务首先会被添加到 ArrayBlockingQueue 中,ArrayBlockingQueue 满了,会根据 maximumPoolSize 的值增加线程数量,如果增加了线程数量还是处理不过来,ArrayBlockingQueue 继续满,那么则会使用拒绝策略 RejectedExecutionHandler 处理满了的任务,默认是 AbortPolicy

28.Java 中用到的线程调度算法是什么

抢占式。一个线程用完 CPU 之后,操作系统会根据线程优先级,线程饥饿情况等数据算出一个总的优先级并分配下一个时间片给某个线程执行

29.Thread.sleep(0)的作用是什么

由于Java 采用抢占式的线程调度算法,因此可能会出现某条线程常常获取到 CPU 控制权的情况,为了让某些优先级比较低的线程也能够获取到 CPU 的控制权,可以使用 Thread.sleep(0) 手动触发依此操作系统分配时间片的操作,也是平衡 CPU 控制权的一种操作。

30.什么是自旋

很多 synchronized 内的代码只是一些很简单的代码,执行时间非常快,此时等待的线程都加锁可能是一种不太值得的操作,因为线程阻塞涉及到用户态和内核态切换的问题。既然 synchronized 里面的代码执行的非常快,不妨让等待锁的线程不要被阻塞,而是在 synchronized 的边界做忙循环,这就是自旋。

31.什么是Java内存模型

Java内存模型定义了多线程访问Java内存的规范。简单总结一下Java内存模型的部分内容:

  1. Java内存模型将内存分为 主内存 和 工作内存。类的状态,也就是类之间共享的变量,是存储在内存中的,每次Java线程用到这些主内存中的变量的时候,会读一次主内存中的变量,并让这些内存在自己的工作内存中有一份拷贝,运行自己线程代码的时候,用到这些变量,操作的都是自己工作内存中的那一份。现成代码执行完毕之后,会将最新的值更新到主内存中去
  2. 定义了几个原子操作,用于操作主内存和工作内存中的变量
  3. 定义了 volatile 变量的使用规则
  4. happens-before,即先行发生原则,定义了操作A必然先行发生于操作B的一些规则。

32.什么是 CAS

CAS,全称为 Compare and Swap,即比较-替换。假设有三个操作数:内存值V,旧的预期值A,要修改的值B,当且仅当预期值A和内存值V相同时,才会将内存值修改为B并返回true,否则什么都不做并返回false。当然 CAS 一定要 volatile 变量配合,这样才能保证每次拿到的变量是主内存中最新的那个值,否则旧的预期值A对某条线程来说,永远是一个不会变的值A,只要某次CAS操作失败,永远都不可能成功。

33.什么是乐观锁和悲观锁

  1. 乐观锁,对于并发间操作产生的线程安全问题持乐观状态,乐观锁认为竞争不总是发生, 因此它不需要持有锁,将 比较 - 替换这两个动作作为一个原子操作尝试去修改内存中变量,如果失败则表示发生冲突,那么就应该有相应的重试逻辑。
  2. 悲观锁,对于并发间操作产生的线程安全问题持悲观状态,悲观锁认为竞争总是会发生,因此每次对某资源进行操作时,都会持有一个独占的锁,就像 synchronized,直接上了锁就操作资源了。

34.什么是AQS

AQS,全称为AbstractQueuedSynchronizer,翻译过来就是抽象队列同步器。
如果说 java.util.concurrent 的基础是 CAS的话,那么 AQS 就是整个Java 并发包的核心了ReentrantLock,CountDownLatch,Semaphore 等等都用到了它。

AQS 定义了对双向队列所有的操作,而只开放了 tryLock 和 tryRelease 方法给开发者使用,开发者可以根据自己的实现重写tryLock 和 tryRelease 方法,以实现自己的并发功能。

35.单例模式的线程安全性

单例模式的线程安全意味着:某个类的实例在多线程环境下只会被创建一次出来

  • 饿汉式单例模式的写法:线程安全
  • 懒汉式单例模式的写法:非线程安全
  • 双检锁单例模式的写法:线程安全

36.Semaphore 有什么作用

Semaphore 是一个信号量,它的作用是限制某段代码块的并发数。Semaphore 有一个构造函数,可以传入一个 int 型整数 n,表示某段代码最多只有 n 个线程可以访问,如果超出了 n,那么请等待,等待某个线程执行完毕这段代码块,下一个线程在进入。

37.HashTable 的 size() 方法中明明只有一条语句 return count,为什么还有做同步

  1. 同一时间只能有一条线程执行固定类的同步方法,但是对于类的非同步方法,可以多条线程同时访问。
  2. CPU执行代码,执行的不是Java代码。Java代码最终是被翻译成机器码执行的,机器码才是真正的可以和硬件电路交互的代码。即使你看到Java代码只有一行,甚至你看到Java代码编译之后生成的字节码也只有一行,也不意味着对于底层来说这句语句的操作只有一个。一句return count假设被翻译成三句汇编语言执行,完全可能执行完第一句,线程就切换了。

38.线程类的构造方法,静态块是被那个线程调用的

是被 new 这个线程类所在的线程所调用的,而 run() 方法里面的代码才是被线程自身所调用的。

39.同步方法和同步块,那个是更好的选择

同步块,这意味着同步块之外的代码是异步执行的,这比同步整个方法更提升代码的效率。同步范围越小越好

40.高并发,任务执行时间短的业务怎样使用线程池?并发不高,任务执行时间长的业务怎么使用线程池?并发高,任务执行时间长的业务怎样使用线程池?

  1. 高并发,任务执行时间短:线程池线程数可以设置为 CPU 核数 +1,减少线程上下文的切换
  2. 并发不高,任务执行时间长:假如是IO密集型业务,因为IO操作不占用CPU,可以加大线程池中的线程数目,让CPU处理更多的业务。假如是计算密集型业务,线程池中的线程数设置的少一些,减少线程上下文的切换。
  3. 并发高,任务执行时间长:看业务里某些数据能否做缓存;增加服务器;线程池设置;
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值