day09【多线程安全问题、死锁、线程状态、线程通讯、线程池】

day09【多线程安全问题、死锁、线程状态、线程通讯、线程池】

今日内容

  • 多线程安全问题
  • synchronized关键字
  • Lock锁
  • 死锁
  • 线程池
  • 线程状态
  • 等待与唤醒

教学目标

第一章 多线程

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

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

1 并发与并行

  • 并行:指两个或多个事件在同一时刻发生(同时执行)。
  • 并发:指两个或多个事件在同一个时间段内发生(交替执行)。

在操作系统中,安装了多个程序,并发指的是在一段时间内宏观上有多个程序同时运行,这在单 CPU 系统中,每一时刻只能有一道程序执行,即微观上这些程序是分时的交替运行,只不过是给人的感觉是同时运行,那是因为分时交替运行的时间是非常短的。

而在多个 CPU 系统中,则这些可以并发执行的程序便可以分配到多个处理器上(CPU),实现多任务并行执行,即利用每个处理器来处理一个可以并发执行的程序,这样多个程序便可以同时执行。目前电脑市场上说的多核 CPU,便是多核处理器,核越多,并行处理的程序越多,能大大的提高电脑运行的效率。

注意:单核处理器的计算机肯定是不能并行的处理多个任务的,只能是多个任务在单个CPU上并发运行。同理,线程也是一样的,从宏观角度上理解线程是并行运行的,但是从微观角度上分析却是串行运行的,即一个线程一个线程的去运行,当系统只有一个CPU时,cpu会以某种顺序执行多个线程,我们把这种情况称之为线程调度。

2 线程与进程

  • 进程:是指一个内存中运行的应用程序,每个进程都有一个独立的内存空间,一个应用程序可以同时运行多个进程;进程也是程序的一次执行过程,是系统运行程序的基本单位;系统运行一个程序即是一个进程从创建、运行到消亡的过程。
  • 线程:是进程中的一个执行单元,负责当前进程中程序的执行,一个进程中至少有一个线程。一个进程中是可以有多个线程的,这个应用程序也可以称之为多线程程序。

进程

线程

进程与线程的区别

  • 进程:有独立的内存空间,进程是程序的一次执行过程。
  • 线程:是进程中的一个执行单元,一个进程中至少有一个线程,一 进程中也可以有多个线程。

注意:下面内容为了解知识点

1:因为一个进程中的多个线程是并发运行的,那么从微观角度看也是有先后顺序的,哪个线程执行完全取决于 CPU 的调度,程序员是干涉不了的。而这也就造成的多线程的随机性。

2:Java 程序的进程里面至少包含两个线程,主进程也就是 main()方法线程,另外一个是垃圾回收机制线程。每当使用 java 命令执行一个类时,实际上都会启动一个 JVM,每一个 JVM 实际上就是在操作系统中启动了一个线程,java 本身具备了垃圾的收集机制,所以在 Java 运行时至少会启动两个线程。

3:由于创建一个线程的开销比创建一个进程的开销小的多,那么我们在开发多任务运行的时候,通常考虑创建多线程,而不是创建多进程。

线程调度:

  • 分时调度

    ​ 所有线程轮流使用 CPU 的使用权,平均分配每个线程占用 CPU 的时间。

  • 抢占式调度

    ​ 优先让优先级高的线程使用 CPU,如果线程的优先级相同,那么会随机选择一个(线程随机性),Java使用的为抢占式调度。

    抢占式调度

3 Thread类

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

  • 构造方法

    方法 说明
    public Thread() 创建线程对象
    public Thread(String name) 创建线程对象并指定线程名字
    public Thread(Runnable target) 使用Runnable创建线程
    public Thread(Runnable target,String name) 使用Runable创建线程并指定线程名字
  • 常用方法

    方法 说明
    String getName() 获取线程的名字
    void start() 开启线程,每个对象只调用一次start
    void run() run方法写线程执行的代码,此线程要执行的任务在此处定义代码
    static void sleep(long millis) 让当前线程睡指定的时间
    static Thread currentThread() 获取当前线程对象

翻阅API后得知创建线程的方式总共有两种,一种是继承Thread类方式,一种是实现Runnable接口方式。

4实现多线程方式

1方式一:继承Thread (理解)

一种方法是:将类声明为 Thread 的子类。该子类应重写 Thread 类的 run 方法。接下来可以分配并启动该子类的实例。

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

1)步骤:

​ A:自定义一个类,继承Thread,这个类称为线程类;

​ B:重写Thread类中的run方法,run方法中就是线程要执行的任务代码;

​ C:创建线程类的对象;

​ D:启动线程,执行任务;

2)代码实现:

需求:演示:实现多线程方式1:继承Thread。

分析和步骤:

1)自定义一个类MyThread ,继承Thread类,在自定义类中重写run函数;

2)在run函数中循环打印1~10数字;

3)定义一个测试类ThreadDemo1,在这个类中创建MyThread类的对象mt;

4)使用对象mt调用start()函数启动线程;

5)在main函数中同样循环输出1~10数字;

/*
  需求:演示:实现多线程方式1:继承Thread。
 * 实现多线程步骤:
 * 	A:定义一个类继承Thread
 *  B:复写Thread类中的run函数
 *  C:创建线程类的对象
 *  D:启动线程,执行任务
 */
//定义一个类继承Thread,这个类称为线程类
class MyThread extends Thread
{
	//B:复写Thread类中的run函数
	/*
	 * 这个函数就是执行线程任务的函数
	 * 开启线程的目的就是要执行代码,比如360体检、杀毒、清理垃圾等
	 */
	public void run() {
		//这里就是线程要执行的代码
		for (int i = 0; i < 10; i++) {
			System.out.println(i);
		}
	}
}
public class ThreadDemo1 {
	public static void main(String[] args) {
		//C:创建线程类的对象
		MyThread mt = new MyThread();
		//D:启动线程,执行任务
		/*
		 * 这里我们启动线程之后,由于MyThread类是一个线程,main函数是一个线程,这里有两个线程
		 * 并且两个线程中都输出0~9,那么根据线程的特点,如果多个线程执行任务,那么CPU执行哪个线程是随机的,
		 * 并且不固定,所以这里的打印结果0~9应该输出一部分MyThread类中的,然后在输出main函数中的,输出的
		 * 内容不固定,这样才对
		 * 但是如果使用线程类对象直接调用run函数,其实这里并没有启动线程,因为启动线程要调用系统资源,开辟内存空间
		 * run函数中只是执行线程任务的代码,所以这里打印的结果是按顺序打印的,和我们之前调用一般函数并没有区别。
		 * 那么怎么能够启动线程呢?
		 * 通过查阅API得知需要调用start()函数才能够启动线程
		 */
		mt.run();
		for (int i = 0; i < 10; i++) {
			System.out.println(i);
		}
	}
}

