多线程笔记

本文详细介绍了Java中的多线程与并发编程,包括线程的并发与并行概念、线程的创建与状态、线程安全问题以及线程同步机制。讲解了Thread类和Runnable接口的使用,展示了死锁、等待与唤醒机制,并探讨了线程池的使用。线程安全问题如死锁、线程间通信、同步代码块和锁机制等是多线程编程中的关键点,而线程池可以有效地管理和复用线程,提高系统资源利用率。
摘要由CSDN通过智能技术生成

本人学习多线程时做的一些笔记,以备以后回顾复习。

第一章、多线程

1.1 并发与并行

  • 并发:指两个或多个事件在同一个时间段内发生。

此时有两个任务,一个任务1,一个任务2,我要在某个时间内让它完成两个任务,一个任务1,一个任务2,我们用并发的方式完成,我们执行任务需要cpu来执行它,比如此时我的电脑是单核心的,它会先执行任务1再执行任务2,然后执行任务1**(QQ,微信,浏览器,交替执行,但是交替速度很快)**.这个指的就是并发即一段时间内,cpu在多个任务之间交替执行

  • 并行:指两个或多个事件在同一时刻发生(同时发生)。

我们现在电脑的cpu都是多核心多线程的了对吧。这个cpu可以执行任务1,还能再来一个cpu让它执行任务2.所以这个并行它叫同时执行

在这里插入图片描述

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

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

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

1.2 线程与进程

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

  • 线程:线程是进程中的一个执行单元,负责当前进程中程序的执行,一个进程中至少有一个线程。一个进程中是可以有多个线程的,这个应用程序也可以称之为多线程程序。

    简而言之:一个程序运行后至少有一个进程,一个进程中可以包含多个线程

我们可以再电脑底部任务栏,右键----->打开任务管理器,可以查看当前任务的进程:

进程与线程

/**
 * 	程序:一堆命令的集合,进程静止的状态,包括了可执行文件、配置文件、
 * 		   数据文件等等软件文件集合,即计算机要执行的任务。
 * 
 * 
 * 	进程: 即具有一定功能的程序 关于某个数据集合上的一次运行活动,
 *       是cpu进行资源分配和调用的 一个独立单元。
 * 		 
 * 	线程: 有时候可以看成轻量级的进程,是程序执行流的最小单元,
 * 		  一个进程可以看做是有n个线程组成的
 * 
 * 		在单个程序运行的过程中,可以同时执行多个线程完成不同的工作,
 * 	    称之为多线程
 *	
 *	以腾讯管家为例,打开腾讯管家,就会进入到内存中,就是一个进程
 *	它里面有很多功能,例如病毒查杀,清理垃圾,电脑加速,他们都是一个线程
 *	此时点击病毒查杀,清理垃圾,电脑加速让它执行,它就会开启一条应用程序到cpu的执行路径
 *	cpu就可以通过路径就可以执行功能,这个路径有个名字叫线程 
 *	腾讯电脑管家实际上是个多线程的路径
 *	
 *	注意:线程是属于进程的,是进程中的一个执行单元,负责程序的执行
 * 
 *	并发:
 *		 官方解释 :并发,在操作系统中,是指一个时间段中有几个程序都处
 *				于已启动运行到运行完毕之间,且这几个程序都是在同一个处理机上运行,
 *				但任一个时刻点上只有一个程序在处理机上运行。
 *	理解: 
 * 	  线程总是并发运行的,操作系统会将时间分成若干个片段(时间片)
 * 	  尽可能的将这些时间片分配均匀的分配给每一个线程,
 *  	 当线程得到时间片以后,就可能被cpu所执行,
 *   	随着cpu高速的运行,从宏观的角度来看, 所有任务都在同时被执行,
 *   	但是从微观的角度看,每个任务都是走走停停的,此种现象被称为并发。
 *   
 *   
 *	并行: 多个任务是被多个cpu真的同时运行。		
 */

线程调度:

  • 分时调度

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

  • 抢占式调度

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

    • 设置线程的优先级

    • 抢占式调度详解

      大部分操作系统都支持多进程并发运行,现在的操作系统几乎都支持同时运行多个程序。比如:现在我们上课一边使用编辑器,一边使用录屏软件,同时还开着画图板,dos窗口等软件。此时,这些程序是在同时运行,”感觉这些软件好像在同一时刻运行着“。

    实际上,CPU(中央处理器)使用抢占式调度模式在多个线程间进行着高速的切换。对于CPU的一个核而言,某个时刻,只能执行一个线程,而 CPU的在多个线程间切换速度相对我们的感觉要快,看上去就是在同一时刻运行。
    其实,多线程程序并不能提高程序的运行速度,但能够提高程序运行效率,让CPU的使用率更高。

1.3 创建线程类

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

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

代码如下:

测试类:

/*
创建多线程程序的第一种方式:创建Thread类的子类
java.lang.Thread类:是描述线程的类,我们想要实现多线程程序,就必须继承Thread类

实现步骤:
    1.创建一个Thread类的子类
    2.在Thread类的子类中重写Thread类中的run方法,设置线程任务(开启线程要做什么?)
    3.创建Thread类的子类对象
    4.调用Thread类中的方法start方法,开启新的线程,执行run方法
         void start() 使该线程开始执行;Java 虚拟机调用该线程的 run 方法。
         结果是两个线程并发地运行;当前线程(main线程)和另一个线程(创建的新线程,执行其 run 方法)。
         多次启动一个线程是非法的。特别是当线程已经结束执行后,不能再重新启动。
java程序属于抢占式调度,那个线程的优先级高,那个线程优先执行;同一个优先级,随机选择一个执行
*/
public class Thread01 {
	public static void main(String[] args) {
		// 3.创建Thread类的子类对象
		MyThread mt = new MyThread();
		// 4.调用Thread类中的方法start方法,开启新的线程,执行run方法
        /*
		 * 注:  启动一个线程,是通过调用其start()来实现的。
		 */
		mt.start();

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

}

自定义线程类:

public class MyThread extends Thread {
	//定义指定线程名称的构造方法
	public MyThread(String name) {
		//调用父类的String参数的构造方法,指定线程的名称
		super(name);
	}
	/**
	 * 重写run方法,完成该线程执行的逻辑
	 */
	@Override
	public void run() {
		for (int i = 0; i < 10; i++) {
			System.out.println(getName()+":正在执行!"+i);
		}
	}
}

第二章、多线程执行原理

JVM执行main方法,找os开辟一条main方法通向CPU的路径,这个路径叫做main线程(主线程)

cpu通过这个线程,这个路径可以执行main方法

当我们在这里new MyThread(); 等于它重新开辟了一个指向CPU的路径

这条路径是执行run方法的

现在对于CPU而言,我是不是有两条执行路径了,cpu就有了选择的权限。

注意:我们控制不了CPU,CPU喜欢谁它就会执行哪条路径

所以就有了程序的随机打印结果

即两个线程,一个main线程,一个thread线程一起抢夺cpu的执行权(cpu的执行时间)

谁抢到了谁执行对应的代码。

2.1、多线程原理

有一个main方法,还有一个run方法。run方法里面有个for循环,

那我们来看看它的内存图解

首先它有一个栈内存

程序怎么执行的呢,首先执行程序的入口也就是main方法。它会压栈执行,从栈内存进来跑到栈内存最下面来

然后一行行执行代码,首先创建对象,对象创建在那里?是不是就是在堆内存中,这里new的对象是在堆内存中的, 它有个内存空间,这个对象它有自己的地址值,然后会把地址赋值给变量,

我们今天重点研究的不是堆内存而是栈内存,这里我要执行一行代码即(mt.run)它这里就是调用这个run方法

它也会压栈执行,但是这么执行,程序就是单线程的程序了。他会先执行完run方法,在执行主方法里的其他代码。

那什么是多线程程序呢

就是我们在这里调用了mt.start方法。调用mt.start方法,它就不一样了。它会开辟一个新的栈空间。它调用的是mt.start方法,执行run方法

此时对于cpu而言,我可以选择执行哪个栈空间里的代码,即cpu有了选择的权利,可以执行main方法也可以执行run方法。这个就是多线程的内存空间

2.2、多线程的好处

多个线程之间互不影响(在不同的栈空间中)

第三章、Thread类

3.1、thread类的常见方法

3.1.1、获取线程名称

/**
 * 线程常见的方法
 * 
 * 1、获取线程名称: 
 * 	1.1、使用Thread类中的方法getName() String getName() 返回该线程的名称
 * 
 * 	1.2、可以先获取当前正在执行的线程,使用线程中的方法getName()获取线程名
 * 	
 * 	static Thread currentThread()返回对当前正在执行的线程对象引用
 *	获取当前的Id       getId()
 * 	获取当前线程的优先级 getPriority()
 * 	获取当前线程的状态 getState()
 * 	判断当前线程是否还处于活动状态 isAlive()
 * 	判断线程是否为守护线程 isDaemon()
 */

//定义一个Thread的子类
class MyThreads extends Thread {
	// 重写Thread类中的run方法,设置线程任务
	@Override
	public void run() {
//		获取线程名
		String name2 = getName();
		System.out.println(name2);
		
		//获取当前执行线程
		Thread currentThread = Thread.currentThread();
		System.out.println(currentThread);
		
		//链式编程
		System.out.println(Thread.currentThread().getName());
	}
}

public class Thread02 {
	public static void main(String[] args) {
		MyThreads mt = new MyThreads();
		//调用start方法,开启新线程,执行run方法
		mt.run();
		new MyThreads().start();	
		//链式编程
		System.out.println(Thread.currentThread().getName());
	}
}

3.1.2、设置线程名称

/*
	设置线程名称:
		1、使用Thread类中方法setName(名字)
			void setName(String name)
		改变线程名称,使之与参数name相同
		
		2、创建带参数的构造方法,参数传递线程的名称
			调用父类的带参构造方法,把线程名称传递给父类
			让父类Thread()给子线程起一个名字
			Thread(String name) 分配新的Thread对象
*/
public class Thread03 {
	public static void main(String[] args) {
		// 开启多线程
		Mythread1 th1 = new Mythread1();
		th1.setName("线程1");
		th1.start();
		
		//开启多线程
		Mythread1 th2 = new Mythread1("线程2");
		th2.start();
	}
}

class Mythread1 extends Thread {
	public  Mythread1() {
		
	}
	
