Java线程以及多线程介绍

深入浅出多线程

线程:线程的概念

线程:(英文:thread)是操作系统能够运算调度的最小单位。它被包含在进程中,是进程中的实际运作单位。一条线程指的是进程中一个 单一顺序的控制流,一个进程中可以并发多个线程,每条线程并行执行不同的任务。

1.1 线程与进程的区别

进程里包含线程/内存/文件(网络句柄)

进程:指在系统中正在运行的一个应用程序;程序一旦运行就是进程;进程是资源分配的最小单位

线程:系统分配处理器时间资源的基本单位,或者说线程之内独立执行的一个单元执行流。线程是程序执行的最小单位。

也就是进程可以包含多个线程,而线程是程序执行的最小单位

1.2 线程的状态

  1. NEW: 线程刚创建
  2. RUNNABLE:在JVM中正在运行的线程,其中运行状态可以有运行RUNNABLE和READY两种状态,而系统调度进行状态切换
  3. BLOCKED:线程处于阻塞状态,等待监视锁,可以重新进行同步代码块中执行
  4. WAITING:等待状态
  5. TIME_WAITING:调用Sleep(),Join(),Wait()方法可能导致线程处于等待状态
  6. TERMINATED:线程执行完毕,已经退出

1.3 Notify和Wait的作用

Notify

唤醒一个正在等待这个对象的线程监控。如果有任何线程正在等待这个对象,那么它们中的一个被选择被唤醒。选择是任意的,发生在执行的酌情权。一个线程等待一个对象通过调用wait()方法进行监视。

notify()需要在同步方法或者同步快中调用,即在调用前,线程也必须获得该对象的对象级别锁。

Wait

