Java 多线程

前言

  • 进程

    • 进程是指在系统中正在运行的一个应用程序。每个进程之间是独立的,每个进程均运行在其专用且受保护的内存空间内。
    • 比如同时打开 QQ、IDEA,系统就会分别启动两个进程。通过 “活动监视器” 可以查看 Mac 系统中所开启的进程。
    • 一个程序的一次运行,在执行过程中拥有独立的内存单元,而多个线程共享一块内存。
  • 线程

    • 线程是进程中执行任务的基本执行单元。一个进程要执行任务,必须得有线程,一个进程(程序)的所有任务都在线程中执行。
    • 每一个进程至少有一条线程,即主线程。一个进程可以开启多条线程,每条线程可以并发(同时)执行不同的任务。
    • 比如使用酷狗播放音乐、使用迅雷下载电影,都需要在线程中执行。
    • 在程序中每一个方法的执行,都是从上向下串行执行的。除非使用 block,否则在一个方法中,所有代码的执行都在同一个线程上。
  • 进程与线程的联系

    • 线程是进程的基本组成单位。
  • 进程与线程的区别

    • 调度:线程作为调度和分配的基本单位,进程作为拥有资源的基本单位。
    • 并发性:不仅进程之间可以并发执行,同一个进程的多个线程之间也可并发执行。
    • 拥有资源:进程是拥有资源的一个独立单位,线程不拥有系统资源,但可以访问隶属于进程的资源。
    • 系统开销:在创建或撤消进程时,由于系统都要为之分配和回收资源,导致系统的开销明显大于创建或撤消线程时的开销。
  • Java 线程教程

  • 多线程 快速入门

1、多线程

  • 多线程即在同一时间,可以做多件事情。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    public class Main {

    public static void main(String[] args) {

    // Create two Thread objects
    Thread t1 = new Thread(Main::print);
    Thread t2 = new Thread(Main::print);

    // Start both threads
    t1.start();
    t2.start();
    }

    public static void print() {
    for (int i = 1; i <= 500; i++) {
    System.out.println(i);
    }
    }
    }
  • 启动线程是 start() 方法,run() 并不能启动一个新的线程。

2、创建多线程

  • 创建多线程有 3 种方式

    • 继承 Thread 线程类
    • 实现 Runnable 接口
    • 匿名类
  • 继承线程类

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    public class KillThread extends Thread {                      // 继承线程类
    private Hero h1;
    private Hero h2;

    public KillThread(Hero h1, Hero h2) {
    this.h1 = h1;
    this.h2 = h2;
    }

    public void run() { // 线程启动的时候,会去执行 run() 方法
    while(!h2.isDead()){
    h1.attackHero(h2);
    }
    }
    }
    1
    2
    3
    4
    5
    KillThread killThread1 = new KillThread(gareen, teemo);
    killThread1.start(); // 启动线程

    KillThread killThread2 = new KillThread(bh, leesin);
    killThread2.start(); // 启动线程
  • 实现 Runnable 接口

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    public class Battle implements Runnable {                     // 实现 Runnable 接口
    private Hero h1;
    private Hero h2;

    public Battle(Hero h1, Hero h2) {
    this.h1 = h1;
    this.h2 = h2;
    }

    public void run() { // 线程启动的时候,会去执行 run() 方法
    while(!h2.isDead()){
    h1.attackHero(h2);
    }
    }
    }
    1
    2
    3
    4
    5
    Battle battle1 = new Battle(gareen, teemo);
    new Thread(battle1).start(); // 启动线程

    Battle battle2 = new Battle(bh, leesin);
    new Thread(battle2).start(); // 启动线程
  • 匿名类

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    Thread t1 = new Thread() {
    public void run() { // 线程启动的时候,会去执行 run() 方法
    while(!teemo.isDead()){
    gareen.attackHero(teemo);
    }
    }
    };
    t1.start(); // 启动线程

    Thread t2 = new Thread() {
    public void run() { // 线程启动的时候,会去执行 run() 方法
    while(!leesin.isDead()){
    bh.attackHero(leesin);
    }
    }
    };
    t2.start(); // 启动线程

3、常见线程方法

