JAVA入门学习十

时间:2022-07-28
本文章向大家介绍JAVA入门学习十,主要内容包括其使用实例、应用技巧、基本知识点总结和需要注意事项,具有一定的参考价值,需要的朋友可以参考一下。

[TOC]

多线程Thread入门

1.简单概述

描述:什么是线程?

  • 线程是程序执行的一条路径, 一个进程中可以包含多条线程
  • 多线程并发执行可以提高程序的效率, 可以同时完成多项工作;(简单说利用了空闲时间)

多线程的应用场景:

  • 迅雷开启多条线程一起下载
  • QQ同时和多个人一起视频
  • 服务器同时处理多个客户端请求

多线程并行和并发的区别?

  • 并行就是两个任务同时运行,就是甲任务进行的同时,乙任务也在进行(需要多核CPU)
    • 比如:我跟两个网友聊天,左手操作一个电脑跟甲聊同时右手用另一台电脑跟乙聊天这就叫并行。
  • 并发是指两个任务都请求运行,而处理器只能按受一个任务,就把这两个任务安排轮流进行,由于时间间隔较短,使人感觉两个任务都在运行。
    • 比如:用一台电脑我先给甲发个消息,然后立刻再给乙发消息,然后再跟甲聊,再跟乙聊。这就叫并发。

注意: 一颗CPU在同一时刻只处理一个任务,只不过执行时间(执行效率高)太短让我们误认为是同一时刻运行多个后台程序;

Java程序运行原理:

  • Java命令会启动java虚拟机,之后启动JVM等同于启动了一个应用程序,但实际上是启动了一个进程
  • 该进程会自动启动一个 “主线程” 然后主线程去调用某个类的 main 方法

JVM的启动是多线程的吗?

  • JVM启动至少启动了垃圾回收线程主线程所以是多线程的。

基础示例:

package com.weiyigeek.Thread;
public class Demo1_Thread {
  public static void main(String[] args) {
    //示例1.证明Java的JVM是多线程执行的
    //CPU在同一时刻只会运行一个任务,但是由于执行效率太高了会出现联系执行相同的任务;
    //线程就是一条路所有的任务将在上面执行
    for(int i = 0; i < 1000000; i++)
      new Demo();
    
    for(int i = 0; i < 1000; i++)
      System.out.println("Master Thread 执行 " + i);	
  }
}   

//Demo:进行垃圾回收
class Demo {
  @Override
  protected void finalize() throws Throwable {
    System.out.println(this.getClass() + "类垃圾回收!");
  }
}

/**
 * 执行结果:
 * class com.weiyigeek.Thread.Demo类垃圾回收!
	class com.weiyigeek.Thread.Demo类垃圾回收!
	class com.weiyigeek.Thread.Demo类垃圾回收!
	Master Thread 执行 620
	class com.weiyigeek.Thread.Demo类垃圾回收!
	class com.weiyigeek.Thread.Demo类垃圾回收!
	Master Thread 执行 621
	Master Thread 执行 622
 ***/
2.多线程实现

描述:线程是程序中的执行线程,Java虚拟机允许应用程序并发的运行多个执行线程;

  • 每一个线程都有一个优先级,高优先级线程的执行优于低优先级进程;
  • 每一个线程可能会或可能不会被标记为一个守护进程。
  • 在某个线程中运行的代码创建了一个新的Thread对象时,新线程的优先级被设置为创建线程的优先级,当且仅当创建线程是一个守护进程,新线程才是守护线程的。

基础语法:

//Thread类
java.lang.Object 
java.lang.Thread 
All Implemented Interfaces: Runnable 

//类声明
public class Thread extends Object implements Runnable

//构造方法
Thread(Runnable target)  //分配一个新的 Thread对象。  
Thread(String name)  //分配一个新的 Thread对象。(参数是一个线程的名称)
Thread(Runnable target, String name) //分配一个新的 Thread对象。 

//类方法
void start() //导致该线程开始执行 java虚拟机自动调用这个线程的run方法。
String getName()  //返回此线程的名称。  
void setName(String name) //改变该线程的名称等于参数 name。
static Thread currentThread()  //返回当前正在执行的线程对象的引用。(可以直接类.调用)

多线程实现的两种方式:

  • 继承Thread类重写run方法
  • 实现Runable接口重写run方法
//这是一个功能接口因此可以作为赋值的目标一个lambda表达式或方法参考
//例如,Runnable通过类Thread实施。激活的意思是说一个线程已启动并且尚未停止。
@FunctionalInterface
public interface Runnable //一个类实现Runnable可以运行run方法,通过自身实例化一个对象并且传入Thread实例作为目标参数;

//一个方法(需要重写)
void run()  //当一个对象实现的接口 Runnable是用来创建一个线程,启动线程使对象的 run方法在单独执行的线程调用。

实现Runnable的原理:

  • 1,看Thread类的构造函数:传递了Runnable接口的引用
  • 2,通过init()方法:找到传递的target给成员变量的target赋值
  • 3,查看run方法:发现run方法中有判断,如果target不为null就会调用Runnable接口子类对象的run方法

实际案例1:

package com.weiyigeek.Thread;
public class Demo2_ThreadClass {
  public static void main(String[] args) {
    //示例1.多线程程序实现的方式1
    /**
		* 1)定义新类继承Thread类
		* 2)重写run方法
		* 3)把新线程要做的事写在run方法中
		* 4)创建线程对象
		* 5)开启start()新线程内部会自动执行run方法
		*/
    NewThread mt = new NewThread();
    mt.start();  //调用start方法开启多线程;
    //比对多线程执行,我们在主线程里面写入一个循环
    for (int i = 0; i < 1000; i++)
      System.out.println(new Thread().getName() + " - MasterThread");  //可以直接获取线程的名称
    
    
    //示例2.多线程程序实现的方式2:实现Runnable接口
    /* 1)定义类实现Runnable接口
		* 2)实现run方法
		* 3)把新线程要做的事写在run方法中
		* 4)创建自定义的Runnable的子类对象
		* 5)创建Thread对象, 传入Runnable对象
		* 6)调用start()开启新线程, 内部会自动调用Runnable的run()方法
		* */
    NewRunnable nr = new NewRunnable(); //创建自定义类对象
    Thread t = new Thread(nr); //注意这里传入Thread的子类Runnable对象(将其当作参数传递给Thread的构造函数)
    t.start();  //自动调用方法中的run方法 
    for (int i = 0; i < 1000; i++) 
      System.out.println(new Thread().getName() + " - MasterThread-NewRunnable");  //可以直接获取线程的名称
  
  }
}

