Java多线程学习笔记

创建线程的两种方式:

  • 继承Thread类;
  • 实现Runnable接口。
  1. 继承Thread类:
public class MyThread extends Thread {
    public void run() {
        System.out.println("My thread.");
    }

    public static void main(String[] args) {
        MyThread myThread = new MyThread();
        myThread.start();
    }
}
  1. 当一个类已经继承了其他类时,就只能实现Runnable接口:
public class MyThread extends OtherClass implements Runnable {
    @Override
    public void run() {
        System.out.println("my thread");
    }

    public static void main(String[] args) {
        // 先创建一个Thread实例,再传入自己的MyThread实例
        MyThread myThread = new MyThread();
        new Thread(myThread).start();
    }
}

Thread类实现了Runnable接口。

public class Thread implements Runnable {
    private volatile String name;
    private int priority;
    ...
}

当线程有返回值时,必须实现Callable接口,执行Callable任务后返回Future对象,调用Future对象的get()方法获取返回值:

public class MyThread2 implements Callable {

    private int i;

    MyThread2(int i) {
        this.i = i;
    }

    @Override
    public Object call() throws Exception {
        return i * i * i;
    }

    public static void main(String[] args) throws ExecutionException, InterruptedException {
        int taskSize = 10;
        ExecutorService pool = Executors.newFixedThreadPool(taskSize);
        List<Future> list = new ArrayList<>();

        for (int i = 0; i < taskSize; i++) {
            Callable callable = new MyThread2(i);
            Future future = pool.submit(callable);
            list.add(future);
        }
        pool.shutdown();

        for (Future future : list) {
            System.out.println(future.get().toString());
        }
    }
}

new Thread的弊端如下:

a. 每次new Thread新建对象性能差。
b. 线程缺乏统一管理,可能无限制新建线程,相互之间竞争,及可能占用过多系统资源导致死机或oom。
c. 缺乏更多功能,如定时执行、定期执行、线程中断。

相比new Thread,Java提供的四种线程池的好处在于:

a. 重用存在的线程,减少对象创建、消亡的开销,性能佳。
b. 可有效控制最大并发线程数,提高系统资源的使用率,同时避免过多资源竞争,避免堵塞。
c. 提供定时执行、定期执行、单线程、并发数控制等功能。

参见 >> Java 四种线程池的用法分析

Java提供的4种线程池:

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

newCachedThreadPool

public class MyThread4 {
    public static void main(String[] args) {
        ExecutorService pool = Executors.newCachedThreadPool();
        for (int i = 0; i < 5; i++) {
            final int index = i;
            try {
                Thread.sleep(i * 500);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            pool.execute(() -> System.out.println(Thread.currentThread().getName() + " " + index));
        }
    }
}

线程池为无限大,当执行第二个任务时第一个任务已经完成,会复用执行第一个任务的线程,而不用每次新建线程。

pool-1-thread-1 0
pool-1-thread-1 1
pool-1-thread-1 2
pool-1-thread-1 3
pool-1-thread-1 4
pool-1-thread-1 5
pool-1-thread-1 6
pool-1-thread-1 7
pool-1-thread-1 8
pool-1-thread-1 9

newFixedThreadPool

ExecutorService pool = Executors.newFixedThreadPool(3);
for (int i = 0; i < 10; i++) {
    final int x = i;
    pool.execute(() -> {
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName() + "  " + x);
    });
}

任务数超出最大线程数,超出的线程会等待:

pool-1-thread-2  1
pool-1-thread-3  2
pool-1-thread-1  0
pool-1-thread-2  3
pool-1-thread-1  5
pool-1-thread-3  4
pool-1-thread-2  6
pool-1-thread-1  7
pool-1-thread-3  8
pool-1-thread-2  9

newSchuduledThreadPool

ScheduledExecutorService pool = Executors.newScheduledThreadPool(3);
// 延迟 1 秒后每 3 秒执行一次
pool.scheduleAtFixedRate(() -> System.out.println(Thread.currentThread().getName()), 1, 3, TimeUnit.SECONDS);

newSingleThreadPool

ExecutorService pool = Executors.newSingleThreadExecutor();
for (int i = 0; i < 10; i++) {
    final int index = i;
    pool.execute(() -> System.out.println(Thread.currentThread().getName() + " " + index));
}
pool-1-thread-1 0
pool-1-thread-1 1
pool-1-thread-1 2
pool-1-thread-1 3
pool-1-thread-1 4
pool-1-thread-1 5
pool-1-thread-1 6
pool-1-thread-1 7
pool-1-thread-1 8
pool-1-thread-1 9

