Java 多线程

线程


1、什么是线程

  • 单核CPU = 一个车间:一次执行一个进程,如果执行多个程序,会在多个进程中来回切换,执行到进程里面会在多个线程之间来回切换。

  • 多核CPU = 一个工厂:每次可执行多个进程;

  • 进程:一个车间为一个进程(一个运行的程序);进程是一种重量级的资源,系统会分配内存和CPU资源,启动和停止慢,内存相互独立

  • 线程:车间内一个工人为一个线程;

  • 多线程:一个进程包含多个线程;多个线程都可以共享一个进程的内存空间;

1.1、什么是多线程?
  • 多线程是在CPU切换到某个进程之后,会在多个线程之间来回切换,每个线程就会分配到一定的cpu时间,线程是CPU分配时间的单元
1.2、并行和并发
  • 并行:多个cpu同时执行多个线程
  • 并发:一个CPU同时执行多个线程,CPU在线程之间来回切换,让线程都能执行(不是同时执行)
1.3、同步和异步
  • 同步:多个指令是依次执行的,一个指令执行时会阻塞当前线程,其他指令必须要在该指令完成之后执行。
  • 异步:多个线程同时指向自己的命令,一个线程执行完后,给另一个线程通知

2、多线程的应用场景

  • 大型企业级应用都有高并发的特点,因为会大量的用户,比如:淘宝、京东、抖音等。如果服务器是单线程,所有用户必须排队执行,必须为每个用户的每个请求,分配一个独立的线程,完成独立任务,相互不影响。单机版程序(如:大型游戏)需要执行大量的任务:图形渲染、动作控制、网络通信等。需要多线程同时执行上面任务。

3、启动线程的方法

3.1、继承Thread
public class MyThread extends Thread{
    @Override
    public void run() {
        for (int i = 0; i <100 ; i++) {
            System.out.println("hello 我是"+Thread.currentThread().getName());
        }
    }
    
    public static void main(String[] args) {
        MyThread myThread = new MyThread();
        myThread.start();//一个线程不能调用两次以上的start方法 IllegalThreadStateException
    }
}

3.2、实现Runnable接口
public class MyRunnable implements Runnable{
    @Override
    public void run() {
        System.out.println("Runnable :"+ Thread.currentThread().getName());
    }
     public static void main(String[] args) {
        MyRunnable myRunnable = new MyRunnable();
        Thread thread  = new Thread(myRunnable);
        thread.start();
        //匿名内部类写法
        new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println("匿名内部类"+Thread.currentThread().getName());
            }
        }).start();
        //lambda表达式写法
        new Thread(() ->{
            System.out.println("lambda:"+Thread.currentThread().getName());
        }).start();
     }
}

3.3、实现Callable接口(有返回值)
public class MyCallable implements Callable<Long> {//需指定返回值类型
    @Override
    public Long call() throws Exception {
        return 1+1L;
    }
    
    public static void main(String[] args) {
        //创建MyCallable对象
        MyCallable myCallable = new MyCallable();
        //创建FutureTask对象
        FutureTask<Long> futureTask = new FutureTask<>(myCallable);
        //创建线程对象
        Thread thread = new Thread(futureTask);
        thread.start();
        try {
            System.out.println("获得结果:"+futureTask.get());
        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (ExecutionException e) {
            e.printStackTrace();
        }
    }
}

4、线程的启动和停止

4.1、线程启动
  • 面试题:启动线程是执行start方法还是run方法?
  • start()方法
  • 只有调用了start方法,系统才会创建新的线程,直接调用run方法,会在主线程中执行
  • 注意:不要手动调用start方法,一个start方法只能被一个线程对象调用一次
4.2、线程停止
  • stop方法(强制停止,不能释放锁资源,可能造成死锁等严重问题,禁用)
  • 等待run方法运行完
  • 通过条件中断run的运行

5、线程的睡眠

  • Thread.sleep(毫秒)
  • 线程一旦进入睡眠,会让出CPU
  • 使用场景:执行大量任务时,进行一定睡眠,可以减少CPU消耗

6、后台进程

  • 也叫守护线程,精灵线程
  • 将一个线程设置为后台线程:setDaemon(true)
  • 守护线程的作用是为其他线程提供服务,一旦其他线程停止了,守护线程会自动停止

7、线程的合并

  • 当前线程可以合并其他线程,当其他线程操作执行完,再执行当前线程的操作。
  • 注意:两个线程不能互相合并,否则会出现死锁的问题

8、线程的优先级

  • 默认情况下线程的执行是抢占式的,没有特定的次序,不同的优先级获得CPU的概率不同
  • setPriority(int) 设置优先级,默认是5,可设置范围1~10

9、线程的生命周期

在这里插入图片描述

