2021-09-27

文章目录


前言

JAVA多线程详解——一篇搞懂多线程


提示:以下是本篇文章正文内容,下面案例可供参考

一、多线程是什么?

为了让大家更加了解多线程的概念,我将从线程、进程、程序的基本概念进行讲解。

  1. 程序:为了完成某种任务,运用某种编程语言编写的一系列有序执行的指令集合。可以将它理解成一段静态的代码,即未运行的代码。
  2. 进程:进程(Process)是计算机中的程序关于某数据集合上的一次运行活动,是系统进行资源分配和调度的基本单位,是操作系统结构的基础每一个进程都有一个独立的内存空间(堆栈)。程序是指令、数据及其组织形式的描述,进程是程序的实体。
  3. 线程:线程是进程中的一个实体,若把进程称为任务的话,那么线程则是应用中的一个子任务的执行。又因为进程都有一个独立的内存空间,因此共一个进程中的线程是共享一块内存空间的,即可以访问相同的变量,这样使得多条线程可以将一个进程拆分,大大提高了CPU运行效率(不提高CPU运行速度,这在接下来的线程调度会进行详细说明)。但是随之而来的也会产生一些线程安全问题。

二、线程调度

为了让大家更好的明白线程调度,首先要明白CPU同一时刻只能干一件事情的基本原则。接下来我将先从单CPU单核的基础上进行线程调度的讲解。

  1. 分时调度:所有线程轮流使用CPU的使用权,将CPU的使用权平均分配给每个线程。来一个栗子说明一下:有兄弟肯定会认为在同一时刻我的电脑也在完成多项事情,这是因为CPU可以将一段很短的时间拆分成很多份(如:将一秒拆分成1000份),这样由于人感受不到,因此就觉得电脑在同时完成多项事情。
  2. 抢占式调度:优先级越高的线程抢到CPU的使用权的时间份额的概率越大。如果优先级相同则,抢到使用权的时间概率相同。
    -线程的优先级
    MAX_PRIORITY:最高优先级
    MIN_PRIORITY:最低优先级
    NORM_PRIORITY:默认优先级
    -获取和设置当前线程的优先级
    getPriority(); 获取
    setPriority(int p); 设置
    说明:优先级只是表面抢到CPU的使用权的概率会大一些,但是差别并不会很大。
    总结:由于进程只有一个,所以分配CPU的占有率是相同的,多线程不过是轮流占用了CPU的使用而已,因此并不会提高CPU的处理速度,只能提高效率。多线程主要提高的是并发的数量。
    补充:
    1、并发:同一时间发生的。(可以是一秒、一天、一小时等)
    2、并行:同一时刻发生的。

三、线程的创建与启动

方式一:继承Thread类

流程:

  1. 创建继承Thread类的子类
  2. 重写Thread的run()方法
  3. 创建继承Thread类的子类的对象
  4. 调用Thread的start()方法来启动线程

代码如下(示例):

public class MyThread extends Thread{
	//MyThread类继承Thread并且重写run方法
	//当线程执行时,都会执行run方法中的代码块
	public void run(){
	for(int i=0;i<10;i++){
		System.out.println(Thread.currentThread().getName()+i);
	}
}
class Test{
	public static void main(String args[]){
	//在主线程中创建该线程对象
		MyThread m = new Thread();
	//调用Thread的start()方法进行线程的启动
		m.start();
		for(int i=0;i<10;i++){
		System.out.println(Thread.currentThread().getName()+i);
		}
	}
}

代码运行图解:
2021-09-27


方式二:实现Runnable接口

流程:

