多线程

1. 线程

概述


  1. 线程和进程的关系
    • 进程:正在进行中的程序(直译)
    • 线程:就是进程中一个负责程序执行的控制单元(独立执行的路径)
    • 一个进程中至少要有一个线程
    • 开启多个线程是为了同时运行多部分代码
    • 每一个线程都有自己运行的内容,这个内容可以称为线程要执行的任务

  1. 多线程的利弊
    • 多线程好处:解决了多部分同时运行的问题,即同步问题
    • 多线程弊端:线程太多导致效率降低
    • 其实应用程序的执行都是cpu在做着快速的切换完成的,这个切换是随机的

  1. 普通方法的调用和线程开启的区别

多线程 - 图1

线程的生命周期

多线程 - 图2

  1. 新建状态
  2. 就绪状态
  3. 运行状态
  4. 阻塞状态
    • 线程调用 sleep() 方法主动放弃所占用的处理器资源
    • 线程调用了一个阻塞式IO方法,在该方法返回之前,该线程被阻塞 (join)
    • 线程试图获得一个同步监视器,但该同步监视器正被其他线程所持有
    • 线程在等待某个通知(wait)
    • 程序调用了线程的 suspend() 方法将该线程挂起,但这个方法容易导致死锁,所以应该尽量避免使用该方法
  5. 死亡状态

2. 线程的创建方式

多线程 - 图3


  1. 继承Thread类
    • 子类继承Thread类具备多线程能力
    • 启动线程:子类对象.start()
    • 不建议使用:避免OOP(面向对象)单继承局限性
  1. public class TestThread01 extends Thread {
  2. @Override
  3. public void run() {
  4. for (int i = 0; i < 200; i++) {
  5. System.out.println("正在run!" + i);
  6. }
  7. }
  8. public static void main(String[] args) {
  9. TestThread01 testThread01 = new TestThread01();
  10. testThread01.start();
  11. for (int i = 0; i < 1000; i++) {
  12. System.out.println("正在main!" + i);
  13. }
  14. }
  15. }

  1. 实现Runnable接口
    • 实现接口Runnable具有多线程能力
    • 启动线程:传入目标对象+Thread对象.start()
    • 推荐使用:避免单继承局限性,灵活方便,方便同一个对象被多个线程使用
  1. public class TestThread02 implements Runnable {
  2. @Override
  3. public void run() {
  4. for (int i = 0; i < 200; i++) {
  5. System.out.println("正在run" + i);
  6. }
  7. }
  8. public static void main(String[] args) {
  9. TestThread02 testThread02 = new TestThread02();
  10. new Thread(testThread02).start();
  11. for (int i = 0; i < 1000; i++) {
  12. System.out.println("正在main" + i);
  13. }
  14. }
  15. }

  1. 实现Callable接口(了解即可)
    • 通过 FutureTask 类构建 Callable 对象
    • Thread 建立时引用 FutureTask 对象
    • 通过 FutureTask.get() 方法调用 call() 线程执行体
    • call() 方法可以有返回值,方法可以抛出异常
  1. import java.util.concurrent.Callable;
  2. import java.util.concurrent.ExecutionException;
  3. import java.util.concurrent.FutureTask;
  4. public class CallableTest {
  5. public static void main(String[] args) throws ExecutionException, InterruptedException {
  6. People people = new People();
  7. FutureTask futureTask = new FutureTask(people); // 适配器
  8. new Thread(futureTask).start();
  9. Object o = futureTask.get(); // 这个get()方法可能会阻塞,一般放在最后
  10. // 或者使用异步通信
  11. System.out.println(o);
  12. }
  13. }
  14. class People implements Callable<String> {
  15. public String call() {
  16. return "人";
  17. }
  18. }

3. 线程的安全问题

如何解决?

使用顺序:Lock>同步代码块>同步方法

synchronized 同步代码块

  1. synchronized (锁对象){ //锁的是增删改查的对象
  2. //要处理的代码
  3. }

注意:


  • 通过代码块中的锁对象,可以使用任意的对象,比如Object对象

  • 但是必须保证多个线程使用的锁对象是同一个

  • 锁对象的作用:

    把同步代码锁住,只让一个线程在同步代码块中执行

synchronized 同步方法

  1. public synchronized void methodName(){ //同步方法锁的是当前对象
  2. //要处理的代码
  3. }
  4. public static synchronized void methodName(){ //静态同步方法锁的是当前类
  5. //要处理的代码
  6. }

Lock 锁(常用)

公平锁:使等待的线程有序的被执行,遵循先进先出原则

  1. Lock lock = new ReentrantLock(); //创建一个ReentrantLock对象
  2. public void methodName(){
  3. lock.lock(); //加锁
  4. try{
  5. //要处理的代码
  6. }catch(Exception e){
  7. e.printStackTrace
  8. }finally{
  9. lock.unlock(); //释放锁
  10. }
  11. }