上述代码输出结果是:

0 1 2 3 4 5 6 7 8 9

0 1 2 3 4 5 6 7 8 9

上述代码有问题,直接使用线程类的对象mt调用run函数这样做是不对的。

这里我们启动线程之后,由于MyThread类是一个线程,main函数是一个线程,这里有两个线程,并且两个线程中都输出0~9,那么根据线程的特点,如果多个线程执行任务,那么CPU执行哪个线程是随机的,并且不固定。

所以这里的打印结果0~9应该输出一部分是MyThread类中的,然后在输出main函数中的,输出的内容不固定,这样才对。

但是如果使用线程类对象直接调用run函数,其实这里并没有启动线程,因为启动线程要调用系统资源,开辟内存空间。而run函数中只是执行线程任务的代码,所以这里打印的结果是按顺序打印的,和我们之前调用一般函数并没有区别。

那么怎么能够启动线程呢?

通过查阅API得知需要调用start()函数才能够启动线程。

启动线程需要使用Thread类中的start()函数。在这个函数的底层调用了系统资源,开辟内存空间,同时调用了该线程的run函数。

也就是说调用了start()函数既启动了线程又调用了run函数。

void start() 导致此线程开始执行; Java虚拟机调用此线程的run方法。 

所以修改上述代码变为:

class MyThread extends Thread
{
	//B:复写Thread类中的run函数
	/*
	 * 这个函数就是执行线程任务的函数
	 * 开启线程的目的就是要执行代码,比如360体检、杀毒、清理垃圾等
	 */
	public void run() {
		//这里就是线程要执行的代码
		for (int i = 0; i < 10; i++) {
			System.out.println(i);
		}
	}
}
public class ThreadDemo1 {
	public static void main(String[] args) {
		//C:创建线程类的对象
		MyThread mt = new MyThread();
		mt.start();
		for (int i = 0; i < 10; i++) {
			System.out.println(i);
		}
	}
}

正确的输出结果:

0
1
2
3
0
1
2
3
4
5
6
7
8
9
4
5
6
7
8
9

线程执行代码的特点就是CPU随机执行代码,执行代码顺序不固定。

几个疑问

1)为什么要继承Thread类?

在Java中使用Thread这个类对线程进行描述。而我们现在希望通过自己的代码操作线程,自己的代码应该需要和Thread类之间产生关系。这里我们采用的继承的关系。

当我们继承了Thread类之后,我们自己的类也就变成了线程类。我们自己的类就继承到了Thread类中的所有功能,就具备了操作线程的各种方法,并且自己的类就可以对线程进行各种操作(开启线程等)。

而我们自己定义的类称为了一个线程类,主要是因为可以复写run方法。

2)为什么要复写run方法?

为什么要使用线程:因为我们希望程序中的某段代码可以同时运行,提高程序的运行效率。

我们定义的类继承了Thread类之后,其实在Thread类中有个run方法,它是开启线程之后,就会直接去运行的方法。而Java在设计线程类(Thread)的时候,就已经明确了线程应该执行的某段代码需要书写在run方法中,也就是说在run方法中的代码开启线程之后才能正常的运行。

我们使用线程的目的是让线程执行后来自己程序中的某些代码, 而Java中规定需要线程执行的代码必须写run方法中,Thread类中的run方法中并没有我们真正需要多线程运行的代码,而开启线程又要去运行run方法,这时我们只能沿用Thread类run方法的定义格式,然后复写run方法的方法体代码。

简单来讲:

​ 设计Thread这个API的人,在设计的时候,只设计了如何启动线程,至于线程要执行什么任务,他并不知道。所以,他这样设计:就是start启动线程之后,JVM会自动的调用run方法。

因此,我们只要把自己的代码写到run方法中,就一定会被执行到。

3)为什么要调用start而不是run?

当书写了一个类继承了Thread类之后,这个子类也变成线程类。这时可以创建这个子类的对象,一旦创建Thread的子类对象,就相当于拥有了当前的线程对象。

创建Thread的子类对象,只是在内存中有了线程这个对象,但是线程还不能真正的去运行。

要让线程真正的在内存运行起来,必须调用start方法,因为start方法会先调用系统资源,启动线程。这样才能够在内存开启一片新的内存空间,然后负责当前线程需要执行的任务。

我们直接通过线程对象去调用run方法,这时只是对象调用普通的方法,并没有调用系统资源,启动线程,也没有在内存中开启一个新的独立的内存空间运行任务代码。只有调用start方法才会开启一个独立的新的空间。并在新的空间中自动去运行run方法。

注意:run方法仅仅是封装了线程的任务。它无法启动线程。

面试题:start方法和run方法的区别?

​ run:只是封装线程任务。

​ start:先调用系统资源,在内存中开辟一个新的空间启动线程,再执行run方法。

4)同一个线程对象是否可以多次启动线程?

不能。如果同一个对象多次启动线程就会报如下异常。

注意:如果想要启动多个线程,可以重新再创建一次自定义线程类的对象调用一次start()函数来启动线程。

OK 相信我们都看到多线程的现象了,那么接下来几天我们就进入多线程的世界!

获取、设置线程名称

通过以上代码的结果,我们发现只能打印出结果,但是不知道是哪个线程打印的,所以接下来我们需要知道是哪些线程打印的结果,那么我们就得先获取线程的名字,然后才能知道是哪些线程打印的结果。

问题一、那么如何获得线程的名字呢?

要想获得当前线程的名字,首先必须先获得当前线程的对象,然后根据当前线程对象调用线程类Thread类中的方法 String getName()就可以获取当前线程的名称。

问题二、如何获取当前线程的对象?

在Thread类中提供一个函数可以返回当前正在执行的线程的对象,这个函数是:

static Thread currentThread()返回对当前正在执行的线程对象的引用。 

说明:这个函数是静态函数,直接通过这个函数所属的类名Thread直接调用即可。

补充:既然有获取线程的名字的函数,那么在Thread类中肯定还会有给线程设置名字的函数,setName(String name)。

void setName(String name) 改变线程名称,使之与参数 name 相同。

需求:演示:获取和设置线程的名称。

分析和步骤:

1)代码和上述代码一致,在run函数中书写getName()函数获取线程的名字;

