10.多线程
1.什么是多线程?
进程:在操作系统中运行的某个软件(主要是指在内存中)。任何软件要运行都要被加载到内存中。而内存负责运行这个软件所需要的那些内存空间,就被称为当前软件在内存中的一个进程。
1.进程需要依赖于操作系统
2.运行中的程序
进程就是在操作系统中正在运行的程序。
2.什么是线程?
线程:软件运行之后真正负责执行软件中具体某个功能的那个独立的内存空间(它必须位于进程中)。
1.线程需要依赖于进程。
2.线程实际上是进程中某一项功能的执行过程【执行轨迹/执行线索】
3.线程可能会在进程中有多个,至少必须有一个。
线程就是在进程某一项功能的执行过程【执行轨迹/执行线索】。
例如:
我们在window操作系统上打开“暴风影音”播放电影,此时“暴风影音”就会在window操作系统中产生一个进程;打开“暴风影音”播放电影的时候有画面,声音,中文字幕等等,这些画面,声音,中文字幕就是这个“暴风影音”进程中的多个线程。
1.没有进程就没有线程。【线程需要依赖与进程】
2.线程可能会在进程中有多个,至少必须有一个。
多线程:
某一个程序在运行的时候【进程】可能会产生多个不同的执行线索【执行轨迹】【线程】,这些多个不同的执行线索【执行轨迹】共同运行的情况就是多线程。往往我们会感觉到这些多个不同的执行线索【执行轨迹】同时执行,实际上这时一种错觉假象,实际上当这些多个不同的执行线索【执行轨迹】在运行的时候,某一个时刻只用一个执行线索【执行轨迹】在运行,只是这多个不同的执行线索【执行轨迹】快速的切换而已。
“暴风影音”播放电影的时候,我们感觉图像和声音在同时运行,实际上你被骗了,因为程序在执行的时候,某一个时刻只能有一条线程运行,那么为啥你感觉图像和声音在同时运行,因为进程控制线程快速的切换导致,我们感觉图像和声音在同时运行。
例如:《超体》
注意:
CPU在某个时间点上其实它只能执行一个线程。但是由于CPU在执行线程的过程中会随时切换到其他的线程上。
这样导致我们看到的效果类似于多个线程在同时运行。CPU随机在多个线程之间进行切换。
上面介绍的这种CPU的执行原理其实是针对的单核单线程的CPU。【单行道】
现在的CPU基本都是双核四线程,四核(四线程)八线程。【双向8车道】
由于CPU在多个线程之间会进行高速的切换。导致我们误认为多个线程同时运行。
2.多线程的创建方式以及区别?
为什么使用多线程
1、使用多线程的目的就是为了提高程序的执行效率。
2、解决并发问题。
并行和并发有什么区别?
并行:多个处理器或多核处理器同时处理多个任务。
并发:多个任务在同一个 CPU 核上,按细分的时间片轮流(交替)执行,从逻辑上来看那些任务是同时执行。
如下图:【并发 = 两个队列和一台咖啡机】 【并行 = 两个队列和两台咖啡机】
5.Java中的线程
1.一个java程序启动运行以后,至少有2个线程要运行。
1.主线程—main线程【主方法中java代码的执行过程】
public static void main(String args[]){
Java 代码
}
2.垃圾回收线程。【垃圾自动回收机制】
2.线程操作中常用的类和接口
Thread类
java.lang 类 Thread【程序中的执行线程】【不需要导包】
public class Thread extends Object implements Runnable
如果我们要想在JAVA中使用线程,就必须通过Thread类完成。
如何通过Thread类创建一个线程
1.新建一个java类,继承Thread类。
2.重写run方法
3.将需要由线程执行动作,写入run方法
package com.click369.test1;
public class MyThread extends Thread{
@Override
public void run() {
for(int i=1;i<=100;i++){
System.out.println("MyThread=="+i);
}
}
}
如何运行通过Thread类创建的线程?
1.创建线程对象
2.通过线程对象调用继承自Thread类的start方法启动线程运行。
package com.click369.test1;
public class ThreadTest {
public static void main(String[] args) {
//1.创建线程对象
MyThread th1=new MyThread();
MyThread th2=new MyThread();
//直接调用run方法执行不是启动线程运行的方式。
//直接调用run方法执行实际上是对象调用方法运行的过程。
//th1.run();
//th2.run();
//2.通过线程对象调用继承自Thread类的start方法启动线程运行。
//启动线程运行是需要使用Thread类的start方法。
//Thread类的start方法会触发run方法运行。
th1.start();
th2.start();
}
}
Runnable接口
java.lang 接口 Runnable
public interface Runnable
Runnable 接口应该由那些打算通过某一线程执行其实例的类来实现。
方法摘要
void run() 使用实现接口 Runnable 的对象创建一个线程时,启动该线程将导致在独立执行的线程中调用对象的 run 方法。
Thread类与Runnabl接口的关系
Thread类实现过Runnabl接口,Thread类是Runnabl接口的子类。
理解Thread类中的run方法,实际上是来自Runnabl接口
如何通过Runnable 接口创建一个线程?
1.新建java类,实现Runnable 接口。
2.重写run方法
3.将需要由线程执行动作,写入run方法
package com.click369.test1;
public class TestRun implements Runnable{
@Override
public void run() {
for(int i=1;i<=100;i++){
System.out.println("TestRun=="+i);
}
}
}
如何运行通过Runnable 接口创建的线程?
由于启动线程需要使用Thread类中的strat方法,可是我们现在通过实现Runnable 接口创建的线程是没有start方法,我们就需要将通过实现Runnable 接口创建的线程与Thread类产生关联才能使用Thread类中的strat方法去启动线程运行。
完成实现Runnable 接口创建的线程与Thread类产生关联的就是Thread类的构造方法:
构造方法摘要
Thread() 分配新的 Thread 对象。
Thread(Runnable target) 分配新的 Thread 对象。
Thread(Runnable target, String name) 分配新的 Thread 对象。
Thread(String name) 分配新的 Thread 对象。
我们发现
Thread(Runnable target)/Thread(Runnable target, String name) Thread类的构造方法与Runnable 接口有关联。
启动线程需要使用Thread类中的strat方法,所以我们要创建Thread类对象,创建Thread类需要构造方法
Thread(Runnable target)/Thread(Runnable target, String name) 的第一参数是Runnable接口类型,这时当一个
方法的参数是接口类型的时候,我们可以传递实现该接口的子类对象,而上面的TestRun 类刚好实现过Runnable接口,这时我们可以将TestRun 类的对象传递给Thread类的构造方法,就达成了实现Runnable 接口创建的线程与Thread类产生关联的这个目的。
步骤:
1.创建实现Runnable 接口创建的线程类的对象【目标对象】
2.创建Thread类的对象【线程对象】,将目标对象传递给Thread类的构造方法
3.通过线程对象调用start方法启动线程运行。
例如:
package com.click369.test1;
public class TestMain {
public static void main(String[] args) {
//1.创建实现Runnable 接口创建的线程类的对象【目标对象】
TestRun tr=new TestRun();
//2.创建Thread类的对象【线程对象】,将目标对象传递给Thread类的构造方法
Thread th1=new Thread(tr);
Thread th2=new Thread(tr);
//3.通过线程对象调用start方法启动线程运行。
th1.start();
th2.start();
}
}
上面我们介绍了两种常用的创建线程的方式,那么这个两种创建线程的方式构造起来的线程到底有什么区别?
通过继承Thread类创建的线程类 与 通过Runnable 接口创建的线程类的区别?
1.创建方式:extends Thread,impements Runnable ;
2.启动方式不同
extends Thread,直接线程对象【继承Thread类的子类对象】调用strat方法启动运行
impements Runnable,先创建目标对象【实现Runnable接口的子类】,创建线程对象【Thread类的对象{Thread(Runnable target)/Thread(Runnable target, String name}】,将目标对象作为参数传递给创建线程对象的Thread类的构造方法,然后通过线程对象的strat方法启动运行。
3.extends Thread创建的线程是不能实现数据共享功能的,impements Runnable创建的的线程是可以实现数据共享功能的。【了解,后面【实现线程同步的时候】我们会通过实例来测试这个区别】
3.线程的常用方法?
Java中线程的常用方法—Thread类的常用方法
void start() 使该线程开始执行;Java 虚拟机调用该线程的 run 方法。
static Thread currentThread()返回对当前正在执行的线程对象的引用。
String getName()返回该线程的名称。
void setName(String name)改变线程名称,使之与参数 name 相同。
线程名名称,java程序运行以后会给程序中的每一个线程一个名称,默认的主线程的名称是”main”.,非主线程的其他线程名称的默认名称都是“Thread-0”“Thread-1”,“Thread-2”,以此类推。
package com.click369.test1;
public class MyThread implements Runnable {
@Override
public void run() {
for(int i=1;i<=100;i++){
//得到当前正在运行的线程名称
String name=Thread.currentThread().getName();
System.out.println(name+" i="+i);
}
}
}
MyThread my=new MyThread();
Thread th1=new Thread(my); //Thread-0
Thread th2=new Thread(my); //Thread-1
th1.setName(“线程1”);
th2.setName(“线程2”);
th1.start();
th2.start();
得到/设置线程的优先级
int getPriority() 返回线程的优先级。
void setPriority(int newPriority) 更改线程的优先级。
线程的优先级:就是线程的执行先后。
因为java中的线程的调用执行是jvm中的线程调度器负责管理线程,在java中线程的执行先后,我们是控制不了的。
我们无法控制线程的执行次序,但是我们可以通过设置线程的优先级,控制线程优先执行的几率。
优先级数字越大优先级越高,被优先执行的几率增大,并不是说谁的优先级大谁就一定先执行。
我们将线程的优先级分为10个级别【1–10】,数字越大优先级越高。
Java还为我们提供了3个静态常量,用于保存
static int MAX_PRIORITY 线程可以具有的最高优先级。10
static int MIN_PRIORITY线程可以具有的最低优先级。1
static int NORM_PRIORITY分配给线程的默认优先级。5
默认java为每一个线程设置的优先级都是一样的级别为5【NORM_PRIORITY】
我们可以通过setPriority来修改线程的优先级,数字越大优先级越高,被优先执行的几率增大,并不是说谁的优先级谁就一定先执行。
注意:绝对不能用修改线程的优先级,来确保某一个线程就要先执行
MyThread my=new MyThread();
Thread th1=new Thread(my); //Thread-0
Thread th2=new Thread(my); //Thread-1
th1.setName("线程1");
th2.setName("线程2");
th1.setPriority(1);
th2.setPriority(10);
int pri1=th1.getPriority();
int pri2=th2.getPriority();
System.out.println("第一个线程的优先级是=="+pri1);
System.out.println("第二个线程的优先级是=="+pri2);
th1.start();
th2.start();
守护线程
boolean isDaemon() 测试该线程是否为守护线程。
void setDaemon(boolean on) 将该线程标记为守护线程用户线程。
守护线程:我们也叫精灵线程,普通的线程又叫用户线程。
当所有用户线程都执行完毕以后,无论守护线程能否继续运行,都要立刻停止运行。【共死】
package com.click369.test2;
public class MyThread implements Runnable {
@Override
public void run() {
for(int i=1;i<=100;i++){
//得到当前正在运行的线程名称
String name=Thread.currentThread().getName();
System.out.println(name+" i="+i);
}
}
}
package com.click369.test2;
public class MyThread2 implements Runnable {
@Override
public void run() {
while(true){
System.out.println("守护线程!!!!");
}
}
}
package com.click369.test2;
public class ThreadTest {
public static void main(String[] args) {
MyThread my=new MyThread();
Thread th1=new Thread(my);
Thread th2=new Thread(my);
MyThread2 my2=new MyThread2();
Thread th3=new Thread(my2);
//void setDaemon(boolean on) 将该线程标记为守护线程用户线程。
th3.setDaemon(true);
// boolean isDaemon() 测试该线程是否为守护线程。
//System.out.println("th1--守护线程?--"+th1.isDaemon());
//System.out.println("th2--守护线程?--"+th2.isDaemon());
//System.out.println("th3--守护线程?--"+th3.isDaemon());
th1.start();
th2.start();
th3.start();
}
}
4.线程的生命周期?
Java中线程的生命周期
1、线程的生命周期就是线程从一开始创建,到run方法执行完毕以后的状态变化。[状态之间的切换]
2、线程的生命周期几种状态【1、新建状态 2、就绪状态 3、运行状态 4.阻塞状态 5.死亡状态】
创建状态:通过new的方式创建出线程对象,此时线程就进入到创建状态【新建状态】。
新建状态的线程是不能运行。
就绪状态:新建状态的线程调用strat方法之后就会进入就绪状态。
就绪状态的线程具备执行能力,但是没有cpu资源。【万事具备只差cpu】.
运行状态:就绪状态的线程得到cpu资源开始执行run方法,此时这个线程就是运行状态。
运行状态的线程当cpu切换到其他线程时候,本线程就再一次进入就绪状态。
阻塞状态:运行状态的线程调用sleep/wait方法…此时线程进入阻塞状态。
处于阻塞状态的线程的休眠时间到/调用notify方法/notifyAll方法在此时线程进入就绪状态,从就绪状态中得到cpu资源从而进入运行状态。
死亡状态:运行状态的线程run方法运行结束/线程执行过程中遇到异常/调用stop方法此时线程就进入到死亡状态。
死亡状态的线程是不能运行,除非再一次使用strat方法重新启动运行。
5.为什么需要线程同步/线程安全?什么是线程同步/线程安全?线程同步/线程安全实现方式有几种,它们有什么区别?
Java中的线程安全【线程同步】
作业:请用买票的过程演示线程同步操作。
1.通过while循环控制买票的持续性
2.判断是否有票
3.线程休眠模拟出收钱,打票,找钱的过程
4.卖出第几张票
5.票数减1
package com.click369.test1;
/**
* 实现买票程序的线程类
* @author Administrator
*
*/
public class MyThread implements Runnable{
//定义一个变量,来保存票数
//假设我们现在有5张票可以卖出
private int piao=5;
@Override
public void run() {
//1.通过while循环控制买票的持续性
//定义一个变量,来控制while循环
boolean flag=true;
while(flag){
//2.判断是否有票
//如果票数大于0,就表是有票,可以卖出
if(piao>0){
//3.线程休眠模拟出收钱,打票,找钱的过程
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
//得到当钱包线程的名称
String name=Thread.currentThread().getName();
//4.卖出第几张票
//5.票数减1
System.out.println(name+",卖出第1张票,还剩"+(--piao)+"张票");
}else{
//如果票数小于/等于0,就是已经无票可卖
flag=false;
}
}
}
}
package com.click369.test1;
public class TestMain {
public static void main(String[] args) {
//创建目标对象
MyThread my=new MyThread();
//创建线程对象【实际上就是买票的窗口】
//开3条线程卖出5张票
Thread th1=new Thread(my);
Thread th2=new Thread(my);
Thread th3=new Thread(my);
//为线程线程设置名称
th1.setName("窗口1");
th2.setName("窗口2");
th3.setName("窗口3");
//启动线程开始买票
th1.start();
th2.start();
th3.start();
}
}
上面的程序在买票的时候,卖还剩-1张票的结果,显然这个结果是不符合逻辑。
当窗口3卖出一张票以后,还剩1张票的时候,此时线程切换给了窗口1,窗口1线程就先检查有没有可以卖出的票,此时得到的结果是还剩1张,这是窗口1就开始收钱,打印票据,当窗口1在收钱/打印票据时候,线程就切换给了窗口2,这是创建开始判断是否有票可买的时候,由于当窗口1在收钱/打印票据,还没有对票数进行减1的操作,因此窗口2判断有一张票可买,窗口2开始收钱,打印票据,找钱完成以后,对票数进行减1操作以后,票数已经为0 ,还没有是输出卖出1张票的结果的时候,线程又一次切换给窗口1,窗口1是在窗口2对票数进行减1操作之后,再一次对票数进行的减1操作,所以创建就会出现还剩-1张票的情况。
原因是因为CPU在执行某个线程的时候,并没有将线程的任务全部执行完成就切换到其他的线程上导致数据有误。
原因:多条线程去访问同一个资源的时候,可能会出现资源访问数据不一致的情况,为了避免这种情况的出现,就需要使用线程安全【线程同步】。
线程安全问题
线程安全问题发生本质:多个线程他们在操作共享的数据(资源)。而CPU在执行线程的过程中操作共享资源的代码还没有彻底执行完,CPU就切换到其他线程上,导致数据不一致。
为什么?
原因:多条线程去访问同一个资源的时候,可能会出现资源访问数据不一致的情况,为了避免这种情况的出现,就需要使用线程同步【线程安全】。
解决安全问题:线程的同步技术。
同步的目的就是保证有一个线程在执行任务代码的时候,其他线程要在外面等待。
同步原理:只要某些代码(牙医)被添加了同步(门,应该门上的那个锁),那么任何线程在进入被同步控制的代码的时候都需要判断有没有其他某个线程在同步中(要想看牙医,需要先能够打开锁),如果有当前其他的任何线程都需要在同步的外面等待,如果没有这时只有某一个线程可以进入到同步中,其他线程就继续在同步的外面等待。
同步原理:当窗口1去访问票数的时候,我们使用线程同步技术,将被访问的票数锁定,如果窗口2要想访问票数,窗口2就得先判读是否有其他的窗口正在使用这个票数,如果判断有其他的窗口正在使用这个票数,那么窗口2就需要去等待,等待窗口1对票数资源访问完毕以后,再去访问这个票数,窗口2在访问票数时候,我们同样需要使用线程同步将被访问票数资源锁定,其他的线程要想访问票数资源,就要等到窗口2对票数资源访问才可以访问票数资源。
是什么?
【线程安全/线程同步】:多条线程去访问同一个资源的时候,每一次保证一条线程正常访问资源,当前该线程访问资源的时候,其他的线程就需要等待,当前该线程访问资源结束之后,允许另一条线程去访问资源,其他线程继续等待。【排队看牙医】
Java中实现线程同步的方式
怎么做?
1.同步代码块
同步代码块的书写格式:
synchronized(任意的对象[锁] ){
书写的被同步的代码(操作共享数据的代码);
}
package com.click369.test1;
/**
* 实现买票程序的线程类
* @author Administrator
*
*/
public class MyThread implements Runnable{
//定义一个变量,来保存票数
//假设我们现在有5张票可以卖出
private int piao=5;
@Override
public void run() {
//1.通过while循环控制买票的持续性
//定义一个变量,来控制while循环
boolean flag=true;
while(flag){
/**
* 同步代码块格式
* synchronized(任意的对象[锁] ){
书写的被同步的代码(操作共享数据的代码);
}
*synchronized---同步关键字
*(同步对象)--需要被锁定的资源所在类的对象
*{}---【块】
*将需要同步的Java程序写上面的{}块中
*/
synchronized(this){
//2.判断是否有票
//如果票数大于0,就表是有票,可以卖出
if(piao>0){
//3.线程休眠模拟出收钱,打票,找钱的过程
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
//得到当钱包线程的名称
String name=Thread.currentThread().getName();
//4.卖出第几张票
//5.票数减1
System.out.println(name+",卖出第1张票,还剩"+(--piao)+"张票");
}else{
//如果票数小于/等于0,就是已经无票可卖
flag=false;
}
}
}
}
}
上面我们通过同步代码块实现线程同步,同步代码块在编写的时候,需要设置一个同步对象,很多人都不明白这个同步对象都是是谁,不容易被判定。
能不能有一个不需要判定同步对象的这个线程同步的实现方式-----同步方法
2.同步方法
同步方法格式:
访问限制修饰符 synchronized 返回值类型 方法名称(参数列表){
书写的被同步的代码(操作共享数据的代码);
}
被synchronized 关键字修饰的方法就是同步方法
package com.click369.test1;
/**
* 实现买票程序的线程类
* @author Administrator
*
*/
public class MyThread implements Runnable{
//定义一个变量,来保存票数
//假设我们现在有5张票可以卖出
private int piao=5;
//定义一个变量,来控制while循环
private boolean flag=true;
@Override
public void run() {
//1.通过while循环控制买票的持续性
while(flag){
//调用同步方法的执行
seller();
}
}
/**
* 创建买票的同步方法
* 同步方法格式:
访问限制修饰符 synchronized 返回值类型 方法名称(参数列表){
书写的被同步的代码(操作共享数据的代码);
}
*/
private synchronized void seller(){
//2.判断是否有票
//如果票数大于0,就表是有票,可以卖出
if(piao>0){
//3.线程休眠模拟出收钱,打票,找钱的过程
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
//得到当钱包线程的名称
String name=Thread.currentThread().getName();
//4.卖出第几张票
//5.票数减1
System.out.println(name+",卖出第1张票,还剩"+(--piao)+"张票");
}else{
//如果票数小于/等于0,就是已经无票可卖
flag=false;
}
}
}
6.sleep 与wait的区别?
sleep和wait的区别:
1、sleep是Thread的静态方法,wait是Object的方法,任何对象实例都能调用。
2、sleep不会释放锁,它也不需要占用锁。wait会释放锁,但调用它的前提是当前线程占有锁(即代码要在synchronized中)。
3、它们都可以被interrupted方法中断。
7.notify 与notifyAll的区别?
notify()和notifyAll()都是Object对象用于通知处在等待该对象的线程的方法。两者的最大区别在于:
notifyAll使所有原来在该对象上等待被notify的线程统统退出wait的状态,变成等待该对象上的锁,一旦该对象被解锁,他们就会去竞争。
notify则文明得多他只是选择一个wait状态线程进行通知,并使它获得该对象上的锁,但不惊动其他同样在等待被该对象notify的线程们,当第一个线程运行完毕以后释放对象上的锁此时如果该对象没有再次使用notify语句,则即便该对象已经空闲,其他wait状态等待的线程由于没有得到该对象的通知,继续处在wait状态,直到这个对象发出一个notify或notifyAll,它们等待的是被notify或notifyAll,而不是锁。