Java 多线程详解

一、线程与进程

在讲多线程之前,我们得先分清楚线程与进程的区别,当然这也是面试中常遇到的问题。

进程是资源(CPU、内存等)分配的基本单位,它是程序执行时的实例。程序运行时系统就会创建一个进程,并为它分配资源,然后把该进程放入进程就绪队列,进程调度器选中它的时候就会为它分配 CPU 时间,程序开始真正运行。

线程是程序执行时的最小单位,它是进程的一个执行流,是 CPU 调度和分派的基本单位,进程由线程组成,线程间共享进程的所有资源,每个线程有自己的堆栈和局部变量。

线程与进程区别:

  • 进程是资源分配的最小单位,线程是程序执行的最小单位。

  • 进程有自己的独立地址空间;线程是共享进程中的数据的,使用相同的地址空间。因此 CPU 切换线程的花费比进程小很多,同时创建线程的开销也比进程小很多。

  • 线程之间的通信更方便,同一进程下的线程共享全局变量、静态变量等数据,而进程之间的通信需要以通信的方式(IPC)进行。

  • 多进程程序更健壮,一个进程死掉并不会对另外一个进程造成影响,因为进程有自己独立的地址空间。而多线程程序只要有一个线程死掉,整个进程也死掉了。

二、线程的状态

线程不是一丝不变的,它是有状态,一个线程从出生到死亡,可能历经多种状态。

  • 新建(New):创建后尚未启动,如:Thread thread = new Thread()

  • 可运行(Runnable):可能正在运行,也可能正在等待 CPU 时间片。包含了操作系统线程状态中的 Running 和 Ready。如:thread.start()

  • 阻塞(Blocked):等待获取一个排它锁,如果其线程释放了锁就会结束此状态。

  • 无限期等待(Waiting):等待其它线程显式地唤醒,否则不会被分配 CPU 时间片。如调用 thread.wait()thread.join() 方法能让线程进入无限期等待状态。

阻塞和等待的区别在于,阻塞是被动的,它是在等待获取一个排它锁。而等待是主动的。

  • 限期等待(Timed Waiting):无需等待其它线程显式地唤醒,在一定时间之后会被系统自动唤醒。如 Thread.sleep(10)

sleep()wait() 的区别:
1、sleep() 是 Thread 的方法,wait() 是 Object 的方法;
2、sleep() 可以在任意地方调用,wait() 必须在同步方法或者同步代码块中调用;
3、sleep() 只会让出 CPU,不会导致锁的改变;
4、wait() 不仅会让出 CPU,还会释放锁。

  • 死亡(Terminated):可以是线程结束任务之后自己结束,或者产生了异常而结束。死亡的线程不能再次调用 start() 方法。
线程状态转换

三、线程的创建

上面说了线程的基本概念,下面我们来具体实操一下,首先得创建一条线程,有三种使用线程的方法:

  1. 继承 Thread 类。
  2. 实现 Runnable 接口;
  3. 实现 Callable 接口;

1、继承 Thread 类

需要实现 run() 方法,因为 Thread 类也实现了 Runable 接口。

当调用 start() 方法启动一个线程时,虚拟机会将该线程放入就绪队列中等待被调度,当一个线程被调度时会执行该线程的 run() 方法。

public static void main(String[] args) {
      Thread thread = new Thread(){
          @Override
          public void run() {
              System.out.println("do something");
          }
      };
      thread.start();
}

2、实现 Runnable 接口

实现 Runnable 接口中的 run() 方法,通过 Thread 调用 start() 方法来启动线程。

public class MyRunnable implements Runnable {
      public void run() {
          System.out.println("do something");
      }
}
public static void main(String[] args) {
      MyRunnable instance = new MyRunnable();
      Thread thread = new Thread(instance);
      thread.start();
}

3、实现 Callable 接口

与 Runnable 相比,Callable 可以有返回值,返回值通过 FutureTask 进行封装。

public class MyCallable implements Callable<Integer> {
      public Integer call() {
          return 123;
      }
}
public static void main(String[] args) throws ExecutionException, InterruptedException {
      MyCallable mc = new MyCallable();
      FutureTask<Integer> ft = new FutureTask<>(mc);
      Thread thread = new Thread(ft);
      thread.start();
      System.out.println(ft.get()); // 获取返回值
}

四、线程的使用

现在我们已经创建好线程了,该用线程干点活儿了。

假设我们有两条线程,每条线程的任务是给服务器上的金额数据 +1,模拟两个人同时存钱,每条线程各加 100 次,那么结果应该是多少呢?200 吗?

static int count = 0;

