【多线程】线程、线程池

182 阅读27分钟

面试题先行

Java内存模型

image.png

每个线程都有自己的本地内存空间(java栈中的帧)。

线程执行时,先把变量从内存读到线程自己的本地内存空间,然对变量进行操作; 对该变量操作完成后,在某个时间再把变量刷新回主内存;

ThreadLocal 了解吗?

1. ThreadLocal是什么?

从名字我们就可以看到ThreadLocal 叫做本地线程变量,意思是说,ThreadLocal 中填充的的是当前线程的变量,该变量对其他线程而言是封闭且隔离的,ThreadLocal 为变量在每个线程中创建了一个副本,这样每个线程都可以访问自己内部的副本变量。

从字面意思很容易理解,但是实际角度就没那么容易了,作为一个面试常问的点,使用场景也是很丰富。

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

现在相信你已经对ThreadLocal有一个大致的认识了,下面我们看看如何用?

2. ThreadLocal怎么用?

public class ThreadLocalTest02 {
    public static void main(String[] args) {
        ThreadLocal<String> local = new ThreadLocal<>();
        IntStream.range(0, 10).forEach(i -> new Thread(() -> {
            local.set(Thread.currentThread().getName() + ":" + i);
            System.out.println("线程:" + Thread.currentThread().getName() + ",local:" + local.get());
        }).start());
    }
}

ThreadLocal 内存泄漏问题 ThreadLocal是一个弱引用,当为null时,会被当成垃圾回收 。 重点来了,突然我们ThreadLocal是null了,也就是要被垃圾回收器回收了,但是此时我们的ThreadLocalMap(thread 的内部属性)生命周期和Thread的一样,它不会回收,这时候就出现了一个现象。那就是ThreadLocalMap的key没了,但是value还在,这就造成了内存泄漏。

线程

线程与进程有什么区别?

程序>线程>进程

线程是程序执行流的最小单位,而进程是系统进行资源分配和调度的一个独立单位。

Thread的几个重要方法

  • start():开始执行该线程;

  • stop():强制结束该线程执行;

  • join():等待该线程结束。

  • sleep():进入等待。

  • run():直接执行线程的run()方法。线程调用start()方法时也会运行run()方法,区别是一个线程调度运行run()方法,一个直接调用线程中的run()方法!!

  • wait()和notify()

  • wait()与notify()是Object的方法,不是Thread的方法!!

  • wait()与notify()会配合使用,wait()表示线程挂起,notify()表示线程恢复;

  • wait()与sleep()的区别:简单来说,wait()会释放对象锁,而sleep()不会释放对象锁;

线程5状态,如何流转?

image.png

image.png

1. 新建状态(New) 用new语句创建的线程处于新建状态,此时它和其他Java对象一样,仅仅在堆区中被分配了内存。

2. 就绪状态(Runnable) 当一个线程对象创建后,其他线程调用它的start()方法,该线程就进入就绪状态,Java虚拟机会为它创建方法调用栈和程序计数器。处于这个状态的线程位于可运行池中,等待获得CPU的使用权。

3. 运行状态(Running) 处于这个状态的线程占用CPU,执行程序代码。只有处于就绪状态的线程才有机会转到运行状态。

4. 阻塞状态(Blocked) 阻塞状态是指线程因为某些原因放弃CPU,暂时停止运行。当线程处于阻塞状态时,Java虚拟机不会给线程分配CPU。直到线程重新进入就绪状态,它才有机会转到运行状态。

阻塞状态可分为以下3种:

  • 等待阻塞:运行的线程执行wait()方法,JVM会把该线程放入这个对象的等待池中。(wait会释放持有的锁)
  • 同步阻塞:运行的线程在获取对象的同步锁时,若该同步锁被别的线程占用,则JVM会把该线程放入锁池中。
  • 其他阻塞(Otherwise Blocked):运行的线程执行sleep()或join()方法,或者发出了I/O请求时,JVM会把该线程置为阻塞状态。

当sleep()状态超时、join()等待线程终止或者超时、或者I/O处理完毕时,线程重新转入就绪状态。(注意,sleep是不会释放持有的锁)

5. 死亡状态(Dead) 当线程退出run()方法时,就进入死亡状态,该线程结束生命周期。

创建线程的4种方式

1. 继承Thread类

Thread类本质上实现了Runnable接口的实例类,代表了线程的一个线程的实例,启动的线程唯一办法就是通过Thread类调用start()方法,start()方法是需要本地操作系统的支持,它将启动一个新的线程,并且执行run()方法。

image.png

public class MyThread extends Thread{
    int piao =10;
    @Override
    public void run() {
        while(piao>0){
          System.out.println(Thread.currentThread().getName()+"......"+piao--);
        }
    }
    public static void main(String[] args) {
        MyThread mt = new MyThread("x");
        mt.start();
    }
}
2. 继承Runnable接口

相比继承Thread类而言,实现接口的可扩展性得到了提升。

public class MyRunnable implements Runnable{
    int piao = 10;
    @Override
    public void run() {
        while(piao>0){
            System.out.println(Thread.currentThread().getName()+"-----"+piao--);
        }
    }

    public static void main(String[] args) {
        Runnable r =new MyRunnable();
        Thread t =new Thread(r);
        t.start();
    }
}
3. 实现Callable接口

Callable接口

  • Callable: 返回结果并且可能抛出异常的任务。
  • 优点:
    • 可以获得任务执行返回值;
    • 通过与Future的结合,可以实现利用Future来跟踪异步计算的结果。

Future接口

  • Future是一个接口,代表了一个异步计算的结果。接口中的方法用来检查计算是否完成、等待完成和得到计算的结果。
  • 当计算完成后,只能通过get()方法得到结果,get方法会阻塞直到结果准备好了。如果想取消,那么调用cancel()方法。其他方法用于确定任务是正常完成还是取消了。一旦计算完成了,那么这个计算就不能被取消。