2)在测试类中分别使用自定义类的线程对象给线程设置新的名字;

代码如下所示:

/*
 需求:演示:获取和设置线程的名称。
 获取线程的名字使用函数getName()
 线程的默认名字是:Thread-x x是从0开始的递增数字
 */
//定义一个类继承Thread,这个类称为线程类
class MyThread2 extends Thread {
	// B:复写Thread类中的run函数
	public void run() {
		// 这里就是线程要执行的代码
		/*
* Thread.currentThread().getName();获取当前线程对象的名称
		 * 由于getName()函数属于Thread类中的函数,而Thread类属于MyThread2的父类
		 * 所以可以直接通过线程对象调用getName()函数就可以获得线程的名字。
*  哪个线程调用run函数,Thread.currentThread()就会获得哪个线程的对象
		 */
		for (int i = 0; i < 10; i++) {
			System.out.println(Thread.currentThread().getName()+"..."+i);
		}
	}
}
public class ThreadDemo2 {

	public static void main(String[] args) {
		// C:创建线程类的对象
		MyThread2 mt = new MyThread2();
		// D:启动线程,执行任务
		mt.start();
		// 再创建一个线程对象
		MyThread2 mt2 = new MyThread2();
		// 再一次开辟一个新的线程
		mt2.start();
		for (int i = 0; i < 10; i++) {
			System.out.println(i);
		}
	}
}

说明:Thread.currentThread().getName();获取当前线程对象的名称。由于getName()函数属于Thread类中的函数,而Thread类属于MyThread2的父类,所以可以直接通过线程对象调用getName()函数就可以获得线程的名字。哪个线程调用run函数,Thread.currentThread()就会获得哪个线程的对象。

结果如下所示:

Thread-0.....0

Thread-1.....0

通过以上结果发现多线程是有默认名字的,即Thread-x  x是从0开始递增数字。

但是如果我们想给多线程设置我们自己想要的名字,就不会使用默认名字,可以通过Thread类中的setName()进行设置。

代码如下所示:

public class ThreadDemo2 {

	public static void main(String[] args) {
		// C:创建线程类的对象
		MyThread2 mt = new MyThread2();
		// 再创建一个线程对象
		MyThread2 mt2 = new MyThread2();
        //给线程起名字
        mt.setName("mt");
        mt2.setName("mt2");
		// D:启动线程,执行任务
		mt.start();
		// 再一次开辟一个新的线程
		mt2.start();
		for (int i = 0; i < 10; i++) {
			System.out.println(i);
		}
	}
}

设置完名字后结果如下所示:

mt.....0

mt2.....0

多线程程序运行路径图解

说明:这个程序总共开辟了3条执行路径,CPU就会在三者之间来回切换。并发运行,所以这是一个多线程程序。

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

通过之前的学习我们得知实现多线程的第一种方式是继承Thread类,那么第二种方式是什么呢?

API描述:

创建线程的另一种方法是声明一个实现Runnable接口的类。 那个类然后实现了run方法。 然后可以分配类的实例,在创建Thread时作为参数传递,并启动。 

通过以上API得知,第二种方式是创建一个类来实现Runnable接口,并实现Runnable接口中的run函数。

Runnable接口介绍

Runnable接口中只有一个函数如下所示:

void run()当使用实现接口 Runnable的对象来创建线程时,启动线程将使该对象的 run方法在单独执行的线程中被调用。 

说明:

1)我们发现,Thread类其实已经实现Runnable接口了,Thread类中的run方法就是实现来自Runnable接口的。

2)Runnable接口中,只有一个run方法,而run方法是用来封装线程任务的。因此,这个接口就是专门用来封装线程任务的接口。为了区分和上述的Thread类,

因此,我们可以这样理解:实现该接口的类,称为任务类。

实现步骤

A:自定义类,实现Runnable接口,这个类就是任务类;

B:实现run方法,run函数中书写的任务代码;

C:创建任务类的对象;

D:创建Thread类对象,并且把任务对象作为参数传递;

​ 说明:既然任务类的对象要作为Thread类构造函数的参数传递,而任务类我们又不知道叫什么名字,但是任务类需要实现Runnable接口,所以在Thread类的构造函数中肯定有Runnable接口类型作为参数接收。

构造方法:

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

注意:第二个构造函数的String类型的name是给自己定义的任务类线程起名字,不用在使用setName()给线程起名字。

E:启动线程

代码如下所示:

/*
 * 演示实现多线程方式2:实现Runnable接口
 * A:定义一个类来实现Runnable接口,这个类是任务类
 * B:实现Runnable接口中的run函数
 * C:创建任务类对象
 * D:创建线程类Thread的对象,并把任务类对象作为参数传递
 * E:启动线程
 */
//A:定义一个类来实现Runnable接口,这个类是任务类
class MyTask implements Runnable
{
	//B:实现Runnable接口中的run函数
	public void run() {
		for (int i = 0; i < 10; i++) {
			System.out.println(Thread.currentThread().getName()+"==="+i);
		}
	}
}
public class ThreadDemo3 {
	public static void main(String[] args) {
		//C:创建任务类对象
		MyTask task = new MyTask();
//		D:创建线程类Thread的对象,并把任务类对象作为参数传递
		Thread t1 = new Thread(task,"锁哥");
		Thread t2 = new Thread(task,"助教");
		//E:启动线程
		t1.start();
		t2.start();
		for (int i = 0; i < 10; i++) {
			System.out.println(Thread.currentThread().getName()+"==="+i);
		}
	}
}

关于调用run方法的疑问

方式1:自定义类继承Thread类,重写run方法,调用start启动线程,会自动调用我们线程类中的run方法。

方式2:自定义类,实现Runnable接口,把任务对象传给Thread对象。调用Thread对象的start方法,执行Thread的run。那么为什么最后执行的是任务类中的run呢?

方式二的好处

A:避免了Java单继承的局限性;

说明:如果使用方式一,那么在Java中一个类只能有一个直接父类,如果一个类已经继承其他的父类,那么当前这个类中假如有需要多线程操作的代码,这时这个类是无法再继承Thread类的。这样就会导致当前这个类中的某些需要多线程执行的任务代码就无法被线程去执行。

B:把线程代码和任务的代码分离,解耦合(解除线程代码和任务的代码模块之间的依赖关系)。代码的扩展性非常好;

说明:Thread类是专门负责描述线程本身的。Thread类可以对线程进行各种各样的操作。如果使用第一种方式,那么把线程要执行的任务也交给了Thread类。这样就会导致操作线程本身的功能和线程要执行的任务功能严重的耦合在一起。

