Java多线程面试知识点整理

Java多线程知识点整理

1.1 线程与进程

进程:
  是资源分配与调度的基本单位。也可表示内存中运行的应用程序(软件),每个进程都有一个独立的内存空间。
线程:

  • 是进程中的一个执行路径,共享一个内存空间,线程之间可以自由切换,并发执行。一个进程最少有一个线程。
  • 线程实际上是在进程基础之上的进一步划分,一个进程启动之后,里面的若干执行路径又可以划分成若干个线程。

1.2 线程和进程有什么区别?

  1. 进程是一个独立的运行环境,可以被看作一个程序或者一个应用。
  2. 线程是进程的子集或执行路径,一个进程可以有很多线程,每条线程并行执行不同的任务。
  3. 不同的进程使用不同的内存空间,而所有的线程共享一片相同的内存空间。每个线程都拥有单独的栈内存用来存储本地数据。

1.3 常用的开启多线程的技术?

1.继承Thread类,实现run()方法

代码实例:
  先创建一个子类MyThread继承Thread类,然后重写这个子进程的执行方法。

public class MyThread extends Thread{
    /**
     * run方法就是线程要执行的任务方法
     */
    @Override
    public void run(){
        //这里的代码 就是一条新的执行路径
        //这个执行路径的触发方式,不是调用run方法,而是调用Thread对象的start()方法来启动
        for (int i=1; i<4; i++){
            System.out.println("小明比较快"+i);
        }
    }
}

  新创建一个Demo类,在main线程中创建子线程对象,通过子线程对象的start()方法来启动子线程,执行run方法。

public class Demo {
    /**
     * 多线程技术
     */
    public static void main(String[] args) {
        MyThread mt = new MyThread();
        mt.start();
        for (int i=1; i<4; i++){
            System.out.println("小红比较快" + i);
        }
    }
}

注意:

  • 每个线程都拥有自己的栈空间,共用一份堆内存。
  • 由一个线程所调用的方法,这个方法也会执行在这个线程里。

2.实现Runnable接口,实现run()方法

  Runnable实例对象作为Thread构造方法中的target参数传入,充当线程执行体。这种方式适用于多个线程共享资源的情况。

代码实例:

public static void main(String[] args) {
        //实现Runnable
        //1.    创建一个任务对象
        MyRunnable mr = new MyRunnable();
        //2.    创建一个线程,并为其分配一个任务
        Thread thread = new Thread(mr);
        thread.start();
        for (int i = 0; i < 3; i++) {
            System.out.println("小明先吃饭" + i);
        }
    }
    static class MyRunnable implements Runnable {
        @Override
        public void run() {
            for (int i = 0; i < 3; i++) {
                System.out.println("小红先吃饭" +i);
            }
        }
    }

  实现Runnable 与 继承Thread相比的优势

  1. 通过创建任务,然后给线程分配的方式来实现多线程,更适合多个线程同时执行相同任务的情况。
  2. 可以避免单继承所带来的局限性。
  3. 任务与线程本身是分离的,提高了程序的健壮性。
  4. 后续学习的线程池技术,接收Runnable类型的任务而不接收Thread类型的线程。
  5. Java不支持类的多重继承,但允许你调用多个接口。所以如果你要继承其他类,当然是调用Runnable接口好。

3.实现Callable接口,实现call()方法。

  实现Callable接口,通过FutureTask包装器创建Thread线程。
使用步骤:

  1. 编写类实现Callable接口 , 实现call方法
class XXX implements Callable<T> {
	@Override
     public <T> call() throws Exception {
     	return T;
     }
}
  1. 创建FutureTask对象 , 并传入第一步编写的Callable类对象
FutureTask future = new FutureTask<>(callable);
  1. 通过Thread,启动线程
new Thread(future).start();

代码实例:

public static void main(String[] args) throws InterruptedException, ExecutionException {
        Callable<Integer> mc = new MyCallable();
        //创建FutureTask对象,传入Callable类对象mc
        FutureTask<Integer> task = new FutureTask<Integer>(mc);
        //通过Thread,启动线程
        new Thread(task).start();
        Integer k = task.get();
        System.out.println("返回值为:" + k);
        for (int j = 0; j < 3; j++) {
            System.out.println("今天不休息");
        }
    }

    static class MyCallable implements Callable<Integer> {
        @Override
        public Integer call() throws Exception {
            for (int i = 0; i < 3; i++) {
                System.out.println("今天休息");
            }
            return 1024;
        }

执行结果:

 今天休息
 今天休息
 今天休息
 返回值为:1024
 今天不休息
 今天不休息
 今天不休息

  Runnable 与 Callable 的区别

相同点:

  • 都是接口
  • 都可以编写多线程程序
  • 都采用Thread.start( )启动线程
    不同点:
  • Callable的 call( ) 方法可以返回值和抛出异常,而Runnable的run( )方法没有这些功能。
  • Callable可以返回装载有计算结果的Future对象。

1.4 Java线程的6种状态(重点)

查看源码(在java.lang.Thread中)可以发现java中的线程状态共有6种而不是像操作系统中的5状态图。

Thead类内部有一个枚举类State,列举了线程中可能存在的6种状态为:

  1. New 新建状态:新建的Thread对象,未调用start()
  2. Runnable 可运行状态:可运行状态中包含:
  • 就绪态:已经获取所有资源,CPU分配执行权就可以执行,所有就绪态线程都在就绪队列中。
  • 运行态:正在执行的线程,一个CPU同一时间只能处理一个线程,因此一个CPU上只有一个运行态程序。
  1. Blocked 阻塞状态:线程请求资源失败时会进入该状态。所有阻塞态线程都存储在一个阻塞队列中,阻塞态线程会不断请求资源成功后进入就绪队列,等待执行。
  2. Waiting 等待状态:wait、join等函数会造成进入该状态。同样有一个等待队列存储所有等待线程,线程会等待其他线程指示才能继续执行。等待状态线程主动放弃CPU执行权。
  3. Timed_Waiting 计时等待:计时等待也是主动放弃CPU执行权的,区别是:超时后会进入阻塞态竞争资源。
  4. Terminal 结束状态:线程执行结束后的状态。

1.5 并行与并发

并行:多核多CPU处理同一段处理逻辑的时候,多个执行流在同一时刻共同执行。
并发:通过CPU的调度算法,使用户感觉像是同时处理多个任务,但同一时刻只有一个执行流占用CPU执行。即使多核多CPU环境还是会使用并发,以提高处理效率。

1.6 Thread 类中的start() 和 run() 方法有什么区别?

   start()方法被用来启动新创建的线程,使被创建的线程状态变为可运行状态。当你调用Thread的run()方法的时候,只会是在原来的线程中调用此方法,而没有新的线程启动,start()方法才会启动新线程。为了在新的线程中执行我们的代码,必须使用Thread.start()方法。

1.7 sleep()方法、wait()方法、notify()、notifyAll()方法

1、sleep()方法:

  • sleep方法为Thread的静态方法;
  • sleep方法的作用:让线程休眠指定时间,在时间到达时自动恢复线程的执行;
  • sleep方法不会释放线程锁;

2、wait()方法:

  • wait方法是Object的方法,任意一个对象都可以调用wait方法,调用wait方法会将调用者的线程挂起,使该线程进入waitSet 的等待区域,直到其他线程调用同一个对象的notify方法才会重新激活调用者;
  • 当wait方法被调用时,它会释放它所占用的锁标记,从而使线程所在对象中的synchronize数据可以被别的线程所使用,所以wait()方法必须在同步块中使用,notify()和notifyAll()方法都会对对象的“锁标记”进行修改,所以都需要在同步块中进行调用,如果不在同步块中调用,虽然可以编辑通过,但是运行时会报IllegalMonitorStateException(非法的监控状态异常);

3、notify()和nofityAll()方法;

  • notify()会通知一个处在wait()状态的线程;如果有多个线程处在wait状态,他会随机唤醒其中一个;
  • notifyAll()会通知所有处在wait()状态的线程,具体执行哪一个线程,根据优先级而定;

2.1 线程调度

分时调度

  • 所有线程轮流使用 CPU 的使用权,平均分配每个线程占用 CPU 的时间。
    抢占式调度
  • 优先让优先级高的线程使用 CPU,如果线程的优先级相同,那么会随机选择一个(线程随机性),Java使用的为抢占式调度。
  • CPU使用抢占式调度模式在多个线程间进行着高速的切换。对于CPU的一个核心而言,某个时刻,只能执行一个线程,而 CPU的在多个线程间切换速度相对我们的感觉要快,看上去就是在同一时刻运行。 其实,多线程程序并不能提高程序的运行速度,但能够提高程序运行效率,让CPU的使用率更高。

  1、调整线程优先级:Java线程有优先级,优先级高的线程获得较多的运行机会(运行时间);

static int Max_priority //线程的最高优先级,值为10;
static int MIN_PRIORIYT //线程的最低优先级,值为1;
static int NORM_PRIORITY //分配给线程的默认优先级,值为5;

  Thread类的setPriority()和getPriority()方法分别用来设置和获取线程的优先级;

  2、线程睡眠:Thread.sleep(long millins)使线程转到阻塞状态;
  3、线程等待:Object.wait()方法,释放线程锁,使线程进入等待状态,直到被其他线程唤醒(notify()和notifyAll());
  4、线程让步:Thread.yeild()方法暂停当前正在执行的线程,使其进入等待执行状态,把执行机会让给相同优先级或更高优先级的线程,如果没有较高优先级或相同优先级的线程,该线程会继续执行;
  5、线程加入:join()方法,在当前线程中调用另一个线程的join()方法,则当前线程转入阻塞状态,直到另一个进程运行结束,当前线程再油阻塞状态转为就绪状态。

2.2 线程类的一些常用方法:

1)sleep():强迫一个线程睡眠N毫秒;
2)isAlive():判断一个线程是否存活;
3)join():线程插队;
4)activeCount():程序中活跃的线程数;
5)enumerate():枚举程序中的线程;
6)currentThread():得到当前线程;
7)isDeamon():设置线程是否为守护线程;
8)setName():为线程设置一个名字;
9)wait():线程等待;
10)notify():唤醒一个线程;
11)setPriority():设置一个线程的优先级;
12)getPriority():获得一个线程的优先级;