FutureTask类

  • FutureTask类实现了RunnableFuture接口,而RunnnableFuture接口继承了Runnable和Future接口,所以说FutureTask是一个提供异步计算的结果的任务;
  • FutureTask可以用来包装Callable或者Runnbale对象。因为FutureTask实现了Runnable接口,所以FutureTask也可以被提交给Executor;

Runnable和Callable的区别

  • 1、Callable规定的方法是call(),Runnable规定的方法是run().
  • 2、Callable的任务执行后可返回值,而Runnable的任务是不能返回值得
  • 3、call方法可以抛出异常,run方法不可以
  • 4、运行Callable任务可以拿到一个Future对象,表示异步计算的结果。它提供了检查计算是否完成的方法,以等待计算的完成,并检索计算的结果。通过Future对象可以了解任务执行情况,可取消任务的执行,还可获取执行结果。
 //Callable 接口
  public interface Callable<V> {
     V call() throws Exception;
  }
  // Runnable 接口
  public interface Runnable {
      public abstract void run();
  }
Callable两种实现方式

1. 借助FutureTask执行 FutureTask类同时实现了两个接口,Future和Runnable接口,所以它既可以作为Runnable被线程执行,又可以作为Future得到Callable的返回值。

  //定义实现Callable接口的的实现类重写call方法。
  public class MyCallableTask implements Callable<Integer>{
      @Override
          public Integer call() throws Exception {
             //TODO 线程执行方法
          }
  }
  ---------------------------------------------------------
  //创建Callable对象
  Callable<Integer> mycallabletask = new MyCallableTask();
  //开始线程
  FutureTask<Integer> futuretask= new FutureTask<Integer>(mycallabletask);
  new Thread(futuretask).start();
  --------------------------------------------------------
  通过futuretask可以得到MyCallableTask的call()的运行结果:
  futuretask.get();

2. 借助线程池来运行

//线程池中执行Callable任务原型
public interface ExecutorService extends Executor {
  //提交一个Callable任务,返回值为一个Future类型
  <T> Future<T> submit(Callable<T> task);
      //other methods...
}

//借助线程池来运行Callable任务的一般流程
   ExecutorService exec = Executors.newCachedThreadPool();
   Future<Integer> future = exec.submit(new MyCallableTask());
   //通过future可以得到MyCallableTask的call()的运行结果:future.get();

代码举例

1public class CallableTest {
  public static void main(String[] args) throws ExecutionException, InterruptedException,TimeoutException{
      //创建一个线程池
      ExecutorService executor = Executors.newCachedThreadPool();
      Future<String> future = executor.submit(()-> {
              TimeUnit.SECONDS.sleep(5);
              return "CallableTest";
      });
      System.out.println(future.get());
      executor.shutdown();
  }
}

例2:Callable任务借助FutureTask运行
public class CallableAndFutureTask {
  Random random = new Random();
  public static void main(String[] args) {
      Callable<Integer> callable = new Callable<Integer>() {
          public Integer call() throws Exception {
              return random.nextInt(10000);
          }
      };
      FutureTask<Integer> future = new FutureTask<Integer>(callable);
      Thread thread = new Thread(future);
      thread.start();
      try {
          Thread.sleep(2000);
          System.out.println(future.get());
      } catch (Exception e) {
          e.printStackTrace();
      }
  }
}

4. 创建线程池 在第三种的方法中就提到了两种实现方法,一种线程池+future,另一种futuretask的方法。线程和数据库连接这些资源都是非常宝贵的资源。那么每次需要的时候创建,不需要的时候销毁,是非常浪费资源的。那么我们就可以使用缓存的策略,也就是使用线程池。

3:Callable任务和线程池一起使用,然后返回值是Future
public class CallableAndFuture {
   Random random = new Random();
   public static void main(String[] args) {
       ExecutorService threadPool = Executors.newSingleThreadExecutor();
       Future<Integer> future = threadPool.submit(new Callable<Integer>() {
           public Integer call() throws Exception {
               return random.nextInt(10000);
           }
       });
       try {
           Thread.sleep(3000);
           System.out.println(future.get());
       } catch (Exception e) {
           e.printStackTrace();
       }
   }
}

例4:当执行多个Callable任务,有多个返回值时,我们可以创建一个Future的集合:
class MyCallableTask implements Callable<String> {
  private int id;
  public OneTask(int id){
      this.id = id;
  }
  @Override
  public String call() throws Exception {
      for(int i = 0;i<5;i++){
          System.out.println("Thread"+ id);
          Thread.sleep(1000);
      }
      return "Result of callable: "+id;
  }
}
public class Test {
  public static void main(String[] args) {
      ExecutorService exec = Executors.newCachedThreadPool();
      ArrayList<Future<String>> results = new ArrayList<Future<String>>();
      for (int i = 0; i < 5; i++) {
          results.add(exec.submit(new MyCallableTask(i)));
      }
      for (Future<String> fs : results) {
          if (fs.isDone()) {
              try {
                  System.out.println(fs.get());
              } catch (Exception e) {
                  e.printStackTrace();
              }
          } else {
              System.out.println("MyCallableTask任务未完成!");
          }
      }
      exec.shutdown();
  }
}

线程方法中的异常如何处理?副线程可以捕获到吗?

线程池

在执行一个异步任务或并发任务时,往往是通过直接new Thread()方法来创建新的线程,这样做弊端较多,更好的解决方案是合理地利用线程池,线程池的优势很明显。 线程池其实就是一种多线程处理形式,处理过程中可以将任务添加到队列中,然后在创建线程后自动启动这些任务。这里的线程就是我们前面学过的线程,这里的任务就是我们前面学过的实现了Runnable或Callable接口的实例对象;

线程池的好处

  1. 降低系统资源消耗,通过重用已存在的线程,降低线程创建和销毁造成的消耗;
  2. 提高系统响应速度,当有任务到达时,无需等待新线程的创建便能立即执行;
  3. 方便线程并发数的管控,线程若是无限制的创建,不仅会额外消耗大量系统资源,更是占用过多资源而阻塞系统或oom(out of memory)等状况,从而降低系统的稳定性。线程池能有效管控线程,统一分配、调优,提高资源使用率;
  4. 更强大的功能,线程池提供了定时、定期以及可控线程数等功能的线程池,使用方便简单。

