Java多线程

简介: 多线程就是一个程序中有多个线程在同时执行。

1 多线程介绍

学习多线程之前,我们先要了解几个关于多线程的概念。

进程

  • 进程指正在运行的程序,表示资源分配的基本单位。
  • 确切的来说,当一个程序进入内存运行,即变成一个进程,进程是处于运行过程中的程序,并且具有一定独立功能。

线程

  • 线程是进程中执行运算的最小单位,亦是调度运行的基本单位。
  • 线程是进程中的一个执行单元,负责当前进程中程序的执行,一个进程中至少有一个线程。一个进程中是可以有多个线程的,这个应用程序也可以称之为多线程程序。

简而言之,一个程序运行后至少有一个进程,一个进程中可以包含多个线程。

例如打开你的计算机上的任务管理器,会显示出当前机器的所有进程,QQ,Chrome等,当QQ运行时,就有很多子任务在同时运行。比如,当你边打字发送表情,边好友视频时这些不同的功能都可以同时运行,其中每一项任务都可以理解成“线程”在工作。

多线程

  • 多线程就是一个程序中有多个线程在同时执行。

提到多线程这里要说两个概念,就是串行和并行,搞清楚这个,我们才能更好地理解多线程。

串行

  • 所谓串行,其实是相对于单条线程来执行多个任务来说的,我们就拿下载文件来举个例子:当我们下载多个文件时,在串行中它是按照一定的顺序去进行下载的,也就是说,必须等下载完A之后才能开始下载B,它们在时间上是不可能发生重叠的。

在这里插入图片描述

并行

  • 并行就是下载多个文件,开启多条线程,多个文件同时进行下载,这里是严格意义上的,在同一时刻发生的,并行在时间上是重叠的。

在这里插入图片描述

了解了这两个概念之后,我们再来说说什么是多线程。举个例子,我们打开腾讯管家,腾讯管家本身就是一个程序,也就是说它就是一个进程,它里面有很多的功能,能查杀病毒、清理垃圾、电脑加速等众多功能。

按照单线程来说,无论你想要清理垃圾、还是要病毒查杀,那么你必须先做完其中的一件事,才能做下一件事,这里面是有一个执行顺序的。

如果是多线程的话,我们其实在清理垃圾的时候,还可以进行查杀病毒、电脑加速等等其他的操作,这个是严格意义上的同一时刻发生的,没有执行上的先后顺序。

2 为什么要用多线程?

  • 为了更好的利用cpu的资源,如果只有一个线程,则第二个任务必须等到第一个任务结束后才能进行,如果使用多线程则在主线程执行任务的同时可以执行其他任务,而不需要等待;
  • 进程之间不能共享数据,线程可以;
  • 系统创建进程需要为该进程重新分配系统资源,创建线程代价比较小;
  • Java语言内置了多线程功能支持,简化了java多线程编程。

3 线程的生命周期

  • 新建 :从新建一个线程对象到程序start() 这个线程之间的状态,都是新建状态;
  • 就绪 :线程对象调用start()方法后,就处于就绪状态,等到JVM里的线程调度器的调度;
  • 运行 :就绪状态下的线程在获取CPU资源后就可以执行run(),此时的线程便处于运行状态,运行状态的线程可变为就绪、阻塞及死亡三种状态。
  • 等待/阻塞/睡眠 :在一个线程执行了sleep(睡眠)、suspend(挂起)等方法后会失去所占有的资源,从而进入阻塞状态,在睡眠结束后可重新进入就绪状态。
  • 终止 :run()方法完成后或发生其他终止条件时就会切换到终止状态。

4 创建线程的方法

4.1 继承Thread类

实现步骤

  1. 定义类继承Thread;
  2. 复写Thread类中的run()方法;

目的:将自定义代码存储在run()方法,让线程运行。

  1. 调用线程的start()方法:

该方法有两步:启动线程,调用run()方法。

public class ThreadDemo1 {
    public static void main(String[] args) {
        //创建两个线程
        ThreadDemo td = new ThreadDemo("zhangsan");
        ThreadDemo tt = new ThreadDemo("lisi");
        //执行多线程特有方法,如果使用td.run();也会执行,但会以单线程方式执行。
        td.start();
        tt.start();
        //主线程
        for (int i = 0; i < 5; i++) {
            System.out.println("main" + ":run" + i);
        }
    }
}

