1.进程线程的由来
最初的计算机只能接收一些特定的指令,用户输入一个指令以后,计算机才操作。然后出现了批处理操作系统,把一系列的操作指令写在一个清单上,一次性交给计算机,但是由于批处理操作系统的指令运⾏⽅式仍然是串⾏的,内存中始终只有⼀个程序在运⾏,后⾯的程序需要等待 前⾯的程序执⾏完成后才能开始执⾏,⽽前⾯的程序有时会由于I/O操作、⽹络等 原因阻塞,所以批处理操作效率也不⾼。
1.1 进程的由来
进程就是应⽤程序在内存中分配的空间,也就是正在运行的程序,各个进程之间互不⼲扰。同时进程保存着程序每⼀个时刻运⾏的状态。
运行机制:
CPU采⽤时间⽚轮转的⽅式运⾏进程:CPU为每个进程分配⼀个时间段,称作它的时间⽚。如果在时间⽚结束时进程还在运⾏,则暂停这个进程的运⾏,并且 CPU分配给另⼀个进程(这个过程叫做上下文切换)。如果进程在时间⽚结束前阻塞或结束,则CPU⽴即进⾏切换,不⽤等待时间⽚⽤完。使用方式,进程让操作系统的并发成为了可能。
上下文切换:(有时也称做进程切换或任务切换)是指 CPU 从⼀个进程(或线程) 切换到另⼀个进程(或线程)。上下⽂是指某⼀时间点 CPU 寄存器和程序计数器的内容。但是在切换前会保存上一个任务的状态,以便下次切换回这个任务时,可以再加载这个任务的状态,所以任务从保存到加载的过程就是一次上下文切换。
上下文切换通常是计算密集型的,意味着此操作系统会消耗大量的CPU时间,故线程也不是越多越好。
1.2线程的由来
让⼀个线程执⾏⼀个⼦任务,这样⼀个进程就包含了多个线程,每个线程负责⼀个单独的⼦任务。线程让进程的内部并发成为了可能。
总之,上面的目的都是提高操作系统的效率。
1.3进程与线程的区别(可以参考富士康厂房和流水线)
流水线:富士康按照厂房(进程)分配所需要的原料,每个厂房有几条生产线(线程),每个生产线上有很多工人(协程)。
进程是操作系统进行资源分配的基本单位,而线程是操作系统进行调度的基本单位,即,CPU分配时间的单位。
进程是一个独立的运行环境,而线程是在进程中执行的一个任务。他们两个本质的区别是是否单独占有内存地址空间及其它系统资源(比如I/O):
。进程有单独的内存地址空间,所以程序间存在内存隔离,数据是分开的,数据共享复杂但是同步简单,各个进程之间互不干扰;而线程共享所属进程占有的内存地址空间和资源,数据共享简单,但是同步复杂。
。进程单独占有⼀定的内存地址空间,⼀个进程出现问题不会影响其他进程,不 影响主程序的稳定性,可靠性⾼;⼀个线程崩溃可能影响整个程序的稳定性, 可靠性较低。
。进程单独占有⼀定的内存地址空间,进程的创建和销毁不仅需要保存寄存器和栈信息,还需要资源的分配回收以及⻚调度,开销较⼤;线程只需要保存寄存 器和栈信息,开销较⼩。
2.Java多线程入门类和接口
2.1Thread类和Runnable接口
如何使用多线程?
首先,我们需要有一个“线程”类。JDK提供了Thread类和Runnalble接口来让我们实现自己的“线程类”。
。继承Thread类,并重写run方法;
。实现Runable接口的run方法;
2.1.1继承Thread类
首先是继承Thread类:
public class Demo1 {
public static class MyThread extends Thread{
@Override
public void run() {
System.out.println("MyThread");
}
}
/*
1.我们在程序里面调用start()方法后,虚拟机会为我们创建一个线程,然后等到
这个线程第一次得到时间片时再调用run()方法。
2.注意不可多次调用start()方法。在第一次调用start()方法后,再次调用start()
方法会抛出异常。
*/
public static void main(String[] args) {
Thread myThread=new MyThread();
myThread.start();
}
}
注意:要调用start()方法后,该线程才算启动!
Thread几个常用的方法:
currentThread():静态方法,返回对当前正在执行的线程对象的引用。
start():开始执行线程的方法,java虚拟机会调用线程内的run()方法;
yield():yield放弃的意思,这里的yield()指的是当前线程愿意让出对当前处理器的占用。这里需要注意的是,就算当前线程调用了yield()方法,程序在调度的时候,也还有可能继续执行这个线程的;
sleep():静态方法,使当前线程睡眠一段时间;
join():使当前线程等待另一个线程执行完毕之后再继续执行,内部调用的使Object类的wait方法实现的;
2.1.2Runnable接口
Runnable函数式接口
@FunctionalInterface
public interface Runnable {
public abstract void run();
}
示例代码:
public class Demo2 {
public static class MyThread implements Runnable{
@Override
public void run() {
System.out.println("MyThread");
}
}
public static void main(String[] args) {
//创建自定义类对象,线程任务对象
MyThread mr= new MyThread();
//创建线程对象
Thread t=new Thread(mr);
t.start();
//Java 8函数式编程,可以省略MyThread类
new Thread(()->{
System.out.println("Java 8 匿名内部类");
}).start();
}
}
2.2Thread和Runnable的区别
如果一个类继承Thread,则不适合资源共享。但是如果实现了Runnable接口的话,则很容易的实现资源共享。
总结:
实现Runnable接口比继承Thread类所具有的优势:
(1)适合多个相同的程序代码的线程去共享一个资源。
(2)可以避免java中单继承的局限性。
(3)增加线程的健壮性,实现解耦操作,代码可以被多个线程共享,代码和线程独立。
(4)线程池只能放入实现Runable或Callable类线程,不能直接放入继承Thread的类。
注意:在java中,每次程序运行至少启动2个线程。一个是main线程,一个是垃圾收集线程。因为每当使用 java命令执行一个类的时候,实际上都会启动一个JVM,每一个JVM其实在就是在操作系统中启动了一个进程。
2.3匿名内部类方式实现线程的创建
使用线程的匿名内部类方法,可以方便的实现每个线程执行不同的线程任务操作。
使用匿名内部类的方式实现Runnable接口,重新Runnable接口中的run方法;
public class Demo3
{
public static void main(String[] args) {
Runnable r=new Runnable() {
@Override
public void run() {
System.out.println("你好");
}
};
new Thread(r).start();
}
}
3.线程组和线程优先级
3.1线程组(ThreadGroup)
Java中⽤ThreadGroup来表示线程组,我们可以使⽤线程组对线程进⾏批量控制。
ThreadGroup和Thread的关系就如同他们的字⾯意思⼀样简单粗暴,每个Thread必然存在于⼀个ThreadGroup中,Thread不能独⽴于ThreadGroup存在。
执⾏main() ⽅法线程的名字是main,如果在new Thread时没有显式指定,那么默认将⽗线程 (当前执⾏new Thread的线程)线程组设置为⾃⼰的线程组.
示例代码:
public class Demo4 {
public static void main(String[] args) {
Thread testThread = new Thread(()->{
System.out.println("testThread当前线程组名字:"+Thread.currentThread().getThreadGroup().getName());
System.out.println("testThread线程名字:"+Thread.currentThread().getName());
});
testThread.start();
System.out.println("执行main方法线程名字:"+Thread.currentThread().getName());
}
}
输出结果:
执行main方法线程名字:main
testThread当前线程组名字:main
testThread线程名字:Thread-0
3.2 线程优先级
Java中线程优先级可以指定,范围是1~10。但是并不是所有的操作系统都⽀持10 级优先级的划分(⽐如有些操作系统只⽀持3级划分:低,中,⾼),Java只是给操作系统⼀个优先级的参考值,线程最终在操作系统的优先级是多少还是由操作系统决定。
Java默认的线程优先级为5,线程的执⾏顺序由调度程序来决定,线程的优先级会在线程被调用之前设定。通常情况下,高优先级的线程将会比低优先级的线程有更高的机率得到执行。线程的优先级在创建线程时可以设置,也可以使⽤⽅法 Thread 类的 setPriority() 实例⽅法来设定线程的优先级,getPriority()方法获取线程的优先级。
Java中的优先级来说不是特别的可靠,Java程序中对线程所设置的优先级只是给操作系统⼀个建议,操作系统不⼀定会采纳。而真正的调⽤顺序,是由操作系统的线程调度算法决定的。
如果某个线程优先级大于线程所在线程组的最⼤优先级,那么该线程的优先级将会失效,取而代之的是线程组的最大优先级。
4.Java线程状态及主要转化方式
4.1操作系统中的线程状态转换
在现在的操作系统中,线程是被视为轻量级进程的,所以操作系统线程的状态其实和操作系统进程的状态保持一致的。
主要的三个状态:
(1)就绪状态(ready):线程正在等待使用CPU,经调度程序调用之后可进入running状态。
(2)执行状态(running):线程正在使用CPU。
(3)等待状态(waiting):线程经过等待事件的调用或者正在等待其他资源(I/O).
4.2 线程生命周期经理的5种状态
(1)新建状态(New):线程创建后处于该状态;
(2)可运行状态(Runnable):新建的线程调用start( )方法,将使线程的状态从New转换为Runnable;
(3)运行状态(Running):运行状态使线程占有CPU并实际运行的状态;
(4)阻塞状态(Blocked):导致该状态的原因很多,注意区别;
(5)终止状态(Dead):线程执行结束的状态,没有任何方法可改变它的状态。
5. Java线程的通信
5.1 锁与同步
在Java中,锁的概念都是基于对象的,所以我们又经常称它为对象锁。线程和锁的关系,我们可以用婚姻关系来理解。一个锁同一时间只能被一个线程持有。也就是说,一个锁如果和一个线程“结婚”(持有),那其他线程如果需要得到这个锁,就得到这个线程和这个锁“离婚”(释放)。
5.1.1 线程同步
什么是同步呢,假如我们现在有2位正在 抄暑假作业答案的同学:线程A和线程B。当他们正在抄的时候,⽼师突然来修改了⼀些答案,可能A和B最后写出的暑假作业就不⼀样。我们为了让A,B能写出2本相 同的暑假作业,我们就需要让⽼师先修改答案,然后A,B同学再抄。或者A,B同 学先抄完,⽼师再修改答案。这就是线程A,线程B的线程同步。
可以以解释为:线程同步是线程之间按照⼀定的顺序执行。
5.1.2 进程同步
进程同步是指进程之间一种直接的协同工作关系,这些进程相互合作,共同完成一项任务。进程间的直接相互作用构成进程的同步。
为了达到线程同步,我们可以使用锁来实现它。
无锁案例:
public class NoneLock {
static class ThreadA implements Runnable{
@Override
public void run() {
for(int i=0;i<100;i++){
System.out.println("Thread A"+i);
}
}
}
static class ThreadB implements Runnable{
@Override
public void run() {
for (int i=0;i<100;i++){
System.out.println("Thread B"+i);
}
}
}
public static void main(String[] args) {
new Thread(new ThreadA()).start();
new Thread(new ThreadB()).start();
}
}
每次运行结果会不同。
加锁案例:
public class ObjectLock {
private static Object lock = new Object();
static class ThreadA implements Runnable{
@Override
public void run() {
synchronized (lock){
for(int i=0; i<100; i++){
System.out.println("ThreadA"+i);
}
}
}
}
static class ThreadB implements Runnable{
@Override
public void run() {
synchronized (lock){
for (int i=0; i<100; i++){
System.out.println("Thread B"+i);
}
}
}
}
public static void main(String[] args) {
new Thread(new ThreadA()).start();
Thread.sleep(10);
new Thread(new ThreadB()).start();
}
}
每次输出的结果都是顺序的。
这里声明了一个名字为lock的对象锁。我们ThreadA和ThreadB内需要同步的代码块里,都是synchronized关键字加上了同一个对象锁lock。
根据线程和锁的关系,同⼀时间只有⼀个线程持有⼀个锁,那么 线程B就会等线程A执⾏完成后释放 lock ,线程B才能获得锁 lock 。
5.2等待/通知机制
上面一种基于“锁”的方式,线程需要不断地去尝试获得锁,如果失败了,再继续尝试。这可能会耗费服务资源。
而等待/通知机制是另一种方式。
Java多线程的等待/通知机制是基于Object类的wait()方法和notify(),notifyAll()方法来实现的.
notify()⽅法会随机叫醒⼀个正在等待的线程,而notifyAll()会叫醒所有正在等待的线程。
前⾯我们讲到,⼀个锁同⼀时刻只能被⼀个线程持有。⽽假如线程A现在持有了⼀ 个锁 lock 并开始执⾏,它可以使⽤ lock.wait() 让⾃⼰进⼊等待状态。这个时 候,lock 这个锁是被释放了的。
这时,线程B获得了 lock 这个锁并开始执⾏,它可以在某⼀时刻,使⽤ lock.notify() ,通知之前持有 lock 锁并进⼊等待状态的线程A,说“线程A你不⽤等了,可以往下执⾏了”。
示例代码:
public class WaitAndNotify {
private static Object lock=new Object();
static class ThreadA implements Runnable{
@Override
public void run() {
synchronized(lock){
for(int i=0;i<100;i++){
try {
System.out.println("ThreadA"+i);
lock.notify();
lock.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
lock.notify();
}
}
}
static class ThreadB implements Runnable{
@Override
public void run() {
synchronized (lock){
for (int i=0;i<100;i++){
try {
System.out.println("ThreadB"+i);
lock.notify();
lock.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
lock.notify();
}
}
}
public static void main(String[] args) throws InterruptedException {
new Thread(new ThreadA()).start();
Thread.sleep(1000);
new Thread(new ThreadB()).start();
}
}
运行结果:
ThreadA0
ThreadB0
ThreadA1
ThreadB1
ThreadA2
ThreadB2
ThreadA3
在这个Demo⾥,线程A和线程B⾸先打印出⾃⼰需要的东⻄,然后使⽤ notify() ⽅法叫醒另⼀个正在等待的线程,然后⾃⼰使⽤ wait() ⽅法陷⼊等待 并释放 lock 锁。
注意:
需要注意的是等待/通知机制使⽤的是同⼀个对象锁,如果你两个线程使用的是不同的对象锁,那它们之间是不能⽤等待/通知机制通信的。
5.3信号量
JDK提供了⼀个类似于“信号量”功能的类 Semaphore 。但这里不是要介绍这个类,而是介绍⼀种基于 volatile 关键字的⾃⼰实现的信号量通信。
volitile关键字能够保证内存的可⻅性,如果⽤volitile关键字声明了⼀个变 量,在⼀个线程⾥⾯改变了这个变量的值,那其它线程是⽴⻢可⻅更改后的值的。
如何让A与B交替输出数据呢?
示例代码:
public class Signal {
private static volatile int signal=0;
static class ThreadA implements Runnable{
@Override
public void run() {
while (signal<5){
if(signal%2==0){
System.out.println("ThreadA:"+signal);
synchronized (this){
signal++;
}
}
}
}
}
static class ThreadB implements Runnable{
@Override
public void run() {
while (signal<5){
if(signal%2==1){
System.out.println("ThreadB:"+signal);
synchronized (this){
signal++;
}
}
}
}
}
public static void main(String[] args) throws InterruptedException {
new Thread(new ThreadA()).start();
Thread.sleep(1000);
new Thread(new ThreadB()).start();
}
}
输入结果:
ThreadA:0
ThreadB:1
ThreadA:2
ThreadB:3
ThreadA:4
我们可以看到,使⽤了⼀个 volatile 变量 signal 来实现了“信号量”的模型。这⾥需要要注意的是,volatile 变量需要进⾏原⼦操作。 signal++ 并不是⼀个原⼦操作,所以我们需要使⽤ synchronized 给它“上锁”。
原子操作:就是无法被别的线程打断的操作,要么不执行,要么执行成功。
5.4 管道
管道是基于“管道流”的通信⽅式。JDK提供了 PipedWriter 、PipedReader 、PipedOutputStream、PipedInputStream 。其中,前⾯两个是基于字符的,后⾯两个是基于字节流的。
Java管道:Java/IO系统是建立再数据流概念之上的,它具有将一个程序的输出当作另一个程序的输入。
5.5 join方法
join()⽅法是Thread类的⼀个实例⽅法。它的作⽤是让当前线程陷⼊“等待”状态,等 join的这个线程执⾏完成后,再继续执⾏当前线程。
有时候,主线程创建并启动了⼦线程,如果⼦线程中需要进⾏⼤量的耗时运算,主线程往往将早于⼦线程结束之前结束。
如果主线程想等待⼦线程执⾏完毕后,获得⼦线程中的处理完的某个数据,就要⽤到join⽅法了。
示例代码:
public class Join {
static class ThreadA implements Runnable{
@Override
public void run() {
try {
System.out.println("我是子线程,我先睡一秒");
Thread.sleep(1000);
System.out.println("我是子线程,我睡完了一秒");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public static void main(String[] args) throws InterruptedException {
Thread thread=new Thread(new ThreadA());
thread.start();
thread.join();
System.out.println("如果不加join⽅法,我会先被打出来,加了就不⼀样了");
}
}
5.6 sleep方法
sleep⽅法是Thread类的⼀个静态⽅法。它的作⽤是让当前线程睡眠⼀段时间。它有这样两个⽅法:
这⾥需要强调⼀下:sleep⽅法是不会释放当前的锁的而wait方法会。这也是最常见的⼀个多线程⾯试题。
sleep跟wait方法的区别:
(1)wait可以指定时间,也可以不指定;而sleep必须指定时间。
(2)wait释放cpu资源,同时释放锁;sleep释放cpu资源,但是不释放锁,所以易死锁。
(3)wait必须放在同步块或同步方法中,而sleep可以任意位置。