public static void main(String[] args) {

    new Thread(new Runnable() { // 创建线程
        public void run() {
            for (int i = 0; i < 100; i++) {
                Thread.sleep(30); // 模拟网络延时,省略了 try catch
                count++; // 金额 +1
            }
        }
    }).start();

    new Thread(new Runnable() { // 创建线程
        public void run() {
            for (int i = 0; i < 100; i++) {
                Thread.sleep(10); // 模拟网络延时,省略了 try catch
                count++; // 金额 +1
            }
        }
    }).start();

    Thread.sleep(4000);
    System.out.println(count);  // 197
}

注意,上述为了让代码看起来简洁,省去了异常处理。

从运行结果来看,在没有加锁的情况下,结果会比预期值 200 小,并且循环次数越多越明显。这是由于自增操作 count++ 不是原子性的,若要解决这个问题,可以使用加锁的方法,关于加锁具体可看《Java 锁机制》这篇文章。

五、线程池

多线程的异步执行方式,虽然能够最大限度发挥多核计算机的计算能力,但是如果不加控制,反而会对系统造成负担。线程本身也要占用内存空间,大量的线程会占用内存资源并且可能会导致 Out of Memory。即便没有这样的情况,大量的线程回收也会给 GC 带来很大的压力。

为了避免重复的创建线程,线程池可以让线程进行复用。通俗点讲,当有工作来,就会向线程池拿一个线程,当工作完成后,并不是直接关闭线程,而是将这个线程归还给线程池供其他任务使用。

先看一下 ThreadPoolExecutor,它是线程池中最核心的类,这里着重说一下这个类的各个构造参数:

public ThreadPoolExecutor(int corePoolSize,
                          int maximumPoolSize,
                          long keepAliveTime,
                          TimeUnit unit,
                          BlockingQueue<Runnable> workQueue,
                          ThreadFactory threadFactory,
                          RejectedExecutionHandler handler)
  • corePoolSize
    核心线程数,线程池创建之后,线程数为 0,当有任务时,会创建一个线程去执行,直到线程数达到 corePoolSize 之后,就会被到达的任务放在队列中。换句更精炼的话:corePoolSize 表示允许线程池中允许同时运行的最大线程数。

  • maximumPoolSize
    线程池允许的最大线程数,表示最大能创建多少个线程。maximumPoolSize >= corePoolSize。

  • keepAliveTime
    表示线程没有任务时最多保持 keepAliveTime 后停止。默认情况下,只有线程池中线程数大于 corePoolSize 时,keepAliveTime 才会起作用。当线程池中的线程数大于 corePoolSize,并且一个线程空闲时间达到了 keepAliveTime,那么该线程就会 shutdown。

  • Unit
    keepAliveTime 的单位,通常是 TimeUnit.SECONDS

  • workQueue
    一个阻塞队列,用来存储等待执行的任务,当线程池中的线程数超过 corePoolSize 的时候,线程会进入阻塞队列进行阻塞等待。总之,来不及处理的任务的队列,是一个 BlockingQueue。

  • threadFactory
    线程工厂,用来生产一组相同任务的线程。

  • handler
    表示拒绝处理任务时的策略。如果当前线程池中的线程数目超过 maximumPoolSize,则会采取任务拒绝策略进行处理。
    策略默认 AbortPolicy,表无法处理新任务并抛出 RejectedExecutionException 异常。
    另外还有 CallerRunsPolicy:该策略直接在调用者线程中,运行当前被丢弃的任务。但线程的性能极有可能会急剧下降。
    DiscardOldestPolicy:丢弃队列中最老的一个请求,即将被执行的一个任务,并尝试再次提交当前任务。
    DiscardPolicy:丢弃任务,不做任何处理。

线程池任务处理策略

ThreadPoolExecutor 的使用:

public class PollTest {

    private static final int CPU_COUNT = Runtime.getRuntime().availableProcessors();
    private static final int CORE_POOL_SIZE = CPU_COUNT + 1;
    private static final int MAXIMUM_POOL_SIZE = CPU_COUNT * 2;
    private static final BlockingQueue<Runnable> workQueue = new LinkedBlockingQueue<>(20);

    public static void main(String[] args) {

        System.out.println("核心线程数=" + CORE_POOL_SIZE);
        System.out.println("最大线程数=" + MAXIMUM_POOL_SIZE);

        ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(CORE_POOL_SIZE, MAXIMUM_POOL_SIZE, 10, TimeUnit.SECONDS, workQueue, r -> {
            Thread thread = new Thread(r);
            thread.setDaemon(false);
            return thread;
        });

        for (int i = 0; i < 20; i++) {
            Runnable runnable = () -> {
                System.out.println("doing..." + Thread.currentThread().getName());
                Thread.sleep(800);
                System.out.println("执行完毕" + Thread.currentThread().getName());
            };
            threadPoolExecutor.execute(runnable);
        }
    }
}

