您的位置 首页 java

Java高级工程师必会知识之JavaSE(中)

  1. Java高级工程师必会知识之JavaSE(中)

(一)Java的线程和 线程池

技能要求:熟练掌握Java多线程编程技术,对线程池有较深对理解。

面试频次:高。面试难度:高。

Tips:多线程是面试一线互联网企业或想获取高薪的必掌握知识。

1. 创建线程的方式有几种、如何启动一个新线程、调用start和run方法的区别

创建线程有两种方式,第一种:自定义类继承Thread类,覆写run()方法。第二种:自定义类实现Runnable接口,覆写run()方法,然后将Runnable对象作为参数传递给Thread类的 构造函数

但是启动一个新线程只有一种方式:创建一个Thread事例,然后调用它的start()方法。

只有通过调用start()方法,JVM才会创建一个新的线程,在新线程中执行run()方法。如果直接调用run()方法,并不会开辟新线程,只会在当前线程中执行run方法中,这样就失去多线程本质的意义了。

2. 线程有哪几种状态以及各种状态之间的转换

在Thread类中,线程状态是通过threadStatus属性以及State枚举类实现的,共有6种状态:

NEW:(Thread state for a thread which has not yet started),线程对象还未被调用start()方法。

RUNNABLE:(A thread in the runnable state is executing in the JVM but it may be waiting for other resources from the operating system such as processor.)正在被JVM执行的线程,但是也可能会等待操作系统的处理器资源。

BLOCKED:(A thread in the blocked state is waiting for a monitor lock to enter a synchronized block/method or reenter a synchronized block/method after calling Object.wait})当线程等待进入同步代码块/方法的时候属于阻塞状态。

WAITING:当调用Object.wait()、Thread.join()、LockSupport.park()后会导致线程处于等待状态。

TIMED_WAITING:明确等待时长时的状态。

TERMINATED:(The thread has completed execution.)执行完任务的线程。

线程状态

3. wait()和sleep()的区别

Object#wait(long timeoutMillis) 方法官方解释:Causes the current thread to wait until it is awakened, typically by being notified or interrupt ed, or until a certain amount of real time has elapsed。

该方法导致当前线程处于等待状态,直到被其他线程唤醒或终止或者指定timeoutMillis毫秒时间结束。

Thread#sleep(long millis)方法官方解释:Causes the currently executing thread to sleep (temporarily cease execution) for the specified number of milliseconds, subject to the precision and accuracy of system timers and schedulers. The thread does not lose ownership of any monitors.)

该方法导致当前正在执行的线程睡眠(临时放弃执行)指定毫秒长度,其精度和准确性受制于系统定时器和调度器。当前线程会让出CPU资源,但是不会放弃它所持有的监视器锁。这与同时让出CPU资源和监视器锁资源的wait方法是不一样的。

Tips:wait(0)代表无限期等待,sleep(0)代表让线程睡眠0毫秒,其意义在于让其他线程有机会优先执行,相当于一个让位动作。

4. 如何中断一个线程

Java中的Thread类虽然有stop方法可以中断一个线程,但是由于可能会导致资源无法正常释放等问题,已经被废弃。

推荐方式一:

中断线程最好的方式就是结合线程的业务逻辑,通过设置标记位,让线程任务提前结束。比如伪代码如下:

 1. //volatile修饰符用来保证其它线程读取的总是该变量的最新的值
2. public volatile boolean exit = false;
3. @Override
4. public void run() {
5. while(!exit){
6. ...//业务逻辑
7. }
8. }  

推荐方式二:

使用Thread类的interrupt()方法。

用 interrupt()方法仅仅是在当前线程中打一个停止的标记,线程并不会立即终止线程,而是通知目标线程,有人希望你终止,至于线程收到通知后会如何处理,则完全由线程自行决定。比如如下代码演示了不能终止的情形:

 1. public static void main(String[] args) throws InterruptedException {
2.
3. Thread thread = new Thread(new Runnable() {
4. @Override
5. public void run() {
6. long startTime = System.currentTimeMillis();
7. for (int i=0;i<1000000;i++){
8. System.out.println("任务正在执行中:"+i);
9. }
10. System.out.println("任务执行完毕,耗时:"+(System.currentTimeMillis() - startTime));
11. }
12. });
13. //启动子线程
14. thread.start();
15. //让主线程睡眠200毫秒,然后中断子线程
16. Thread.sleep(200);
17. //终止线程
18. thread.interrupt();

19. }  

