线程的使用,第一部分
1.多线程理解
如果程序只有一条执行路径,那么该程序是单线程程序。如果该程序有多条执行路径,那么该程序是多线程程序。
1 | 1:要想了解多线程,必须先了解线程,而要想了解线程,必须先了解进程,因为线程是依赖于进程而存在。 |
总结:
- 进程:
- 正在运行的程序,是系统进行资源分配和调用的独立单位。
- 每一个进程都有它自己的内存空间和系统资源。
- 线程:
- 是进程中的单个顺序控制流,是一条执行路径
- 一个进程如果只有一条执行路径,则称为单线程程序。
- 一个进程如果有多条执行路径,则称为多线程程序。
Java程序的运行原理:
由java命令启动JVM,JVM启动就相当于启动了一个进程。接着有该进程创建了一个主线程去调用main方法。
jvm虚拟机的启动是单线程的还是多线程的?
多线程的。
原因是垃圾回收线程也要先启动,否则很容易会出现内存溢出。现在的垃圾回收线程加上前面的主线程,最低启动了两个线程,所以,jvm的启动其实是多线程的。
2.实现多线程的程序
2-1.如何实现呢?
由于线程是依赖进程而存在的,所以我们应该先创建一个进程出来。而进程是由系统创建的,所以我们应该去调用系统功能创建一个进程。Java是不能直接调用系统功能的,所以,我们没有办法直接实现多线程程序。Java可以去调用写好的程序来实现多线程程序。由JAVA去调用这样的东西,然后提供一些类供我们使用。我们就可以实现多线程程序了。
2-2.那么Java提供的类是什么呢?
Thread。通过查看API,我们知道了有2中方式实现多线程程序。
方式1:继承Thread类。
步骤:
- A:自定义类MyThread继承Thread类。
- B:MyThread类里面重写run()
- C:创建对象
- D:启动线程
问题1:该类要重写run()方法,为什么呢?
不是类中的所有代码都需要被线程执行的。而这个时候,为了区分哪些代码能够被线程执行,java提供了Thread类中的run()用来包含那些被线程执行的代码。
问题2:调用run()方法为什么是单线程的呢?
因为run()方法直接调用其实就相当于普通的方法调用,所以你看到的是单线程的效果要想看到多线程的效果,就必须说说另一个方法:start()。
面试题:run()和start()的区别?
- run():仅仅是封装被线程执行的代码,直接调用是普通方法
- start():首先启动了线程,然后再由jvm去调用该线程的run()方法。
代码实现:
1 | public class MyThread extends Thread { |
方式2:实现Runnable接口
步骤:
- 自定义类MyRunnable实现Runnable接口
- 重写run()方法
- 创建MyRunnable类的对象
- 创建Thread类的对象,并把C步骤的对象作为构造参数传递
实现接口方式的好处:
- 可以避免由于Java单继承带来的局限性。
- 适合多个相同程序的代码去处理同一个资源的情况,把线程同程序的代码,数据有效分离,较好的体现了面向对象的设计思想。
代码:
1 | public class MyRunnable implements Runnable { |
线程名称
- 如何获取线程对象的名称呢?
- public final String getName() 获取线程的名称。
- 如何设置线程对象的名称呢?
- public final void setName(String name) 设置线程的名称
代码:
1 | public class MyThread extends Thread { |
方式二
1 | public class MyThread extends Thread { |
问题1:针对不是Thread类的子类中如何获取线程对象名称呢?
- public static Thread currentThread() 返回当前正在执行的线程对象
- Thread.currentThread().getName()
1 | System.out.println(Thread.currentThread().getName()); |
线程调度
假如我们的计算机只有一个 CPU,那么 CPU 在某一个时刻只能执行一条指令,线程只有得到 CPU时间片,也就是使用权,才可以执行指令。那么Java是如何对线程进行调用的呢?
线程有两种调度模型:
- 分时调度模型 所有线程轮流使用 CPU 的使用权,平均分配每个线程占用 CPU 的时间片
- 抢占式调度模型 优先让优先级高的线程使用 CPU,如果线程的优先级相同,那么会随机选择一个,优先级高的线程获取的 CPU 时间片相对多一些。
- Java使用的是抢占式调度模型。
如何设置和获取线程优先级?
线程没有设置优先级,肯定有默认优先级。线程默认优先级是5。
获取线程对象的优先级:
- public final int getPriority():返回线程对象的优先级
设置线程对象的优先级:
- public final void setPriority(int newPriority):更改线程的优先级。
注意:
线程优先级的范围是:1-10。
线程优先级高仅仅表示线程获取的 CPU时间片的几率高,但是要在次数比较多,或者多次运行的时候才能看到比较好的效果。因为,线程有随机性。
IllegalArgumentException 非法参数异常。
抛出的异常表明向方法传递了一个不合法或不正确的参数。
代码:
1 | public class ThreadPriorityDemo { |
线程控制-睡眠
public static void sleep(long millis)
1 | public class ThreadSleep extends Thread { |
线程控制-加入
public final void join():等待该线程终止。
1 | //ThreadJoin为普通继承Thread |
线程控制-礼让
public static void yield()
暂停当前正在执行的线程对象,并执行其他线程。 让多个线程的执行更和谐,但是不能靠它保证一人一次。(后面会完善用法)
1 | public class ThreadYield extends Thread { |
线程控制-后台线程
public final void setDaemon(boolean on)
将该线程标记为守护线程或用户线程。当正在运行的线程都是守护线程时,Java 虚拟机退出。 该方法必须在启动线程前调用。
1 | //ThreadDaemon为普通继承Thread |
线程控制-中断线程
public final void stop()
- 让线程停止,过时了,但是还可以使用。
public void interrupt()
- 中断线程,方法:把线程的状态终止,并抛出一个InterruptedException。
- 接着代码走,抛出的异常和剩下的代码
1 | public class ThreadStop extends Thread { |
线程的生命周期
- 新建:创建线程对象
- 就绪:有执行资格,没有执行权
- 运行:有执行资格,有执行权
- 阻塞:由于一些操作让线程处于该状态。没有执行状态,没有执行权,而另一些操作却可以把它给激活,激活后处于就绪状态
- 死亡:线程对象变成垃圾,等待被回收
3.线程练习
某电影院目前正在上映贺岁大片,共有100张票,而它有3个售票窗口售票,请设计一个程序模拟该电影院售票。
两种方式实现:
继承Thread类
1 | public class SellTicket extends Thread { |
会有错误,后面修订
实现Runnable接口
1 | public class SellTicket implements Runnable { |
相比较之下,Runnable接口的方式产生错误会比继承Thread少,但还是会有,后面修订
问题与解决
电影院售票程序,从表面上看不出什么问题,但是在真实生活中,售票时网络是不能实时传输的,总是存在延迟的情况,所以,在出售一张票以后,需要一点时间的延迟
接口方式的卖票程序:每次卖票延迟100毫秒
1 | public class SellTicket implements Runnable { |
通过加入延迟后,就产生了两个问题:
- A:相同的票卖了多次
- CPU的一次操作必须是原子性的
- B:出现了负数票
- 随机性和延迟导致的
线程安全问题在理想状态下,不容易出现,但一旦出现对软件的影响是非常大的
要想解决问题,就要知道哪些原因会导致出问题:(而且这些原因也是以后我们判断一个程序是否会有线程安全问题的标准)
- 是否是多线程环境
- 是否有共享数据
- 是否有多条语句操作共享数据
解决思想:
把多条语句操作共享数据的代码给包成一个整体,让某个线程在执行的时候,别人不能来执行。Java给我们提供了:同步机制。
同步代码块:
synchronized(对象){
需要同步的代码;
}
需要同步的代码是哪些呢?
把多条语句操作共享数据的代码的部分给包起来
注意:
- 同步可以解决安全问题的根本原因就在那个对象上。该对象如同锁的功能。
- 多个线程必须是同一把锁。
代码实现:
1 | public class SellTicket implements Runnable { |
4.同步
同步的特点:
- 前提:多个线程
- 解决问题的时候要注意:多个线程使用的是同一个锁对象
- 同步的好处 :同步的出现解决了多线程的安全问题。
- 同步的弊端: 当线程相当多时,因为每个线程都会去判断同步上的锁,这是很耗费资源的,无形中会降低程序的运行效率。
同步的各种问题
1 | private Object obj = new Object(); |
- 同步代码块的锁对象是:任意对象。
1 | private Demo d = new Demo(); |
同步方法的格式及锁对象:把同步关键字加在方法上。
同步方法是:this
1 |
|
- 静态方法及锁对象是:类的字节码文件对象。(反射)(要比静态先存在,class文件)
1 | private static int tickets = 100; |
如果锁对象是this,就可以考虑使用同步方法。否则能使用同步代码块的尽量使用同步代码块
5.以前的线程安全的类
线程安全:StringBuffer、Vector、Hashtable
如何把一个线程不安全的集合类变成一个线程安全的集合类?
用Collections工具类的方法即可。
1 | public class ThreadDemo { |