//继承Thread类
class ThreadDemo extends Thread{
    //设置线程名称
    ThreadDemo(String name){
        super(name);
    }
    
    //重写run方法。
    public void run(){
        for(int i = 0; i < 5; i++){
            System.out.println(this.getName() + ":run" + i);  
            //currentThread() 获取当前线程对象(静态)。  
            //getName() 获取线程名称。
        }
    }
}

4.2 实现Runnable接口

接口应该由那些打算通过某一线程执行其实例的类来实现,类必须定义一个称为run 的无参方法。

实现步骤

  1. 定义类实现Runnable接口;
  2. 覆盖Runnable接口中的run()方法,将线程要运行的代码放在该run()方法中;
  3. 通过Thread类建立线程对象;
  4. 将Runnable接口的子类对象作为实际参数传递给Thread类的构造函数;

自定义的run()方法所属的对象是Runnable接口的子类对象。所以要让线程执行指定对象的run()方法就要先明确run()方法所属对象。

  1. 调用Thread类的start()方法开启线程并调用Runnable接口子类的run方法。
public class RunnableDemo {
    public static void main(String[] args) {
        RunTest rt = new RunTest();
        //建立线程对象
        Thread t1 = new Thread(rt);
        Thread t2 = new Thread(rt);
        //开启线程并调用run方法。
        t1.start();
        t2.start();
    }
}

//定义类实现Runnable接口
class RunTest implements Runnable{
    private int tick = 10;
    //覆盖Runnable接口中的run方法,并将线程要运行的代码放在该run方法中。
    public void run(){
        while (true) {
            if(tick > 0){
                System.out.println(Thread.currentThread().getName() + "..." + tick--);
            }
        }
    }
}

4.3 通过Callable和Future创建线程

实现步骤

  1. 创建Callable接口的实现类,并实现call()方法,改方法将作为线程执行体,且具有返回值;
  2. 创建Callable实现类的实例,使用FutrueTask类进行包装Callable对象,FutureTask对象封装了Callable对象的call()方法的返回值;
  3. 使用FutureTask对象作为Thread对象启动新线程;
  4. 调用FutureTask对象的get()方法获取子线程执行结束后的返回值。
public class CallableFutrueTest {
    public static void main(String[] args) {
        //创建对象
        CallableTest ct = new CallableTest();
        //使用FutureTask包装CallableTest对象
        FutureTask<Integer> ft = new FutureTask<Integer>(ct);
        for(int i = 0; i < 100; i++){
            //输出主线程
            System.out.println(Thread.currentThread().getName() + "主线程的i为:" + i);
            //当主线程执行第30次之后开启子线程
            if(i == 30){
                Thread td = new Thread(ft,"子线程");
                td.start();
            }
        }
        //获取并输出子线程call()方法的返回值
        try {
            System.out.println("子线程的返回值为" + ft.get());
        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (ExecutionException e) {
            e.printStackTrace();
        }
    }
}

class CallableTest implements Callable<Integer>{
    //复写call() 方法,call()方法具有返回值
    public Integer call() throws Exception {
        int i = 0;
        for( ; i<100; i++){
            System.out.println(Thread.currentThread().getName() + "的变量值为:" + i);
        }
        return i;
    }
}

5 继承Thread类和实现Runnable接口、实现Callable接口的区别

继承Thread:线程代码存放在Thread子类run方法中。

  • 优点:编写简单,可直接用this.getname()获取当前线程,不必使用Thread.currentThread()方法。
  • 缺点:已经继承了Thread类,无法再继承其他类。

实现Runnable:线程代码存放在接口的子类的run方法中。

  • 优点:避免了单继承的局限性、多个线程可以共享一个target对象,非常适合多线程处理同一份资源的情形。
  • 缺点:比较复杂、访问线程必须使用Thread.currentThread()方法、无返回值。

实现Callable