	public Mythread1(String name) {
		//把线程名给父类,让父类Thread给子线程起个名字
		super(name);
	}
	
	@Override
	public void run() {
		// 获取线程名
		System.out.println(Thread.currentThread().getName());

	}
}

3.1.3、设置守护线程

/**
 * 守护线程: 守护线程也叫做后台线程,正常创建出来的线程又叫做前台线程 或者 主线程
 * 
 * 主线程可以通过setDemeon()设置成后台线程(守护线程)
 * 
 * 当一个程序中,所有的主线程都结束后,此时如果程序中还有守护线程在执行任务 则不管守护线程是否执行完任务,整个程序结束。
 * 
 * 当一个程序中,所有的守护线程任务结束,当此时如果程序中还有主线程在执行任务 则整个程序继续执行
 * 
 */
public class Demo04 {
	public static void main(String[] args) {
		Thread th1 = new Thread(){
			public void run(){
				for (int i = 0; i < 1000; i++) {
					System.out.println("主线程在执行任务 " + i);
					try {
						//线程睡眠
						Thread.sleep(1000);
					} catch (InterruptedException e) {
						// TODO Auto-generated catch block
						e.printStackTrace();
					}
				}
			}
		};
		Thread th2 = new Thread(){
			public void run(){
				for (int i = 0; i < 1000; i++) {
					System.out.println("守护线程在执行任务 " + i);
					
					try {
						Thread.sleep(500);
					} catch (InterruptedException e) {
						e.printStackTrace();
					}
				}
			}
		};	
		//将线程2设置成守护线程
		th2.setDaemon(true);
		th1.start();
		th2.start();
	}
}

3.1.4、线程睡眠

/**
 * sleep(int value) 让当前线程陷入沉睡,沉睡时间为指定的时间,单位是毫秒
 * sleep为静态方法
 */
public class Demo05 {
	public static void main(String[] args) {
		Thread th = new Thread() {
			public void run() {
				for (int i = 5; i >= 0; i--) {
					System.out.println(i);
					try {
						Thread.sleep(1000);
					} catch (InterruptedException e) {
						e.printStackTrace();
					}
				}
			}
		};
		th.start();
	}
}

3.1.5、设置线程优先级

/**
 * 线程的优先级:
 * 
 * 我们不能强制干涉cpu将时间片分配给某个线程
 * 
 * 但是我们可以控制线程的优先级,从而提高该线程获取时间片的概率
 * 
 * 数据越大,优先级越高。
 * 
 * 优先级:1-10级,数字越大,优先级越高
 * 
 * 优先级默认等级为5
 */
public class Demo03 {
	public static void main(String[] args) {
		Thread th1 = new Thread("th1") {
			public void run() {
				for (int i = 0; i < 1000; i++) {
					System.out.println("th1正在执行");
				}
			}
		};
		Thread th2 = new Thread("th2") {
			public void run() {
				for (int i = 0; i < 1000; i++) {
					System.out.println("th2正在执行");
				}
			}
		};
		/**
		 * setPriority(int value)
		 * 
		 * 设置当前线程的优先级,需要在线程启动之前调用,之后调用就没有意义
		 */
		th1.setPriority(2);
		th2.setPriority(8);

		th1.start();
		th2.start();
	}
}

3.1.6、线程让步

/**
 * yield : 线程让步,当前线程让出自己的cpu资源让其他线程优先执行
 * 
 * 注:  1、让出的时间不确定; 
 *      2、在让出cpu资源时,会先检查是否有相同优先级且处于可运行状态的线程,如果有则让出,否则不让
 */
public class Demo07 {
	public static void main(String[] args) {
		Thread th1 = new Thread() {

			public void run() {
				for (int i = 0; i < 10; i++) {
					System.out.println("老太太排队买票 " + i);
					try {
					} catch (Exception e) {
						// TODO Auto-generated catch block
						e.printStackTrace();
					}
				}
			}
		};
		Thread th2 = new Thread() {

			public void run() {
				for (int i = 0; i < 1000; i++) {
					System.out.println("小朋友在排队买票 " + i);
					try {
						if (i == 4) {
							System.out.println("开始让出cpu");
							/**
							 * 让出cpu资源,让出的时间不确定,若要把cpu全让出,可用线程插队
							 */
							Thread.yield();

						}
					} catch (Exception e) {
						// TODO Auto-generated catch block
						e.printStackTrace();
					}
				}
			}
		};
		th1.start();
		th2.start();
	}
}

3.1.7、线程插队

/**
 * jion() 该方法表示线程插队,当当前线程插队时,其他线程陷入阻塞状态,直到当前线程插队将任务执行完毕
 *
 */
public class Demo06 {
	public static void main(String[] args) {
		Thread th1 = new Thread() {

			public void run() {
				for (int i = 0; i < 10; i++) {
					System.out.println("老太太排队买票 " + i);
					try {
						Thread.sleep(1000);
					} catch (InterruptedException e) {
						// TODO Auto-generated catch block
						e.printStackTrace();
					}
				}
			}
		};
		Thread th2 = new Thread() {

			public void run() {
				for (int i = 0; i < 1000; i++) {
					System.out.println("小朋友在排队买票 " + i);
					try {
						Thread.sleep(1000);

						if (i == 3) {
							System.out.println("老太太又事情,先插个队");
							/**
							 * 让th1线程插队
							 */
							th1.join();
						}
					} catch (InterruptedException e) {
						// TODO Auto-generated catch block
						e.printStackTrace();
					}
				}
			}
		};
		th1.start();
		th2.start();
	}
}

第四章、实现多线程的第二种方法

4.1、实现Runnable接口

/**
 * 	 在java中使用多线程方式2(推荐):
 * 		  通过实现Runnable接口, 重写run() ,在其中定义要执行的任务。
 * 		  然后将该任务子类对象添加到线程中执行。
 * 		   
 * 		  好处在于将线程 和 线程要执行的任务分开,避免的线程和任务的强耦合,
 * 		  当前这个线程在完成某个任务以后,可以再去执行别的任务。
 * 
 * 	构造方法:
 * 		Thread(Runnable target)分配新的Thread对象
 * 		Thread(Runnable target,String name)分配新的name对象
 * 
 * 	实现步骤:
 * 		1、创建一个Runnable接口的实现类
 * 		2、在实现类中重写Runnable接口中的run方法
 * 		3、创建一个Runnable接口实现类对象
 * 		4、创建Thread类对象,构造方法中传递Runnable接口的实现类对象
 * 		5、调用Thread类中的start方法开启新的线程,执行run方法
 */
public class Thread06 {
	public static void main(String[] args) {
		//Runnable 可以看作任务类接口
		Runnable1 run1 = new Runnable1();
		Runnable2 run2 = new Runnable2();
		
		// 将任务对象 添加线程中执行
		Thread th1 = new Thread(run1);
		Thread th2 = new Thread(run2);
		
		th1.start();
		th2.start();
	}
}

class Runnable1 implements Runnable{
	@Override
	public void run() {
		for (int i = 0; i < 1000; i++) {
			System.out.println("runnable1接口实现多线程");
		}
	}
}

class Runnable2 implements Runnable{
	@Override
	public void run() {
		for (int i = 0; i < 1000; i++) {
			System.out.println("runnable2接口实现多线程");
		}
	}
}

第五章、Thread和Runnable接口的区别

如果一个类继承Thread,则不适合资源共享,但是如果实现了Runnable接口的话,则很容易的实现资源共享。

总结:

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

1、适合多个相同的程序代码的线程去共享一个资源

2、可以避免Java中的单继承的局限性

3、增加程序的健壮性,实现解耦操作,代码可以被多个线程共享,代码和线程独立

4、线程池只能放入实现Runnable或者Callable类线程。不能直接放入继承Thread的类

扩充:在Java中,每次程序运行至少启动两个线程,一个是main线程,一个是垃圾收集线程,因为每当使用Java命令执行一个类的时候,实际上都会启动一个JVM,每个JVM其实在就是在操作系统中启动了一个进程

/**
	实现Runnable接口创建多线程程序的好处
		
	1、避免了单继承的局限性
		一个类只能继承一个类(一个人只能有一个亲爹)
		类继承了Thread类就不能继承其它类
		实现了Runnable接口,还可以继承其它类,实现其它接口
	2、增强程序的扩展性,降低了程序的耦合性(解耦)
		实现Runnable接口的方式,把设置线程任务和开启新线程进行分离(解耦)
		实现类中,重写了run方法,用来设置线程任务
		创建Thread类对象,调用start方法,用来开启新线程

*/
public class Thread08 {
	public static void main(String[] args) {
		Runnable3 run3 = new Runnable3();
		Runnable4 run4 = new Runnable4();
		
		Thread th1 = new Thread(run3);
		Thread th2 = new Thread(run4);
		
		th1.start();
		th2.start();
	}
}

class Runnable3 implements Runnable {
	@Override
	public void run() {
		for (int i = 0; i < 20; i++) {
			System.out.println("Hello,Runnable" + (i + 1));
		}
	}
}

class Runnable4 implements Runnable{
	@Override
	public void run() {
		for (int i = 0; i < 20; i++) {
			System.out.println("HelloWorld" + (i + 1));
		}
	}
}

第六章、匿名内部类的方式实现多线程

/**
 * 	匿名内部类方式
 * 
 * 	匿名:没有名字 内部类:写在其他类内部的类
 * 
 * 	匿名内部类的作用:简化代码 把子类继承父类,重写父类的方法创建子类对象可以一步完成 
 * 	把实现实现类接口,重写接口中的方法,创建实现类对象一步完成
 * 
 * 	匿名内部类的最终产物:子类/实现类对象,而这个类它没有名字
 * 
 * 	格式: 
 * 	new 父类/接口(){
 * 		 重写父类/接口中的方法
 * 	 };
 * 
 */
public class Thread09 {
	public static void main(String[] args) {
		// 线程的父类是Thread
		Thread thread = new Thread() {
			@Override
			public void run() {
				for (int i = 0; i < 20; i++) {
					System.out.println(Thread.currentThread().getName() + " " + (i + 1));
				}
			}
		};
//		thread.start();

		// 实现线程接口Runnable
		Runnable r = new Runnable() {

			@Override
			public void run() {
				for (int i = 0; i < 20; i++) {
					System.out.println(Thread.currentThread().getName() + "Runnable" + (i + 1));

				}
			}
		};
		Thread th2 = new Thread(r) {
		};
		th2.start();
	}
}

第七章、线程安全问题

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

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

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

我们来模拟电影院的售票窗口,实现多个窗口同时卖 “葫芦娃大战奥特曼”这场电影票(多个窗口一起卖这100张票) 需要窗口,采用线程对象来模拟;需要票,Runnable接口子类来模拟

/*
 	线程安全问题:
 		
 	案例:此时有个电影院开始售票,它一共贩卖100张
 	
 	现在只能让它现场买票,这个就相当于单线程程序(不会出现线程安全问题)
 	
 	那此时,一个窗口卖票速度太慢了,变成了三个窗口
 	
 	1号窗口贩卖(1-33) 2号窗口贩卖(34-67) 3号窗口贩卖(68-100)
 	
 	三个窗口一起卖票,但是卖的票不同,也不会出问题(多线程程序没有访问共享数据)
 	
 	现在假如有这么一个情景,还是开设了三个窗口,但是每个窗口都是从1号票开始卖
 	
 	这就会出现问题了,是不是就会出现混乱的情况了啊!是不是就会出现不存在的票啊!
 	
 	多线程访问了共享的数据会产生安全问题,这个问题是可以解决的
 */
/**
 * 	线程并发带来的安全问题 : 当多个线程操作同一个资源的时候就会引发安全问题
 *	 当多个线程访问同一份临界资源时,由于cpu切换线程的不确定性,会引发多个线程抢资源的现象,导致逻辑上的伦乱,引发数据安全问题。
 *
 * Java中一般引发安全问题的前提:  
 *		1、多个线程同时访问同一个对象	  
 *		2、操作同一个全局变量
 * 	解决办法 : 通过添加synchronized锁
 */
public class Demo01 {
	public static void main(String[] args) {
		//模拟卖票案例
		Runnable1 run1 = new Runnable1();
		
		Thread th1 = new Thread(run1);
		Thread th2 = new Thread(run1);
		Thread th3 = new Thread(run1);
	
		th1.start();
		th2.start();
		th3.start();
	}
}

class Runnable1 implements Runnable {
//	定义一个多线程共享的票源
	private int ticket = 100;

