JavaSE——线程、线程池

多线程

我们在之前,学习的程序在没有跳转语句的前提下,都是由上至下依次执行,那现在想要设计一个程序,边打游戏边听歌,怎么设计?

要解决上述问题,咱们得使用多进程或者多线程来解决.

多线程代码运行的特点:

  1. 每一条线程,如果抢不到CPU的执行权,就执行不了代码

  2. 只有抢到了CPU的执行权,才能执行run方法里面的代码

  3. 在执行代码的过程中,随时都有可能被别人抢走CPU的执行权

  4. 如果我们同时开启了多条线程,在执行的时候,结果有可能每次都不一样,是随机的。

    因为在任意时刻,CPU到底执行哪条线程,我们是不确定的。

Thread类

线程开启我们需要用到了java.lang.Thread类,API中该类中定义了有关线程的一些方法,具体如下:

构造方法:

  • public Thread():分配一个新的线程对象。
  • public Thread(String name):分配一个指定名字的新的线程对象。
  • public Thread(Runnable target):分配一个带有指定目标新的线程对象。
  • public Thread(Runnable target,String name):分配一个带有指定目标新的线程对象并指定名字。

常用方法:

  • public String getName():获取当前线程名称。
  • public void start():导致此线程开始执行; Java虚拟机调用此线程的run方法。
  • public void run():此线程要执行的任务在此处定义代码。
  • public static void sleep(long millis):使当前正在执行的线程以指定的毫秒数暂停(暂时停止执行)。
  • public static Thread currentThread():返回对当前正在执行的线程对象的引用。

翻阅API后得知创建线程的方式总共有两种,一种是继承Thread类方式,一种是实现Runnable接口方式,方式一我们上一天已经完成,接下来讲解方式二实现的方式。

创建线程方式一_继承方式

Java使用java.lang.Thread类代表线程,所有的线程对象都必须是Thread类或其子类的实例。每个线程的作用是完成一定的任务,实际上就是执行一段程序流即一段顺序执行的代码。Java使用线程执行体来代表这段程序流。Java中通过继承Thread类来创建启动多线程的步骤如下:

  1. 定义Thread类的子类,并重写该类的run()方法,该run()方法的方法体就代表了线程需要完成的任务,因此把run()方法称为线程执行体。
  2. 创建Thread子类的实例,即创建了线程对象
  3. 调用线程对象的start()方法来启动该线程

代码如下:

测试类:

public class Test {
    public static void main(String[] args) {
        //1.创建线程的对象
        MyThread t1 = new MyThread();
        MyThread t2 = new MyThread();

        //2.开启线程,让线程跑起来
        t1.start();
        t2.start();

        //并没有开启线程
        //普通的方法调用
        //是运行在虚拟机中的main线程中的。
        //t1.run();

        //特点:
        //在Java中,线程启动之后。
        //在每一个时刻,到底是那条线程执行,我们是无法控制的
        //随机性、
        //当线程启动之后,多条线程,都在抢夺CPU的执行权
        //谁抢到了CPU,那么就执行哪条线程。
    }
}

自定义线程类:

public class MyThread extends Thread{

    //因为run方法里面的代码,就是线程开启之后,要运行的代码
    @Override
    public void run() {
        for (int i = 1; i <= 100; i++) {
            System.out.println(getName() + "@" + i);
        }
    }
}

创建线程的方式二_实现方式

采用java.lang.Runnable也是非常常见的一种,我们只需要重写run方法即可。

步骤如下:

  1. 定义Runnable接口的实现类,并重写该接口的run()方法,该run()方法的方法体同样是该线程的线程执行体。
  2. 创建Runnable实现类的实例,并以此实例作为Thread的target来创建Thread对象,该Thread对象才是真正的线程对象。
  3. 调用线程对象的start()方法来启动线程。

代码如下:

public class MyRunnable implements Runnable{

    @Override
    public void run() {
        for (int i = 1; i <= 100; i++) {
            System.out.println(i);
        }
    }
}

public class Test {
    public static void main(String[] args) {
        //1.创建的是线程要执行的参数对象
        //创建了一个参数对象
        MyRunnable mr1 = new MyRunnable();
        //又创建了一个参数对象
        MyRunnable mr2 = new MyRunnable();

        //2.创建线程对象
        //两条线程,各自跑各自的
        //线程一,跑参数一
        Thread t1 = new Thread(mr1);
        //线程二,跑参数二
        Thread t2 = new Thread(mr2);

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

    }
}

通过实现Runnable接口,使得该类有了多线程类的特征。run()方法是多线程程序的一个执行目标。所有的多线程代码都在run方法里面。Thread类实际上也是实现了Runnable接口的类。

在启动的多线程的时候,需要先通过Thread类的构造方法Thread(Runnable target) 构造出对象,然后调用Thread对象的start()方法来运行多线程代码。

实际上所有的多线程代码都是通过运行Thread的start()方法来运行的。因此,不管是继承Thread类还是实现Runnable接口来实现多线程,最终还是通过Thread的对象的API来控制线程的,熟悉Thread类的API是进行多线程编程的基础。

tips:Runnable对象仅仅作为Thread对象的target,Runnable实现类里包含的run()方法仅作为线程执行体。而实际的线程对象依然是Thread实例,只是该Thread线程负责执行其target的run()方法。

