去年去阿里面试被问到java-多线程我是这样手撕面试官的

40次阅读

共计 7336 个字符,预计需要花费 19 分钟才能阅读完成。

1. 多线程的基本概念

1.1 进程与线程

程序:是为完成特定任务,用某种语言编写的一组指令的集合,即一段静态代码,静态对象。

进程:是程序的一次执行过程,或是正在运行的一个程序,是一个动态的过程,每个程序都有一个独立的内存空间

线程:是进程中的一个执行路径,共享一个内存空间,线程之间可以自由切换,并发执行. 一个进程最少有一个线程

线程实际上是在进程基础之上的进一步划分,一个进程启动之后,里面的若干执行路径又可以划分成若干个线程

 

1.2 并行与并发

并发:指两个或多个事件在同一个时间段内发生。

并行:指两个或多个事件在同一时刻发生(同时发生)。

1.3 同步与异步

同步: 排队执行 , 效率低但是安全.

异步: 同时执行 , 效率高但是数据不安全.

1.4 线程的调度

分时调度(时间片):所有线程轮流使用 CPU 的使用权,平均分配每个线程占用 CPU 的时间
抢占式调度:高优先级的线程抢占 CPU

Java 使用的为抢占式调度。

 

CPU 使用抢占式调度模式在多个线程间进行着高速的切换。对于 CPU 的一个核新而言,某个时刻,只能执行一个线程,而 CPU 的在多个线程间切换速度相对我们的感觉要快,看上去就是 在同一时刻运行。其实,多线程程序并不能提高程序的运行速度,但能够提高程序运行效率,让 CPU 的 使用率更高。

1.5 线程的优先级

Java 线程有优先级,优先级高的线程会获得较多的运行机会。

java 线程的优先级用整数表示,取值范围是 1~10,Thread 类有以下三个静态常量:

static int MAX_PRIORITY
          线程可以具有的最高优先级,取值为 10。
static int MIN_PRIORITY
          线程可以具有的最低优先级,取值为 1。
static int NORM_PRIORITY
          分配给线程的默认优先级,取值为 5。

Thread 类的 setPriority()和 getPriority()方法分别用来设置和获取线程的优先级。
主线程的默认优先级为 Thread.NORM_PRIORITY。
setPriority(int newPriority): 改变线程的优先级
高优先级的线程要抢占低优先级的线程的 cpu 的执行权。但是仅是从概率上来说的,高优先级的线程更有可能被执行。并不意味着只有高优先级的线程执行完以后,低优先级的线程才执行。

2. 三种多线程的创建方式

2.1 继承于 Thread 类

1. 创建一个集成于 Thread 类的子类(通过 ctrl+o(override)输入 run 查找 run 方法)
2. 重写 Thread 类的 run()方法
3. 创建 Thread 子类的对象
4. 通过此对象调用 start()方法