	// 设置线程任务,卖票
	@Override
	public void run() {
		// 使用死循环让卖票重复执行
		while (true) {
			// 先判断票是否存在
			if (ticket > 0) {
				System.out.println("还有余票" + Thread.currentThread().getName() + "正在售卖第" + ticket + "张票");
				ticket = ticket - 1;
			}
		}
	}
}

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

  1. 相同的票数,比如5这张票被卖了两回。

  2. 不存在的票,比如0票与-1票,是不存在的。

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

线程安全问题都是由全局变量及静态变量引起的。若每个线程中对全局变量、静态变量只有读操作,而无写

操作,一般来说,这个全局变量是线程安全的;若有多个线程同时执行写操作,一般都需要考虑线程同步,

否则的话就可能影响线程安全。

7.1 线程同步

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

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

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

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

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

  1. 同步代码块。

  2. 同步方法。

  3. 锁机制。

7.2、同步代码块

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

格式

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

同步锁:

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

  1. 锁对象 可以是任意类型。

  2. 多个线程对象 要使用同一把锁。

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

(BLOCKED)。

使用同步代码块解决代码

/**
 * 	注意:
 * 		线程安全问题不能产生,我们可以让一个线程在访问共享数据
 * 		的时候,无论是否丢失了cpu的执行权,让其他的线程只能等待
 * 		等待当前线程卖完票,其它线程才能进行卖票。
 * 	
 * 	保证:始终只有一个线程在卖票
 * 
 * 	同步代码块:
 * 	synchronized(同步锁){
 *   	可能会出现线程安全的代码
 *	}
 *	
 *	注意:
 *		1、同步代码块的锁对象,可以是任意对象
 *		2、但是必须保证多个线程使用的锁对象是同一个
 *		3、锁对象作用:
 *			把同步代码块锁住,只能一个线程在同步代码块中执行
 */
public class Demo02 {
	public static void main(String[] args) {
		Runnable2 run1 = new Runnable2();
		
		
		Thread th1 =new Thread(run1);
		Thread th2 =new Thread(run1);
		Thread th3 =new Thread(run1);
		
		th1.start();
		th2.start();
		th3.start();
	}
}

class Runnable2 implements Runnable {

	private int ticket = 100;

//	创建一个锁对象
	Object obj = new Object();

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

//			创建同步代码块
			synchronized (obj) {
				if (ticket > 0) {
					try {
						Thread.sleep(1000);
					} catch (InterruptedException e) {
						e.printStackTrace();
					}
					// 票存在,卖票ticket
					System.out.println(Thread.currentThread().getName() + "正在出售第 " + ticket + "张票");

					ticket--;
				}
			}
		}
	}
}

7.3、同步基数原理

