多线程

本文详细探讨了进程与线程的概念,线程的创建与特性,线程同步机制,包括synchronized、Lock、死锁及线程通信。此外,还介绍了线程池的原理与使用,以及AQS框架和并发工具类如CountDownLatch、CyclicBarrier和Semaphore的运用。
摘要由CSDN通过智能技术生成

多线程

仅个人笔记

进程与线程

  1. 进程:是代码在数据集合上的一次运行活动,是系统进行资源分配和调度的基本单位。

  2. 线程:是进程的一个执行路径,一个进程中至少有一个线程,进程中的多个线程共享进程的 资源。

  3. 虽然系统是把资源分给进程,但是CPU很特殊,是被分配到线程的,所以线程是CPU分配的基本单位。

  4. 进程与线程的关系:一个进程中有多个线程,多个线程共享进程的堆和方法区资源,但是每个线程有自己的程序计数器和栈区域。

  5. 程序计数器:是一块内存区域,用来记录线程当前要执行的指令地址 。

  6. :用于存储该线程的局部变量,这些局部变量是该线程私有的,除此之外还用来存放线程的调用栈祯。

  7. :是一个进程中最大的一块内存,堆是被进程中的所有线程共享的。

  8. 方法区:则用来存放 NM 加载的类、常量及静态变量等信息,也是线程共享的 。

  9. 进程与线程的区别:

    • 进程:有独立的地址空间,一个进程崩溃后,在保护模式下不会对其它进程产生影响。
    • 线程:是一个进程中的不同执行路径。线程有自己的堆栈和局部变量,但线程之间没有单独的地址空间,一个线程死掉就等于整个进程死掉。

总结1

  1. 一个程序至少有一个进程,一个进程至少有一个线程.

  2. 线程的划分尺度小于进程,使得多线程程序的并发性高。

  3. 另外,进程在执行过程中拥有独立的内存单元,而多个线程共享内存,从而极大地提高了程序的运行效率。

  4. 每个独立的线程有一个程序运行的入口、顺序执行序列和程序的出口。但是线程不能够独立执行,必须依存在应用程序中,由应用程序提供多个线程执行控制。

  5. 从逻辑角度来看,多线程的意义在于一个应用程序中,有多个执行部分可以同时执行。但操作系统并没有将多个线程看做多个独立的应用,来实现进程的调度和管理以及资源分配。这就是进程和线程的重要区别

并发与并行

  1. 并发:是指同一个时间段内多个任务同时都在执行,并且都没有执行结束。并发任务强调在一个时间段内同时执行,而一个时间段由多个单位时间累积而成,所以说并发的多个任务在单位时间内不一定同时在执行 。
  2. 并行:是说在单位时间内多个任务同时在执行 。
  3. 在多线程编程实践中,线程的个数往往多于CPU的个数,所以一般都称多线程并发编程而不是多线程并行编程。

并发过程中常见的问题

  1. 线程安全问题:

    多个线程同时操作共享变量i时,会出现线程1更新共享变量i的值,但是其他线程获取到的是共享变量没有被更新之前的值。就会导致数据不准确问题。

  2. 共享内存不可见性问题

    Java 内存模型规定,将所有的变量都存放在主内存中,当线程使用变量时,会把主内存里面的变量复制到自己的工作空间或者叫作工作内存,线程读写变量时操作的是自己工作内存中的变量 。(当线程A里有缓存,线程B修改变量后,线程A并不知道,即线程B写入的值对线程A不可见)

  3. synchronized 的内存语义:

    这个内存语义就可以解决共享变量内存可见性问题。进入synchronized块的内存语义是把在synchronized块内使用到的变量从线程的工作内存中清除,这样在synchronized块内使用到该变量时就不会从线程的工作内存中获取,而是直接从主内存中获取。退出synchronized块的内存语义是把在synchronized块内对共享变量的修改刷新到主内存。会造成上下文切换的开销,独占锁,降低并发性。

  4. Volatile的理解:

    该关键字可以确保对一个变量的更新对其他线程马上可见。当一个变量被声明为volatile时,线程在写入变量时不会把值缓存在寄存器或者其他地方,而是会把值刷新回主内存。当其他线程读取该共享变量时-,会从主内存重新获取最新值,而不是使用当前线程的工作内存中的值。volatile的内存语义和synchronized有相似之处,具体来说就是,当线程写入了volatile变量值时就等价于线程退出synchronized同步块(把写入工作内存的变量值同步到主内存),读取volatile变量值时就相当于进入同步块(先清空本地内存变量值,再从主内存获取最新值)。不能保证原子性。