导致当前线程等待,知道另一个线程调用{@link java.lang.Object#notify()}方法或{@link java.lang.Object#notifyAll()}方法。换句话说,这个方法的行为就像它简单一样执行调用{@code wait(0)}。当前线程必须拥有该对象的监视器。线程释放该监视器的所有权,并等待另一个线程通知等待该对象的监视器的线程唤醒通过调用{@code notify}方法或{@code notifyAll}方法。然后线程等待,直到它可以重新获得监视器的所有权,然后继续执行。

wait()的作用是使当前执行代码的线程进行等待,它是Object类的方法,该方法用来将当前线程置入预执行队列中,并且在wait所在的代码行处停止执行,直到接到通知或被中断为止。在调用wait方法之前,线程必须获得该对象的对象级别锁,即只能在同步方法或同步块中调用wait方法。

Wait和Sleep的区别

它们最大本质的区别是:

  1. sleep()不释放同步锁,wait()释放同步锁,使得其他线程可以使用同步控制代码块或者方法
  2. 还有用法的上的不同是:sleep(milliseconds)可以用时间指定来使他自动醒过来,如果时间不到你只能调用interreput()来强行打断;wait()可以用notify()直接唤起.
  3. 两个方法来自不同的类分别是Thread和Object

1.4 Thread.sleep()和Thread.yield()的异同

  • 相同:sleep()和yield()都会释放CPU

  • 不同:sleep() 使当前线程进入停滞状态,所以执行sleep()的线程是在执行时间内肯定不会执行;yield()只是使当前线程重新回到可执行状态,所以执行yield()的线程有可能进入到可执行状态后马上执行。

    sleep()可使优先级的得到执行的机会,当然也可以让同优先级和优先级高的线程有可执行的机会;yield()只能是同优先级的线程有可执行的机会。

1.5 死锁的概念

死锁:指两个或理论那个个以上的进程(或线程)在执行过程中,因争夺资源而造成的一种互相等待的现象,若无外力作用,他们都将无法推进下去。此时成系统处于死锁状态或系统产生了死锁,这些永远在相互等待的线程成为死锁线程

产生死锁的的四个必要条件(缺一不可)

  1. 互斥条件:顾名思义线程对资源的访问是排他性,当该线程释放资源后,下一个线程才可进行使用

  2. 请求和保持:简单来说就是自己拿的不放手又等待新的资源到手。线程T1持有资源A1不释放,又发起请求想要获得A2,线程T2持有资源A2不释放,又发起请求想要获得A1

  3. 不可剥夺:在没有使用完资源时,其他先行不能进行剥夺

  4. 循环等待:一直等待对方线程释放资源

1.6 并发和并行的区别

并发:是指在某个时间段内,多个任务交替执行任务。当多个线程在操作时,把CPU运行时间或分成若干时间段,在将时间段分配给各个线程执行。在一个时间段的线程代码运行时,其他线程处于挂起状态。

并行:是指同一时刻同时处理多任务的能力。当有多个线程在操作是,CPU同时处理这些线程请求的能力。

区别就在于CPU是否能同时处理所有任务,并发不能,并行能

1.7 线程安全三要素

  • 原子性:Atomic包、CAS算法、Synchronized、Lock
  • 可见性:Synchronized、volatile(不能保证原子性)
  • 有序性:happends-before规则

1.8 如何实行线程安全

  • 互斥同步:Synchronized、Lock
  • 非阻塞同步:CAS
  • 无序同步的方法:如果一个方法本来就不涉及共享数据,那它自然就无线任何同步操作去保证正确性

1.9 保证线程安全的机制

  1. synchronized关键字
  2. lock
  3. CAS、原子变量
  4. ThreadLocal:让每个线程对同一个变量,都有自己独立的副本,每个线程实际访问的对象都是自己的,自然就不存在线程安全的问题
  5. volatile
  6. CopyOnWrite写时复制

多线程

  1. 介绍

随着CPU核心的增多以及互联网迅速发展,单线程的程序处理速度越来越跟不上发展速度和大数据量的增长速度,多线程应运而生,充分利用CPU资源的同时,极大提高了程序处理速度。

  1. 创建多线程的方法

    • 继承Thread类

      public class ThreadCreateTest {
          public static void main(String[] args) {
              new MyThread().start();
          }
      }
      
      class MyThread extends Thread {
          @Override
          public void run() {
              System.out.println(Thread.currentThread().getName() + "\t" + Thread.currentThread().getId());
          }
      }
      
    • 实现Runable接口

      public class RunableCreateTest {
          public static void main(String[] args) {
              MyRunnable runnable = new MyRunnable();
              new Thread(runnable).start();
          }
      }
      
      class MyRunnable implements Runnable {
          @Override
          public void run() {
              System.out.println(Thread.currentThread().getName() + "\t" + Thread.currentThread().getId());
          }
      }
      
    • 通过Callable和Future创建线程

      public class demo02 {
          public static void main(String[] args) throws ExecutionException, InterruptedException {
              multiThread callable = new multiThread();
              FutureTask<Integer> futureTask = new FutureTask<>(callable);
              new Thread(futureTask).start();
      
      
              Integer sum = futureTask.get();
              System.out.println(Thread.currentThread().getName() + Thread.currentThread().getId() + "\t" + sum);
          }
      }
      
      class multiThread implements Callable<Integer> {
      
          @Override
          public Integer call() throws Exception {
              System.out.println(Thread.currentThread().getName() + "\t" + Thread.currentThread().getId() + "\t" + new Date() + "\tstarting...");
      
              int sum = 0;
              for (int i = 0; i <= 100000; i++) {
                  sum += i;
              }
              Thread.sleep(5000);
      
              System.out.println(Thread.currentThread().getName() + "\t" + Thread.currentThread().getId() + "\t" + new Date() + "\tover...");
              return sum;
          }
      }
      

线程池方式创建

实现Runnable接口这种方式更受欢迎,因为这不需要继承Thread类。在应用设计中已经继承了别的对象的情况下,这需要多继承(而java不支持多继承,但可以多实现),只能实现接口。同时,线程池也是非常搞笑的,很容易实现和使用。

2.1 线程池创建线程

线程池:顾名思义就是线程存放的地方。和数据库连接池一样,存在的目的就是为了较少系统开销,主要有一下几个特点。

  • 降低资源消耗。通过重复利用已创建的线程降低线程创建和销毁的造成的消耗(主要)
  • 提高响应速度。当任务到达时,任务可以不需要等到线程创建就能立刻执行
  • 提高线程的客观理性。线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性。

java提供四种线程池创建方式:

  1. newCachedThreadPool创建一个可缓存线程池,如果线程池长度超过处理需要,可能活回收空闲线程,若无可回收,则新建线程
  2. newFixedThreadPool 创建一个定长线程池,课控制线程最大并发数,超出的线程会在队列中等待。
  3. newScheduledThreadPool 创建一个定长线程池,支持定时及周期性任务执行。
  4. newSingleThreadExecutor 创建一个单线程化的线程池,它只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序(FIFO、LIFO,优先级)执行。

通过源码我们得知ThreadPoolExecutor继承自AbstractExecutorService,而AbstractExecutorService实现了ExecutorService

public class ThreadPoolExecutor extends AbstractExecutorService

public abstract class AbstractExecutorService implements ExecutorService

2.2 ThreadPoolExecutor介绍

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个线程在候着等任务。
  • maximumPoolSize: 最大线程数,不管提交多少任务,线程池里最多工作线程数就是maximumPoolSize。
  • keepAliveTime: 线程的存活时间。当线程池里的线程数大于corePoolSize时,如果等了keepAliveTime时长还没有任务可执行,则线程退出。
  • unit: 这个用来指定keepAliveTime的单位,比如秒:TimeUnit.SECONDS。
  • BlockingQueue: 一个阻塞队列,提交的任务将会被放到这个队列里。
  • threadFactory: 线程工厂,用来创建线程,主要是为了给线程起名字,默认工厂的线程名字:pool-1-thread-3。
  • handler: 拒绝策略,当线程池里线程被耗尽,且队列也满了的时候会调用。
2.2.1 BlockingQueue

对于BlockingQueue个人感觉还需要单独拿出来说一下

BlockingQueue:阻塞队列,有先进先出(注重公平性)和先进后出(注重时效性)两种,常见的有两种阻塞队列:ArrayBlockingQueueLinkedBlockingQueue

队列的数据结构大致如图:

图片

队列一端进入,一端输出。而当队列满时,阻塞。

BlockingQueue核心方法:1. 放入数据 put 2. 获取数据take。

常见的两种Queue:

2.2.2 ArrayBlockingQueue

基于数组实现,在ArrayBlockingQueue内部,维护了一个定长数组,以便缓存队列中的数据对象,这是一个常用的阻塞队列,除了定长数组外,ArrayBlockingQueue内部还保留着两个整型变量,分别表示这队列的头部和尾部在数组中的位置。

public class Demo05 {
    private static final int maxSize = 10;

    public static void main(String[] args) {
        ArrayBlockingQueue<Integer> integers = new ArrayBlockingQueue<>(maxSize, true);
        new Thread(new Producer(integers)).start();
        new Thread(new Consumer(integers)).start();

    }
}

// 生产者
class Producer implements Runnable{

    private BlockingQueue<Integer> blockingQueue;

    private int count = 1;

    Producer(BlockingQueue<Integer> blockingQueue) {
        this.blockingQueue = blockingQueue;
    }

    @Override
    public void run() {
        this.product();
    }

    public void product() {
        for(;;){
            try {
                blockingQueue.put(count);
                System.out.println(String.format("生产者正在生产第%d个商品", count));
                count++;
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }

    }


}

// 消费者
class Consumer implements Runnable{

    private BlockingQueue<Integer> queue;

    Consumer(BlockingQueue<Integer> queue) {
        this.queue = queue;
    }

    @Override
    public void run() {
        this.consume();
    }

    /** 消费 */
    private void consume() {
        while (true) {
            try {
                Integer take = queue.take();
                System.out.println(String.format("消费者正在消费第%d个商品", take));
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

        }

    }
}
2.2.3 LinkedBlockingQueue

基于链表的阻塞队列,内部也维护了一个数据缓存队列。需要我们注意的是如果构造一个LinkedBlockingQueue对象,而没有指定其容量大小的,LinkedBlockingQUeue会默认一个类似无限大小的容量(Integer.Max_VALUE),这样的话,如果生产者的速度一旦大于消费者的速度,也许还没有等到队列满阻塞产生,系统内存就有可能被消耗殆尽。

2.2.4 LinkedBlockingQueue和ArrayBlockingQueue的主要区别
  • ArrayBlockingQueue的初始化必须传入队列大小,LinkedBlockingQueue可以不传入
  • ArrayBlockingQueue用一把锁控制并发,LinkedBlockingQueue两把锁控制并发,锁的细粒度更新。前者生产者消费者进度都是一把锁,后者生产者进入是一把锁,消费者消费是另一把锁。
  • ArrayBlockingQueue采用数组的方式存取,LinkedBlockingQueue用Node链表方式存取
2.2.5 handler拒绝策略

java提供了4中丢弃处理的方法,当然你也可以自己实现,主要是实现接口:RejectedExecutionhandler中方法

  • AbortPolicy:不处理,直接抛出异常
  • CallerRunsPolicy:只用调用者所在线程来运行任务,即提交任务的线程。
  • DiscardPolicy:不处理,丢弃掉,不跑出异常
2.2.6 线程池五种状态
private static final int RUNNING = -1 << COUNT_BITS;
private static final int SHUTDOWN = 0 << COUNT_BITS;
private static final int STOP = 1 << COUNT_BITS;
private static final int TIDYING = 2 << COUNT_BITS;
private static final int TERMINATED = 3 << COUNT_BITS;
  • RUNNING:在这个状态的线程池能判断接受新提交的任务,并且也能处理阻塞队列中的任务
  • SHUTDOWN:处于关闭的状态,该线程池不能接受新提交的任务,但是可以处理阻塞队列中已经保存的任务,在线程处于RUNNING状态,调用shutdown()方法能切换为该状态。
  • STOP:线程池处于该状态时既不能接受新的任务也不能处理阻塞队列中的任务,并且能中断现在线程中的任务。当线程处于RUNNING和SHUTDOWN状态,调用shutdownNow()方法就可以使线程变为该状态
  • TIDYING:在SHUTDOWN状态下阻塞队列为空,且线程中的工作线程数量为0就会进入该状态,当在STOP状态下时,只要线程中的工作线程数量为0就会进入该状态。
  • TERMINATED:在TIDYING状态下调用terminated()方法就会进入该状态。可以认为该状态是最终的终止状态

回到线程池创建ThreadPoolExecutor,我们了解了这些参数,再来看看ThreadPoolExecutor的内部工作原理:

图片

  1. 判断核心线程是否已满,是进入队列,否:创建线程
  2. 判断等待队列是否已满,是:查看线程池是否已满,否:进入等待队列
  3. 查看线程池是否已满,是:拒绝,否创建线程

2.3 深入理解ThreadPoolExecutor

进入execute方法可以看到:

public void execute(Runnable command) {
        if (command == null)
            throw new NullPointerException();
        int c = ctl.get();
      
        if (workerCountOf(c) < corePoolSize) {
            if (addWorker(command, true))
                return;
            c = ctl.get();
        }
      
        if (isRunning(c) && workQueue.offer(command)) {
            int recheck = ctl.get();
            if (! isRunning(recheck) && remove(command))
                reject(command);
            else if (workerCountOf(recheck) == 0)
                addWorker(null, false);
        } else if (!addWorker(command, false))
            reject(command);
    }

addWorker方法:

  • 创建Worker对象,同时也会实例化一个Thread对象。在创建Worker时会调用threadFactory来创建一个线程。
  • 启动启动这个线程
2.3.1 线程池中ctl属性的作用是什么?

ctl属性包含两个概念:

private final AtomicInteger ctl = new AtomicInteger(ctlOf(RUNNING, 0));
  
private static int ctlOf(int rs, int wc) { 
    return rs | wc; 
}
  1. runState:即rs 表明当前线程池的状态,是否处于Running,Shutdown,Stop,Tidying,
  2. workerCount:即wc 表明当前有效的线程数

我们点击workerCount即工作状态记录值,以RUNNING为例,RUNNING = -1 << COUNT_BITS;,即-1无符号左移COUNT_BITS位,进一步我们得知COUNT_BITS位29,因为Integer位数为31位(2的五次方减一

2.3.2 shutdownNow和Shutdown的区别

shutdown会把线程池的状态改为SHUTDOWN,而shutdownNow把当前线程池状态改为STOP

shutdown只会中断所有空闲的线程,而shutdownNow会中断所有的线程。

shutdown返回方法为空,会将当前任务队列中的所有任务执行完毕;而shutdownNow把任务队列中的所有任务都取出来返回。

2.3.3 线程复用原理
final void runWorker(Worker w) {
        Thread wt = Thread.currentThread();
        Runnable task = w.firstTask;
        w.firstTask = null;
        w.unlock();
        boolean completedAbruptly = true;
        try {
            while (task != null || (task = getTask()) != null) {
                w.lock();
                
                if ((runStateAtLeast(ctl.get(), STOP) ||
                     (Thread.interrupted() &&
                      runStateAtLeast(ctl.get(), STOP))) &&
                    !wt.isInterrupted())
                    wt.interrupt();
                try {
                    beforeExecute(wt, task);
                    Throwable thrown = null;
                    try {
                        task.run();
                    } catch (RuntimeException x) {
                        thrown = x; throw x;
                    } catch (Error x) {
                        thrown = x; throw x;
                    } catch (Throwable x) {
                        thrown = x; throw new Error(x);
                    } finally {
                        afterExecute(task, thrown);
                    }
                } finally {
                    task = null;
                    w.completedTasks++;
                    w.unlock();
                }
            }
            completedAbruptly = false;
        } finally {
            processWorkerExit(w, completedAbruptly);
        }
    }

就是任务在并不只执行创建时指定的firstTask第一任务,还会从任务队列的中自己主动取任务执行,而且是有或者无时间限定的阻塞等待,以保证线程的存活。
默认的是不允许

2.4 CountDownLatch和CyclicBarrier区别

  1. CountDownLatch是一个计数器,线程完成一个记录一个,计数器递减,只能用一次
  2. CycliBarrier的计数器更想一个阀门,需要所有线程都到达,然后继续执行,计数器递增,提供reset功能,可以多次使用

3. 多线程间通信的几种方式

提及多线程又不得不提及多线程通信的机制。首先,要多线程间通信的模型有两种:共享内存消息传递,以下方式都是基本这两种模型来实现的。我们来基本一道面试常见的题目来分析:

题目:有两个线程A、B,A线程向一个集合里面依次添加元素abc字符串,一共添加十次,当添加到第五次的时候,希望B线程能够收到A线程的通知,然后B线程执行相关的业务操作

3.1 使用 volatile 关键字

public class Demo07 {
    public static volatile boolean flag = false;
    public static void main(String[] args) {
        notifyThreadWithVolatile();
    }

    public static void notifyThreadWithVolatile() {
        Thread threadA = new Thread(new Runnable() {
            @Override
            public void run() {
                for (int i = 0; i < 10; i++) {
                    if (i == 4) {
                        flag = true;
                    }
                    System.out.println(String.format("当前是第%d个", i));
                }
            }
        }, "线程A");

        Thread threadB = new Thread("线程B") {
            @Override
            public void run() {
                while (true) {
                    while (flag) {
                        System.out.println(Thread.currentThread().getName());
                        try {
                            System.out.println("do something...." + flag);
                            Thread.sleep(4000);
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    }
                }
            }
        };
        threadB.start();
        threadA.start();

    }
}

posted @ 2022-05-03 17:06  Randlly  阅读(83)  评论(0)    收藏  举报