  1. 新建:对象刚被创建出来,还没有调用start
  2. 就绪:调用了start,但没有抢到CPU,不能执行
  3. 运行:抢到了CPU,正在执行run
  4. 阻塞:线程遇到某些情况,暂停执行
  5. 死亡:run方法执行完

10、线程同步问题

10.1、为什么会出现线程同步问题?
  • 线程是抢占式的,一个线程在执行指令的时候,可能被其他线程中断,可能会出现数据不一致的情况
  • 多个线程同时操作一个资源
10.2、线程同步问题的解决方案(上锁机制)
  • 同步方法

    • 语法
    public synchronized 返回类型 方法名(参数){
    
    }
    
    • 作用是对整个方法上锁,保证代码的原子性,一旦一个线程进入方法后,就会持有锁,其他线程不能进入该方法,等线程执行完,其他线程才能进去。
    • 相关面试题:
      • StringBufferStringBuilder的区别
      • ArrayListVector的区别
      • HashMapHashtable的区别
    • 相关知识点
      • 同步方法锁的对象是this,当前的对象
      • 锁机制:上锁后,JVM会启动监视器(Monitor),监视进入代码块或方法体的线程,如果方法体或代码块的锁被某个线程所有,监视器就会拒绝其他线程进入代码块或方法,直到持有锁的线程执行完,释放锁,才会让其他线程进去
      • 一旦上锁,程序的性能会有所降低
  • 同步代码块

    • 语法
    public 返回类型 方法名(参数){
    ...
        synchronized(锁对象){
        代码
        } 
    ...
    }
    
    • 注意
      • 任何Java对象(Object)都可以成为锁对象
      • 锁对象不能是局部变量
      • 一旦线程进入代码块就上锁,持有锁对象,执行完代码块后自动释放锁,抛异常也会释放锁
    • 对比同步方法和同步代码块
      • 同步方法更简洁
      • 同步方法锁粒度更大(粒度越大锁的范围越大),粒度越大性能越差,同步代码块的性能高于同步方法
  • 同步锁

    • 在java1.5出现,在java.util.concurrent包中
    • Lock接口,最上层接口
    • 子接口:ReadLockWriteLockReadWriteLock
    • 常用的实现类:ReentrantLock重入锁
    • 创建对象:Lock lock = new RentrantLock()
    • 常用方法:lock.lock()上锁、lock.unlock()释放锁
    • 语法
    lock.lock();
    try{
    	代码
    }finally{
    	lock.unlock();
    }
    

11、单例模式

  • 单例模式主要分位饿汉式和懒汉式,作用是保证一个类只有一个实例对象

    • 优点:
      • 减少系统资源的消耗
      • 符合某些特殊业务的要求
  • 饿汉式

    • 一开始就创建对象,不管getInstance()是否被调用,内存资源都会被消耗掉
    • 系统中的Runtime属于饿汉式
    public class Singleton01 {
        private static Singleton01 instance = new Singleton01();
    
        private Singleton01(){
    
        }
        
        private static Singleton01 getInstance(){
            return instance;
        }
    }
    
  • 懒汉式

    • 一开始不创建对象,getInstance()被调用时候创建对象,内存一开始不消耗
    • 存在线程同步问题(需要在getInstance()方法中判断对象是否为null)
    //普通懒汉式
    public class Singleton02 {
        private static Singleton02 instance ;
    
        private Singleton02(){
    
        }
    
        private static Singleton02 getInstance(){
            if (instance == null){
                instance = new Singleton02();
            }
            return instance;
        }
    }
    
    //双检锁懒汉式
    public class Singleton06 {
        private static Singleton06 instance ;
    
        private Singleton06(){
    
        }
    
        private static Singleton06 getInstance(){
            if (instance == null){
                synchronized (Singleton06.class){
                    if (instance == null){
                        instance = new Singleton06();
                    }
                }
            }
            return instance;
        }
    }
    
    

12、volatile关键字

  • 用于修饰变量,提高线程可见性,是线程同步的轻量级解决方案,能保证可见性,不能保证原子性
  • 可见性:变量的值每个线程都能直接获取到
  • 原子性:代码作为整体运行,不可再分
  • 变量加上volatile关键字后,线程直接从主内存读取值,不是从缓存读取,任意线程修改变量后,其他线程都可以看到
  • 变量没有被volatile修饰

在这里插入图片描述

  • 变量被volatile修饰

在这里插入图片描述

13、线程死锁