使用线程池最大的原因就是可以根据系统的需求和硬件环境灵活的控制线程的数量,且可以对所有线程进行统一的管理和控制,从而提高系统的运行效率,降低系统运行运行压力;当然了,使用线程池的原因不仅仅只有这些,我们可以从线程池自身的优点上来进一步了解线程池的好处; 1:线程和任务分离,提升线程重用性; 2:控制线程并发数量,降低服务器压力,统一管理所有线程; 3:提升系统响应速度,假如创建线程用的时间为T1,执行任务用的时间为T2,销毁线程用的时间为T3,那么使用线程池就免去了T1和T3的时间;

blog.csdn.net/qq_43061290…

线程池应用场景介绍

1:网购商品秒杀 2:云盘文件上传和下载 3:12306网上购票系统等

只要有并发的地方、任务数量大或小、每个任务执行时间长或短的都可以使用线程池; 只不过在使用线程池的时候,注意一下设置合理的线程池大小即可;

通用线程工厂

public static class testThreadPoolFactory implements ThreadFactory {
    private AtomicInteger threadIdx = new AtomicInteger(0);
    private String threadNamePrefix;
    public testThreadPoolFactory(String Prefix) {
        threadNamePrefix = Prefix;
    }
    @Override
    public Thread newThread(Runnable r) {
        Thread thread = new Thread(r);
        thread.setName(threadNamePrefix + "-xxljob-" + threadIdx.getAndIncrement());
        return thread;
    }
}

4种线程池创建方式

java通过Executors提供四种线程池 image.png 其他参数都相同,其中线程工厂的默认类为DefaultThreadFactory,线程饱和的默认策略为ThreadPoolExecutor.AbortPolicy。

newCachedThreadPool

/**
 * 可缓存无界线程池测试
 * 当线程池中的线程空闲时间超过60s则会自动回收该线程,核心线程数为0
 * 当任务超过线程池的线程数则创建新线程。线程池的大小上限为Integer.MAX_VALUE,
 * 可看做是无限大。
 */
@Test
public void cacheThreadPoolTest() {
    // 创建可缓存的无界线程池,可以指定线程工厂,也可以不指定线程工厂
    ExecutorService executorService = Executors.newCachedThreadPool(new testThreadPoolFactory("cachedThread"));
    for (int i = 0; i < 10; i++) {
        executorService.submit(() -> {
            print("cachedThreadPool");
            System.out.println(Thread.currentThread().getName());
                }
        );
    }
}

newFixedThreadPool

/**
 * 创建固定线程数量的线程池测试
 * 创建一个固定大小的线程池,该方法可指定线程池的固定大小,对于超出的线程会在LinkedBlockingQueue队列中等待
 * 核心线程数可以指定,线程空闲时间为0
 */
@Test
public void fixedThreadPoolTest() {
    ExecutorService executorService = Executors.newFixedThreadPool(5, new testThreadPoolFactory("fixedThreadPool"));
    for (int i = 0; i < 10; i++) {
        executorService.submit(() -> {
                    print("fixedThreadPool");
                    System.out.println(Thread.currentThread().getName());
                }
        );
    }
}

newScheduledThreadPool

/**
 * 创建定时周期执行的线程池测试
 *
 * schedule(Runnable command, long delay, TimeUnit unit),延迟一定时间后执行Runnable任务;
 * schedule(Callable callable, long delay, TimeUnit unit),延迟一定时间后执行Callable任务;
 * scheduleAtFixedRate(Runnable command, long initialDelay, long period, TimeUnit unit),延迟一定时间后,以间隔period时间的频率周期性地执行任务;
 * scheduleWithFixedDelay(Runnable command, long initialDelay, long delay,TimeUnit unit),与scheduleAtFixedRate()方法很类似,
 * 但是不同的是scheduleWithFixedDelay()方法的周期时间间隔是以上一个任务执行结束到下一个任务开始执行的间隔,而scheduleAtFixedRate()方法的周期时间间隔是以上一个任务开始执行到下一个任务开始执行的间隔,
 * 也就是这一些任务系列的触发时间都是可预知的。
 * ScheduledExecutorService功能强大,对于定时执行的任务,建议多采用该方法。
 *
 * 作者:张老梦
 * 链接:https://www.jianshu.com/p/9ce35af9100e
 * 来源:简书
 * 著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
 */