// (1)继承Thread类

class NewThread extends Thread {
  //2.重写Run方法:将要执行的代码放入run方法之中
  public void run() {
    for (int i = 0; i < 1000; i++)
      System.out.println(this.getName() + " - " + this.getClass());
  }
}


//(2)实现Runnable接口
class NewRunnable implements Runnable {
  //实现接口中方法
  @Override
  public void run() {
    for (int i = 0; i < 1000; i++) 
      System.out.println(i + "-" + this.getClass());
  }
}

/**
 *  #示例1执行结果(节选了比较匀称的地方)
 *  Thread-0 - class com.weiyigeek.Thread.NewThread
	Thread-232 - MasterThread
	Thread-0 - class com.weiyigeek.Thread.NewThread
	Thread-233 - MasterThread
	Thread-0 - class com.weiyigeek.Thread.NewThread
	Thread-234 - MasterThread

	#示例2执行结果(节选了比较匀称的地方)
	833-class com.weiyigeek.Thread.NewRunnable
	Thread-1524 - MasterThread-NewRunnable
	.....................
	Thread-1968 - MasterThread-NewRunnable
	834-class com.weiyigeek.Thread.NewRunnable
 ***/

两种实现方式的区别总结: 1.查看源码的区别:

  • a.继承Thread : 由于子类重写了Thread类的run(), 当调用start()时, 直接找子类的run()方法
  • b.实现Runnable : 构造函数中传入了Runnable的引用, 成员变量记住了它, start()调用run()方法时内部判断成员变量Runnable的引用是否为空, 不为空编译时看的是Runnable的run(),运行时执行的是子类的run()方法

2.两种实现方式优缺点:

  • 继承Thread
    • 好处是:可以直接使用Thread类中的方法,代码简单
    • 弊端是:如果已经有了父类,就不能用这种方法(由于JAVA是单继承的特性)
  • 实现Runnable接口
    • 好处是:即使自己定义的线程类有了父类也没关系,因为有了父类也可以实现接口,而且接口是可以多实现的(扩展性比较好)
    • 弊端是:不能直接使用Thread中的方法需要先获取到Thread类线程对象后,才能得到Thread的方法代码复杂

3.在实际开发中根据业务需求来定,一般先用基础的Thread类如果满足不了就采用Runnable接口;

3.线程匿名内部类

描述:匿名内部类实现线程的两种方式更能方便的实现线程执行程序代码并且更加的简介,它也有两种方法

  • 继承Thread类
  • 实现Runnable接口

基础示例:

package com.weiyigeek.Thread;
public class Demo3_AnonmouseThread {
  public static void main(String[] args) {
    //1.采用匿名内部类直接实现,精简代码但是流程是与实现线程的两种方式大致相同;、
    //方法1:new 类(){}继承这个类
    new Thread() {
      public void run() { //重写run方法
        for (int i = 0; i < 1000; i++) { //将要执行的代码,写在run方法中
          System.out.println(this.getName() + " - Thread Anonymous Inner Class");
        }
      }
    }.start(); //注意调用start()方法开启线程
    
    //方法2:将Runable的子类对象传递给Thread的构造方法(值得注意)
    new Thread(new Runnable() {
      @Override
      public void run() {
        for(int i = 0; i < 1000; i++)
          System.out.println(new Thread().getName() + " - Runnable Anonymous Inner Class");
      }
    }).start(); //同样是开启线程
  }
}

执行结果:

Thread-0 - Thread Anonymous Inner Class
Thread-2 - Runnable Anonymous Inner Class
.....
Thread-1001 - Runnable Anonymous Inner Class
4.线程类常用方法

1.获取线程名字:通过getName()方法获取线程对象的名字,我们前面的代码有所接触 2.设置线程名字:通过构造函数可以传入String类型的名字,还可以通过setName(String)方法可以设置线程对象的名字 3.获取当前线程:当前 currentThread()主线程对象也可以获取,可以使用在Runable接口之中获取当前线程对象就能利用线程的方法了;

基础示例:

package com.weiyigeek.Thread;
public class Demo4_ThreadMethod {
  public static void main(String[] args) {
    //示例1.线程的常用的方法(设置与获取名字)
    demo1();
    //实例2.获取当前正在执行线程对象(可以使用在Runable接口之中获取当前线程对象就能利用线程的方法了)
    new Thread(new Runnable() {
      @Override
      public void run() {
        Thread t = Thread.currentThread();////获取主函数线程的引用,runnable子类接口便可以直接使用Thread类中的方法
        t.setName("Slave-01");  //直接给当前线程对象进行设置线程名称
        System.out.println(t.getName() + "#Runnable接口实现并获取当前线程对象-并调用其Thread类中方法");
      }
    }).start(); //必须开启线程 (重要 重要 Important!)
  }

  public static void demo1() {
    //方式1.通过构造方法进行线程名称赋值
    new Thread("Master") {
      public void run() {
        System.out.println( this.getName() + "#线程执行的代码块!");
      }
    }.start();
    
    //方式2.通过this.setName进行设置线程名称
    new Thread() {
      public void run() {
        this.setName("Master-01");
        System.out.println( this.getName() + "#线程执行的代码块!");
      }
    }.start();
        
    //方式3:通过Thread类的setName方法进行设置线程名称
    Thread th = new Thread() {
      public void run() {
        System.out.println( this.getName() + "#线程执行的代码块!");
      }
    };
    th.setName("Slave");
    th.start();
  }
}

执行结果:

Master-01#线程执行的代码块!
Slave#线程执行的代码块!
Master#线程执行的代码块!
Slave-01#Runnable接口实现并获取当前线程对象-并调用其Thread类中方法
5.线程休眠与守护

描述:在windows上一般采用毫秒级别(不支持纳秒值),但是Linux系统比起Windows更加合适处理纳秒级别的休眠sleep; 线程也需要等待和唤醒线程采用正常继续工作;

基础方法:

  • Thread.sleep(毫秒,纳秒) 控制当前线程休眠若干毫秒1秒= 1000毫秒 1秒 = 1000 1000 1000纳秒 = 1000000000
  • Thread setDaemon(true) 设置一个线程为守护线程,该线程不会单独执行当其他非守护线程都执行结束后,自动退出,注意他会存在时间缓冲当非守护进程执行完毕后线程守护进程不会立即结束(比如QQ:聊天界面传文件)

基础实例:

package com.weiyigeek.Thread;
public class Demo5_SleepThread {
  public static void main(String[] args) throws InterruptedException {
    // 实例1.休眠线程 Thread.sleep(毫秒,纳秒)
    // (1)这里主要以main主线程进行测试
    for(int i = 10; i > 0; i--)
    {
      Thread.sleep(1000); //线程休眠中断1s,由于是暂停需要加入一个中断异常处理
      System.out.println("倒计时"+i+"s");
    }
    // (2)采用Thread创建两个线程进行体现进程休眠
    demo1();
  }

  //实例1:构建方法
  public static void demo1() {
    new Thread("Thread1") {
      public void run() {
        for(int i = 0; i < 10; i++)
        {
          //这里不是抛异常,由于父类在写run方法的时候没有抛异常,所有子类重写也不能抛(自能自己进行处理try...catch)
          try {
            Thread.sleep(1000);
          } catch (InterruptedException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
          } 
          System.out.println(this.getName() + " - " + i);  //输出线程名称查看执行效果
        }
      }
    }.start();
    
    new Thread("Thread2" ) {
      public void run() {
        for(int i = 0; i < 10; i++)
        {
          try {
            Thread.sleep(1000);
          } catch (Exception e) {
            e.printStackTrace();
          }
          System.out.println(this.getName() + " - " + i);
        }
      }
    }.start();
  }

  //基础示例2
  public static void demo2() {
    Thread t1 = new Thread("Master") {
      public void run() {
        for(int i = 0; i < 2;i++)  //这里将非守护线程故意设置成为2次
        {
          try {
            Thread.sleep(100);
          } catch (Exception e) {
            // TODO: handle exception
            e.printStackTrace();
          }
          System.out.println(getName() + " - " + i); //注意这里由于采用了匿名内部类的方法可以直接调用Thread中的方法
        }
      }
    };
    
    //假设是作为守护线程查看效果;
    Thread t2 = new Thread("SetDeamon") {
      public void run() {
        for(int i = 0; i < 50;i++)
        {
          try {
            Thread.sleep(100);
          } catch (Exception e) {
            // TODO: handle exception
            e.printStackTrace();
          }
          System.out.println(getName() + " - " + i); //注意这里由于采用了匿名内部类的方法可以直接调用Thread中的方法
        }
      }
    };
    
    t2.setDaemon(true); //将t1设置为守护线程(当Master线程执行两次结束后setDeamon线程也跟着结束)
    t1.start();
    t2.start();
  }
}

执行结果

#示例1
倒计时2s
倒计时1s
Thread1 - 0
Thread2 - 0
Thread1 - 1
Thread2 - 1
Thread1 - 2
Thread2 - 2
Thread1 - 3

#示例2
Master - 0
SetDeamon - 0
Master - 1
SetDeamon - 1
6.线程添加与优先级

描述:添加线程给当前执行任务的CPU进行插队执行,我们也可以让程序让出CPU给其他线程执行; 还可以设置线程的优先级使其优先执行;

基础方法:

  • join(), 当前线程暂停, 等待指定的线程执行结束后, 当前线程再继续
  • join(int), 可以等待指定的毫秒之后继续
  • Thread.yield() 给调度程序的一个提示,当前线程愿意得到它当前的处理器的使用,调度程序可以自由地忽略这个提示。
  • setPriority()设置线程的优先级范围(1~10)默认值是5,优先执行的线程;

基础示例:

package com.weiyigeek.Thread;
public class Demo7_JointThread {
  public static void main(String[] args) {
    //示例1.线程加入
    demo1();
    
    //示例2.yield让出cpu礼让线程
    MyThread mt1 = new MyThread("MasterYield");
    MyThread mt2 = new MyThread("SlaveYield");
    // mt1.start();
    // mt2.start();
  }
  
  //(1)
  public static void demo1() {
    final Thread th1 = new Thread("Slave-Join") { //由于需要被匿名内部类调用所以这里采用final进行修饰
      public void run() {
        for(int i = 0; i < 20; i++)
        {
          try {
            Thread.sleep(20);
          } catch (Exception e) {
            // TODO: handle exception
            e.printStackTrace();
          }
          System.out.println(getName() + " - " + i);  //需要加入的线程执行输出的语句
        }
      }
    };
    Thread th2 = new Thread("Master") { 
      public void run() {
        for(int i = 0; i < 20; i++)
        {
          try {
            //th1.join();						   //插队,加入(执行完成后才执行Master线程)
            th1.join(30);						//加入,有固定的时间过了固定时间继续交替执行
            Thread.sleep(20);
          } catch (Exception e) {
            // TODO: handle exception
            e.printStackTrace();
          }
          System.out.println(getName() + " - " + i);
        }
      }
    };
    
    //示例3.优先级设置(注意是在start()方法前进行设置)
    th1.setPriority(Thread.MIN_PRIORITY);
    th2.setPriority(Thread.MAX_PRIORITY); //优先执行Master线程
    th1.start();
    th2.start();
  }
}

//(2)
class MyThread extends Thread{
  public MyThread(String name) {
    super(name);
    // TODO Auto-generated constructor stub
  }

  @Override
  public void run() {
    // TODO Auto-generated method stub
    for(int i = 0; i <= 30; i++)
    {
      if(i%2 == 0)  //当可以整除2时候进行让出CPU让其他线程执行
      {
        Thread.yield(); //让出CPU这里就不采用Thread.sleep() 方式
      }
      System.out.println(getName() + " - " + i);
    }
  }
}

/**
*示例1(按照加入的线程时间进行执行)
Slave-Join - 0
Slave-Join - 1
Master - 0  (由于加了setPriority优先执行,但其中又加入其他线程则也同时执行)
Slave-Join - 2
Slave-Join - 3
Master - 1
Slave-Join - 4
Slave-Join - 5
Slave-Join - 6

*示例2(实际上yield效果还是比较明显的)
SlaveYield - 0
MasterYield - 0
SlaveYield - 1
MasterYield - 1
SlaveYield - 2
MasterYield - 2
SlaveYield - 3
MasterYield - 3
SlaveYield - 4
SlaveYield - 5
MasterYield - 4
MasterYield - 5
**/

注意事项:

  • 匿名内部类使用局部变量的时候必须采用final进行修饰的变量才可调用;
  • CPU在随机的切换正在执行的线程如果要让出线程执行时间需要采用yield()方法,但是实际上达不到效果只是理论上可以;
7.线程同步锁对象