  • 出现的情况:
    • 上锁后没有正常释放锁
    • 两个线程都持有对方需要的锁,又需要对方持有的锁,就可能进入相互等待的情况
public class LockTest {
    private static Object lock1 = new Object();
    private static Object lock2 = new Object();
    public static void main(String[] args) {
        Thread thread1 = new Thread(() -> {
            for (int i = 0; i < 100; i++) {
                synchronized (lock1){
                    synchronized (lock2){
                        System.out.println("thread1------------"+i);
                        try {
                            Thread.sleep(100);
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    }
                }
            }
        });
        Thread thread2 = new Thread(()->{
            for (int i = 0; i < 100; i++) {
                synchronized (lock2){
                    synchronized (lock1){
                        System.out.println("thread2------------"+i);
                    }
                }
            }
        });
        thread1.start();
        thread2.start();
    }
}

14、线程的等待和通知

  • 通过锁对象的调用,定义在Object类中

  • 等待wait:让线程进入阻塞状态

    • wait():一直等待,直到被通知
    • wait(毫秒):等待指定时长,线程可以被通知或自动唤醒
  • 通知notify:让等待状态的线程从阻塞状态恢复到就绪状态

    • notify():通知一个等待线程
    • notifyAll():通知所有等待线程
  • 注意,只有锁对象才能调用notify或者wait方法,必须在同步代码块的锁对象或同步方法的this调用。否则会出现IllegalMonitorStateException异常

15、wait和sleep的区别?

  • wait会释放锁资源,sleep不会
  • sleep是线程对象调用,wait是锁对象调用;
  • sleep必须等睡眠时间结束,wait可以等时间结束,也可以被唤醒

16、生产者消费者模式

  • 不是GOF23设计模式之一,是与线程相关的设计模式

  • 作用:

    1. 解耦:让生产者消费者之间解耦,生产者消费者之间不会直接访问
    2. 高并发:生产者消费者解耦,生产者和消费者不需要互相等待,生产者能处理更多的消费者请求
    3. 解决忙闲不均:解决了生产者速度太快消费者需求太低,或者生产者速度太慢,消费者需求太大的问题
  • 实现过程:

    1. 定义缓冲区,用于存放数据,缓冲区有上限
    2. 生产者线程将生产数据存入缓冲区,当缓冲区满了,让生产者等待,如果没满,通知生产者继续生产
    3. 消费者从缓冲区取数据,当缓冲区空了,让消费者等待,等有数据,通知消费者继续消费

17、阻塞队列

  • 是一种集合,根据数据的个数自动产生阻塞
  • BlockingQueue<T> 父接口
  • 常用实现类:
    • LinkedBlockingQueue 链表结构的阻塞队列 (插入删除效率高)
    • ArrayBlockingQueue 数组结构的阻塞队列(查询效率高)
  • 常用方法
    • put() 添加数据到末尾,如果达到上线,就阻塞当前线程
    • take()从队列头删除数据,如果为空,就阻塞

18、线程池

18.1、线程的作用?
  • 回收线程资源,线程是一种很重要的资源,线程的创建和销毁都很消耗资源,频繁的创建和销毁线程会降低程序的性能
  • 注意:线程池中的线程执行完任务后不是直接死亡,而是回到池中,可以重复利用
18.2、线程池API
  • Executor接口

    • execute(Runnable)执行单个线程任务
  • ExecutorService接口

    • showdown:关闭
    • shutdownNow:立刻关闭
    • submit:提交
  • ThreadPoolExecutor 线程实现类

  • Executors 工具类

    • 帮助创建不同类型的线程池

    • 主要方法:

      • ExecutorService newCachedThreadPool()长度不限的线程,不能控制并发量,速度更快
      //长度不限线程池
      public static void testCachedThreadPool(){
          ExecutorService threadPool = Executors.newCachedThreadPool();//长度不限的线程池
          for (int i = 0; i < 10; i++) {
              threadPool.execute(()->{
                  System.out.println("当前执行的是"+Thread.currentThread().getName());
              });
              try {
                  Thread.sleep(1);
              } catch (InterruptedException e) {
                  e.printStackTrace();
              }
          }
          threadPool.shutdown();
      }
      
      • ExecutorService newFixedThreadPool(int) 长度固定的线程池,可以控制并发量,并发量大时,需要排队
      //长度固定线程池
      public static void testFixedThreadPool(){
          ExecutorService threadPool = Executors.newFixedThreadPool(5);//长度不限的线程池
          for (int i = 0; i < 10; i++) {
              threadPool.execute(()->{
                  System.out.println("当前执行的是"+Thread.currentThread().getName());
                  try {
                      Thread.sleep(3000);
                  } catch (InterruptedException e) {
                      e.printStackTrace();
                  }
              });
      
          }
          threadPool.shutdown();
      }
      