@Test
public void scheduleThreadPoolTest() {
    // 创建指定核心线程数,但最大线程数是Integer.MAX_VALUE的可定时执行或周期执行任务的线程池
    ScheduledExecutorService executorService = Executors.newScheduledThreadPool(5, new testThreadPoolFactory("scheduledThread"));

    // 定时执行一次的任务,延迟1s后执行
    executorService.schedule(new Runnable() {
        @Override
        public void run() {
            print("scheduleThreadPool");
            System.out.println(Thread.currentThread().getName() + ", delay 1s");
        }
    }, 1, TimeUnit.SECONDS);


    // 周期性地执行任务,延迟2s后,每3s一次地周期性执行任务
    executorService.scheduleAtFixedRate(new Runnable() {
        @Override
        public void run() {
            System.out.println(Thread.currentThread().getName() + ", every 3s");
        }
    }, 2, 3, TimeUnit.SECONDS);


    executorService.scheduleWithFixedDelay(new Runnable() {
        @Override
        public void run() {
            long start = new Date().getTime();
            System.out.println("scheduleWithFixedDelay 开始执行时间:" +
                    DateFormat.getTimeInstance().format(new Date()));
            try {
                Thread.sleep(5000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            long end = new Date().getTime();
            System.out.println("scheduleWithFixedDelay执行花费时间=" + (end - start) / 1000 + "m");
            System.out.println("scheduleWithFixedDelay执行完成时间:"
                    + DateFormat.getTimeInstance().format(new Date()));
            System.out.println("======================================");
        }
    }, 1, 2, TimeUnit.SECONDS);

}

newSingleThreadExecutor

/**
 * 创建只有一个线程的线程池测试
 * 该方法无参数,所有任务都保存队列LinkedBlockingQueue中,核心线程数为1,线程空闲时间为0
 * 等待唯一的单线程来执行任务,并保证所有任务按照指定顺序(FIFO或优先级)执行
 */
@Test
public void singleThreadPoolTest() {
    // 创建仅有单个线程的线程池
    ExecutorService executorService = Executors.newSingleThreadExecutor(new testThreadPoolFactory("singleThreadPool"));
    for (int i = 0; i < 10; i++) {
        executorService.submit(() -> {
                    print("singleThreadPool");
                    System.out.println(Thread.currentThread().getName());
                }
        );
    }

}

有哪几种常用的线程池?对应的好处,如何用?线程池如何设计?

线程池原理

Executors类提供4个静态工厂方法:newCachedThreadPool()、newFixedThreadPool(int)、newSingleThreadExecutor和newScheduledThreadPool(int)。这些方法最终都是通过ThreadPoolExecutor类来完成的,这里强烈建议大家直接使用Executors类提供的便捷的工厂方法,能完成绝大多数的用户场景,当需要更细节地调整配置,需要先了解每一项参数的意义。

线程池流程

image.png 判断核心线程池是否已满,即已创建线程数是否小于corePoolSize?没满则创建一个新的工作线程来执行任务。已满则进入下个流程。 判断工作队列是否已满?没满则将新提交的任务添加在工作队列,等待执行。已满则进入下个流程。 判断整个线程池是否已满,即已创建线程数是否小于maximumPoolSize?没满则创建一个新的工作线程来执行任务,已满则交给饱和策略来处理这个任务。

线程池关闭

调用线程池的shutdown()或shutdownNow()方法来关闭线程池

shutdown原理:将线程池状态设置成SHUTDOWN状态,然后中断所有没有正在执行任务的线程。 shutdownNow原理:将线程池的状态设置成STOP状态,然后中断所有任务(包括正在执行的)的线程,并返回等待执行任务的列表。 中断采用interrupt方法,所以无法响应中断的任务可能永远无法终止。但调用上述的两个关闭之一,isShutdown()方法返回值为true,当所有任务都已关闭,表示线程池关闭完成,则isTerminated()方法返回值为true。当需要立刻中断所有的线程,不一定需要执行完任务,可直接调用shutdownNow()方法。

ThreadPoolExecutor

public ThreadPoolExecutor(int corePoolSize,
                          int maximumPoolSize,
                          long keepAliveTime,
                          TimeUnit unit,
                          BlockingQueue<Runnable> workQueue) {
    this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue,
         Executors.defaultThreadFactory(), defaultHandler);
}

创建线程池,在构造一个新的线程池时,必须满足下面的条件: corePoolSize(线程池基本大小)必须大于或等于0; maximumPoolSize(线程池最大大小)必须大于或等于1; maximumPoolSize必须大于或等于corePoolSize; keepAliveTime(线程存活保持时间)必须大于或等于0; workQueue(任务队列)不能为空; threadFactory(线程工厂)不能为空,默认为DefaultThreadFactory类 handler(线程饱和策略)不能为空,默认策略为ThreadPoolExecutor.AbortPolicy。

public ThreadPoolExecutor(int corePoolSize,
                          int maximumPoolSize,
                          long keepAliveTime,
                          TimeUnit unit,
                          BlockingQueue<Runnable> workQueue,
                          ThreadFactory threadFactory,
                          RejectedExecutionHandler handler) {
    if (corePoolSize < 0 ||
        maximumPoolSize <= 0 ||
        maximumPoolSize < corePoolSize ||
        keepAliveTime < 0)
        throw new IllegalArgumentException();
    if (workQueue == null || threadFactory == null || handler == null)
        throw new NullPointerException();
    this.acc = System.getSecurityManager() == null ?
            null :
            AccessController.getContext();
    this.corePoolSize = corePoolSize;
    this.maximumPoolSize = maximumPoolSize;
    this.workQueue = workQueue;
    this.keepAliveTime = unit.toNanos(keepAliveTime);
    this.threadFactory = threadFactory;
    this.handler = handler;
}

参数说明:

corePoolSize(线程池基本大小):当向线程池提交一个任务时,若线程池已创建的线程数小于corePoolSize,即便此时存在空闲线程,也会通过创建一个新线程来执行该任务,直到已创建的线程数大于或等于corePoolSize时,才会根据是否存在空闲线程,来决定是否需要创建新的线程。除了利用提交新任务来创建和启动线程(按需构造),也可以通过 prestartCoreThread() 或 prestartAllCoreThreads() 方法来提前启动线程池中的基本线程。 maximumPoolSize(线程池最大大小):线程池所允许的最大线程个数。当队列满了,且已创建的线程数小于maximumPoolSize,则线程池会创建新的线程来执行任务。另外,对于无界队列,可忽略该参数。 keepAliveTime(线程存活保持时间):默认情况下,当线程池的线程个数多于corePoolSize时,线程的空闲时间超过keepAliveTime则会终止。但只要keepAliveTime大于0,allowCoreThreadTimeOut(boolean) 方法也可将此超时策略应用于核心线程。另外,也可以使用setKeepAliveTime()动态地更改参数。 unit(存活时间的单位):时间单位,分为7类,从细到粗顺序:NANOSECONDS(纳秒),MICROSECONDS(微妙),MILLISECONDS(毫秒),SECONDS(秒),MINUTES(分),HOURS(小时),DAYS(天); workQueue(任务队列):用于传输和保存等待执行任务的阻塞队列。可以使用此队列与线程池进行交互: 如果运行的线程数少于 corePoolSize,则 Executor 始终首选添加新的线程,而不进行排队。 如果运行的线程数等于或多于 corePoolSize,则 Executor 始终首选将请求加入队列,而不添加新的线程。 如果无法将请求加入队列,则创建新的线程,除非创建此线程超出 maximumPoolSize,在这种情况下,任务将被拒绝。 threadFactory(线程工厂):用于创建新线程。由同一个threadFactory创建的线程,属于同一个ThreadGroup,创建的线程优先级都为Thread.NORM_PRIORITY,以及是非守护进程状态。threadFactory创建的线程也是采用new Thread()方式,threadFactory创建的线程名都具有统一的风格:pool-m-thread-n(m为线程池的编号,n为线程池内的线程编号); handler(线程饱和策略):当线程池和队列都满了,则表明该线程池已达饱和状态。 ThreadPoolExecutor.AbortPolicy:处理程序遭到拒绝,则直接抛出运行时异常 RejectedExecutionException。(默认策略) ThreadPoolExecutor.CallerRunsPolicy:调用者所在线程来运行该任务,此策略提供简单的反馈控制机制,能够减缓新任务的提交速度。 ThreadPoolExecutor.DiscardPolicy:无法执行的任务将被删除。 ThreadPoolExecutor.DiscardOldestPolicy:如果执行程序尚未关闭,则位于工作队列头部的任务将被删除,然后重新尝试执行任务(如果再次失败,则重复此过程)。

队列排队详解

工作队列BlockingQueue详解

BlockingQueue的插入/移除/检查这些方法,对于不能立即满足但可能在将来某一时刻可以满足的操作,共有4种不同的处理方式:第一种是抛出一个异常,第二种是返回一个特殊值(null 或 false,具体取决于操作),第三种是在操作可以成功前,无限期地阻塞当前线程,第四种是在放弃前只在给定的最大时间限制内阻塞。如下表格: image.png

实现BlockingQueue接口的常见类如下:

ArrayBlockingQueue:基于数组的有界阻塞队列。队列按FIFO原则对元素进行排序,队列头部是在队列中存活时间最长的元素,队尾则是存在时间最短的元素。新元素插入到队列的尾部,队列获取操作则是从队列头部开始获得元素。 这是一个典型的“有界缓存区”,固定大小的数组在其中保持生产者插入的元素和使用者提取的元素。一旦创建了这样的缓存区,就不能再增加其容量。试图向已满队列中放入元素会导致操作受阻塞;试图从空队列中提取元素将导致类似阻塞。ArrayBlockingQueue构造方法可通过设置fairness参数来选择是否采用公平策略,公平性通常会降低吞吐量,但也减少了可变性和避免了“不平衡性”,可根据情况来决策。 LinkedBlockingQueue:基于链表的无界阻塞队列。与ArrayBlockingQueue一样采用FIFO原则对元素进行排序。基于链表的队列吞吐量通常要高于基于数组的队列。 SynchronousQueue:同步的阻塞队列。其中每个插入操作必须等待另一个线程的对应移除操作,等待过程一直处于阻塞状态,同理,每一个移除操作必须等到另一个线程的对应插入操作。SynchronousQueue没有任何容量。不能在同步队列上进行 peek,因为仅在试图要移除元素时,该元素才存在;除非另一个线程试图移除某个元素,否则也不能(使用任何方法)插入元素;也不能迭代队列,因为其中没有元素可用于迭代。Executors.newCachedThreadPool使用了该队列。 PriorityBlockingQueue:基于优先级的无界阻塞队列。优先级队列的元素按照其自然顺序进行排序,或者根据构造队列时提供的 Comparator 进行排序,具体取决于所使用的构造方法。优先级队列不允许使用 null 元素。依靠自然顺序的优先级队列还不允许插入不可比较的对象(这样做可能导致 ClassCastException)。虽然此队列逻辑上是无界的,但是资源被耗尽时试图执行 add 操作也将失败(导致 OutOfMemoryError)。

线程池监控

利用线程池提供的参数进行监控,参数如下:

taskCount:线程池需要执行的任务数量。 completedTaskCount:线程池在运行过程中已完成的任务数量,小于或等于taskCount。 largestPoolSize:线程池曾经创建过的最大线程数量,通过这个数据可以知道线程池是否满过。如等于线程池的最大大小,则表示线程池曾经满了。 getPoolSize:线程池的线程数量。如果线程池不销毁的话,池里的线程不会自动销毁,所以这个大小只增不减。 getActiveCount:获取活动的线程数。 通过扩展线程池进行监控:继承线程池并重写线程池的beforeExecute(),afterExecute()和terminated()方法,可以在任务执行前、后和线程池关闭前自定义行为。如监控任务的平均执行时间,最大执行时间和最小执行时间等。

合理配置线程池

需要针对具体情况而具体处理,不同的任务类别应采用不同规模的线程池,任务类别可划分为CPU密集型任务、IO密集型任务和混合型任务。

对于CPU密集型任务:线程池中线程个数应尽量少,不应大于CPU核心数; 对于IO密集型任务:由于IO操作速度远低于CPU速度,那么在运行这类任务时,CPU绝大多数时间处于空闲状态,那么线程池可以配置尽量多些的线程,以提高CPU利用率; 对于混合型任务:可以拆分为CPU密集型任务和IO密集型任务,当这两类任务执行时间相差无几时,通过拆分再执行的吞吐率高于串行执行的吞吐率,但若这两类任务执行时间有数据级的差距,那么没有拆分的意义。

线程池每个参数解释一遍?

实现 newSingleThreadPoll,应该怎么配置,构造方法传什么参数?

设置每个参数,给个线程,描述出完整的线程池执行流程?

线程方法中的异常如何处理?副线程可以捕获到吗?

为什么需要UncaughtExceptionHandler? ① 主线程可以轻松发现异常,子线程却不行 ② 子线程异常无法用传统方法捕获

两个解决方案: 1.手动在每个run方法里进行try catch(不建议,因为要在每一个run方法里加,而且不知道异常的类型); 2.利用UncaughtExceptionHandler; 使用UncaughtExceptionHandler就能捕捉到异常

//创建一个异常处理器,实现Thread.UncaughtExceptionHandler接口
public class MyUncaughtExceptionHanlder implements Thread.UncaughtExceptionHandler{
	//设定自己的处理器名字
    private String name;
    public MyUncaughtExceptionHanlder(String name){
        this.name = name;
    }
    @Override
    public void uncaughtException(Thread t, Throwable e) {
        Logger logger = Logger.getAnonymousLogger();
        //打印到日志
        logger.log(Level.WARNING,"线程异常,终止啦"+t.getName(),e);
        //输出到控制台
        System.out.println(name+"捕获了异常"+t.getName()+" 异常"+e);
    }
}

public class UseOwnUncaughtExceptionHandler implements Runnable {
    public static void main(String[] args) throws InterruptedException {
    	//使用异常处理器时,只需要指定异常处理器即可
        Thread.setDefaultUncaughtExceptionHandler(new MyUncaughtExceptionHanlder("捕获器1"));
        new Thread(new UseOwnUncaughtExceptionHandler(), "MyThread-1").start();
        Thread.sleep(300);
        new Thread(new UseOwnUncaughtExceptionHandler(), "MyThread-2").start();
        Thread.sleep(300);
        new Thread(new UseOwnUncaughtExceptionHandler(), "MyThread-3").start();
    }
    @Override
    public void run() {
        throw new RuntimeException();
    }
}

多线程下Java 线程间怎么实现同步?

一、什么是线程的同步 线程有自己的私有数据,比如栈和寄存器,同时与其它线程共享相同的虚拟内存和全局变量等资源。 在一般情况下,创建一个线程是不能提高程序的执行效率的,所以要创建多个线程。但是当多个线程同时读写同一份共享资源的时候,会引起冲突,例如在多个线程同时对同一个内存地址进行写入,由于CPU时间调度上的问题,写入数据会被多次的覆盖,所以就要使线程同步。这时候就需要引入线程同步机制使各个线程排队一个一个的对共享资源进行操作,而不是同时进行。

简单的说就是,在多线程编程里面,一些数据不允许被多个线程同时访问,此时就使用同步访问技术,保证数据在任何时刻,最多有一个线程访问,以保证数据的完整性。

二、为什么需要线程间的通信

  1. 多个线程并发执行时, 在默认情况下CPU是随机切换线程的,当我们需要多个线程来共同完成一件任务,并且我们希望他们有规律的执行, 那么多线程之间需要一些协调通信,以此来帮我们达到多线程共同操作一份数据。 2.当然如果我们没有使用线程通信来使用多线程共同操作同一份数据的话,虽然可以实现,但是在很大程度会造成多线程之间对同一共享变量的争夺,那样的话势必为造成很多错误和损失! 3.所以,我们才引出了线程之间的通信,多线程之间的通信能够避免对同一共享变量的争夺。

使用synchronized方式进行线程交互,用到的是同步对象的wait,notify和notifyAll方法 Lock也提供了类似的解决办法,首先通过lock对象得到一个Condition对象,然后分别调用这个Condition对象的:await, signal,signalAll 方法

线程间的通信方式 Object类中相关的方法有notify方法和wait方法。因为wait和notify方法定义在Object类中,因此会被所有的类所继承。这些方法都是final 的不能被重写的,不能通过子类覆写去改变它们的行为。

  1. wait()
    ①wait()方法:** 让当前线程进入等待,并释放锁。 ②wait(long)方法:** 让当前线程进入等待,并释放锁,不过等待时间为long,超过这个时间没有对当前线程进行唤醒,将自动唤醒

  2. notify() ③notify()方法让当前线程通知那些处于等待状态的线程,当前线程执行完毕后释放锁,并从其他线程中唤醒其中一个继续执行。 ④notifyAll()方法:** 让当前线程通知那些处于等待状态的线程,当前线程执行完毕后释放锁,将唤醒所有等待状态的线程。

  3. wait()与sleep()比较 当线程调用了wait()方法时,它会释放掉对象的锁。 Thread.sleep(),它会导致线程睡眠指定的毫秒数,但线程在睡眠的过程中是不会释放掉对象的锁的。 之所以我们应该尽量使用notifyAll()的原因就是,notify()非常容易导致死锁。

notify()与 notifyAll()的区别? Java提供了两个方法notify和notifyAll来唤醒在某些条件下等待的线程。

  • 当你调用notify时,只有一个等待线程会被唤醒而且它不能保证哪个线程会被唤醒,这取决于线程调度器。(随机性)
  • 如果你调用notifyAll方法,那么等待该锁的所有线程都会被唤醒,但是在执行剩余的代码之前,所有被唤醒的线程都将争夺锁。
  • 因此,notify和notifyAll之间的关键区别在于notify()只会唤醒一个线程,而notifyAll方法将唤醒所有线程。
private synchronized void go() {
    while (go == false){
        System.out.println(Thread.currentThread()
        + " is going to notify all or one thread waiting on this object");
        go = true; 
        //notify(); // only 1 out of 3 waiting thread WT1, WT2,WT3 will woke up
        notifyAll(); // all waiting thread  WT1, WT2,WT3 will woke up
}

wait()和notify()方法只能从synchronized方法或块中调用

lock锁实现 1.使用ReentrantLock实现同步 lock()方法:上锁 unlock()方法:释放锁 trylock():synchronized 是不占用到手不罢休的,会一直试图占用下去。与 synchronized 的钻牛角尖不一样,Lock接口还提供了一个trylock方法。

2.使用Condition实现等待/通知 首先通过lock对象得到一个Condition对象, 然后分别调用这个Condition对象的:await, signal,signalAll 方法。 使用 Condition 的 await signal signalAll 时,同样需要获得 Lock 锁,特性等同于 wait notify notifyAll

/** 存钱 */
public void saveMoney(int value, int threadId, String name,Condition condition) {
    System.out.println("在线程" + threadId + "运行存钱方法时" + name + "账户有" + money + "元");
    if (value > 0) {
        money = money + value;
    }
    //如果当前有线程在等待,存钱之后唤醒等的线程
    if (isAwait) {
        condition.signal();
        System.out.println(name+"账户余额充足,线程"+threadId+"调用signal()方法");
        //线程唤醒之后,将是否有等待线程的标志设置为false
        isAwait = false;
    }
    try {
        //休息一秒钟
        System.out.println("线程" + threadId + "存钱" + value + "元到" + name + "账户,现有余额" + money + "元");
        Thread.sleep(1000L);
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
}
 /** 取钱 */
public void fechMoney(int value, int threadId, String name,Condition condition ) {
    try {
        System.out.println("在线程" + threadId + "运行取钱方法时" + name + "账户有" + money + "元");
        //如果当前所要取的钱,大于银行卡余额,让释放当前取钱线程的占有,并等待
        while (value > money) {
            System.out.println(name+"账户余额不足,线程"+threadId+"调用await方法");
            //将是否有等待线程标志设置为true
            isAwait = true;
            condition.await();
        }
        money = money - value;
        System.out.println("线程" + threadId + "取钱" + value + "元," + name + "账户现有余额" + money + "元");
        //休眠一秒钟
        Thread.sleep(1000L);
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
}

生产者消费者模型是什么?

生产者消费者问题是线程模型中的经典问题:生产者和消费者在同一时间段内共用同一存储空间,生产者向空间里生产数据,而消费者取走数据。 image.png 阻塞队列就相当于一个缓冲区,平衡了生产者和消费者的处理能力。这个阻塞队列就是用来给生产者和消费者解耦的。

生产者消费者问题是研究多线程程序时绕不开的经典问题之一,它描述是有一块缓冲区作为仓库,生产者可以将产品放入仓库,消费者则可以从仓库中取走产品。在Java中一共有四种方法支持同步,其中前三个是同步方法,一个是管道方法。

  • (1)Object的wait() / notify()方法
  • (2)Lock和Condition的await() / signal()方法
  • (3)BlockingQueue阻塞队列方法
  • (4)PipedInputStream / PipedOutputStream

线程安全:阻塞非阻塞与同步异步的区别?

并发:在操作系统中,同个处理机上有多个程序同时运行即并发。并发可分为同步和互斥。

1)同步、互斥:

互斥:同一个资源同一时间只有一个访问者可以进行访问,其他访问者需要等前一个访问者访问结束才可以开始访问该资源。但互斥无法限制访问者对资源的访问顺序,即访问是无序的。

同步:分布在不同进程之间的若干程序片断,它们的运行必须严格按照规定的某种先后次序来运行,这种先后次序依赖于要完成的特定的任务。所以同步就是在互斥的基础上,通过其它机制实现访问者对资源的有序访问。

总结:同步是一种更为复杂的互斥,而互斥是一种特殊的同步。

2)同步、异步:

同步:同步就是顺序执行,执行完一个再执行下一个,需要等待、协调运行。

异步:异步和同步是相对的,异步就是彼此独立,在等待某事件的过程中继续做自己的事,不需要等待这一事件完成后再工作。

注意:

1)线程是实现异步的一个方式。可以在主线程创建一个新线程来做某件事,此时主线程不需等待子线程做完而是可以做其他事情。2)异步和多线程并不是一个同等关系。异步是最终目的,多线程只是我们实现异步的一种手段。

