多线程基础知识
这是我再次学习多线程知识的一个总结,对于刚刚接触的学习者是比较友好易懂的,便于快速的理解和掌握。
一.基本概念:
1.进程:进程就是运行中的程序,当一个程序开始执行,操作系统就会给这个进程分配内存空间来执行它。而进程的执行过程是一个动态的过程,它有着自己产生、存在和小王的过程。
2.线程:线程是由进程创建的,是进程的一个实体。一个进程可以拥有多个线程,一个线程也可以去创建新的线程。
3.并发与并行
并发:同一个多个任务交替进行,造成了一种貌似同时执行的感觉,简单说单核cpu实现的多任务就是并发。(一时多发)
并行:在同一个时刻,多个任务可以同时执行,多核cpu可以实现并行。(一时一发)
二、线程的基本使用
总共由两种方法实现线程:
1.继承Thread类,重写run方法,当一个类继承它,那么这个类就是一个线程。
2.实现Runnable接口,重写run方法,这样的操作就相当于自定义的线程。
下面是Thread的接口关系图:
我们的业务逻辑一般写在run方法中,而run不是Thread中定义的方法,它是重写于Runnable接口方法的,最初在Runnable中定义。
实例化继承了Thread的类之后,还需要start才可以启动这个线程。启动程序的时候,main就是主线程,之后启动子线程主线程不会被阻塞,两个线程并行执行。主线程可能会提前结束,但是这并不影响子线程的生命,main挂掉之后,0号线程依然执行。
三、线程的底层原理
1、用start()启动线程,发生了什么?
先start,然后就会调用到我们重写的run方法。run()就是一个简单的方法,如果直接调用它,相当于还是在主线程。start() 才能真正的开启一个新的线程。
源码解释:
1.在Thread的start方法中,调用了start0()这个方法,start0()是一个本地方法,它是由JVM来调用的,我们没有办法调用这个方法,底层由 c/c++ 来实现。
四、线程的基本使用
1.Runnable接口的使用
使用的原因:java是单继承的,如果一个类已经继承了某一个父类,这时显然不能直接继承Thread来开启线程,就要用到Runnable接口来创建线程。
但是Runnable本身实现的方法只有一个,那就是run()方法,所以我们没办法使用start()来开启一个新的线程。
这里要使用:
Dog dog = new Dog();
Thread thread = new Thread(dog);
thread.start();
//这里创建了Thread对象,把实例化的dog对象,放到这个新的实例化线程中。因为,dog已经实现了Runnable接口,这里的底层使用了一个设计模式【代理模式】,接下来用代码模拟一下代理模式的实现。
//线程代理类,模拟了极简的Thread类
class ThreadProxy implements Runnable{
private Runnable target=null;
public ThreadProxy(Runnable target) {
this.target = target;
}
//在这个构造器中,接收了一个实现了Runnable接口的对象,也就是上面的那一句。就这样,我们把外面的对象传到了代理类中,走到了start0()方法里。而进一步的,start0就真的开启了线程,这个时候调用重写的run方法,就是在新的线程中跑run。
public void start(){
start0();
}
public void start0(){
//JVM调用该方法
run();
}
@Override
public void run() {
if (target!=null){
target.run();
}
}
}
2.多线程的使用
如果在主线程中开启了线程1、2,那么,如果主线程已经结束,不会影响到子线程的执行。从某种意义上说,它们是相互独立的,有着各自的生命周期。
如果在继承了Thrad的类中,有一部分资源可以被创建的线程共享,如果此时我们多开了线程来使用这部分资源,那么就会出现线程之间抢占资源的情况,会出现重复使用的问题。这里也就引出了锁的概念。
3.线程终止
线程的停止可以有两种方法:一种是使用完成后的自然销毁,一种是通知销毁,通过使用变量控制run方法退出来停止线程。
具体的使用方法就是在线程中设置一个boolean类型的私有变量loop,用set方法使外界可以修改线程内部的loop,这样我们就可以从外界控制一个线程的生死。
4.线程插队(yield、join)
yield:线程的礼让,让出cpu资源,让其他线程先去执行,但是礼让的时间不确定,能不能成功也不确定,取决于当下cpu的资源够不够用。(这么随缘真的有用处吗?)
join:线程的插队。插队的线程一旦插队成功,则必定完成插入线程的所有任务。
5.用户线程和守护线程
用户线程:也叫做工作线程,当线程的任务执行完或被以通知方式结束。
守护线程:一般是为了工作线程服务的,当所有用户线程结束,守护线程会自动结束(垃圾回收机制)。守护的作用往往用于监控其他线程,获取其他线程的信息。
DaemonbThread.setDaemon(true);
DaemonbThread.start();
六、线程的生命周期
1.线程的六种状态
NEW :🐤初始状态,一个新创建的线程,还没有开始执行。
RUNNABLE :可执行状态,可能正在执行,也可能一切准备就绪在等待执行。
WAITING : 等待状态,等待其他的线程去执行特定的操作,没有时间限制
TIMED WAITING : 限时等待状态,等待其它线程去执行特定的工作,有时间限制,sleep就是典例。
BLOCKED : 阻塞状态,等待锁,以便于进入同步块。
TERMINATED : 终止状态,线程执行结束。
整体上可以分为两部分,顺利的流程就是:产生---->工作---->终止
不顺利就会进入下面的三种状态中。这里需要着重理解一下阻塞的含义,如果一个线程要执行的代码被其它线程锁住了,那么久阻塞了,需要等待人家解开锁后才可以执行。
非正常情况结束后,就要回到 RUNNABLE 状态,继续执行。
七、线程同步机制(Synchronized)
1.如何理解线程同步机制?
在多线程中,有一些敏感数据不能允许多个线程同时的操作和使用,这时候就要用到线程同步机制。它可以确保在同一时间,只能有一个线程可以访问这些资源,这样一来就保证了数据的完整性。
线程同步的本质:当一个线程在对内存进行操作时,其他的线程就不能对这个内存地址进行操作。
2.互斥锁
Java中,引入了互斥锁的概念,用来保证数据的完整性。只要有一个称为“互斥锁”的标记,就可以确保只有一个线程访问该对象。
同步方法,如果是非静态的,锁就是this或者是其他对象。如果是静态的,锁就是当前类本身。
synchronized (this){
//非静态(没用static修饰),要锁的内容、数据
}
synchronized (MainActivity.class){
//静态(用static修饰),要锁的内容、数据
}
实现锁的具体步骤:
1.先分析出来要上锁的代码
2.选择同步代码块或者同步方法
3.保证多个线程的锁对象时同一个即可
3.线程的死锁
当多个线程都占用了对方的锁资源,但是彼此都互不相让,就导致了死锁。
下面就是死锁的简单示例:
若flag为true,A线程先得到o1对象锁,然后尝试拿到o2对象锁。拿不到o2就会阻塞。
若flag为false,B线程先得到o2对象锁,然后尝试拿到o1对象锁。拿不到o2就会阻塞。
当A、B线程各自拿到一个锁,就会死锁。
boolean flag=true;
if (flag){
synchronized (o1){
synchronized (o2){
}
}
}else{
synchronized (o2){
synchronized (o1){
}
}
}
4.释放锁
锁的释放有四种情况:
1.当前线程的同步方法执行结束
2.线程在同步代码块中遇到了break、return,被迫结束。
3.线程在同步代码块中遇到了未处理的Error、Exception,异常结束。
4.线程在同步代码块中执行了线程对象的wait()方法,线程会暂停并释放锁。