  1. 创建一个实现Runnable接口的类
  2. 子类去实现Runnable接口中的抽象方法:run()
  3. 创建实现Runnable接口的类的对象
  4. 将此对象作为参数传到Thread类的构造器中,创建Thread类的对象。
  5. 通过Thread类的对象调用start()方法。

代码如下(示例):

public class Test {
    public static void main(String[] args) {
        //创建实现Runnable接口的对象
        MyRunnable r = new Runnable();
        //创建Thread类的对象,并将实现接口的对象当做参数传入构造器
        Thread t1 = new Thread(r);
        //使用Thread类的对象去调用Thread类的start()方法
        t1.start();
    }
}

//MyRunnable实现Runnable接口的run()抽象方法
class MyRunnable implements Runnable {
    @Override
    public void run() {
        for (int i = 0; i < 100; i++) {
            if (i % 2 == 0) System.out.println(Thread.currentThread().getName() + ":\t" + i);
        }
    }
}

方式二相较于方式一的优势:

-实现Runnable与继承Thread相比较有如下优势:

  1. 通过创建任务,然后给线程分配任务,更适合多个线程同时执行相同任务的情况。
  2. 可以避免单继承所带来的局限。
  3. 任务与线程之间是分离的,从而提高了程序的健壮性。
  4. 线程池(接下来会讲解)接受Runnable类型的任务,不接受Thread类型的线程。

方式三:实现Callable接口

相较于前两种方式的优势:这种线程可以有返回值,并且该返回值支持泛型。可用于首先需要多个线程执行后将返回值返回给主线程,之后主线程再运行的情况就可以使用Callable接口。

流程:

  1. 实现Callable接口,并且实现call方法。
  2. 创建FutureTask对象,并且传入编写的Callable类的接口
  3. 通过Thread的start方法启动。

代码如下(示例):

public class Test4 {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        //创建MyCallbale对象
        MyCallable m = new MyCallable();
        //创建FutureTask对象,并且传入m
        FutureTask<Integer> f = new FutureTask<>(m);
        //创建一个Thread的匿名对象并且传入f,调用start方法
        new Thread(f).start();
        //只有调用该方法,主线程才会等到子线程全部执行完毕才能执行
        Integer j = f.get();
        System.out.println("返回值为:"+j);
        System.out.println(Thread.currentThread().getName()+"开始执行");
        System.out.println("哈哈哈哈哈哈哈");
    }
}

//创建一个MyCallbale类并且实现Callable接口
class MyCallable implements Callable<Integer>{
//实现它的call方法
    @Override
    public Integer call() throws Exception {
        for (int i=0;i<10;i++){
            if(i%2==0)
                System.out.println(i);
        }
        return 666;
    }
}

执行结果:
2021-09-27


方式四:线程池

  1. 好处:当出现执行的线程太多且执行的任务较少的情况,由于频繁的创建线程会大大降低CPU的效率,又因为频繁的创建和关闭线程需要大量的时间而执行的任务较少,因此提出了线程池这一概念,它是一个以线程为对象的线程数组,里面包含多个线程可以反复使用,这样就大大节约了创建和关闭线程的时间。
  2. 类别:
    —大类分为:定长线程池、不定长线程池。
    定长线程池: 线程数组中的线程是一定的,当任务来的时候若有空闲线程则执行,若无则等待某个线程执行完任务后空闲,在执行该任务。
    不定长线程池: 线程数组的线程数是可变的,若当前所有线程无空闲,则当任务来临时会自动扩容执行该任务,当线程空闲时间过长则会自动减小线程长度。
    —小类分为:缓冲线程池、定长线程池、单线程线程池、周期性任务线程池
    缓冲线程池(不定长): 判断有无空闲线程、存在则使用、不存在则新创建一个线程并加入到线程池后进行任务执行。
    定长线程池(定长): 判断有无空闲线程、存在则使用、不存在则判断该线程数组是否已满、达到线程数组长度后则等待、未达到则创建线程并加入线程池执行任务。
    单线程线程池: 只有一个线程重复执行任务,可以用于线程需要排队执行时。
    周期性任务线程池(定长):
    两种玩法:
    1、定时执行一次
    2、周期性执行

缓冲线程池代码如下(示例):