3)阻塞,非阻塞:

阻塞和非阻塞是当进程在访问数据时,根据IO操作的就绪状态不同而采取的不同处理方式,比如主程序调用一个函数要读取一个文件的内容,阻塞方式下主程序会等到函数读取完再继续往下执行,非阻塞方式下,读取函数会立刻返回一个状态值给主程序,主程序不等待文件读取完就继续往下执行。一般来说可以分为:同步阻塞,同步非阻塞,异步阻塞,异步非阻塞。

4)同步阻塞,同步非阻塞,异步阻塞,异步非阻塞:

以发送方发出请求要接收方读取某文件内容为例。

同步阻塞:发送方发出请求后一直等待(同步),接收方开始读取文件,如果不能马上得到读取结果就一直等,直到获取读取结果再响应发送发,等待期间不可做其他操作(阻塞)。

同步非阻塞:发送方发出请求后一直等待(同步),接收方开始读取文件,如果不能马上的得到读取结果,就立即返回,接收方继续去做其他事情。此时并未响应发送方,发送方一直在等待。直到IO操作(这里是读取文件)完成后,接收方获得读取结果响应发送方,接收方才可以进入下一次请求过程。(实际不应用)

异步阻塞:发送方发出请求后,不等待响应,继续其他工作(异步),接收方读取文件如果不能马上得到结果,就一直等到返回结果后,才响应发送方,期间不能进行其他操作(阻塞)。(实际不应用)