描述:什么情况下需要同步?

  • 当多线程并发有多段代码同时执行时, 我们希望某一段代码执行的过程中CPU不要切换到其他线程工作这时就需要同步机制.
  • 如果两段代码是同步的那么同一时间只能执行一段, 在一段代码没执行结束之前, 不会执行另外一段代码.

实现同步互斥的机制方式:

  • 同步代码块:使用synchronized关键字加上一个锁对象来定义一段代码这就叫同步代码块,多个同步代码块如果使用相同的锁对象那么他们就是同步的;
  • 同步方法:使用synchronized关键字修饰一个方法, 该方法中所有的代码都是同步的

基础示例:

package com.weiyigeek.Thread;
public class Demo8_Synchronized {
  public static void main(String[] args) {
    // 示例1.同步代码块与同步方法
    final Printer pp = new Printer();
    new Thread("Sync-0") {
      public void run() {
        while(true){
          //pp.p1();
          //pp.p3();
          pp.p4();
        }
      }
    }.start();
    
    new Thread("Sync-1") {
      public void run() {
        while(true){
          //pp.p2();
          //pp.p31();
          pp.p41();
        }
      }
    }.start();
  }
}

//测试类
class Printer {
  Syn d = new Syn();
  public void p1() {
    //(1)锁对象可以是任意对象,但是被锁的代码需要保证是同一把锁,并且不能用匿名对象
    //注意不能采用匿名内部类synchronized(new)
    synchronized (d) {
      System.out.print("P1");
      System.out.print("Method");
      System.out.println("");
    }
  }
  public void p2() {
    synchronized (d) {
      System.out.print("P2");
      System.out.print("Method");
      System.out.println("");
    }
  }
  //(2)同步放只需要在方法上加synchrnized关键字即可
  //非静态同步函数的锁是:this	
  public synchronized void p3() {
    System.out.print("W");
    System.out.print("e");
    System.out.print("i");
    System.out.println();
  }
  public void p31() {
    synchronized(this) {  //this 这里与出现p3()表示同一把锁所以不出现乱序;
      System.out.print("G");
      System.out.print("e");
      System.out.print("e");
      System.out.print("k");
      System.out.println();
    }
  }
  
  //(3)静态的同步函数的锁是:字节码对象(本类) - 注意静态方法优先于对象存在;
  public static synchronized void p4() {	
    System.out.print("W");
    System.out.print("e");
    System.out.print("i");
    System.out.print("rn");
  }
  public static void p41() {
    synchronized (Printer.class) {  //参数是该类的字节码,与p4()表示同一把锁
    System.out.print("G");
    System.out.print("e");
    System.out.print("e");
    System.out.print("k");
    System.out.print("rn");
    }
    
  }
}

//新建立一个类构建同步锁(空类即可)
class Syn {
  
}

/**
#1.未加入同步锁的情况下
P1Method
P1MethodP2Method

P2Method
#2.加入synchronized()关键字便不会出现这样的问题
 
3.同步方法(静态与非静态)
Geek
Geek
Geek
Geek
**/

注意事项:

  • 同步锁对象不能采用匿名对象,因为匿名对象不是同一个对象;
  • 非静态的同步方法的锁对象是什么? 答:非静态同步函数的锁是this
  • 静态的同步方法的锁对象是什么? 答:静态的同步函数的锁是字节码对象,原因由于静态方法优先于对象存在;
8.线程安全

描述:多线程并发操作同一数据时, 就有可能出现线程安全问题,所以使用同步技术可以解决这种问题, 把操作数据的代码进行同步, 不要多个线程一起操作; 如果需要所有的对象的都共享一个数据,让一个类中变量编程一个静态变量;否则每个线程对象都将执行run中代码并且类中变量是独立且不影响得;

死锁:多线程同步的时候, 如果同步代码嵌套使用相同锁就有可能出现死锁,所以尽量不要嵌套使用

#一定不要出现同步代码块 嵌套
synchronized(){
  ...
  synchronized(){
    ...
  }
}

基础示例:

package com.weiyigeek.Thread;
public class Demo9_SyncTest {
  public static void main(String[] args) {
    //示例1.利用Thread实现火车站购票(一共30张,4个窗口同时卖)四条线程;
//		new Tickets().start(); //注意每一个对象都有自己的成员变量,我们可以采用static来共享成员变量并且利用同步锁对象来进防止线程错误发生
//		new Tickets().start();
//		new Tickets().start();
//		new Tickets().start();

    //示例2.只创建了一次对象所以run中采用synchronized同步锁并且传入this指向我们的那一个Thread对象
    NewTickets nr = new NewTickets(); 
    new Thread(nr).start();
    new Thread(nr).start();
    new Thread(nr).start();
    new Thread(nr).start();
  }
}

//车站售卖票类
class Tickets extends Thread{
  private static int ticket = 30; //让四个售卖员一起买这三十张票,为了让线程操作同一个数据不发生错误采用同步代码块
  private static Object obj = new Object(); //创建一个静态的Obj对象可作为同步锁;(如果用引用数据类型成员变量当做锁对象则必须是静态的)
  public void run() {
      //当票没有买完一直进行售票状态
      while(true)
      {
        //同步锁(为了共同操作30同一个synchronized(obj),不过建议采用下面的方式
        synchronized (Tickets.class) {
          if(ticket <= 0)
            break;
          try {
            Thread.sleep(10);  //线程1睡,线程2睡,线程3睡,线程4睡(随机唤醒线程)
          } catch (Exception e) {
            // TODO: handle exception
            e.printStackTrace();
          }
          System.out.println( getName()+ " | 卖出第"  + ticket-- + "张票");
        }
      }
  }
}

//采用runnable接口实现售卖车票
class NewTickets implements Runnable{
  private static int ticket = 30;
  private static Object obj = new Object(); //锁对象:非常注意是静态的哟!
  @Override
  public void run() {
    while(true)
    {
      //同步锁,防止线程操作同一个变量时候出错; 锁对象也可以是NewTickets.class
      synchronized (obj) {
        if(ticket <= 0)
          break;
        
        try {
          Thread.sleep(10);
        } catch (Exception e) {
          // TODO: handle exception
          e.printStackTrace();
        }

        //注意这里是Thread子类接口实现不能直接调用其内部方法,需要先获取当前执行线程.Thread类方法
        System.out.println( Thread.currentThread().getName()+ " | 卖出第"  + ticket-- + "张票");
      }
    }
  }
}

执行结果:

# Thread-0 | 卖出第30张票
# Thread-0 | 卖出第29张票
# Thread-0 | 卖出第28张票
# Thread-0 | 卖出第27张票
# Thread-0 | 卖出第26张票
# Thread-0 | 卖出第25张票
# Thread-3 | 卖出第24张票
# Thread-3 | 卖出第23张票
# Thread-3 | 卖出第22张票
# Thread-3 | 卖出第21张票
# Thread-3 | 卖出第20张票
# Thread-3 | 卖出第19张票
# Thread-3 | 卖出第18张票
# Thread-3 | 卖出第17张票
# Thread-3 | 卖出第16张票
# Thread-3 | 卖出第15张票
# Thread-3 | 卖出第14张票
# Thread-3 | 卖出第13张票
# Thread-3 | 卖出第12张票
# Thread-3 | 卖出第11张票
# Thread-1 | 卖出第10张票
# Thread-1 | 卖出第9张票
# Thread-1 | 卖出第8张票
# Thread-1 | 卖出第7张票
# Thread-1 | 卖出第6张票
# Thread-1 | 卖出第5张票
# Thread-1 | 卖出第4张票
# Thread-1 | 卖出第3张票
# Thread-1 | 卖出第2张票
# Thread-1 | 卖出第1张票

示例2:死锁代码(哲学家就餐问题-在操作系统同步互斥中学习过)

private static String s1 = "筷子左";
private static String s2 = "筷子右";
public static void main(String[] args) {
  new Thread() {
  public void run() {
    while(true) {
      synchronized(s1) {
        System.out.println(getName() + "...拿到" + s1 + "等待" + s2);
        synchronized(s2) {
          System.out.println(getName() + "...拿到" + s2 + "开吃");
        }
      }
    }
  }
  }.start();

  new Thread() {
  public void run() {
    while(true) {
    synchronized(s2) {
      System.out.println(getName() + "...拿到" + s2 + "等待" + s1);
      synchronized(s1) {
        System.out.println(getName() + "...拿到" + s1 + "开吃");
        }
      }
    }
  }
  }.start();
}

学习多线程以前的线程安全的类问题,如何判断线程是安全的? 答:通过ctrl+shift+t进行查找下面类是否使用了同步锁sychronized来修饰方法即(后面ctrl+o搜索具体的方法)看源码Vector,StringBuffer,Hashtable,,可判断线程是不是安全的;

//线程安全的
public synchronized void addElement(E obj) {
    modCount++;
    ensureCapacityHelper(elementCount + 1);
    elementData[elementCount++] = obj;
}

学习总结:

  • Vector是线程安全的,ArrayList是线程不安全的
  • StringBuffer是线程安全的,StringBuilder是线程不安全的
  • Hashtable是线程安全的,HashMap是线程不安全的
  • Collections.synchroinzed(xxx) 支持线程不安全的列表集合变成线程安全的;
static <T> Collection<T> synchronizedCollection(Collection<T> c)  //返回由指定集合支持的同步(线程安全)集合。  
static <T> List<T> synchronizedList(List<T> list)  //返回由指定列表支持的同步(线程安全)列表。  
static <K,V> Map<K,V> synchronizedMap(Map<K,V> m) //返回由指定的Map支持的同步(线程安全)Map。  
static <K,V> NavigableMap<K,V> synchronizedNavigableMap(NavigableMap<K,V> m) //返回指定的导航Map支持的同步(线程安全)导航Map。  
static <T> NavigableSet<T> synchronizedNavigableSet(NavigableSet<T> s) //返回由指定的导航集支持的同步(线程安全)导航集。  
static <T> Set<T> synchronizedSet(Set<T> s) //返回一个由指定集合支持的同步(线程安全)集。  
static <K,V> SortedMap<K,V> synchronizedSortedMap(SortedMap<K,V> m) //返回一个由指定的排序映射支持的同步(线程安全)排序的Map。  
static <T> SortedSet<T> synchronizedSortedSet(SortedSet<T> s) //返回一个由指定的排序集支持的同步(线程安全)排序集。

多线程进阶

单例设计模式

单例设计模式思想:保证类在内存中只有一个对象,方便让大家指向同一个对象。

如何保证类在内存中只有一个对象呢?

  • (1)控制类的创建,不让其他类来创建本类的对象:private构造方法;
  • (2)在本类中定义一个本类的对象:Singleton s;
  • (3)提供公共的访问方式本类对象: public static Singleton getInstance(){return s};
  • (4)单例写法有三种,在基础示例中进行体现;

基础示例:

package com.weiyigeek.Thread;
public class Demo11_Singleton {
  public static void main(String[] args) {
    // 实现单例设计模式的几种方法
    // 1.饿汉式
    Singleton s1 = Singleton.getInstance(); 
    Singleton s2 = Singleton.getInstance();
    if(s1 == s2) {
      s1.print();
    }
    
    // 2.懒汉式
    Singleton1 sl1 = Singleton1.getInstance(); 
    Singleton1 sl2 = Singleton1.getInstance();
    if(sl1 == sl2) {
      sl1.print();
    }
    
    // 3.无名式
    Singleton2 slt1 = Singleton2.s;
    Singleton2 slt2 = Singleton2.s;
    System.out.println(slt1 == slt2);
  }
}

//方式1:饿汉式开发建议使用这种方式。
class Singleton {
  //1,私有构造函数
  private Singleton(){}
  //2,创建本类对象
  private static Singleton s = new Singleton();
  //3,对外提供公共的访问方法
  public static Singleton getInstance() {
    return s;
  }
  public static void print() {
    System.out.println("方法1");
  }
}


//方式2:懒汉式面试时候建议使用
class Singleton1 {
  //1,私有构造函数
  private Singleton1() {};
  //2,创建本类对象(但不对其进行赋值)
  private static Singleton1 s;
  //3,对外提供公共的访问方法
  public static Singleton1 getInstance() {
    if(s == null)
      //线程1,线程2(多线程的时候会导致安全问题,所以面试的时候不建议使用)
      s = new Singleton1();
    return s;
  }
  public static void print() {
    System.out.println("方式2");
  }
}

//方式3:暂无名称
class Singleton2 {
  //第一步.都是构造私有函数
  private Singleton2() {};
  //第二步.集其精华之所在
  public static final Singleton2 s = new Singleton2();
}

//执行结果
// 方法1
// 方式2
// true

Runtime类 描述:Runtime类是一个单例类并且允许应用程序与环境中运行应用程序接口的一个实例,以后在写命令执行的shell的时候非常有用;

java.lang.Object 
java.lang.Runtime 

//类定义
public class Runtime extends Object