corePoolSize 的值应该如何设置?这个应该是最重要的参数了,所以如何合理的设置它十分重要。因此我们要清楚的知道执行的任务是 CPU密集型还是 IO密集型

CPU 密集型:也叫计算密集型,特点是要进行大量的计算,消耗 CPU资源,比如计算圆周率、对视频进行高清解码等等,全靠 CPU 的运算能力。任务越多,任务切换花费的时间就越多,CPU 执行任务的效率就越低,所以,要最高效地利用 CPU,计算密集型任务同时进行的数量应当等于 CPU 的核心数。

IO 密集型:涉及网络、磁盘 IO 的任务都是 IO 密集型任务,这类任务的特点是 CPU 消耗少,任务的大部分时间都在等待 IO 操作完成(因为 IO 的速度远远低于 CPU 和内存的速度)。对于 IO 密集型任务,任务越多,CPU 效率越高,但也有一个限度。

所以,建议
CPU密集型:corePoolSize = CPU核数 + 1
IO密集型:corePoolSize = CPU核数 * 2
当然这不是绝对的,可以根据实际情况进行调整。

五、几种常见的线程池

1、newFixedThreadPool (固定数量线程池)

public static ExecutorService newFixedThreadPool(int nThreads) {
        return new ThreadPoolExecutor(nThreads, nThreads,
                                      0L, TimeUnit.MILLISECONDS,
                                      new LinkedBlockingQueue<Runnable>());
}

corePoolSize = maximumPoolSize = 初始化的参数

workQueue:使用无界队列 LinkedBlockingQueue 链表阻塞队列。

keepAliveTime = 0,由于使用无界队列 LinkedBlockingQueue 作为缓存队列,所以当 corePoolSize 满后,后面添加的线程任务都会添加到 LinkedBlockingQueue 中去,所以 maximumPoolSize 就失去了意义,这样也就没有必要设置空闲时间。

public static void main(String[] args) {
        ExecutorService executorService = Executors.newFixedThreadPool(5);
        for (int i = 0; i < 10; i++) {
            executorService.execute(new Runnable() {
                @Override
                public void run() {
                    System.out.println(Thread.currentThread());
                }
            });
        }
        executorService.shutdown();
}

2、newSingleThreadExecutor (单例线程池)

public static ExecutorService newSingleThreadExecutor() {
        return new FinalizableDelegatedExecutorService
            (new ThreadPoolExecutor(1, 1,
                                    0L, TimeUnit.MILLISECONDS,
                                    new LinkedBlockingQueue<Runnable>()));
}

corePoolSize = maximumPoolSize =1 由于是单例线程池,所以线程池中是有一个重用的线程

workQueue:使用无界队列LinkedBlockingQueue链表阻塞队列

3、newCachedThreadPool (缓存线程池)

public static ExecutorService newCachedThreadPool() {
        return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
                                      60L, TimeUnit.SECONDS,
                                      new SynchronousQueue<Runnable>());
}

corePoolSize = 0 表示线程池中没有核心线程,都是非核心线程

maximumPoolSize = Integer.最大值

keepAliveTime:60秒,线程池中创建的线程都是非核心线程,所以设置空闲时间 60 秒,当非核心线程60秒后没有被重用,将会被销毁,如果没有线程提交给该线程池,超过空闲时间,该线程池就没有非空闲线程,那么该线程池也就不会消耗过多的资源。

workQueue = SynchronousQueue 是一个不存储元素的阻塞队列。每一个put操作必须等待一个take操作,否则不能继续添加元素。

4、newScheduledThreadPool (定时线程池)

public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize) {
        return new ScheduledThreadPoolExecutor(corePoolSize);
}
public ScheduledThreadPoolExecutor(int corePoolSize) {
        super(corePoolSize, Integer.MAX_VALUE, 0, NANOSECONDS,
              new DelayedWorkQueue());
}

定时线程池,该线程池可用于周期性地去执行任务,通常用于周期性的同步数据。

scheduleAtFixedRate:是以固定的频率去执行任务,周期是指每次执行任务成功执行之间的间隔。

schedultWithFixedDelay:是以固定的延时去执行任务,延时是指上一次执行成功之后和下一次开始执行的之前的时间。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 220,458评论 6 513
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 94,030评论 3 396
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 166,879评论 0 358
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 59,278评论 1 295
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 68,296评论 6 397
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 52,019评论 1 308
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,633评论 3 420
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,541评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 46,068评论 1 319
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 38,181评论 3 340
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 40,318评论 1 352
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,991评论 5 347
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,670评论 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 32,183评论 0 23
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,302评论 1 272
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,655评论 3 375
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 45,327评论 2 358