第六章、Java基础语法----多线程

第六章、Java基础语法----多线程

本人也是刚入门Java语言,可能会有一些地方出现错误,描述的不对。如果发现不对的地方请及时指出,好对其进行修改。这样不仅可以让我学到东西,也可以让其他刚入门的朋友学习更正确的内容。

所有内容仅供参考。不具有完全的准确性!

注:关于Java的所有内容都会参考到尚硅谷在网上公开的学习视频及其提供的PPT

推荐:https://blog.csdn.net/kwame211/article/details/78963044

一、基本概念:程序、进程、线程

程序(program)是为完成特定任务、用某种语言编写的一组指令的集合。即指一段静态的代码,静态对象。

进程(process)是程序的一次执行过程,或是正在运行的一个程序。是一个动态的过程:有它自身的产生、存在和消亡的过程。——生命周期
如:运行中的QQ,运行中的MP3播放器
程序是静态的,进程是动态的(程序运行起来以后就成了进程)
进程作为资源分配的单位,系统在运行时会为每个进程分配不同的内存区域

线程(thread),进程可进一步细化为线程,是一个程序内部的一条执行路径。
① 若一个进程同一时间并行执行多个线程,就是支持多线程的
② 线程作为调度和执行的单位,每个线程拥有独立的运行栈和程序计数器(pc),线程切换的开
销小
③ 一个进程中的多个线程共享相同的内存单元/内存地址空间它们从同一堆中分配对象,可以
访问相同的变量和对象。这就使得线程间通信更简便、高效。但多个线程操作共享的系统资
源可能就会带来安全的隐患。

并行与并发
并行:多个CPU同时执行多个任务。比如:多个人同时做不同的事。
并发:一个CPU(采用时间片)同时执行多个任务。比如:秒杀、多个人做同一件事。

二、线程的使用
(一)线程的创建和启动

  • JVM允许程序运行多个线程,它通过java.lang.Thread类来体现。

  • Thread类的特性

    1. 每个线程都是通过某个特定Thread对象的run()方法来完成操作的,经常把run()方法的主体称为线程体
    2. 通过该Thread对象的start()方法来启动这个线程,而非直接调用run()
      在这里插入图片描述
  • 多线程的创建
    方式一:继承Thread类
    1. 创建出一个继承于Thread类的子类
    2. 重写Thread中的run()方法 -->将此线程执行的操作声明在run()中
    3. 创建Thread类的子类的对象
    4. 通过此对象调用start()方法

例:

//1. 创建出一个继承于Thread类的子类
class MyThread extends Thread{

    //2. 重写Thread中的run()方法
    @Override
    public void run() {
        for (int i = 0; i < 100; i++) {
            if(i % 2 == 0){
                System.out.println(i);
            }
        }
    }
}

//对上边线程的使用
public class ThreadTest {
    public static void main(String[] args) {
//      3. 创建Thread类的子类的对象
        MyThread t1 = new MyThread();

//      4. 通过此对象调用start()方法: ① 启动当前线程  ② 调用当前线程的run()
        t1.start();

        //若直接通过引用掉run()方法,则run的操作不属于新开辟的线程,而是在main线程中执行的

        //以下操作仍是在main()线程中执行的
        //程序入口的main方法也属于线程,为程序的主线程
        System.out.println("hello");
    }
}

注意:

  1. 如果自己手动调用run()方法(直接通过引用调run()方法),那么就只是普通方法,没有启动多线程模式。
  2. run()方法由JVM调用,什么时候调用,执行的过程控制都有操作系统的CPU调度决定。
  3. 想要启动多线程,必须通过调用start方法
  4. 一个线程对象只能调用一次start()方法启动,如果重复调用了,则将抛出以上的异常“IllegalThreadStateException”。

方式二:实现Runnable接口

  1. 定义子类,实现Runnable接口。
  2. 子类中重写Runnable接口中的run方法
  3. 通过Thread类含参构造器创建线程对象。
  4. 将Runnable接口的子类对象作为实际参数传递给Thread类的构造器中。
  5. 调用Thread类的start方法:开启线程,调用Runnable子类接口的run方法。

例:

// 1. 创建一个实现了Runnable接口的类
class MyRunnable implements Runnable{