import java.util.concurrent.Executor;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

/**
 * 缓冲线程池,不定长。
 * 要是线程执行完任务后,则空闲。当下一个任务来的时候就给空闲的线程执行
 */
public class CachedPool {
    public static void main(String[] args) {
        //创建缓冲线程池
        ExecutorService service = Executors.newCachedThreadPool();
        //创建任务对象并且执行,这里采用了创建Runnable的匿名对象的方式
        service.execute(new Runnable() {
            @Override
            public void run() {
                System.out.println(Thread.currentThread().getName() + "正在执行!");
            }
        });
        service.execute(new Runnable() {
            @Override
            public void run() {
                System.out.println(Thread.currentThread().getName() + "正在执行!");
            }
        });
        service.execute(new Runnable() {
            @Override
            public void run() {
                System.out.println(Thread.currentThread().getName() + "正在执行!");
            }
        });
        //此处睡眠一段时间,为了让别的前面的线程执行完毕。
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        service.execute(new Runnable() {
            @Override
            public void run() {
                System.out.println(Thread.currentThread().getName() + "正在执行!");
            }
        });
    }
}

2021-09-27
结果分析:这里就是当thread3、thread1、thread2、执行完毕后在执行sleep休眠一秒钟,接下来在继续任务的执行,这里就可以很清晰的看到thread3执行完后空闲又继续执行了下一个任务。

定长线程池代码如下(示例):

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class FixedPool {
    public static void main(String[] args) {
        //创建定长线程池并且给上定长线程池的长度。
        ExecutorService service = Executors.newFixedThreadPool(2);
        //创建Runnable的匿名对象作为参数传入excute方法,调用excute方法进行任务的执行
        service.execute(new Runnable() {
            @Override
            public void run() {
                System.out.println(Thread.currentThread().getName()+"正在执行!");
            }
        });
        service.execute(new Runnable() {
            @Override
            public void run() {
                System.out.println(Thread.currentThread().getName()+"正在执行!");
            }
        });
        service.execute(new Runnable() {
            @Override
            public void run() {
                System.out.println(Thread.currentThread().getName()+"正在执行!");
            }
        });
        service.execute(new Runnable() {
            @Override
            public void run() {
                System.out.println(Thread.currentThread().getName()+"正在执行!");
            }
        });
    }
}

2021-09-27
结果分析:从定长线程池的执行结果来看,我们可以得到4个任务只有两个线程轮流执行,并且由于JAVA是采用了抢占式调度,并且这两个线程从的优先级是相同的,因此线程执行的先后顺序是随机的。