      • ExecutorService newSingleThreadExecutor() 单线程线程池
      //单线程线程池
      public static void testSingleThreadPool(){
          ExecutorService threadPool = Executors.newSingleThreadExecutor();//长度不限的线程池
          for (int i = 0; i < 10; i++) {
              threadPool.execute(()->{
                  System.out.println("当前执行的是"+Thread.currentThread().getName());
                  try {
                      Thread.sleep(3000);
                  } catch (InterruptedException e) {
                      e.printStackTrace();
                  }
              });
      
          }
          threadPool.shutdown();
      }
      
      • ScheduledExecutorService newScheduledThreadPool(int)可调度的线程池,执行线程时,可以设置执行周期和延时
      public static void testScheduleThreadPool(){
          ScheduledExecutorService threadPool = Executors.newScheduledThreadPool(10);
      
              threadPool.scheduleAtFixedRate(()->{
                  System.out.println("当前执行的是"+Thread.currentThread().getName());
              },5,1, TimeUnit.SECONDS);
          //threadPool.scheduleWithFixedDelay()
      }
      
  • 自定义线程池

    • 构造方法:ThreadPoolExector
    • 核心线程数:corePoolSize
    • 最大线程数:maximumPoolSize
    • 存活时间:keepAliveTime
    • 时间单位:timeUnit
    • 阻塞队列,保存执行任务(Runnable):workingQueue
    • 优化配置
      1. 核心线程配置和最大线程数一样,减少创建新线程和销毁线程的开销
      2. 核心线程数配置和cpu核心数相关, 核心数 * N (N >= 1) N具体看任务量、执行时间等情况
      3. 阻塞队列使用LinkedBlockingQueue,添加和删除任务效率高
public static void myselfThreadPool(){
        int processors = Runtime.getRuntime().availableProcessors();
        System.out.println(processors);
        int n = 2;
        ThreadPoolExecutor threadPool = new ThreadPoolExecutor(processors * n, processors * n, 60L, TimeUnit.SECONDS, new LinkedBlockingQueue<Runnable>());
        for (int i = 0; i < 10; i++) {
            threadPool.execute(()->{
                System.out.println("当前执行的是"+Thread.currentThread().getName());

            });

        }
        threadPool.shutdown();
    }

19、ThreadLocal

19.1、ThreadLocal是什么?
  • ThreadLocal的作用是提供线程内的局部变量,这种变量在线程的生命周期内起作用,减少同一个线程内多个函数或者组件之间一些公共变量的传递的复杂度。
19.2、syncronized 与 ThreadLocal的区别?
  • syncronized虽然能解决线程间变量的隔离的问题,解决的方法是当一个线程上锁之后,拒绝其他线程访问,当前只有一个线程在访问这个资源,但是这种方式需要对每一个线程都加锁,降低了程序的效率,也降低了程序的并发性
  • syncronized 以时间换空间,让所有线程排队访问,只提供了一份变量,侧重的是多个线程之间访问资源的同步
  • ThreadLocal不需要加锁,也能解决线程间变量的隔离性,可满足程序的高并发,变量只在线程的生命周期内起作用
  • ThreadLocal以空间换时间,为每一个线程都提供一份单独的副本,实现访问互不干扰,每个线程都访问自己独有的那份变量,使用ThreadLocal可以保证程序拥有更高的并发性。
19.3、ThreadLocal的优点?
  • 绑定参数:保存每个线程绑定的数据,在需要的地方可以直接获取,避免参数直接传递带来的代码耦合问题
  • 线程隔离:各个线程之间的数据相互隔离又具有并发性,避免同步方法带来的性能损失
19.4、ThreadLocal使用场景
  • JDBC连接数据库,获取Connection的时候,为保证在高并发的情况下进行事务操作,保证在dao层getConnection()时候得到的对象和事务的Connection对象是同一个对象,就需要使用到ThreadLocal来实现线程之间的数据隔离,同时也不影响数据库连接的性能。
  • 解决方案,在DBUtils中定义ThreadLocal对象,当connection为空的时候,使用数据库连接池获取一个连接对象,然后将这个连接对象保存到ThreadLocal,之后调用getConnection()方法返回的是ThreadLocal中的connection,这样就实现了connection对象的隔离,需要注意的是,事务提交之后需要解绑当前线程绑定的连接对象,threadLocal.remove(),目的是为了避免内存泄漏

在这里插入图片描述

©️2020 CSDN 皮肤主题: 数字20 设计师:CSDN官方博客 返回首页