/**
 * 	同步技术原理:
 * 		使用了一个锁对象,这个锁对象叫做同步锁,也叫对象锁
 * 		也叫对象监视器,
 * 	
 * 	流程:
 * 		两个线程一起抢夺cpu执行权,谁抢到了谁执行run方法进行卖票
 * 
 * 		t0抢到了cpu的资源执行run方法,遇到synchronized同步代码块
 * 		此时t0检查同步代码块是否有锁对象,发现有,就会获取到锁对象,进入到同步中执行	
 * 
 * 		t1抢到了cpu的执行权,执行run方法,遇到synchronized同步代码块
 *		此时t1检查同步代码块是否有锁对象,发现有,就会获取到锁对象,进入到同步中执行	
 *
 *		t1发现没有锁对象,就会进入到阻塞状态,会一直等待。t0线程归还锁对象
 *		一直到t0线程执行完同步中的代码,就会把锁对象归还给同步代码块
 *		t1才能获取到锁对象,进入到同步中执行
 *
 *	总结:
 *		同步中的线程,没有执行完,不会释放锁,同步外的线程没有锁进不去
 *
 *	注意:
 *		同步保证了只能有一个线程在同步中执行共享数据,保证了安全
 *		程序频繁的判断锁,获取锁,释放锁,程序的效率会变低
 */
public class Demo03 {
	public static void main(String[] args) {
		Runnable3 run3 = new Runnable3();
		
		Thread th1 = new Thread(run3) ;
		Thread th2 = new Thread(run3);
		th1.start();
		th2.start();
		
	}
}

class Account{
	private String name;
	private double money;
	
	public Account(String name, double money) {
		super();
		this.name = name;
		this.money = money;
	}
	
	public void takeMoney(double money) {
		try {
			Thread.sleep(1000);
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
		
		synchronized (this) {
			this.money -= money;
			System.out.println(Thread.currentThread().getName() 
					+ "取款后剩余" + this.money);
		}
	}
}
class Runnable3 implements Runnable{
	Account account = new Account("llw", 10000);
	@Override
	public void run() {
		account.takeMoney(1000);
	}
}

7.4、同步方法

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

格式:

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

同步锁是谁?

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

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

使用同步方法代码如下:

/*
 	在方法上加个synchronized关键字
 		
 	案例:
 		卖票案例出现线程安全问题
 		卖出了重复的票和不存在的票
 		
 		解决线程问题的方案:使用同步方法来解决
 		
 	使用步骤
 	1、把访问共享数据的代码抽取出来,方法方法中
 	2、在方法上添加修饰符synchronized
 	
 	格式:
 		定义方法的格式
 	
 	修饰符 synchronized 返回值类型 方法名(参数列表){
 		可能会出现线程安全问题的代码
 	}
 */
/**
 * synchronized锁  实际上锁的不是某段代码,某个方法,而是锁的是此方法,或者同步块所指定的对象
 *	 当某个线程去访问这些加了synchronized锁的代码时,对应的对象就被上锁。
 *
 * 	同步锁的互斥:当我们使用线程访问了某个对象中的带synchronized的方法(代码段)
 * 	那么该对象中的其他synchronized方法(代码段)
 * 	也都不可以被其他线程同时访问,但是非synchronized方法不受限制
 */
public class Demo04 {
	public static void main(String[] args) {
		Runnable4 run4 = new Runnable4();
		
		Thread th1 = new Thread(run4);
		Thread th2 = new Thread(run4);
		Thread th3 = new Thread(run4);
		
		th1.start();
		th2.start();
		th3.start();
		
	}
}

class Runnable4 implements Runnable{
	private int ticket = 100;
	
	//卖票
	@Override
	public void run() {
		while(true) {
			payTicket();
		}
	}
	/*
	 	定义一个同步方法
	 	同步方法也会把方法内部的代码锁住
	 	只让一个线程执行 
	 	同步方法锁的对象是谁?
	 	就是实现类对象,即new Runnable4() 也就是this
	 	即谁调用这个方法,对象就是谁
	 */
	public synchronized void payTicket() {
		if(ticket > 0) {
			try {
				Thread.sleep(1000);
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
			//票存在ticket--
			System.out.println(Thread.currentThread().getName()
					+"正在贩卖" + ticket +"张票");
			ticket--;
		}
	}
}

7.5、静态锁

/**
 *	 静态锁: 在静态方法上加锁, 此时锁的不是方法所在的对象(因为静态方法不是某个对象的,而是被该类的全体对象所共有的),
 *   此时实际上锁的是当前类对应的Class(存放类信息的类,jvm加载的每一个类,都有一个对应的Class实例)对象。
 *   所以此时,调用上锁的静态方法时,是否同步执行,和是哪个对象无关。
 *
 *	 注意:静态的同步方法,锁对象是谁? 不是this,this是创建对象之后产生的
 *		  静态方法优先于对象
 *		  静态方法的锁对象是本来的class属性 --> class文件对象(反射)
 */
public class Demo05 {
	public static void main(String[] args) {
		Runnalbe5 run5 = new Runnalbe5();
		
		Thread th1 = new Thread(run5);
		Thread th2 = new Thread(run5);
		Thread th3 = new Thread(run5);
		
		th1.start();
		th2.start();
		th3.start();
	}
}
class Runnalbe5 implements Runnable {
	private static int ticket = 100;
	
	@Override
	public void run() {
		while(true){
            payStaticTicket();
		}
	}
	
	public static synchronized void payStaticTicket() {
		
		
		/**
		 * 这样也可以保证锁对象是唯一的
		 */
		/*
		 * synchronized (Runnalbe5.class) {
		 * 
		 * }
		 */
		if(ticket > 0) {
			try {
				Thread.sleep(1000);
			} catch (InterruptedException e) {
				e.printStackTrace();
			}	
			//票存在ticket--
			System.out.println(Thread.currentThread().getName()
					+"正在贩卖" + ticket +"张票");
			ticket--;
		}
	}
}

7.6、线程脏读问题

/*
 * 	线程脏读问题:
 * 	此时可能就会出现脏读:即一个线程进行写数据操作的时候,执行到一半时间片耗尽,
 * 	此时有另一个线程去执行读数据的操作,但是读操作是没有加锁的可以被并发访问
 * 	此时可能读到的是另一个线程处理了一般的数据,此时可能读到的是另一个线程处理了一半的数据
 */
public class Demo06 {
	public static void main(String[] args) {
		Accounts accounts = new Accounts("llw",1000);
		
		Thread th1 = new Thread("th1") {
			@Override
			public void run() {
				accounts.setMoney(500);
				System.out.println("th1 :" + accounts.getMoney());
			}
		};
		
		Thread th2 = new Thread("th2") {
			@Override
			public void run() {
				System.out.println("th2 :" + accounts.getMoney());
			}
		};
		
		th1.start();
		th2.start();
	}
}
class Accounts{
	String userName;
	double money;
	
	public Accounts(String userName,double money) {
		super();
		this.userName = userName;
		this.money = money;
	}
	
	public /*synchronized*/void setMoney(double money) {
		try {
			Thread.sleep(500);
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
		this.money -= money;
	}
	
	public/*synchronized*/ double getMoney() {
		return money;
	}
}

7.7、内存的可见性问题 和 volatile关键字

/**
  内存的可见性问题 和 volatile关键字
  
  在下述案例中我们期望通过线程2修改status变量的值,去停止线程1 但是实际上并没有成功
 
  那么线程1在运行的时候,会将stop变量的值拷贝一份放在自己的工作内存当中。

  那么当线程2更改了stop变量的值之后,但是还没来得及写入主存当中,线程2转去做其他事情了,那么线程1由于不知道线程2对stop变量的更改,因此还会一直循环下去。
  
  	为什么发生这样的结果:和jvm的内存模型相关,在jvm中全局共享变量存放在主存中(堆中) 而每个线程都有属于自己的私有的本地内存,当程序运行起来以后,
  	CPU为了避免频繁的去主存中读取数据,而导致性能降低,所以会先将主存中的数据复制一份到线程的本地内存中,
  	然后所有对数据的操作都是对本地内存中的副本进行操作,操作完毕后,会在合适的时机将数据刷新到主存中去 此时如果多个线程去访问同一份数据都是先拷贝一份到本地中
 	而某个线程刷新了主存中的数据以后,其他的线程并不一定能感知到主存中的数据已经发生了改变 此时就导致了线程之间的可见性问题。
  
  	解决的办法:
  		1、对数据操作时使用线程锁 synchronized 或者 Lock 
  		2、在对操作的变量前加volatile关键字
 
  volatile关键字: 是Java提供的一种轻量级的同步机制(并不能解决线程安全问题!!!!)
 
  	当一个变量被volatile关键字修饰以后,某个线程对此数据修改,刷新到主存中后,jvm会通知其他线程次数据已经发生改变

 	此时数据已经发生了变化,其他线程就会重新载入这个数据
 
  线程的可见性:指的是某条线程修改了多个线程的共享的全局变量数据,新的值对其他线程来说也是可以立刻得知的

 */
public class Demo07 {
//	boolean status = true;
	volatile boolean status = true;

