第十二章 多线程机制
小结
1:多线程(理解)
(1)多线程:一个应用程序有多条执行路径
进程:正在执行的应用程序
线程:进程的执行单元,执行路径
单线程:一个应用程序只有一条执行路径
多线程:一个应用程序有多条执行路径
多进程的意义?
提高CPU的使用率
多线程的意义?
提高应用程序的使用率
(2)Java程序的运行原理及JVM的启动是多线程的吗?
A:Java命令去启动JVM,JVM会启动一个进程,该进程会启动一个主线程。
B:JVM的启动是多线程的,因为它最低有两个线程启动了,主线程和垃圾回收线程。
(3)多线程的实现方案(自己补齐步骤及代码 掌握)
A:继承Thread类
B:实现Runnable接口
(4)线程的调度和优先级问题
A:线程的调度
a:分时调度
b:抢占式调度 (Java采用的是该调度方式)
B:获取和设置线程优先级
a:默认是5
b:范围是1-10
(5)线程的控制(常见方法)
A:休眠线程
B:加入线程
C:礼让线程
D:后台线程
E:终止线程(掌握)
(6)线程的生命周期(参照 线程生命周期图解.bmp)
A:新建
B:就绪
C:运行
D:阻塞
E:死亡
(7)电影院卖票程序的实现
A:继承Thread类
B:实现Runnable接口
(8)电影院卖票程序出问题
A:为了更符合真实的场景,加入了休眠100毫秒。
B:卖票问题
a:同票多次
b:负数票
(9)多线程安全问题的原因(也是我们以后判断一个程序是否有线程安全问题的依据)
A:是否有多线程环境
B:是否有共享数据
C:是否有多条语句操作共享数据
(10)同步解决线程安全问题
A:同步代码块
synchronized(对象) {
需要被同步的代码;
}
这里的锁对象可以是任意对象。
B:同步方法
把同步加在方法上。
这里的锁对象是this
C:静态同步方法
把同步加在方法上。
这里的锁对象是当前类的字节码文件对象(反射再讲字节码文件对象)
(11)回顾以前的线程安全的类
A:StringBuffer
B:Vector
C:Hashtable
D:如何把一个线程不安全的集合类变成一个线程安全的集合类
用Collections工具类的方法即可。
1.进程与线程
程序
是一段静态的代码,它是应用软件执行的蓝本。
进程
正在运行的程序,是系统进行资源分配和调用的独立单位。
每一个进程都有它自己的内存空间和系统资源。
-
多进程的意义:
- 单进程的计算机只能做一件事情,而我们现在的计算机都可以做很多件事情。
举例:一边玩游戏(游戏进程),一边听音乐(音乐进程)。 - 也就是说在的计算机都是支持多进程的,就可以在一个时间段内执行多个任务。
并且可以提高CPU的使用率 - 问题:
一边玩游戏,一边听音乐是同时进行的吗?
不是,因为CPU在某一个时间点上只能做一件事情。
而我们在玩游戏,或者听音乐的时候,是CPU在走着程序间的高校切换让我们觉得是同时进行的。
线程
是程序的执行单元,执行路径,是程序使用CPU的最基本单位。
线程间可以共享进程中的某些内存单元(包括代码与数据),线程的中断与恢复可以更加节省系统的开销。 - 单进程的计算机只能做一件事情,而我们现在的计算机都可以做很多件事情。
(1) java中的多线程机制
Java语言的一大特性点就是内置对多线程的支持。
Java虚拟机快速地把控制从一个线程切换到另一个线程。这些线程将被轮流执行,使得每个线程都有机会使用CPU资源。
- 多线程的意义:
- 多线程的存在,不是提高程序的执行速度,其实是为了提高应用程序的 使用率。
- 程序的执行其实都是在抢CPU的资源,CPU的执行权。
- 多进程是在抢这个资源,而其中的某一个进程如果执行路径比较多,就会有更高的几率抢到CPU的执行权。
- 我们是不敢保证哪一个线程能够在那个时刻抢到,所以线程的执行有 随机性。
(2)主线程(main线程)
每个Java应用程序都有一个缺省的主线程。
当JVM(Java Virtual Machine 虚拟机)加载代码,发现main方法之后,就会启动一个线程,这个线程称为“主线程”(main线程),该线程负责执行main方法。
JVM一直要等到Java应用程序中的所有线程都结束之后,才结束Java应用程序 。
(3)线程的常用方法
1.start() :
线程调用该方法将启动线程,使之从新建状态进入就绪队列排队,一旦轮到它来享用CPU资源时,就可以脱离创建它的线程独立开始自己的生命周期了。
2.run():
Thread类的run()方法与Runnable接口中的run()方法的功能和作用相同,都用来定义线程对象被调度之后所执行的操作,都是系统自动调用而用户程序不得引用的方法。
3.sleep(int millsecond):
优先级高的线程可以在它的run()方法中调用sleep方法来使自己放弃CPU资源,休眠一段时间。
4.isAlive():
线程处于“新建”状态时,线程调用isAlive()方法返回false。在线程的run()方法结束之前,即没有进入死亡状态之前,线程调用isAlive()方法返回true。
5.currentThread():
该方法是Thread类中的类方法,可以用类名调用,该方法返回当前正在使用CPU资源的线程。
6.interrupt() :
一个占有CPU资源的线程可以让休眠的线程调用interrupt()方法“吵醒”自己,即导致休眠的线程发生InterruptedException异常,从而结束休眠,重新排队等待CPU资源。
(4)线程的状态与生命周期
建的线程在它的一个完整的生命周期中通常要经历如下的四种状态:
1.新建: 当一个Thread类或其子类的对象被声明并创建时,新生的线程对象处于新建状态。
2.运行 :线程必须调用start()方法(从父类继承的方法)通知JVM,这样JVM就会知道又有一个新一个线程排队等候切换了。一旦轮到它来享用CPU资源时,此线程的就可以脱离创建它的主线程独立开始自己的生命周期了。
3.中断:有4种原因的中断:
◆ JVM将CPU资源从当前线程切换给其他线程,使本线程让出CPU的使用权处于中断状态。
◆线程使用CPU资源期间,执行了sleep(int millsecond)方法,使当前线程进入休眠状。
◆线程使用CPU资源期间,执行了wait()方法。
◆线程使用CPU资源期间,执行某个操作进入阻塞状态。
4.死亡 :处于死亡状态的线程不具有继续运行的能力。线程释放了实体。
例子(一般来说,执行线程的代码肯定是比较耗时的,所以我们用循环改进)
public class Example12_1 {
public static void main(String[] args) { //主线程
SpeakElephant speakelephant;
SpeakCar speakcar;
speakelephant=new SpeakElephant(); //创建线程
speakcar=speakcar=new SpeakCar(); //创建线程
speakelephant.start(); //启动线程
speakcar.start(); //启动线程
for(int i=1;i<15;i++) {
System.out.println("主人"+i+" ");
}
}
public static class SpeakElephant extends Thread{ //Thread类的子类
public void run() {
for(int i=1;i<20;i++) {
System.out.println("大象"+i+" ");//如果要获取线程的名称
} //System.out.println(getName()+"大象"+i+" ");
} //这个类是继承了Thread类了的
}
public static class SpeakCar extends Thread{ //Thread类的子类
public void run() {
for(int i=1;i<20;i++) {
System.out.print("轿车"+i+" ");
}
}
}
}
运行结果(注意最后一个用的是print不是println,每次运行的结果都不带一样的,真的很好体现了线程执行的随机性啊,附上三张图)
(4)线程调度与优先级
- 处于就绪状态的线程首先进入就绪队列排队等候CPU资源,同一时刻在就绪队列中的线程可能有多个。Java虚拟机(JVM)中的线程调度器负责管理线程,调度器把线程的优先级分为10个级别,分别用Thread类中的类常量表示。
- Java使用的是抢占式调度模型。 优先让优先级高的线程使用 CPU,如果线程的优先级相同,那么会随机选择一个,优先级高的线程获取的 CPU 时间片相对多一些。
设置和获取线程优先级
public final int getPriority()
默认优先级的值是5
public final void setPriority(int newPriority)
参数范围 1-10
(5)实现线程的控制
-
线程休眠
(在指定的毫秒数内让当前正在执行的线程休眠(暂停执行),此操作受到系统计时器和调度程序精度和准确性的影响。)
public static voi sleeplong (millis) -
线程加入
(等待该线程终止。)
public final void join() -
线程礼让
(暂停当前正在执行的线程对象,并执行其他线程。让多个线程的执行更和谐,近似于一人执行一次,但是不能靠它保证一人一次)
public static void yield() -
后台线程
(将该线程标记为守护线程或用户线程。当正在运行的线程都是守护线程时,Java 虚拟机退出。 该方法必须在启动线程前调用。)
public final void setDaemon(boolean on) -
中断线程
(让线程停止,过时了,但是还可以使用。只是不太安全,太暴力了)
public final void stop()
2.Thread类与线程的创建
(1)如何实现多线程的程序
- 由于线程是依赖进程而存在的,所以我们应该先创建一个进程出来,
而进程是由系统创建的,所以我们应该去调用系统功能创建一个进程
JAVA是不能世界调用系统功能的,所以我们没有办法直接实现多线程程序。
但是呢?JAVA可以调用C/C++写好的程序来实现多线程程序。
由C/C++去调用系统功能创建进程,然后由JAVA去调用这样的东西,然后提供一些类供我们使用,我们就可以实现多线程程序了。 - 那么JAVA提供的类是什么呢?
Thread类!
方式一 继承Thread类
- 自定义MyThread继承Thread类
- MyThread类里面重写run()方法
- 创建对象
- 启动线程
方式二 实现Runnable接口
- 自定义类MyRunnable实现Runnable接口
- 重写run()方法
- 创建MyRunnable类的对象
- 创建Thread类的对象,并把第三步的对象作为构造参数传递
public class MyRunnable implements Runnable {
@Override
public void run() {
for (int x = 0; x < 100; x++) {
// 由于实现接口的方式就不能直接使用Thread类的方法了,但是可以间接的使用
System.out.println(Thread.currentThread().getName() + ":" + x);
}
}
}
public class MyRunnableDemo {
public static void main(String[] args) {
// 创建MyRunnable类的对象
MyRunnable my = new MyRunnable();
// 创建Thread类的对象,并把C步骤的对象作为构造参数传递,参数要的是接口实质上要的是实现类对象
// Thread(Runnable target)
// Thread t1 = new Thread(my);
// Thread t2 = new Thread(my);
// t1.setName("林青霞");
// t2.setName("刘意");
// Thread(Runnable target, String name)
Thread t1 = new Thread(my, "zjl");
Thread t2 = new Thread(my, "zyf");
t1.start();
t2.start();
}
}
(2)Thread类的基本获取和设置方法
-
public final String getName()
public class MyThread extends Thread { public MyThread() { } public MyThread(String name){ super(name); } @Override public void run() { for (int x = 0; x < 100; x++) { System.out.println(getName() + ":" + x); } } }
-
public final void setName(String name)
其实通过构造方法也可以给线程起名字,当然我觉得最方便的还是用set方法设置名字。
public class MyThreadDemo {
public static void main(String[] args) {
// 创建线程对象
//无参构造+setXxx()
// MyThread my1 = new MyThread();
// MyThread my2 = new MyThread();
// //调用方法设置名称
// my1.setName("林青霞");
// my2.setName("刘意");
// my1.start();
// my2.start();
//带参构造方法给线程起名字
// MyThread my1 = new MyThread("zjl");
// MyThread my2 = new MyThread("zrf");
// my1.start();
// my2.start();
}
}
- 获取main方法所在的线程名称
public static Thread currentThread()
//我要获取main方法所在的线程对象的名称,该怎么办呢?
System.out.println(Thread.currentThread().getName());
(3) 关于run方法启动的次数
对于具有相同目标对象的线程,当其中一个线程享用CPU资源时,目标对象自动调用接口中的run方法,这时,run方法中的局部变量被分配内存空间,当轮到另一个线程享用CPU资源时,目标对象会再次调用接口中的run方法,那么,run()方法中的局部变量会再次分配内存空间。也就是说run()方法已经启动运行了两次,分别运行在不同的线程中,即运行在不同的时间片内。
3.线程同步
- 在处理多线程问题时,我们必须注意这样一个问题:
当两个或多个线程同时访问同一个变量,并且一个线程需要修改这个变量。我们应对这样的问题作出处理。- 同步可以解决安全问题的根本原因就在那个锁对象上。该对象如同锁的功能。当线程开始执行同步代码块前,必须先获得对同步代码块的锁定。并且任何时刻只能有一个线程可以获得对同步监视器的锁定,当同步代码块执行完成后,该线程会释放对该同步监视器的锁定
- 在处理线程同步时,要做的第一件事就是要把修改数据的方法用关键字synchronized来修饰。
(1) 线程的安全问题
- 出现安全问题的原因
(1)是否是多线程环境
(2)是否有共享数据
(3)是否有多条语句操作共享数据 - 解决问题
把多个语句操作共享数据的代码给锁起来,让任意时刻只能有一个线程执行即可—> synchronized
( 一般都是解决第三个问题,你都已经在做多线程的东西了,自然也是有共享数据的,所以一二问题不考虑了,当然,这是一般)
同步代码块
synchronized(对象)
{
需要同步的代码;
}
注意:锁对象不一样的三种情况
(1) 同步代码块的锁对象:任意对象
public class SellTicket implements Runnable {
// 定义100张票
private int tickets = 100;
//创建锁对象,得是同一个锁对象,这个对象我们可以认为是任意一个对象,同步方法锁对象是 this
private Object obj = new Object();
@Override
public void run() {
while (true) {
synchronized (obj) {
if (tickets > 0) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()
+ "正在出售第" + (tickets--) + "张票");
}
}
}
}
}
private void sellTicket() {
if (tickets > 0) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()
+ "正在出售第" + (tickets--) + "张票 ");
}
}
(2)如果一个方法一进去就看到了代码被同步,就能把同步加在方法上。
格式:把同步关键字加在方法上
这时的锁对象:this
public class SellTicket implements Runnable {
// 定义100张票
private int tickets = 100;
//创建锁对象,得是同一个锁对象,这个对象我们可以认为是任意一个对象,同步方法锁对象是 this
// private Object obj = new Object();
@Override
public void run() {
while (true) {
synchronized (this) {
if (tickets > 0) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()
+ "正在出售第" + (tickets--) + "张票");
}
}
}
}
}
// 同步方法
private synchronized void sellTicket() {
if (tickets > 0) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()
+ "正在出售第" + (tickets--) + "张票 ");
}
}
(3)如果方法是静态方法,锁对象:类的class对象(当前类.class)
public class SellTicket implements Runnable {
// 定义100张票
private static int tickets = 100;
//创建锁对象,得是同一个锁对象,这个对象我们可以认为是任意一个对象,同步方法锁对象是 this
// private Object obj = new Object();
@Override
public void run() {
while (true) {
synchronized (SellTicket.class) {
if (tickets > 0) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()
+ "正在出售第" + (tickets--) + "张票");
}
}
}
}
}
// 同步方法
private static synchronized void sellTicket() {
if (tickets > 0) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()
+ "正在出售第" + (tickets--) + "张票 ");
}
}
同步的前提
程序中需要多个线程
多个线程使用的是同一个锁对象
同步的好处
同步的出现解决了多线程的安全问题。
同步的弊端
当线程相当多时,因为每个线程都会去判断同步上的锁,这是很耗费资源的,无形中会降低程序的运行效率。
4.死锁问题
是指两个或者两个以上的线程在执行的过程中,因争夺资源产生的一种互相等待现象
public class DieLock extends Thread {
private boolean flag;
public DieLock(boolean flag) {
this.flag = flag;
}
@Override
public void run() {
if (flag) {
synchronized (MyLock.objA) {
System.out.println("if objA");
synchronized (MyLock.objB) {
System.out.println("if objB");
}
}
} else {
synchronized (MyLock.objB) {
System.out.println("else objB");
synchronized (MyLock.objA) {
System.out.println("else objA");
}
}
}
}
}
5.协调同步的线程
6.线程间通信
- 针对同一个资源的操作有不同种类的线程
举例:卖票有进的,也有出的。 - 通过设置线程(生产者)和获取线程(消费者)针对同一个学生对象进行操作
线程间通信的代码改进
A:通过等待唤醒机制实现数据依次出现
B:把同步代码块改进为同步方法实现
7.线程状态转换图
8.线程组
Java中使用ThreadGroup来表示线程组,它可以对一批线程进行分类管理,Java允许程序直接对线程组进行控制。
默认情况下,所有的线程都属于主线程组。
public final ThreadGroup getThreadGroup()
我们也可以给线程设置分组
Thread(ThreadGroup group, Runnable target, String name)
9.线程池
程序启动一个新线程成本是比较高的,因为它涉及到要与操作系统进行交互。而使用线程池可以很好的提高性能,尤其是当程序中要创建大量生存期很短的线程时,更应该考虑使用线程池。
- 线程池里的每一个线程代码结束后,并不会死亡,而是再次回到线程池中成为空闲状态,等待下一个对象来使用。
- 在JDK5之前,我们必须手动实现自己的线程池,从JDK5开始,Java内置支持线程池
JDK5新增了一个Executors工厂类来产生线程池,有如下几个方法
- public static ExecutorService newCachedThreadPool()
- public static ExecutorService newFixedThreadPool(int nThreads)
- public static ExecutorService newSingleThreadExecutor()
- 这些方法的返回值是ExecutorService对象,该对象表示一个线程池,可以执行Runnable对象或者Callable对象代表的线程。它提供了如下方法
Future<?> submit(Runnable task)
Future submit(Callable task)
10.匿名内部类方式使用多线程
- 匿名内部类方式使用多线程
- new Thread(){代码…}.start();
- New Thread(new Runnable(){代码…}).start();
11.线程联合
一个线程A在占有CPU资源期间,可以让其它线程调用join()和本线程联合,如:
B.join();
称A在运行期间联合了B。如果线程A在占有CPU资源期间一旦联合B线程,那么A线程将立刻中断执行,一直等到它联合的线程B执行完毕,A线程再重新排队等待CPU资源,以便恢复执行。如果A准备联合的B线程已经结束,那么B.join()不会产生任何效果。
12.GUI线程
当Java程序包含图形用户界面(GUI)时,Java虚拟机在运行应用程序时会自动启动更多的线程,其中有两个重要的线程:AWT-EventQuecue和AWT-Windows。AWT-EventQuecue线程负责处理GUI事件,AWT-Windows线程负责将窗体或组件绘制到桌面。JVM要保证各个线程都有使用CPU资源的机会,比如,程序中发生GUI界面事件时,JVM就会将CPU资源切换给AWT-EventQuecue线程,AWT-EventQuecue线程就会来处理这个事件,比如,你单击了程序中的按钮,触发ActionEvent事件,AWT-EventQuecue线程就立刻排队等候执行处理事件的代码
13.计时器线程
-
定时器是一个应用十分广泛的线程工具,可用于调度多个定时任务以后台线程的方式执行。在Java中,可以通过Timer和TimerTask类来实现定义调度的功能
-
Timer
- public Timer()
- public void schedule(TimerTask task, long delay)
- public void schedule(TimerTask task,long delay,long period)
-
TimerTask
- public abstract void run()
- public boolean cancel()
14.守护线程
一个线程调用void setDaemon(boolean on)方法可以将自己设置成一个守护(Daemon)线程,例如:
thread.setDaemon(true);
当程序中的所有用户线程都已结束运行时,即使守护线程的run方法中还有需要执行的语句,守护线程也立刻结束运行。
内容有点多了,过两天回家,祈祷自己回家还学得进去