代码结果输出如下:

调用intercept()方法后子线程依然继续执行完任务

如果想终止任务需要需要使用Thread.isInterrupted()方法:

 1. Thread thread = new Thread(new Runnable() {
2. @Override
3. public void run() {
4. long startTime = System.currentTimeMillis();
5. for (int i=0;i<1000000;i++){
6. //获取当前线程的状态是否已经标记为终止
7. if (Thread.interrupted()){
8. //如果线程已经标记为终止则跳出循环(其实就是结束任务了)
9. break;
10. }
11. System.out.println("任务正在执行中:"+i);
12. }
13. System.out.println("任务执行完毕,耗时:"+(System.currentTimeMillis() - startTime));
14. }
15. });  

在上面这段代码中,我们增加了 Thread.isInterrupted() 来判断当前线程是否被中断了,如果是,则退出 for 循环,结束线程。

程序运行结果如下:

线程被正确终止

Tips:当线程位于sleep或wait状态时,终止线程时会抛出一个 InterruptedException 异常,同时会清除中断标记。比如对上面对run方法进行修改:

 1. Thread thread = new Thread(new Runnable() {
2. @Override
3. public void run() {
4. long startTime = System.currentTimeMillis();
5. for (int i=0;i<1000000;i++){
6. //获取当前线程的状态是否已经标记为终止
7. if (Thread.interrupted()){
8. //如果线程已经标记为终止则跳出循环(其实就是结束任务了)
9. break;
10. }
11. try {
12. //如果线程正在睡眠状态,被终止时被抛出InterruptedException异常,并且会清除interrupt标识
13. Thread.sleep(10);
14. System.out.println("任务正在执行中:"+i);
15. } catch (InterruptedException e) {
16. e.printStackTrace();
17. }
18.
19. }
20. System.out.println("任务执行完毕,耗时:"+(System.currentTimeMillis() - startTime));
21. }
22. });  

这个时候线程就无法终止,需要想终止,就需要在第15行代码中,也就是catch代码块中加入结束循环对操作,让线程结束。

 1. Thread thread = new Thread(new Runnable() {
2. @Override
3. public void run() {
4. long startTime = System.currentTimeMillis();
5. for (int i=0;i<1000000;i++){
6. //获取当前线程的状态是否已经标记为终止
7. if (Thread.interrupted()){
8. //如果线程已经标记为终止则跳出循环(其实就是结束任务了)
9. break;
10. }
11. try {
12. //如果线程正在睡眠状态,被终止时被抛出InterruptedException异常,并且会清除interrupt终止位
13. Thread.sleep(10);
14. System.out.println("任务正在执行中:"+i);
15. } catch (InterruptedException e) {
16. e.printStackTrace();
17. //当线程被终止时会遇到异常,在异常中终止线程
18. break;
19. }
20. }
21. System.out.println("任务执行完毕,耗时:"+(System.currentTimeMillis() - startTime));
22. }
23. });  

运行结果如下:

线程被成功终止

5. 线程池的种类和执行流程

先说一下使用线程池的优点:

1. 降低资源消耗。通过重复利用已创建的线程降低线程创建、销毁造成的消耗。

2. 提高响应速度。当任务到达时,任务可以不需要等到线程创建就能立即执行。

3. 提高线程的可管理性。线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一的分配、调优和监控。

JDK(8)中提供了Excutors工具类,该类有如下工厂方法,用于创建不同类型的线程池对象:

线程池的种类