	public static void main(String[] args) {
		Demo07 d7 = new Demo07();

		Thread th1 = new Thread("th1") {
			@Override
			public void run() {
				// TODO Auto-generated method stub
				String thName = Thread.currentThread().getName();
				System.out.println(thName + "开始执行..");

				while (d7.status) {

				}
				System.out.println(thName + "执行完毕");
			}
		};

		Thread th2 = new Thread("th2") {
			@Override
			public void run() {
				String thName = Thread.currentThread().getName();
				System.out.println(thName + "开始执行");
				d7.status = false;
			}
		};
		
		th1.start();
		try {
			Thread.sleep(2000);
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
		th2.start();
		System.out.println(d7.status);
	}
}
/**
 * volatile 不能替代synchronized锁机制,它还是会引发线程安全问题
 *
 */	
public class Demo08 {
	public static volatile int num = 0;
	
	public static void main(String[] args) {
		// 1、测试volatile会引发安全问题
//		Thread th1 = new Thread() {
//			@Override
//			public void run() {
//				for (int i = 0; i < 10000; i++) {
//					numadd();
//				}
//			}
//		};
//		Thread th2 = new Thread() {
//			@Override
//			public void run() {
//				for (int i = 0; i < 10000; i++) {
//					numadd();
//				}
//			}
//		};
//		th1.start();
//		th2.start();
//		
//		try {
//			Thread.sleep(1000);
//		} catch (InterruptedException e) {
//			e.printStackTrace();
//		}
//		System.out.println(num);
		
		
		//2、测试 synchronized不会引发安全问题
		Thread th1 = new Thread("th1") {
			@Override
			public void run() {
				for (int i = 0; i < 10000; i++) {
					numadd();
				}
			}
		};
		Thread th2 = new Thread("th2") {
			@Override
			public void run() {
				for (int i = 0; i < 10000; i++) {
					numadd();
				}
			}
		};
		th1.start();
		th2.start();
		try {
			Thread.sleep(5000);
		} catch (InterruptedException e) {
			e.printStackTrace();
		}

		System.out.println(num);
	}

	
//	  public static void numadd() { num++; }
	 
	public static synchronized void numadd() {
		num++;
	} 
}

7.8、synchronized锁的重入机制

/**
 * synchronized锁的重入(重新进入)机制:
 * 当一个线程获得一个对象的锁以后,在该锁中执行代码的时候,可以再次获得该对象的锁。
 * 例子:比如下面的method1()方法被线程访问了以后,对应的对象被锁定了,其他的带synchronized的方法也不可以被访问,
 * 但是在method1()里面又调用了method2()方法。按理说此时程序是不可以继续向下执行的,变成死锁
 * 但是实际上它是可以继续执行的,因为锁的重入机制,虽然调用method1()的时候,获取了对应的对象的锁,此时在这个锁的内部执行代码
 * 可以再次获取同一个对象的锁,即继续执行method2()方法
 */
public class Demo01 {
	public synchronized void method1() {
		method2();
		System.out.println("method1()....");
	}

	public synchronized void method2() {
		System.out.println("method2()...");
	}

	public static void main(String[] args) {
		Demo01 demo01 = new Demo01();
		Thread th = new Thread("th1") {
			@Override
			public void run() {
				demo01.method1();
			}
		};
		th.start();
	}
}

7.9、死锁

/**
 * 死锁: 多个线程在竞争资源的时候,过多的同步方法导致线程推进不当, 两个或者以上线程持有对方所需要的锁,但是又不可以及时释放,
 * 导致线程之间都在等待对方释放锁,最终引发死锁问题。
 * 
 * 例子: 有线程th1和th2,以及有两个被锁对象obj1和obj2 线程1执行的时候,需要先锁obj1在去锁obj2
 * 与此同时,线程th2也在并发执行,需要先锁obj2,在去锁obj1
 * 
 * 此时,可能就会出现线程th1拿到obj1锁的时候,发现obj2锁已经被th2拿了
 * 那么此时th1就只能等待th2释放锁,但是th2释放obj2的锁,需要先拿到obj1
 */
public class Demo02 {
	Object obj1 = new Object();
	Object obj2 = new Object();

	public void method1() {
		synchronized (obj1) {
			System.out.println("锁完boj1以后,去锁obj2");
			try {
				Thread.sleep(500);
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
            // 想要2
			synchronized (obj2) {
				System.out.println("锁定obj2");
			}
		}
	}
    
	public void method2() {
		synchronized (obj2) {
			System.out.println("锁完boj2以后,去锁obj1");

			try {
				Thread.sleep(500);
			} catch (InterruptedException e) {
				// TODO Auto-generated catch block
				e.printStackTrace();
			}
            
            // 想要1
			synchronized (obj1) {
				System.out.println("锁定obj1");
			}
		}
	}
	public static void main(String[] args) {
		Demo02 demo02 = new Demo02();
		Thread th1 = new Thread("th1") {
			@Override
			public void run() {
				demo02.method2();
			}
		};
		Thread th2 = new Thread("th2") {
			@Override
			public void run() {
				demo02.method1();
			}
		};
		th1.start();
		th2.start();
	}
}

7.10、Lock锁

/**
 * Lock锁: Lock实现了比synchronized锁更广泛的锁操作
 * 接口中提供了两个方法
 * 1、Lock:获取锁
 * 2、unlock:释放锁
 * 他能够知道什么时候获取锁,什么时候释放锁
 * 
 * java.util.conurrent.locks.ReentrantLock implement Lock
 * 使用步骤(既然是接口,就要实现它的实现类)
 * 
 * 1、在成员位置上创建一个ReentrantLock对象
 * 2、可能会出现安全问题的代码前,调用Lock方法获取锁
 * 3、可能会出现安全问题的代码后,调用unLock释放锁
 */
public class Demo10 {
	public static void main(String[] args) {
		
		Rannable10 run10 = new Rannable10();
		
		Thread th1 = new Thread(run10);
		Thread th2 = new Thread(run10);
		Thread th3 = new Thread(run10);
		
		th1.start();
		th2.start();
		th3.start();
	}
}

class Rannable10 implements Runnable{
	private static int ticket = 100;
//	1、在成员变量前创建Lock锁
	Lock lock = new ReentrantLock();
	@Override
	public void run() {
		while(true) {
//		2、可能会出现安全问题的代码前,调用Lock方法获取锁
			lock.lock();
			payticket();
		}
	}
	public void payticket() {
		if(ticket > 0) {
			try {
				Thread.sleep(1000);
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
			System.out.println(Thread.currentThread().getName() + "正在卖第" + ticket +"张票");
			ticket--;
		}
//		3、可能会出现安全问题的代码后,调用unLock释放锁
		lock.unlock();
	}
}

其实它还有一种更好的写法,那我们来一起看一下怎个要怎么写吧

/**
 * Lock锁: Lock实现了比synchronized锁更广泛的锁操作 
 * 接口中提供了两个方法 
 * 		1、Lock:获取锁 
 *		2、unlock:释放锁
 * 他能够知道什么时候获取锁,什么时候释放锁
 * 
 * java.util.conurrent.locks.ReentrantLock implement Lock 使用步骤(既然是接口,就要实现它的实现类)
 * 
 * 1、在成员位置上创建一个ReentrantLock对象 
 * 2、可能会出现安全问题的代码前,调用Lock方法获取锁
 * 3、可能会出现安全问题的代码后,调用unLock释放锁
 */
/**
 * Lock(java提供的lock对象来实现synchronized相同的效果);
 * Lock是一个接口类型,常见的子实现类为Reentrantlock
 * 使用的时候先创建lock对象,通过lock方法加锁,unlock()方法手动释放锁
 * 在他们之间的代码就都被上锁了,变成同步执行的代码
 */
public class Demo10 {
	public static void main(String[] args) {

		Rannable10 run10 = new Rannable10();

		Thread th1 = new Thread(run10);
		Thread th2 = new Thread(run10);
		Thread th3 = new Thread(run10);

		th1.start();
		th2.start();
		th3.start();
	}
}

class Rannable10 implements Runnable {
	private static int ticket = 100;
//	1、在成员变量前创建Lock锁
	Lock lock = new ReentrantLock();

	@Override
	public void run() {
		while (true) {
//		2、可能会出现安全问题的代码前,调用Lock方法获取锁
			lock.lock();
			payticket();
		}
	}

