实现线程的方式:

第一种方式:

  1. 通过实现Runnable 接口的方式实现多线程
  • 首先通过RunnableThread类实现Runnable接口
  • 重写 run()方法
  • 之后只要把这个实现了 run()方法 的实例传到Thread类中去就可以实现多线程
  1. public class RunnableThread implements Runnable {
  2. @Override
  3. public void run() {
  4. System.out.println("用实现Runnable接口实现线程");
  5. }
  6. }

第二种方式:

  1. 继承Thread
  • 继承Thread类,并重写其中的 run()方法
  1. public class ExtendsThread extends Thread {
  2. @Override
  3. public void run() {
  4. System.out.println("用Thread类实现线程");
  5. }
  6. }

第三种方式:

  1. 通过线程池创建线程
  • 默认采用DefaultThreadFactory
  • 它会给我们线程池创建的线程设置一些默认的值,比如它的名字,他是不是守护线程,以及它的优先级
  • 其本质还是通过 new Thread() 前面两种方式实现的

第四种方式:

  1. 有返回值的callable也是新建线程的方式
  • 实现了callable接口,并且给它的泛型设置成integer,然后它会返回一个随机数回来
  1. class CallableTask implements Callable<Integer> {
  2. @Override
  3. public Integer call() throws Exception {
  4. return new Random().nextInt();
  5. }
  6. }
  7. //创建线程池
  8. ExecutorService service = Executors.newFixedThreadPool(10);
  9. //提交任务,并用Future提交返回结果
  10. Future<Integer>future = service.submit(new CallableTask());

第五种方式:

  1. 定时器Timer
  • TimerTask的实现了Runnable接口,Timer内部有个TimerThread继承自Thread因此还是Thread+
  1. class TimerThread extends Thread {
  2. //具体实现
  3. }

第六种方式:

  1. 匿名内部类

第七种方式:

  1. Lambda表达式
  1. new Thread(()->
  2. System.out.println(Thread.currentThread().getName())).start();
  3. )

最终实现的都是实现runnable接口或是继承Thread类

两个方式的最主要区别还是 run()方法 的内容来源

  • 方式一
    最终调用target.run()

  • 方式二
    run()整个都被重写

  1. @Override
  2. pubilc void run() {
  3. if(target != null) {
  4. target.run();
  5. }
  6. }

创建线程只有一中方式: 构造 Thread 类

实现线程”运行内容”的两种方式:

  1. 方式一:
  2. 实现Runnable接口的run方式,并把Runnable实例作为target对象,传给Thread类,最终调用target.run()
  3. 方式二:
  4. 继承Thread类,重写Threadrun方法(),Thread.start() 会执行run()

==实现runnable接口比继承Thread类更好

  • 可以把不同的内容进行解耦,权责分明

  • 某些情况下可以提升性能,减少开销

  • 继承Thread类相当于限制了代码未来的可扩展性

停止线程

对于 Java 而言,最正确的停止线程的方式是使用 interrupt。但 interrupt 仅仅起到通知被停止线程的作用。而对于被停止的线程而言,它拥有完全的自主权,它既可以选择立即停止,也可以选择一段时间后停止,也可以选择压根不停止。那么为什么 Java 不提供强制停止线程的能力呢?

事实上,Java 希望程序间能够相互通知、相互协作地管理线程,因为如果不了解对方正在做的工作,贸然强制停止线程就可能会造成一些安全的问题,为了避免造成问题就需要给对方一定的时间来整理收尾工作。比如:线程正在写入一个文件,这时收到终止信号,它就需要根据自身业务判断,是选择立即停止,还是将整个文件写入成功后停止,而如果选择立即停止就可能造成数据不完整,不管是中断命令发起者,还是接收者都不希望数据出现问题。

阻塞调用将会被Interrupted Exception 异常中断,将中断标记位设置成false

即使线程在休眠,仍然能够响应中断,并抛出异常

