1.多线程
1.1概念
我们在之前,学习的程序在没有跳转语句的前提下,都是由上至下依次执行,那现在想要设计一个程序,边打游戏边听歌,怎么设计 ?
要解决上述问题,咱们得使用多进程或者多线程来解决.
其实通俗易懂的来说,多线程技术就是可以让一个程序同时做多件事,比如你打开音乐软件,它既可以播放音乐,又可以滚动歌词,又可以显示图片。这些功能都是同时进行的,而不需要像运行代码一样一行一行的排队进行。
1.2并发与并行
- 并发 : 指两个或多个事件在同一个时间段内发生。
- 并行: 指两个或多个事件在同一时刻发生( 同时发生 )。
在操作 系统中,安装了多个程序,并发指的是在一段时间内宏观上有多个程序同时运行,这在单 CPU 系统中,每一时刻只能有一道程序执行,即微观上这些程序是分时的交替运行,只不过是给人的感觉是同时运行,那是因为分时交替运行的时间是非常短的。
而在多个 CPU 系统中,则这些可以并发执行的程序便可以分配到多个处理器上(CPU),实现多任务并行执行即利用每个处理器来处理一个可以并发执行的程序,这样多个程序便可以同时执行。目前电脑市场上说的多核CPU,便是多核处理器,核 越多,并行处理的程序越多,能大大的提高电脑运行的效率。
注意:单核处理器的计算机肯定是不能并行的处理多个任务的,只能是多个任务在单个CPU上并发运行。同理,线程也是一样的,从宏观角度上理解线程是并行运行的,但是从微观角度上分析却是串行运行的,即一个线程一个线程的去运行,当系统只有一个CPU时,线程会以某种顺序执行多个线程,我们把这种情况称之为线程调度。
1.3线程与进程
- 进程:是指一个内存中运行的应用程序,每个进程都有一个独立的内存空间,一个应用程序可以同时运行多个进程,进程也是程序的一次执行过程,是系统运行程序的基本单位,系统运行一个程序即是一个进程从创建、运行到消亡的过程。
- 线程:线程是进程中的一个执行单元,负责当前进程中程序的执行,一个进程中至少有一个线程。一个进程中是可以有多个线程的,这个应用程序也可以称之为多线程程序。
简而言之:一个程序运行后至少有一个进程,一个进程中可以包含多个线程
我们可以再电脑底部任务栏,右键----->打开任务管理器,可以查看当前任务的进程:
进程
是指一个内存中运行的应用程序,每个进程都有一个独立的内存空间。
内存:所有的应用程序都需要进入到内存中执行 临时存储RAM
硬盘:永久存储ROM
- 进入到内存的程序叫进程
- 任务管理器-->结束进程
- 那么就把进程从内存中清除了
线程:
点击一个应用程序的功能执行,就会开启一条应用程序到cpu的执行路径,cup就可以通过这个路径执行功能,这个路径有一个名字,叫线程。
线程属于进程:是进程中的一个执行单元,负责程序的执行
线程的好处:
- 效率高
- 多线程之间互不影响
如:
单核心线程cpu
- cpu在多个线程之间做高速的切换
- 轮流执行多个线程
- 效率低
- 切换的速度块(1/n毫秒)
4核心8线程
- 有8个线程,可以同时执行8个线程
- 8个线程在多个任务之间做高速的切换
- 速度是单线程cpu的8倍(每个执行到的几率都被提高了8倍)
线程调度:
分时调度
- 所有线程轮流使用 CPU 的使用权,平均分配每个线程占用 CPU 的时间。
抢占式调度
- 优先让优先级高的线程使用 CPU,如果线程的优先级相同,那么会随机选择一个(线程随机性),Java使用的为抢占式调度。
(设置的优先级)
主线程
主线程:执行主(main)方法的线程
单线程程序:Java程序中只有一个线程
执行从main方法开始,从上到下依次执行
- JVM执行main方法,main方法会进入到栈内存
- JVM会找操作系统开辟一条main方法通向cpu的执行路径
- cpu就可以通过这个路径来执行main方法
- 而这个路径有一个名字,叫main(主)线程
1.4创建线程类
Java使用 java.lang.Thread 类代表线程,所有的线程对象都必须是Thread类或其子类的实例。每个线程的作用是完成一定的任务,实际上就是执行一段程序流即一段顺序执行的代码。Java使用线程执行体来代表这段程序流。Java中通过继承Thread类来创建并启动多线程的步骤如下:
Thread类
在上一天内容中我们已经可以完成最基本的线程开启,那么在我们完成操作过程中用到了 java.lang.Thread 类API中该类中定义了有关线程的一些方法,具体如下:
构造方法:
- public Thread():分配一个新的线程对象。
- public Thread(String name) :分配一个指定名字的新的线程对象.
- public Thread(Runnable target):分配一个带有指定目标新的线程对象。
- public Thread(Runnable target,string name):分配一个带有指定目标新的线程对象并指定名字。
常用方法:
- public string getName():获取当前线程名称。
- public void start():导致此线程开始执行;Java虚拟机调用此线程的run方法。
- public void run() :此线程要执行的任务在此处定义代码。
- public static void sleep(long millis) :使当前正在执行的线程以指定的毫秒数暂停 (暂时停止执行)。
- public static Thread currentThread() :返回对当前正在执行的线程对象的引用。
翻阅API后得知创建线程的方式总共有两种,一种是继承Thread类方式,一种是实现Runnable接口方式,方式一我们上一天已经完成,接下来进解方式二实现的方式
方法一:
- 定义Thread类的子类,并重写该类的run(方法,该run(方法的方法体就代表了线程需要完成的任务,因此把run(方法称为线程执行体。
- 创建Thread子类的实例,即创建了线程对象
- 调用线程对象的start()方法来启动该线程
实现Thread:
先写两个类来实现Threa接口!作为线程的运行对象!
第一个类:
package xiancheng;
public class MyThread1 extends Thread{
@Override
public void run(){
for (int i=0;i<100;i++){
System.out.println("执行打游戏..."+i);
}
}
}
第二个类:
package xiancheng;
public class MyThread2 extends Thread{
@Override
public void run(){
for (int i=0;i<100;i++){
System.out.println("执行听歌..."+i);
}
}
}
测试类:
package xiancheng;
/**
* 多线程的实现步骤:
* 1.定义类继承Thread类;
* 2.重写run 方法,run 方法中的代码就会按照多线程机制进行调用和执行;
* 3.在main方法中定义类的对象,调用start()方法启动线程,会自动调用run 方法;
*
* 这样就可以实现多线程程序《两个程序会交执行)
*
* 案例: 用多线程完成两个循环交替执行
*/
public class ThreadTest1 {
public static void main(String[] args) {
MyThread1 t1=new MyThread1();
MyThread2 t2=new MyThread2();
//
t1.start();
t2.start();
}
}
输出结果:
执行听歌...0
执行打游戏...0
执行听歌...1
执行打游戏...1
执行打游戏...2
执行听歌...2
执行听歌...3
执行听歌...4
执行打游戏...3
执行听歌...5
执行听歌...6
执行听歌...7
执行听歌...8
执行听歌...9
执行听歌...10
.....
执行听歌...99
方法二:
采用 java.lang.Runnable 也是非常常见的一种,我们只需要重写run方法即可。步骤如下:
实现runnable与继承Tread相比有如优势:
- 通过创建任务,然后给线程分配的方式来实现的多线程。更适合多个线程同时执行相同任务的情况
- 可以避免单继承带来的局限性。
- 任务与线程本身是分离的,提高了程序的健壮性。
- 线程池技术,接受Runnable类型的任务,不接受Thread类型的线程。
但Thread也有好处,可以用匿名内部类
实现Runnable:
先写两个类来实现Runnable接口!作为线程的运行对象!
第一个类:
package xiancheng;
public class MyRunnable1 implements Runnable{
@Override
public void run(){
for (int i=0;i<3;i++){
System.out.println("执行睡觉");
}
}
}
第二个类:
package xiancheng;
public class MyRunnable2 implements Runnable{
@Override
public void run(){
for (int i=0;i<3;i++){
System.out.println("执行吃饭");
}
}
}
测试类:
package xiancheng;
/**
* Runnable接口实现类实现多线程的步骤:
* 1.定义类实现Runnable接口
* 2.重写run方法;
* 3.在main方法中实例化Runnable接口的实现类对象:
* 4.定义两个线程Thread类的对象,把Runnable接口的实现对象传入构造方法中:
* 5.线程类对象调用start方法,启动线程,自动执行Runnable接口的实现类中的run 方法:
*/
public class ThreadTest2 {
public static void main(String[] args) {
MyRunnable1 r1=new MyRunnable1();
MyRunnable2 r2=new MyRunnable2();
//Runnable接口的实现实现多线程,必须借助Thredd类才能实现
Thread t1=new Thread(r1);
Thread t2=new Thread(r2);
t1.start();
t2.start();
}
}
输出结果:
执行睡觉
执行睡觉
执行睡觉
执行吃饭
执行吃饭
执行吃饭
实现Runnable接口创建多线程程序的好处:
1 避免了单继承的局限性
- 一个类只能继承一个类(一个人只能有一个亲爹),类继承了Thread类就不能继承其他的类
- 实现Runnable接口,还可以继承其他的类,实现其他的接口
2 增强了程序的扩展性,降低了程序的耦合性(解耦)
- 实现Runnable接口的方式,把设置线程任务和开启新线程进行了分离(解耦)
- 实现类中,重写了run方法:用来设置线程任务
- 创建Thread类对象,调用Thread类中的方法start方法,开启新的线程,执行run方法
匿名内部类方式实现线程的创建
- 匿名:没有名字
- 内部类:写在其他类内部的类
匿名内部类的作用:简化代码
- 把子类继承父类,重写父类的方法,创建子类对象合一步完成
- 把实现类实现类接口,重写接口中的方法,创建实现类对象合一步完成
格式:
new 父类/接口(){
重置父类/接口中的方法
};
//线程的父类是Thread
//new MyThread().start();
//线程的接口Runnable
//Runnable r = RunnableTmpl();//多态
//new Runnable(r).start();
1.5Thread类的常用方法
获取线程的名称:
- 使用Thread类中的方法getName() 返回该线程的名称。
- static Thread currentThread() 返回对当前正在执行的线程对象的引用。
设置线程的名称:
- 使用Thread类中的方法setName(名字)
- void setName(String name) 改变线程的名称,使之参数 name相同。
- public void start():使该线程开始执行;Java虚拟机调用该线程的run方法
- public void run():此线程要执行的任务在此处定义代码
- public static void sleep(long millis):使当前正在执行的线程以指定的毫秒数暂停(暂时停止执行)。
1.6线程的安全问题
模拟卖票案例
创建三个的线程,同时开启,对共享的票进行出售
public class RunnableImpl implementsc Runnable{
//定义一个多线程共享的票源
private int ticket = 100;
//设置线程任务:买票
@Override
public void run(){
//使用死循环,让卖票操作重复执行
while (true){
//先判断票是否存在
if(ticket>0){
//提高安全问题出现的概率,让程序睡眠
try{
Thread.sleep(10);
}catch (InterruptedException e){
e.printStackTrace();
}
//票存在,卖票 ticket--
System.out.println(Thread.currentThread().getName()+"-->正在卖第"+ticket+"张票");
ticket --;
}
}
}
}
public class CaiNiao{
public static void main(String[] args){
//创建Runnable接口的实现类对象
RunnableImpl run = new RunnableImpl();
//创建Thread类对象,构造方法中传递Runnable接口的实现类对象
Thread t0 = new Thread(run);
Thread t1 = new Thread(run);
Thread t2 = new Thread(run);
//调用start方法开启多线程
t0.start();
t1.start();
t2.start();
}
}
这样会导致一个结果
- Thread-0 -->正在卖第1张票
- Thread-1 -->正在卖第1张票
- Thread-2 -->正在卖第0张票
解决线程安全问题的一种方案:使用同步代码块
格式:
格式:
syncharonized(锁对象){
可能会出现线程安全问题的代码(访问了共享数据的代码)
}
注意:
- 通过代码块中的锁对象,可以使用任意的对象
- 但是必须要保证多个线程使用的锁对象是同一个
锁对象作用
- 把同步代码块锁住,只让一个线程在同步代码中执行
public class RunnableImpl implementsc Runnable{
//定义一个多线程共享的票源
private int ticket = 100;
//设置线程任务:买票
@Override
public void run(){
//使用死循环,让卖票操作重复执行
while (true){
//同步代码块
syncharonized(obj){
//先判断票是否存在
if(ticket>0){
//提高安全问题出现的概率,让程序睡眠
try{
Thread.sleep(10);
}catch (InterruptedException e){
e.printStackTrace();
}
//票存在,卖票 ticket--
System.out.println(Thread.currentThread().getName()+"-->正在卖第"+ticket+"张票");
ticket --;
}