- 并行和并发的区别
- 守护线程
- 创建线程的方式
- 线程有哪些状态
- Sleep和Wait的区别
- notify()和notifyAll()的区别
- run()和start()的区别
- 阿里为什么禁止使用Executors创建线程池
- 线程池的种类
- 线程池有哪些状态
- submit()和execute()的区别
- 怎么保证多线程的安全问题
- 多线程锁的升级原理
- 死锁
- 什么是活锁
- 什么是无锁
- ThreadLocal
- Synchronize的底层实现原理
- Synchronize和volatile的区别
- Synchronize和Lock的区别
- 线程同步有几种方法
- 多线程之间如何进行通信
- T1、T2、T3 三个线程,如何保证按顺序执行?
- join()方法
- CyclicBarrier 和 CountDownLatch 的区别
- 怎么控制同一时间只有 3 个线程运行
- atomic的原理
并行和并发的区别
并行:多个时间在同一时刻发生;并发:多个时间在同一时间间隔发生。
守护线程
是服务线程的线程,JVM的垃圾回收线程就是典型的守护线程。其特点是如果其他线程都消亡了,守护线程也会死亡
创建线程的方式
除了继承Thread
和实现Runnable
外,还可以通过Callable
和Future
创建线程。创建接口Callable
的实现类,并重写call
方法,并且有返回值。并可以使用ExecutorService
的submit
方法来提交任务。或者使用FutureTask
类来包装Callable
对象,最为Thread对象的入参创建线程。
线程有哪些状态
创建、就绪、运行、阻塞、死亡
- 创建状态:在生成线程对象,且没有调用start()方法前。
- 就绪状态:
- 线程调用了start()方法,该线程就进入了就绪状态。但是此时,线程调度程序还没有把该线程设置为当前线程。
- 在线程运行之后,从等待或者睡眠中回来之后,也处于就绪状态
- 运行状态:开始执行run方法
- 阻塞状态:正在运行的线程,为了等待某个事件的发生(比如某项资源)之后再继续运行。sleep、wait等方法都可以导致线程阻塞
- 死亡状态:run方法执行结束,或者调用了stop()方法
从另一个角度看,线程的状态包括:
- 初始(NEW):新创建了一个线程对象,但还没有调用start()方法。
- 运行(RUNNABLE):Java线程中将就绪(ready)和运行中(running)两种状态笼统的称为“运行”。线程对象创建后,其他线程(比如main线程)调用了该对象的start()方法。该状态的线程位于可运行线程池中,等待被线程调度选中,获取CPU的使用权,此时处于就绪状态(ready)。就绪状态的线程在获得CPU时间片后变为运行中状态(running)。
- 阻塞(BLOCKED):表示线程阻塞于锁。
- 等待(WAITING):进入该状态的线程需要等待其他线程做出一些特定动作(通知或中断)。
- 超时等待(TIMED_WAITING):该状态不同于WAITING,它可以在指定的时间后自行返回。
-
Sleep和Wait的区别
Sleep被调用后,线程会让出CPU,但由于Sleep是静态方法,所以不会释放对象锁。
Wait是Object类的Native方法,当
synchronized
调用了wait方法后,线程进入阻塞状态,并进入到一个和该对象相关的等待池,同时释放锁。可以通过notify和notifyAll方法唤醒等待的线程。共同点:都是
Object
类的方法,在Synchronize
块中调用,都是将线程从某个对象的等待队列中移到锁池中,使得该线程重新具备竞争锁的资格区别:
notify()
方法随机将一个线程移到锁池中,而notifyAll
是将等待池中所有的线程都移到锁池中run()和start()的区别
start()方法时
Thread
类的一个方法。在主线程中,调用Thread类对象的start()方法使得线程进入就绪状态,也即启动线程- run()是Runnable接口定义的方法,
Thread
类是实现Runnable
接口的。run()
方法定义了线程的主要功能,但如果直接在主线程中调用run()方法的话,相当于调用了一个普通的方法,并不会启动一个线程。阿里为什么禁止使用Executors创建线程池
由于ThreadPoolExecutor
有7个不同的参数,而使用Executors可以很方便的创建不同种类的线程池。之所以被禁止的原因是,其创建出来的线程池使用的全都是无界队列,可以无限保留任务,导致OOM问题
- FixedThreadPool和SingleThreadExecutor => 允许的请求队列长度为Integer.MAX_VALUE,可能会堆积大量的请求,从而引起
OOM
异常 - CachedThreadPool => 允许创建的线程数为Integer.MAX_VALUE,可能会创建大量的线程,从而引起
OOM
异常
线程池的种类
- newFixedThreadPool(int nThreads)
创建一个固定长度的线程池,每当提交一个任务就创建一个线程,直到达到线程池的最大数量,这时线程规模将不再变化,当线程发生未预期的错误而结束时,线程池会补充一个新的线程。
- newCachedThreadPool()
创建一个可缓存的线程池,如果线程池的规模超过了处理需求,将自动回收空闲线程,而当需求增加时,则可以自动添加新线程,线程池的规模不存在任何限制。
- newSingleThreadExecutor()
这是一个单线程的Executor,它创建单个工作线程来执行任务,如果这个线程异常结束,会创建一个新的来替代它;它的特点是能确保依照任务在队列中的顺序来串行执行。
- newScheduledThreadPool(int corePoolSize)
创建了一个固定长度的线程池,而且以延迟或定时的方式来执行任务,类似于Timer。
线程池有哪些状态
Running、Shutdown、Stop、Tdiing、Terminated
submit()和execute()的区别
- 接受的参数不一样,submit的入参是Callable对象,而execute的入参是Runnable对象
- submit有返回值,execute没有
submit方法会抛出线程内部的异常。其实现原理是把异常保存在成员变量中,在FutureTask.get阻塞获取的时候再把异常抛出来
怎么保证多线程的安全问题
原子性:提供互斥访问,同一个时刻只能有一个线程对数据进行操作(atomic,synchronize)
- 有序性:由于指令重排序,导致的有序性问题。volatile可以禁止指令重拍,以及happen-before原则
- 可见性:一个线程对主内存的修改可以及时被其他线程看到(volatile,synchronize)
多线程锁的升级原理
- 互斥:不同进程对资源的互斥访问
- 请求并保持:进程已经申请到一些资源,在申请其他资源时,由于这些资源已经被其他进程持有,所以被阻塞,且不释放已经持有的资源
- 不可剥夺:进程申请到的资源,只能自行释放,不能被剥夺
循环等待:是指发生死锁后,多个进程之间形成一种环形的循环等待资源的关系
预防死锁的方法
固定加锁顺序:所有进程必须按照确定的顺序获取锁。它只有获得了从顺序上排在前面的锁之后,才能获取后面的锁。也就可以破坏循环等待的条件
- 设置加锁时间:尝试加锁时设置一个超时时间(java中可以使用
tryLock())
,如果在指定的时间内没有成功获得锁,则释放其持有的锁,等待随机时间后再重试。可以破坏请求并保持条件 死锁检测:不能算是预防,而是用来检测是否发生死锁。java中提供了两种方法,分别是JconsoleJDK和jstackJDK
Synchronize造成死锁的例子
public class LeftRightDeadlock {
private final Object left = new Object();
private final Object right = new Object();
public void leftRight() {
// 得到left锁
synchronized (left) {
// 得到right锁
synchronized (right) {
doSomething();
}
}
}
public void rightLeft() {
// 得到right锁
synchronized (right) {
// 得到left锁
synchronized (left) {
doSomethingElse();
}
}
}
}
什么是活锁
活锁与死锁相反,死锁是大家都拿不到资源都占用着对方的资源,而活锁是拿到资源却又相互释放不执行。当多线程中出现了相互谦让,都主动将资源释放给别的线程使用,这样这个资源在多个线程之间跳动而又得不到执行,这就是活锁。
什么是无锁
无锁,即没有对资源进行锁定,即所有的线程都能访问并修改同一个资源,但同时只有一个线程能修改成功。无锁典型的特点就是一个修改操作在一个循环内进行,线程会不断的尝试修改共享资源,如果没有冲突就修改成功并退出否则就会继续下一次循环尝试。所以,如果有多个线程修改同一个值必定会有一个线程能修改成功,而其他修改失败的线程会不断重试直到修改成功。JDK的CAS原理及应用即是无锁的实现。
可以看出,无锁是一种非常良好的设计,它不会出现线程出现的跳跃性问题,锁使用不当肯定会出现系统性能问题,虽然无锁无法全面代替有锁,但无锁在某些场合下是非常高效的。ThreadLocal
ThreadLocal是什么
线程局部变量是局限于线程内部的变量,属于线程自身所有,不会被其他线程共享。Java提供的ThreadLocal来支持线程局部变量,是一种线程安全的方式。
ThreadLocal使用场景
可以用来减少同一个线程内的不同方法使用的公共变量的传递的复杂度。
语雀内容Synchronize的底层实现原理
Synchronize和volatile的区别
Synchronize和Lock的区别
- synchronized是关键字,lock是接口
- 锁的释放:异常发生后,synchronized会自动释放锁,而lock不会,需要手动unlock
- synchronized是线程独占,其他线程需要阻塞等待,如HashTable之所以效率不好,就是因为方法都是用了synchronize修饰。lock是用更为灵活,可以自由控制临界资源
- 实现方式:synchronize是托管给jvm执行的,而lock是java写的控制锁的代码
- synchronized原始采用的是CPU悲观锁机制,即线程获得的是独占锁。独占锁意味着其他线程只能依靠阻塞来等待线程释放锁。而在CPU转换线程阻塞时会引起线程上下文切换,当有很多线程竞争锁的时候,会引起CPU频繁的上下文切换导致效率很低。而Lock用的是乐观锁方式。所谓乐观锁就是,每次不加锁而是假设没有冲突而去完成某项操作,如果因为冲突失败就重试,直到成功为止。乐观锁实现的机制就是CAS操作(Compare and Swap)。我们可以进一步研究ReentrantLock的源代码,会发现其中比较重要的获得锁的一个方法是compareAndSetState。这里其实就是调用的CPU提供的特殊指令。
- Lock如ReentrantLock底层调用的是Unsafe的park方法加锁,Synchronize操作的是对象头中的mark word
线程同步有几种方法
Synchronize和lock锁实现,不包括volatile多线程之间如何进行通信
notify/notifyAllT1、T2、T3 三个线程,如何保证按顺序执行?
使用join()方法。主线程等待所有其他线程执行完成也是用join()方法join()方法
join用于让当前执行线程等待join线程执行结束。其实现原理是不停检查join线程是否存活,如果存活则让当前线程永远等待。直到join线程终止后,线程的this.notifyAll()方法会被调用。CyclicBarrier 和 CountDownLatch 的区别
两个看上去有点像的类,都在 java.util.concurrent 下,都可以用来表示代码运行到某个点上,二者的区别在于:
- CyclicBarrier 的某个线程运行到某个点上之后,该线程即停止运行,直到所有的线程都到达了这个点,所有线程才重新运行;CountDownLatch 则不是,某线程运行到某个点上之后,只是给某个数值-1 而已,该线程继续运行
- CyclicBarrier 只能唤起一个任务,CountDownLatch 可以唤起多个任务
- CyclicBarrier 可重用,CountDownLatch 不可重用,计数值为 0 该 CountDownLatch就不可再用了
怎么控制同一时间只有 3 个线程运行
使用semaphore(信号量)。信号量是用来控制同时访问特定资源的线程数量。其构造方法Semaphore(int permits)
接受一个整形的数字,表示可用的许可证数量,也就是最大并发数。用法也很简单,首先线程使用acquire
方法获取一个许可证,使用完之后调用release
方法归还许可证。atomic的原理
- TODO