如果我们负责编写的方法需要调用sleep或者wait时,就要求在方法中使用try/catch或在方法中声明throws InteruptedException

  1. void subTask{
  2. try{
  3. sleep(delay);
  4. }catch(InterruptedException e){}
  5. //这里不处理该异常是非常不好的
  6. }

如何用 interrupt 停止线程

  1. while (!Thread.currentThread().islnterrupted() && more work to do) {
  2. do more work
  3. }

具体例子。

  1. public class StopThread implements Runnable {
  2. @Override
  3. public void run() {
  4. int count = 0;
  5. while (!Thread.currentThread().isInterrupted() && count < 1000) {
  6. System.out.println("count = " + count++);
  7. }
  8. }
  9. public static void main(String[] args) throws InterruptedException {
  10. Thread thread = new Thread(new StopThread());
  11. thread.start();
  12. Thread.sleep(5);
  13. thread.interrupt();
  14. }
  15. }

sleep 期间能否感受到中断

  1. Runnable runnable = () -> {
  2. int num = 0;
  3. try {
  4. while (!Thread.currentThread().isInterrupted() &&
  5. num <= 1000) {
  6. System.out.println(num);
  7. num++;
  8. Thread.sleep(1000000);
  9. }
  10. } catch (InterruptedException e) {
  11. e.printStackTrace();
  12. }
  13. };

改写上面的代码,如果线程在执行任务期间有休眠需求,也就是每打印一个数字,就进入一次 sleep ,而此时将 Thread.sleep() 的休眠时间设置为 1000 秒钟。

  1. public class StopDuringSleep {
  2. public static void main(String[] args) throws InterruptedException {
  3. Runnable runnable = () -> {
  4. int num = 0;
  5. try {
  6. while (!Thread.currentThread().isInterrupted() && num <= 1000) {
  7. System.out.println(num);
  8. num++;
  9. Thread.sleep(1000000);
  10. }
  11. } catch (InterruptedException e) {
  12. e.printStackTrace();
  13. }
  14. };
  15. Thread thread = new Thread(runnable);
  16. thread.start();
  17. Thread.sleep(5);
  18. thread.interrupt();
  19. }
  20. }

主线程休眠 5 毫秒后,通知子线程中断,此时子线程仍在执行 sleep 语句,处于休眠中。那么就需要考虑一点,在休眠中的线程是否能够感受到中断通知呢?是否需要等到休眠结束后才能中断线程呢?如果是这样,就会带来严重的问题,因为响应中断太不及时了。正因为如此,Java 设计者在设计之初就考虑到了这一点。

如果 sleep、wait 等可以让线程进入阻塞的方法使线程休眠了,而处于休眠中的线程被中断,那么线程是可以感受到中断信号的,并且会抛出一个 InterruptedException 异常,同时清除中断信号,将中断标记位设置成 false。这样一来就不用担心长时间休眠中线程感受不到中断了,因为即便线程还在休眠,仍然能够响应中断通知,并抛出异常。

两种最佳处理方式:

方法一:

  1. **方法签名异常,run()强制try/catch**
  1. void subTask() throws InterruptedException {
  2. sleep(delay);
  3. }
  • 用throws InterruptedException标记方法,不采用try语句块捕获异常,以便于该异常可以传递到顶层,让run()方法可以捕获异常
  • 由于run方法内无法抛出checked Exception(只能用try catch),所以在方法上抛出异常的好处是,顶层方法必须处理该异常,不能够漏掉或者被吞掉

方法二:

再次中断

  1. private void reInterrupt(){
  2. trt{
  3. Thread.sleep(2000);
  4. }catch(InterruptedException e){
  5. Thread.currentThread().interrup();
  6. e.printStackTrace();
  7. }
  8. }
  • 在catch语句块中调用Thread.currentThread().Interrupt()函数
  • 因为如果线程在休眠期间被中断,那么会自动清除中断信号
  • 如果这时手动添加中断信号,中断信号依然可以捕捉到
  • 这样后续执行的方法依然可以检测到这里发生过中断,可以做出相应的处理,整个线程可以正常退出

