1 线程的创建
1.1 创建线程的方式
- 继承Thread
- 实现Runnable接口,多个线程可共享同一个资源
- 实现Callable接口,执行完线程后可拿到返回值
1.2 Runnable和Thread比较
- 实现了Runnable接口可以再继承其他类,继承Thread不可以再继承其他类
- Runnable可以实现多个线程去共享同一个资源,而Thread不适合
1.3 Runnable和Callable的区别
- 实现Callable接口能返回执行结果,实现Runnable接口不能返回结果;
- Callable接口的call()方法允许抛出异常,而Runnable接口的run()方法的异常只能在内部消化,不能继续上抛;
- Callable接口返回执行结果,需要调用FutureTask.get()方法实现,此方法会阻塞主线程直到获取结果;当不调用此方法时,主线程不会阻塞!
1.4 为什么不可以直接Runnable接口的run方法
Runnable相对于一个Task,并不具有线程的概念。直接去调用Runnable的run,相当于执行了一个函数而已,并未开启线程。必须先调用它start()开启线程,使得线程处于就绪状态,当线程获取cpu的执行权后,就可以执行run()。
2 线程的生命周期
2.1 线程的状态
- 新建-New:新建线程,调用start进入就绪状态
- 就绪-Runnable:获取cup执行权后进入运行状态
- 运行-Running:阻塞状态下阻塞时间到、获得同步锁、notify/notifyAll可进入
- 阻塞-Blocked:运行状态下调用IO阻塞方法、等待同步锁进入
- 等待-Waiting:运行状态下调用sleep/join/wait进入
- 有时间的等待-TimedWaiting:运行状态下调用sleep/join/wait(timeout)进入
- 终止-Terminated:run()执行完、Exception/Error.
2.2 线程的状态转换
2.3 sleep、wait、yield、join、notify/notifyAll
2.31 sleep()
- sleep()是Thread类的类方法,表示让一个线程进入睡眠状态,等待一定的时间之后,自动醒来进入到可运行状态。不会马上进入运行状态,因为线程调度机制恢复线程的运行也需要时间。
- 一个线程对象调用了sleep方法后,并不会释放其所持有的所有对象锁,不会影响其他线程对象的运行。
- sleep()可以在任何地方调用,在sleep过程中,可能会被其他对象调用它的interrupt()方法,产生Interrupted Exception异常。必须捕获这个异常,否则线程就会异常终止。
- sleep()方法是静态方法,只对当前对象有效。t.sleep()只会是使当前线程被sleep,而不是 t 线程。
2.32 wait()
- wait()是Object类的成员方法,用于暂停当前线程进入等待/阻塞状态,可指定时间。
- 一个对象调用了wait方法后,必须要用notify或notifyAll方法唤醒该线程。
- 线程调用了wait方法后,会释放该线程所持有的所有同步资源/锁,而不限于这个被调用了wait方法的对象。
- 在wait的过程中,也有可能被其他对象调用interrupt方法而产生Interrupted Exception异常。
- wait、notify 和 notifyAll 只能在同步方法或同步块中调用,且不需要捕获异常。
2.33 yield()
- yield()停止当前线程,进入就绪状态,让同等优先权或更高优先权的线程有执行的机会。若没有其他可执行的线程,会立刻又被执行。
2.34 join()
- join()用于在一个线程的执行过程中调用另一个线程,等到被调用的线程执行完后,再继续执行当前线程。主要用于暂停当前线程,等待被调用线程运行结束。
2.35 notify()/notifyAll()
- notify()只会唤醒一个等待(对象的)线程,并使该线程开始执行。若有多个线程等待一个对象,唤醒哪一个线程,取决于操作系统对多线程管理的实现。
- notifyAll()唤醒所有等待(对象的)线程,具体先执行哪一个线程取决于操作系统。
2.36 sleep和wait的区别
- sleep是Thread的类方法,wait是Object的成员方法;
- sleep没有释放锁,wait释放了锁;
- sleep可以在任何地方使用,wait只能在同步方法或同步块中使用;
- sleep必须捕获异常,wait不需要捕获异常。
2.37 sleep和yield的区别
- sleep()后线程转入阻塞(blocked)状态,yield()后线程转入就绪(ready)状态;
- sleep不考虑其他线程的优先级,yield只会给相同及更高优先级的线程运行机会;
- sleep方法需要参数,yield方法不需要参数;
- sleep方法声明抛出Interrupted Exception异常,yield方法没有声明任何异常;
- sleep方法比yield方法具有更好的可移植性,跟操作系统CPU调度相关。
2.38 注意事项
- wait时间到或被中断唤醒,不一定会继续执行或者跳到catch里,而是还需要等待获得锁。在执行notify()或notifyAll()方法后,当前线程不会马上释放该对象锁,需要等到notify()或notifyAll()方法所在的同步方法或同步代码块执行完成,当前线程才会释放锁。
- wait、notify和notifyAll方法是Object类的final native方法。所以这些方法不能被子类重写,Object类是所有类的超类,这些方法都只能在同步方法或同步块中调用。
- 在sleep()状态下interrupt()中断线程,会进入catch语句,并且清除停止状态值,使之变成false。
- wait(long)方法:如果线程在指定时间(long)内未被唤醒,则自动唤醒。wait(0)等价于wait()。
- Thread.Sleep(0)的作用是“触发操作系统立刻重新进行一次CPU竞争”。
- 一个线程是不允许start()启动两次的,第二次调用必然会抛出IllegalThread StateException,这是一种运行时异常,多次调用start被认为是编程错误。
3 线程池
3.1线程池的实现原理
池: 池指的就是"容器",这里容器就是集合。数据库连接池: List集合保存连接对象。线程池: List集合保存线程对象。
线程池内部维护了一个线程列表,我们使用线程池只需要定义好任务,然后提交给线程池,而不用关心该任务是如何执行、被哪个线程执行,以及什么时候执行。JDK1.5引入Executor线程池框架。
3.2 四种线程池
-
newFixedThreadPool():初始化一个指定线程数的线程池,其中corePoolSize == maxiPoolSize,使用LinkedBlockingQuene作为阻塞队列。
-
newCachedThreadPool():初始化一个可以缓存线程的线程池,默认缓存60s,线程池的线程数可达到Integer.MAX_VALUE,即2147483647,内部使用SynchronousQueue作为阻塞队列。
-
newSingleThreadExecutor():初始化只有一个线程的线程池,内部使用LinkedBlockingQueue作为阻塞队列。
-
newScheduledThreadPool():初始化的线程池可以在指定的时间内周期性的执行所提交的任务,在实际的业务场景中可以使用该线程池定期的同步数据。
3.3 线程池参数
- ThreadPoolExecutor全参构造:
public ThreadPoolExecutor(int corePoolSize,int maximumPoolSize,
long keepAliveTime,TimeUnit unit,BlockingQueue workQueue,
ThreadFactory threadFactory,RejectedExecutionHandler handler)
- corePoolSize - 池中所保存的线程数,包括空闲线程。
- maximumPoolSize - 池中允许的最大线程数。
- keepAliveTime - 当线程数大于核心时,此为终止前多余的空闲线程等待新任务的最长时间。
- unit - keepAliveTime 参数的时间单位。
- workQueue - 执行前用于保持任务的队列。此队列仅保持由 execute 方法提交的 Runnable 任务。
- threadFactory - 执行程序创建新线程时使用的工厂。
- handler - 由于超出线程范围和队列容量而使执行被阻塞时所使用的处理程序。
3.4 handle拒绝策略
任务处理顺序:核心线程-阻塞队列-普通线程-handle
- AbortPolicy:不执行新任务,直接抛出异常,提示线程池已满
- DisCardPolicy:不执行新任务,也不抛出异常
- DisCardOldSetPolicy:将消息队列中的第一个任务,替换为当前新进来的任务执行
- CallerRunsPolicy:直接调用execute来执行当前任务
3.5 任务调度框架:
- Spring Task框架(用的多),corn 时间表达式
- Quratz框架,corn 时间表达式
比如: 无效订单在30分钟后(从订单创建到持续30分钟结束,订单的付款状态),自动清除。
- 提交任务给线程池
- Executor.execute(Runnable command);
- ExecutorService.submit(Callable task);
- 线程池的关闭
- shutdown():不会立即终止线程池,而是要等所有任务缓存队列中的任务都执行完后才终止,但再也不会接受新的任务。
- shutdownNow():立即终止线程池,并尝试打断正在执行的任务,并且清空任务缓存队列,返回尚未执行的任务。
3.6 多线程辅助类CountDownLatch、CyclicBarrier
- CountDownLatch是一个同步的辅助类,允许一个或多个线程,等待其他一组线程完成操作,再继续执行, await()可以让线程等待,countDown() 每调用一次就减1,指定减到0所有等待的线程就开始执行。
- CyclicBarrier是一个同步的辅助类,允许一组线程相互之间等待,达到一个共同点,再继续执行。当所有线程都准备好再一起执行。
4 锁
4.1 synchronized 与 lock 的区别