//类方法
static Runtime getRuntime()  //返回与当前应用程序相关的java运行时对象。 
Process exec(String command) //在一个单独的进程中执行指定的字符串命令。

基础示例:

package com.weiyigeek.Thread;
import java.io.IOException;
public class Demo10_Runtime {
  public static void main(String[] args) throws IOException, InterruptedException{
    //单线程类学习 Runtime
    Runtime r = Runtime.getRuntime(); //注意由于Runtime类是单实例的所以用其静态公共的方法获取Runtime内部实例化对象
    //演示操作同一个对象:命令执行关机命令和取消关机
    System.out.println("正在执行关机命令!");
    r.exec("shutdown -s -t 300");  //300s 关机
    Thread.sleep(5000);  //主线程 休眠 5s
    r.exec("shutdown -a");
    System.out.println("已经取消关机!");
  }
}

执行结果:

正在执行关机命令!
已经取消关机!

注意事项: 1.饿汉式与懒汉式之间的区别:

  • 饿汉式:不管怎么样运行时候先建立一个对象,在实际开发中使用因为在多线程编程中它不会创建多个对象,它以空间换取时间;
  • 懒汉式:在调用静态方法的时候需要进行判断然后创建对象,但是在开发中不建议使用,因为在多线程开发时候会出现问题导致创建多个对象,它以时间换空间;
Timer定时器

描述:简单的说定时器就是指定时间执行指定的某一任务;任务可能被安排指定时间为一次性执行,或定期重复执行。

语法:

java.lang.Object 
java.util.Timer 

//类声明
public class Timer extends Object

//类方法
void cancel() //终止此计时器,丢弃任何当前计划的任务。  
int purge() //从这个计时器的任务队列中移除所有已取消的任务。  
void schedule(TimerTask task, Date time) //在指定的时间计划执行指定的任务。  
void schedule(TimerTask task, Date firstTime, long period) //计划重复固定延迟执行指定的任务,开始在指定的时间。  

//由它的子类实现由定时器一次性或重复执行的任务。 (具体的要执行的任务-它是抽象类)
public abstract class TimerTask extends Object implements Runnable

基础示例:

package com.weiyigeek.Thread;
import java.util.Date;
import java.util.Timer;
import java.util.TimerTask;
public class Demo_TimerTask {
  public static void main(String[] args) throws InterruptedException {
    //定时器timer的实现演示
    Timer t = new Timer(); //定时器对象
    t.schedule(new NewTask(), new Date(2019-1900,9,19,11,21,30),3000); //定时器任务 参数一传入TimerTask对象 3s重复一次
    while(true){
      Thread.sleep(1000);  //f2-添加一次中断
      System.out.println(new Date());
    }
  }
}
//新建立一个类继承timer的子类来自定要指定的任务(实际上继承了Runnable实现多线程)
class NewTask extends TimerTask {
  @Override
  public void run() {
    System.out.println("正在执行任务!");
  }
}

执行结果:

Sat Oct 19 11:21:29 CST 2019
正在执行任务!
Sat Oct 19 11:21:30 CST 2019
Sat Oct 19 11:21:31 CST 2019
Sat Oct 19 11:21:32 CST 2019
正在执行任务!
Sat Oct 19 11:21:33 CST 2019
Sat Oct 19 11:21:34 CST 2019
Sat Oct 19 11:21:35 CST 2019
正在执行任务!
进程间通信

1.什么时候需要通信?

  • 多个线程并发执行时, 在默认情况下CPU是随机切换线程的
  • 如果我们希望他们有规律的执行, 就可以使用通信, 例如每个线程执行一次打印

2.怎么通信?

  • 如果希望线程等待, 就调用wait()
  • 如果希望唤醒等待的线程, 就调用notify();
  • 这两个方法必须在同步代码中执行, 并且使用同步锁对象来调用

语法方法:

//类:Object类是类层次结构的根。每个类都有 Object作为超类。所有对象包括数组实现这个类的方法。 
java.lang.object

//类中的方法
//等待:
void wait()  ///使当前线程等待另一个线程调用此对象的方法或 notify() notifyAll()方法。  
void wait(long timeout) //使当前线程等待另一个线程调用此对象的方法或 notify() notifyAll()方法,或一个指定的时间流逝。  
void wait(long timeout, int nanos)  //使当前线程等待另一个线程调用此对象的方法或 notify() notifyAll()方法,或者其他某个线程中断当前线程,或一定量的实际时间已经过去了。  

//唤醒
nofify(); 方法是随机唤醒一个线程
notifyAll()方法是唤醒所有线程

多个线程通信的问题(三个或三个以上间的线程通信):

  • JDK5之前无法唤醒指定的一个线程(而是随机唤醒):多个线程之间通信,需要使用notifyAll()通知所有线程,用while来反复判断条件;
    • 简单的说假如有三个保安,第一个保安夜班结束了但它不知道剩下的两个保安谁值班,他就将两个保安都唤醒,其中一个保安说今天该我上班,而剩下的那个还是继续回到床上睡觉; (所以对线程程序来说他不知道谁满足条件她就把所有的线程都唤醒)
  • 在JDK1.5之后有更改的解决方案互斥锁(后面描述)

基础示例:

package com.weiyigeek.Thread;
public class Demo12_Waitnotify {
  public static void main(String[] args) {
    //JDK5之前无法唤醒指定的一个线程(而是随机唤醒),多个线程之间通信需要使用notifyAll()通知所有线程,用while来反复判断条件;
    //三个线程通信实例:
    final Waitnofity wn = new Waitnofity(); //注意必须采用final修饰才能在匿名内部类中使用
    //线程1
    new Thread() {
      public void run() {
        //匿名内部类中的子类自能自己处理异常
        while(true) {
          try {
            wn.print1();
          } catch (InterruptedException e) {
            e.printStackTrace();
          }
        }
      }
    }.start();
    
    //线程2
    new Thread() {
      public void run() {
        //匿名内部类中的子类自能自己处理异常
        while(true){
          try {
            wn.print2();
          } catch (InterruptedException e) {
            e.printStackTrace();
          }
        }
      }
    }.start();
    
    //线程3
    new Thread() {
      public void run() {
        while(true) {
          //匿名内部类中的子类自能自己处理异常
          try {
            wn.print3();
          } catch (InterruptedException e) {
            e.printStackTrace();
          }
        }
      }
    }.start();
  }
}