	public void payticket() {
		if (ticket > 0) {
			try {
				Thread.sleep(1000);
				System.out.println(Thread.currentThread().getName() + "正在卖第" + ticket + "张票");
				ticket--;
			} catch (InterruptedException e) {
				e.printStackTrace();
                // 加了finally
			} finally {
//				3、可能会出现安全问题的代码后,调用unLock释放锁
				lock.unlock();
			}
		}
	}
}

第八章、线程状态

8.1、线程状态概述

当线程被创建并启动以后,它既不是已启动就进入了执行状态,也不是一直处于执行状态,在线程的生命周期中,有几种状态呢?

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

注意:他们各个状态之间可以相互转换

在这里插入图片描述

8.2 Timed Waiting(计时等待)

Timed Waiting在API中的描述为:一个正在限时等待另一个线程执行一个(唤醒)动作的线程处于这一状态。单独的去理解这句话,真是玄之又玄,其实我们在之前的操作中已经接触过这个状态了,在哪里呢?

在我们写卖票的案例中,为了减少线程执行太快,现象不明显等问题,我们在run方法中添加了sleep语句,这样就强制当前正在执行的线程休眠(暂停执行),以“减慢线程”。

其实当我们调用了sleep方法之后,当前执行的线程就进入到“休眠状态”,其实就是所谓的Timed Waiting(计时等

待),那么我们通过一个案例加深对该状态的一个理解。

实现一个计数器,计数到100,在每个数字之间暂停1秒,每隔10个数字输出一个字符串

/**
 *	实现一个计数器,计数到100,在每个数字之间暂停1秒,
 *	每隔10个数字输出一个字符串	
 */
/*
 * 	在这我们新建一个线程,它是处于可以运行状态
 * 	如果我们调用了sleep方法,它就会进入到计时等待状态
 * 	等待结束以后,它又会回到可运行的状态
 * 	这个就被叫做计时等待
 */
public class Demo01 {
	public static void main(String[] args) {
		Thread th1 = new Thread() {
			@Override
			public void run() {
				for (int i = 0; i < 100; i++) {
					if (i % 10 == 0) {
						System.out.println("每间隔10个数字输出一个字符串" + i);
						System.out.println("------------------------");
					}
					System.out.println("i = " + i);
				}
				try {
					Thread.sleep(1000);
				} catch (InterruptedException e) {
					e.printStackTrace();
				}
			}
		};
		th1.start();
	}
}

通过案例可以发现,sleep方法的使用还是很简单的。我们需要记住下面几点:

  1. 进入 TIMED_WAITING 状态的一种常见情形是调用的 sleep 方法,单独的线程也可以调用,不一定非要有协作关系。

  2. 为了让其他线程有机会执行,可以将Thread.sleep()的调用放线程run()之内。这样才能保证该线程执行过程中会睡眠

  3. sleep与锁无关,线程睡眠到期自动苏醒,并返回到Runnable(可运行)状态。

小提示:sleep()中指定的时间是线程不会运行的最短时间。因此,sleep()方法不能保证该线程睡眠到期后就

开始立刻执行。

8.3 BLOCKED(锁阻塞)

Blocked状态在API中的介绍为:一个正在阻塞等待一个监视器锁(锁对象)的线程处于这一状态。

我们已经学完同步机制,那么这个状态是非常好理解的了。比如,线程A与线程B代码中使用同一锁,如果线程A获取到锁,线程A进入到Runnable状态,那么线程B就进入到Blocked锁阻塞状态。

这是由Runnable状态进入Blocked状态。除此Waiting以及Time Waiting状态也会在某种情况下进入阻塞状态,而这部分内容作为扩充知识点带领大家了解一下。