异步非阻塞:发送方发出请求后,不等待响应,继续其他工作(异步),接收方读取文件如果不能马上得到结果,也不等待,而是马上返回取做其他事情。当IO操作(读取文件)完成以后,将完成状态和结果通知接收方,接收方在响应发送方。(效率最高)

总结:1)同步与异步是对应的,它们是线程之间的关系,两个线程之间要么是同步的,要么是异步的。2)阻塞与非阻塞是对同一个线程来说的,在某个时刻,线程要么处于阻塞,要么处于非阻塞。3)阻塞是使用同步机制的结果,非阻塞则是使用异步机制的结果。

理解同步阻塞、同步非阻塞、异步阻塞、异步阻塞、异步非阻塞

同步/异步关注的是消息通知的机制,而阻塞/非阻塞关注的是程序(线程)等待消息通知时的状态。

以小明下载文件打个比方,从这两个关注点来再次说明这两组概念,希望能够更好的促进大家的理解。

同步阻塞:小明一直盯着下载进度条,到 100% 的时候就完成。 - 同步体现在:等待下载完成通知。 - 阻塞体现在:等待下载完成通知过程中,不能做其他任务处理。

同步非阻塞:小明提交下载任务后就去干别的,每过一段时间就去瞄一眼进度条,看到 100% 就完成。 - 同步体现在:等待下载完成通知。 - 非阻塞体现在:等待下载完成通知过程中,去干别的任务了,只是时不时会瞄一眼进度条。【小明必须要在两个任务间切换,关注下载进度】