创建线程

  1. 继承Thread类

    重写run方法:使用继承方式的好处是,在run()方法内获取当前线程直接使用this就可以了,无须使用Thread.currentThread()方法;不好的地方是Java不支持多继承,如果继承了Thread类,那么就不能再继承其他类。另外任务与代码没有分离,当多个线程执行一样的任务时需要多份任务代码。

  2. 实现Runable接口

    实现run方法:解决继承Thread的缺点,没有返回值

  3. 实现Callable接口

    实现call方法:前两种方式都没办法拿到任务的返回结果,但是Callable方式可以

    public class TestCall implements Callable {
        @Override
        public Object call() throws Exception {
            return "hhh";
        }
     
        public static void main(String[] args){
            FutureTask<String> futureTask = new FutureTask<String>(new TestCall());
            new Thread(futureTask).start();
            try {
                
                String result = futureTask.get();
                System.out.println(result);
                
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }

线程特性

  1. 线程能被标记为守护线程,也可以是用户线程
  2. 每个线程均分配一个name,默认为(Thread-自增数字)的组合
  3. 每个线程都有优先级.高优先级线程优先于低优先级线程执行. 1-10,默认为5
  4. main所在的线程组为main,构造线程的时候没有现实的指定线程组,线程组默认和父线程一样
  5. 当线程中的run()方法代码里面又创建了一个新的线程对象时,新创建的线程优先级和父线程优先级一样.
  6. 当且仅当父线程为守护线程时,新创建的线程才会是守护线程.
  7. 当JVM启动时,通常会有唯一的一个非守护线程(这一线程用于调用指定类的main()方法)
    • JVM会持续执行线程直到下面情况某一个发生为止:
      • 类运行时exit()方法被调用 且 安全机制允许此exit()方法的调用.
      • 所有非守护类型的线程均已经终止,or run()方法调用返回or在run()方法外部抛出了一些可传播性的异常.

线程同步

为了解决并发问题,Java的多线程支持引入同步监视器,使用同步监视器的通用方法就是同步代码块。

  1. 同步代码块

    //使用obj为同步监视器,任何线程进入下面同步代码块之前
    //必须先获得对obj的锁定----其它线程无法获得锁,也就无法修改它
    //符合“加锁->修改->释放锁”的逻辑
    synchronized(obj){
        //此处的代码块就是同步代码块
    }
  2. 同步方法

    使用synchronized关键字来修改某个方法,称为同步方法。同步方法的同步监视器就是this,也就是调用该方法的对象。

    public synchronized void draw(double drawAmount){
        
    }
  3. 同步锁(Lock)

    class x{
      //定义锁对象
      private final ReentrantLock lock = new ReentrantLock();
      //...
      //需要保证线程安全的方法
      public void m(){
         //加锁
         lock.lock();
         try{
           //..需要保证线程安全的代码
         }finally{
           lock.unlock();
         }
      }
    }
  4. 死锁

    当两个线程相互等待对方释放同步监视器时就会发生死锁,Java虚拟机没有检测,也没有采用措施来处理死锁的情况。

线程通信

  1. 传统的线程通信

    Object提供了三个方法:wait(),notify()和notifyAll(),必须由同步监视器调用。

    • wait():导致当前线程等待,直到其他线程调用该同步监视器的notify()或notifyAll()方法来唤醒该线程。
    • notify():唤醒在此同步监视器上等待的单个线程。如果所有线程都在此同步监视器上等待,则任意唤醒一个线程。
  2. 使用Condition控制线程通信

    当使用Lock对象来保证同步时,Java提供了一个Condition类来保持协调。Lock替代了同步方法或同步代码块,Condition替代了同步监视器的功能。

    • await():导致当前线程等待,知道其他线程调用该Condition的Signal()方法或signalAll()方法
    • signal():唤醒在此Lock上的单个线程。若有多个线程在Lock对象上等待,任意唤醒一个。
    • signalAll():唤醒在此Lock对象上等待的所有线程
    //显示定义Lock对象
    private final Lock lock = new ReentrantLock();
    //获得指定Lock对象对应的Condition
    private final Condition cond = lock.newCondition();
    int flag = false;
    public void X(int a){
      lock.lock();
      try{
         if(!flag){
           cond.await();
         }else{
           //...
           cond.signalAll();
         }finally{
           lock.unlock();
         }
      }
    }
  3. 使用阻塞队列(BlockingQueue)控制线程通信

    class Consumer extends Thread{
        private BlockingQueue<String> bq;
        public Consumer(BlockingQueue<String> bq) {
            this.bq = bq;
        }
        public void run() {
            while(true) {
                System.out.println(getName() + "消费者准备消费集合元素!");
                try {
                    Thread.sleep(200);
                    //尝试取出元素,如果队列已空,则线程被阻塞
                    bq.take();
                }catch(Exception ex){
                    ex.printStackTrace();
                }
                System.out.println(getName() + "消费完成" + bq);
            }
        }
    }
    public class Producer extends Thread{
        private BlockingQueue<String> bq;
        public Producer(BlockingQueue<String> bq) {
            this.bq = bq;
        }
        public void run() {
            String[] strArr = new String[]{"Java","Struts","Spring"};
            for(int i = 0; i < 5; i++) {
                System.out.println(getName() + "生产者准备生产集合元素!");
                try {
                    Thread.sleep(200);
                    //尝试放入元素,如果队列已满,则线程被阻塞
                    bq.put(strArr[i%3]);
                }catch(Exception ex){
                    ex.printStackTrace();
                }
                System.out.println(getName() + "生产完成" + bq);
            }
        }
        
        public static void main(String[] args) {
            //创建一个容量为1的BockingQueue
            BlockingQueue<String> bq = new ArrayBlockingQueue<>(1);
            new Producer(bq).start();
            new Producer(bq).start();
            new Producer(bq).start();
            new Consumer(bq).start();
        }
    }

线程池

当我们线程创建过多时,容易引发内存溢出,因此就有必要使用线程池的技术

线程池的优势

  1. 降低资源消耗。通过重复利用已创建的线程降低线程创建和销毁造成的消耗。
  2. 提高响应速度。当任务到达时,任务可以不需要等到线程创建就能立即执行。
  3. 提高线程的可管理性。线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一的分配,调优和监控。

线程池的使用

//线程池的真正实现类是ThreadPoolExecutor,其构造方法有如下4种:

public ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue) {

    this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue, Executors.defaultThreadFactory(),  defaultHandler);

}

 

public ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue, ThreadFactory threadFactory) {

    this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue, threadFactory, defaultHandler);

}

 

public ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue, RejectedExecutionHandler handler) {

    this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue, Executors.defaultThreadFactory(), handler);

}

 

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.corePoolSize = corePoolSize;
    this.maximumPoolSize = maximumPoolSize;
    this.workQueue = workQueue;
    this.keepAliveTime = unit.toNanos(keepAliveTime);
    this.threadFactory = threadFactory;
    this.handler = handler;

}
参数含义
corePoolSize(必需)核心线程数。默认情况下,核心线程会一直存活,但是当将allowCoreThreadTimeout设置为true时,核心线程也会超时回收。
maximumPoolSize(必需)线程池所能容纳的最大线程数。当活跃线程数达到该数值后,后续的新任务将会阻塞。
keepAliveTime(必需)线程闲置超时时长。如果超过该时长,非核心线程就会被回收。如果将allowCoreThreadTimeout设置为true时,核心线程也会超时回收。
unit(必需)指定keepAliveTime参数的时间单位。常用的有:TimeUnit.MILLISECONDS(毫秒)、TimeUnit.SECONDS(秒)、TimeUnit.MINUTES(分)。
workQueue(必需)任务队列。通过线程池的execute()方法提交的Runnable对象将存储在该参数中。其采用阻塞队列实现。
threadFactory(可选)线程工厂。用于指定为线程池创建新线程的方式。
handler(可选)拒绝策略。当达到最大线程数时需要执行的饱和策略。
//线程池的使用流程如下:


// 创建线程池
Executor threadPool = new ThreadPoolExecutor(CORE_POOL_SIZE,
                                             MAXIMUM_POOL_SIZE,
                                             KEEP_ALIVE,
                                             TimeUnit.SECONDS,
                                             sPoolWorkQueue,
                                             sThreadFactory
                                            );

// 向线程池提交任务
threadPool.execute(new Runnable() {
    @Override
    public void run() {
        ... // 线程执行的任务
    }
});

// 关闭线程池
threadPool.shutdown(); // 设置线程池的状态为SHUTDOWN,然后中断所有没有正在执行任务的线程
threadPool.shutdownNow(); // 设置线程池的状态为 STOP,然后尝试停止所有的正在执行或暂停任务的线程,并返回等待执行任务的列表

线程池的工作原理

img

任务队列(workQueue)