//声明一个类3个方法一个属性;
class Waitnofity {
  private int flag = 1;
  public void print1() throws InterruptedException {
    //同步锁对象
    synchronized (this) {
      while(flag != 1){ //线程1在次等待,while()循环是循环判断每次都会判断标记
        this.wait();
      }
      System.out.print(Thread.currentThread().getName() + " - Print1()");
      System.out.println(" ");
      flag=2;
      this.notifyAll(); // 唤醒所有的线程(但还是随机选项线程,当满足条件就行执行)
    }
  }
  
  public void print2() throws InterruptedException {
    synchronized(this) {
      //如果采用if它会在哪里等待哪里就起来
      while(flag != 2){ //线程2在次等待
        this.wait();
      }
      System.out.print(Thread.currentThread().getName() + " - Print2()");
      System.out.println(" ");
      flag=3;
      this.notifyAll(); // 唤醒所有的线程
    }
  }
  
  public void print3() throws InterruptedException {
    synchronized(this) {
      while(flag != 3){ //线程3在次等待记
        this.wait();
      }
      System.out.print(Thread.currentThread().getName() + " - Print3()");
      System.out.println(" ");
      flag=1;
      this.notifyAll(); // 唤醒所有的线程
    }
  }
}

//执行结果按照顺序执行;
Thread-0 - Print1() 
Thread-1 - Print2() 
Thread-2 - Print3() 
Thread-0 - Print1() 
Thread-1 - Print2() 
Thread-2 - Print3() 
Thread-0 - Print1()

学习总结: 1,在同步代码块中用哪个对象锁就用哪个对象调用wait方法

2,为什么wait方法和notify方法定义在Object这类中?

  • 答:因为锁对象可以是任意对象,Object是所有的类的基类,所以wait方法和notify方法需要定义在Object这个类中

3,sleep方法和wait方法的区别? 区别1:

  • sleep方法必须传入参数,参数就是时间时间到了自动醒来,
  • wait方法可以传入参数也可以不传入参数,传入参数就是在参数的时间结束后等待不传入参数就是直接等待

区别2:

  • sleep方法在同步函数或同步代码块中,不释放锁 - 睡着了也抱着锁睡(他不能被唤醒)
  • wait方法在同步函数或者同步代码块中释放锁 - 如果不释放锁,线程产生等待CPU也会一直在该段程序中耗着不能执行其他任务;
互斥锁

描述:学过操作系统或者信号和通信的人应该了解过互斥锁(不多讲自己百度在信号同步那一章节的), 它是JDK1.5的新特性;

  • 1.同步: 使用ReentrantLock类的lock() 获取锁和unlock()释放锁方法进行同步
  • 2.通信
    • 使用ReentrantLock类的newCondition()方法可以获取Condition对象
    • 需要等待的时候使用Condition的await()方法, 唤醒的时候用signal()方法
    • 不同的线程使用不同的Condition, 这样就能区分唤醒的时候找哪个线程了

基础语法:

//一个可重入的互斥锁 Lock,具有与synchronized方法和语义为隐式监控相同的基本行和语义使用,但扩展功能更加强大;
public class ReentrantLock
extends Object
implements Lock, Serializable

//常用方法:
void lock()  //获取锁。 
void unlock() //试图释放这个锁。  

//在 Lock取代 synchronized方法和语句的使用,一个 Condition取代对象监视器的使进行唤醒和等待
Condition newCondition() //返回一个用于这 Lock 的Condition实例。
void await()  //使当前线程等待它暗示或 interrupted。  
void signal() ///唤醒一个等待线程。  
void signalAll() //唤醒所有等待线程。

基础实例:

package com.weiyigeek.Thread;

import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.ReentrantLock;

public class Demo13_ReentranLock {
  public static void main(String[] args) {
    // (1)1.5新特性互斥锁实现:不需要将每个线程都唤醒,只是唤醒指定线程;
    final Reentran rt = new Reentran();
    new Thread() {
      @Override
      public void run() {
        //线程1中执行的代码
        while(true)
        {
          try {
            rt.p1();
          } catch (InterruptedException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
          }
        }
      }
    }.start();
    new Thread() {
      @Override
      public void run() {
        //线程2中执行的代码
        while(true) {
          try {
            rt.p2();
          } catch (InterruptedException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
          }
        }
      }
    }.start();
    new Thread() {
      @Override
      public void run() {
        //线3中执行的代码
        while(true){
          try {
            rt.p3();
          } catch (InterruptedException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
          }
        }
      }
    }.start();
  }
}

class Reentran {
  private ReentrantLock r = new ReentrantLock(); //互斥锁对象
  //创建三个监听器
  private Condition c1 = r.newCondition(); //返回Condition对象
  private Condition c2 = r.newCondition(); //返回Condition对象
  private Condition c3 = r.newCondition(); //返回Condition对象
  private int flag = 1;
  public void p1() throws InterruptedException {
    r.lock();//获取锁->这里替换了synchronzied同步锁
    if(flag != 1)
      c1.await();  //使线程1等待
    System.out.println("Wei");
    flag = 2;  //灵魂之所在
    c2.signal(); //唤醒线程2
    r.unlock(); //释放锁
    
  }
  public void p2() throws InterruptedException {
    r.lock();//获取锁->这里替换了synchronzied同步锁
    if(flag != 2)
      c2.await();  //使线程2等待
    System.out.println("Geek");
    flag = 3;  //灵魂之所在
    c3.signal(); //唤醒线程3
    r.unlock(); //释放锁
  }
  public void p3() throws InterruptedException {
    r.lock();//获取锁->这里替换了synchronzied同步锁
    if(flag != 3)
      c3.await();  //使线程3等待
    System.out.println("Hacker");
    flag = 1;  //灵魂之所在
    c1.signal(); //唤醒线程1
    r.unlock(); //释放锁
  }
}

/**
执行结果:
Wei
Geek
Hacker
Wei
Geek
Hacker
***/
线程组

描述:为什么要存在组? 答:那是为了方便管理和维护,同样JAVA中线程中也有自己的组;

线程组概述: Java中使用ThreadGroup来表示线程组,它可以对一批线程进行分类管理,Java允许程序直接对线程组进行控制。默认情况下所有的线程都属于主线程组

基础语法:

// ThreadGroup类:一个线程组表示一组线程。
// 此外一个线程组还可以包括其他线程组。线程组形成一个树,其中每一个线程组除了初始线程组有一个父。 
// 允许一个线程访问它自己的线程组的信息,但不允许访问它的线程组的父线程组或其他任何线程组的信息。
public class ThreadGroup
extends Object
implements Thread.UncaughtExceptionHandler