但是方式二,自定义一个类来实现Runnable接口,这样就把任务抽取到Runnable接口中,在这个接口中定义线程需要执行的任务的规则。当需要明确线程的任务时,我们就让这个类实现Runnable接口,只要实现Runnable接口的类,就相当于明确了线程需要执行的任务。

​ 当一个类实现Runnable接口,就相当于有了线程的任务,可是还没有线程本身这个对象。这时我们就可以直接使用Thread这个类创建出线程,然后把任务交给线程。这样就达到任务和线程的分离以及结合。

3 匿名内部类方式实现线程的创建

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

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

public static void main(String[] args) {
		//使用匿名内部类实现多线程 r表示任务类的对象
		Runnable r=new Runnable(){
			public void run() {
				for (int i = 0; i < 10; i++) {
					System.out.println(Thread.currentThread().getName()+"---"+i);
				}
			}
		};
		//创建线程类对象
		Thread t1=new Thread(r,"t1");
		Thread t2=new Thread(r,"t2");
		//启动线程
		t1.start();
		t2.start();
	}

5线程控制

线程休眠(掌握)

使用Thread类中的sleep()函数可以让线程休眠,函数如下所示:

static void sleep(long millis) 在指定的毫秒数内让当前正在执行的线程休眠(暂停执行)

说明:这个函数是静态的,使用线程类名调用。使用哪个线程调用就让哪个线程休眠。

代码如下所示:

分析和步骤:

1)创建一个测试类SleepDemo ,并添加main函数;

2)创建一个线程任务类SleepTask 来实现Runnable接口,并实现run函数,打印0到9十个数字,并且使用Thread类调用sleep()函数让当前线程睡一秒,并使用Date类的对象来获得睡眠的时间;

3)在main函数中创建线程任务类对象st;

4)创建线程类对象t,并给线程起名为兔子;

5)启动线程;

/*
 * 演示线程的休眠
 * public static void sleep(long millis)
 */
//定义一个线程任务类
class SleepTask implements Runnable
{
	//实现run函数
	public void run() {
		for (int i = 0; i < 10; i++) {
			System.out.println(Thread.currentThread().getName()+"---"+i+new Date());
			//让线程睡一秒
			try {
				Thread.sleep(1000);
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
		}
	}
}
public class SleepDemo {
	public static void main(String[] args) {
		//创建任务类对象
		SleepTask st = new SleepTask();
		//创建线程对象
		Thread t = new Thread(st,"兔子");
		//启动线程
		t.start();
	}
}

第二章 线程安全

1 多线程安全问题

  • 多个线程在对共享数据进行读改写的时候,可能导致的数据错乱就是线程的安全问题了。

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

分析:

最近万达影城上映:《葫芦娃大战奥特曼》 , 我们现在模拟一下电影院卖票:

我就卖一个放映厅中100张票。

我们有多个售票窗口,同时对外出售这100张票。

我们可以用线程来模拟售票的窗口。每个窗口可以认为是一个线程。窗口售票的过程,就可以认为是线程的任务

步骤:

1)定义一个测试类SellTicektDemo ,并定义一个main函数;

2)定义一个线程类SellTicketTask 来实现Runnable接口;

3)在SellTicketTask 任务类中定义一个变量tickets来存储100张票;

4)在run函数中使用循环模拟一直卖票,使用判断结构根据变量票数tickets是否大于0来确定是否还有余票;

5)如果有余票,使用打印语句来模拟卖票,然后票数量变量tickets-1;

6)在main函数中创建任务类对象stt,同时并创建四个线程类对象来模拟四个窗口,最后启动线程;

/*
 * 售票,多个售票窗口卖100张票
 */
//定义线程任务类
class SellTicketTask implements Runnable
{
	//定义100张票
	private int tickets=100;
	//实现run函数
	public void run() 
	{
		//使用循环模拟一直卖票
		while(true)
		{
			//判断是否还有余票
			if(tickets>0)
			{
			    try {
                    Thread.sleep(10);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
				//有余票 使用打印语句来模拟卖票
				System.out.println(Thread.currentThread().getName()+"出票:"+tickets);
				//票数量-1
				tickets--;
			}
		}
	}
}
public class SellTicketDemo {
	public static void main(String[] args) {
		//创建线程任务类对象
		SellTicketTask stt = new SellTicketTask();
		//创建线程对象,四个线程模拟四个窗口
		Thread t1 = new Thread(stt,"窗口1");
		Thread t2 = new Thread(stt,"窗口2");
		Thread t3 = new Thread(stt,"窗口3");
		Thread t4 = new Thread(stt,"窗口4");
		//启动线程
		t1.start();
		t2.start();
		t3.start();
		t4.start();
	}
}

2 多线程安全问题分析

通过上述代码,我们发现输出的结果有如下问题:

上述代码出现的问题:重复票、跳票等问题。

出现上述问题的图解如下图所示:

原因分析:

​ A:多线程程序,如果是单线程就不会出现上述卖票的错误信息;

​ B:多个线程操作共享资源,如果多线程情况下,每个线程操作自己的也不会出现上述问题;

​ C:操作资源的代码有多行,如果代码是一行或者很少的情况下,那么一行代码很快执行完毕,也不会出现上述情况;

​ D:CPU的随机切换。本质原因是CPU在处理多个线程的时候,在操作共享数据的多条代码之间进行切换导致的;

3 多线程安全问题解决

解决方案:

​ A:无法改变,就是多线程程序。

​ B:无法改变,多个线程就是要操作同一资源。

​ C:无法改变,因为就是有多行代码

​ D:CPU的运行我们无法解决。针对CPU的切换,由操作系统去控制,而我们人为是无法干预。因此这个问题解决不了。

要解决安全问题:

​ 可以人为的控制CPU在执行某个线程操作共享数据的时候,不让其他线程进入到操作共享数据的代码中去,这样就可以保证安全。

​ 上述的这个解决方案:称为线程的同步。使用 synchronized关键字。

synchronized关键字概述

  • synchronized关键字:表示“同步”的。它可以对“多行代码”进行“同步”——将多行代码当成是一个完整的整体,一个线程如果进入到这个代码块中,会全部执行完毕,执行结束后,其它线程才会执行。这样可以保证这多行的代码作为完整的整体,被一个线程完整的执行完毕。

  • synchronized被称为“重量级的锁”方式,也是“悲观锁”——效率比较低。

