Java多线程入门超详解(小白也可以看懂的)
一、名词详解
名词解释有个概念即可,修过操作系统这门课的同学可以快速浏览一遍在脑海中回忆起来。
1.1 进程和线程
进程: 进程就是一个运行的程序实体,进程拥有独立的代码和数据空间,比如我们打开了QQ,就是启动了一个QQ的进程。进程间的切换开销较大,进程是资源分配的最小单位,一个进程可以创建多个线程。
线程: 线程类似于进程,是轻量级的进程,同一个进程创建的线程共享代码段和数据空间,因此线程间的切换开销较小。线程是cpu调度的最小单位。
1.2 并行和并发
并行: 指的是同一时刻的同时执行,比如你在吃饭,你旁边的同学在喝水,便可说你们两个是在并行。
并发: 在同一时间间隔执行,比如你吃一口饭,喝一口水,便可以说你是在并发地执行吃饭和喝水两个任务。
因此,并行指的是不同实体的多个任务,用多个cpu完成多个任务,如hadoop集群;而并发指的是同一个实体进行的多个任务,旨在提高处理器每个核的运行效率。
1.3 同步和异同步
同步: 即实时处理,可以理解为方法执行完时等待系统的返回值或消息,再执行下一步的操作。如一个顽固的老头,充了100块钱话费,一定要看到话费到账的信息再去做其他事。
异步: 方法执行之后不必一直等到返回值或消息,交由系统进行异步处理。如上面提到了老头,在充了100块钱话费后去喝茶了,晚上才看到话费到账的提示。
1.4 临界资源和临界区
临界资源: 。一次只允许一个线程访问的资源,如打印机,一个线程抢占后其他线程只能等待其用完后再去抢占。因为多个线程同时访问一个临界源可能会造成数据的不一致甚至毁坏,因此对临界资源进行访问的方法一般要加上同步方法
临界区: 指的是对临界资源进行访问的代码段,如使用打印机的代码。
二、为什么要用多线程
通过多个线程提高系统的工作效率以及资源的使用效率
三、线程的五种状态
创建、就绪、运行、阻塞、终止
重要:记熟状态转换图!看懂了这张图,也就基本可以理解线程的运行状态。
创建: 新建了一个线程对象 t = new Thread()
就绪: t.start(),此时线程并没有开始运行,而是进入了就绪状态,告诉操作系统“ 我准备好了可以运行 ”,具体什么时候运行由操作系统的调度决定。
运行: 线程分配到了cpu的时间片,此时占用cpu资源运行
阻塞: 由于线程自身的原因,此时线程释放cpu资源,暂停运行进入阻塞状态。满足某些条件后可重新进入就绪状态等待系统调度。阻塞分为三种:
1: 等待阻塞: 执行object.wati()方法,线程释放持有的对象锁,同时进入阻塞状态,等待其他线程执行object.notify或者object.notifyAll()唤醒,进入就绪状态等待系统调度。
2:同步阻塞: 线程在尝试获取比如objectX的锁资源时,若X已经被其他线程所持有了,那么本线程就进入阻塞状态,直至X被释放时再去争抢X,成功获取锁资源objectX再进入就绪状态。
3: 其它阻塞: 如执行Thread.sleep()或t.join()。sleep()不会释放持有的锁资源,时间到了直接进入就绪状态。
终止: 线程结束执行并消亡了。一般线程里抛出了异常,执行return或者代码执行完毕都会使线程终止。
四、Java创建线程的两种方法
Java创建线程有三种方式,继承Thread类,实现Runnable接口,以及通过Callable和FutureTask创建。最后一种暂时不做介绍。
4.1、继承Thread类
/**
* @function:学习使用继承Thread的方法实现多线程
* @author: 程志军
* @date: 2020/3/6 下午7:53
**/
class Thread1 extends Thread{
private String name;
public Thread1(String name) {
this.name = name;
}
public void run(){
for (int i=0;i<10;i++){
System.out.println(name + "is running ");
}
}
}
class Mian{
public static void main(String[] args) {
Thread1 t1=new Thread1("A");
Thread1 t2=new Thread1("B");
t1.start();
t2.start();
}
}
在这里我们编写了一个Thread1继承Thread类,重写了run方法。新创建的线程执行的代码写在run方法里。
我们发现多线程各个线程的执行顺序是不固定的,每次运行都可以是不同的运行顺序,因此多线程适用于运行顺序乱序的场景。若需要顺序访问或互斥访问时需要用上synchronized。
注意:在运行t.start()时线程不会马上执行,而是进入就绪态,等到系统调度抢到了cpu资源再运行,如结果所示,先执行t1.start(),却先打印了了t2线程执行的信息。
4.2、实现Runnable接口
接着我们编写Thread2类实现Runnable接口
/**
* @function: 学习使用实现runnable接口实现多线程
* @author: 程志军
* @date: 2020/3/6 下午11:35
**/
public class Thread2 implements Runnable{
private String name;
public Thread2(String name){
this.name = name;
}
@Override
public void run() {
for(int i=0;i<10;i++){
System.out.println(name + " is running " + i);
}
}
}
class Main2{
public static void main(String args[]) throws InterruptedException {
Thread t1 = new Thread(new Thread2("A"));
Thread t2 = new Thread(new Thread2("B"));
t1.start();
t2.start();
}
}
在这里我们编写了一个Thread2类实现了Runnable接口,要运行的代码逻辑写在run方法里。
我们来看看Thread的构造方法
/ ** @param target
* the object whose {@code run} method is invoked when this thread
* is started. If {@code null}, this classes {@code run} method does
* nothing.
*/
public Thread(Runnable target) {
init(null, target, "Thread-" + nextThreadNum(), 0);
}
按照注释的说明,我们在new Thread(Runnable target)构造一个线程时,实际上传进的参数便是实现了run方法的类对象,在线程执行start方法时进入就绪状态等待执行run方法。
因此继承Thread类和实现Runnable接口本质都是实现run方法,而我们也更推荐使用Runnable接口的方法,一是更利于线程间资源的共享,二是可以避免Java单继承的限制,即Java类只能继承一个类,但却可以实现多个接口。
注:不能直接t.run()运行线程,这样相当于执行了一个普通类的普通方法,只有t.start()才会通过新建一个线程来执行run方法里的代码。
Runnable接口拓展:使用lambda表达式
既然只是继承接口实现方法,那我们便可使用lambda匿名内部类的方式实现
/**
* @function:
* @author: 程志军
* @date: 2020/3/26 下午9:56
**/
public class ThreadCreat {
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(()->{
for (int i = 0; i < 10; i++) {
System.out.println("A第" + i + "次运行");
}
});
Thread t2 = new Thread(()->{
for (int i = 0; i < 10; i++) {
System.out.println("B第" + i + "次运行");
}
});
Thread.sleep(10);
t1.start();
t2.start();
}
}
如上,我们通过lambda表达式的方法,创建了两个实现Runnable接口的匿名内部类,作为参数传进Thread类创建线程。
五、Java线程的常用方法
5.1、wait()和notify()
wait和notify或notifyAll配合使用,且必须在synchronized方法体内(关于synchronized的介绍也是个较长的内容,这里简单概述下使用:
synchronized(object){....}
,执行方括号里的代码前要获取object对象并将其锁上,此时其它线程不能获取object;若object已被锁上则代码不能执行,线程等待object被释放获取到时才能执行。)
o.wait()为释放对象锁o,且对象锁o必须为synchronized获取到的对象锁,同时释放cpu并进入睡眠状态,直至o.notify()将其唤醒进入就绪状态。
o.notify为随机唤醒一个等待o锁的线程,但并不是立即使其进入就绪态,而是自己synchronized方法体内的代码执行完毕,释放了o对象锁才唤醒进入就绪态。o.notifyAll()唤醒所有等待对象锁o的线程。
现在编写一个程序,num作为产品数量,用两个线程进行操作,一个加一,另一个减一,顺序循环五次。
package producerAndConsumer;
/**
* @function:
* @author: 程志军
* @date: 2020/3/10 下午3:49
**/
public class Pool {
private int num=0;
public synchronized void increase() throws InterruptedException {
if (num!=0){
wait();
}
num++;
System.out.println(num);
notify();
}
public synchronized void decrease() throws InterruptedException {
if (num!=1){
wait();
}
num--;
System.out.println(num);
notify();
}
public static void main(String[] args) {
Pool pool = new Pool();
new Thread(()->{
for (int i = 0; i <5 ; i++) {
try {
pool.decrease();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}).start();
new Thread(()->{
for (int i = 0; i <5 ; i++) {
try {
pool.increase();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}).start();
}
}
我们编写了increase和decrease两个同步方法(synchronized加在方法上时的对象锁即为this,后面的wait()和notify实际也都是this.wait()和this.notify()),然后新建了两个线程执行increase和decrease方法。
开始执行时,第一个线程因num=0而进入等待状态,第二个线程执行了num++,接着唤醒了第一个线程使其执行num–,第一个线程再唤醒第二个线程,循环五次,顺序打印了10五次.
5.2、sleep(),yield(),join()
Thread.sleep(long millis): 当前线程释放cpu资源睡眠指定的毫秒数,但并不释放持有的锁资源。
Thread.yield(): 让步函数,当前线程由运行态变成就绪态。当前线程重新进入就绪态与其它线程一起再抢夺cup资源,可能是自己再次运行,也可能不是。
5.3、setPriority()
设置线程的优先级,Java线程设置1-10优先级依次从低到高。需要注意的是,优先级高的不是一定会比优先级低的先执行,而是先执行的概率更大。
MIN_PRIORITY = 1
NORM_PRIORITY = 5
MAX_PRIORITY = 10
线程默认的优先级为5,通过t.setPriority()可以设置线程的优先级,但必须要在t.start()之前设置,否则无效。
六、synchronized的使用
synchronized的内容也是一个很大的板块,在这里就不展开介绍了,下篇博客再来详细介绍。