    private int ticket = 100;

// 2. 实现Runnable中的抽象方法:run()
    @Override
    public void run() {
        while(true){
            if(ticket > 0){
                System.out.println(Thread.currentThread().getName() + ":" + ticket);
                ticket--;
            }else{
                break;
            }
        }
    }
}
public class RunnableThreadTest {
    public static void main(String[] args) {
//      3. 创建实现类的对象
        MyRunnable myRun1 = new MyRunnable();
//      4. 将此对象作为参数传递到Thread类的构造器中,创建 Thread类的对象
		Thread t = new Thread(myRun1);
//      5. 通过Thread类的对象调用start()方法
		t.start();
		//通过匿名对象调用start(),将 4 和 5 和为一步
        new Thread(myRun1).start();

    }
}

两种方式的区别和联系:

  • 区别:
    继承Thread:线程代码存放Thread子类run方法中。
    实现Runnable:线程代码存在接口的子类的run方法。

  • 开发中:优先选择:实现Runnable接口的方式
    原因:

    1. 实现的方式避免了类的单继承性的局限性
    2. 实现的方式更适合处理多个线程共享数据的情况
  • 联系:
    public class Thread implements Runnable --Thread类也实现了Runnable接口
    相同点:

    1. 都需要实现run()方法,将线程要执行的逻辑写在run()中
    2. 都需要通过Thread类的对象调用start()方法执行
  • 何时需要多线程

    1. 程序需要同时执行两个或多个任务。
    2. 程序需要实现一些需要等待的任务时,如用户输入、文件读写操作、网络操作、搜索等。
    3. 需要一些后台运行的程序时。
  • 多线程程序的优点:

    1. 提高应用程序的响应。对图形化界面更有意义,可增强用户体验。
    2. 提高计算机系统CPU的利用率
    3. 改善程序结构。将既长又复杂的进程分为多个线程,独立运行,利于理解和修改

三、Thread类中的方法
既然都需要通过Thread类的对象运行,那Thread类中的方法就可以供我们使用。
常用的方法:

  1. void start(): 启动线程,并执行对象的run()方法
  2. run(): 线程在被调度时执行的操作
  3. String getName(): 返回线程的名称
  4. void setName(String name):设置该线程名称
  5. static Thread currentThread(): 返回当前线程。在Thread子类中就是this,通常用于主线程和Runnable实现类
  6. static void yield():线程让步
    暂停当前正在执行的线程,把执行机会让给优先级相同或更高的线程
    若队列中没有同优先级的线程,忽略此方法
  7. join() :当某个程序执行流中调用其他线程的 join() 方法时,调用线程将被阻塞,直到 join() 方法加入的 join 线程执行完为止。
    低优先级的线程也可以获得执行
  8. static void sleep(long millis):(指定时间:毫秒)
    令当前活动线程在指定时间段内放弃对CPU控制,使其他线程有机会被执行,时间到后重排队。
    抛出InterruptedException异常
  9. stop(): 强制线程生命期结束,不推荐使用
  10. boolean isAlive():返回boolean,判断线程是否还活着
  • 关于线程的优先级:
    Java在Thread类中定义了三个常用级别:
	public static final int MIN_PRIORITY = 1;
    /**
     * The default priority that is assigned to a thread.
     */
    public static final int NORM_PRIORITY = 5;

    /**
     * The maximum priority that a thread can have.
     */
    public static final int MAX_PRIORITY = 10;

通过这个我们可以知道,线程的最高级别是10级,最低是1级。
涉及优先级的方法:
getPriority() :返回线程优先值
setPriority(int newPriority) :改变线程的优先级

class HelloThread extends Thread{
    @Override
    public void run() {
        for (int i = 0; i < 100; i++) {
            if(i % 2 == 0){
            	System.out.println(i);
            }
        }

    }
    public HelloThread(String name){
        super(name);
    }
}

public class ThreadMethodTest {
    public static void main(String[] args) {
        HelloThread h1 = new HelloThread("Thread:1");
//        h1.setName("线程一");
        //设置分线程的优先级
        h1.setPriority(Thread.MAX_PRIORITY);
        h1.start();

        //给主线程命名
        Thread.currentThread().setName("主线程");
      	//给主线程设置优先级
        Thread.currentThread().setPriority(Thread.MIN_PRIORITY);
    }
}