任务队列是基于阻塞队列实现的,即采用生产者消费者模式,在Java中需要实现BlockingQueue接口。但Java提供了7种阻塞队列的实现:

  1. ArrayBlockingQueue:一个由数组结构组成的有界阻塞队列(数组结构可配合指针实现一个环形队列)。
  2. LinkedBlockingQueue: 一个由链表结构组成的有界阻塞队列,在未指明容量时,容量默认为Integer.MAX_VALUE
  3. PriorityBlockingQueue: 一个支持优先级排序的无界阻塞队列,对元素没有要求,可以实现Comparable接口也可以提供Comparator来对队列中的元素进行比较。跟时间没有任何关系,仅仅是按照优先级取任务。
  4. DelayQueue:类似于PriorityBlockingQueue,是二叉堆实现的无界优先级阻塞队列。要求元素都实现Delayed接口,通过执行时延从队列中提取任务,时间没到任务取不出来。
  5. SynchronousQueue: 一个不存储元素的阻塞队列,消费者线程调用take()方法的时候就会发生阻塞,直到有一个生产者线程生产了一个元素,消费者线程就可以拿到这个元素并返回;生产者线程调用put()方法的时候也会发生阻塞,直到有一个消费者线程消费了一个元素,生产者才会返回。
  6. LinkedBlockingDeque: 使用双向队列实现的有界双端阻塞队列。双端意味着可以像普通队列一样FIFO(先进先出),也可以像栈一样FILO(先进后出)。
  7. LinkedTransferQueue: 它是ConcurrentLinkedQueue、LinkedBlockingQueue和SynchronousQueue的结合体,但是把它用在ThreadPoolExecutor中,和LinkedBlockingQueue行为一致,但是是无界的阻塞队列。

注意有界队列和无界队列的区别:如果使用有界队列,当队列饱和时并超过最大线程数时就会执行拒绝策略;而如果使用无界队列,因为任务队列永远都可以添加任务,所以设置maximumPoolSize没有任何意义。

线程工厂(threadFactory)
/*
线程工厂指定创建线程的方式,需要实现**ThreadFactory**接口,并实现newThread(Runnable r)方法。该参数可以不用指定,Executors框架已经为我们实现了一个默认的线程工厂
*/


/**
 * The default thread factory.
 */
private static class DefaultThreadFactory implements ThreadFactory {

    private static final AtomicInteger poolNumber = new AtomicInteger(1);
    private final ThreadGroup group;
    private final AtomicInteger threadNumber = new AtomicInteger(1);
    private final String namePrefix;

 

    DefaultThreadFactory() {

        SecurityManager s = System.getSecurityManager();
        group = (s != null) ? s.getThreadGroup():Thread.currentThread().getThreadGroup();
        namePrefix = "pool-" +
                      poolNumber.getAndIncrement() +
                     "-thread-";
    }

    public Thread newThread(Runnable r) {

        Thread t = new Thread(group, r,
                              namePrefix + threadNumber.getAndIncrement(),
                              0);

        if (t.isDaemon())
            t.setDaemon(false);
        if (t.getPriority() != Thread.NORM_PRIORITY)
            t.setPriority(Thread.NORM_PRIORITY);
        return t;
    }
}
拒绝策略(handler)

当线程池的线程数达到最大线程数时,需要执行拒绝策略。拒绝策略需要实现RejectedExecutionHandler接口,并实现rejectedExecution(Runnable r, ThreadPoolExecutor executor)方法。不过Executors框架已经实现了4种拒绝策略:

  1. AbortPolicy(默认):丢弃任务并抛出RejectedExecutionException异常。
  2. CallerRunsPolicy:由调用线程处理该任务。
  3. DiscardPolicy:丢弃任务,但是不抛出异常。可以配合这种模式进行自定义的处理方式。
  4. DiscardOldestPolicy:丢弃队列最早的未处理任务,然后重新尝试执行任务。

功能线程池

Executors封装了4种常见的功能线程池(现在已经不建议使用),如下:

  • 定长线程池(FixedThreadPool)
    • 特点:只有核心线程,线程数量固定,执行完立即回收,任务队列为链表结构的有界队列。
    • 应用场景:控制线程最大并发数。
  • 定时线程池(ScheduledThreadPool )
    • 特点:核心线程数量固定,非核心线程数量无限,执行完闲置10ms后回收,任务队列为延时阻塞队列。
    • 应用场景:执行定时或周期性的任务。
  • 可缓存线程池(CachedThreadPool)
    • 特点:无核心线程,非核心线程数量无限,执行完闲置60s后回收,任务队列为不存储元素的阻塞队列。
    • 应用场景:执行大量、耗时少的任务。
  • 单线程化线程池(SingleThreadExecutor)
    • 特点:只有1个核心线程,无非核心线程,执行完立即回收,任务队列为链表结构的有界队列。
    • 应用场景:不适合并发但可能引起IO阻塞性及影响UI线程响应的操作,如数据库操作、文件操作等。