单线程池代码如下(示例):

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class SinglePool {
    public static void main(String[] args) {
        //创建一个单线程线程池
        ExecutorService service = Executors.newSingleThreadExecutor();
        service.execute(new Runnable() {
            @Override
            public void run() {
                System.out.println(Thread.currentThread().getName()+"正在执行!");
                try {
                    Thread.sleep(3000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });
        service.execute(new Runnable() {
            @Override
            public void run() {
                System.out.println(Thread.currentThread().getName()+"正在执行!");
                try {
                    Thread.sleep(3000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });
        service.execute(new Runnable() {
            @Override
            public void run() {
                System.out.println(Thread.currentThread().getName()+"正在执行!");
            }
        });
    }
}

2021-09-27
结果分析:从执行结果我们可以很直观的看到四个任务都是由同一个线程所执行的。

周期性任务定长线程池代码如下(示例):
1、定时执行一次

import java.util.concurrent.*;

public class ScheduleFixedPool {
    public static void main(String[] args) {
    	//创建周期性任务定长线程池并且给定长度
        ScheduledExecutorService service = Executors.newScheduledThreadPool(2);
        /**
         *  定时执行一次,各个参数的意义
         *  1、任务对象
         *  2、时长数字
         *  3、时长数字单位
         */
        service.schedule(new Runnable() {
            @Override
            public void run() {
                System.out.println("我在维也纳的海边*的舞蹈!");
            }
        },5, TimeUnit.SECONDS);
  	}
}

过程分析:经过5 Seconds时间执行打印任务。
2、周期性执行

import java.util.concurrent.*;

public class ScheduleFixedPool {
    public static void main(String[] args) {
    	//创建周期性任务定长线程池并且给定长度
        ScheduledExecutorService service = Executors.newScheduledThreadPool(2);
         /**
         * 周期性执行,各个参数的意义
         * 1、任务对象
         * 2、延迟时间(第一次任务在多少秒后执行)
         * 3、周期时间(每次间隔多少秒后执行)
         * 4、单位
         */
        service.scheduleAtFixedRate(new Runnable() {
            @Override
            public void run() {
                System.out.println(Thread.currentThread().getName()+"我在维也纳的海边*的舞蹈!");
            }
        },5,2,TimeUnit.SECONDS);
    }
}

过程分析:经过五秒后首次执行,接下来没经过两秒后执行一次该任务。

线程池小结

由上述可知,缓冲线程池(newCachedThreadPool())、定长线程池(newFixedThreadPool(线程数组长度))、单线程池(newSingleThreadExecutor())的创建方式以及执行方式都类似:
ExecutorService service = Executors.对应的方法。
而唯有周期性任务定长线程池的创建方法不同于以上几种,ScheduledExecutorService service = Executors.newScheduledThreadPool(线程数);

四、Thread类的常用方法

常用构造方法:

构造器 描述
Thread() 不分配任务对象
Thread(Runnable target) 分配新的任务对象
Thread(Runnable target,String name) 分配新的任务对象且命名
Thread(String name) 创建新的线程并且命名

最常用方法:

变量和类型 方法 描述
void setName() 更改(设置)此线程的名称
void setPriority(int newPriotity ) 更改此线程的优先级
String getName() 返回此线程的名称
int fetPriority() 返回此线程的优先级
static void sleep​(long millis) 导致当前正在执行的线程休眠(暂时停止执行)指定的毫秒数,具体取决于系统计时器和调度程序的精度和准确性。
static void sleep​(long millis, int nanos) 导致当前正在执行的线程休眠(暂时停止执行)指定的毫秒数加上指定的纳秒数,具体取决于系统定时器和调度程序的精度和准确性。

五、线程的同步

1、多线程的安全性问题解析

由于多线程是在同一个进程中的,因此他们共享同一块内存,这就导致多条线程再对同一块内存的数据进行操作,那么这样在多条操作共享数据的之间线程就可能发生切换。只要切换就会有安全问题。
举个栗子说明一下:
代码如下:

public class Sale {
    public static void main(String[] args) {
        Runnable t = new Tickets();
        new Thread(t).start();
        new Thread(t).start();
        new Thread(t).start();
    }
}
//创建一个卖票的类
class Tickets implements Runnable{
    int count = 10;//总票数10张
    @Override
    public void run() {
        while (count>0){
        //这里加一个休眠方法是为了别的线程能够更容易的进入该任务。
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            count--;
            System.out.println(Thread.currentThread().getName()+"卖票成功,余票:"+count);
        }
    }
}

执行结果:2021-09-27
执行结果分析:从结果可以很清楚的看到卖票系统会出现票数为负数的情况。当票数为1的时候,三个线程中有线程被阻塞没有执行票数-1的操作,这时其它线程就会通过if语句的判断,这样一来就会造成多卖了一张票,出现错票的情况。当三个线程都同时通过while(count>0)的语句时就会出现票数为-2的情况,而这个是实际卖票系统中不允许存在的。

2、多线程安全性问题的解决

原理:当一个线程在进行共享数据的操作时,其余线程不能参与进来,直到该线程操作完共享数据的时候其它线程才能够进行操作。

方式一:采用同步代码块

  • 同步代码块格式:
  • synchronized(锁对象){需要被同步的代码}
  • 1、锁对象:任何类的对象都可以作为锁,但是所有的线程必须共用同一把锁
  • 2、需要被同步的代码:这里多个线程都能够操作的共享数据(代码),
  • 但是千万不能在run()方法一开始就直接上锁,这样就类似于单线程了
public class Sale {
    public static void main(String[] args) {
        Runnable t = new Tickets();
        new Thread(t).start();
        new Thread(t).start();
        new Thread(t).start();
    }
}

/**
 * 同步代码块格式:
 * synchronized(锁对象){需要被同步的代码}
 * 1、锁对象:任何类的对象都可以作为锁,但是所有的线程必须共用同一把锁
 * 2、需要被同步的代码:这里多个线程都能够操作的共享数据(代码),
 * 但是千万不能在run()方法一开始就直接上锁,这样就类似于单线程了
 */
class Tickets implements Runnable{
    //票数10张
    int count = 10;
    //在run()方法外创建一个锁对象,保证所有的线程都是用同一把锁
    Object o = new Object();
    @Override
    public void run() {
            while (true){
                //把所有线程共同执行的代码作为同步代码块,切记不是把run()方法里的代码全部括起来,因为
                //这就相当于一个线程执行了
                synchronized (o){
                    if(count>0){
                        try {
                            Thread.sleep(1000);
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                        count--;
                        System.out.println(Thread.currentThread().getName()+"卖票成功,余票:"+count);
                    }else {
                        break;
                    }
            }
        }
    }
}

方式二:采用同步方法

将所要同步的代码放到一个方法中,将方法声明为synchronized同步方法。之后可以在run()方法中调用同步方法。
同步方法的格式:
public synchronized boolean sale();

在方法前加一个synchronized关键字修饰。
非静态的同步方法,同步监视器是:this。
静态的同步方法,同步监视器是:当前类本身。

public class Sale {
    public static void main(String[] args) {
        Runnable t = new Tickets();
        new Thread(t).start();
        new Thread(t).start();
        new Thread(t).start();
    }
}


class Tickets implements Runnable{
    //票数10张
    int count = 10;
    //在run()方法外创建一个锁对象,保证所有的线程都是用同一把锁
    Object o = new Object();
    @Override
    public void run() {
            while (true){
                boolean flag = judge();
                if(!flag){
                    System.out.println("票已卖空!");
                    break;
                }
            }
        }
    //同步方法的锁:若是没有用static修饰则是锁对象是调用该方法的对象
    //若是加了static关键字则锁对象是  类名.class
    public synchronized boolean judge(){
        if(count>0){
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            count--;
            System.out.println(Thread.currentThread().getName()+"卖票成功,余票:"+count);
            return true;
        }
        return false;
    }
}

方式三:采用显示锁

JDK5.0之后,可以通过实例化ReentrantLock对象,在所需要同步的语句前,调用ReentrantLock对象的lock()方法,实现同步锁,在同步语句结束时,调用unlock()方法结束同步锁。
显示锁的格式:
Lock l = new ReentrantLock();

synchronized(隐式锁)与lock(显示锁)的方法的异同:显式锁和隐式锁都是为了保证线程安全Java官方提出来的解决办法,它们的区别简而言之就是 是否能自动 开/关锁 ,能自动 开/关锁 的属于隐式锁,需要程序员操作进行开关锁的属于显示锁。

import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

public class Sale {
    public static void main(String[] args) {
        Runnable t = new Tickets();
        new Thread(t).start();
        new Thread(t).start();
        new Thread(t).start();
    }
}


class Tickets implements Runnable{
    //票数10张
    int count = 10;
    //在run()方法外创建一个锁对象,保证所有的线程都是用同一把锁
    Lock l = new ReentrantLock();
    @Override
    public void run() {
            while (true){
                //调用lock方法进行加锁
                l.lock();
                if(count>0) {
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    count--;
                    System.out.println(Thread.currentThread().getName() + "卖票成功,余票:" + count);
                }else{
                    break;
                }
                //当需要多线程执行的代码块执行完毕之后解锁。
                l.unlock();
            }
    }
}

3、线程死锁

1、原理:​ 不同的线程分别占用对方需要的同步资源不放弃,都在等待对方放弃自己需要的同步资源,就形成了死锁。出现死锁后,并不会出现异常,不会出现提示,只是所有的线程都处于阻塞状态,无法继续。使用同步时应避免出现死锁。

2、线程死锁形成的四个必要条件:

  • 互斥条件:一个资源只能被一个线程占用
  • 请求与保持:一个线程请求被占用资源,且不放开手上的资源
  • 不剥夺:即自己抢到资源不会被别的线程抢走
  • 循环等待条件:当发生死锁时,会形成一个环路(类似于死循环)

解决方法:只需要打破其中一个条件即可。

六、多线程通信问题

1、为什么需要线程通信?
若是每个线程都单独执行各自的任务,就会造成资源浪费。因此需要每个线程按照指定的规则来完成任务,这就需要每个线程之间相互协调配合,这个过程就叫做线程的通信。
我们主要以生产者和消费者的角度去讲解线程之间的通信。接下来,我们以厨师和服务员的角度去分析。
2、所要用到的方法:

方法 描述
wait() 一旦执行此方法,当前线程就会进入阻塞,一旦执行wait()会释放同步监视器。
​ notify() 一旦执行此方法,将会唤醒被wait的一个线程。如果有多个线程被wait,就唤醒优先度最高的。
​ ​ notifyAll() 一旦执行此方法,就会唤醒所有被wait的线程

代码如下(示例):

/**
 * 线程通信问题:
 * 过程:厨师生产好一份菜,服务员端走一份。厨师再生产,服务员再端走,重复上述过程
 * 重点要求:在厨师线程工作,服务员线程休眠,厨师生产完菜后,厨师线程休眠,服务员线程开始端菜,
 * 服务生线程休眠,厨师线程工作。由此反复。。。
 *
 */
public class Test3 {
    public static void main(String[] args) {
        Food f = new Food();
        new Cook(f).start();
        new Waiter(f).start();
    }
}
//厨师线程
class Cook extends Thread{
    private Food f;
    public Cook(Food f){
        this.f = f;
    }
    public void run(){
        for(int i=0;i<50;i++){
            if(i%2 == 0){
                f.setAll("麦多","藤椒味儿");
            }
            else{
                f.setAll("蟹肉煲","烧烤味儿");
            }
        }
    }
}
//服务员线程
class Waiter extends Thread{
    private Food f;
    public Waiter(Food f){
        this.f = f;
    }
    public void run(){
        for (int i=0;i<50;i++){
            f.getAll();
        }
    }
}
//创建一个Food类
class Food{
    //创建两个私有变量:菜名、味道
   private String name;
   private String taste;
   //创建一个标志,作为厨师线程和服务员线程工作或者休眠的标志。(true时厨师线程工作。false时服务员线程工作)
   private boolean flag = true;
   //厨师生产菜品
    public synchronized void setAll (String name,String taste){
        if(flag) {
            this.name = name;
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            this.taste = taste;
            flag = false;//修改标志
            this.notifyAll();//唤醒所有线程
            try {
                this.wait();//该线程处于等待直至被唤醒
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
   }
   //服务员端菜
   public synchronized void getAll(){
        if(!flag) {
            System.out.println("这道菜是:" + name + ",味道:" + taste);
            flag = true;//修改标志
            this.notifyAll();//唤醒所有线程
            try {
                this.wait();//该线程处于等待直至被唤醒
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
   }
}

注意:notifyAll();需要在同步方法或者同步代码块中执行。

上一篇:线程的创建和使用


下一篇:已获千赞,安卓面试题2020