//构造方法
ThreadGroup(String name) //构建一个新的线程组,并给其赋值名字 
Thread(ThreadGroup?group, Runnable?target, String?name) //设置整组的优先级或者守护线程

//常用方法:
ThreadGroup getThreadGroup() //通过线程对象获取他所属于的组
String getName() //通过线程组对象获取他组的名字
ThreadGroup getParent() //返回这个线程组的父。  
String toString() //返回这个线程组的字符串表示形式

线程的生命周期:

  • 新建:创建线程;
  • 就绪:线程对象已经启动了,但是还没回去到CPU执行权;
  • 运行:获取到了CPU的执行权;
  • 阻塞:没有CPU的执行权回到就绪;
  • 死亡:代码运行完毕线程消亡;

基础示例:

package com.weiyigeek.Thread;
public class Demo13_ThreadGroup {
  public static void main(String[] args) {
    //1.线程组的使用案例
    // 线程默认情况下属于main线程组
    // 通过下面的测试,你应该能够看到,默任情况下,所有的线程都属于同一个组
    System.out.println("默认线程组: "+Thread.currentThread().getThreadGroup().getName());
        
    // 声明使用线程对象,创建Runable的子类对象;
    Mrunnable mr = new Mrunnable();
    
    //创建两个线程
    Thread t1 = new Thread(mr,"One");
    Thread t2 = new Thread(mr,"Two");
    
    ThreadGroup tg1 = t1.getThreadGroup();
    ThreadGroup tg2 = t2.getThreadGroup();
    
    System.out.println("+ 默认都是主线程组:" + tg1.getName()); //输出线程组名称
    System.out.println("+ 默认都是主线程组:" + tg2.getName());
    
    //设置创建新的线程组名称
    ThreadGroup tg = new ThreadGroup("NewThreadGroup");
    Thread t3 = new Thread(tg,mr,"Group");
    ThreadGroup tg3 =  t3.getThreadGroup(); //获取进程组对象
    System.out.println("+ 设置的线程组:" + tg3.getName());
    
    //通过组名称设置后台线程,表示该组的线程都是后台线程
    tg.setDaemon(true);
  }
}

//线程对象类 (ctrl+1 => 错误提示添加)
class Mrunnable implements Runnable {
  @Override
  public void run() {
    for(int i = 0; i < 1000; i++) {
      System.out.println(Thread.currentThread().getName() + "...." + i);
    }
  }
}

执行结果:

默认线程组: main
+ 默认都是主线程组:main
+ 默认都是主线程组:main
+ 设置的线程组:NewThreadGroup
线程池

概述:程序启动一个新线程成本是比较高的(经过五种状态),因为它涉及到要与操作系统进行交互;而使用线程池可以很好的提高性能,尤其是当程序中要创建大量生存期很短的线程时,更应该考虑使用线程池。 线程池里的每一个线程代码结束后,并不会死亡,而是再次回到线程池中成为空闲状态,等待下一个对象来使用。

  • 在JDK5之前我们必须手动实现自己的线程池
  • 从JDK5开始Java内置支持线程池

内置线程池的使用JDK5新增了一个Executors工厂类来产生线程池,有如下几个方法:

//Executors工程类的使用
public class Executors
extends Object

* public static ExecutorService newFixedThreadPool(int nThreads)  //线程池中可存放线程的数量使用固定数量的线程操作了共享无界队列。 
* public static ExecutorService newSingleThreadExecutor() //创建一个执行器,使用一个单一的工作线程操作关闭一个无限的队列。

这些方法的返回值是ExecutorService对象,该对象表示一个线程池可以执行Runnable对象或者Callable对象代表的线程。

它提供了如下方法:

//管理终端和方法可以用于跟踪一个或多个异步任务的进展产生 Future提供方法。 
public interface ExecutorService extends Executor

//常用方法:
void shutdown()  //启动一个有序的关机,在以前提交的任务被执行,但没有新的任务将被接受。  
<T> Future<T> submit(Callable<T> task)  //提交一个值返回任务执行,并返回一个表示任务挂起结果的未来。  
Future<?> submit(Runnable task)  //提交执行一个Runnable任务并返回一个表示该任务的未来。  
<T> Future<T> submit(Runnable task, T result)  //提交执行一个Runnable任务并返回一个表示该任务的未来。

基础示例:多线程程序实现

// 使用步骤:
// * 创建线程池对象 ExecutorService  Pool
// * 创建Runnable实例
// * 提交Runnable实例
// * 关闭线程池
package com.weiyigeek.Thread;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
public class Demo14_Executor {
  public static void main(String[] args) throws InterruptedException, ExecutionException {
    //(1) 线程池的使用
    ExecutorService pool =  Executors.newFixedThreadPool(2); //创建线程池对象
    //提交执行的线程(可以执行Runnable对象或者Callable对象代表的线程)
    pool.submit(new Mrunnable()); //线程一放入池中执行
    pool.submit(new Mrunnable()); //线程二
    //避免重复执行
    pool.shutdown(); //关闭线程
    
    //(2)多线程程序实现(实现线程方法3)
    ExecutorService pool1 =  Executors.newFixedThreadPool(2); //创建线程池对象
    Future<Integer> f1 = pool1.submit(new MyCallable(100)); //将线程放入池中执行并且返回其值
    Future<Integer> f2 = pool1.submit(new MyCallable(50));
    System.out.println("前100之和:" + f1.get()); //采用get方法获取其值(需要抛出异常)
    System.out.println("前50之和:" + f2.get());
    pool1.shutdown();
  }
}

//实现线程的第三种方式Callable(笔试中常常会问到)
class MyCallable implements Callable<Integer>{
  private int num;
  //构造方法传入要求前num的之综合
  public MyCallable(int num) {
    this.num = num;
  }
  @Override
  //call方法相比于run方法可以抛出异常以及有返回值;
  public Integer call() throws Exception {
    int sum = 0;
    while(num >= 0){
      sum += num--;
    }
    return sum;
  }
}

//执行结果:
//	pool-1-thread-1....0
//	pool-1-thread-1....1
//	pool-1-thread-1....2
//	pool-1-thread-2....0
//	pool-1-thread-1....3
//	pool-1-thread-1....4
//	pool-1-thread-1....5

//前100之和:5050
//前50之和:1275

总结:多线程程序实现的方式3的好处和弊端 好处:

  • 可以有返回值
  • 可以抛出异常

弊端:

  • 代码比较复杂,所以一般不用