多线程的概述
- 进程
- 正在运行的程序,是系统进行资源分配和调用的独立单位。
- 每一个进程都有它自己的内存空间和系统资源。
说起线程,它又分为单线程和多线程
- 线程
- 是进程中的单个顺序控制流,是一条执行路径
- 一个进程如果只有一条执行路径,则称为单线程程序
- 一个进程如果有多条执行路径,则称为多线程程序
多线程的实现(1)
如何实现多线程的程序呢?
由于线程是依赖进程而存在的,所以我们应该先创建一个进程出来。而进程是由系统创建的,所以我们应该去调用系统功能创建一个进程。Java是不能直接调用系统功能的,所以,我们没有办法直接实现多线程程序。但是呢?Java可以去调用C/C++写好的程序来实现多线程程序。由C/C++去调用系统功能创建进程,然后由Java去调用这样的东西,然后提供一些类供我们使用。我们就可以实现多线程程序了。
-
方式1:继承Thread类
- 步骤
A:自定义类MyThread继承Thread类。
B:MyThread类里面重写run()
C:创建对象
D:启动线程
- 步骤
下面我们就自定义一个MyThread类继承Thread类启动线程
public class MyThread extends Thread {
@Override
public void run() {
// 自己写代码
// 一般来说,被线程执行的代码肯定是比较耗时的。所以我们用循环改进
for (int x = 0; x < 100; x++) {
System.out.println(x);
}
}
}
public class MyThreadDemo {
public static void main(String[] args) {
// 创建两个线程对象
MyThread my1 = new MyThread();
MyThread my2 = new MyThread();
my1.start();
my2.start();
}
}
这样我们就创建并启动了两个线程
start()方法:首先启动了线程,然后再由jvm去调用该线程的run()方法。
那么,我们在继承Thread类之后,为什么要重写run()方法呢?
- 因为不是类中的所有代码都需要被线程执行的。而这个时候,为了区分哪些代码能够被线程执行,java提供了Thread类中的run()用来包含那些被线程执行的代码。
获取和设置线程名称
- Thread类的基本获取和设置方法
- public final String getName():获取线程的名称。
- public final void setName(String name):设置线程的名称
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 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("阿杜");
// MyThread my2 = new MyThread("杜鹏程");
// my1.start();
// my2.start();
//我们可以使用无参构造的方法,也可以使用带参构造的方法
}
}
但是我们要获取main方法所在的线程对象的名称,该怎么办呢?
遇到这种情况,Thread类提供了一个很好玩的方法:
public static Thread currentThread():返回当前正在执行的线程对象
System.out.println(Thread.currentThread().getName());
这句话如果在main中执行,就会输出main。会返回当前执行的线程对象
线程控制
- public static void sleep(long millis):线程休眠
- public final void join():线程加入
- public static void yield():线程礼让
- public final void setDaemon(boolean on):后台线程
- public final void stop():中断线程
- public void interrupt():中断线程
public static void sleep(long millis):线程休眠
public class ThreadSleep extends Thread {
@Override
public void run() {
for (int x = 0; x < 100; x++) {
System.out.println(getName() + ":" + x + ",日期:" + new Date());
// 睡眠1秒钟
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
public class ThreadSleepDemo {
public static void main(String[] args) {
ThreadSleep ts1 = new ThreadSleep();
ThreadSleep ts2 = new ThreadSleep();
ts1.setName("阿杜");
ts2.setName("杜鹏程");
ts1.start();
ts2.start();
}
}
public final void join():线程加入,等待该线程终止
public class ThreadJoin extends Thread {
@Override
public void run() {
for (int x = 0; x < 100; x++) {
System.out.println(getName() + ":" + x);
}
}
}
public class ThreadJoinDemo {
public static void main(String[] args) {
ThreadJoin tj1 = new ThreadJoin();
ThreadJoin tj2 = new ThreadJoin();
ThreadJoin tj3 = new ThreadJoin();
tj1.setName("中秋节");
tj2.setName("国庆节");
tj3.setName("圣诞节");
tj1.start();
try {
tj1.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
tj2.start();
tj3.start();
}
}
运行程序,我们发现名字为中秋节的线程走完了之后才开始走下面的两个线程。
给那个线程用这个方法就是等待该线程终止后,再继续执行接下来的线程。
public static void yield():线程礼让
public class ThreadYield extends Thread {
@Override
public void run() {
for (int x = 0; x < 100; x++) {
System.out.println(getName() + ":" + x);
Thread.yield();
}
}
}
public class ThreadYieldDemo {
public static void main(String[] args) {
ThreadYield ty1 = new ThreadYield();
ThreadYield ty2 = new ThreadYield();
ty1.setName("阿杜");
ty2.setName("杜鹏程");
ty1.start();
ty2.start();
}
}
这个方法暂停当前正在执行的线程对象,并执行其他线程。
让多个线程的执行更和谐,但是不能靠它保证一人一次。
public final void setDaemon(boolean on):守护线程
public class ThreadDaemon extends Thread {
@Override
public void run() {
for (int x = 0; x < 100; x++) {
System.out.println(getName() + ":" + x);
}
}
}
public class ThreadDaemonDemo {
public static void main(String[] args) {
ThreadDaemon td1 = new ThreadDaemon();
ThreadDaemon td2 = new ThreadDaemon();
td1.setName("关羽");
td2.setName("张飞");
// 设置守护线程
td1.setDaemon(true);
td2.setDaemon(true);
td1.start();
td2.start();
Thread.currentThread().setName("刘备");
for (int x = 0; x < 5; x++) {
System.out.println(Thread.currentThread().getName() + ":" + x);
}
}
}
运行程序可以看到,当刘备执行完5次后,张飞和关于也会执行完,并不会执行100次。
将该线程标记为守护线程或用户线程。
当正在运行的线程都是守护线程时,Java 虚拟机退出。
该方法必须在启动线程前调用。
**public final void stop():中断线程 **
public void interrupt():中断线程
这两个方法都是中断线程的意思,但是他们还是有区别的,我们来一起研究一下
public class ThreadStop extends Thread {
@Override
public void run() {
System.out.println("开始执行:" + new Date());
// 我要休息10秒钟,亲,不要打扰我哦
try {
Thread.sleep(10000);
} catch (InterruptedException e) {
// e.printStackTrace();
System.out.println("线程被终止了");
}
System.out.println("结束执行:" + new Date());
}
}
public class ThreadStopDemo {
public static void main(String[] args) {
ThreadStop ts = new ThreadStop();
ts.start();
// 你超过三秒不醒过来,我就干死你
try {
Thread.sleep(3000);
// ts.stop();
ts.interrupt();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
我们分别运行stop()方法和interrupt()方法。
我们可以发现stop()方法执行后,该线程就停止了,不再继续执行了
但是interrupt()方法执行后,它会终止线程的状态,还会继续执行run方法里面的代码。
线程的生命周期图
多线程的实现(2)
- 方式2:实现Runnable接口
- 步骤:
- A:自定义类MyRunnable实现Runnable接口
- B:重写run()方法
- C:创建MyRunnable类的对象
- D:创建Thread类的对象,并把C步骤的对象作为构造参数传递
- 步骤:
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, String name)
Thread t1 = new Thread(my, "阿杜");
Thread t2 = new Thread(my, "杜鹏程");
t1.start();
t2.start();
}
}
这样我们就实现了多线程的第二种启动方式
多线程程序练习
某电影院目前正在上映贺岁大片,共有100张票,而它有3个售票窗口售票,请设计一个程序模拟该电影院售票。
我们分别用两种实现多线程的方法来完成这个需求
1.继承Thread类来实现
public class SellTicket extends Thread {
// 定义100张票
private static int tickets = 100;
@Override
public void run() {
// 定义100张票
// 每个线程进来都会走这里,这样的话,每个线程对象相当于买的是自己的那100张票,这不合理,所以应该定义到外面
// int tickets = 100;
// 是为了模拟一直有票
while (true) {
if (tickets > 0) {
System.out.println(getName() + "正在出售第" +(tickets--) + "张票");
}
}
}
}
public class SellTicketDemo {
public static void main(String[] args) {
// 创建三个线程对象
SellTicket st1 = new SellTicket();
SellTicket st2 = new SellTicket();
SellTicket st3 = new SellTicket();
// 给线程对象起名字
st1.setName("窗口1");
st2.setName("窗口2");
st3.setName("窗口3");
// 启动线程
st1.start();
st2.start();
st3.start();
}
}
这样我们就实现了三个窗口同时在出售这100张票的多线程程序
2.实现Runnable接口的方式实现
public class SellTicket implements Runnable {
// 定义100张票
private int tickets = 100;
@Override
public void run() {
while (true) {
if (tickets > 0) {
System.out.println(Thread.currentThread().getName() + "正在出售第"+ (tickets--) + "张票");
}
}
}
}
public class SellTicketDemo {
public static void main(String[] args) {
// 创建资源对象
SellTicket st = new SellTicket();
// 创建三个线程对象
Thread t1 = new Thread(st, "窗口1");
Thread t2 = new Thread(st, "窗口2");
Thread t3 = new Thread(st, "窗口3");
// 启动线程
t1.start();
t2.start();
t3.start();
}
}
我们这个电影院售票程序,从表面上看不出什么问题,但是在真实生活中,售票时网络是不能实时传输的,总是存在延迟的情况,所以,在出售一张票以后,需要一点时间的延迟,所以我们每次卖票延迟100毫秒
while (true) {
if (tickets > 0) {
// 为了模拟更真实的场景,我们稍作休息
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "正在出售第"+ (tickets--) + "张票");
}
}
}
这样我们模拟真是场景,稍作休息了,可是运行程序后,还是会出现下面两个问题。
- 相同的票出现多次
- CPU的一次操作必须是原子性的 - 还出现了负数的票
- 随机性和延迟导致的
这里就牵扯到了线程的安全问题,线程安全问题在理想状态下,不容易出现,但一旦出现对软件的影响是非常大的。
多线程安全问题
如何解决多线程安全问题呢?
- 把多个语句操作共享数据的代码给锁起来,让任意时刻只能有一个线程执行即可。
解决线程安全问题实现(1)
- 同步代码块
- 格式:
- synchronized(对象){ 需要同步的代码; }
- 同步可以解决安全问题的根本原因就在那个对象上。该对象如同锁的功能。
- 格式:
我们多上面售票的代码进行改进
public class SellTicket implements Runnable {
// 定义100张票
private int tickets = 100;
//创建锁对象
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--) + "张票");
}
}
}
}
}
我们只要运用同步代码块的格式来解决线程的问题就可以,主要就是这里的对象,必须使用的是同一个锁对象。
所以我们可以来总结一下同步的特点
同步的特点
- 同步的前提
- 多个线程
- 多个线程使用的是同一个锁对象
- 同步的好处
- 同步的出现解决了多线程的安全问题。
- 同步的弊端
- 当线程相当多时,因为每个线程都会去判断同步上的锁,这是很耗费资源的,无形中会降低程序的运行效率。
解决线程安全问题实现(2)
我们 还有一种方法可以解决多线程的安全问题
同步方法:就是把同步的关键字加到方法上
private synchronized void sellTicket() {
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--) + "张票 ");
}
}
我们要来总结一下,同步代码块的锁对象可以时任意对象。
但是,当把同步关键字加在方法上,它的对象是this
当此方法为精态方法时,它的对象是类的字节码文件对象,也就是 类名.class