  • synchronized有几种使用方式:
    a).同步代码块

    b).同步方法【常用】

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

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

根据案例简述:

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

同步代码块

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

格式:

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

同步锁:

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

  1. 锁对象 可以是任意类型。
  2. 多个线程对象要使用同一把锁才能起到同步作用。
  3. 操作共享数据的代码需要加同步

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

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

上述代码为了避免多线程的安全问题,我们需要把上述卖票的代码加上同步代码块,这样就可以解决多线程的安全问题。

/*
 * 售票,多个售票窗口卖100张票
 */
//定义线程任务类
class SellTicketTask implements Runnable
{
	//定义100张票
	private int tickets=100;
	//定义一个对象充当同步代码块上的锁
	private Object obj = new Object();
	//实现run函数
	public void run() 
	{
		//使用循环模拟一直卖票
		while(true)//t1 t2
		{
			//为了解决多线程的安全问题,给操作的共享资源代码加同步
			synchronized(obj)//t1 进来 关上门 上锁,此时t2进不来
			{
				//判断是否还有余票
				if(tickets>0)
				{
					//休眠1毫秒,模拟延迟
					try {Thread.sleep(1);} catch (InterruptedException e) {e.printStackTrace();}
					//有余票 使用打印语句来模拟卖票
					System.out.println(Thread.currentThread().getName()+"出票:"+tickets);
					//票数量-1
					tickets--;
				}
			}
			//t1出来,释放锁,打开门 其他线程可以进入
		}
	}
}
public class SellTicketDemo {
	public static void main(String[] args) {
		//创建线程任务类对象
		SellTicketTask stt = new SellTicketTask();
		//创建线程对象,四个线程模拟四个窗口
		Thread t1 = new Thread(stt,"窗口1");
		Thread t2 = new Thread(stt,"窗口2");
		Thread t3 = new Thread(stt,"窗口3");
		Thread t4 = new Thread(stt,"窗口4");
		//启动线程
		t1.start();
		t2.start();
		t3.start();
		t4.start();
	}
}

同步方法

1)演示同步方法

代码和上述代码几乎差不多,只是将同步代码块处调用方法method,然后将同步代码块变为同步方法。

如下所示:

如上图所示,如果一个方法进来后,直接就是同步,也就是说,这个方法的所有代码都需要被同步。此时我们可以考虑把同步直接加到方法上:

以上被synchronized关键字修饰的方法称为同步方法。

注意:

​ 1.非静态同步方法的锁是this;

​ 2.如果一个方法内部,所有代码都需要被同步,那么就用同步方法;

静态同步方法

1)演示

既然有非静态同步方法,那么肯定也会有静态同步方法。

将上述非静态同步方法改为静态同步方法,代码如下所示:

问题:非静态同步方法有隐式变量this作为锁,那么静态方法中没有this,那么静态同步方法中的锁又是什么呢?

​ 静态同步方法的锁是:当前类的字节码文件对象(Class对象)。

​ 其实可以这么理解:什么是字节码文件对象呢?其实就是class文件,类名.class。比如这里,就是  SellTicketTask.class

​ 获取Class对象的方式:类名.class; (每个类只有一个字节码对象)

总结:

同步代码块:锁是任意对象,但是必须唯一;

非静态同步方法:锁是this;

静态同步方法:锁是当前类的字节码文件对象;类名.class

第三章 Lock锁(掌握)

从jdk5后java.util.concurrent.locks.Lock机制提供了比synchronized代码块和synchronized方法更广泛的锁定操作,同步代码块/同步方法具有的功能Lock都有,除此之外更强大.Lock锁是Java中更加符合编程习惯的解决方式。

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

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

由于Lock属于接口,不能创建对象,所以我们可以使用它的子类ReentrantLock来创建对象并使用Lock接口中的函数。

需求:使用Lock实现线程安全的卖票。

分析和步骤:

1)定义一个卖票的任务类SellTicketTask 类并实现Runnable接口;

2)在任务类中定义一个成员变量tickets保存票数100;

3)定义一把锁Lock的对象l;

4)在run函数中模拟卖票,if语句的上面使用锁对象l调用lock()函数获取锁,等待if语句结束之后,使用锁对象l调用unlock()函数释放锁;

5)定义测试类SellTicketDemo ,在这个类中分别创建任务类的对象和线程类的对象,并使用线程类的对象调用start()函数来启动线程;

/*
 * 需求:使用Lock实现线程安全的卖票。
 * Lock是接口,只能通过他的子类ReentrantLock创建对象
 * 构造函数 ReentrantLock()  创建一个 ReentrantLock 的实例。
 *  void lock() 获取锁。 
 *  void unlock() 试图释放此锁。 
 */
//定义一个任务类用来卖票
class SellTicketTask implements Runnable
{
	//定义100张票
	private static int tickets=100;
	//创建对象作为任意一把锁
//	private Object obj=new Object();
	//定义一把锁
	Lock l=new ReentrantLock();
	//模拟卖票
	public void run() {
		/*while(true)
		{
			synchronized (obj) {
				if(tickets>0)
				{
					System.out.println(Thread.currentThread().getName()+"出票:"+tickets--);
				}
			}
		}*/
		//使用Lock锁替换synchronized
		while(true)
		{
				//获取锁
				l.lock();
				if(tickets>0)
				{
					try {Thread.sleep(1);} catch (InterruptedException e) {}
					System.out.println(Thread.currentThread().getName()+"出票:"+tickets--);
				}
				//释放锁
				l.unlock();
		}
	}
}
public class SellTicketDemo {
	public static void main(String[] args) {
		// 创建任务类对象
		SellTicketTask stt = new SellTicketTask();
		//创建线程对象
		Thread t1 = new Thread(stt,"窗口1");
		Thread t2 = new Thread(stt,"窗口2");
		Thread t3 = new Thread(stt,"窗口3");
		Thread t4 = new Thread(stt,"窗口4");
		//启动线程
		t1.start();
		t2.start();
		t3.start();
		t4.start();
	}
}

第四章 死锁

1. 什么是死锁

死锁:是指两个或者两个以上的线程在执行的过程中,因争夺资源产生的一种互相等待的现象。

死锁在开发中,也会遇到,当线程进入到死锁状态时,程序中线程就会一直处于等待状态。

死锁的发生:

举例1:

我请柳岩吃饭(麻辣烫)

我点了一份。上了一双筷子

我:一支

柳岩:一支

我和柳岩就相当于两个线程,而麻辣烫相当于共享资源,两个人都没办法吃,都在等待看着麻辣烫,这种现象就是死锁。