生产者消费者模式

  1. class Producer implements Runnable {
  2. public volatile boolean canceled = false;
  3. BlockingQueue storage;
  4. public Producer(BlockingQueue storage){
  5. this.storage = storage;
  6. }
  7. @Override
  8. public void run() {
  9. int num = 0;
  10. try {
  11. while (num <= 100000 && !canceled) {
  12. if (num % 50 == 0) {
  13. storage.put(num);
  14. System.out.println(num + "是50的倍数,被放到仓库");
  15. }
  16. num++;
  17. }
  18. } catch (InterruptedException e){
  19. e.printStackTrace();
  20. } finally {
  21. System.out.println("生产者运行结束");
  22. }
  23. }
  24. }

首先,声明了一个生产者 Producer,通过 volatile 标记的初始值为 false 的布尔值 canceled 来停止线程。而在 run() 方法中,while 的判断语句是 num 是否小于 100000 及 canceled 是否被标记。while 循环体中判断 num 如果是 50 的倍数就放到 storage 仓库中,storage 是生产者与消费者之间进行通信的存储器,当 num 大于 100000 或被通知停止时,会跳出 while 循环并执行 finally 语句块,告诉大家“生产者结束运行”。

  1. class Consumer {
  2. BlockingQueue storage;
  3. public Consumer (BlockingQueue storage) {
  4. this.storage = storage;
  5. }
  6. public boolean needMoreNums() {
  7. if (Math.random() > 0.97) {
  8. return false;
  9. }
  10. return true;
  11. }
  12. }

而对于消费者 Consumer,它与生产者共用同一个仓库 storage,并且在方法内通过 needMoreNums() 方法判断是否需要继续使用更多的数字,刚才生产者生产了一些 50 的倍数供消费者使用,消费者是否继续使用数字的判断条件是产生一个随机数并与 0.97 进行比较,大于 0.97 就不再继续使用数字。

  1. public static void main(String[] args) throws InterruptedException {
  2. ArrayBlockingQueue storage = new ArrayBlockingQueue(8);
  3. Producer producer = new Producer(storage);
  4. Thread producerThread = new Thread(producer);
  5. producerThread.start();
  6. Thread.sleep(500);
  7. Consumer consumer = new Consumer(storage);
  8. while (consumer.needMoreNums()) {
  9. System.out.println(consumer.storage.take() + "被消费了");
  10. Thread.sleep(100);
  11. }
  12. System.out.println("消费者不需要更多的数据了");
  13. //一旦消费不需要更多数据,我们应该让生产者也停下来,但实际情况却停不下来
  14. producer.canceled = true;
  15. System.out.println(producer.canceled);
  16. }

下面来看下 main 函数,首先创建了生产者/消费者共用的仓库 BlockingQueue storage,仓库容量是 8,并且建立生产者并将生产者放入线程后启动线程,启动后进行 500 毫秒的休眠,休眠时间保障生产者有足够的时间把仓库塞满,而仓库达到容量后就不会再继续往里塞,这时生产者会阻塞,500 毫秒后消费者也被创建出来,并判断是否需要使用更多的数字,然后每次消费后休眠 100 毫秒,这样的业务逻辑是有可能出现在实际生产中的。

当消费者不再需要数据,就会将 canceled 的标记位设置为 true,理论上此时生产者会跳出 while 循环,并打印输出“生产者运行结束”。

然而结果却不是我们想象的那样,尽管已经把 canceled 设置成 true,但生产者仍然没有停止,这是因为在这种情况下,生产者在执行 storage.put(num) 时发生阻塞,在它被叫醒之前是没有办法进入下一次循环判断 canceled 的值的,所以在这种情况下用 volatile 是没有办法让生产者停下来的,相反如果用 interrupt 语句来中断,即使生产者处于阻塞状态,仍然能够感受到中断信号,并做响应处理。