因为Thread类中定义的优先级是静态的,我们可以直接通过类名调用。如果不想使用已定的级别,我们也可以直接替换成整型值(1-10);

  • 说明
    1. 线程创建时继承父线程的优先级
    2. 低优先级只是获得调度的概率低,并非一定是在高优先级线程之后才被调用

四、线程的生命周期

JDK中用Thread.State类定义了线程的几种状态

一个完整的生命周期中通常要经历如下的五种状态

新建: 当一个Thread类或其子类的对象被声明并创建时,新生的线程对象处于新建状态
就绪:处于新建状态的线程被start()后,将进入线程队列等待CPU时间片,此时它已具备了运行的条件,只是没分配到CPU资源
运行:当就绪的线程被调度并获得CPU资源时,便进入运行状态, run()方法定义了线程的操作和功能
阻塞:在某种特殊情况下,被人为挂起或执行输入输出操作时,让出 CPU 并临时中止自己的执行,进入阻塞状态
死亡:线程完成了它的全部工作或线程被提前强制性地中止或出现异常导致结束

状态之间的转换:
在这里插入图片描述

五、线程同步

(一)为什么要有线程同步
先看一下如果没有线程同步会发生什么
在没有线程同步的时候,我拿着银行卡(储蓄卡)去取款机取钱,在一切所有操作都就差点击确认取款的时候,这个时候我媳妇(噢,我没有)在支付宝上也要用这个卡充钱。假设有个在程序源码中有个取款的方法,这时我在ATM机上已经进入这个方法,由于没有同步控制,我媳妇在手机上也进入到了这个方法。最后的结果就是我和我媳妇都取款成功了。而此时卡上的余额是不够我俩取款总数的,这就导致超出的钱需要银行出,但这个卡又不是信用卡,最终就导致银行赔钱了。现实中这种赔钱的事银行是不可能做的。

类似的情况还有很多,为了避免这种情况就提出了线程同步控制。

举个买车票的例子:

public class WindowTest {
    public static void main(String[] args) {
        Window w1 = new Window();
        Window w2 = new Window();
        Window w3 = new Window();
        w1.setName("窗口一:");
        w2.setName("窗口二:");
        w3.setName("窗口三:");

        w1.start();
        w2.start();
        w3.start();

    }
}

//此时的方法也是有bug的---线程不同步
class Window extends Thread{
    //要使用static修饰(多个窗口共享一个变量)
    public static int ticket = 20;//总票数
    @Override
    public void run() {
        while (true){
           if (ticket > 0) {
                System.out.println(getName() + ticket);
                ticket--;
            } else {
                break;
       		}
      }
   }
}

通过结果会发现票号为20的卖出去了多次或是票号为负数的情况,这就导致有三个人的票号是一样的,现实中是不允许出现这种情况的。

  • 出现这种问题的原因:
    当多条语句在操作同一个线程共享数据时,一个线程对多条语句只执行了一部分,还没有执行完,另一个线程参与进来执行。导致共享数据的错误。
  • 解决办法:
    对多条操作共享数据的语句,只能让一个线程都执行完,在执行过程中,其他线程不可以参与执行。

(二)如何使用线程同步控制

  • 方式一:通过Synchronized同步代码块
    synchronized (对象){
    // 需要被同步的代码;
    }

说明:

  1. 操作共享数据的代码,即为需要被同步的代码。 -->不能包含代码多了,也不能包含代码少了。
  2. 共享数据:多个线程共同操作的变量。比如:ticket就是共享数据。
  3. 同步监视器,俗称:锁。任何一个类的对象,都可以充当锁。

要求:多个线程必须要共用同一把锁
补充:
在实现Runable接口创建多线程方式中,可以考虑使用this充当同步监视器(保证this唯一)
在继承Thread类创建多线程方式中,不建议使用this充当同步监视器,可以考虑当前类充当同步监视器

例:
还用上边售票的例子

public class WindowTest {
    public static void main(String[] args) {
        Window w1 = new Window();
        Window w2 = new Window();
        Window w3 = new Window();
        w1.setName("窗口一:");
        w2.setName("窗口二:");
        w3.setName("窗口三:");

        w1.start();
        w2.start();
        w3.start();

    }
}