举例2:有两个线程(t1线程、t2线程),有两个对象锁(lock_a、lock_b)

​ t1线程在执行时,先拿到lock_a对象锁(此时lock_a对象锁绑定在t1线程上)。而正在此时CPU切换到t2线程上,t2线程拿到lock_b对象锁(此时lock_b对象锁绑定在t2线程上),这时CPU又切换到t1线程上,这时t1线程需要拿lock_b对象锁,此时t1线程获取不到lock_b对象锁(t1线程处于等待)。

当CPU切换到t2线程上,这时t2线程需要拿lock_a对象锁,此时t2线程获取不到lock_a对象锁(B线程处于等待)。

上述案例2简化如下说法:

​ 有2个线程,需要执行相同的任务,但是需要分别获取的A锁和B锁才能去执行,第一个线程获取锁的顺序是先A后B。第二个线程获取锁的顺序是先B后A。

当第一个线程获取A锁,CPU切换到第二个线程,此时第二个线程获取B锁。而此时第一个线程缺少B锁,第二个线程缺少A锁。两个线程都在等待,发生死锁现象。

2.产生死锁的条件

  1. 有多把锁

  2. 有多个线程

  3. 有同步代码块嵌套

3. 代码演示

分析和步骤:

1)创建一个任务类DeadLockTask 实现Runnable接口,复写run函数;

2)创建两个Object类的对象lock_a,lock_b作为锁对象;

3)定义一个变量flag,让不同的线程切换到不同的地方去执行,按照不同的方式来获取锁;

4)在run函数中使用if-else结构来控制两个线程去执行不同的内容,并使用while循环一直让其执行;

5)在if中嵌套书写两个同步代码块lock_a和lock_b分别作为两个代码块的锁,将if中相同的内容复制一份写到else中;

6)创建测试类DeadThreadLockDemo,在这个类的主函数中创建任务类的对象;

7)创建两个线程对象t1和t2;

8)让主线程休息1毫秒;

9)使用t1对象调用start函数开启线程,让下一个线程进入到else中;

10)开启t2线程;

代码如下所示:

/*
 * 演示线程死锁的问题
 */
//定义一个线程任务类
class DeadLockTask implements Runnable
{
	//定义两个锁对象
	private Object lock_a=new Object();
	private Object lock_b=new Object();
	//定义一个变量作为标记,控制取锁的方式
	 boolean flag=true;
	public void run() {
		//当线程进来之后,一个线程进入到if中,另一个进入到else中
		if(flag)
		{
			while(true)
			{
				synchronized(lock_a)
				{
					System.out.println(Thread.currentThread().getName()+"if.....lock_a");
					synchronized(lock_b)
					{
						System.out.println(Thread.currentThread().getName()+"if.....lock_b");
					}
				}
			}
		}else
		{
			while(true)
			{
				synchronized(lock_b)
				{
					System.out.println(Thread.currentThread().getName()+"else.....lock_b");
					synchronized(lock_a)
					{
						System.out.println(Thread.currentThread().getName()+"else.....lock_a");
					}
				}
			}
		}
	}
}
public class DeadThreadLockDemo {

	public static void main(String[] args) {
		// 创建任务类对象
		DeadLockTask dlt = new DeadLockTask();
		//创建线程对象
		Thread t1 = new Thread(dlt);
		Thread t2 = new Thread(dlt);
		//开启第一个线程
		t1.start();
		//修改标记让下一个线程进入到else中
		dlt.flag=false;
		t2.start();
	}
}

上述代码的结果如下图所示:

通过以上结果发现,线程一直在执行else中的代码,根本就没有执行if语句中的代码。

说明:

出现上述结果是因为在主线程开启第一个线程之后,很有可能CPU还在主线程上运行,那么开启的线程是不会被CPU立刻去执行,而CPU继续处理主线程中的代码, 就会直接去执行d.flag = false; ,这时就已经把标记修改成false,不管线程是否进入到run 方法中,flag都已经变成false,那么就无法在进入if中,因此我们为了保证第一个线程一定能够进入到if中,于是在这里让主线程在开启第一个线程之后,主线程进行休眠1毫秒。

死锁的结果如下图所示:

注意:在开发中一旦发生了死锁现象,不能通过程序自身解决。必须修改程序的源代码。

​ 在开发中,死锁现象可以避免,但不能直接解决。当程序中有多个线程时,并且多个线程需要通过嵌套对象锁(在一个同步代码块中包含另一个同步代码块)的方式才可以操作代码,此时就容易出现死锁现象。

​ 可以使用一个同步代码块解决的问题,不要使用嵌套的同步代码块,如果要使用嵌套的同步代码块,就要保证同步代码块的上的对象锁使用同一个对象锁(唯一的对象锁)

第五章 线程状态

线程状态概述

线程由生到死的完整过程:技术素养和面试的要求。

当线程被创建并启动以后,它既不是一启动就进入了执行状态,也不是一直处于执行状态。在线程的生命周期中,有几种状态呢?在API中java.lang.Thread.State这个枚举中给出了六种线程状态:

这里先列出各个线程状态发生的条件,下面将会对每种状态进行详细解析

线程状态 导致状态发生条件
NEW(新建) 线程刚被创建,但是并未启动。还没调用start方法。MyThread t = new MyThread()只有线程对象,没有线程特征。
Runnable(可运行) 线程可以在java虚拟机中运行的状态,可能正在运行自己代码,也可能没有,这取决于操作系统处理器。调用了t.start()方法 :就绪(经典教法)
Blocked(锁阻塞) 当一个线程试图获取一个对象锁,而该对象锁被其他的线程持有,则该线程进入Blocked状态;当该线程持有锁时,该线程将变成Runnable状态。
Waiting(无限等待) 一个线程在等待另一个线程执行一个(唤醒)动作时,该线程进入Waiting状态。进入这个状态后是不能自动唤醒的,必须等待另一个线程调用notify或者notifyAll方法才能够唤醒。
Timed Waiting(计时等待) 同waiting状态,有几个方法有超时参数,调用他们将进入Timed Waiting状态。这一状态将一直保持到超时期满或者接收到唤醒通知。带有超时参数的常用方法有Thread.sleep 、Object.wait。
Teminated(被终止) 因为run方法正常退出而死亡,或者因为没有捕获的异常终止了run方法而死亡。

我们不需要去研究这几种状态的实现原理,我们只需知道在做线程操作中存在这样的状态。那我们怎么去理解这几个状态呢,新建与被终止还是很容易理解的,我们就研究一下线程从Runnable(可运行)状态与非运行状态之间的转换问题。