关键字 简介
sleep 当前线程暂停,Thread.sleep(1000)
yield 临时暂停,Thread. yield()
join 加入到当前线程中
setPriority 设置线程优先级
setDaemon 设为守护线程
  • 当前线程暂停

    • 表示当前线程暂停 1000 毫秒 ,其他线程不受影响。
    • 会抛出 InterruptedException 中断异常,因为当前线程 sleep 的时候,有可能被停止,这时就会抛出 InterruptedException。

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      Thread t1 = new Thread() {
      public void run() {
      int seconds =0;
      while(true){
      try {
      Thread.sleep(1000); // 当前线程暂停
      } catch (InterruptedException e) {
      e.printStackTrace();
      }
      System.out.printf("已经玩了 LOL %d 秒 %n", seconds++);
      }
      }
      };
      t1.start();
  • 临时暂停

    • 当前线程,临时暂停,使得其他线程可以有更多的机会占用 CPU 资源。

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      Thread t1 = new Thread() {
      public void run(){
      while(!teemo.isDead()) {
      gareen.attackHero(teemo);
      }
      }
      };

      Thread t2 = new Thread() {
      public void run(){
      while(!leesin.isDead()) {
      Thread.yield(); // 临时暂停,使得 t1 可以占用 CPU 资源
      bh.attackHero(leesin);
      }
      }
      };

      t1.setPriority(5);
      t2.setPriority(5);
      t1.start();
      t2.start();
  • 加入到当前线程中

    • 所有进程,至少会有一个线程即主线程,即 main 方法开始执行,就会有一个看不见的主线程存在。
    • 主线程会等待该线程结束完毕,才会往下运行。

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      public static void main(String[] args) {

      Thread t1 = new Thread(){
      public void run() {
      while(!teemo.isDead()) {
      gareen.attackHero(teemo);
      }
      }
      };
      t1.start();

      // 代码执行到这里,一直是 main 线程在运行
      try {
      t1.join(); // t1 线程加入到 main 线程中来,只有 t1 线程运行结束,才会继续往下走
      } catch (InterruptedException e) {
      e.printStackTrace();
      }
      }
  • 线程优先级

    • 当线程处于竞争关系的时候,优先级高的线程会有更大的几率获得 CPU 资源。
    • 线程 1 的优先级是 MAX_PRIORITY,所以它争取到了更多的 CPU 资源执行代码。

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      Thread t1= new Thread() {
      public void run() {
      while(!teemo.isDead()) {
      gareen.attackHero(teemo);
      }
      }
      };

      Thread t2= new Thread() {
      public void run() {
      while(!leesin.isDead()) {
      bh.attackHero(leesin);
      }
      }
      };

      t1.setPriority(Thread.MAX_PRIORITY); // 设置线程优先级
      t2.setPriority(Thread.MIN_PRIORITY);

      t1.start();
      t2.start();
  • 守护线程

    • 当一个进程里,所有的线程都是守护线程的时候,结束当前进程。
    • 守护线程通常会被用来做日志,性能统计等工作。

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      Thread t1= new Thread() {
      public void run() {
      int seconds =0;
      while(true){
      try {
      Thread.sleep(1000);
      } catch (InterruptedException e) {
      e.printStackTrace();
      }
      System.out.printf("已经玩了LOL %d 秒%n", seconds++);
      }
      }
      };

      t1.setDaemon(true); // 设为守护线程
      t1.start();

4、死锁

  • 当业务比较复杂,多线程应用里有可能会发生死锁

  • 演示

    • 线程 1 首先占有对象 1,接着试图占有对象 2;
    • 线程 2 首先占有对象 2,接着试图占有对象 1;
    • 线程 1 等待线程 2 释放对象 2;
    • 与此同时,线程 2 等待线程 1 释放对象 1;
    • 就会 一直等待下去。

5、线程同步

  • 多线程的同步问题指的是多个线程同时修改一个数据的时候,可能导致的问题。多线程的问题,又叫 Concurrency 问题。

  • Java 编程语言内置了两种同步

    • 互斥同步
    • 条件同步
  • 同步关键字

    • synchronized 关键字用于声明需要同步的关键部分。
    • 有两种方法可以使用 synchronized 关键字。
      • 将方法声明为关键部分
      • 将语句块声明为关键段
    • 构造函数不能声明为同步。

5.1 互斥同步

  • 在一个时间点只允许一个线程访问代码段。

  • 将方法声明为关键部分

    1
    2
    3
    4
    5
    6
    7
    public synchronized void someMethod_1() {
    // Method code goes here
    }

    public static synchronized void someMethod_2() {
    // Method code goes here
    }
  • 将语句块声明为关键段

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    public void someMethod_3() {
    // multiple threads can execute here at a time
    synchronized (this) {
    // only one thread can execute here at a time
    }
    // multiple threads can execute here at a time
    }

    public static void someMethod_4() {
    // multiple threads can execute here at a time
    synchronized (Main.class) {
    // only one thread can execute here at a time
    }
    // multiple threads can execute here at a time
    }