//此时的方法也是有bug的---线程不同步
class Window extends Thread{
    //要使用static修饰(多个窗口共享一个变量)
    public static int ticket = 20;//总票数
	//用来充当同步监视器,静态可以保证唯一
	private static Object obj = new Object();
    @Override
    public void run() {
        while (true){
	        synchronized(obj){
	         //错误的方式:this代表着t1,t2,t3三个对象
//           synchronized (this){
	           if (ticket > 0) {
	                System.out.println(getName() + ticket);
	                ticket--;
	            } else {
	                break;
	       		}
	       	}
      	}
   	}
}

此时这个售票系统就是安全的,不会出现错票和重票等问题。
只需要使用synchronized将需要被同步的代码包裹起来即可。

注意:synchronized使用的同步监视器(也就是括号中的对象)必须是唯一的,但凡有一个线程使用的同步监视器与其他线程不一致,那这个线程就不是安全的。

  • 方式二:同步方法
    将synchronized 放在方法声明中,表示整个方法都是同步方法。
    public synchronized void show (){
    ….
    }

说明:

  1. 将需要被同步的代码写在一个被synchronized修饰的方法中,然后在run中调用此方法
  2. 在同步方法中仍然涉及到同步监视器
  3. 在接口中synchronized修饰的方法,默认锁的对象是this
  4. 在类中,默认的是类,若锁唯一,默认的就是 类名.class

例:

class Window3 implements Runnable {

	//因为只需要创建一个Window3的对象,所以可以不适用static修饰
    private int ticket = 100;

    @Override
    public void run() {
        while (true) {
			//在run方法中调用同步方法
            show();
        }
    }

//同步方法
    private synchronized void show(){//同步监视器:this
        //synchronized (this){

            if (ticket > 0) {

                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }

                System.out.println(Thread.currentThread().getName() + ":卖票,票号为:" + ticket);

                ticket--;
            }
        //}
    }
}


public class WindowTest3 {
    public static void main(String[] args) {
        Window3 w = new Window3();

        Thread t1 = new Thread(w);
        Thread t2 = new Thread(w);
        Thread t3 = new Thread(w);

        t1.setName("窗口1");
        t2.setName("窗口2");
        t3.setName("窗口3");

        t1.start();
        t2.start();
        t3.start();
    }

}
  1. 同步方法仍然涉及到同步监视器,只是不需要我们显式的声明。
  2. 非静态的同步方法,同步监视器是:this
    静态的同步方法,同步监视器是:当前类本身

关于同步机制中的锁(同步监视器)
在这里插入图片描述

  • 如何判断代码是否存在线程安全?(重要!!!)

    1. 明确哪些代码是多线程运行的代码
    2. 明确多个线程是否有共享数据
    3. 明确多线程运行代码中是否有多条语句操作共享数据
  • 如何解决
    对多条操作共享数据的语句,只能让一个线程都执行完,在执行过程中,其他线程不可以参与执行。
    所有操作共享数据的这些语句都要放在同步范围中

  • 注意:
    范围太小:没锁住所有有安全问题的代码
    范围太大:没发挥多线程的功能。

(三)释放锁的操作

  1. 当前线程的同步方法、同步代码块执行结束。
  2. 当前线程在同步代码块、同步方法中遇到break、return终止了该代码块、该方法的继续执行。
  3. 当前线程在同步代码块、同步方法中出现了未处理的Error或Exception,导致异常结束。
  4. 当前线程在同步代码块、同步方法中执行了线程对象的wait()方法,当前线程暂停,并释放锁。

(四)不会释放锁的操作

  1. 线程执行同步代码块或同步方法时,程序调用Thread.sleep()、Thread.yield()方法暂停当前线程的执行
  2. 线程执行同步代码块时,其他线程调用了该线程的suspend()方法将该线程挂起,该线程不会释放锁(同步监视器)。
  3. 应尽量避免使用suspend()和resume()来控制线程

(五)线程的死锁问题

  • 死锁
  1. 不同的线程分别占用对方需要的同步资源不放弃,都在等待对方放弃自己需要的同步资源,就形成了线程的死锁
  2. 出现死锁后,不会出现异常,不会出现提示,只是所有的线程都处于阻塞状态,无法继续
  • 解决方法
  1. 专门的算法、原则
  2. 尽量减少同步资源的定义
  3. 尽量避免嵌套同步

死锁的演示:

class A {
	public synchronized void foo(B b) { //同步监视器:A类的对象:a
		System.out.println("当前线程名: " + Thread.currentThread().getName()
				+ " 进入了A实例的foo方法"); // ①
//		try {
//			Thread.sleep(200);
//		} catch (InterruptedException ex) {
//			ex.printStackTrace();
//		}
		System.out.println("当前线程名: " + Thread.currentThread().getName()
				+ " 企图调用B实例的last方法"); // ③
		b.last();
	}

	public synchronized void last() {//同步监视器:A类的对象:a
		System.out.println("进入了A类的last方法内部");
	}
}

class B {
	public synchronized void bar(A a) {//同步监视器:b
		System.out.println("当前线程名: " + Thread.currentThread().getName()
				+ " 进入了B实例的bar方法"); // ②
//		try {
//			Thread.sleep(200);
//		} catch (InterruptedException ex) {
//			ex.printStackTrace();
//		}
		System.out.println("当前线程名: " + Thread.currentThread().getName()
				+ " 企图调用A实例的last方法"); // ④
		a.last();
	}

	public synchronized void last() {//同步监视器:b
		System.out.println("进入了B类的last方法内部");
	}
}

public class DeadLock implements Runnable {
	A a = new A();
	B b = new B();

	public void init() {
		Thread.currentThread().setName("主线程");
		// 调用a对象的foo方法
		a.foo(b);
		System.out.println("进入了主线程之后");
	}

	public void run() {
		Thread.currentThread().setName("副线程");
		// 调用b对象的bar方法
		b.bar(a);
		System.out.println("进入了副线程之后");
	}

	public static void main(String[] args) {
		DeadLock dl = new DeadLock();
		new Thread(dl).start();


		dl.init();
	}
}

(六)Lock锁

  • 从JDK 5.0开始,Java提供了更强大的线程同步机制——通过显式定义同步锁对象来实现同步。同步锁使用Lock对象充当。
  • **java.util.concurrent.locks.Lock接口是控制多个线程对共享资源进行访问的工具。**锁提供了对共享资源的独占访问,每次只能有一个线程对Lock对象加锁,线程开始访问共享资源之前应先获得Lock对象。
  • ReentrantLock 类实现了 Lock ,它拥有与 synchronized 相同的并发性和内存语义,在实现线程安全的控制中,比较常用的是ReentrantLock,可以显式加锁、释放锁。

例:

class Window implements Runnable{

    private int ticket = 100;
    
    //1.实例化ReentrantLock
    private ReentrantLock lock = new ReentrantLock();

    @Override
    public void run() {
        while(true){
            try{

                //2.调用锁定方法lock()----每次只能有一个线程进行上锁,知道该线程释放所以后其他线程才能进入,且只能进入一个
                lock.lock();

                if(ticket > 0){

                    try {
                        Thread.sleep(100);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }

                    System.out.println(Thread.currentThread().getName() + ":售票,票号为:" + ticket);
                    ticket--;
                }else{
                    break;
                }
                //一定要将unlock()方法写在finally中,这样是为了避免上边的代码出现异常,导致永远不会释放锁
            }finally {
            
                //3.调用解锁方法:unlock()
                //如果不执行此操作,其他线程将永远不会进入执行,导致多线程的程序变成了单线程
                lock.unlock();
            }

        }
    }
}

public class LockTest {
    public static void main(String[] args) {
        Window w = new Window();

        Thread t1 = new Thread(w);
        Thread t2 = new Thread(w);
        Thread t3 = new Thread(w);

        t1.setName("窗口1");
        t2.setName("窗口2");
        t3.setName("窗口3");

        t1.start();
        t2.start();
        t3.start();
    }
}

(七)synchronized 与 Lock的异同?

相同:二者都可以解决线程安全问题

不同:

  1. synchronized机制在执行完相应的同步代码以后,自动的释放同步监视器
  2. Lock需要手动的启动同步(lock()),同时结束同步也需要手动的实现(unlock())

注意: 使用Lock锁,JVM将花费较少的时间来调度线程,性能更好。并且具有更好的扩展性(提供更多的子类)

推荐优先使用顺序:
Lock —> 同步代码块(已经进入了方法体,分配了相应资源) —> 同步方法(在方法体之外)

创建线程的方式有四种
解决线程安全的方式有三种:同步代码块、同步方法、lock锁(JDK5.0 新增)

六:线程通信

推荐:https://blog.csdn.net/u011635492/article/details/83043212