第六章 等待唤醒机制(包子铺卖包子)

​ 我们的卖票案例中,所有的线程都是在做相同的任务:卖票。而我们真实情况中,不同的线程有可能需要去做不同的任务。在程序中,最常见的一种模型,就是生产者和消费者模型。生产者线程和消费者线程之间需要进行通信,我们可以使用等待唤醒机制来实现生产者线程和消费者线程之间的通信。

  • Object类的方法

    wait()		:让当前线程进入等待状态
    notify()	:唤醒一个正在等待的线程,唤醒是随机的
    void notifyAll() 唤醒在此对象监视器上等待的所有线程。 
    
    注意事项: 必须要使用锁对象来调用的。
    
    • 两个方法的小疑问

      • 两个方法为什么要定义在Object类中?

        因为需要用锁对象调用这两个方法,任意对象都可以作为锁对象。
        也就是说任意类型的对象都可以调用的两个方法,就需要定义在Object类中
        
      • 两个方法必须写在同步里面吗?

        两个方法必须要在同步里面调用,因为在同步里面才有锁对象。
        
    • 如果一个线程执行了wait()方法,那么当前线程进入等待状态,并且会释放锁对象,下次即使被唤醒必须获取到锁对象才可以执行。

    代码演示:

    public class MyRun1 implements Runnable {
        @Override
        public void run() {
            synchronized ("abc"){
                try {
                    //等待
                    "abc".wait();
                } catch (InterruptedException e) {
                }
            }
            System.out.println(1);
            System.out.println(2);
        }
    }
    
    public class MyRun2 implements Runnable {
        @Override
        public void run() {
            synchronized ("abc"){
                //notify()也必须要用锁对象调用
                "abc".notify();
    
                System.out.println("A");
                System.out.println("B");
            }
        }
    }
    
    public class Test01 {
        public static void main(String[] args) throws Exception{
            //只能用锁对象调用  别的对象调用就会报错
    //        "abc".wait();
    
            //开启线程
            MyRun1 mr = new MyRun1();
            new Thread(mr).start();
    
            //睡一秒钟
            Thread.sleep(1000);
    
            MyRun2 mr2 = new MyRun2();
            new Thread(mr2).start();
        }
    }
    
  • 包子案例

    说明:

    ​ 1.定义一个包子类,类中成员变量:

     pi //皮儿
     xian //馅儿
     flag://用来表示有没有包子,用true来代表有   用false来代表没有
    

    ​ 2.定义一个生产包子的任务类即生产者线程类:

    生产者线程思想:如果有包子就不需要制作,让生产者线程进入等待状态;如果没有包子,开始制作包子,并且唤醒消费者线程来吃包子
    

    ​ 3.定义一个消费包子的任务类即消费者线程类:

    消费者线程思想:如果没有包子就不消费,让消费者线程进入等待状态;如果有包子,开始吃包子,并且唤醒生产者线程来生产包子
    
  /*
      包子类需要定义3个成员变量:
          pi
          xian
          flag:表示是否有包子
   */
  //包子类
  public class BaoZi {
      //皮儿
      String pi;
      //馅儿
      String xian;
      //布尔值
      boolean flag=false;  //用来表示有没有包子,用true来代表有   用false来代表没有
  }
  
  //生产包子:生产者线程执行的任务
  /*
      生产者线程思想:如果有包子就不需要制作,让生产者线程进入等待状态;如果没有包子,开始制作包子,并且唤醒消费者线程来吃包子
   */
  public class ZhiZuo implements Runnable {
      //成员变量
      BaoZi baoZi;
      //构造方法
      public ZhiZuo(BaoZi baoZi) {
          this.baoZi = baoZi;
      }
  
      @Override
      public void run() {
          //制作包子
          while (true){
              synchronized ("锁"){//t1
                  if(baoZi.flag == true){
                      //如果有包子就不需要制作
                      //就让制作的线程进入等待状态
                      try {
                          "锁".wait();
                      } catch (InterruptedException e) {
                      }
                  }else{
                      //else表示没有包子
                      //制作包子
                      baoZi.pi = "白面";
                      baoZi.xian = "韭菜大葱";
                      //修改包子状态
                      baoZi.flag = true;
                      System.out.println("生产出了一个包子!");
  
                      //生产好了包子叫醒吃货(消费者)来吃
                      "锁".notify();
                  }
              }
          }
      }
  }
  
  //吃包子:消费者线程执行的任务
  /*
      消费者线程思想:如果没有包子就不消费,让消费者线程进入等待状态;如果有包子,开始吃包子,并且唤醒生产者线程来生产包子
   */
  public class ChiHuo implements Runnable {
      //成员变量
      BaoZi baoZi;
      //构造方法
      public ChiHuo(BaoZi baoZi) {
          this.baoZi = baoZi;
      }
  
      @Override
      public void run() {
          //吃包子
          while(true){
              synchronized ("锁"){
                  if(baoZi.flag == false){
                      //没包子
                      //让吃包子的线程进入等待
                      try {
                          "锁".wait();
                      } catch (InterruptedException e) {
                      }
                  }else{
                      //else表示有包子
                      //开吃
                      System.out.println("吃货吃了一个" + baoZi.pi+"皮儿," + baoZi.xian + "馅儿的大包子");
                      baoZi.pi = null;
                      baoZi.xian = null;
                      //修改包子状态
                      baoZi.flag = false;
  
                      //吃完包子叫醒对方(生产者)来做
                      "锁".notify();
                  }
              }
          }
      }
  }
  
  //测试类
  public class Test01 {
      public static void main(String[] args) {
          //创建包子
          BaoZi baoZi = new BaoZi();
          //创建对象
          ZhiZuo zz = new ZhiZuo(baoZi);
          Thread t1 = new Thread(zz);//生产者线程
          t1.start();
          //创建对象:消费者线程
          ChiHuo ch = new ChiHuo(baoZi);
          Thread t2 = new Thread(ch);
          t2.start();
      }
  }

第七章 wait和sleep区别

1.sleep(time) 属于Thread类中的,静态方法直接使用类名调用,让当前某个线程休眠,休眠的线程cpu不会执行,该方法可以使用在同步中也可以不使用在同步中,和锁对象无关,如果使用在同步中,不会释放锁对象,直到线程休眠时间到自然醒,然后cpu继续执行