Thread和Runnable的区别

总结:

实现Runnable接口比继承Thread类所具有的优势:

  1. 适合多个相同的程序代码的线程去共享同一个资源。
  2. 可以避免java中的单继承的局限性。
  3. 增加程序的健壮性,实现解耦操作,代码可以被多个线程共享,代码和线程独立。
  4. 线程池只能放入实现Runable或Callable类线程,不能直接放入继承Thread的类。

创建线程方式三-callable方式

前两种方式,线程运行完毕之后,没有一个运行结果。

如果我们想要有一个结果,可以用第三种方式。

实现过程:

① 得到任务对象

​ 1.定义类实现Callable接口,重写call方法,封装要做的事情。

​ 2.用FutureTask把Callable对象封装成线程任务对象。

② 把线程任务对象交给Thread处理。

③ 调用Thread的start方法启动线程,执行任务

④ 线程执行完毕后、通过FutureTask的get方法去获取任务执行的结果。

代码示例:

public class MyCallable implements Callable<Integer> {

    @Override
    public Integer call() throws Exception {
        //1.开启一条线程,求1~100之间的和
        int sum = 0;
        for (int i = 1; i <= 100; i++) {
            sum = sum + i;
        }
        return sum;
    }
}


public class Test {
    public static void main(String[] args) {
        //多线程:
        //当我们在开发中,要花大量的时间去处理一个单独问题的时候
        //可以用多线程去处理。

        //如果在处理完毕之后,需要有一个结果返回,可以用第三种方式
        //如果不需要有结果返回,可以用第一种或第二种方式
        //1.参数对象
        MyCallable mc = new MyCallable();
        //2.创建线程任务对象
        FutureTask<Integer> ft = new FutureTask<>(mc);
        //3.创建线程对象
        Thread t1 = new Thread(ft);
        //4.开启线程
        t1.start();
        //5.获取线程的处理结果
        Integer result = ft.get();
        //6.打印结果
        System.out.println(result);

    }
}

匿名内部类方式

使用线程的内匿名内部类方式,可以方便的实现每个线程执行不同的线程任务操作。

使用匿名内部类的方式实现Runnable接口,重新Runnable接口中的run方法:

public class NoNameInnerClassThread {
   	public static void main(String[] args) {	   	
//		new Runnable(){
//			public void run(){
//				for (int i = 0; i < 20; i++) {
//					System.out.println("张宇:"+i);
//				}
//			}  
//	   	}; //---这个整体  相当于new MyRunnable()
        Runnable r = new Runnable(){
            public void run(){
                for (int i = 0; i < 20; i++) {
                  	System.out.println("张宇:"+i);
                }
            }  
        };
        new Thread(r).start();

        for (int i = 0; i < 20; i++) {
          	System.out.println("费玉清:"+i);
        }
   	}
}

线程的姓名

扩展点:

​ 线程有默认名称:格式Thread-序号。

​ 在创建线程对象的时候,系统底层自动设置的。

需要掌握的知识点:

第一种创建多线程的方式中。

​ 如果利用set方法设置姓名

​ 如果利用构造方法设置姓名

​ 如果利用get方法获取姓名

代码示例:

public class Test {
    public static void main(String[] args) {
        //子类不能继承父类的构造方法
        //但是可以通过super进行调用
        //1.创建线程的对象
        MyThread t1 = new MyThread("火车");
        MyThread t2 = new MyThread("飞机");

        //细节
        //起名字,要在创建对象之后,开启线程之前
        //t1.setName("火车");
        //t2.setName("飞机");

        //2.开启线程,让线程跑起来
        t1.start();
        t2.start();
      
    }
}

public class MyThread extends Thread{
    //此时我们没有写构造,jvm会加一个无参构造
    public MyThread() {
    }
    public MyThread(String name) {
        super(name);
    }

    //因为run方法里面的代码,就是线程开启之后,要运行的代码
    @Override
    public void run() {
        for (int i = 1; i <= 100; i++) {
            //此时getName是获取当前线程的姓名
            System.out.println(getName() + "@" + i);
        }
    }
}
第二种创建多线程的方式中。

​ 如果利用set方法设置姓名

​ 如果利用构造方法设置姓名

​ 如果利用get方法获取姓名

代码示例:

public class MyRunnable implements Runnable{
    @Override
    public void run() {
        for (int i = 1; i <= 100; i++) {
            //Thread.currentThread(); 哪条线程执行到这行代码,此时获取的就是当前线程的对象
            System.out.println(Thread.currentThread().getName() + "@" +i);
        }
    }
}


public class Test {
    public static void main(String[] args) {
        //1.创建的是线程要执行的参数对象
        //创建了一个参数对象
        MyRunnable mr1 = new MyRunnable();
        //又创建了一个参数对象
        MyRunnable mr2 = new MyRunnable();

        //2.创建线程对象
        //两条线程,各自跑各自的
        //线程一,跑参数一
        Thread t1 = new Thread(mr1,"线程A");
        //线程二,跑参数二
        Thread t2 = new Thread(mr2,"线程B");

       // t1.setName("郭琴");
       // t2.setName("杨康");

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

    }
}

线程安全