异步阻塞:小明换了个有下载完成通知功能的软件,下载完成就“叮”一声。不过小明不做别的事,仍然一直等待“叮”的声音。 - 异步体现在:下载完成“叮”一声通知。 - 阻塞体现在:等待下载完成“叮”一声通知过程中,不能做其他任务处理。

异步非阻塞:仍然是那个会“叮”一声的下载软件,小明提交下载任务后就去干别的,听到“叮”的一声就知道完成了。 - 异步体现在:下载完成“叮”一声通知。 - 非阻塞体现在:等待下载完成“叮”一声通知过程中,去干别的任务了,只需要接收“叮”声通知即可。【软件处理下载任务,小明处理其他任务,不需关注进度,只需接收软件“叮”声通知,即可】

也就是说,同步/异步是“下载完成消息”通知的方式(机制),而阻塞/非阻塞则是在等待“下载完成消息”通知过程中的状态(能不能干其他任务),在不同的场景下,同步/异步、阻塞/非阻塞的四种组合都有应用。

所以,综上所述,同步和异步仅仅是关注的消息如何通知的机制,而阻塞与非阻塞关注的是等待消息通知时的状态。也就是说,同步的情况下,是由处理消息者自己去等待消息是否被触发,而异步的情况下是由触发 机制来通知处理消息者,所以在异步机制中,处理消息者和触发机制之间就需要一个连接的桥梁。在小明的例子中,这个桥梁就是软件“叮”的声音。