- 存在层次:Lock是一个接口,synchronized是Java关键字,JVM层面的。
- 锁的释放:synchronized执行完同步代码或发生异常,会自动释放锁;而lock需要在finally中写入unlock,避免死锁的发生。
- 等待中断:lock等待锁过程中可以用interrupt来中断等待;synchronized只能等待锁的释放,不能中断。
- 获取锁状态:Lock可以通过trylock来知道有没有获取锁,而synchronized不能。
- 用途:Lock适合大量代码通途,synchronized适合少量代码同步。
- 当竞争资源非常激烈,即有大量线程同时竞争时,Lock的性能要远远优于synchronized。
- Lock可以提高多个线程进行读操作的效率,可以通过readwritelock实现读写分离。
- synchronized使用Object对象本身的wait 、notify、notifyAll调度机制,而Lock可以使用Condition进行线程之间的调度。
//Condition定义了等待/通知两种类型的方法
Lock lock=new ReentrantLock();
Condition condition=lock.newCondition();
...
condition.await();
...
condition.signal();
condition.signalAll();
4.2 ReadWriteLock锁
前置处理(获取锁定)
try{
实际操作
}finally{
后续处理(释放锁)
}
4.3 CAS(比较与交换,Compare and swap)
- CAS 是一种有名的无锁算法,无锁编程,即不使用锁的情况下实现多线程之间的变量同步,也就是在没有线程被阻塞的情况下实现变量的同步,所以也叫非阻塞同步(Non-blocking Synchronization)。
- 当多个线程尝试使用CAS同时更新同一个变量时,只有其中一个线程能更新变量的值,而其它线程都失败,失败的线程并不会被挂起,而是被告知这次竞争中失败,并可以再次尝试。
- CAS有3个操作数,内存值V,旧的预期值A,要修改的新值B。当且仅当预期值A和内存值V相同时,将内存值V修改为B,否则什么都不做。
4.4 其它问题
- 产生死锁的四个条件:互斥、请求与保持、不剥夺、循环等待。
- Java中有哪些同步方案:重量级锁、显式锁、并发容器、并发同步器、CAS、volatile、AQS等。
- 多个线程同时读写,读线程的数量远远⼤于写线程,你认为应该如何解决并发的问题?你会选择加什么样的锁?
- 采用Read-Write Lock Pattern是一种将对于共享资源的访问与修改操作分离,称为读写分离。用单独的线程来处理读写,允许多个读线程同时读,但是不能多个写线程同时写。
- wait/notify/notifyAll⽅法需不需要被包含在synchronized块中?这是为什么?
- 需要。因为wait/notify/notifyAll需要通过监视器操作对象锁,达到对对象锁得释放和获取。
- ExecutorService你一般是怎么⽤的?是每个Service放一个还是个项目放一个?有什么好处?
- Java线程池ExecutorService,如果有一套相同逻辑的多个任务的情况下,应用一个线程池是个好选择。如果项目中有多套不同的这种任务,那每套任务应该一个线程池不是很正常的吗。
- 两个线程设计题。记得一个是:t1,t2,t3,让t1,t2执行完才执行t3,原生实现。
- 使用线程的join() 方法控制。