多线程很复杂的,内容很多的,大块的大家去看我多线程的文章,基本都涵盖到了,这里记录下面是的一些问题,怎么回答,不会大块介绍知识点
大家在复习多线程时需要学习下面的内容:
- 线程池、SYNC和Lock锁机制、线程通信、volatile、ThreadLocal、CyclicBarrier、Atom包、CountDownLatch、AQS、CAS原理
下面这些我就不写了:
- atomic 与 volatile的区别?
- Thread的 notify()给notifyAll()的区别?
- notifiy()是唤醒的那一个线程?
- Thread.sleep()唤醒以后是否需要重新竞争?
- GC回收算法,及实现原理?
java 多线程基础部分
问:怎么唤醒一个阻塞的线程
答: - 如果线程是因为调用了 wait()、sleep()、join() 方法而导致的阻塞,可以中断线程,并且通过抛出 InterruptedException 来唤醒它;如果线程遇到了 IO 阻塞,无能为力,因为 IO 是操作系统实现的,Java 代码并没有办法直接接触到操作系统
问:Thread.sleep(0)的作用是什么
答: - 由于Java采用抢占式的线程调度算法,因此可能会出现某条线程常常获取到CPU控制权的情况,为了让某些优先级比较低的线程也能获取到CPU控制权,可以使用Thread.sleep(0)手动触发一次操作系统分配时间片的操作,这也是平衡CPU控制权的一种操作
问:创建线程的方式
- 继承Thread类
- 实现Runnable接口
问:start 方法和 run 方法的区别
答: - 只有调用了start()方法,才会表现出多线程的特性,不同线程的run()方法里面的代码交替执行。如果只是调用run()方法,那么代码还是同步执行的,必须等待一个线程的run()方法里面的代码全部执行完毕之后,另外一个线程才可以执行其run()方法里面的代码。
问:sleep 方法和 wait 方法有什么区别
答: - sleep方法和wait方法都可以用来放弃CPU一定的时间,不同点在于如果线程持有某个对象的监视器,sleep方法不会放弃这个对象的监视器,wait方法会放弃这个对象的监视器
问:如何在两个线程之间共享数据
答: - 通过在线程之间共享对象就可以了,然后通过wait/notify/notifyAll、await/signal/signalAll进行唤起和等待,比方说阻塞队列BlockingQueue就是为线程之间共享数据而设计的
问:wait 方法和 notify / notifyAll 方法在放弃对象监视器时有什么区别
答: - wait 方法立即释放对象监视器,notify / notifyAll 方法则会等待线程剩余代码执行完毕才会放弃对象监视器
问:wait、notify、notifyAll 方法注意事项
答: - wait、notify、notifyAll 方法在使用前都要求所在线程已经获取到目标锁才能执行,换言之就是只能在同步方法或是同步代码块中执行,wait 会立马释放锁,但是 notify、notifyAll 会在所在的同步方法或是同步代码执行完之后才释放锁,即便我们执行 notify 方法后 wait 阻塞的线程 也不能马上获得锁,需要等 notify 所在同步方法或是同步代码快执行完毕缩放锁之后 wiat 阻塞的线程才能拿到锁
java 多线程进阶部分
问:线程通信手段
- wait()、notify() 等待通知方式
- join() 方式
- volatile 共享内存
- CountDownLatch、CyclicBarrier 并发工具,功能和 join 相同
- interrupt() 线程响应中断
- 线程池 awaitTermination() ,等待线程池任务结束方式,这种方式需要关闭线程池才有效
- PipedWriter、PipedReader 管道通信方式
问:Semaphore有什么作用
答: - Semaphore就是一个信号量,它的作用是限制某段代码块的并发数。Semaphore有一个构造函数,可以传入一个int型整数n,表示某段代码最多只有n个线程可以访问,如果超出了n,那么请等待,等到某个线程执行完毕这段代码块,下一个线程再进入。由此可以看出如果Semaphore构造函数中传入的int型整数n=1,相当于变成了一个synchronized了。
问:同步方法和同步块,哪个是更好的选择
答: - 同步块,这意味着同步块之外的代码是异步执行的,这比同步整个方法更提升代码的效率。请知道一条原则:同步的范围越少越好。借着这一条,我额外提一点,虽说同步的范围越少越好,但是在Java虚拟机中还是存在着一种叫做锁粗化的优化方法,这种方法就是把同步范围变大。这是有用的,比方说StringBuffer,它是一个线程安全的类,自然最常用的append()方法是一个同步方法,我们写代码的时候会反复append字符串,这意味着要进行反复的加锁->解锁,这对性能不利,因为这意味着Java虚拟机在这条线程上要反复地在内核态和用户态之间进行切换,因此Java虚拟机会将多次append方法调用的代码进行一个锁粗化的操作,将多次的append的操作扩展到append方法的头尾,变成一个大的同步块,这样就减少了加锁–>解锁的次数,有效地提升了代码执行的效率
问:怎么检测一个线程是否持有对象监视器
答: -Thread类提供了一个 holdsLock(Object obj) 方法,当且仅当对象obj的监视器被某条线程持有的时候才会返回true,注意这是一个static方法,这意味着“某条线程”指的是当前线程。
问:ConcurrentHashMap的并发度是什么
答: - 是指 segment的大小,默认为16,这意味着最多同时可以有16条线程操作 ConcurrentHashMap,这也是 ConcurrentHashMap 对 Hashtable 的最大优势,任何情况下,Hashtable 能同时有两条线程获取 Hashtable中的数据吗?
问:如果你提交任务时,线程池队列已满,这时会发生什么
答: - 如果你使用的 LinkedBlockingQueue,也就是无界队列的话,没关系,继续添加任务到阻塞队列中等待执行,因为 LinkedBlockingQueue 可以近乎认为是一个无穷大的队列,可以无限存放任务;如果你使用的是有界队列比方说 ArrayBlockingQueue 的话,任务首先会被添加到 ArrayBlockingQueue 中,ArrayBlockingQueue 满了,则会使用拒绝策略 RejectedExecutionHandler 处理满了的任务,默认是 AbortPolicy
问:什么是AQS简单说一下AQS
答: - AQS全称为AbstractQueuedSychronizer,翻译过来应该是抽象队列同步器。如果说java.util.concurrent的基础是CAS的话,那么AQS就是整个Java并发包的核心了,ReentrantLock、CountDownLatch、Semaphore等等都用到了它。AQS实际上以双向队列的形式连接所有的Entry,比方说ReentrantLock,所有等待的线程都被放在一个Entry中并连成双向队列,前面一个线程使用ReentrantLock好了,则双向队列实际上的第一个Entry开始运行。AQS定义了对双向队列所有的操作,而只开放了tryLock和tryRelease方法给开发者使用,开发者可以根据自己的实现重写tryLock和tryRelease方法,以实现自己的并发功能
问:什么是CAS
答: - 全称为Compare and Set,即比较-设置。假设有三个操作数:内存值V、旧的预期值A、要修改的值B,当且仅当预期值A和内存值V相同时,才会将内存值修改为B并返回true,否则什么都不做并返回false。当然CAS一定要volatile变量配合,这样才能保证每次拿到的变量是主内存中最新的那个值,否则旧的预期值A对某条线程来说,永远是一个不会变的值A,只要某次CAS操作失败,永远都不可能成功,关于 cas 我由一篇文章请去查看
问:单例模式的线程安全性
答: - 老生常谈的问题了,首先要说的是单例模式的线程安全意味着:某个类的实例在多线程环境下只会被创建一次出来。单例模式有很多种的写法,我总结一下:
- 饿汉式单例模式的写法:线程安全
- 懒汉式单例模式的写法:非线程安全
- 双检锁单例模式的写法:线程安全
问:Hashtable 的 size() 方法中明明只有一条语句”return count”,为什么还要做同步?
答: - size() 方实际涉及到了数据的读取,所以和读写操作一样都要考虑并发问题,你在获取 size 的时候
android 自身部分
问:子线程怎么开启线程通信,如果不使用了需要关闭吗?
答: - 在子线程looper.Prepare() 方法之后就创建了looper和MessageQueue ,然后looper.loop()开启我们的循环,这个时候我们在使用handler,才能达到线程通信的效果。如果不需要使用了一定要关闭,(凡是消耗性能的东西确定不需要使用了就关闭)Looper.quit()可以退出looper;还有使用handler 的时候防止内存泄漏
问:简单概述下 handle
- 先说 handle 的4个角色:
- Handler - 消息发送器,内部持有一个 MessageQueue 消息队列,在发送消息时把 message.obj 指向 handle 自己,这样实现最终的消息执行
- messageQueue - 消息队列,消息的储存单位
- Looper - 遍历 MessageQueue 队列,启动主线程的阻塞式死循环
- message - 通信的消息实体,使用 handle 发送消息时,其内部的 obj 会被指向该 handle 的引用
- 再说下逻辑过程:
- 开启循环 - 每个线程只有一个Looper,用来阻塞式循环,每个Looper对应一个MessgeQueue;无限的循环遍历MessageQueue,如果里边有消息就去处理消息,消息处理完继续循环,这样就一直循环下去,也是我们程序为什么不会退出的原因
- 发送消息 - handler创建的时候会根据线程去绑定,拿到对应线程的队列looper和MessageQueue,发送消息的过程就是在其他线程把Message放到MessageQueue当中
- 回调消息 - handler发送消息的时候会对Message消息打上tag,当looper遍历到Message对象,这个时候已经到了主线程,Message.tag就拿到了handler对象,然后回调对应的方法handler.handleMessage
问:子线程中更新 UI 的方式
- handler.handleMessage(msg)
- handler.post(Runnable r)
- view.post(Runnable r)
- Activity的 runOnUiThread(Runnable r)
问:CyclicBarrier 和 CountDownLatch的区别
答: - 两个看上去有点像的类,都在java.util.concurrent下,都可以用来表示代码运行到某个点上,二者的区别在于:
- CyclicBarrier的某个线程运行到某个点上之后,该线程即停止运行,直到所有的线程都到达了这个点,所有线程才重新运行,CountDownLatch则不是,某线程运行到某个点上之后,只是给某个数值-1而已,该线程继续运行
- CyclicBarrier只能唤起一个任务,CountDownLatch可以唤起多个任务
- CyclicBarrier可重用,CountDownLatch不可重用,计数值为0该CountDownLatch就不可再用了
问:一个线程如果出现了运行时异常会怎么样
答: - 如果这个异常没有被捕获的话,这个线程就停止执行了。另外重要的一点是:如果这个线程持有某个某个对象的监视器,那么这个对象监视器会被立即释放