synchronized 和 Lock 的区别

  1. synchronized 是 Java 关键字,在 JVM 层面实现加锁和解锁;Lock 是一个接口,在代码层面实现加锁和解锁
  2. synchronized 可以用在代码块、方法上;Lock 只能写在代码里
  3. synchronized 在代码执行完或出现异常时自动释放锁;Lock 不会自动释放锁,需要在 finally 中显示释放锁
  4. synchronized 无法得知是否获取锁成功;Lock 可以通过 tryLock 得知加锁是否成功
  5. synchronized 锁可重入、不可中断、非公平;Lock 锁可重入、可中断、可公平 / 不公平,并且可以细分读写锁提高效率

4. Lambda表达式

Lambda使用的注意事项:

  1. 必须具有接口,且要求接口中有且仅有一个抽象方法,这种接口也称,函数式接口
  2. 如果接口的方法中有多行代码,那么就用代码块包裹
  3. 接口方法中,多个参数也可以去掉参数类型,但必须加上括号

标准的Lambda表达式:

  1. //接口
  2. public interface Runnable{
  3. public abstract void run();
  4. }
  5. public class Demo implements Runnable{
  6. public static void main(String[] args) {
  7. //Lambda表达式
  8. Demo demo = new Demo(()->{
  9. System.out.println("");
  10. });
  11. }
  12. }

5. 死锁

死锁是指两个或两个以上的进程在执行过程中,由于竞争资源或者由于彼此通信而造成的一种阻塞的现象

产生死锁的四个必要条件:

  • 互斥条件:一个资源每次只能被一个进程使用
  • 请求与保持条件:一个进程因请求资源而阻塞时,对已获得的资源保持不放
  • 不剥夺条件:进程已获得的资源,在未使用完之前,不能强行剥夺
  • 循环等待条件:若干进程之间形成一种头尾相接的循环等待资源关系

6. 线程之间的通信

通信方式

  1. wait()、notify()、notifyAll()
    • 这三个方法是 Object 类中的方法,都是本地方法,且都是 final 修饰,无法重写
    • 这三个方法用于同步代码块或同步方法中
    • wait():执行该方法,线程进入阻塞状态,并释放同步锁
    • notify():唤醒被 wait 的一个线程,使其进入就绪状态,如果有多个线程被 wait,则唤醒优先级高的线程
    • notifyAll():唤醒所有被 wait 的线程
  1. await()、signal()、siganlAll()
    • 这三个方法是用于 lock 锁的通信,它们属于 condition 接口的方法。condition 接口依赖于 lock 接口,需要 lock.newCondtion 来创建
    • 这三个方法跟 wait()、notify()、notifyAll() 效果一样
  1. 阻塞队列

面试题

sleep() 和 wait() 的区别

  1. 方法声明位置不同:
    • sleep() 是 Thread 类中的静态方法
    • wait() 是 Object 类中的成员方法
  1. 调用的范围不同:
    • sleep() 可以在任何地方调用
    • wait() 必须在同步代码块或同步方法下使用
  1. 是否释放同步锁:
    • sleep() 不会释放锁
    • wait() 会释放锁,且需要通过 notify() 或 notifyAll() 来重新获得锁

7. 线程池

经常创建和销毁线程,特别是并发情况下的线程,对性能影响很大。线程池的思路就是提前创建好多个线程,放入池中,使用直接取,用完放回池中。

优点

  1. 提高响应速度(减少了创建新线程的时间)
  2. 降低资源消耗(重复利用线程池中线程,不需要每次都创建)
  3. 便于线程管理

线程池的工作流程

多线程 - 图4

线程池创建方式

  1. import java.util.concurrent.ExecutorService;
  2. import java.util.concurrent.Executors;
  3. public class TestThreadPool {
  4. public static void main(String[] args) {
  5. //使用工厂类Executors里的静态方法newFixedThreadPool生产一个指定线程数量的线程池
  6. //返回一个ExecutorService对象
  7. ExecutorService es = Executors.newFixedThreadPool(2);
  8. es.submit(new Demo(), "A"); //submit方法,可以一直开启线程池,使用完线程,会自动把线程归还
  9. es.submit(new Demo(), "B"); // 用于Callable
  10. // es.execute(Runnable runnable);
  11. es.shutdown(); // 关闭连接池
  12. }
  13. }
  14. class Demo implements Runnable {
  15. @Override
  16. public void run() {
  17. System.out.println(Thread.currentThread().getName() + "创建了一个线程");
  18. }
  19. }