如果有多个线程在同时运行,而这些线程可能会同时运行这段代码。程序每次运行结果和单线程运行的结果是一样的,而且其他的变量的值也和预期的是一样的,就是线程安全的。

我们通过一个案例,演示线程的安全问题:

电影院要卖票,我们模拟电影院的卖票过程。假设要播放的电影是 “葫芦娃大战奥特曼”,本次电影的座位共100个(本场电影只能卖100张票)。

我们来模拟电影院的售票窗口,实现多个窗口同时卖 “葫芦娃大战奥特曼”这场电影票(多个窗口一起卖这100张票)

需要窗口,采用线程对象来模拟;需要票,Runnable接口子类来模拟。

模拟票:

public class Ticket implements Runnable {
    private int ticket = 100;
    /*
     * 执行卖票操作
     */
    @Override
    public void run() {
        //每个窗口卖票的操作 
        //窗口 永远开启 
        while (true) {
            if (ticket <= 0) {
                //无票 可以结束
               break;
            }else{
                //有票 可以卖
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    // TODO Auto-generated catch block
                    e.printStackTrace();
                }
                //获取当前线程对象的名字 
                String name = Thread.currentThread().getName();
                System.out.println(name + "正在卖:" + ticket--);
            }
        }
    }
}

测试类:

public class Demo {
	public static void main(String[] args) {
		//创建线程任务对象
		Ticket ticket = new Ticket();
		//创建三个窗口对象
		Thread t1 = new Thread(ticket, "窗口1");
		Thread t2 = new Thread(ticket, "窗口2");
		Thread t3 = new Thread(ticket, "窗口3");
		
		//同时卖票
		t1.start();
		t2.start();
		t3.start();
	}
}

发现程序出现了两个问题:

  1. 相同的票数,比如5这张票被卖了两回。
  2. 不存在的票,比如0票与-1票,是不存在的。

这种问题,几个窗口(线程)票数不同步了,这种问题称为线程不安全。

线程安全问题都是由全局变量及静态变量引起的。若每个线程中对全局变量、静态变量只有读操作,而无写操作,一般来说,这个全局变量是线程安全的;若有多个线程同时执行写操作,一般都需要考虑线程同步,否则的话就可能影响线程安全。

线程同步

线程同步是为了解决线程安全问题。

当我们使用多个线程访问同一资源的时候,且多个线程中对资源有写的操作,就容易出现线程安全问题。

要解决上述多线程并发访问一个资源的安全性问题:也就是解决重复票与不存在票问题,Java中提供了同步机制(synchronized)来解决。

根据案例简述:

窗口1线程进入操作的时候,窗口2和窗口3线程只能在外等着,窗口1操作结束,窗口1和窗口2和窗口3才有机会进入代码去执行。也就是说在某个线程修改共享资源的时候,其他线程不能修改该资源,等待修改完毕同步之后,才能去抢夺CPU资源,完成对应的操作,保证了数据的同步性,解决了线程不安全的现象。

为了保证每个线程都能正常执行原子操作,Java引入了线程同步机制。

那么怎么去使用呢?有三种方式完成同步操作:

  1. 同步代码块。
  2. 同步方法。
  3. 锁机制。

同步代码块

  • 同步代码块synchronized关键字可以用于方法中的某个区块中,表示只对这个区块的资源实行互斥访问。

格式:

synchronized(同步锁){
     需要同步操作的代码
}

同步锁:

对象的同步锁只是一个概念,可以想象为在对象上标记了一个锁.

  1. 锁对象 可以是任意类型。
  2. 多个线程对象 要使用同一把锁。

注意:在任何时候,最多允许一个线程拥有同步锁,谁拿到锁就进入代码块,其他的线程只能在外等着(BLOCKED)。

使用同步代码块解决代码:

public class MyTicket extends Thread{
    static int ticket = 100;
    //锁对象,可以是任意对象
    static Object obj = new Object();

    //关于锁的细节:
    //1.锁对象默认是打开的。
    //2.当有线程进入之后,锁自动关闭
    //3.当线程出去之后,锁再次自动打开


    //套路:
    //1.while(true) 死循环
    //2.synchronized同步代码块   多个线程一定要共享同一把锁
    //3.判断,共享数据是否到了结尾。(到结尾)
    //4.判断,共享工具是否到了结尾。(还没有到结尾)


    @Override
    public void run() {
        while(true){//t1
            synchronized (obj){//t2   //t3
                if(ticket < 0){
                    break;
                }else{
                    try {
                        Thread.sleep(10);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println(getName() + "在卖第" + ticket + "张票");

                    ticket--;
                }
            }

        }
    }
}


public class Test {
    public static void main(String[] args) {
        //创建三个线程对象

        //当多线操作共享数据的时候
        //1,重复票号
        //2,负数票号

        MyTicket t1 = new MyTicket();
        MyTicket t2 = new MyTicket();
        MyTicket t3 = new MyTicket();


        t1.setName("窗口A");
        t2.setName("窗口B");
        t3.setName("窗口C");


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

    }
}

当使用了同步代码块后,上述的线程的安全问题,解决了。

同步方法

