目录
01多线程
Java 是少数的几种支持多线程
的语言之一。大多数的程序语言只能循序运行单独一个程序块,无法同时运行不同的多个程序块。 Java 的多线程
恰可弥补这个缺憾,它可以让不同的程序块一起运行,如此一来可让程序运行更为顺畅,同时也可达到多任务处理的目的。
例如:我们的微信(一个进程)可以边看传输数据(一个线程)边聊天(一个线程),就是多线程
1.1进程与线程
进程:本质上是一个独立执行的程序,进程是操作系统资源分配的基本单位。多进程操作系统能同时运行多个进程(程序)
,由于 CPU 具备分时机制,所以每个进程都能循环获得自己的 CPU 时间片。由于 CPU 执行速度非常快,使得所有程序好象是在同时
运行一样,实际上是交替执行
的。
线程:线程是任务调度和执行的基本单位
。它被包含
在进程之中,是进程中的实际运作单位。一个进程中可以并发多个线程,每条线程执行不同的任务,切换受系统控制
。
多线程:是实现并发机制的一种有效手段
,进程和线程一样,都是实现并发的一个基本单位,线程和进程的主要差别
体现在以下两个方面:
- 1、同样作为基本的执行单元,线程是划分得比进程更小的执行单位,
一个进程可以包含多个线程
。 - 2、每个
进程
都有一段专用
的内存区域。与此相反,线程间却共享内存单元(例如数据),通过共享的内存单元来实现数据交换、实时通信与必要的同步操作。
所以多线程下如何操作
共享数据
是要解决的问题
1.2并发和并行的区别
并发(concurrency) :一核CPU,模拟出来多条线程,快速交替执行,看似并行实际不是并行。
并行 (parallellism):多核CPU ,多个线程可以同时执行;
举例:
- 并发:是一心多用,听课和看电影,但是CPU大脑只有一个,所以轮着来
- 并行:一根猴毛吹出猴万个,不同的猴做不同的事
1.3认识线程
-
在传统的程序语言里,运行的顺序总是必须顺着程序的流程来走,遇到
if-else 语句
就加以判断,遇到for
、while
等循环就会多绕几个圈,最后程序还是按着一定的程序走,且一次只能运行一个程序块。 -
Java 的
多线程
打破了这种传统的束缚,所谓的线程( Thread)是指程序的运行流程
,多线程
的机制则是指可以同时运行多个程序块,这样使CPU的利用率更高,程序运行的效率也变得更高。 -
注意,并发是交替执行,只不过提升了CPU的利用率。
1.4Java中如何实现多线程
1.4.1 通过继承 Thread 类实现多线程
- Thread 类存放在
java.lang
包下,不用手动导包
通过继承Thread类实现多线程的步骤:
-
1,编写一个类继承Thread类
-
2,重写父类的run()方法,将要执行的语句放入方法体中
-
3,使用类对象或者匿名对象调用父类的start()方法启动线程
-
注意:
当我们调用start()方法后,java虚拟机会调用我们重写的run()方法,所以run()方法不需要我们自己调用
-
编写类的格式:
class 类名称 extends Thread // 从 Thread 类扩展出子类
{
属性
方法…
修饰符 run(){ // 复写 Thread 类里的 run()方法
使用线程处理的程序;
}
}
- 优缺点:
优点:代码编写最简单直接操作。
缺点:没返回值,继承一个类后,没法继承其他的类,拓展性差。
- 演示代码:
/*测试使用继承Thread的方式实现多线程*/
public class TestThread01 {
public static void main(String[] args) throws InterruptedException {
//3.使用匿名对象调用父类的start()方法,启动线程
//线程启动后会执行run()方法中的内容
new TestThread().start();
// 3.使用类对象调用父类的start()方法,启动线程
// Thread t=new TestThread();
// t.start();
for(int i=0;i<5;i++){
System.out.println("main执行了。。。");
//让出线程
TimeUnit.MILLISECONDS.sleep(100);
}
}
}
//1.编写一个类实现Thread类
class TestThread extends Thread{
//2.重写父类的run()方法
@Override
public void run() {
for (int i=0;i<5;i++){
System.out.println("TestThread执行了。。。");
try {
//让出线程
TimeUnit.MILLISECONDS.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
- 运行结果
main执行了。。。
TestThread执行了。。。
TestThread执行了。。。
main执行了。。。
TestThread执行了。。。
TestThread执行了。。。
main执行了。。。
TestThread执行了。。。
TestThread执行了。。。
TestThread执行了。。。
main执行了。。。
TestThread执行了。。。
main执行了。。。
TestThread执行了。。。
TestThread执行了。。。
- 分析:
通过上面代码执行的结果可以发现,在main方法执行的过程中,穿插着执行了TestThread方法中run()方法中的内容。
1.4.2通过实现 Runnable 接口实现多线程
- 由于JAVA 程序只允许单一继承,即一个子类只能有一个父类,所以在 Java 中如果一个类
已经继承了一个父类(其他类)
,就不能用继承Thread
类的方式来实现多线程了,因为 Java 不允许多继承,这时候就要用Runnable接口
来创建线程了 - 注意:Thread类是Runnable接口的子类
通过实现 Runnable 接口实现多线程的步骤
- 1,编写一个类实现Runnable接口
- 2,重写父接口中的run()方法,将要执行的语句放入方法体中
- 3,创建Runnable接口子类的实例化对象,把它丢到Thread类中
- 4,通过 Thread 类的 start()方法,启动多线程序
为什么可以把Runnable接口的子类丢到Thread类中?
通过查找JDK 文档中的 Thread 类发现,在 Thread 类之中,有这样一个构造方法:
public Thread(Runnable target)
,由此构造方法可以发现,可以将一个 Runnable 接口的实例化对象作为参数去实例化 Thread 类对象
- 编写类的格式:
class 类名称 implements Runnable // 实现 Runnable 接口
{
属性
方法…
修饰符 run(){ // 复写 Thread 类里的 run()方法
以线程处理的程序;
}
}
- 优缺点:
- 优点:线程类可以实现多个几接口,可以再继承一个类。
- 缺点:没返回值,不能直接启动,需要通过构造一个 Thread 实例传递进去启动。
- 演示代码:
//1.编写一个类实现Runnable接口
class TestThread implements Runnable{
//2.重写父接口的run()方法
public void run(){
for (int i=0;i<3;i++){
System.out.println("TestThread执行了。。。");
}
try {
TimeUnit.MILLISECONDS.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public class TestThread01 {
public static void main(String[] args) throws Exception {
//3.创建Runnable子类的实例化对象
TestThread t=new TestThread();
//4.通过 Thread 类的 start()方法,启动多线程序
new Thread(t).start();
//在主线程中定义一个循环
for(int i=0;i<3;i++){
System.out.println("main执行了。。。");
TimeUnit.MILLISECONDS.sleep(100);
}
}
}
- 运行结果
main执行了。。。
TestThread执行了。。。
TestThread执行了。。。
TestThread执行了。。。
main执行了。。。
main执行了。。。
1.4.3实现Callable接口来实现多线程
Callable接口在:java.util.concurrent
包下
- 功能:类似于Runnable接口,可以创建一个可以
被其他线程执行的实例
实现步骤分析:
- 无论使用什么样的方式创建多线程,我们都需要使用Thread的
start()方法
来启动线程,通过查Thread类发现,此类的构造方法中只能接收Runnable
接口的实现类 - 所以要想使用Callable创建线程,必须要和Thread或者Runnable接口扯上关系
- 经过查看JDK文档发现:
- Runnable有个实现类FutureTask,在`java.util.concurrent`包下面
- FutureTask有个构造可以接收Callable接口的实现类
于是乎我们可以这样
1.创建一个类实现Callable接口(通过泛型指定返值的类型)
2.把实现类的实例放到FutureTask的构造中,FutureTask是Runnable接口的实现类
3.把FutureTask的实例放到Thread中用来创建线程
- 优缺点:
优点:有返回值,拓展性也高
- Callable可以有返回值,返回值在FutureTask里面,使用get方法获取返回值
缺点:Jdk5以后才支持,需要重写call()方法,结合多个类比如 FutureTask 和 Thread 类
public class CallableTest01 {
public static void main(String[] args) {
MyThread2 myThread2=new MyThread2();
//2.把实现类的实例放到FutureTask的构造中,FutureTask是Runnable接口的实现类
FutureTask futureTask=new FutureTask(myThread2);
//3.把FutureTask的实例放到Thread中用来创建线程
new Thread(futureTask,"Thread_A").start();
new Thread(new FutureTask<String>(new MyThread2()),"Thread_B").start();
try {
String returnValue=(String) futureTask.get();//此方法会产生阻塞,一般往后放
System.out.println(returnValue);
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}
}
}
//注意:泛型的参数等于方法的返回值
//1.创建一个类实现Callable接口(通过泛型指定返值的类型)
class MyThread2 implements Callable<String>{
@Override
public String call() throws InterruptedException{
System.out.println("Callable.call.....");
return "aismall";
}
}
1.4.4 三种多线程实现机制的比较
-
Thread
是一个抽象类,只能被继承,而Runable
、Callable
是接口,需要实现接口中的方法。 -
继承 Thread 重写
run()方法
,实现Runable接口需要实现run()方法
,而Callable是需要实现call()方法
。 -
Thread 和 Runable
没有返回值
,Callable有返回值
。 -
实现 Runable 接口的类不能直接调用start()方法,需要 new 一个 Thread 并发该实现类放入 Thread,再通过新建的 Thread 实例来调用start()方法。
-
实现
Callable 接口
的类需要借助FutureTask
(将该实现类放入其中),再将 FutureTask 实例放入Thread
,再通过新建的 Thread 实例来调用start()方,获取返回值只需要借助 FutureTask 实例调用get()方法
即可!
1.5线程状态
- 每个 Java 程序都有一个缺省的主线程(所谓缺省:就是自动启动,不用手动创建),对于 Java 应用程序,主线程就是 main()方法执行的线程。
- 要想实现多线程,必须在主线程中创建新的线程对象。
- 任何线程一般具有五种状态,即
创建
、就绪
、运行
、阻塞
、终止
1、 新建状态
- 在程序中用构造方法创建了一个线程对象后,新的线程对象便处于新建状态,此时,它已经有了相应的内存空间和其它资源,但还处于不可运行状态。
- 新建一个线程对象可采用线程构造方法来实现,例如:
Thread thread=new Thread();
2、 就绪状态
- 新建线程对象后,调用该线程的 start()方法就可以启动线程。当线程启动时,线程进入就绪状态。此时,线程将进入线程队列排队,等待 CPU 服务,这表明它已经具备了运行条件
3、 运行状态
- 当就绪状态的线程被调用并获得处理器资源时,线程就进入了运行状态。此时,自动调用该线程对象的 run()方法。 run()方法定义了该线程的操作和功能。
4、 堵塞状态
-
一个正在执行的线程在某些特殊情况下,如被人为挂起或需要执行耗时的输入输出操作时,将让出 CPU 并暂时中止自己的执行,进入堵塞状态。在可执行状态下,如果调用 sleep()、 suspend()、 wait()等方法,线程都将进入堵塞状态,堵塞时,线程不能进入排队队列,只有当引起堵塞的原因被消除后,线程才可以转入就绪状态。
-
等待阻塞
:运行的线程执行wait()方法,该线程会释放占用的所有资源,JVM会把该线程放入“等待池”中。进入这个状态后,是不能自动唤醒的,必须依靠其他线程调用notify()或notifyAll()方法才能被唤 醒,wait()是 Object 类的方法。 -
同步阻塞
:当线程处于运行状态时,试图获得某个对象的同步锁时,如果该对象的同步锁已经被其他线程占用,Java虚拟机就会把这个线程放到这个对象的锁池中。 -
其他阻塞状态
:当前线程执行了sleep()方法,或者调用了其他线程的join()方法,或者发出了 I/O 请求时,就会进入这个状态。线程会进入到阻塞状态。当sleep()状态超时、join()等待线程终止或者超时、或者 I/O处理完毕时,线程重新转入就绪状态。
5、 死亡状态
- 线程调用 stop()方法时或 run()方法执行结束后,线程即处于死亡状态。处于死亡状态的线程不具有继续运行的能力。
1.6线程操作的一些方法
- 在 JAVA 实现多线程的程序里,虽然 Thread 类实现了Runnable 接口,通过查看JDK可知Runnable接口中只有一个run()方法,操作线程的主要方法在 Thread类之中。
在下面介绍Thread类中的方法
1.6.1 获取和设置线程的名称
public final void setName(String name)
:获取线程名称
public final String getName()
:设置线程名称
- 线程的名称一般在启动线程前设置,但也允许为已经运行的线程设置名称。
- 允许两个 Thread 对象有相同的名字,但为了清晰,应该尽量避免这种情况的发生。
- 另外,如果程序并没有为线程指定名称,则系统会自动的为线程分配一个名称
- 注意:默认情况下JAVA程序会产生两个线程:一个是 main()方法线程,另外一个就是垃圾回收( GC)线程,无需手动开启。
1.6.2 判断线程是否启动
- 调用Thread 类的 start()方法之后启动线程
public final boolean isAlive()
:判断线程是否处于活动状态 - 线程已经启动且尚未终止,则为活动状态。
- 如果该线程处于活动状态,则返回 true;否则返回 false。
- 演示代码:
class TestThread implements Runnable{
public void run(){
System.out.println("TestThread线程启动了。。。");
}
}
public class Test {
public static void main(String[] args) throws Exception {
//创建线程对象
TestThread t=new TestThread();
//调用Thread类的构造
Thread t1=new Thread(t);
//设置线程名字
t1.setName("TestThread");
//获取线程名字
String threadName=t1.getName();
System.out.println(threadName);
System.out.println("还未调用start方法。。。。");
System.out.println("线程状态:"+t1.isAlive());
t1.start();
System.out.println("调用了start方法。。。");
System.out.println("线程状态:"+t1.isAlive());
}
}
- 运行结果:
TestThread
还未调用start方法。。。。
线程状态false
调用了start方法。。。
TestThread线程启动了。。。
线程状态true
1.6.3 后台线程与 setDaemon()方法
- 对 Java 程序来说,只要还有一个前台线程在运行,这个进程就不会结束,如果一个进程中只有后台线程在运行,这个进程就会结束。
- 前台线程是相对后台线程而言的,前面所介绍的线程都是前台线程。
- 那么什么样的线程是后台线程呢?如果某个线程对象在启动(调用 start()方法)之前调用了 setDaemon(true)方法,这个线程就变成了后台线程。
public final void setDaemon(boolean on)
:将该线程标记为守护(后台)线程- 该方法必须在启动线程前调用
1.6.4线程的强制运行
public final void join()throws InterruptedException
:用来强制某一线程运行- 使用 join()方法会抛出一个 InterruptedException,所以在程序中需要用 try…catch()捕获
- 此方法应放在start方法之后
- 所谓的强制执行其实是,当某一线程调用join方法后,该线程中的代码将会被并入到了 main 线程中,此线程中的代码不执行完, main 线程中的代码就只能一直等待
- 查看 JDK 文档可以发现,除了有无参数的 join 方法外,还有两个带参数的 join 方法,分别是
join(long millis)
和join(long millis,int nanos)
,它们的作用是指定合并时间,前者精确到毫秒,后者精确到纳秒,
意思是两个线程合并指定的时间后,又开始分离,回到合并前的状态 - 代码演示
class TestThread implements Runnable{
public void run(){
for(int i=0;i<10;i++) {
System.out.println("TestThread线程启动了。。。");
}
}
}
public class Test {
public static void main(String[] args) throws Exception {
//创建线程对象
TestThread t=new TestThread();
//调用Thread类的构造
Thread t1=new Thread(t);
//设置线程名字
t1.setName("TestThread");
t1.start();
//调用join方法强制此线程执行
// 此线程结束之前不会执行主线程中的内容
try {
t1.join();
}catch (InterruptedException e){
System.out.println("线程中断了。。。");
}
System.out.println("主线程执行了。。。");
}
}
1.6.5 线程的休眠
public static void sleep(long millis) throws InterruptedException
:在指定的毫秒数内让当前正在执行的线程休眠- 使用 sleep()方法会抛出一个 InterruptedException,所以在程序中需要用 try…catch()捕获
1.6.6 线程的中断
public void interrupt()
:中断线程
-public boolean isInterrupted()
:测试线程是否已经中断
1.7多线程的同步
1.7.1 同步问题的引出
- 在卖票程序中,使用多线程,极有可能碰到一种意外,就是同一张票号被打印两次或多次,也可能出现打印出的票号为 0 或是负数。
- 分析下面这行代码
if(tickets>0){
System.out.println(Thread.currentThread().getName()+" 出 售 票
"+tickets--);
}
分析:
- 假设 tickets 的值为 1 的时候,线程 1 刚执行完 if(tickets>0)这行代码,正准备执行下面的代码,就在这时,操作系统将 CPU 切换到了线程 2 上执行,此时 tickets 的值仍为 1,线程 2 执行完上面两行代码, tickets 的值变为 0 后, CPU 又切回到了线程 1上执行,线程 1 不会再执行 if(tickets>0)这行代码,因为先前已经比较过了,并且比较的结果为真,线程 1 将直接往下执行这行代码:
System.out.println(Thread.currentThread().getName()+"出售票"+tickets--);
但此刻 tickets 的值已变为 0,屏幕打印出的将是 0。 - 要想立即见到这种意外,可用在程序中调用 Thread.sleep()静态方法来刻意造成线程间的这种切换,Thread.sleep()方法迫使线程执行到该处后暂停执行,让出 CPU 给别的线程,在指定的时间(这里是毫秒)后, CPU 回到刚才暂停的线程上执行。
- 修改完的 TestThread 代码如下
class TestThread implements Runnable{
//预设2张票
private int ticket=2;
public void run(){
while(true) {
if (ticket > 0) {
//捕获异常
try {
//休眠线程
Thread.sleep(100);
}catch (Exception e){
}
System.out.println(Thread.currentThread().getName() + "出售票" + ticket--);
}else {
break;
}
}
}
}
public class Test {
public static void main(String[] args) throws Exception {
//创建线程对象
TestThread t=new TestThread();
//启动了四个线程,实现了资源共享的目的
new Thread(t).start();
new Thread(t).start();
new Thread(t).start();
new Thread(t).start();
}
}
- 运行结果:
Thread-2出售票0
Thread-1出售票1
Thread-3出售票-1
Thread-0出售票2
- 造成这种意外的根本原因就是因为资源数据访问不同步引起的。那么该如何去解决这个问题呢?解决这种问题的关键是下面要引入的同步的概念
1.7.2同步代码块
if(tickets>0){
System.out.println(Thread.currentThread().getName()+"出售票"+tickets--);
}
- 当一个线程运行到 if(tickets>0)后,CPU 不去执行其它线程,必须等到下一句执行完后才能去执行其它线程中的有关代码块。这段代码就好比一座独木桥,任何时刻,都只能有一个人在桥上行走,程序中不能有多个线程同时在这两句代码之间执行,这就是线程同步。
- 同步代码块定义语法:
synchronized(对象){
需要同步的代码 ;
}
- 修改5.1中的代码:
class TestThread implements Runnable{
//预设2张票
private int ticket=2;
public void run(){
while(true) {
//同步代码块
synchronized (this) {
if (ticket > 0) {
//捕获异常
try {
//休眠线程
Thread.sleep(100);
} catch (Exception e) {
}
System.out.println(Thread.currentThread().getName() + "出售票" + ticket--);
} else {
break;
}
}
}
}
}
public class Test {
public static void main(String[] args) throws Exception {
//创建线程对象
TestThread t=new TestThread();
//启动了四个线程,实现了资源共享的目的
new Thread(t).start();
new Thread(t).start();
new Thread(t).start();
new Thread(t).start();
}
}
- 运行结果:
Thread-0出售票2
Thread-0出售票1
1.7.3 对方法加锁:使用synchronized关键字
- 除了可以对代码块进行同步外,也可以对方法实现同步,只要在需要同步的方法定义前加上 synchronized 关键字即可
- 同步方法定义语法
访问控制符 synchronized 返回值类型 方法名称(参数)
{
…. ;
}
- 在同一类中,使用 synchronized 关键字定义的若干方法,可以在多个线程之间同步,当有一个线程进入了有 synchronized 修饰的方法时,其它线程就不能进入同一个对象使用 synchronized 来修饰的所有方法,直到第一个线程执行完它所进入的synchronized 修饰的方法为止。
1.7.4 死锁
- 一旦有多个进程,且它们都要争用对多个锁的独占访问,那么就有可能发生死锁。
- 如果有一组进程或线程,其中每个线程都在等待其它进程或线程才可以执行操作,那么就称它们被死锁了。
- 最常见的死锁形式是
当线程 1 持有对象 A 上的锁,而且正在等待对象 B 上的锁;
而线程 2 持有对象 B 上的锁,却正在等待对象 A 上的锁。
这两个线程永远都不会获得第二个锁,或是释放第一个锁,所以它们只会永远等待下去。 - 要避免死锁,应该确保在获取多个锁时,在所有的线程中都以相同的顺序获取锁。
- 强制出现死锁的方法:
创建了两个类 Class_A 和 Class_B,它们分别具有方法 funA()和 funB(),在调用对方的方法前, funA()和 funB()都睡眠一会儿。在主类 Test 中创建 Class_A 和Class_B实例,然后,产生第二个线程以构成死锁条件。 funA()和 funB()使用 sleep()方法来强制死锁条件出现。而在真实程序中死锁是较难发现的:
//定义Class_A类
class Class_A{
synchronized void funA(Class_B b){
String ThreadName=Thread.currentThread().getName();
try {
//在调用Class_B中的方法前先睡一会
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(ThreadName+"调用Class_B中的last方法");
b.last();
}
synchronized void last(){
System.out.println("Class_A中的last方法");
}
}
//定义Class_B类
class Class_B{
synchronized void funB(Class_A a){
String ThreadNmae=Thread.currentThread().getName();
try {
//在调用Class_A中的方法前先睡一会
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(ThreadNmae+"调用Class_A中last方法");
a.last();
}
synchronized void last(){
System.out.println("Class_B中的last方法");
}
}
//定义测试类并实现Runnable接口(主类)
public class Test implements Runnable{
Class_A a=new Class_A();
Class_B b=new Class_B();
//无参构造
Test(){
//设置当前线程的名字
Thread.currentThread().setName("Main——>Thread");
//调用此方法会默认调用该类的run方法
new Thread(this).start();
a.funA(b);
System.out.println("mian线程运行结束");
}
@Override
public void run() {
Thread.currentThread().setName("Test——>Thread");
b.funB(a);
System.out.println("其他线程执行完毕");
}
public static void main(String[] args) {
//创建一个类的实例会自动调用这个类的空参构造
new Test();
}
}
- 分析:
从运行结果可以发现, Test -->> Thread 进入了 b 的监视器,然后又在等待 a 的监视器。同时 Main -->> Thread 进入了 a 的监视器并等待 b 的监视器。这个程序永远不会完成,构成死锁。
1.8线程间的通讯
1.8.1 问题的引出
下面通过这样的一个应用来讲解线程间的通信。
- 把一个数据存储空间划分为两部分:
一部分用于存储人的姓名
另一部分用于存储人的性别。 - 这里的应用包含两个线程:
一个线程向数据存储空间添加数据
另一个线程从数据存储空间中取出数据 - 这个程序有两种意外需要读者考虑?
第一种意外:假设添加数据的线程刚向数据存储空间中添加了一个人的姓名,还没有加入这个人的性别, CPU 就切换到了取出数据线程,取出数据的线程就会把这个人的姓名和上一个人的性别联系到了一起。
假设我们把存入数据线程命名为生产者线程,取出数据的线程命名为消费者线程,这种意外可以用下图表示:
第二种意外:存入数据的线程放了若干次数据,取出数据的线程才开始取数据,或者是取出数据的线程取完一个数据后,还没等到存入数据的线程放入新的数据,又重复取出已取过的数据
1.8.2 问题如何解决
1.8.2.1问题的演示
- 先看一下刚才假设的情况的代码演示
- 第一步:定义一个Person类
//定义Person类
class Person{
String name;
String sex;
}
- 第二步:定义生产者线程Producer
//定义生产者线程
class Producer implements Runnable{
//类做成员变量
Person p=null;
//有参构造
public Producer(Person p) {
this.p = p;
}
@Override
public void run() {
int i=0;
while(true) {
//如果线程不停,就交替赋值
if(i==0) {
p.name="张三";
p.sex="男";
} else {
p.name="李四";
p.sex="女";
}
i=(i+1)%2;
}
}
}
- 第三步:定义消费者线程
//定义消费者线程
class Consumer implements Runnable{
//类做成员变量
Person p=null;
//Consumer的有参构造
public Consumer(Person p) {
this.p = p;
}
@Override
public void run() {
while(true){
System.out.println(p.name+"<---->"+p.sex);
}
}
}
- 第四步:编写测试类
public class Test {
public static void main(String[] args) {
Person p=new Person();
new Thread(new Producer(p)).start();
new Thread(new Consumer(p)).start();
}
}
- 运行结果(部分)
张三<---->女
张三<---->男
李四<---->女
张三<---->女
李四<---->男
张三<---->男
张三<---->女
张三<---->女
张三<---->男
张三<---->男
- 运行结果分析:
从输出结果可以发现,原本李四是女、张三是男,现在却打印出了张三是女的奇怪现象,这是什么原因呢?从程序可以发现, Producer 类和 Consumer 类都是操纵了同一个 Person 类,有可能 Producer 类还未操纵完 Person 类, Consumer 类就已经将 Person 类中的内容取走了,这就是资源不同步的与原因
1.8.2.2问题的解决
- Person 类中增加两个同步方法: set()和 get()
- 第一步:定义Person类
//定义Person类
class Person{
private String name;
private String sex;
//定义set方法,将两个属性一起赋值
public synchronized void set(String name,String sex){
this.name=name;
this.sex=sex;
}
public synchronized void get(){
System.out.println(this.name+"<---->"+this.sex);
}
}
- 第二步:定义Producer线程
//定义生产者线程
class Producer implements Runnable{
//类做成员变量
Person p=null;
//有参构造
public Producer(Person p) {
this.p = p;
}
@Override
public void run() {
int i=0;
while(true) {
//如果线程不停,就交替赋值
if(i==0) {
p.set("张三","男");
} else {
p.set("李四","女");
}
i=(i+1)%2;
}
}
}
- 第三步:定义Consumer线程
//定义消费者线程
class Consumer implements Runnable{
//类做成员变量
Person p=null;
//Consumer的有参构造
public Consumer(Person p) {
this.p = p;
}
@Override
public void run() {
while(true){
p.get();
}
}
}
- 第四步:编写测试类:
public class Test {
public static void main(String[] args) {
Person p=new Person();
new Thread(new Producer(p)).start();
new Thread(new Consumer(p)).start();
}
}
- 运行结果:
张三<---->男
张三<---->男
张三<---->男
张三<---->男
李四<---->女
李四<---->女
李四<---->女
李四<---->女
- 结果分析:
可以发现程序的输出结果是正确的,但是这里又有一个新的问题产生了,从程序的执行结果来看, Consumer 线程对 Producer 线程放入的一次数据连续读取了多次,并不符合实际的要求。实际要求的结果是, Producer 放一次数据, Consumer 就取一次;反之, Producer 也必须等到 Consumer 取完后才能放入新的数据,而这一问题的解决就需要使用下面所要讲到的线程间的通信
1.8.2.3问题的进一步解决
-
Java 是通过 Object 类的 wait、 notify、 notifyAll,这几个方法来实现线程间的通信的,又因为所有的类都是从 Object 继承的,所以任何类都可以直接使用这些方法。下面是这三个方法的简要说明:
wait:调用此方法的线程进入睡眠状态,直到其它线程调用该线程的 notify 方法才能被唤醒。
notify:唤醒同一对象监听器中调用 wait 的第一个线程。类似排队买票,一个人买完之后,后面的人可以继续买。
notifyAll:唤醒同一对象监听器中调用 wait 的所有线程,具有最高优先级的线程首先被唤醒并执行。 -
如果想让上面的程序符合预先的设计需求,必须在类 Person中定义一个新的成员变量status 来表示数据存储空间的状态,当Consumer 线程取走数据后,status 值为 false,当Producer 线程放入数据后, status 值为 true。
也就是status 为 true 时, Consumer 线程才能取数据,否则就必须等待 Producer 线程放入新的数据后的通知
反之,只有 status为 false, Producer 线程才能放入新的数据,否则就必须等待 Consumer 线程取走数据后的通知 -
第一步:定义Person类
//定义Person类
class Person{
private String name;
private String sex;
private boolean status=false;
//定义set方法,将两个属性一起赋值
public synchronized void set(String name,String sex){
if (status){
try {
// 睡眠线程,如果没有其他线程调用此线程的notify方法,此线程将一直睡眠
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
this.name=name;
try {
//将此线程睡眠一会
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
this.sex=sex;
status=true;
//唤醒最先被睡眠的的此类的线程
notify();
}
public synchronized void get(){
if(!status){
try {
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println(this.name+"<---->"+this.sex);
status=false;
notify();
}
}
- 第二步:定义Producer线程
//定义生产者线程
class Producer implements Runnable{
//类做成员变量
Person p=null;
//有参构造
public Producer(Person p) {
this.p = p;
}
@Override
public void run() {
int i=0;
while(true) {
//如果线程不停,就交替赋值
if(i==0) {
p.set("张三","男");
} else {
p.set("李四","女");
}
i=(i+1)%2;
}
}
}
- 第三步:定义Consumer线程
//定义消费者线程
class Consumer implements Runnable{
//类做成员变量
Person p=null;
//Consumer的有参构造
public Consumer(Person p) {
this.p = p;
}
@Override
public void run() {
while(true){
p.get();
}
}
}
- 第四步:定义测试类
public class Test {
public static void main(String[] args) {
Person p=new Person();
new Thread(new Producer(p)).start();
new Thread(new Consumer(p)).start();
}
}
- 运行结果
张三<---->男
李四<---->女
张三<---->男
李四<---->女
张三<---->男
李四<---->女
张三<---->男
- 运行结果分析
上面的程序满足了设计的需求,解决了线程间通信的问题,即一个线程自己调用wait()方法进入睡眠,让其他线程来唤醒。 - wait、 notify、 notifyAll 这三个方法只能在 synchronized 方法中调用,即无论线程调用一个对象的 wait 还是 notify 方法,该线程必须先得到该对象的锁标记,这样, notify只能唤醒同一对象监视器中调用 wait 的线程,使用多个对象监视器,就可以分别有多个 wait、 notify 的情况,同组里的 wait 只能被同组的 notify 唤醒。
- 一个线程的等待和唤醒过程
1.9线程生命周期
- 任何事务都有一个生命周期,线程也不例外。那么在一个程序中,怎样控制一个线程的生命并让它更有效地工作呢?要想控制线程的生命,先得了解线程产生和消亡的整个过程。
- 任何线程一般具有五种状态,即创建、就绪、运行、阻塞、终止
- Java 语言中线程共有六种状态,分别是:
NEW(初始化状态)
RUNNABLE(可运行 / 运行状态)
BLOCKED(阻塞状态)
WAITING(无时限等待)
TIMED_WAITING(有时限等待)
TERMINATED(终止状态)
其中:BLOCKED,WAITING,TIMED_WAITING 这三种状态属于Not Runnable状态。 - 线程的声明周期图
- 从上图可知通过调用不同的方法可以把线程装换为不同的状态
02 线程池
2.1 为什么要使用线程池?
我们使用线程的时候就去创建一个线程,这样实现起来非常简便,但是就会有一个问题:
- 如果并发的线程数量很多,并且每个线程都是执行一个时间很短的任务就结束了,这样频繁创建线程就会大大降低系统的效率,因为频繁创建线程和销毁线程需要时间。
- 那么有没有一种办法使得线程可以复用,就是执行完一个任务,并不被销毁,而是可以继续执行其他的任务?
- 在Java中可以通过线程池来达到这样的效果
2.2 线程池的概念
- 线程池其实就是一个容纳多个线程的容器,其中的线程可以反复使用,省去了频繁创建线程对象的操作,无需反复创建线程而消耗过多资源。
- 合理利用线程池能够带来三个好处:
- 降低资源消耗。减少了创建和销毁线程的次数,每个工作线程都可以被重复利用,可执行多个任务。
- 提高响应速度。当任务到达时,任务可以不需要的等到线程创建就能立即执行。
- 提高线程的可管理性。可以根据系统的承受能力,调整线程池中工作线线程的数目,防止消耗过多的内存(每个线程需要大约1MB内存,线程开的越多,消耗的内存也就越大,最后死机)。
2.3 线程池的简单使用
-
JDK1.5之后才有
-
Java里面线程池的顶级接口是
java.util.concurrent.Executor
,但是严格意义上讲Executor
并不是一个线程池,而只是一个执行线程的工具。真正的线程池接口是java.util.concurrent.ExecutorService
。 -
java.util.concurrent.ExecutorService
是java.util.concurrent.Executor
接口的子接口 -
要配置一个线程池是比较复杂的,尤其是对于线程池的原理不是很清楚的情况下,很有可能配置的线程池不是较优的,因此在
java.util.concurrent.Executors
线程工厂类里面提供了一些静态工厂,生成一些常用的线程池。官方建议使用Executors工厂类来创建线程池对象。
Executors类中有个创建线程池的方法如下:
public static ExecutorService newFixedThreadPool(int nThreads)
:返回线程池对象。(创建的是有界线程池,也就是池中的线程个数可以指定最大数量)
获取到了一个线程池ExecutorService 对象,那么怎么使用呢,在这里定义了一个使用线程池对象的方法如下:
-
public Future<?> submit(Runnable task)
:获取线程池中的某一个线程对象,并执行Future接口:用来记录线程任务执行完毕后产生的结果。线程池创建与使用。
使用线程池中线程对象的步骤:
- 创建线程池对象。
- 创建Runnable接口子类对象。(task)
- 提交Runnable接口子类对象。(take task)
- 关闭线程池(一般不做)。
Runnable实现类代码:
public class MyThread 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 service = Executors.newFixedThreadPool(2);//包含2个线程对象
// 创建Runnable实例对象
MyThread t = new MyThread();
//自己创建线程对象的方式
// Thread t = new Thread(r);
// t.start(); ---> 调用MyThread中的run()
// 从线程池中获取线程对象,然后调用MyThread中的run()
service.submit(t);
// 再获取个线程对象,调用MyThread的run()
service.submit(t);
service.submit(t);
// 注意:submit方法调用结束后,程序并不终止,是因为线程池控制了线程的关闭。
// 将使用完的线程又归还到了线程池中
// 关闭线程池(一般不做)
//service.shutdown();
}
}
- 运行结果:
创建一个线程
创建一个线程
创建的线程: pool-1-thread-1
创建的线程: pool-1-thread-2
线程执行结束,将线程返回给线程池
线程执行结束,将线程返回给线程池
创建一个线程
创建的线程: pool-1-thread-2
线程执行结束,将线程返回给线程池