线程通信,即线程之间进行的通信,当一个线程满足某些条件时,通知其他线程可以做哪些操作了。比如:经典的生产者消费者问题:当队列满时,生产者需要等待队列有空间才能继续往里面放入商品,而在等待的期间内,生产者必须释放对临界资源(即队列)的占用权。日常生活中有很多类似的事情发生,在程序中也是必不可免的。为了解决这种问题,就出现了线程通信的使用。

  • 涉及到的三个方法:
  1. wait():一旦执行此方法,当前线程就进入阻塞状态,并释放同步监视器。
  2. notify():一旦执行此方法,就会唤醒被wait的一个线程。如果有多个线程被wait,就唤醒优先级高的那个。
  3. notifyAll():一旦执行此方法,就会唤醒所有被wait的线程。
  • 说明:
  1. wait(),notify(),notifyAll()三个方法必须使用在同步代码块或同步方法中。
  2. wait(),notify(),notifyAll()三个方法的调用者必须是同步代码块或同步方法中的同步监视器。否则,会出现IllegalMonitorStateException异常
  3. wait(),notify(),notifyAll()三个方法是定义在java.lang.Object类中。
  • sleep() 和 wait()的异同?
  1. 相同点:一旦执行方法,都可以使得当前的线程进入阻塞状态。
  2. 不同点:
    1)两个方法声明的位置不同:Thread类中声明sleep() , Object类中声明wait()
    2)调用的要求不同:sleep()可以在任何需要的场景下调用。 wait()必须使用在同步代码块或同步方法中
    3)关于是否释放同步监视器:如果两个方法都使用在同步代码块或同步方法中,sleep()不会释放锁,wait()会释放锁。

例:

class Number implements Runnable{
    private int number = 1;
    private Object obj = new Object();
    @Override
    public void run() {

        while(true){

            synchronized (obj) {

                obj.notify();

                if(number <= 100){

                    try {
                        Thread.sleep(10);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }

                    System.out.println(Thread.currentThread().getName() + ":" + number);
                    number++;

                    try {
                        //使得调用如下wait()方法的线程进入阻塞状态
                        obj.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }

                }else{
                    break;
                }
            }

        }

    }
}


public class CommunicationTest {
    public static void main(String[] args) {
        Number number = new Number();
        Thread t1 = new Thread(number);
        Thread t2 = new Thread(number);

        t1.setName("线程1");
        t2.setName("线程2");

        t1.start();
        t2.start();
    }
}

生产者消费者问题:

//店员  用来和生产者消费者(可以想象为厨师顾客)的通信
class Clerk{
    private int productCount = 0;
    //生产产品
    public synchronized void produceProduct() {

        if(productCount < 20){
            productCount++;
            System.out.println(Thread.currentThread().getName() + ":开始生产第" + productCount + "个产品");

            notify();

        }else{
            //等待
            try {
                wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }

    }
    //消费产品
    public synchronized void consumeProduct() {
        if(productCount > 0){
            System.out.println(Thread.currentThread().getName() + ":开始消费第" + productCount + "个产品");
            productCount--;

            notify();
        }else{
            //等待
            try {
                wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }

    }
}
//生产者
class Producer extends Thread{

    private Clerk clerk;

    public Producer(Clerk clerk) {
        this.clerk = clerk;
    }

    @Override
    public void run() {
        System.out.println(getName() + ":开始生产产品.....");

        while(true){

            try {
                Thread.sleep(10);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

            clerk.produceProduct();
        }

    }
}
//消费者
class Consumer extends Thread{
    private Clerk clerk;

    public Consumer(Clerk clerk) {
        this.clerk = clerk;
    }
    @Override
    public void run() {
        System.out.println(getName() + ":开始消费产品.....");

        while(true){

            try {
                Thread.sleep(20);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

            clerk.consumeProduct();
        }
    }
}

public class ProductTest {

    public static void main(String[] args) {
    	//创建店员对象,
        Clerk clerk = new Clerk();
        //
        Producer p1 = new Producer(clerk);
        p1.setName("生产者1");
		
		//创建多个消费者对象
        Consumer c1 = new Consumer(clerk);
        c1.setName("消费者1");
        Consumer c2 = new Consumer(clerk);
        c2.setName("消费者2");

        p1.start();
        c1.start();
        c2.start();

    }
}

七、JDK 5.0 新增创建线程方式

(一)新增方式一:实现Callable接口

  • 与使用Runnable相比, Callable功能更强大些
  1. 相比run()方法,可以有返回值
  2. 方法可以抛出异常
  3. 支持泛型的返回值
  4. 需要借助FutureTask类,比如获取返回结果
  • Future接口
  1. 可以对具体Runnable、Callable任务的执行结果进行取消、查询是否完成、获取结果等。
  2. FutrueTask是Futrue接口的唯一的实现类
  3. FutureTask 同时实现了Runnable, Future接口。它既可以作为Runnable被线程执行,又可以作为Future得到Callable的返回值
  • 如何理解实现Callable接口的方式创建多线程比实现Runnable接口创建多线程方式强大?
  1. call()可以有返回值的。
  2. call()可以抛出异常,被外面的操作捕获,获取异常的信息
  3. Callable是支持泛型的

例:

// 1.创建Callable接口的实现类,泛型可有可无
class NumThread implements Callable{
    // 2. 实现call方法,并将需要执行的代码写在call()中
    @Override
    public Object call() throws Exception {//可以抛出异常
        int sum = 0;
        for (int i = 0; i <= 100; i++) {
            if(i % 2 == 0){
                System.out.println(i);
                sum += i;
            }
        }
        // 返回的sum 是 int型,而方法的返回值为Object,此处有一个自动装箱
        //在不需要返回值时,就return null;
        return sum;
    }
}

public class ThreadNew {
    public static void main(String[] args) {
        // 3. 创建NumThread接口实现类的对象
        NumThread numThread = new NumThread();
        // 4. 将此NumThread接口实现类的对象作为参数传递到FutureTask构造器中,并创建FutureTask的对象
        FutureTask futureTask = new FutureTask(numThread);
        // 5. 将FutureTask的对象作为参数传递到Thread类的构造器中,创建Thread类的对象,并调用start()
        new Thread(futureTask).start();

        try {
            // 6. 获取Callable中call的返回值
            // get()返回值即为futureTask构造器参数Callable中实现call()的返回值
            // 若没有将实现接口类的对象传递到FutureTask中且在将FutureTask的对象传递到Thread类的对象中
            // 并调用start(), 则无法获取Callable接口实现类中call()的返回值
            Object sum = futureTask.get();
            System.out.println(sum);
        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (ExecutionException e) {
            e.printStackTrace();
        }


    }
}

(二)新增方式二:使用线程池

  • 背景:经常创建和销毁、使用量特别大的资源,比如并发情况下的线程,对性能影响很大。
  • 思路:提前创建好多个线程,放入线程池中,使用时直接获取,使用完放回池中。可以避免频繁创建销毁、实现重复利用。类似生活中的公共交通工具。
  • 好处:
  1. 提高响应速度(减少了创建新线程的时间)
  2. 降低资源消耗(重复利用线程池中线程,不需要每次都创建)
  3. 便于线程管理
    ① corePoolSize:核心池的大小
    ② maximumPoolSize:最大线程数
    ③ keepAliveTime:线程没有任务时最多保持多长时间后会终止

例:

public class ThreadPool {
    public static void main(String[] args) {
        // 1. 提供指定线程数量的线程池
        // ExecutorService是一个接口 Executors相当于工具类  service才是线程池
        ExecutorService service = Executors.newFixedThreadPool(10);
        //因为service的实现类是ThreadPoolExecutor,即可以将service强转过去
        ThreadPoolExecutor service1 = (ThreadPoolExecutor) service; //service本身就是ThreadPoolExecutor类型的

        //设置线程池的属性
//        System.out.println(service.getClass());
//        service1.setCorePoolSize(15);
//        service1.setKeepAliveTime();
        
        // 查看service实现类 System.out.println(service.getClass());
        
        // 2. 执行指定线程的操作,需要提供Runnable接口实现类的对象或Callable接口实现类的对象
        service.execute(new NumberThread());//适用于Runnable
//        service.submit(Callable call);//适用于Callable

        // 3. 使用完之后关闭线程池
        service.shutdown();
    }
}

class NumberThread implements Runnable{

    @Override
    public void run() {
        for (int i = 0; i <= 100; i++) {
            if(i % 2 == 0)
                System.out.println(Thread.currentThread().getName() + ":" + i);
        }
    }
}

注意:使用完一定要关闭线程池

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值