 /**
 * 锁阻塞:
 * 	首先它也是先创建一个线程,它处于可运行的状态
 * 	什么被称之为可运行的状态呢,就是它可以运行,但是它需要和别的线程去争抢cpu执行权
 * 	没有争取到锁的对象进入到锁阻塞的状态
 * 	如果锁阻塞状态的线程,获取到锁对象,它就会进入到可运行的状态
 */
public class Demo02 {
	public static void main(String[] args) {

	}
}

8.4 Waiting(无限等待)

Wating状态在API中介绍为:一个正在无限期等待另一个线程执行一个特别的(唤醒)动作的线程处于这一状态。

那么我们之前遇到过这种状态吗?答案是并没有,但并不妨碍我们进行一个简单深入的了解。我们通过一段代码来

学习一下:

/**
 * 	无限等待:
 *	等待唤醒案例(线程之间的通信)
 *	有两个商铺,一个卖包子的商铺,一个买包子的商铺
 *	顾客要买包子,和老板说明买包子的数量和种类,
 *	顾客就等着老板做包子(调用wait方法)
 *	waiting状态,无限等待
 *
 *
 *	老板开始做包子,根据你的需要来做包子,做好包子告诉顾客(调用notify)
 *	告诉顾客包子做好了,就可以开始吃了
 *	所以顾客和老板之间就形成了一个通信
 *
 *	顾客来买包子,要等待老板做包子,调用wait方法
 *	老板做好包子,告诉顾客包子做好了,调用notify方法
 *	这个就形成线程之间的通信
 *	
 */	
/**
 * 	等待唤醒案例:线程之间的通信
 * 		创建一个顾客线程(消费者)告知老板要的包子的种类和数量
 * 		调用wait方法,放弃cpu的执行,进入到WAITING无限等待状态
 * 		
 *		创建一个老板线程(生产者)花了5秒做包子,做好包子之后,调用
 *		notify方法唤醒顾客,吃包子
 *	
 *	注意:顾客和老板线程必须使用同步代码块包裹起来,包子等待和唤醒只能有
 *		一个在执行
 *		同步使用的锁对象必须保证唯一
 *
 *		只有锁对象才能调用wait和notify方法,非锁对象是不能调用的
 *
 *	object类中的方法
 *		void wait()
 *			在其他线程调用此对象的notify()方法或者notifyAll()方法前,导致当前线程等待
 *		
 *		void notify()
 *			唤醒在此对象监视器上等待的单个线程
 *			会继续执行wait方法之后的代码
 */

public class Demo05 {
	public static void main(String[] args) {
		// 创建锁对象,保证唯一
		Object obj = new Object();

		// 创建一个顾客线程(消费者)
		Thread th1 = new Thread() {
			@Override
			public void run() {
				while (true) {
					// 保证等待和唤醒的线程只能有一个执行,需要使用同步技术
					synchronized (obj) {
						System.out.println("顾客线程(消费者)告知老板要的包子的种类和数量");
						// 调用wait方法,让它进入到无限等待状态
						try {
							obj.wait();
						} catch (InterruptedException e) {
							e.printStackTrace();
						}
						// 唤醒之后执行的代码
						System.out.println("包子已经做好了,可以开始吃了");
						System.out.println("------------------------");
					}
				}
			}
		};
		th1.start();

		// 创建一个老板线程(生产者)
		Thread th2 = new Thread() {
			@Override
			public void run() {
				while (true) {
//						老板线程(生产者)花了5秒做包子,做好包子之后,
					try {
						Thread.sleep(5000);// 花5秒做包子
					} catch (InterruptedException e) {
						e.printStackTrace();
					}
					// 保证等待和唤醒的线程只能有一个执行,需要使用同步技术
					synchronized (obj) {
						System.out.println("做好包子,告知顾客,可以吃包子了");
//						调用notify方法唤醒顾客,吃包子
						obj.notify();
					}
				}
			}
		};
		th2.start();
	}
}

通过上述案例我们会发现,一个调用了某个对象的 Object.wait 方法的线程会等待另一个线程调用此对象的

Object.notify()方法 或 Object.notifyAll()方法。

其实waiting状态并不是一个线程的操作,它体现的是多个线程间的通信,可以理解为多个线程之间的协作关系,

多个线程会争取锁,同时相互之间又存在协作关系。就好比在公司里你和你的同事们,你们可能存在晋升时的竞争,但更多时候你们更多是一起合作以完成某些任务。

当多个线程协作时,比如A,B线程,如果A线程在Runnable(可运行)状态中调用了wait()方法,那么A线程就进入了Waiting(无限等待)状态,同时失去了同步锁。假如这个时候B线程获取到了同步锁,在运行状态中调用了

notify()方法,那么就会将无限等待的A线程唤醒。注意是唤醒,如果获取到锁对象,那么A线程唤醒后就进入

Runnable(可运行)状态;如果没有获取锁对象,那么就进入到Blocked(锁阻塞状态)。

8.5、wait和notify

/**
 * 	Object类中wait带参方法和notifyAll()方法
 *	 
 *	进入到TimeWaiting(即使等待)有两种方式
 *	1、使用sleep(long m) 方法,在毫秒结束以后,线程睡醒
 *	进入到Runnable/Blocked状态
 *	
 *	2、使用wait(long m)方法,wait方法如果在毫秒结束之后
 *	还没被notify唤醒,就会自动醒来
 *
 *	唤醒的方法:
 *		void notify()
 *			唤醒在此对象监视器上等待的单个线程,
 *		void notifyAll()
 *			唤醒在此对象监视器上等待的所有线程,
 *	
 */
public class Demo06 {
	public static void main(String[] args) {
		// 创建锁对象,保证唯一
		Object obj = new Object();
		Thread th1 = new Thread("th1") {
			@Override
			public void run() {
				while (true) {
					synchronized (obj) {
						System.out.println("顾客1告诉老板要的包子种类和数");
						try {
							obj.wait(5000);
						} catch (InterruptedException e) {
							e.printStackTrace();
						}
						// 唤醒之后执行的代码
						System.out.println("顾客1包子已经做好了,开吃");
						System.out.println("-----------------");
					}
				}
			}
		};
		th1.start();

		Thread th2 = new Thread() {
			@Override
			public void run() {
				while (true) {
					synchronized (obj) {
						System.out.println("顾客2告诉老板要的包子种类和数");
						try {
							obj.wait(5000);
						} catch (InterruptedException e) {
							e.printStackTrace();
						}
						// 唤醒之后执行的代码
						System.out.println("顾客2包子已经做好了,开吃");
						System.out.println("-----------------");
					}
				}
			}
		};
		th2.start();

		// 老板线程
		Thread th3 = new Thread() {
			@Override
			public void run() {
				while (true) {
					try {
						Thread.sleep(5000);
					} catch (InterruptedException e) {
						e.printStackTrace();
					}
					synchronized (obj) {
						System.out.println("老板5秒钟后做好包子"
								+"告知顾客,包子可以吃了");
						obj.notify();
					}
				}
			}
		};
		th3.start();
	}
}

第九章、等待与唤醒机制

9.1、线程之间的通信

**概念:**多线程在处理同一个资源,但是处理的动作(线程任务)却不同。

比如:A线程是用来生产包子的,B线程是用来吃包子的。包子可以理解为同一资源,线程A和线程B处理的动作一个是生产,一个是消费。那么线程A和线程B之间就存在线程通信问题,大家也可以把他们理解成以后工作以后的合作,一个人做登陆,一个人做注册,一个人做购买,等等。所以线程之间也可以存在一个通信,称为合作。

9.2、为什么要处理线程通信

多线程并发执行时,在默认情况下cpu是随机切换线程的,当我们需要多个线程共同完成一个任务,并且我们希望他们有规律的去执行,那么多线程之间需要一些协调通信,以此来帮我们达到多线程共同操作一份数据。

9.3、如何保证线程间通信有效利用资源

多个线程在处理同一个资源,并且任务不同时,需要线程通信来帮助解决线程之间对同一个变量的使用或操作,就说多个线程在操作同一个数据时,避免对同一变量的争夺,也就是我们需要通过一定的手段,使各个线程能有效的利用资源,而这种手段被称之为 – 等待唤醒机制

9.4、等待唤醒机制

什么是等待唤醒机制

这个是多个线程之间的一种协作机制,讲到线程,我们经常讲到的是线程之间的竞争(race),比如去争夺锁,但是这并不是故事的全部,线程之间也会有协作机制,就好比在公司里面,你和你的同事,你们之间就存在着晋升时的竞争,但是更多的时候,你们可能存在的时候,你们需要一起合作完成某些事情的一个合作关系。

就是在线程进行了规定的操作以后,就进入到一个等待状态(wait() ) ,等待其他线程完成执行完他们指定的代码过后,在将其唤醒(notify() );在有多个线程进入等待以后,如果需要,可以使用 notifyAll() 来唤醒所有线程,

wait/notify就是一种线程协作机制

例子:

我们用包子举例,等待与唤醒机制也被称作线程之间的通信重点:有效的利用资源(生产包子,吃一个包子,在生产包子,吃一个包子。。。)这里的资源呢,指的就是包子(生产一个吃一个),

重点解释:

通信:对包子的状态进行判断(判断是否有包子,如果没有,吃包子线程则唤醒生产包子线程,吃包子线程等待,生产包子线程制作包子,做好包子之后修改包子状态,改为有包子;

有存在包子,生产包子线程则唤醒吃包子线程,生产包子线程等待,吃包子线程吃包子,吃完包子修改包子状态为没有)

他们可以反复执行,这也就叫做线程之间的通信,也被称之为等待和唤醒。

重点的是我们合理运用这个包子来判断到底是哪条线程在执行。

那等待与唤醒就要用到我们着几个方法

等待唤醒中的方法

等待唤醒机制就是用来解决线程之间的通信问题,使用到的3个方法的含义如下:

1、wait:线程不在活动,不在参与调度,进入wait set中,因此不会浪费CPU资源,也不会去竞争锁了,这个时候的线程即是WAITING状态,它还要等待别的线程执行一个特别的动作,也即是**”通知“( notify() )**在这个对象上等待的线程从wait set中释放出来,重新进入到调度队列中(ready quene)中。

2、notify:则选取所通知的对象的wait set中的一个线程释放,例如:餐厅有位置以后,等待最久的顾客优先入座。

3、notifyAll:则释放所通知对象的wait set上的全部线程

注意:哪怕只通知了一个等待的线程,被通知的线程也不能立即恢复执行,因为它当初中断的地方是在同步块中,而此刻它已经不在持有锁,所以这个时候它需要再次尝试去获取锁,(很可能面临其他线程的竞争)成功以后才能调用wait方法之后的地方恢复执行。

调用wait和notify所需要注意的细节

1、wait和notify方法必须要由同一个锁对象调用,因为对应的锁对象可以通过notify唤醒使用同一个锁对象调用的wait方法后的线程

2、wait方法和notify方法都是属于Object类的方法,因为锁对象可以是任意对象,而任意对象的所属类都是继承了Object类

3、wait方法和notify方法必须使用在同步代码块或者是同步函数中使用,因为必须要通过锁对象才能调用者两个方法

/**
 * 分析:需要哪些类
 * 		1、资源类:包子
 * 			设置包子的属性 :皮   馅料  状态:1、有包子true 2、没包子false
 * 		2、生产者(包子铺): 线程类可以继承Thread 
 * 			设置线程任务(run),重写run方法 -- 生产包子(对包子状态进行判断true或者false)
 * 			true:有包子,包子铺调用wait方法进入等待状态
 * 			false:没有包子,包子铺生产包子(可以增加趣味性,生产两种包子)
 * 			即有两种状态,(i%2==0)这样是不是就有两个状态了
 * 			包子铺生产好了包子修改状态为true
 * 			唤醒吃货线程,让吃货线程吃包子
 * 		   消费者(吃货)类:是一个线程类,可以继承Thread
 * 			设置线程任务(run),重写run方法 -- 吃包子(对包子的状态进行判断true或者false)
 *			false:没有包子,吃货线程调用wait方法,进入等待状态
 *			true:有包子,吃货吃包子,吃货吃完包子,修改包子的状态为false
 *			吃货唤醒包子线程,生产包子
 *		3、测试类:
 *			包含main方法的类,是程序执行的入口,启动程序
 *			创建包子对象,创建包子铺线程,开启
 *			创建吃货线程,开启
 */	
public class Demo01 {
	public static void main(String[] args) {
		
	}
}

9.5、生产者与消费者

等待与唤醒机制,其实就是经典的生产者与消费者的问题

就拿生产包子和消费包子来说等待唤醒机制如何有效的利用资源

包子铺生产包子,吃货线程消费包子当。
包子没有时(包子状态为false),吃货线程等待,包子铺线程生产包子(即包子状态为true),并通知吃货线程(解除吃货线程的等待状态),因为此时已经有包子了,那么包子铺线程进入等待状态。
接下来,吃货线程能否进一步执行,取决于锁的获取情况,如果吃货获取到锁,那么就执行吃包子动作,包子吃完(包子状态为false),并通知包子铺线程(解除包子铺的等待状态),吃货线程进入等待,包子铺线程是否能够进一步执行,则取决于锁的获取情况

代码演示:

//包子类
public class Demo01 {
	//皮
	String pi;(wrapper)
	//馅
	String xian;(stuffing)
	//包子的状态为true或者false。设置初始值为false
	boolean flag = false;
}
/*
	生产者(包子铺): 线程类可以继承Thread 
 		设置线程任务(run),重写run方法 -- 生产包子(对包子状态进行判断true或者false)
 		true:有包子,包子铺调用wait方法进入等待状态
 		false:没有包子,包子铺生产包子(可以增加趣味性,生产两种包子)
 		即有两种状态,(i%2==0)这样是不是就有两个状态了
 		包子铺生产好了包子修改状态为true
 		唤醒吃货线程,让吃货线程吃包子
 	注意:
 		包子铺线程和包子线程关系 - 通信关系(互斥关系)
 		必须同时同步技术保证两个线程只能有一个在执行
 	锁对象必须保证唯一,可以使用包子对象作为锁对象。
 	包子铺类和吃货类需要吧包子对象作为参数传递进来
 		1、需要在成员位子创建一个包子变量
 		2、使用带参数构造方法,为这个包子变量赋值
 		
 */
//包子铺类
public class Demo02 extends Thread {
	// 1、需要定义在成员位置创建一个变量
	private Demo01 bz;