2.等待方法:
	1)wait(time) :可以让某个线程计时等待,时间到自然醒,或者时间未到,中途被其他线程唤醒
	2)wait()无参数的方法,让某个线程无限等待,只能被其他线程唤醒
等待方法位于Object类中,必须在synchronized中使用,必须使用锁对象调用, 和锁对象有关。当某个线程遇到等待方法那么会立刻释放锁对象,cpu不会执行等待的线程,如果某个线程被唤醒,那么必须具有锁对象才可以执行,没有锁对象进入到锁阻塞状态。只有获取到锁对象才进入运行状态

第八章 jdk5后的Lock实现线程之间的通讯

如果使用Lock解决多线程安全问题,并实现通讯,我们需要借助于Condition.

使用步骤:

1.创建Lock锁对象:Lock l = new ReentrantLock();
2.
   上锁 l.lock()
   释放锁:l.unlock()
3.使用锁对象调用Lock中的方法获取Condition对象:Condition newCondition() 返回绑定到此 Lock 实例的新 Condition 实例。 
   Condition c =  l.newCondition() ;
	说明:
        Condition是一个接口,Condition 替代了 Object 监视器方法的使用。 和Lock一起使用完成线程的通讯
4.
        线程等待:c.await()  ;
		唤醒线程:c.signal() 或者c.signalAll() 

第九章 线程池方式

1 线程池的思想

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

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

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

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

2 线程池概念

程序启动一个新线程成本是比较高的,因为它涉及到要与操作系统进行交互。因为启动线程的时候会在内存中开辟一块空间,消耗系统资源,同时销毁线程的时候首先要把和线程相关东西进行销毁,还要把系统的资源还给系统。这些操作都会降低操作性能。尤其针对一个线程用完就销毁的更加降低效率。

而使用线程池可以很好的提高性能,尤其是当程序中要创建大量生存期很短的线程时,生存期较短的线程指的是用完一次线程就丢掉。更应该考虑使用线程池。

线程池里的每一个线程代码结束后,并不会死亡,而是再次回到线程池中成为空闲状态,等待下一个对象来使用。

线程池工作原理如下图所示:

需求:我有一段任务,需要执行100次。

image-20200501114722456

说明:

每次线程执行完任务以后,线程不会销毁,会放回线程池中,每次在执行任务的时候又会到线程池中去取线程。这样会提高效率。

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

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

3 线程池的使用

Java里面线程池的顶级接口是java.util.concurrent.Executor,以及他的子接口java.util.concurrent.ExecutorService

要配置一个线程池是比较复杂的,尤其是对于线程池的原理不是很清楚的情况下,很有可能配置的线程池不是较优的,因此在java.util.concurrent.Executors线程工厂类里面提供了一些静态工厂,生成一些常用的线程池。官方建议使用Executors工程类来创建线程池对象。

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

  • public static ExecutorService newFixedThreadPool(int nThreads):返回线程池对象。(创建的是有界线程池,也就是池中的线程个数可以指定最大数量)

获取到了一个线程池ExecutorService 对象,那么怎么使用呢,在这里定义了一个使用线程池对象的方法如下:

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

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

使用线程池中线程对象的步骤:

​ A:自定义一个类,作为任务类并实现Runnable接口;

​ B:实现Runnable接口中的run方法;

​ C:创建任务类的对象;

​ D:获取线程池对象;

​ E:直接执行任务;​

需求:使用线程池来完成卖票任务。

Runnable实现类代码:

//A:自定义一个类,作为任务类并实现Runnable接口;
class SellTicketTask implements Runnable
{
	//定义成员变量存储100张票
	private static int tickets=100;
	//创建锁对象
	private Lock l=new ReentrantLock();
	//B:实现Runnable接口中的run方法;
	public void run() {
		// 模拟卖票
		while(true)
		{
			//获取锁
			l.lock();
			if(tickets>0)
			{
				//休眠
				try {
					Thread.sleep(1);
				} catch (InterruptedException e) {
				}
				System.out.println(Thread.currentThread().getName()+"出票:"+tickets);
                  tickets--;
			}
			//释放锁
			l.unlock();
		}
	}
}

线程池测试类:

public class SellTicketDemo {
	public static void main(String[] args) {
		//C:创建任务类的对象;
		SellTicketTask stt = new SellTicketTask();
		//D:获取线程池对象; 获取2个线程
		ExecutorService es = Executors.newFixedThreadPool(2);
		//E:直接执行任务; 
      	  //自己创建线程对象的方式
        // Thread t = new Thread(stt);
        // t.start(); ---> 调用MyRunnable中的run()

        // 从线程池中获取线程对象,然后调用SellTicketTask中的run()
        es.submit(stt);
        // 再获取个线程对象,调用SellTicketTask中的run()
        es.submit(stt);
	}
}

4 Callable开启多线程

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

    问题1:Callable是什么?

/*
 * 演示:演示Callable
 * 我们忽略返回值,这个接口就与Runnable接口一样了
 */
class MyTask implements Callable<Object>{

	@Override
	public Object call() throws Exception {
		for( int i = 0; i < 10; i++){
			System.out.println(Thread.currentThread().getName() + " ... " + i);
		}
		return null;
	}
}
public class CallableDemo {
	public static void main(String[] args) {
		// 创建任务对象
		MyTask mt = new MyTask();
		// 获取线程池
		ExecutorService es = Executors.newFixedThreadPool(2);
		// 执行任务
		es.submit(mt);
		es.submit(mt);
	}
}

问题2:Future是什么?

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

A:我们自定义类,实现Callable接口

B:实现Call方法,Call方法有返回值

C:然后把任务类对象交给线程池执行

D:执行完成的结果保存Future中

E:最后我们调用Future的get方法拿到真正的结果。

/*
 * 演示:带返回值的线程任务
 * 需求:通过Callable计算从1到任意数字的和
 */
class SumTask implements Callable<Integer>{
	
	@Override
	public Integer call() throws Exception {
		int sum = 0;
		for(int i = 1; i <= 5; i++){
			sum += i;
		}
		return sum ;
	}
}
public class CallableDemo02 {
	public static void main(String[] args) throws InterruptedException, ExecutionException {
		// 创建任务对象
		SumTask st = new SumTask();
		// 获取线程池
		ExecutorService es = Executors.newFixedThreadPool(1);
		// 执行任务
		Future<Integer> future = es.submit(st);
		// 等待运算结束,获取结果
		Integer i = future.get();
		System.out.println(i);
	}
}
posted @ 2023-02-15 16:22  忘了鱼尾纱的猫  阅读(108)  评论(0)    收藏  举报