接下来重点说ThreadPoolExecutor类,它有7个核心参数,如下:

 1. public ThreadPoolExecutor(int corePoolSize,
2. int maximumPoolSize,
3. long keepAliveTime,
4. TimeUnit unit,
5. BlockingQueue<Runnable> workQueue,
6. ThreadFactory threadFactory,
7. RejectedExecutionHandler handler) {  

1. corePoolSize

核心池大小,除非设置了 allowCoreThreadTimeOut 否则哪怕线程超过空闲时间,池中也要最少要保留这个数目的线程。

需要注意的是,corePoolSize所需的线程并不是立即创建的,需要在提交任务之后进行创建,所以如果有大量的缓存线程数可以先提交一个空任务让线程池将线程先创建出来,从而提升后续的执行效率。

2. maximumPoolSize

允许的最大线程数。

3. keepAliveTime

空闲线程空闲存活时间,核心线程需要 allowCoreThreadTimeOut 为true才会退出。

4. Unit

与 keepAliveTime 配合,设置 keepAliveTime 的单位,如:毫秒、秒。

5. workQueue

线程池中的任务队列。上面提到线程池的主要作用是复用线程来处理任务,所以我们需要一个队列来存放需要执行的任务,在使用池中的线程来处理这些任务,所以我们需要一个任务队列。

6. threadFactory

当线程池判断需要新的线程时通过线程工程创建线程。

7. handler

执行被阻止时的处理程序,线程池无法处理。这个与任务队列相关,比如队列中可以指定队列大小,如果超过了这个大小默认情况下会抛出 RejectedExecutionException 异常。

当向线程池提交一个新的任务时,线程池有三种处理情况,分别是:创建一个工作线程来执行该任务、将任务加入阻塞队列、拒绝该任务。

线程池工作流程

1、如果正在运行的线程数量小于 corePoolSize,那么马上创建线程运行这个任务

2、如果正在运行的线程数量大于或等于 corePoolSize,那么将这个任务放入队列

3、如果这时候队列满了,而且正在运行的线程数量小于 maximumPoolSize,那么还是要创建非核心线程立刻运行这个任务

4、如果队列满了,而且正在运行的线程数量大于或等于 maximumPoolSize,那么线程池会抛出异常RejectExecutionException

6. ThreadLocal 的原理

  1. ThreadLocal的作用
  • 在进行对象跨层传递的时候,使用ThreadLocal可以避免多次传递,打破层次间的约束。
  • 线程间数据隔离
  • 进行事务操作,用于存储线程事务信息。
  • 数据库连接,Session会话管理。
    1. ThreadLocal的实现原理

    先温习一下,ThreadLocal的使用方法:

     1. public class ThreadLocalDemo {
    2. //声明threadLocal对象
    3. private static ThreadLocal<String> threadLocal = new ThreadLocal<>();
    4. public static void main(String[] args) {
    5. //往threadLocal存入值
    6. threadLocal.set("main");
    7. new Thread(()->{
    8. //往threadLocal存入值
    9. threadLocal.set("sub");
    10. try {
    11. Thread.sleep(200);
    12. } catch (InterruptedException e) {
    13. e.printStackTrace();
    14. }
    15. //从threadLocl中取值
    16. String subVal = threadLocal.get();
    17. System.out.println("在子线程中获取的值:"+subVal);
    18. }).start();
    19. //从threadLocal中取值
    20. String mainVal = threadLocal.get();
    21. System.out.println("在主线程中获取的值:"+mainVal);
    22. }
    23. }  

    ThreadLocal使用演示

    上面代码演示了ThreadLocal线程间隔离的功能,也就是在主线程中设置的值,只能在主线程中取出来,在子线程中设置的值只能在子线程中取出来。

    想搞明白其原理,需要从ThreadLocal的set()和get()方法源码分析:

    set()方法

    在set方法中,先获取当前线程t,然后通过getMap(t)方法获取ThreadLocalMap对象map,如果map存在则直接设置值value,否则则创建并设置值value。

    getMap()方法

    getMap(t)方法其实非常简单,直接返回了t的threadLocals属性。

    hread类有个threadLocals变量

    从这里我们可以得知,其实每个Thread对象内部都有一个threadLocals变量,其类型是ThreadLocal.ThreadLocalMap。

    ThreadLocalMap的内部类Entry

    ThreadLocalMap有一个内部类Entry,是键值对结构,key是ThreadLodcal对象本身,value就是用户设置的value。

    我们还能看到Entry继承自WeakReference,在Entry的构造函数中看到,Entry对象是以弱引用的形式引用了ThreadLocal对象。

    createMap方法

    在createMap()方法中,调用ThreadLocalMap的构造函数创建对象并且赋值给当前线程t的成员变量threadLocals。这里能看到key时当前对象this,其实就是我们使用的ThreadLocal对象。

    ThreadLocalMap的构造函数

    ThreadLocalMap的构造函数中可以看到,初始化了长度为16的数组Entry[16]命名为table,然后把新创建的Entry对象放入数组中,根据key的hash低四位打散存储。因此我们可以确定的是每个Thread可以通过不同的ThreadLocal对象绑定多个值,这些值存储在Entry[]中。

    get()方法

    在get()中,先获取当前线程t,然后获取线程t的成员变量map,然后以当前ThreadLocal对象作为key,获取Entry,如果Entry不为null,则获取Entry的value。

    总结两点:

    ① 每个Thread内部有一个ThreadLocalMap类型的成员变量threadLocals,ThreadLocalMap内部有个Entry[]数组,用于存放绑定的值。

    ② ThreadLocal本身并不存储值,它只是作为一个key来让线程从ThreadLocalMap中获取value。

    我们可以简单的这样理解,ThreadLocal对象就好比是一个用于给Thread对象绑定值的工具类,当调用ThreadLocal对象的set(v)方法时,它会先获取当前线程对象thread,然后找到thread对象自己内部的ThreadLocalMap对象 map,然后把ThreadLocal对象作为key,v作为值存进map中,map中是用Entry。

    3) ThreadLocal的内存泄漏问题避免

    从源码中我们可以看到,ThreadLocal对象是被Thread类中的ThreadLocalMap.Entry对象以弱引用的方式引用的,那么当把ThreadLocal设置为null时,ThreadLocal对象的可见行就变成了弱可见,弱可见对象的特点是当GC发现时就会被回收,但是ThreadLocalMap生命周期和Thread是一样的,只要Thread还运行就不会被GC回收,这时候就会导致Thread#ThreadLocalMap#Entry这个对象继续存在,且无法被使用,这就是所谓的内存泄漏。

    简单说就是,如果把ThreadLocal对象设置为null了,但是通过其绑定的value还依然绑定在Thread中,但是却无法使用这个值,这就是所谓的内存泄漏。

    解决办法:在设置ThreadLocal为null前,先执行ThreadLocal的remove操作,把值销毁,避免出现内存溢出情况。

    7. 同步锁、 死锁 、乐观锁、悲观锁

    面试频次:高

    同步锁:

    当多个线程同时访问同一个数据时,很容易出现问题。为了避免这种情况出现,我们要保证线程同步互斥,就是指并发执行的多个线程,在同一时间内只允许一个线程访问共享数据。Java 中可以使用 synchronized 关键字来取得一个对象的同步锁。

    死锁:

    何为死锁,就是多个线程同时被阻塞,它们中的一个或者全部都在等待某个资源被释放。

    死锁案例:

     1. public class DeadLock {
    2. public static void main(String[] args) throws InterruptedException {
    3. MyRunnable myRunnable = new MyRunnable();
    4. new Thread(myRunnable).start();
    5. new Thread(myRunnable).start();
    6. }
    7. static class MyRunnable implements Runnable{
    8.
    9. private Object objA = new Object();
    10. private Object objB = new Object();
    11.
    12. @Override
    13. public void run() {
    14. synchronized (objA){
    15. System.out.println("嵌套1:objA");
    16. synchronized (objB){
    17. System.out.println("嵌套1:objB");
    18. }
    19. }
    20. synchronized (objB){
    21. System.out.println("嵌套2:objB");
    22. synchronized (objA){
    23. System.out.println("嵌套2:objA");
    24. }
    25. }
    26.
    27. }
    28. }
    29. }  

    乐观锁:

    总是假设最好的情况,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,可以使用版本号机制和CAS(Compare And Set/Swap)算法实现。乐观锁适用于多读的应用类型,这样可以提高吞吐量。在Java中java.util.concurrent.atomic包下面的原子变量类就是使用了乐观锁的一种实现方式CAS实现的。

    悲观锁:

    总是假设最坏的情况,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会阻塞直到它拿到锁(共享资源每次只给一个线程使用,其它线程阻塞,用完后再把资源转让给其它线程)。传统的关系型数据库里边就用到了很多这种锁机制,比如行锁,表锁等,读锁,写锁等,都是在做操作之前先上锁。Java中synchronized和ReentrantLock等独占锁就是悲观锁思想的实现。


    1. Java高级工程师必会知识之JavaSE(中)

    文章来源:智云一二三科技

    文章标题:Java高级工程师必会知识之JavaSE(中)

    文章地址:https://www.zhihuclub.com/188422.shtml

    关于作者: 智云科技

    热门文章

    网站地图