public class MyThread extends Thread{
    /*
     run 方法就是线程要执行的任务方法
     */
    @Override
    public void run() {
        // 这里的代码就是一条新的执行路径
        // 这个执行路径的触发方法,不是调用 run 方法,而是通过 Thread 对象的 start()来起启动任务
       for (int i=0;i<10;i++){System.out.println("大大大"+i);
       }
    }
}
 
 
 
 
 public static void main(String[] args) {MyThread m = new MyThread();
        m.start();
        for (int i=0;i<10;i++){System.out.println("小星星"+i);
        }

2.2  实现 Runable 接口方式

1. 创建一个实现了 Runable 接口的类
2. 实现类去实现 Runnable 中的抽象方法:run()
3. 创建实现类的对象
4. 将此对象作为参数传递到 Thread 类中的构造器中,创建 Thread 类的对象
5. 通过 Thread 类的对象调用 start()

public class MyRunnable implements Runnable{
    @Override
    public void run() {
        // 线程的任务
        for (int i=0;i<10;i++){System.out.println("床前明月光"+i);
 
        }
 
    }
}
 
 
 
 
       //1.  创建一个任务对象
        MyRunnable r = new MyRunnable();
        //2.  创建一个线程,并为其分配一个任务
        Thread t = new Thread(r);
        //3.   执行这个线程
        t.start();
        for (int i=0;i<10;i++){System.out.println("疑是地上霜"+i);

实现 Runnable 与 继承 Thread 相比有如下优势

1. 通过创建任务,然后给线程分配的方式来实现多线程,更适合多个线程同时执行相同的任务
2. 可以避免单继承带来的局限性
3. 任务与线程本身是分离的,提高了程序的健壮性
4. 后续学习的线程池技术,接受 Runnable 接口的任务,而不接受 Thread 类型的线程

main 方法其实也是一个线程。在 java 中所以的线程都是同时启动的,至于什么时候,哪个先执行,完全看谁先得到 CPU 的资源。在 java 中,每次程序运行至少启动 2 个线程。一个是 main 线程,一个是垃圾收集线程。因为每当使用 java 命令执行一个类的时候,实际上都会启动一个JVM,每一个jVM实习在就是在操作系统中启动了一个进程。

2.3 实现 Callable 接口方式

1. 创建一个实现 callable 的实现类
 2. 实现 call 方法,将此线程需要执行的操作声明在 call()中
 3. 创建 callable 实现类的对象
 4. 将 callable 接口实现类的对象作为传递到 FutureTask 的构造器中,创建 FutureTask 的对象
 5. 将 FutureTask 的对象作为参数传递到 Thread 类的构造器中,创建 Thread 对象,并调用 start 方法启动(通过 FutureTask 的对象调用方法 get 获取线程中的 call 的返回值)

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

Runnable 与 Callable 的异同

相同点:都是接口
               都可以编写多线程程序
                都采用 Thread.start() 启动线程

不同点:Runnable 没有返回值;Callable 可以返回执行结果
               Callable 接口的 call() 允许抛出异常;Runnable 的 run()不能抛出

Callable 还会获取返回值——Callalble 接口支持返回执行结果,需要调用 FutureTask.get()得到,此方法会阻塞主进程的继续往下执行,如果不调用不会阻塞。

3. 线程安全问题

线程安全问题是指,多个线程对同一个共享数据进行操作时,线程没来得及更新共享数据,从而导致另外线程没得到最新的数据,从而产生线程安全问题。

三种安全锁:

3.1 同步代码块

使用同步监视器(锁)
Synchronized(同步监视器){
// 需要被同步的代码
}

说明:

操作共享数据的代码(所有线程共享的数据的操作的代码)(视作卫生间区域(所有人共享的厕所)),即为需要共享的代码(同步代码块,在同步代码块中,相当于是一个单线程,效率低)
共享数据:多个线程共同操作的数据,比如公共厕所就类比共享数据
同步监视器(俗称:锁):任何一个的对象都可以充当锁。(但是为了可读性一般设置英文成 lock)当锁住以后只能有一个线程能进去(要求: 多个线程必须要共用同一把锁,比如火车上的厕所,同一个标志表示有人)

3.2 同步方法

使用同步方法,对方法进行 synchronized 关键字修饰。将同步代码块提取出来成为一个方法,用 synchronized 关键字修饰此方法。对于 runnable 接口实现多线程,只需要将同步方法用 synchronized 修饰而对于继承自 Thread 方式,需要将同步方法用 static 和 synchronized 修饰,因为对象不唯一(锁不唯一)
 

3.3 显示锁

Lock 子类 ReentrantLock

3.4 公平锁与非公平锁

显示锁 的 fair 参数为 true 就表示是公平锁   先到先得

public static void main(String[] args) {
        // 线程不安全
        // 同步代码块 和 同步方法 都属于隐式锁
        // 解决方案 3. 显示锁 Lock 子类 ReentrantLock
        Runnable run = new Ticket();
        new Thread(run).start();
        new Thread(run).start();
        new Thread(run).start();}
    static class Ticket implements Runnable{
        private int count = 10;
        // 票数
        // 显示锁 l : fair 参数为 true  就表示是公平锁
        private Lock l = new ReentrantLock(true);
        @Override
        public void run() {while (true) {l.lock();  // 锁住
                if (count > 0) {System.out.println("正在准备卖票");
                    try {Thread.sleep(1000);
                    } catch (InterruptedException e) {e.printStackTrace();
                    }
                    count--;  // 卖票
                    System.out.println(Thread.currentThread().getName() + "出票成功,余票" + count);
                }else {break;}
                l.unlock();// 开锁}
        }
    }

3.5 出现死锁问题

出现死锁以后,不会出现提示,只是所有线程都处于阻塞状态,无法继续

死锁的解决办法:

1. 减少同步共享变量
2. 采用专门的算法,多个线程之间规定先后执行的顺序,规避死锁问题
3. 减少锁的嵌套。

4. 线程通信问题

通信常见方法:

这三种方法只能在同步代码块或同步方法中使用。

线程通信的应用:生产者 / 消费者问题
  1. 是否是多线程问题?是的,有生产者线程和消费者线程(多线程的创建,四种方式)
  2. 多线程问题是否存在共享数据?存在共享数据 —- 产品(同步方法,同步代码块,lock 锁)
  3. 多线程是否存在线程安全问题?存在 —- 都对共享数据产品进行了操作。(三种方法)
  4. 是否存在线程间的通信,是,如果生产多了到 20 时,需要通知停止生产(wait)。(线程之间的通信问题,需要 wait,notify 等)
 

5. 线程生命周期

线程生命周期的阶段    描述

新建    当一个 Thread 类或其子类的对象被声明并创建时,新生的线程对象处于新建状态
就绪    处于新建状态的线程被 start 后,将进入线程队列等待 CPU 时间片,此时它已具备了运行的条件,只是没分配到 CPU 资源
运行    当就绪的线程被调度并获得 CPU 资源时,便进入运行状态,run 方法定义了线程的操作和功能
阻塞    在某种特殊情况下,被人为挂起或执行输入输出操作时,让出 CPU 并临时终止自己的执行,进入阻塞状态
死亡    线程完成了它的全部工作或线程被提前强制性地中止或出现异常导致结束

6. 线程池   ExecutorService

6.1 缓存线程池

/**
  * 缓存线程池.
  * (长度无限制)
  * 执行流程:
  *   1. 判断线程池是否存在空闲线程
  *   2. 存在则使用
  *   3. 不存在, 则创建线程 并放入线程池, 然后使用
  */
 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());
   }
 });