面试题目举例 1、什么是线程同步和互斥?

线程同步:每个线程之间按预定的先后次序进行运行,协同、协助、互相配合。可以理解成“你说完,我再做”。有了线程同步,每个线程才不是自己做自己的事情,而是协同完成某件大事。

线程互斥:当有若干个线程访问同一块资源时,规定同一时间只有一个线程可以得到访问权,其它线程需要等占用资源者释放该资源才可以申请访问。线程互斥可以看成是一种特殊的线程同步。

2、线程同步与阻塞的关系?同步一定阻塞吗?阻塞一定同步吗?

同步是个过程,阻塞是线程的一种状态:当多个线程访问同一资源时,规定同一时间只有一个线程可以进行访问,所以后访问的线程将阻塞,等待前访问的线程访问完。

注意:线程同步不一定发生阻塞!线程同步的时候,需要协调推进速度,只有当访问同一资源出现互相等待和互相唤醒会发生阻塞。而阻塞了一定是同步,后访问的等待获取资源,线程进入阻塞状态,借以实现多线程同步的过程。

3、线程同步互斥的方式

临界区(Critical Section):适合一个进程内的多线程访问公共区域或代码段时使用 互斥量 (Mutex):适合不同进程内多线程访问公共区域或代码段时使用,与临界区相似。 事件(Event):通过线程间触发事件实现同步互斥 信号量(Semaphore):与临界区和互斥量不同,可以实现多个线程同时访问公共区域数据,原理与操作系统中PV操作类似,先设置一个访问公共区域的线程最大连接数,每有一个线程访问共享区资源数就减一,直到资源数小于等于零。

讲一下线程安全问题产生的原因?

  1. CPU抢占式执行,线程是抢占式的,线程之间调度充满随机性。无法避免;
  2. 多个线程同时操作一个变量:让每个线程操作自己的私有变量,但是实现较为复杂,可能解决,也可能解决不了;
  3. 原子性问题:针对变量的操作不是原子性的,这就是上面的load->add->save等指令,这些操作是绑定的,原子性的,把多个指令看成一个.这需要加锁才能解决;
  4. 内存不可见:使用synchronized和volatile关键字解决;
  5. 指令重排序(编译器优化):系统不按照代码顺序执行,而为提高效率,改变指令的执行顺序,这样就会产生bug;

多线程和高并发有什么区别?

多线程是完成任务的一种方法,高并发是系统运行的一种状态,通过多线程有助于系统承受高并发的状态的实现。

高并发是系统运行过程中遇到的一种“短时间内遇到大量操作请求”的情况,收到大量请求(例如:12306的抢票情况;天猫双十一活动)该情况的发生会导致系统在这段时间内执行大量操作,例如对资源的请求、数据库的操作等。

  1. 高并发的处理指标

高并发相关常用的一些指标有:响应时间、吞吐量、每秒查询率QPS、并发用户数

  • 响应时间(Response Time)
    系统对请求做出响应的时间。例如系统处理一个HTTP请求需要200ms,这个200ms就是系统的响应时间
  • 吞吐量(Throughput)
    单位时间内处理的请求数量。
  • 每秒查询率QPS(Query Per Second)
    每秒响应请求数。在互联网领域,这个指标和吞吐量区分的没有这么明显。
  • 并发用户数
    同时承载正常使用系统功能的用户数量。例如一个即时通讯系统,同时在线量一定程度上代表了系统的并发用户数。
  1. 高并发解决方案
  • 静态资源结合CDN来解决图片文件等访问
  • 分布式缓存:redis、memcached等。
  • 消息队列中间件:activeMQ等,解决大量消息的异步处理能力。
  • 应用拆分:一个工程被拆分为多个工程部署,利用dubbo解决多工程之间的通信。
  • 数据库垂直拆分和水平拆分(分库分表)等。
  • 数据库读写分离,解决大数据的查询问题。
  • 利用nosql ,例如mongoDB配合mysql组合使用。
  • 建立大数据访问情况下的服务降级以及限流机制等。