2.3 面试可能问:公平锁和非公平锁

  • 公平锁先来先到,排队依次执行,假如A线程先过来等,当锁解开时,A线程先执行,有一个排队的过程。
  • 非公平锁谁抢到谁先执行
    如何实现公平锁?
    代码实例:
static class SellTicket implements Runnable {
        private int count = 10;
        //显式锁 ReentrantLock(true),参数fair为true,就表示公平锁。
        private Lock lk = new ReentrantLock(true);
        @Override
        public void run() {
            while (true) {
                //给需要锁住的代码上锁
                lk.lock();
                if (count > 0) {
                    System.out.println(Thread.currentThread().getName() + "准备开始售票");
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    count--;
                    System.out.println("售票成功," + "剩余票数为" + count);
                } else {
                    break;
                }
                //解锁
                lk.unlock();
            }
        }
    }

线程池

1.概念

  创建线程要花费昂贵的资源和时间,如果任务来了才创建线程那么响应时间会变长,而且一个进程能创建的线程数有限。
  线程池就是一个容纳多个线程的容器,池中的线程可以反复使用,省去了频繁创建线程对象的操作,节省了大量的时间和资源。

2.线程池的优点

  1)重用存在的线程,减少对象创建销毁的开销,降低资源消耗。

  2)提高响应速度。

  3)提高线程的可管理性。

3.Java中的四种线程池

3.1 缓存线程池

Executors.newCachedThreadPool(); 

创建一个可缓存线程池。
  缓存线程池的长度是无限制的。

执行流程:

  1. 判断线程池是否存在空闲线程
  2. 存在则使用
  3. 不存在,则创建线程并放入线程池,,然后使用。

代码实例:

public static void main(String[] args) {
        //向线程池中加入新的任务
        ExecutorService service = Executors.newCachedThreadPool();
        service.execute(new Runnable() {
            @Override
            public void run() {
                System.out.println(Thread.currentThread().getName()+"我执行了");
            }
        });
        service.execute(new Runnable() {
            @Override
            public void run() {
                System.out.println(Thread.currentThread().getName()+"我执行了");
            }
        });
        service.execute(new Runnable() {
            @Override
            public void run() {
                System.out.println(Thread.currentThread().getName()+"我执行了");
            }
        });
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        service.execute(new Runnable() {
            @Override
            public void run() {
                System.out.println(Thread.currentThread().getName()+"新的任务由我来执行");
            }
        });
    }

Demo执行结果:

pool-1-thread-3我执行了
pool-1-thread-1我执行了
pool-1-thread-2我执行了
pool-1-thread-1新的任务由我来执行

3.2 定长线程池

Executors.newFixedThreadPool(2);

创建一个定长线程池,可控制线程最大并发数。
  定长线程池(长度是指定的数值)