	// 2、使用带参数构造方法,为这个包子变量赋值
	public Demo02(Demo01 bz) {
		this.bz = bz;
	}

	// 设置线程任务(run) 生产包子
	@Override
	public void run() {
		// 定义一个变量
		int count = 0;
		while (true) {

			synchronized (bz) {
				// 对包子状态进行判断
				if (bz.flag == true) {
					// 包子铺调用wait方法进入等待状态
					try {
						bz.wait();
					} catch (InterruptedException e) {
						e.printStackTrace();
					}
				}
				// 被唤醒之后执行,包子铺生产包子
				// 增加一些趣味性,交替产生两种包子
				if (count % 2 == 0) {
					// 生产薄片包子
					bz.pi = "薄片";
					bz.xian = "三鲜";
				} else {
					// 生产厚皮,牛肉馅
					bz.pi = "厚皮";
					bz.xian = "牛肉";
				}
				count++;
				System.out.println("包子铺正在生产" + bz.pi + bz.xian + "包子");
				// 生产包子需要3秒
				try {
					Thread.sleep(3000);
				} catch (InterruptedException e) {
					e.printStackTrace();
				}
				// 此时包子铺生产好了包子
				// 修改包子的状态为true
				bz.flag = true;
				// 唤醒吃货线程,让吃货线程吃包子
				bz.notify();
				System.out.println("包子铺已经生产好了:" + bz.pi + bz.xian + "包子,吃货们可以开始吃了");
			}
		}
	}
}
/**
  消费者(吃货)类:是一个线程类,可以继承Thread
  		设置线程任务(run),重写run方法 -- 吃包子(对包子的状态进行判断true或者false)
 		false:没有包子,吃货线程调用wait方法,进入等待状态
 		true:有包子,吃货吃包子,吃货吃完包子,修改包子的状态为false
 		吃货唤醒包子线程,生产包子
 */
public class Demo03 extends Thread{
	//1、需要在成员位置创建一个包子变量
	private Demo01 bz;
	//2、使用带参数的构造方法,为这个包子变量赋值
	public Demo03(Demo01 bz) {
		this.bz = bz;
	}
	
	//设置线程任务(run)  吃包子
	@Override
	public void run() {
		//使用死循环,让吃货一直吃包子
		while(true) {
			//必须同时同步技术保证两个线程只能有一个在执行
			synchronized (bz) {
				//对包子状态进行判断
				if(bz.flag == false){
					//调用吃货的wait方法进入等待状态
					try {
						bz.wait();
					} catch (InterruptedException e) {
						e.printStackTrace();
					}
				}
				//被唤醒之后执行的代码,吃包子
				System.out.println("吃货正在吃:" + bz.pi
						+bz.xian +"的包子");
				//修改包子的状态为false 
				bz.flag = false;
				//唤醒包子铺线程
				bz.notify();
				System.out.println("吃货已经把" +bz.pi + bz.xian
					+"的包子吃完了,包子铺开始生产包子");
				System.out.println("-----------------------");
			}
		}
	}
}
/**		
 * 3、测试类:
 *		包含main方法的类,是程序执行的入口,启动程序
 *		创建包子对象,创建包子铺线程,开启
 *		创建吃货线程,开启
*/
public class Demo04 {
	public static void main(String[] args) {
		//创建包子对象:
		Demo01 bz = new Demo01();
		
		//创建包子铺线程,开启,生产包子
		Demo02 baozipu = new Demo02(bz);
		baozipu.start();
		
		//创建吃货线程,开启,吃包子
		Demo03 chibaozi = new Demo03(bz);
		chibaozi.start();
	}
}

第十章、线程池

10.1、线程池思想概念

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

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

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

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

10.2、线程池概念

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

由于线程池中有很多操作都是与优化资源相关的,我们在这里就不多叙述,我们通过一张图来了解线程的工作原理

线程池可以理解成一个容器,容器我们可以理解成什么?是不是可以理解成一个集合(ArrayList,HashSet,LinkedList,HashMap)这里我们可以用什么呢?是不是可以用一个LinkedList也可以用ArrayList集合这些集合的泛型就放什么呢,就放我们的线程。所以这个线程池实际上就是我们的集合就是我们的容器。

我们来画个图,来理解一下线程池的概念。在这里面创建一下线程到我们的集合中来

使用add方法添加线程

1、当程序第一次启动的时候,我们可以创建多个线程,保存到一个集合中

2、当我们想要使用线程的时候,就可以从集合中取出线程来使用

3、Thread th1 = list.remove(0) :返还的是被移除的元素(线程只能被 一个任务使用)

4、如果使用Thread th2 = LinkedList.removeFirest()

5、当使用线程需要把线程归还给线程池

6、list.add(th1);

7、Linked.addLast(th2);

8、所以线程里面有一个叫做队列的东西。依次拿出线程使用

在JDK1.5以后,JDK内置了线程池,我们可以直接使用。

这就是线程池的原理

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

1、降低资源消耗,减少了创建和销毁线程的次数,每个工作线程都可以被重复利用,可执行多个任务

2、提高相应速度,当任务到达时,任务可以不需要的等到线程创建就可以立即执行

3、提高线程的可管理性,可以根据系统的承受能力,调整线程池中工作线程的数目,防止因为消耗过多的内存,而把服务器累趴下(每个线程需要大约1MB,线程开的越多,消耗的内存也就越大,最后死机)

10.3、线程池的使用

Java里面线程池的顶级接口是java.util.concurrent,Executor,但是严格意义上来说Executor并不是一个线程池,而是一个执行线程的工具。真正的线程池接口是java.util.concurrent.ExecutorService。要配置一个线程池是比较复杂的,尤其是对于线程池的原理并不是很清楚的情况下,很有可能配置的线程池不是比较优的。因此在java.util.concurrent.Executors线程工程类里面提供了一些镜头工厂,生成一些常用的线程池,官方建议使用Executors工程类来创建线程池对象

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

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

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

  • public Future<?> submit(Runnable task):获取线程池的某一个对象并执行
    • Future接口:用来记录线程任务执行完毕后产生的结果,线程池创建与使用。

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

1、创建线程池对象

2、创建Runnable接口子类对象(task)

3、提交Runnable接口子类对象。(task task)

4、关闭线程池(一般不做)

/*
 	线程池:JDK1.5以后提供的
 	在java.util.concurrent.Executors
 	线程池工厂类,用来生产线程池
 	Executors有个静态方法 -- 生产线程池的方法
 	static ExecutorService newFicedThreadPool(int nThreads)
 	创建一个可重用固定线程数的线程池
 	参数:
 		int nThreads:创建线程池中包含的线程数量
 	返回值:
 		ExecutorService接口,返回的是ExecutorService
 		接口的实现类对象,我们可以使用ExecutorService接口接收(面向接口编程)
 
 	java.util.concurrent.ExectorService:线程池接口
 		用来从线程池中获取线程,调用start方法,执行线程任务
 			submit(Runnable task) 提交一个Runnable任务用于执行
 			
 		关闭/消耗线程池的方法
 			shutdown()
 			
 		线程池的使用步骤:
 		1、使用线程池的工厂类Executors里面提供的静态方法newFixedThreadPool
 		生产一个执行线程数量的线程池
 		2、创建一个类,实现Runnable接口,重写run方法,设置线程任务
 		3、调用ExectorService中的方法submit,传递线程任务(实现类),开启线程,执行run方法
 		4、调用ExectorService中的方法shutdown销毁线程池(不建议使用)
 		注意:我们使用线程池的目的是为了反复使用,销毁了就没了对吧。
 */
public class Demo01 {
	public static void main(String[] args) {
		//1、使用线程池的工厂类Executors里面提供的静态方法newFixedThreadPool
// 		生产一个执行线程数量的线程池
		ExecutorService es = Executors.newFixedThreadPool(2);
		
//		3、调用ExectorService中的方法submit,传递线程任务(实现类),开启线程,执行run方法
		es.submit(new RunnableImpl());
//		线程池会一直开启,使用完了线程,会自动把线程归还给线程池,线程池可以继续使用
		es.submit(new RunnableImpl());//pool-1-thread-1创建了一个新的线程
		es.submit(new RunnableImpl());//pool-1-thread-2创建了一个新的线程
//		4、调用ExectorService中的方法shutdown销毁线程池(不建议使用)
		es.shutdown();
		
		//线程池被销毁了,便不能在执行了。会抛出异常
    //不能在获取线程
		es.submit(new RunnableImpl());
	}
}
public class RunnableImpl 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
发出的红包

打赏作者

Jegret

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值