5.2 条件同步

  • 通过条件变量和三个交互操作来实现,等待,信号和广播。
方法 介绍
wait() 让占用了这个同步对象的线程,临时释放当前的占用,并且等待。所以调用 wait 是有前提条件的,一定是在 synchronized 块里,否则就会出错。
notify() 通知一个等待在这个同步对象上的线程,你可以苏醒过来了,有机会重新占用当前对象了。
notifyAll() 通知所有的等待在这个同步对象上的线程,你们可以苏醒过来了,有机会重新占用当前对象了。
  • wait 方法和 notify 方法,并不是 Thread 线程上的方法,它们是 Object 上的方法。

  • 因为所有的 Object 都可以被用来作为同步对象,所以准确的讲,wait 和 notify 是同步对象上的方法。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    public synchronized void hurt() {
    。。。
    this.wait();
    。。。
    }

    public synchronized void recover() {
    。。。
    this.notify();
    }
  • sleep() 和 wait() 的区别

    • sleep 和 wait 之间没有任何关系。
    • sleep 是 Thread 类的方法,指的是当前线程暂停。
    • wait 是 Object 类的方法,指的占用当前对象的线程临时释放对当前对象的占用,以使得其他线程有机会占用当前对象。所以调用 wait 方法一定是在 synchronized 中进行。

6、线程安全的类

  • 如果一个类,其方法都是有 synchronized 修饰的,那么该类就叫做线程安全的类

  • 借助 Collections.synchronizedList,可以把 ArrayList 转换为线程安全的 List。

    1
    2
    3
    List<Integer> list1 = new ArrayList<>();

    List<Integer> list2 = Collections.synchronizedList(list1);
  • 与此类似的,还有 HashSetLinkedListHashMap 等等非线程安全的类,都通过工具类 Collections 转换为线程安全的。

  • 当一个线程进入一个对象的一个 synchronized 方法后,其它线程是否可进入此对象的其它方法。

    • 这要看情况而定,如果该对象的其他方法也是有 synchronized 修饰的,那么其他线程就会被挡在外面。否则其他线程就可以进入其他方法。

7、线程池

  • 每一个线程的启动和结束都是比较消耗时间和占用资源的。

  • 如果在系统中用到了很多的线程,大量的启动和结束动作会导致系统的性能变卡,响应变慢。

  • 为了解决这个问题,引入线程池这种设计思想。

  • 线程池的模式很像生产者消费者模式,消费的对象是一个一个的能够运行的任务。

  • Java 自带线程池

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    import java.util.concurrent.LinkedBlockingQueue;
    import java.util.concurrent.ThreadPoolExecutor;
    import java.util.concurrent.TimeUnit;

    ThreadPoolExecutor threadPool = new ThreadPoolExecutor(
    10, // 线程池初始化了 10 个线程在里面工作
    15, // 如果 10 个线程不够用了,就会自动增加到最多 15 个线程
    60, // 结合第四个参数,表示经过 60 秒,多出来的线程还没有接到活儿,就会回收,最后保持池子里就 10 个
    TimeUnit.SECONDS, // 单位 秒
    new LinkedBlockingQueue<Runnable>()); // 用来放任务的集合

    threadPool.execute(new Runnable() { // execute 方法用于添加新的任务
    @Override
    public void run() {
    System.out.println("任务 1");
    }
    });

    threadPool.execute(new Runnable() {
    @Override
    public void run() {
    System.out.println("任务 2");
    }
    });