执行流程:

  1. 判断线程池是否存在空闲线程
  2. 存在则使用
  3. 不存在空闲线程,且线程池未满的情况下,则创建线程 并放入线程池,,然后使用
  4. 不存在空闲线程,且线程池已满的情况下,则等待线程池存在空闲线程

代码实例:

public static void main(String[] args) {
        ExecutorService service = Executors.newFixedThreadPool(2);
        service.execute(new Runnable() {
            @Override
            public void run() {
                System.out.println(Thread.currentThread().getName() + "我执行这个线程");
                try {
                    Thread.sleep(2000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });
        service.execute(new Runnable() {
            @Override
            public void run() {
                System.out.println(Thread.currentThread().getName() + "我执行这个线程");
                try {
                    Thread.sleep(2000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });
        service.execute(new Runnable() {
            @Override
            public void run() {
                System.out.println(Thread.currentThread().getName() + "我执行这个新的线程");
                try {
                    Thread.sleep(2000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });
    }

3.3 单线程线程池

Executors.newSingleThreadExecutor();

创建一个单线程化的线程池,它只会用唯一的工作线程来执行任务。
  效果与定长线程池创建时,传入数值1 效果一致。
执行流程:

  1. 判断线程池的那个线程是否空闲
  2. 空闲则使用
  3. 不空闲,则等待池中的单个线程空闲后使用

代码实例:

public static void main(String[] args) {
        ExecutorService service = Executors.newSingleThreadExecutor();
        service.execute(new Runnable() {
            @Override
            public void run() {
                System.out.println(Thread.currentThread().getName() + " 我来执行这个线程");
            }
        });
        service.execute(new Runnable() {
            @Override
            public void run() {
                System.out.println(Thread.currentThread().getName() + " 我来执行这个线程");
            }
        });
        service.execute(new Runnable() {
            @Override
            public void run() {
                System.out.println(Thread.currentThread().getName() + " 我来执行这个线程");
            }
        });
    }

3.4 周期性任务定长线程池

Executors.newScheduledThreadPool(2);

创建一个定长线程池,支持定时及周期性任务执行。
  周期任务,定长线程池。定时执行,当某个时机触发时,自动执行某任务。
执行流程:

  1. 判断线程池是否存在空闲线程
  2. 存在则使用
  3. 不存在空闲线程,且线程池未满的情况下,则创建线程 并放入线程池,然后使用
  4. 不存在空闲线程,且线程池已满的情况下,则等待线程池存在空闲线程

代码实例:

ScheduledExecutorService service = Executors.newScheduledThreadPool(2);
        /**
         * 定时执行
         * 参数1. runnable类型的任务
         * 参数2. 时长数字
         * 参数3. 时长数字的单位
         */
        service.schedule(new Runnable() {
            @Override
            public void run() {
                System.out.println("我执行了");
            }
        },5, TimeUnit.SECONDS);
      /**
      * 周期执行
      * 参数1.  runnable类型的任务
      * 参数2.  时长数字(延迟执行的时长)
      * 参数3.  周期时长(每次执行的间隔时间)
      * 参数4.  时长数字的单位
      */
		service.scheduleAtFixedRate(new Runnable() {
            @Override
            public void run() {
                System.out.println("你好啊");
            }
        },1,2,TimeUnit.SECONDS);

4.Java死锁以及如何避免?

4.1 Java死锁

  死锁:指两个或两个以上的进程在执行过程中,因争夺资源而造成的一种互相等待的现象,若无外力作用,它们都将无法推进下去。这是一个严重的问题,因为死锁会让你的程序挂起无法完成任务。

死锁产生的原因:

  • 系统资源不足。
  • 资源分配不当。
  • 进程运行推进的顺序不合适。

4.2 死锁产生的四个条件

  1. 互斥条件:一个资源每次只能被一个进程使用。

  2. 请求与保持条件:一个进程因请求资源而阻塞时,对已获得的资源保持不放。

  3. 不剥夺条件:进程已获得的资源,在末使用完之前,不能强行剥夺。

  4. 循环等待条件:若干进程之间形成一种头尾相接的循环等待资源关系。

4.3 如何避免死锁?

  • 破除互斥等待:一般无法做到。

  • 破除请求和保持:一次性获取所有的资源。

  • 破除循环等待:按顺序获取资源。

  • 破除无法剥夺的等待:加入超时机制。

  • 将系统中所有的资源设置标志位、排序,规定所有的进程申请资源必须以一定的顺序(升序或降序)做操作来避免死锁。

评论 5
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值