  • 优势:有返回值、避免了单继承的局限性、多个线程可以共享一个target对象,非常适合多线程处理同一份资源的情形。
  • 劣势:比较复杂、访问线程必须使用Thread.currentThread()方法

建议使用实现接口的方式创建多线程。

6 线程状态管理

6.1 线程睡眠(sleep)

线程睡眠的原因

  • 线程执行的太快,或需要强制执行到下一个线程。

线程睡眠的方法

  • sleep(long millis)在指定的毫秒数内让正在执行的线程休眠。
  • sleep(long millis,int nanos)在指定的毫秒数加指定的纳秒数内让正在执行的线程休眠。
public class SynTest {
    public static void main(String[] args) {
        new Thread(new CountDown(),"倒计时").start();
    }
}

class CountDown implements Runnable{
    int time = 10;
    public void run() {
        while (true) {
            if(time>=0){
                System.out.println(Thread.currentThread().getName() + ":" + time--);
                try {
                    //睡眠时间为1秒
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

每隔一秒则会打印一次,打印结果为:

倒计时:10
倒计时:9
倒计时:8
倒计时:7
倒计时:6
倒计时:5
倒计时:4
倒计时:3
倒计时:2
倒计时:1
倒计时:0

扩展

  • Java线程调度是Java多线程的核心,只有良好的调度,才能充分发挥系统的性能,提高程序的执行效率。但是不管程序员怎么编写调度,只能最大限度的影响线程执行的次序,而不能做到精准控制。因为使用sleep()方法之后,线程是进入阻塞状态的,只有当睡眠的时间结束,才会重新进入到就绪状态,而就绪状态进入到运行状态,是由系统控制的,我们不可能精准的去干涉它,所以如果调用Thread.sleep(1000)使得线程睡眠1秒,可能结果会大于1秒。

6.2 线程让步(yield)

该方法和sleep()方法类似,也是Thread类提供的一个静态方法,可以让正在执行的线程暂停,但是不会进入阻塞状态,而是直接进入就绪状态。相当于只是将当前线程暂停一下,然后重新进入就绪的线程池中,让线程调度器重新调度一次。也会出现某个线程调用yield方法后暂停,但之后调度器又将其调度出来重新进入到运行状态。

public class SynTest {
    public static void main(String[] args) {
        yieldDemo ms = new yieldDemo();
        Thread t1 = new Thread(ms,"张三吃完还剩");
        Thread t2 = new Thread(ms,"李四吃完还剩");
        Thread t3 = new Thread(ms,"王五吃完还剩");
        t1.start();
        t2.start();
        t3.start();
    }
}

class yieldDemo implements Runnable{
    int count = 20;
    public void run() {
        while (true) {
            if(count>0){
                System.out.println(Thread.currentThread().getName() + count-- + "个瓜");
                if(count % 2 == 0){
                    //线程让步
                    Thread.yield();
                }
            }
        }
    }
}

sleep和yield的区别

  • sleep方法声明抛出InterruptedException,调用该方法需要捕获该异常。yield没有声明异常,也无需捕获。
  • sleep方法暂停当前线程后,会进入阻塞状态,只有当睡眠时间到了,才会转入就绪状态。而yield方法调用后 ,是直接进入就绪状态。

6.3 线程合并(join)

当B线程执行到了A线程的.join()方法时,B线程就会等待,等A线程都执行完毕,B线程才会执行。

join可以用来临时加入线程执行。

public static void main(String[] args) throws InterruptedException {    
    yieldDemo ms = new yieldDemo();
    Thread t1 = new Thread(ms,"张三吃完还剩");
    Thread t2 = new Thread(ms,"李四吃完还剩");
    Thread t3 = new Thread(ms,"王五吃完还剩");
    t1.start();
    t1.join();
    t2.start();
    t3.start();
    System.out.println( "主线程");
}

6.4 停止线程

原stop方法因有缺陷已经停用了,那么现在改如何停止线程?现在分享一种,就是让run方法结束。

开启多线程运行,运行的代码通常是循环结构,只要控制住循环,就可以让run方法结束,也就是线程结束。

public class StopThread {
    public static void main(String[] args) {
        int num = 0;
        StopTh st = new StopTh();
        Thread t1 = new Thread(st);
        Thread t2 = new Thread(st);
        t1.start();
        t2.start();
        //设置主线程执行50次,执行结束之后停止线程
        while (true) {
            if(num++ == 50){
                st.flagChange();
                break;
            }
            System.out.println(Thread.currentThread().getName() + "..." + num);
        }
    }
}

class StopTh implements Runnable{
    private boolean flag = true;
    public void run() {
        while(flag){
            System.out.println(Thread.currentThread().getName() + "stop run" );
        }
    }

    public void flagChange(){
        flag = false;
    }
}

特殊情况

  • 当线程处于了冻结状态,就不会读取到标记,也就不会结束。当没有指定方法让冻结的线程回复到运行状态时,我们需要对冻结状态进行清除,也就是强制让线程恢复到运行状态中来,这样可就可以操作标记让线程结束。
  • Thread类提供该方法: interrupt();(如果线程在调用Object类的wait()、wait(long)、wait(long、int)方法,或者该类的join()、join(long)、join(long、int)、sleep(long)或sleep(long、int)方法过程中受阻,则其中断状态将被清除,还将收到一个InterruptedException。)

6.5 设置优先级

每个线程执行时都有一个优先级的属性,优先级高的线程可以获得较多的执行机会,而优先级低的线程则获得较少的执行机会。与线程休眠类似,线程的优先级仍然无法保障线程的执行次序。只不过,优先级高的线程获取CPU资源的概率较大,优先级低的也并非没机会执行。

Thread类中提供了优先级的三个常量,代码如下:

MAX_PRIORITY = 10
MIN_PRIORITY = 1
NORM_PRIORITY = 5
------------------------------------------------------
ThreadDemo td = new ThreadDemo();
Thread t1 = new Thread(td,"张三");
//设置优先级
t1.priority(9);
//设置完毕     
t1.start();

7 线程同步与锁

为什么要进行线程同步?

  • java允许多线程并发控制,当多个线程同时操作一个可共享资源变量时(如对其进行增删改查操作),会导致数据不准确,而且相互之间产生冲突。所以加入同步锁以避免该线程在没有完成操作前被其他线程调用,从而保证该变量的唯一性和准确性。

不同步会发生的问题?

  • 在介绍同步方法之前先演示一下当多个线程操作一个共享资源时可能会发生的错误,这里用的方法是让线程在执行时睡眠10毫秒,会导致多个线程去操作同一个资源变量。
public class SynTest {
    public static void main(String[] args) {
        //定义三个线程
        MySyn ms = new MySyn();
        Thread t1 = new Thread(ms,"线程1输出:");
        Thread t2 = new Thread(ms,"线程2输出:");
        Thread t3 = new Thread(ms,"线程3输出:");
        t1.start();
        t2.start();
        t3.start();
    }
}

class MySyn implements Runnable{
    //共执行10次线程
    int tick = 10; 
    public void run() {
        while(true){
            if(tick>0){
                try {
                    //执行中让线程睡眠10毫秒
                    Thread.sleep(10);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread().getName() + " " + tick--);
            }
        }
    }
}

输出结果用以下图片展示,可以看到我勾选的部分都发生了冲突数据:

在这里插入图片描述

同步方法1

  • 同步函数:就是用synchronize关键字修饰的方法。因为每个java对象都有一个内置锁,当用synchronize关键字修饰方法时内置锁会保护整个方法,而在调用该方法之前,要先获得内置锁,否则就会处于阻塞状态。
  • 代码演示:请将上方代码的第17行改为以下代码
    public synchronized void run() {

同步方法2

  • 同步代码块:就是拥有synchronize关键字修饰的语句块,被该关键字修饰的语句块会自动被加上内置锁,从而实现同步。
  • 代码演示:将上方代码的run方法改成下方代码
public void run() {
    while(true){
        //同步代码块
        synchronized (this) {
            if(tick>0){
                try {
                    //执行中让线程睡眠10毫秒
                    Thread.sleep(10);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread().getName() + " " + tick--);
            }
        }
    }
}

加同步之后的输出数据为:

线程1输出: 10
线程2输出: 9
线程2输出: 8
线程2输出: 7
线程2输出: 6
线程2输出: 5
线程2输出: 4
线程3输出: 3
线程3输出: 2
线程3输出: 1

追加问题:如果同步函数被静态修饰之后,使用的锁是什么?静态方法中不能定义this!

静态内存是:内存中没有本类对象,但是一定有该类对应的字节码文件对象。 类名.class 该对象类型是Class。

所以静态的同步方法使用的锁是该方法所在类的字节码文件对象。 类名.class。代码如下:

public static mySyn(String name){
    synchronized (Xxx.class) {
        Xxx.name = name;
    }
}

同步的前提

  1. 必须要有两个或者两个以上的线程。
  2. 必须是多个线程使用同一个锁。
  3. 必须保证同步中只能有一个线程在运行。
  4. 只能同步方法,不能同步变量和类。
  5. 不必同步类中所有方法,类可以拥有同步和非同步的方法。
  6. 如果一个线程在对象上获得一个锁,就没有任何其他线程可以进入(该对象的)类中的任何一个同步方法。
  7. 线程睡眠时,它所持的任何锁都不会释放。
  • 好处:解决了多线程的安全问题。
  • 弊端:多个线程需要判断,消耗资源,降低效率。

如何找问题?

  1. 明确哪些代码是多线程运行代码。
  2. 明确共享数据。
  3. 明确多线程运行代码中哪些语句是操作共享数据的。

8 死锁

进程A中包含资源A,进程B中包含资源B,A的下一步需要资源B,B的下一步需要资源A,所以它们就互相等待对方占有的资源释放,所以也就产生了一个循环等待死锁。

public class DeadLock {
    public static void main(String[] args) {
        Thread t1 = new Thread(new DeadLockTest(true));
        Thread t2 = new Thread(new DeadLockTest(false));
        t1.start();
        t2.start();
    }
}

class DeadLockTest implements Runnable{
    private boolean flag;
    static Object obj1 = new Object();
    static Object obj2 = new Object();
    public DeadLockTest(boolean flag) {
        this.flag = flag;
    }
    
    public void run(){
        if(flag){
            synchronized(obj1){
                System.out.println("if lock1");
                synchronized (obj2) {
                    System.out.println("if lock2");
                }
            }
        }else{
            synchronized (obj2) {
                System.out.println("else lock2");
                synchronized (obj1) {
                    System.out.println("else lock1");
                }
            }
        }
    }
}  

死锁形成的必要条件总结(都满足之后就会产生)

  1. 互斥条件:资源不能被共享,只能被同一个进程使用;
  2. 请求与保持条件:已经得到资源的进程可以申请新的资源;
  3. 非剥夺条件:已经分配的资源不能从相应的进程中强制剥夺;
  4. 循环等待条件:系统中若干进程形成环路,该环路中每个进程都在等待相邻进程占用的资源。
相关文章
|
8天前
|
安全 Java 测试技术
Java并行流陷阱:为什么指定线程池可能是个坏主意
本文探讨了Java并行流的使用陷阱,尤其是指定线程池的问题。文章分析了并行流的设计思想,指出了指定线程池的弊端,并提供了使用CompletableFuture等替代方案。同时,介绍了Parallel Collector库在处理阻塞任务时的优势和特点。
|
4天前
|
安全 Java 开发者
深入解读JAVA多线程:wait()、notify()、notifyAll()的奥秘
在Java多线程编程中,`wait()`、`notify()`和`notifyAll()`方法是实现线程间通信和同步的关键机制。这些方法定义在`java.lang.Object`类中,每个Java对象都可以作为线程间通信的媒介。本文将详细解析这三个方法的使用方法和最佳实践,帮助开发者更高效地进行多线程编程。 示例代码展示了如何在同步方法中使用这些方法,确保线程安全和高效的通信。
23 9
|
7天前
|
存储 安全 Java
Java多线程编程的艺术:从基础到实践####
本文深入探讨了Java多线程编程的核心概念、应用场景及其实现方式,旨在帮助开发者理解并掌握多线程编程的基本技能。文章首先概述了多线程的重要性和常见挑战,随后详细介绍了Java中创建和管理线程的两种主要方式:继承Thread类与实现Runnable接口。通过实例代码,本文展示了如何正确启动、运行及同步线程,以及如何处理线程间的通信与协作问题。最后,文章总结了多线程编程的最佳实践,为读者在实际项目中应用多线程技术提供了宝贵的参考。 ####
|
4天前
|
监控 安全 Java
Java中的多线程编程:从入门到实践####
本文将深入浅出地探讨Java多线程编程的核心概念、应用场景及实践技巧。不同于传统的摘要形式,本文将以一个简短的代码示例作为开篇,直接展示多线程的魅力,随后再详细解析其背后的原理与实现方式,旨在帮助读者快速理解并掌握Java多线程编程的基本技能。 ```java // 简单的多线程示例:创建两个线程,分别打印不同的消息 public class SimpleMultithreading { public static void main(String[] args) { Thread thread1 = new Thread(() -> System.out.prin
|
7天前
|
Java
JAVA多线程通信:为何wait()与notify()如此重要?
在Java多线程编程中,`wait()` 和 `notify()/notifyAll()` 方法是实现线程间通信的核心机制。它们通过基于锁的方式,使线程在条件不满足时进入休眠状态,并在条件满足时被唤醒,从而确保数据一致性和同步。相比其他通信方式,如忙等待,这些方法更高效灵活。 示例代码展示了如何在生产者-消费者模型中使用这些方法实现线程间的协调和同步。
21 3
|
6天前
|
安全 Java
Java多线程集合类
本文介绍了Java中线程安全的问题及解决方案。通过示例代码展示了使用`CopyOnWriteArrayList`、`CopyOnWriteArraySet`和`ConcurrentHashMap`来解决多线程环境下集合操作的线程安全问题。这些类通过不同的机制确保了线程安全,提高了并发性能。
|
7天前
|
Java
java小知识—进程和线程
进程 进程是程序的一次执行过程,是系统运行的基本单位,因此进程是动态的。系统运行一个程序即是一个进程从创建,运行到消亡的过程。简单来说,一个进程就是一个执行中的程序,它在计算机中一个指令接着一个指令地执行着,同时,每个进程还占有某些系统资源如CPU时间,内存空间,文件,文件,输入输出设备的使用权等等。换句话说,当程序在执行时,将会被操作系统载入内存中。 线程 线程,与进程相似,但线程是一个比进程更小的执行单位。一个进程在其执行的过程中产生多个线程。与进程不同的是同类的多个线程共享同一块内存空间和一组系统资源,所以系统在产生一个线程,或是在各个线程之间做切换工作时,负担要比
17 1
|
7天前
|
Java UED
Java中的多线程编程基础与实践
【10月更文挑战第35天】在Java的世界中,多线程是提升应用性能和响应性的利器。本文将深入浅出地介绍如何在Java中创建和管理线程,以及如何利用同步机制确保数据一致性。我们将从简单的“Hello, World!”线程示例出发,逐步探索线程池的高效使用,并讨论常见的多线程问题。无论你是Java新手还是希望深化理解,这篇文章都将为你打开多线程的大门。
|
8天前
|
安全 Java 编译器
Java多线程编程的陷阱与最佳实践####
【10月更文挑战第29天】 本文深入探讨了Java多线程编程中的常见陷阱,如竞态条件、死锁、内存一致性错误等,并通过实例分析揭示了这些陷阱的成因。同时,文章也分享了一系列最佳实践,包括使用volatile关键字、原子类、线程安全集合以及并发框架(如java.util.concurrent包下的工具类),帮助开发者有效避免多线程编程中的问题,提升应用的稳定性和性能。 ####
33 1
|
11天前
|
存储 设计模式 分布式计算
Java中的多线程编程:并发与并行的深度解析####
在当今软件开发领域,多线程编程已成为提升应用性能、响应速度及资源利用率的关键手段之一。本文将深入探讨Java平台上的多线程机制,从基础概念到高级应用,全面解析并发与并行编程的核心理念、实现方式及其在实际项目中的应用策略。不同于常规摘要的简洁概述,本文旨在通过详尽的技术剖析,为读者构建一个系统化的多线程知识框架,辅以生动实例,让抽象概念具体化,复杂问题简单化。 ####