6.2 定长线程池

/**
  * 定长线程池.
  * (长度是指定的数值)
  * 执行流程:
  *   1. 判断线程池是否存在空闲线程
  *   2. 存在则使用
  *   3. 不存在空闲线程, 且线程池未满的情况下, 则创建线程 并放入线程池, 然后使用
  *   4. 不存在空闲线程, 且线程池已满的情况下, 则等待线程池存在空闲线程
  */
 ExecutorService service = Executors.newFixedThreadPool(2);
 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());
   }
 });

6.3 单线程线程池

效果与定长线程池 创建时传入数值 1 效果一致.
 /**
  * 单线程线程池.
  * 执行流程:
  *   1. 判断线程池 的那个线程 是否空闲
  *   2. 空闲则使用
  *   4. 不空闲, 则等待 池中的单个线程空闲后 使用
  */
 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());
   }
 });

 

6.4 周期性任务定长线程池

public static void main(String[] args) {
 /**
  * 周期任务 定长线程池.
  * 执行流程:
  *   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("俩人相视一笑~ 嘿嘿嘿");
   }
 },5,2,TimeUnit.SECONDS);
}

7.Lambda 表达式

Lambda 体现的是函数式编程思想

Thread t = new Thread(new Runnable() {
    @Override
    public void run() {System.out.println("hhh");
    }
});
t.start();
 
 
 
 
Thread t = new Thread(() -> {System.out.println("hhh");
});
t.start();

这个表达式就是省略了中间的接口功能用表达式代替,保留了参数和方法部分。

8. 小总结

线程同步的目的是为了保护多个线程反问一个资源时对资源的破坏。
线程同步方法是通过锁来实现,每个对象都有切仅有一个锁,这个锁与一个特定的对象关联,线程一旦获取了对象锁,其他访问该对象的线程就无法再访问该对象的其他非同步方法
对于静态同步方法,锁是针对这个类的,锁对象是该类的 Class 对象。静态和非静态方法的锁互不干预。一个线程获得锁,当在一个同步方法中访问另外对象上的同步方法时,会获取这两个对象锁。
对于同步,要时刻清醒在哪个对象上同步,这是关键。
编写线程安全的类,需要时刻注意对多个线程竞争访问资源的逻辑和安全做出正确的判断,对“原子”操作做出分析,并保证原子操作期间别的线程无法访问竞争资源。
当多个线程等待一个对象锁时,没有获取到锁的线程将发生阻塞。
死锁是线程间相互等待锁锁造成的,在实际中发生的概率非常的小。真让你写个死锁程序,不一定好使,呵呵。但是,一旦程序发生死锁,程序将死掉。

正文完
 0