多线程
我们在之前,学习的程序在没有跳转语句的前提下,都是由上至下依次执行,那现在想要设计一个程序,边打游戏边听歌,怎么设计?
要解决上述问题,咱们得使用多进程或者多线程来解决.
并发与并行
- 并发:指两个或多个事件在同一个时间段内发生。
- 并行:指两个或多个事件在同一时刻发生(同时发生)。
在操作系统中,安装了多个程序,并发指的是在一段时间内宏观上有多个程序同时运行,这在单 CPU 系统中,每一时刻只能有一道程序执行,即微观上这些程序是分时的交替运行,只不过是给人的感觉是同时运行,那是因为分时交替运行的时间是非常短的。
而在多个 CPU 系统中,则这些可以并发执行的程序便可以分配到多个处理器上(CPU),实现多任务并行执行,即利用每个处理器来处理一个可以并发执行的程序,这样多个程序便可以同时执行。目前电脑市场上说的多核 CPU,便是多核处理器,核越多,并行处理的程序越多,能大大的提高电脑运行的效率。
注意:单核处理器的计算机肯定是不能并行的处理多个任务的,只能是多个任务在单个CPU上并发运行。同理,线程也是一样的,从宏观角度上理解线程是并行运行的,但是从微观角度上分析却是串行运行的,即一个线程一个线程的去运行,当系统只有一个CPU时,线程会以某种顺序执行多个线程,我们把这种情况称之为线程调度。
线程与进程
进程:是指一个内存中运行的应用程序,每个进程都有一个独立的内存空间,一个应用程序可以同时运行多个进程;进程也是程序的一次执行过程,是系统运行程序的基本单位;系统运行一个程序即是一个进程从创建、运行到消亡的过程。
线程:线程是进程中的一个执行单元,负责当前进程中程序的执行,一个进程中至少有一个线程。一个进程中是可以有多个线程的,这个应用程序也可以称之为多线程程序。
简而言之:一个程序运行后至少有一个进程,一个进程中可以包含多个线程
我们可以再电脑底部任务栏,右键——->打开任务管理器,可以查看当前任务的进程:
进程
线程
线程调度:
创建线程类
Java使用java.lang.Thread
类代表线程,所有的线程对象都必须是Thread类或其子类的实例。每个线程的作用是完成一定的任务,实际上就是执行一段程序流即一段顺序执行的代码。Java使用线程执行体来代表这段程序流。Java中通过继承Thread类来创建并启动多线程的步骤如下:
- 定义Thread类的子类,并重写该类的run()方法,该run()方法的方法体就代表了线程需要完成的任务,因此把run()方法称为线程执行体。
- 创建Thread子类的实例,即创建了线程对象
- 调用线程对象的start()方法来启动该线程
代码如下:
测试类:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27
| package com.tipdm.Demo03;
public class demo2 { public static void main(String[] args) { MyThread myThread = new MyThread();
myThread.start(); for (int i = 0; i < 20; i++) { System.out.println("main:" + i); } } }
|
自定义线程类:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| package com.tipdm.Demo03;
public class MyThread extends Thread{ public MyThread() { super(); }
public MyThread(String name) { super(name); }
@Override public void run() { for (int i = 0; i < 20; i++) { System.out.println("run:" + i); } } }
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| main:0 run:0 run:1 run:2 main:1 run:3 main:2 main:3 main:4 main:5 main:6 ... run:15 run:16 run:17 run:18 run:19
进程已结束,退出代码0
|
线程名称
获取线程名称
获取线程的名称:
使用Thread类中的方法getName()
String getName()
返回该线程的名称。
默认情况下线程的名称为Thread-0,Thread-1...Thread-N
可以先获利到当前正在执行的线程,使用线程中的方法getName()获取线程的名称
static Thread currentThread()
返回对当前正在执行的线程对象的引用
自定义线程类:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28
| package com.tipdm.Demo01;
public class MyThread extends Thread{
@Override public void run() {
System.out.println(Thread.currentThread().getName()); } }
|
主类:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| package com.tipdm.Demo01;
public class demo1 { public static void main(String[] args) { MyThread myThread = new MyThread(); myThread.start();
new MyThread().start(); new MyThread().start();
System.out.println(Thread.currentThread().getName()); } }
|
1 2 3 4 5 6
| Thread-1 Thread-2 main Thread-0
进程已结束,退出代码0
|
设置线程名称
设置线程名称:(了解)
- 使用Thread类中的方法setName(名字)
void setName(String name) 改变线程名称,使之与参数 name 相同。
- 创建一个带参数的构造方法,参数传递线程的名称;
调用父类的带参构造方法,让线程名称传递给父类,让父类(Thread)给子线程起一个名字
Thread(String name) 分配新的 Thread 对象
自定义线程类:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
| package com.tipdm.Demo02;
import java.util.TreeMap;
public class MyThread extends Thread{ public MyThread() { }
public MyThread(String name) { super(name); }
@Override public void run() { System.out.println(Thread.currentThread().getName()); } }
|
主类:
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| package com.tipdm.Demo02;
public class demo1 { public static void main(String[] args) { MyThread mt = new MyThread(); mt.setName("小强"); mt.start();
new MyThread("旺财").start(); } }
|
线程等待
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| package com.tipdm.Demo03;
public class demo1 { public static void main(String[] args) { for (int i = 0; i < 60; i++) { System.out.println(i); try { Thread.sleep(1000L); } catch (InterruptedException e) { e.printStackTrace(); } } } }
|
1 2 3 4 5 6 7 8
| 0 1 2 ... 58 59
进程已结束,退出代码0
|
创建多线程的第二种方式
创建多线程程序的第二种方式:实现Runnable接口
java.lang.Runnable
Runnable
接口应该由那些打算通过某一线程执行其实例的类来实现。类必须定义一个称为 run 的无参数方法。
java.lang.Thread
类的构造方法
1 2
| Thread(Runnable target) 分配新的 Thread 对象。 Thread(Runnable target, String name) 分配新的 Thread 对象。
|
实现步骤:
创建一个Runnable接口的实现类
在实现类中重写Runnable接口的run方法,设置线程任务
创建一个Runnable接口的实现类对象
创建Thread类对象,构造方法中传递Runnable接口的实现类对象
调用Thread类中的start方法,开启新的线程执行run方法
实现Runnable接口创建多线程程序的好处:
- 避免了单继承的局限性
一个类只能继承一个类(一个人只能由一个亲爹),类继承了Thread类就不能继承其他的类
实现了Runnable接口,还可以继承其他的类,实现其他的接口
- 增强了程序的扩展性,降低了程序的耦合性(解耦)
实现了Runnable接口的方式,把设置线程任务和开启新线程任务进行了分离(解耦)
实现类中,重写了run方法:用来设置线程任务
创建Thread类对象,调用start方法:用来开启新线程。
自定义线程类1:
1 2 3 4 5 6 7 8 9 10 11
| package com.tipdm.Demo04;
public class RunnableImpl implements Runnable{ @Override public void run() { for (int i = 0; i < 20; i++) { System.out.println(Thread.currentThread().getName() + i); } } }
|
自定义线程类2:
1 2 3 4 5 6 7 8 9 10 11
| package com.tipdm.Demo04;
public class RunnableImpl2 implements Runnable{ @Override public void run() { for (int i = 0; i < 20; i++) { System.out.println("helloWorld!" + i); } } }
|
主类:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| package com.tipdm.Demo04;
public class demo1 { public static void main(String[] args) { RunnableImpl run = new RunnableImpl();
Thread t2 = new Thread(new RunnableImpl2()); t2.start(); for (int i = 0; i < 20; i++) { System.out.println(Thread.currentThread().getName() + i); } } }
|
1 2 3 4 5 6 7 8 9 10 11 12
| main0 main1 helloWorld!0 helloWorld!1 helloWorld!2 ... main16 main17 main18 main19
进程已结束,退出代码0
|
匿名内部类方式实现线程的创建
匿名内部类方式实现线程的创建
匿名内部类作用:简化代码
把子类继承父类,重写父类的方法,创建子类对象合一步完成。
把实现类实现类接口,重写接口中的方法,创建实现类对象合成一步完成
匿名内部类的最终产物:子类/实现类对象,而这个类没有名字
格式:
1 2 3
| new 父类/接口(){ 重写父类/接口中的方法 }
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38
| package com.tipdm.Demo05;
public class demo1 { public static void main(String[] args) { new Thread(){ @Override public void run() { for (int i = 0; i < 20; i++) { System.out.println(Thread.currentThread().getName() + "-->" + i); } } }.start();
Runnable r = new Runnable(){ @Override public void run() { for (int i = 0; i < 20; i++) { System.out.println(Thread.currentThread().getName() + "-->" + i); } } }; new Thread(r).start();
new Thread(new Runnable() { @Override public void run() { for (int i = 0; i < 20; i++) { System.out.println(Thread.currentThread().getName() + "-->" + i); } } }).start(); } }
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| Thread-0-->0 Thread-0-->1 Thread-0-->2 Thread-0-->3 ... Thread-0-->15 Thread-0-->16 Thread-0-->17 Thread-1-->4 ... Thread-2-->16 Thread-2-->17 Thread-2-->18 Thread-2-->19
|
线程安全
以电影院卖票为例:
线程安全导致的结果:可能会出现同一张电影票被卖给了不同的用户。
实现卖票案例
线程实现类:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30
| package com.tipdm.Demo06;
public class RunnableImpl implements Runnable{ private int ticket = 100;
@Override public void run() { while(true){ if(ticket > 0){ System.out.println(Thread.currentThread().getName() + "--->" + "正在卖第" + ticket + "张票"); ticket--; }else{ break; } try { Thread.sleep(10); } catch (InterruptedException e) { e.printStackTrace(); } } } }
|
主类:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| package com.tipdm.Demo06;
public class demo1Ticket { public static void main(String[] args) { RunnableImpl run = new RunnableImpl(); Thread t0 = new Thread(run); Thread t1 = new Thread(run); Thread t2 = new Thread(run); t0.start(); t1.start(); t2.start();
} }
|
1 2 3 4 5 6
| ... Thread-0--->正在卖第1张票 Thread-1--->正在卖第1张票 Thread-2--->正在卖第1张票
进程已结束,退出代码0
|
解决线程安全问题——使用同步代码块
卖票案例出现了线程安全问题
卖出了不存在的票和重复的票
解决线程安全问题的一种方案:使用同步代码块
格式:
1 2 3
| synchronized(锁对象){ 可能会出现线程安全问题的代码(访问了共享数据的代码) }
|
注意:
通过代码块中的锁对象,可以使用任意的对象
但是必须保证多个线程使用的锁对象是同一个
锁对象作用:
把同步代码块锁住,只让一个线程在同步代码块中执行。
线程实现类:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33
| package com.tipdm.Demo07;
public class RunnableImpl implements Runnable{ private int ticket = 100;
Object obj = new Object();
@Override public void run() { while(true){ synchronized (obj){ if(ticket > 0){ System.out.println(Thread.currentThread().getName() + "--->" + "正在卖第" + ticket + "张票"); try { Thread.sleep(10); } catch (InterruptedException e) { e.printStackTrace(); } ticket--; }else{ break; } } } } }
|
主类:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| package com.tipdm.Demo07;
public class demo1Ticket { public static void main(String[] args) { RunnableImpl run = new RunnableImpl(); Thread t0 = new Thread(run); Thread t1 = new Thread(run); Thread t2 = new Thread(run); t0.start(); t1.start(); t2.start(); } }
|
1 2 3 4 5 6 7 8 9 10
| ... Thread-2--->正在卖第19张票 Thread-2--->正在卖第18张票 Thread-2--->正在卖第17张票 Thread-1--->正在卖第16张票 Thread-1--->正在卖第15张票 Thread-1--->正在卖第14张票 Thread-1--->正在卖第13张票 ... 进程已结束,退出代码0
|
解决线程安全问题——使用同步方法
解决线程安全问题的第二种方案:使用同步方法
使用步骤:
把访问了共享数据的代码抽取出来,放到一个方法种
在方法上添加synchronized修饰符
格式:定义方法的格式
1 2 3
| 修饰符 synchronized 返回值类型 方法名(参数列表){ 可能会出现线程安全问题的代码(访问了共享数据的代码) }
|
线程实现类:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40
| package com.tipdm.Demo08;
public class RunnableImpl implements Runnable{ private int ticket = 100;
@Override public void run() { System.out.println("this:" + this); while(ticket>0){ payTicket(); } }
public void payTicket(){ synchronized (this){ if(ticket > 0){ System.out.println(Thread.currentThread().getName() + "--->" + "正在卖第" + ticket + "张票"); try { Thread.sleep(10); } catch (InterruptedException e) { e.printStackTrace(); } ticket--; } } } }
|
主类:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| package com.tipdm.Demo08;
public class demo1Ticket { public static void main(String[] args) { RunnableImpl run = new RunnableImpl(); System.out.println("run:" + run); Thread t0 = new Thread(run); Thread t1 = new Thread(run); Thread t2 = new Thread(run); t0.start(); t1.start(); t2.start(); } }
|
1 2 3 4 5 6 7 8 9 10
| ... Thread-0--->正在卖第71张票 Thread-2--->正在卖第70张票 Thread-2--->正在卖第69张票 Thread-2--->正在卖第68张票 Thread-1--->正在卖第67张票 Thread-1--->正在卖第66张票 ...
进程已结束,退出代码0
|
解决线程安全问题——使用静态同步方法
解决线程安全问题的第二种方案:使用静态同步方法
使用步骤:
把访问了共享数据的代码抽取出来,放到一个方法中
在方法上添加synchronized修饰符
格式:定义方法的格式
1 2 3
| 修饰符 synchronized 返回值类型 方法名(参数列表){ 可能会出现线程安全问题的代码(访问了共享数据的代码) }
|
线程实现类:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39
| package com.tipdm.Demo09;
public class RunnableImpl implements Runnable{ private static int ticket = 100;
@Override public void run() { System.out.println("this:" + this); while(ticket>0){ payTicketStatic(); } }
public static void payTicketStatic(){ synchronized (RunnableImpl.class) { if (ticket > 0) { System.out.println(Thread.currentThread().getName() + "--->" + "正在卖第" + ticket + "张票"); try { Thread.sleep(10); } catch (InterruptedException e) { e.printStackTrace(); } ticket--; } } } }
|
主类:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| package com.tipdm.Demo09;
public class demo1Ticket { public static void main(String[] args) { RunnableImpl run = new RunnableImpl(); System.out.println("run:" + run); Thread t0 = new Thread(run); Thread t1 = new Thread(run); Thread t2 = new Thread(run); t0.start(); t1.start(); t2.start(); } }
|
1 2 3 4 5 6 7 8 9
| ... Thread-2--->正在卖第20张票 Thread-2--->正在卖第19张票 Thread-2--->正在卖第18张票 Thread-0--->正在卖第17张票 Thread-0--->正在卖第16张票 ...
进程已结束,退出代码0
|
解决线程安全问题——使用Lock锁
解决线程安全问题的第三种方案:使用Lock锁
java.util.concurrent.locks.Lock
接口
Lock接口种的方法:
void Lock() 获取锁
void unlock() 释放锁
java.util.concurrent.locks.ReentrantLock implements Lock
接口
使用步骤:
在成员位置创建一个ReentrantLock对象
在可能会出现安全问题的代码前调用Lock接口中的方法lock获取锁
在可能会出现安全问题的代码后调用Lock接口中的方法unlock释放锁
线程实现类:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50
| package com.tipdm.Demo10;
import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock;
public class RunnableImpl implements Runnable{ private int ticket = 100;
Lock l = new ReentrantLock();
@Override public void run() { while(ticket > 0){ l.lock(); try { System.out.println(Thread.currentThread().getName() + "--->" + "正在卖第" + ticket + "张票"); ticket--; Thread.sleep(10); } catch (InterruptedException e) { e.printStackTrace(); }finally { l.unlock(); } } } }
|
主类:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| package com.tipdm.Demo10;
public class demo1Ticket { public static void main(String[] args) { RunnableImpl run = new RunnableImpl(); Thread t0 = new Thread(run); Thread t1 = new Thread(run); Thread t2 = new Thread(run); t0.start(); t1.start(); t2.start(); } }
|
1 2 3 4
| Thread-0--->正在卖第2张票 Thread-0--->正在卖第1张票
进程已结束,退出代码0
|
线程状态
当线程被创建并启动后,它既不是一启动就进入了执行状态。在线程的生命周期中,有几种状态呢?在API中java.lang.Thread.State
这个枚举中给出了六个线程状态。
Timed Waiting(计时等待)
Timed Waiting在API中的描述为:一个正在限时等待另一个线程执行一个(唤醒)动作的线程处于这一状态。单独的去理解这句话,真是玄之又玄,其实我们在之前的操作中已经接触过这个状态了,在哪里呢?
在我们写卖票的案例中,为了减少线程执行太快,现象不明显等问题,我们在run方法中添加了sleep语句,这样就强制当前正在执行的线程休眠(暂停执行),以“减慢线程”。
其实当我们调用了sleep方法之后,当前执行的线程就进入到“休眠状态”,其实就是所谓的Timed Waiting(计时等待),那么我们通过一个案例加深对该状态的一个理解。
1 2 3 4 5
| try{ Thread.sleep(5000); }catch(Eception e){ e.printStackTrace(); }
|
Blocked(锁阻塞)
Blocked 状态在API中的介绍为:一个正在阻塞等待一个监视器锁(锁对象)的线程处于这一状态。
我们已经学完同步机制,那么这个状态是非常好理解的了。比如,线程A与线程B代码中使用同一锁,如果线程A获取到锁,线程A进入到Runnable状态,那么线程B就进入到Blocked锁阻塞状态。
Waiting(无限等待)
Wating状态在API中介绍为:一个正在无限期等待另一个线程执行一个特别的(唤醒)动作的线程处于这一状态。
等待唤醒案例:线程之间的通信
创建一个顾客线程(消费者):告知老板要的包子的种类和数量,调用wait方法,放弃cpu的执行,进入到WAITING状态(无线等待)
创建一个老板线程(生产者):花了5秒做包子,做好包子之后,调用notify方法,唤醒顾客吃包子。
注意:
- 顾客和老板线程必须使用同步代码包裹起来,保证等待和唤醒只能有一个在执行
- 同步使用的锁对象必须保证唯一
- 只有锁对象才能调用wait和notify方法
Object类中的方法
- void wait() 在其他线程调用此对象的notify()方法或 notifyAll()方法前,导致当前线程等待。
- void notify() 唤醒在此对象监视器上等待的单个线程。 会继续执行wait方法之后的代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50
| package com.tipdm.Demo11;
public class demo1 { public static void main(String[] args) { Object obj = new Object(); new Thread(){ @Override public void run() { while(true){ System.out.println("告知老板要的包子的种类和数量");
synchronized (obj){ try { obj.wait(); } catch (InterruptedException e) { e.printStackTrace(); } } System.out.println("包子真香!!!!!!!!!!"); System.out.println("------------------"); } } }.start();
new Thread(){ @Override public void run() { while(true){ try { Thread.sleep(5000); System.out.println("包子做好了!"); } catch (InterruptedException e) { e.printStackTrace(); } synchronized (obj){ obj.notify(); } } } }.start(); } }
|
进入到TimeWaiting(计时等待)有两种方式
使用sleep(long m)方法,在毫秒值结束之后,线程睡醒进入到Runnable/Blocked状态
使用wait(long m)方法,wait方法如果在毫秒值结束之后,还没有被notify唤醒,就会自动醒来,线程睡醒进入到Runnable/Blocked状态
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30
| package com.tipdm.Demo11;
public class demo2 { public static void main(String[] args) { Object obj = new Object(); new Thread(){ @Override public void run() { while(true){ System.out.println("告知老板要的包子的种类和数量");
synchronized (obj){ try { obj.wait(5000); } catch (InterruptedException e) { e.printStackTrace(); } } System.out.println("包子真香!!!!!!!!!!"); System.out.println("------------------"); } } }.start(); } }
|
void notifyAll() 唤醒在此对象监视器上等待的所有线程。 会继续执行wait方法之后的代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74
| package com.tipdm.Demo11;
public class demo3 { public static void main(String[] args) { Object obj = new Object(); new Thread(){ @Override public void run() { while(true){ System.out.println("顾客1,告知老板要的包子的种类和数量");
synchronized (obj){ try { obj.wait(5000); } catch (InterruptedException e) { e.printStackTrace(); } } System.out.println("顾客1,包子真香!!!!!!!!!!"); } } }.start();
new Thread(){ @Override public void run() { while(true){ System.out.println("顾客2,告知老板要的包子的种类和数量");
synchronized (obj){ try { obj.wait(5000); } catch (InterruptedException e) { e.printStackTrace(); } } System.out.println("顾客2,包子真香!!!!!!!!!!"); } } }.start();
new Thread(){ @Override public void run() { while(true){ try { Thread.sleep(5000); System.out.println("包子做好了!"); } catch (InterruptedException e) { e.printStackTrace(); } synchronized (obj){ obj.notifyAll(); } } } }.start(); } }
|
1 2 3 4 5 6 7 8 9 10
| 告知老板要的包子的种类和数量 包子做好了! 包子真香!!!!!!!!!! ------------------ 告知老板要的包子的种类和数量 包子做好了! 包子真香!!!!!!!!!! ------------------ 告知老板要的包子的种类和数量 包子做好了!
|