线程的生命周期:

新建New:JVM分配内存,并初始化成员变量的值;

就绪Runnable:创建方法调用栈和程序计数器;

运行 Running:获得CPU,执行run()方法;

阻塞 Blocked:让出CPU时间片,停止运行;

死亡 Dead:正常结束,异常结束或调用stop()方法。

wait()和sleep()的关键的区别在于,sleep()方法属于Thread类,wait()方法属于Object类,wait()是用于线程间通信的,而sleep()是用于短时间暂停当前线程。更加明显的一个区别在于,当一个线程调用wait()方法的时候,会释放它锁持有的对象的管程和锁,但是调用sleep()方法的时候,不会释放他所持有的管程。

而当调用 wait()方法的时候,线程会放弃对象锁,进入等待此对象的等待锁定池,只有针对此对象调用 notify()方法后本线程才进入对象锁定池准备获取对象锁进入运行状态。

终止线程的3种方式 > Java中如何优雅正确的终止线程

  1. 使用stop()方法

    thread.stop()调用之后,创建子线程的线程就会抛出 ThreadDeatherror 的错误,并且会释放子线程所持有的所有锁。一般任何进行加锁的代码块,都是为了保护数据的一致性,如果在调用thread.stop()后导致了该线程所持有的所有锁的突然释放(不可控制),那么被保护数据就有可能呈现不一致性,其他线程在使用这些被破坏的数据时,有可能导致一些很奇怪的应用程序错误。因此,并不推荐使用 stop 方法来终止线程。

  2. 使用volatile标志位:

    public class MyRunnable implements Runnable {
    
        public volatile boolean flag = true;
    
        @Override
        public void run() {
            System.out.println(Thread.currentThread().getName() + "创建");
            try {
                Thread.sleep(1000L);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            while (flag) {
    
            }
            System.out.println(Thread.currentThread().getName() + "终止");
        }
    
        public static void main(String[] args) throws InterruptedException {
            MyRunnable myRunnable = new MyRunnable();
            for (int i = 0; i < 3; i++) {
                Thread thread = new Thread(myRunnable, i + " ");
                thread.start();
            }
            Thread.sleep(2000L);
            System.out.println("-------");
            myRunnable.flag = false;
        }
    }
    

    output

    0 创建
    2 创建
    1 创建
    -------
    2 终止
    0 终止
    1 终止
    
  3. 使用interrupt方式

    1. 线程处于阻塞状态时:

      如使用了 sleep,同步锁的 wait,socket 中的 receiver,accept 等方法时,会使线程处于阻塞状态。当调用线程的 interrupt()方法时,会抛出 InterruptException 异常。阻塞中的那个方法抛出这个异常,通过代码捕获该异常,然后 break 跳出循环状态,从而让我们有机会结束这个线程的执行。通常很多人认为只要调用 interrupt 方法线程就会结束,实际上是错的, 一定要先捕获 InterruptedException 异常之后通过break 来跳出循环,才能正常结束 run 方法。

      public class MyThread5 extends Thread {
      
          @Override
          public void run() {
              try {
                  Thread.sleep(3000L);
              } catch (InterruptedException e) {
                  e.printStackTrace();
              }
          }
      
          public static void main(String[] args) {
              MyThread5 thread5 = new MyThread5();
              thread5.start();
              boolean interrupted = thread5.isInterrupted();
              if (!interrupted) {
                  thread5.interrupt();
              }
          }
      }
      
      output
      java.lang.InterruptedException: sleep interrupted
          at java.base/java.lang.Thread.sleep(Native Method)
          at ch4.MyThread5.run(MyThread5.java:12)
      
    2. 线程未处于阻塞状态:

      使用 isInterrupted()判断线程的中断标志来退出循环。当使用
      interrupt()方法时,中断标志就会置 true,和使用自定义的标志来控制循环是一样的道理。

      public class MyThread5 extends Thread {
          public void run() {
              super.run();
      
              try {
                  System.out.println("线程开始。。。");
                  Thread.sleep(200000);
                  System.out.println("线程结束。");
              } catch (InterruptedException e) {
                  System.out.println("在沉睡中被停止, 进入catch, 调用isInterrupted()方法的结果是:" + this.isInterrupted());
                  e.printStackTrace();
              }
          }
      
          public static void main(String[] args) {
              MyThread5 thread5 = new MyThread5();
              thread5.start();
              thread5.interrupt();
          }
      }
      
      output
      线程开始。。。
      在沉睡中被停止, 进入catch, 调用isInterrupted()方法的结果是:false
      java.lang.InterruptedException: sleep interrupted
          at java.base/java.lang.Thread.sleep(Native Method)
          at ch4.MyThread5.run(MyThread5.java:13)
      

线程基本方法:

yield:线程的礼让指的是先将线程的资源让出去,让别的线程先执行。

public class ThreadMethod extends Thread {
    public static void main(String[] args) {
        Thread thread = new Thread(() -> {
            for (int x = 0; x < 10; x++) {
                if (x % 3 == 0) {
                    Thread.yield(); //线程礼让
                    System.out.println("玩耍线程礼让执行 ***********************");
                }
                System.out.println(Thread.currentThread().getName() + "、 x = " + x);
            }
        }, "玩耍的线程");
        thread.start();
        for (int x = 0; x < 10; x++) {
            System.out.println("【霸道的main主线程】 x = " + x);
        }
    }
}

output

玩耍线程礼让执行 ***********************
【霸道的main主线程】 x = 0
【霸道的main主线程】 x = 1
【霸道的main主线程】 x = 2
【霸道的main主线程】 x = 3
【霸道的main主线程】 x = 4
【霸道的main主线程】 x = 5
【霸道的main主线程】 x = 6
【霸道的main主线程】 x = 7
【霸道的main主线程】 x = 8
【霸道的main主线程】 x = 9
玩耍的线程、 x = 0
玩耍的线程、 x = 1
玩耍的线程、 x = 2
玩耍线程礼让执行 ***********************
玩耍的线程、 x = 3
玩耍的线程、 x = 4
玩耍的线程、 x = 5
玩耍线程礼让执行 ***********************
玩耍的线程、 x = 6
玩耍的线程、 x = 7
玩耍的线程、 x = 8
玩耍线程礼让执行 ***********************
玩耍的线程、 x = 9

join: 线程强制执行,指的当满足某些条件之后,某一个线程对象将一直可以独占资源,一直到该线程的程序执行结束。

public class ThreadMethod extends Thread {
    public static void main(String[] args) throws InterruptedException {
        Thread mainThread = Thread.currentThread(); //获得主线程
        Thread thread = new Thread(() -> {
            for (int x = 0; x < 10; x++) {
                if (x == 3) {  //如果x == 3,就强制执行主线程
                    try {
                        mainThread.join();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                System.out.println(Thread.currentThread().getName() + "、 x = " + x);
            }
        }, "玩耍的线程");
        thread.start();
        for (int x = 0; x < 10; x++) {
            Thread.sleep(100L);
            System.out.println("【霸道的main主线程】 x = " + x);
        }
    }
}

output

玩耍的线程、 x = 0
玩耍的线程、 x = 1
玩耍的线程、 x = 2
【霸道的main主线程】 x = 0
【霸道的main主线程】 x = 1
【霸道的main主线程】 x = 2
【霸道的main主线程】 x = 3
【霸道的main主线程】 x = 4
【霸道的main主线程】 x = 5
【霸道的main主线程】 x = 6
【霸道的main主线程】 x = 7
【霸道的main主线程】 x = 8
【霸道的main主线程】 x = 9
玩耍的线程、 x = 3
玩耍的线程、 x = 4
玩耍的线程、 x = 5
玩耍的线程、 x = 6
玩耍的线程、 x = 7
玩耍的线程、 x = 8
玩耍的线程、 x = 9

sleep: 休眠的主要特点是可以自动实现线程的唤醒,以继续进行后续的处理。但是需要注意的是,如果现在有多个线程对象,休眠也是有多个先后顺序的。

wait和notify:

  • synchronized关键词用于互斥访问。
  • 为了让一个方法是同步的,简单的添加synchronized关键词到它们的声明上。那么同一个对象上没有两个同步方法的调用可以相互交错。
  • 同步语句必须指定提供内在锁的对象。当使用synchronized(this)时候,必须避免使用同步其他对象方法的调用。
  • wait()告诉调用的线程放弃监视器和进入休眠状态,直到其他线程进入相同的监视器和调用notify()。
  • notify()唤醒在相同对象上第一个调用wait()的线程。
public class ThreadA {
    public static void main(String[] args) {
        ThreadB b = new ThreadB();
        b.start();

        synchronized (b) {
            try {
                System.out.println("Waiting for b to complete...");
                b.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

            System.out.println("Total is: " + b.total);
        }
    }
}

class ThreadB extends Thread {
    int total;

    @Override
    public void run() {
        synchronized (this) {
            for (int i = 0; i < 100; i++) {
                total += i;
            }
            notify();
        }
    }
}