  • 同步方法:使用synchronized修饰的方法,就叫做同步方法,保证A线程执行该方法的时候,其他线程只能在方法外等着。

格式:

public synchronized void method(){
   	可能会产生线程安全问题的代码
}

同步锁是谁?

​ 对于非static方法,同步锁就是this。

​ 对于static方法,我们使用当前方法所在类的字节码对象(类名.class)。

使用同步方法代码如下:

public class Ticket implements Runnable{
	private int ticket = 100;
	/*
	 * 执行卖票操作
	 */
	@Override
	public void run() {
		//每个窗口卖票的操作 
		//窗口 永远开启 
		while(true){
			sellTicket();
		}
	}
	
	/*
	 * 锁对象 是 谁调用这个方法 就是谁 
	 *   隐含 锁对象 就是  this
	 *    
	 */
	public synchronized void sellTicket(){
        if(ticket>0){//有票 可以卖	
            //出票操作
            //使用sleep模拟一下出票时间 
            try {
              	Thread.sleep(100);
            } catch (InterruptedException e) {
              	// TODO Auto-generated catch block
              	e.printStackTrace();
            }
            //获取当前线程对象的名字 
            String name = Thread.currentThread().getName();
            System.out.println(name+"正在卖:"+ticket--);
        }
	}
}

Lock锁

java.util.concurrent.locks.Lock机制提供了比synchronized代码块和synchronized方法更广泛的锁定操作,同步代码块/同步方法具有的功能Lock都有,除此之外更强大

Lock锁也称同步锁,加锁与释放锁方法化了,如下:

  • public void lock():加同步锁。
  • public void unlock():释放同步锁。

使用如下:

public class Ticket implements Runnable{
	private int ticket = 100;
	
	Lock lock = new ReentrantLock();
	/*
	 * 执行卖票操作
	 */
	@Override
	public void run() {
		//每个窗口卖票的操作 
		//窗口 永远开启 
		while(true){
			lock.lock();
			if(ticket>0){//有票 可以卖
				//出票操作 
				//使用sleep模拟一下出票时间 
				try {
					Thread.sleep(50);
				} catch (InterruptedException e) {
					// TODO Auto-generated catch block
					e.printStackTrace();
				}
				//获取当前线程对象的名字 
				String name = Thread.currentThread().getName();
				System.out.println(name+"正在卖:"+ticket--);
			}
			lock.unlock();
		}
	}
}

第三章 死锁

什么是死锁

因为由于锁的嵌套,就有可能导致死锁现象。

以后我们怎么办?不要写锁的嵌套。

产生死锁的条件

1.有多把锁
2.有多个线程
3.有同步代码块嵌套

死锁代码

public class MyThread extends Thread {
    //创建两把锁对象
    static Object objA = new Object();
    static Object objB = new Object();