8、线程锁

  • Lock 对象是一个接口,与 synchronized 类似的,lock 也能够达到同步的效果。

  • 实现同步效果

    • 与 synchronized (someObject) 类似的,lock() 方法,表示当前线程占用 lock 对象,一旦占用,其他线程就不能占用了。
    • 与 synchronized 不同的是,一旦 synchronized 块结束,就会自动释放对 someObject 的占用。
    • lock 却必须调用 unlock 方法进行手动释放,为了保证释放的执行,往往会把 unlock() 放在 finally 中进行。

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      import java.util.concurrent.locks.Lock;
      import java.util.concurrent.locks.ReentrantLock;

      Lock lock = new ReentrantLock();

      Thread t1 = new Thread() {
      public void run() {
      try {
      lock.lock(); // 线程启动,试图占有对象 lock
      Thread.sleep(5000); // 占有对象 lock,进行 5 秒的业务操作
      } catch (InterruptedException e) {
      e.printStackTrace();
      } finally {
      lock.unlock(); // 释放对象 lock
      }
      }
      };
      t1.start();
  • trylock 方法

    • synchronized 是不占用到手不罢休的,会一直试图占用下去。
    • 与 synchronized 的钻牛角尖不一样,Lock 接口还提供了一个 trylock 方法。
    • trylock 会在指定时间范围内试图占用。
    • 因为使用 trylock 有可能成功,有可能失败,所以后面 unlock 释放锁的时候,需要判断是否占用成功了,如果没占用成功也 unlock,就会抛出异常。

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      import java.util.concurrent.TimeUnit;
      import java.util.concurrent.locks.Lock;
      import java.util.concurrent.locks.ReentrantLock;

      Lock lock = new ReentrantLock();

      Thread t1 = new Thread() {
      public void run() {
      boolean locked = false;
      try {
      locked = lock.tryLock(1, TimeUnit.SECONDS); // 线程启动,试图占有对象 lock
      if (locked) {
      Thread.sleep(5000); // 占有对象 lock,进行 5 秒的业务操作
      } else {
      log("经过 1 秒钟的努力,还没有占有对象,放弃占有");
      }
      } catch (InterruptedException e) {
      e.printStackTrace();
      } finally {
      if (locked) {
      lock.unlock(); // 释放对象 lock
      }
      }
      }
      };
      t1.start();
  • 线程交互

    • 使用 synchronized 方式进行线程交互,用到的是同步对象的 wait,notify 和 notifyAll 方法。
    • 首先通过 lock 对象得到一个 Condition 对象。
    • 然后分别调用这个 Condition 对象的:await()signal()signalAll() 方法。

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      31
      32
      33
      34
      35
      36
      37
      import java.util.concurrent.locks.Condition;
      import java.util.concurrent.locks.Lock;
      import java.util.concurrent.locks.ReentrantLock;

      Lock lock = new ReentrantLock();
      Condition condition = lock.newCondition();

      Thread t1 = new Thread() {
      public void run() {
      try {
      lock.lock();
      Thread.sleep(5000);
      condition.await(); // 临时释放对象 lock,并等待
      Thread.sleep(5000); // 重新占有对象 lock,并进行 5 秒的业务操作
      } catch (InterruptedException e) {
      e.printStackTrace();
      } finally {
      lock.unlock();
      }
      }
      };
      t1.start();

      Thread t2 = new Thread() {
      public void run() {
      try {
      lock.lock();
      Thread.sleep(5000);
      condition.signal(); // 唤醒等待中的线程
      } catch (InterruptedException e) {
      e.printStackTrace();
      } finally {
      lock.unlock();
      }
      }
      };
      t2.start();
      1
      condition.signalAll();                                           // 唤醒所有线程
  • Lock 和 synchronized 的区别

    • Lock 是一个接口,而 synchronized 是 Java 中的关键字,synchronized 是内置的语言实现,Lock 是代码层面的实现。

    • Lock 可以选择性的获取锁,如果一段时间获取不到,可以放弃。synchronized 不行,会一根筋一直获取下去。

    • 借助 Lock 的这个特性,就能够规避死锁,synchronized 必须通过谨慎和良好的设计,才能减少死锁的发生。

    • synchronized 在发生异常和同步块结束的时候,会自动释放锁。而 Lock 必须手动释放,所以如果忘记了释放锁,一样会造成死锁。

9、原子访问

  • 原子访问即不可中断的操作,比如赋值操作。

    • 原子性操作本身是线程安全的。
    • i++i--i = i+1 这些都是非原子性操作。
  • JDK6 以后,新增加了一个包 java.util.concurrent.atomic,里面有各种原子类,比如 AtomicInteger。

  • 而 AtomicInteger 提供了各种自增,自减等方法,这些方法都是原子性的。

  • 换句话说,自增方法 incrementAndGet 是线程安全的,同一个时间,只有一个线程可以调用这个方法。

    1
    2
    3
    4
    5
    6
    7
    import java.util.concurrent.atomic.AtomicInteger;

    AtomicInteger atomicI = new AtomicInteger();

    int j = atomicI.incrementAndGet();
    int i = atomicI.decrementAndGet();
    int k = atomicI.addAndGet(3);
文章目录
  1. 1. 前言
  2. 2. 1、多线程
  3. 3. 2、创建多线程
  4. 4. 3、常见线程方法
  5. 5. 4、死锁
  6. 6. 5、线程同步
    1. 6.1. 5.1 互斥同步
    2. 6.2. 5.2 条件同步
  7. 7. 6、线程安全的类
  8. 8. 7、线程池
  9. 9. 8、线程锁
  10. 10. 9、原子访问
隐藏目录