并发工具工具

CountDownLatch、CyclicBarrier、Semophore

CountDownLatch

(Latch,门闩)

public class ThreadPoolTest {
    private static AtomicInteger ai = new AtomicInteger();
    private static CountDownLatch count = new CountDownLatch(4); 

    public static void main(String[] args) throws InterruptedException {
        for (int i = 0; i < 4; i++) {
            new Thread(new Runnable() {
                @Override
                public void run() {                    
                    System.out.println(ai.getAndIncrement());
                    try {
                        Thread.sleep(3000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    count.countDown();
                }
            }).start();
        }
        count.await();
        System.out.println("门开了");
    }
}

(跟Thread.join()类似,就是当前线程等待别的线程执行完了再执行。原理就是await时加共享锁,countDown时释放共享锁。)

CyclicBarrier

(循环使用的屏障)

public class ThreadPoolTest {

    private static AtomicInteger ai = new AtomicInteger();

    private static CyclicBarrier barrier = new CyclicBarrier(4, new Runnable() {
        @Override
        public void run() {
            System.out.println("到我了?");
        }
    }); 

    public static void main(String[] args) {
        for (int i = 0; i < 4; i++) {
            new Thread(new Runnable() {
                @Override
                public void run() {                    
                    System.out.println(ai.getAndIncrement());
                    try {
                        Thread.sleep(3000);
                        barrier.await();
                    } catch (Exception e) {
                        e.printStackTrace();
                    }
                }
            }).start();

        }
    }
}

(可以在屏障里指定如果所有线程都到达屏障了,后面应该执行哪个线程。)

Semophore

(信号量,控制并发量,分布式系统也有相似概念,称为“限流”。限流是有必要的,比如系统里有一个导出汇总excel的功能,不限制并发量,系统立马就OOM了。)

public class ThreadPoolTest {
    private static AtomicInteger ai = new AtomicInteger();
    //我们新建一条三车道的高速公路
    private static Semaphore semaphore = new Semaphore(3);
 
    public static void main(String[] args) {
        //高速入口堵了20辆车
        ExecutorService executors = Executors.newFixedThreadPool(20);
        for (int i = 0; i < 20; i++) {
            executors.execute(new Runnable() {
                @Override
                public void run() {
                    try {
                        //放行一辆
                        semaphore.acquire();
                        Thread.sleep(3000);
                    } catch (Exception e) {
                        e.printStackTrace();
                    } finally {
                        //车道空出来了
                        semaphore.release();
                    }
                    System.out.println(ai.getAndIncrement());
                }
            });
        }
    }
}

线程组

在java的多线程处理中有线程组ThreadGroup的概念,ThreadGroup是为了方便线程管理出现了,可以统一设定线程组的一些属性,比如setDaemon,设置未处理异常的处理方法,设置统一的安全策略等等;也可以通过线程组方便的获得线程的一些信息。

每一个ThreadGroup都可以包含一组的子线程和一组子线程组,在一个进程中线程组是以树形的方式存在,通常情况下根线程组是system线程组。system线程组下是main线程组,默认情况下第一级应用自己的线程组是通过main线程组创建出来的。

线程组和线程池的区别

线程组和线程池是两个不同的概念,他们的作用完全不同,前者是为了方便线程的管理,后者是为了管理线程的生命周期,复用线程,减少创建销毁线程的开销。

AQS

所谓AQS,指的是AbstractQueuedSynchronizer,它提供了一种实现阻塞锁和一系列依赖FIFO等待队列的同步器的框架,ReentrantLock、Semaphore、CountDownLatch、CyclicBarrier等并发类均是基于AQS来实现的,具体用法是通过继承AQS实现其模板方法,然后将子类作为同步组件的内部类。

(头大,先略了)

参考自:

https://www.cnblogs.com/zsql/p/11144688.html

https://www.cnblogs.com/chenzida/articles/9515084.html

https://blog.csdn.net/u013541140/article/details/95225769

https://blog.csdn.net/u010266988/article/details/83960541

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值