    @Override
    public void run() {
        while (true) {
            if("小黑".equals(getName())){
                //当前线程是第一条小黑线程
                synchronized (objA){
                    //小黑
                    synchronized (objB){
                        System.out.println("小黑在执行了");
                    }
                }

            }else{
                //当前线程是第二条小美线程
                synchronized (objB){
                    //小美
                    synchronized (objA){
                        System.out.println("小美在执行了~~~~~~~~~");
                    }
                }
            }
        }
    }
}


public class Test {
    public static void main(String[] args) {
        //创建两个线程对象

        MyThread t1 = new MyThread();
        MyThread t2 = new MyThread();

        //给两条线程起名字
        t1.setName("小黑");
        t2.setName("小美");

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

注意:我们应该尽量避免死锁

线程间的通信

睡眠sleep方法

我们看到状态中有一个状态叫做计时等待,可以通过Thread类的方法来进行演示.

public static void sleep(long time) 让当前线程进入到睡眠状态,到毫秒后自动醒来继续执行

public class Test{
  public static void main(String[] args){
    for(int i = 1;i<=5;i++){
      	Thread.sleep(1000);
        System.out.println(i)   
    } 
  }
}

这时我们发现主线程执行到sleep方法会休眠1秒后再继续执行。

等待和唤醒

Object类的方法

public void wait() : 让当前线程进入到等待状态 此方法必须锁对象调用.

public void notify() : 唤醒当前锁对象上等待状态的线程 此方法必须锁对象调用.

等待唤醒案例(包子铺卖包子)

代码示例:

生成包子类:

public class ChuShi extends Thread{

    //套路:
    //1.while(true)
    //2.同步代码块
    //3.判断共享数据是否到末位-- 到了末尾
    //4.判断共享数据是否到末位-- 没有到末尾

    @Override
    public void run() {
        while(true){
            synchronized (Desk.obj){
                if(Desk.count == 0){
                    break;
                }else{
                    if(Desk.flag == 1){
                        try {
                            //当厨师线程执行到这行代码的时候
                            //就会等待,并且跟前面锁对象绑定在一起了。
                            Desk.obj.wait();
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    }else{
                        //表示桌子上,没有汉堡包,厨师可以做
                        System.out.println("厨师正在做汉堡包");
                        //此时不需要让汉堡包的总数量-1,因为只有等吃货吃完了才需要减一
                        //修改桌子的状态
                        Desk.flag = 1;
                        //唤醒等待的吃货开吃
                        Desk.obj.notifyAll();
                    }
                }
            }
        }
    }
}

消费包子类:

package com.itheima.a07waitnotifydemo;

public class ChiHuo extends Thread{
    //套路:
    //1.while(true)
    //2.同步代码块
    //3.判断共享数据是否到末位-- 到了末尾
    //4.判断共享数据是否到末位-- 没有到末尾

    @Override
    public void run() {
        while(true){
            synchronized (Desk.obj){
                if(Desk.count == 0){
                    //此时是控制线程是否停止的判断
                    break;
                }else{
                    //表示汉堡包没有吃光.需要继续吃
                    //判断flag
                    if(Desk.flag == 0){
                        //等待  wait
                        //细节:需要用锁对象去调用wait方法。
                        //相当于,拿着锁对象,跟当前线程绑定在一起了。
                        try {
                            Desk.obj.wait();
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    }else{
                        //汉堡包的总数量-1
                        Desk.count--;
                        //表示桌子上有汉堡包
                        System.out.println("吃货在吃,今天还能再吃" + Desk.count + "个汉堡包");
                        //修改桌子上汉堡包的状态
                        Desk.flag = 0;
                        //唤醒等待的厨师继续做汉堡包
                        Desk.obj.notifyAll();//唤醒跟锁对象绑定的所有线程。
                        //细节1:必须用wait方法,跟锁进行绑定,此时锁才知道要唤醒哪些线程
                        //细节2:如果没有线程在等待,那么也可以唤醒,没有任何问题。
                    }
                }
            }
        }
    }
}

测试类:

package com.itheima.a07waitnotifydemo;

public class Demo {
    public static void main(String[] args) {

        //创建吃货的线程
        ChiHuo ch = new ChiHuo();
        //创建厨师的线程
        ChuShi cs = new ChuShi();

        ch.setName("吃货");
        cs.setName("厨师");


        //开启线程
        ch.start();
        cs.start();
    }
}

桌子类:

package com.itheima.a07waitnotifydemo;

public class Desk {
    //1.汉堡包的数量
    //决定了线程循环的总次数
    public static int count = 10;

    //2.定义汉堡包的状态
    //0 没有  1 有
    //控制线程是否执行
    //0 没有汉堡包 --- 吃货等待  厨师执行
    //1 有汉堡包   --- 吃货执行  厨师等待
    public static int flag = 0;


    //3.锁对象
    //锁对象可以定义,也可以不定义
    //如果没有定义,我们可以找一个类的字节码文件对象,作为锁即可
    public static Object obj = new Object();
}

线程池

线程池方式

线程池的思想

​ 我们使用线程的时候就去创建一个线程,这样实现起来非常简便,但是就会有一个问题:

如果并发的线程数量很多,并且每个线程都是执行一个时间很短的任务就结束了,这样频繁创建线程就会大大降低系统的效率,因为频繁创建线程和销毁线程需要时间,线程也属于宝贵的系统资源。

那么有没有一种办法使得线程可以复用,就是执行完一个任务,并不被销毁,而是可以继续执行其他的任务?

在Java中可以通过线程池来达到这样的效果。今天我们就来详细讲解一下Java的线程池。

线程池概念

  • **线程池:**其实就是一个容纳多个线程的容器,其中的线程可以反复使用,省去了频繁创建线程对象的操作,无需反复创建线程而消耗过多资源。

由于线程池中有很多操作都是与优化资源相关的,我们在这里就不多赘述。

我们通过一张图来了解线程池的工作原理:

线程池的工作原理

合理利用线程池能够带来三个好处:

  1. 降低资源消耗。减少了创建和销毁线程的次数,每个工作线程都可以被重复利用,可执行多个任务。
  2. 提高响应速度。当任务到达时,任务可以不需要的等到线程创建就能立即执行。
  3. 提高线程的可管理性。可以根据系统的承受能力,调整线程池中工作线线程的数目,防止因为消耗过多的内存,而把服务器累趴下(每个线程需要大约1MB内存,线程开的越多,消耗的内存也就越大,最后死机)。

线程池的使用

获取线程池的方式有两种:

  • 利用工具类Executors里面的静态方法
  • 自己new对象

利用工具类获取

创建池子

Executors类中有个创建线程池的方法如下:

  • public static ExecutorService newFixedThreadPool(int nThreads):返回线程池对象。

    (创建的是有上限的线程池,也就是池中的线程个数可以指定最大数量)

  • public static ExecutorService newCachedThreadPool():返回线程池对象。

    (很多资料说没有上限,其实不对,上限很大,为int的最大值)

提交任务
  • public Future<?> submit(Runnable task):将任务提交给线程池

    Future接口:用来记录线程任务执行完毕后产生的结果。

    如果是用多线程的第三种方式提交任务,那么可以获取线程运行的结果。

提交后的情况
  • 有空闲线程就执行

  • 没有空闲线程,但是线程池中的线程还没有到达上限,则创建新的线程执行提交的任务

  • 没有空闲线程,但是线程池中的线程已经到达上限,则排队等待

  • 铁子们想想,排队的线程有没有数量限制呢?如果排队的任务数量到上限了怎么办?

    此时就触发任务拒绝策略,具体的任务拒绝策略请参加下一小节。

使用线程池中线程的步骤
  1. 创建线程池对象。
  2. 将要执行的任务交给池子
  3. 关闭线程池(一般不做)。

Runnable实现类代码:

public class MyRunnable implements Runnable {
    @Override
    public void run() {
        System.out.println("我要一个教练");
        try {
            Thread.sleep(2000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("教练来了: " + Thread.currentThread().getName());
        System.out.println("教我游泳,交完后,教练回到了游泳池");
    }
}

线程池测试类:

public class ThreadPoolDemo {
    public static void main(String[] args) {
        // 创建线程池对象
        ExecutorService pool = Executors.newFixedThreadPool(2);//包含2个线程对象

        // 将要执行的任务提交给线程池
        pool.submit(new MyRunnable());
        pool.submit(new MyRunnable());
        pool.submit(new MyRunnable());
        // 注意:submit方法调用结束后,将使用完的线程又归还到了线程池中
        // 销毁线程池,相当于把池子给砸了,在开发中一般不砸
        //了解一下,知道有这个方法即可。
        //service.shutdown();
    }
}

Callable测试代码:

  • <T> Future<T> submit(Callable<T> task) : 获取线程池中的某一个线程对象,并执行.

    Future : 表示计算的结果.

  • V get() : 获取计算完成的结果。

public class ThreadPoolDemo2 {
    public static void main(String[] args) throws Exception {
        // 创建线程池对象
      ExecutorService service = Executors.newFixedThreadPool(2);//包含2个线程对象

        // 创建Runnable实例对象
        Callable<Double> c = new Callable<Double>() {
            @Override
            public Double call() throws Exception {
                return Math.random();
            }
        };

        // 从线程池中获取线程对象,然后调用Callable中的call()
        Future<Double> f1 = service.submit(c);
        System.out.println(f1.get());

        Future<Double> f2 = service.submit(c);
        System.out.println(f2.get());

        Future<Double> f3 = service.submit(c);
        System.out.println(f3.get());
    }
}

自己创建线程池的对象

直接创建ThreadPoolExecutor的对象即可。

参数分析:

  • 参数一:核心线程数量(不能小于0)
  • 参数二:最大线程数(不能小于等于0,最大数量 >= 核心线程数量)
  • 参数三:空闲线程最大存活时间(不能小于0)
  • 参数四:时间单位(时间单位)
  • 参数五:任务队列(不能为null)
  • 参数六:创建线程工厂(不能为null)
  • 参数七:任务的拒绝策略(不能为null)

代码示例:

package com.itheima.a02threadpooldemo;


import java.util.concurrent.*;

public class ThreadPoolDemo1 {
    public static void main(String[] args)  {
        /*//创建了一个上限为10的线程池
        ExecutorService pool = Executors.newFixedThreadPool(10);

        //给线程池提交任务
        //pool.submit();


        //把池子给砸了
        pool.shutdown();*/

        //如果我们自己想要创建线程池对象,就自己new ThreadPoolExecutor他的对象即可
        //ThreadPoolExecutor pool = new ThreadPoolExecutor();

        //扩展:
        //任务的队列有两种:
        //ArrayBlockingQueue:有界队列,我们在创建对象的时候可以指定上限
        //LinkedBlockingQueue:无界队列,但是事实上还是有界的,只不过队伍可以很长,int的最大值21亿多
        //以后这些参数,以后不要自己随便写,需要跟公司的组长或者项目经理去沟通
        ThreadPoolExecutor pool = new ThreadPoolExecutor(
            5,
            10,
            60,
            TimeUnit.SECONDS,
            new ArrayBlockingQueue<>(10),
            Executors.defaultThreadFactory(),
            new ThreadPoolExecutor.AbortPolicy()
        );

        //提交任务
        while (true) {
            pool.submit(new MyRunnable());
        }

    }
}

class MyRunnable implements Runnable{

    @Override
    public void run() {
        System.out.println("线程执行了~~~~");
        while(true){}
    }
}

扩展点1:队列的种类

队列分为两种,有界和无界

  • ArrayBlockingQueue:有界队列,我们在创建对象的时候可以指定上限,也必须指定上限
  • LinkedBlockingQueue:无界队列,但是事实上还是有界的,只不过队伍可以很长,int的最大值21亿多
扩展点2:任务的拒绝策略

分为四种:

  • ThreadPoolExecutor.AbortPolicy:丢弃任务并抛出RejectedExecutionException异常,是默认的策略
  • ThreadPoolExecutor.DiscardPolicy:丢弃任务,但是不抛出异常 这是不推荐的做法
  • ThreadPoolExecutor.DiscardOldestPolicy:抛弃队列中等待最久的任务 然后把当前任务加入队列中
  • ThreadPoolExecutor.CallerRunsPolicy:调用任务的run()方法绕过线程池直接执行。
扩展点3:临时线程创建时机

新任务提交时发现核心线程都在忙,任务队列也满了,并且还可以创建临时线程,此时才会创建临时线程。

注意:核心线程在忙,再有任务提交,先排队,队伍满了,再开临时线程。

扩展点4:触发拒绝策略时机

核心线程和临时线程都在忙,任务队列也满了,新的任务过来的时候才会开始任务拒绝

简单理解:所有的都满了,才会拒绝。

定时器

Timer

到了某一个时间就直接一段指定的代码,底层就是单独开启了一条线程去执行这个操作。

  • Timer
  • ScheduledExecutorService

代码示例:

//1.创建一个定时器对象
//在底层,实际上就是创建了一条线程。
//并给这条线程起了个名字:小黑定时器
Timer timer = new Timer();

//获取当前时间
Date date = new Date();
System.out.println(date);

//2.给定时器提交任务
//参数一:其实就是Runnable的实现类,就是线程执行的参数对象
//参数二:以当前时间为基准,进行了时间的偏移
//参数三:间隔时间

//其实就是把第一个参数,交给了Timer这条线程去执行。
timer.schedule(new TimerTask() {
    @Override
    public void run() {
        System.out.println(2/0);
        System.out.println(Thread.currentThread().getName() + "在执行了~~~" + new Date());
    }
},1000,2000);

扩展点:Timer的源码

  • 在底层Timer就是创建了一条线程,并给线程起了个名字,名字就是小括号中的参数,如果我们没有给线程起名字也会有默认的名字。

  • schedule中第一个参数就是Runnable的实现类

    所以,此时我们相当于就是创建了一个线程要执行的参数对象,交给了Timer这条线程去执行了。

  • 如果Timer中的线程,执行的代码出现了异常,会导致Timer这条线程直接停止。所有的任务都无法执行了。

ScheduledExecutorService

获取定时器对象:

Executors里面的静态方法说明
public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize)得到定时器对象

提交任务:

ScheduledExecutorService的方法说明
public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize)提交任务

代码示例:

public class ScheduledExecutorServiceDemo1 {
    public static void main(String[] args) {
        Date date = new Date();
        System.out.println("当前时间为:" + date);


        //创建一个定时器对象
        //但是从源码的角度来看,这个定时器,实际上在底层就是一个线程池
        //参数表示:线程池中的线程数量
        ScheduledExecutorService pool = Executors.newScheduledThreadPool(10);

        //提交任务
        //pool.schedule(new MyRunnable(),2,TimeUnit.SECONDS);

        pool.scheduleAtFixedRate(new MyRunnable(),0,2,TimeUnit.SECONDS);
    }
}

class MyRunnable implements Runnable{
    @Override
    public void run() {
        System.out.println("线程在运行了~~~~" + new Date());
    }
}

扩展点1:ScheduledExecutorService 的源码

​ ScheduledExecutorService在底层就相当于是一个线程池。

提交任务有两种方式:

  • schedule提交一次,不会周期性执行
  • scheduleAtFixedRate提交一次,可以周期性执行

扩展点2:两种定时器有什么区别呢?

第一种定时器:Timer,单线程的。

​ 如果我们提交了两个任务,按照顺序依次执行的。

​ 如果其中有一个任务中出现了异常,会导致所有的任务都无法执行。

第二种定时器:ScheduledExecutorService,在本质上是一个线程池。

​ 所以,在底层是多线程。
​ 如果我们提交了多个,是同时执行的。

​ 而且,如果一个任务出现了异常,不会影响其他任务的执行。

结论:

​ 以后如果用到了,建议使用第二种。

扩展知识点1:

并发和并行:

  • 并发:CPU在多个线程之间进行切换。
  • 并行:真·同时执行。

线程的生命周期:

  • 新建
  • 就绪
  • 结束
  • 阻塞
  • 等待
  • 计时等待

**注意点:**其中就绪包含:就绪 + 运行

新建:创建对象的时候就是新建状态

就绪:调用start方法进入就绪状态,现在可以开始抢CPU,但是还无法执行代码

运行:此时是跟本地CPU产生交互,所以虚拟机没有定义运行状态

​ 虚拟机中定义的就绪状态包含了运行状态

结束:代码全部执行完毕

阻塞:执行的过程中遇到锁了。

​ 再次获得到锁,会到就绪状态开始抢CPU的执行权

等待:遇到wait方法进入等待状态

​ 其他线程调用nofify,来唤醒等待的线程回到就绪状态

计时等待:遇到sleep方法进入计时等待状态

​ 当时间到了之后,会到就绪状态

线程小练习:

练习1

package thread;

public class Test104 {
    public static void main(String[] args) {
        Thread main = Thread.currentThread();
        Thread th1 = new MyThread3();
        System.out.println("=======默认优先级=======");
        System.out.println("主线程名:" + main.getName() + "优先级:" + main.getPriority());
        System.out.println("子线程名:" + th1.getName() + "优先级:" + th1.getPriority());
        System.out.println("======修改后优先级======");
        main.setPriority(Thread.MAX_PRIORITY);
        th1.setPriority(Thread.MIN_PRIORITY);
        System.out.println("主线程名:" + main.getName() + "优先级:" + main.getPriority());
        System.out.println("子线程名" + th1.getName() + "子线程优先级" + th1.getPriority());
    }
}

class MyThread3 extends Thread {
    @Override
    public void run() {
    }
}

练习2

package thread;

public class Test105 {
    public static void main(String[] args) {
        Thread h = Thread.currentThread();
        vip v = new vip();
        //设置进程名
        h.setName("普通号");
        v.setName("特需号");
        //run方法
        v.start();                    //特需号

        for (int i = 1; i <= 30; i++) {    //普通号
            System.out.println("●" + h.getName() + ":" + i + "号病人正在看病");
            try {
                Thread.sleep(300);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            //当普通号等于10
            if (i == 10) {
                try {
                    v.join();        //当普通号等于10,等待特需号终止,再执行
                } catch (InterruptedException e1) {
                    e1.printStackTrace();
                }
            }
        }
    }
}

//特需号病人
class vip extends Thread {
    @Override
    public void run() {
        String str = Thread.currentThread().getName();
        for (int i = 1; i <= 10; i++) {
            System.out.println("○" + str + ":" + i + "号病人正在看病");
            try {
                Thread.sleep(600);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

练习3

package thread;

public class Test106 {
    public static void main(String[] args) throws InterruptedException {
        MyRunnable m = new MyRunnable();
        Thread thread1 = new Thread(m, "1号选手");
        Thread thread2 = new Thread(m, "2号选手");
        Thread thread3 = new Thread(m, "3号选手");
        Thread thread4 = new Thread(m, "4号选手");
        Thread thread5 = new Thread(m, "5号选手");
        Thread thread6 = new Thread(m, "6号选手");
        Thread thread7 = new Thread(m, "7号选手");
        Thread thread8 = new Thread(m, "8号选手");
        Thread thread9 = new Thread(m, "9号选手");
        Thread thread10 = new Thread(m, "10号选手");
        thread1.start();
        thread2.start();
        thread3.start();
        thread4.start();
        thread5.start();
        thread6.start();
        thread7.start();
        thread8.start();
        thread9.start();
        thread10.start();

    }
}

class MyRunnable implements Runnable {
    static int num = 10;
    private String thread;
    Object lock = new Object();

    public void run() {
        while (true) {
            synchronized (lock) {
                if (num == 0) {
                    System.out.println("比赛结束!");
                    break;
                }
                thread = Thread.currentThread().getName();
                System.out.println(thread + "拿到了接力棒!");
                num--;
                for (int i = 1; i <= 10; i++) {
                    try {
                        Thread.sleep(10);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println(thread + "跑了" + (i * 10));
                }
                return;
            }
        }
    }
}

练习4

package thread;

public class Test107 {
    public static void main(String[] args) {
        System.out.println("**************开始抢票**************");
        Ticket ticket = new Ticket();
        Thread th1 = new Thread(ticket);
        th1.setName("桃跑跑");
        th1.start();
        Thread th2 = new Thread(ticket);
        th2.setName("张票票");
        th2.start();
        Thread th3 = new Thread(ticket);
        th3.setName("黄牛党");
        th3.start();
    }
}

class Ticket implements Runnable {
    private int count = 10;// 记录剩余票数
    private int num = 0;//记录买到第几张票

    @Override
    public void run() {
        while (true) {
            if (!sale()) {
                break;
            }
        }
    }

    // 同步方法
    public synchronized boolean sale() {
        if (count <= 0) {
            return false;
        }
        num++;
        count--;
        try {
            Thread.sleep(500); // 模拟网络延时
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName() + "抢到第" + num
                + "张票,剩余" + count + "张票!");
        if (Thread.currentThread().getName().equals("黄牛党")) {
            return false;
        }
        return true;
    }
}

总结:

1.什么是多线程?

​ 如果没有多线程,我们的程序,同时只能做一件事情。

​ 如果有了多线程,我们同时做多件事情。

具体的体现:

​ 就是我们的计算机,如果没有多线程,我们只能运行一个软件。

​ 如果有了多线程,我们就可以同时运行多个软件。

2.开启线程的方式?

​ 方式一:继承Thread类(掌握)

​ 方式二:实现Runnable接口(掌握)

​ 方式三:实现callable接口(了解)

​ 方式四:匿名内部类/lambda表达式(了解)

3.Thread类中的常见方法

​ setName、getName、sleep、获取当前线程的对象

​ 利用构造方法,给线程起名字

4.多线程操作共享数据

​ 弊端1:有可能出现重复数据。

​ 弊端2:有可能出现超出范围的数据。

​ 细节1:如果我们使用第一种方式创建线程对象,共享数据一定要用static修饰。

​ 细节2:如果我们使用第二种方式创建线程对象,共享数据可以加static也可以不加static。

​ 但是,多个线程一定要跑同一个参数对象。

5,死锁

​ 知道死锁出现的原因

​ 我们以后如何避免

6,等待唤醒机制

7,线程池

1.线程池的作用
以前写法弊端:用到的时候就创建,不用的时候 就销毁,浪费时间,浪费资源
线程池,就是把线程临时存储。
用到的时候,不需要创建,直接到池子里面拿
用完,不会销毁,还会池子,以备下次再用

2.如何获取一个线程池
Executors 可以调用里面的两个方法:
newCachedThreadPool:没有上线的池子。真正的上限其实是有的,是int的最大值。
newFixedThreadPool:指定一个有上线的线程池。

3.如何把任务交给池子submit

4.把任务交给池子的时候,会有两种情况。
情况一:池子中没有空闲线程,已有线程也达到了上限。任务就等待。
情况二:池子中没有空闲线程,但是线程池没有到达上限。线程池自己会创建一个线程。
情况三:池子有有空闲线程,直接拿过来用。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值