暮爱深秋 的笔记

人如果没有梦想,那跟咸鱼有什么区别呢?

2020-12-24 09:54

Java多线程基础

暮爱深秋

JavaEE

(1742)

(0)

收藏

blog

多线程基础

一、线程、进程、纤程

  • 进程:OS分配资源的基本单位。OS会为每一个进程独立分配一部分资源,每个进程的资源都有独立的内存空间。每个进程对应一个端口号。

  • 线程:CPU调度的基本单位。共享进程的内存空间,线程没有独立的内存空间。

  • 纤程:相当于用户空间的线程。纤程存在与用户态,不与内核打交道,所以速度会更快,且占用资源比较少。

二、创建线程的方式

  1. Thread:继承Thread类并重写run方法。

    class MyThread01 extends Thread {    @Override
        public void run() {        for (int i = 0; i < 10; i++) {            try {                TimeUnit.SECONDS.sleep(1);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }            System.out.println(Thread.currentThread().getName() + "--" + i);
            }
        }    
        public static void main(String[] args) {        // 通过Thread.start方法启动线程
            new MyThread01().start();
        }
    }
  2. Runnable:实现Runnable接口,重写run方法

    class MyThread02 implements Runnable {    @Override
        public void run() {        for (int i = 0; i < 10; i++) {            try {                TimeUnit.SECONDS.sleep(1);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }            System.out.println(Thread.currentThread().getName() + "--" + i);
            }
        }    
        public static void main(String[] args) {    	// 通过Thread.start方法启动线程
        	new Thread(new MyThread02()).start();
        }
    }
  3. 线程池:

    • corePoolSize:核心池的大小。默认情况下,在创建线程池后,线程池的线程数为0,当有新任务时,就会创建一个线程去执行任务,当线程池中的线程数量达到corePoolSize后,就会把到达的任务放到缓存队列当中。

    • maximumPoolSize:线程池的最大线程数,表示线程池中最多能创建多少个线程。

    • keepAliveTime:表示线程没有任务执行时最多存活时间。默认情况下,只有当线程数大于maximumPoolSize时,keepAliveTime才会起作用。

    • unit:参数keepAliveTime的时间单位。

    • workQueue:一个阻塞队列,用于存储等待执行的任务。

    • threadFactory:线程工厂,用来创建线程。

    • handler:表示当拒绝任务时的策略,有四种取值:

    • ThreadPoolExecutor.AbortPolicy:丢弃任务并抛出RejectedExecutionException异常。

    • ThreadPoolExecutor.DiscardPolicy:也是丢弃任务,但是不抛出异常。

    • ThreadPoolExecutor.DiscardOldestPolicy:丢弃队列最前面的任务,然后重新尝试执行任务(重复此过程)

    • ThreadPoolExecutor.CallerRunsPolicy:由调用线程处理该任务

    • 使用线程池的几个优点:

    • ThreadPoolExecutor创建线程池:

      ThreadPoolExecutor提供了四个构造器

      public class ThreadPoolExecutor extends AbstractExecutorService{	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.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;
          }
      }

      参数:

      示例:

      class MyThread03{
          public static void main(String[] args) {
              ThreadPoolExecutor executor = new ThreadPoolExecutor(
                      2, // 核心线程数
                      4, // 最大线程数
                      5000, TimeUnit.MILLISECONDS,  // 线程没有执行任务时最多存活时间
                      new LinkedBlockingDeque<>()
      
              );
      
              for (int i = 0; i < 10; i++) {
                  executor.execute(new ThreadTask());
              }
          }
      
          private static class ThreadTask implements Runnable {
      
              @Override
              public void run() {
                  System.out.println(Thread.currentThread().getName());
                  try {
                      TimeUnit.MILLISECONDS.sleep(1000);
                  } catch (InterruptedException e) {
                      e.printStackTrace();
                  }
              }
          }
      }
    • Java通过Executors提供四种线程池(不推荐,缓存队列LinkedBlockingQueue没有设置固定容量大小, 并发量多的时候容易发生OOM),分别是:

    1. newCachedThreadPools创建一个可缓存线程池,如果线程池长度超过处理需要,可以灵活回收空闲线程,若无可回收则创建线程。

    2. newFixedThreadPools创建一个定长线程池,可控制最大线程数,超出的线程在队列中等待。

    3. newScheduledThreadPools创建一个定长线程池,支持定时及周期性任务执行。

    4. newSingleThreadExecutor创建一个单线程化的线程池,它只会用唯一的工作线程来执行任务,保证所有的任务按照指定顺序(FIFO,LIFO,优先级)执行。

    5. 降低资源消耗:通过重用已经创建好的线程来减少创建线程时的消耗。

    6. 提高响应速度:任务到达时不需要等待线程创建就可以立即执行。

    7. 提高线程的客观理性:线程可以统一管理、分配、调优和监控。

    三、sleep()yield()join()

    • Thread.sleep(long millis):用来暂停当前线程的执行,会通知线程调度器把当前线程在指定的时间周期内置为wait状态。当wait时间结束,线程重新变为Runnable并等待CPU的再次调度,所以sleep的实际时间取决于线程调度器,这是由操作系统来完成的。

    • Thread.join():很多情况下,主线程生成并启动了子线程,如果子线程需要大量的耗时的运算,主线程往往将于子线程之前结束,但如果主线程处理完其他的事物后,需要用到子线程的处理结果,也就是主线程等待子线程完成之后再结束,这个时候就用到join()方法了。

    • Thread.yield():暂停当前线程, 以便其他线程有机会执行。

    • 示例:

      public class ThreadDemo02 {    public static void main(String[] args) {        Thread thread = new Thread(new MyThread(), "MyThread");
              thread.start();        for (int i = 0; i < 10; i++) {            System.out.println("mian--" + i);            try {                Thread.sleep(500);                if(i == 2) {                    // 暂停该线程
                          Thread.yield();
                      }                if(i == 5) {                    // 等待thread线程执行完成再执行
                          thread.join();
                      }
                  } catch (InterruptedException e) {
                      e.printStackTrace();
                  }
              }
          }    private static class MyThread implements Runnable {        @Override
              public void run() {            for (int i = 0; i < 10; i++) {                try {                    Thread.sleep(500);
                      } catch (InterruptedException e) {
                          e.printStackTrace();
                      }                System.out.println(Thread.currentThread().getName() + "--" + i);
                  }
              }
          }
      }

    四、线程的状态

                        +-------------->TIMED_WAITED---------------------+
                        |Thread.sleep(Long)                              | Object.notify()    
                        |Object.wait(Long)                               | Object.notifyAll()
                        |Thread.join(Long)                               | LockSupport.unpark(Thread)
                        |LockSupport.parkNanos()                         |
                        |LockSupport.parkUntil()                         |
                        |                 (RUNNABLE)                     v
                       +-----------------------------------------------------+
                       |                                                     |
                       |  +-------+     system scheduling       +---------+  |           +-----------+
    +-----+            |  |       |  -------------------------> |         |  |           |           |
    | NEW |--start()-->|  | READY |                             | RUNNING |  |---------->| TEMINATED |  
    +-----+            |  |       | <----Thread.yield---------- |         |  |           |           |
                       |  +-------+                             + --------+  |           +-----------|
                       +-----------------------------------------------------+
                        |   |Ojbect.wait()               Ojbect.notify()  ^ ^
                        |   |Thread.join()             Ojbect.notifyAll() | |获取到锁
                        |   |LocKSupport.park() LocKSupport.unpark(Thread)| |
                        |   +-------------------->WAIT--------------------+ |
                        +----------------------->BLOCKED--------------------+
                         等待进入Synchronized方法(块)
    • NEW(初始):通过实现Runnable或继承Thread得到一个线程类,new一个实例出来,线程就进入了初始状态。

    • READY(就绪):线程进入就绪状态说明该线程有了运行资格,可以通过系统的调度进入运行状态。

      1. 调用线程的start()方法,该线程就进入了就绪状态。

      2. 线程的sleep()方法结束,或者其他方法join()结束,等待用户输入结束,或者某个线程拿到锁,线程进入就绪状态。

      3. 调用当前线程yield()方法,当前线程进入就绪状态。

      4. 锁池中的对象拿到对象锁之后,进入就绪状态。

    • RUNNING(运行):线程调度程序从可运行池中选择一个线程作为当前线程所处的状态。这也是线程进入运行状态的唯一一种方式。

    • BOLCKED(阻塞):组赛状态是线程阻塞进入synchronized关键字修饰的方法或代码块(获取锁)时的状态。

    • WAIT(等待):这种状态的线程不会被分配CPU执行时间,它们要显式的被唤醒,否则会进入无期限等待的状态。

    • TIMED_WAITING(超时等待):这种状态的线程不会被分配CPU执行时间 ,不过它们无须被唤醒,在达到一定时间它们会被自动唤醒。

    • TEMINATED(终止):线程的run方法或者主线程的main方法执行完成之后,这个线程就终止了。终止的线程不能复生。如果强行调用start()方法执行线程,会抛出 java.lang.IllegalThreadStateException异常。

    五、synchronized关键字

    既可以保证线程的原子性,又可以保证线程的可见性

    先看下面这个程序:

    package com.kangfawei.item01;import java.util.concurrent.TimeUnit;public class ThreadDemo03 {    public static void main(String[] args) {         UnSyncDemo unSyncDemo = new UnSyncDemo();        for (int i = 0; i < 10; i++) {            Thread thread = new Thread(unSyncDemo);
                thread.start();
            }        try { 			TimeUnit.MILLISECONDS.sleep(500);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }        System.out.println(unSyncDemo.count);
        }    private static class UnSyncDemo implements Runnable {        int count = 0;        @Override
            public void run() {            for (int i = 0; i < 1000; i++) {
                    count++;
                }
            }
        }
    }

    上面程序中开启了10个线程,每个线程都累加了1000次,结果应该时10*1000=10000,但是多次执行下来发现结果都是比10000小,有些甚至少很多。原因就是每个线程都有不同的栈空间,一个线程将count中的数据拿到之间的栈空间执行但还没有执行完成的时候,另外一个线程拿到同样结果的count在自己的空间中执行,这样就会导致之前的情况发生。

    设想如果多个线程依次执行的时候就不会产生上面的那种情况。java中的synchronized关键字就是让每个线程依次执行,但是同时,synchronized关键字由于每个线程依次执行的原因会很明显的降低程序的效率。

    package com.kangfawei.item01;import java.util.concurrent.TimeUnit;public class ThreadDemo03 {    public static void main(String[] args) {        SyncDemo syncDemo = new SyncDemo();        for (int i = 0; i < 10; i++) {            Thread thread = new Thread(syncDemo);
                thread.start();
            }        try { 			TimeUnit.MILLISECONDS.sleep(500);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }        System.out.println(syncDemo.count);
        }    private static class SyncDemo implements Runnable {        int count = 0;        @Override
            public void run() {            synchronized (this) { // 增加的代码块加锁
                    for (int i = 0; i < 1000; i++) {
                        count++;
                    }
                }
            }
        }
    }

    此时不管执行多少次,执行结果都会是10000,因为前面执行的线程会占用对象锁,等待执行完成之后才会释放锁,其他线程抢到锁的时候继续占有锁,剩下的线程继续等待,直到所有线程执行完毕。这样就不会产生多个线程拿到同样的count执行。

    • 锁的对象:

      • 实例方法中:public synchronized void mothed()实例方法中锁的对象是该类的实例对象

      • 静态方法中:public static synchronized void methor()静态方法中所得对象是类对象

      • 实例对象:synchronized(this) {} 同步代码块,锁住的是该类的实例对象

      • class对象:synchronized(SyncDemo.class) {}同步代码块,锁住的是该类的对象

      • 任意Object:synchronized(obj) {}任意对象可以作为同步代码块的锁(谨慎使用String、Integer等包装类)

    • 可重入

      同一个锁程中,线程可以多次获取同一把锁。每个对象拥有一个计数器,当线程获取该对象锁后,计数器就会加一,释放锁后就会将计数器减一,当计数器为零的时候释放锁。

    • 锁升级

      synchronized锁可以升级,无锁-->偏向锁-->自旋锁-->OS锁

      没有线程访问同步方法/同步代码块的时候,是没有锁的。当只有一个线程访问的时候,由无锁升级为偏向锁,当锁发现访问的还是同一个线程的时候,默认放行。继续增加线程的时候,锁会升级为自旋锁,线程会循环获取锁,如果线程多次循环还是无法获取到锁,或者等待线程过多,那么锁就会升级为最终的OS锁,线程会进入OS的就绪队列中,直到锁被释放后去抢到锁。

      自旋锁:效率高,但是自旋过程会占用CPU资源,所以线程数比较小、时间比较短的情况下适合使用。

      重量级锁:不占用CPU资源,但是效率比较低

    六、volatile关键字

    • 保证线程可见性

      public class ThreadDemo04 {    public static void main(String[] args) {        VolatileTest test = new VolatileTest();        Thread thread = new Thread(test, "T1");
              thread.start();        try {            TimeUnit.SECONDS.sleep(2);
              } catch (InterruptedException e) {
                  e.printStackTrace();
              }		// 2s过后更改flag的值,让线程停止运行
              test.flag = false;
          }
      }class VolatileTest implements Runnable{	// 此处volatile必须要加,如果不加会导致更改flag变量此处接收不到
          volatile boolean flag = true;    @Override
          public void run() {        while(flag) {            // 通过更改变量flag让线程停止
                  // 如果不加volatile线程会一直向下运行
              }
          }
      }

      由于上面两条规则,所以volatile可以保证每次看到的都会是最新值,从而保证可见性。但是volatile不保证线程的原子性,所以不可以保证线程的安全性。

      • volatile关键字每次读取变量前必须先从主内存刷新最新的值。

      • volatile关键字写入后必须同步会主内存中

    • 禁止指令重排序

      在一段代码的运行过程中,对于一些没有依赖关系的代码,编译器和处理器为了提高效率,可能会产生一些顺序的改变,比如:

      int a = 1;  // 1int b = 2;  // 2int c = a + b;  // 3

      在上面的代码中,代码1跟2没有依赖关系,所以处理器执行过程中可能会先执行2,然后再执行1。在单线程中,这种指令重排序不会产生任何问题,但是再多线程的情况下,可能就会有一些问题出现。如在下面的懒加载单例模式中:

      class Singleton {    private static  Singleton instance;    private Singleton() {}    public static Singleton getInstance() {        if(instance == null) {            synchronized (Singleton.class) {                if(instance == null) {
                          instance = new Singleton();
                      }
                  }
              }        return instance;
          }
      }

      在懒加载的单例模式中,上面的例子几乎可以说是完美的,一般情况下不会由问题产生,但这个问题并不是百分百完美的,在一些并发量极大的情况下,会发生instance不为null,但是变量没有被初始化。

      instance == new Singleton()这个赋值语句并不是一个原子操作,它可以分为三部分执行:

      当发生指令重排序的时候,第二步跟第三步调换位置,instance指向分配好的内存空间的时候,这个内存空间没有被初始化。如果此时另外一个线程进入,就会直接返回没有被初始化内存空间的对象。解决这个问题就需要将声明加上volatile

      private static volatile Singleton instance;
      1. 分配内存空间

      2. 初始化对象

      3. 设置instance执行刚分配的内存地址


    0条评论

    点击登录参与评论