【Java进阶】多线程(一)

简介: 【Java进阶】多线程(一)

@[toc]

【Java进阶】多线程

第一章:线程

1 并发与并行

  • :dagger: 并发:指两个或多个事件在同一时间段内发送
  • :airplane: 并行:指两个或多个事件在同一时刻发生

在操作系统中,安装了多个程序,并发指的是在一段时间内宏观上有多个程序同时运行,这在但CPU系统中,每一时刻只能有一道程序执行,即微观上这些程序是分时的交替运行,只不过是给人的感觉是在同时运行,其实是因为分时交替运行的事件是非常短的。(可能只有几十微秒)

而在多个CPU系统中,则这些可以并发执行的程序便可以分配到多个处理器上,实现多任务并行执行,即利用每个处理器来处理一个可以并发程序,这样多个程序便可以同时执行,目前电脑市场上说的多核CPU,便是多核处理器,核越多,并行处理的程序越多,能大大的提高电脑运行的效率。

:popcorn: 线程调度:单核处理器的计算机肯定是不饿能并行的处理多个任务的,只能是多个任务在单个CPU上并发运行。同理,线程也是一样的,从宏观角度上看线程是并行的,但是从微观角度上看确实串行的,当系统只有一个CPU时,线程会以某种顺序执行多个线程,我们把这种情况称之为线程调度。

2 进程与线程

:hot_pepper: 进程 :是指一个内存中运行的应用程序,每个进程都有一个独立的内存空间,一个应用程序可以同时运行多个进程,进程也是程序的一次执行过程,也是系统运行程序的基本单位;系统运行一个程序是一个进程从创建、运行到消亡的过程。

我们打开任务管理器就可以看到系统中正在运行的各个进程。

image-20220723210033281

:hotdog: 线程:线程是进程中的一个执行单元,负责当前进程中程序的执行,一个进程中至少有一个线程。一个进程中是可以有多个线程的,这个应用程序也称之为多线程程序。一个程序运行后至少有一个进程,一个进程中可以包含多个线程。

:flight_arrival: 线程调度

  • 分时调度:所有线程轮流使用CPU,平均分配每个线程占用CPU的时间
  • 抢占式调度:优先让优先级高的线程使用CPU,如果线程的优先级相同,那么会随机选择一个,Java语言中使用的是抢占式调度。

    • 设置线程的优先级

      我们可以去任务管理器中设置自己的优先级

      image-20220723212026108

    • 抢占式调度详解

      大部分操作系统都支持多进程并发运行,现在的操作系统几乎都直指同时运行多个程序,比如:同时使用QQ、微信、浏览器等等。此时,这些进程是在同时运行,但我们感觉这些软件好像在同一时刻运行着。

      实际上不是这样,CPU使用抢占式调度模式在多个线程间进行着告诉的切换,对于CPU的一个核而言,某个时刻,只能执行一个线程,而CPU在多个线程间切换速度非常快,看上去就像是在同一个时刻运行。其实,多线程程序并不能提高程序的运行速度,但能够提高程序运行效率,让CPU使用率更高。

      image-20220723213615072

3.Java 创建线程类

:star: Java 使用 java.lang.Thread类代表线程,所有的线程对象都必须是Thread类或其子类的实例。每个线程的作用是完成一定的任务,实际上就是执行一段程序流即一段顺序执行的代码。Java使用线程执行体来代表这段程序流。

Java中通过继承Thread类来创建并启动多线程的步骤如下:

  1. 定义Thread类的子类,并重写改类的run方法,改run方法的方法体就代表了线程需要完成的任务,因此把run方法称为线程执行体。
  2. 创建Thread子类的实例,即创建了线程对象
  3. 调用线程对象的start()方法来启动该线程

自定义继承类

public class MyThread extends Thread {
    // 定义指定线程名称的构造方法
    public MyThread(String name) {
        // 调用父类的String参数的构造方法,指定线程的名称
        super(name);
    }

    // 重写run()方法,完成该线程执行的逻辑
    @Override
    public void run() {
        for (int i = 0; i < 10; i++) {
            System.out.println(getName() + ": 正在执行!" + i);
        }
    }
}

测试类

public class Demo01 {
    public static void main(String[] args) {
        MyThread myThread = new MyThread("小新");
        myThread.start();
    }
}

测试结果

image-20220723214714670

4.多线程原理

我们改写一些刚才的测试类,在测试类中加入main线程中的代码

public class Demo01 {
    public static void main(String[] args) {
        System.out.println("这里时Main线程");
        MyThread myThread = new MyThread("小新");
        myThread.start();
        for (int i = 0; i < 100; i++) {
            System.out.println("Main + " + i);
        }
    }
}

我们就会得到这样的结果

image-20220912104745936

我们可以发现,这两个线程是交替执行的,谁抢到处理机谁执行。

程序启动main时候,java虚拟机启动一个进程,主线程在main()调用时被创建。随着调用myThread的对象的start方法,另外一个新的进程也启动了,这样,整个应用就在多线程下运行。

流程图如下所示:

image-20220912105138214

我们可以看到多线程的执行流程,那么为什么可以完成并发执行呢?我们来讲一下原理:

多线程之u行时,在占内存中,其实每一个执行线程都有一篇自己所属的栈内存空间,进行方法的压栈和弹栈。

image-20220912110003443

当执行线程的任务结束了,线程自动在栈内存中释放了。但是当所有的执行线程都结束了,那么进程就结束了。

5 .Thread类

java.lang.Thread类的方法

构造方法

  • public Thread():分配一个新的线程对象
  • public Thread(String name):分配一个指定名字的新的线程对象
  • public Thread(Runnable target):分配一个带有指定目标新的线程对象。
  • public Thread(Runnable target,String name):分配一个带有指定目标新的线程对象并指定名字。

常用方法

  • public String getName():获取当前线程名称。
  • public void start():导致此线程开始执行,java虚拟机调用此线程的run方法。
  • public void run():此线程要执行的任务在此处定义代码
  • public static void sleep(long millis):使当前正在执行的线程以指定的毫秒数暂停(暂时停止执行)。
  • public static Thread currentThread():返回对当前正在执行的线程对象的引用。

6 创建线程方式二

采用 java.lang.Runnable 也是非常常见的一种,我们只需要重写run方法即可。 步骤如下:

  1. 定义Runnable接口的实现类,并重写该接口的run()方法,该run()方法的方法体同样是该线程的线程执行体。
  2. 创建Runnable实现类的实例,并以此实例作为Thread的target来创建Thread对象,该Thread对象才是真正 的线程对象。
  3. 调用线程对象的start()方法来启动线程。
public class MyRunnable implements Runnable {
    @Override
    public void run() {
        for (int i = 0; i < 20; i++) {
            System.out.println(Thread.currentThread().getName() + " " + i);
        }
    }
}
public class demo02 {
    public static void main(String[] args) {
        // 创建自定义对象 线程任务对象
        MyRunnable myRunnable = new MyRunnable();
        // 创建线程对象
        Thread thread = new Thread(myRunnable, "小强");
        thread.start();
        for (int i = 0; i < 20; i++) {
            System.out.println("旺财 " + i);
        }
    }
}

通过实现Runnable接口,使得该类有了多线程类的特征。run()方法是多线程程序的一个执行目标。所有的多线程代码都在run方法里。Thread类实际上也是实现了Runnable接口的类。

在启动的多线程的时候,需要先通过Thread类的构造方法Thrad(Runnable target)构造出对象,然后调用Thread对象的start方法里面。Thread类实际上也是实现了Runnable接口的类。

在启动的多线程的时候,需要先通过Thread类的构造方法Thread(Runnable target)构造出对象,然后调用Thread对象的start()方法来运行多线程代码。

实际上所有的多线程代码都是通过运行Thread的start()方法来运行的。因此,不管是几次Thread类还是实现Runnable接口来实现多线程,最终还是通过Thread的对象的API来控制线程的,数学Thread类的API是进行多线程编程的基础。

Runnable对象仅仅作为Thread对象的target,Runnable实现类里包含的run()方法仅作为线程执行体。 而实际的线程对象依然是Thread实例,只是该Thread线程负责执行其target的run()方法。

7 Thread和Runnable区别

如果一个类继承Thread,则不适合资源共享,但是如果实现Runnable接口的话,则很容易的实现资源共享。

实现Runnable接口比继承Thread类所具有的游戏:

  1. 适合多个相同的程序代码的线程去共享同一个资源
  2. 可以避免java中的单继承的局限性
  3. 增加程序的健壮性,实现解耦操作,代码可以被多个线程共享,代码和线程独立
  4. 线程池只能放入实现Runnable或Callable类基础,不饿能直接放入基础Thread的累。

在java中,每次程序运行至少启动2个线程。一个是main线程,一个是垃圾收集线程。因为每当使用 java命令执行一个类的时候,实际上都会启动一个JVM,每一个JVM其实在就是在操作系统中启动了一个进 程。

使用匿名内部类方式实现线程的创建

使用线程的内匿名内部类方式,可以方便的实现每个线程执行不同的线程任务操作。 使用匿名内部类的方式实现Runnable接口,重新Runnable接口中的run方法:

public class demo03 {
    public static void main(String[] args) {
        Runnable r = new Runnable() {
            @Override
            public void run() {
                for (int i = 0; i < 20; i++)
                    System.out.println(i);
            }
        };
        new Thread(r).start();

        new Thread(new Runnable() {
            @Override
            public void run() {
                for (int i = 0; i < 20; i++)
                    System.out.println(i);
            }
        }).start();
    }
}

第二章:线程安全

1线程安全

线程安全:如果有多个线程在同时运行,而这些线程可能会同时运行这段代码。程序每次运行结果和单线程运行的结果是一样的,而其他的变量的值也是和预期的是一样的,就是线程安全的。

通过一个案例,演示线程的安全问题:

电影院要买票,我们模拟电影院的买票过程,假设当前总共有一百个作为,有多个售票窗口,这几个窗口同时卖票。

窗口用线程对象来模拟,票用Runnable接口子类来模拟。

模拟票

public class Ticket implements Runnable{
    private int ticket=100;
    /**
     * 执行卖票操作
     */
    @Override
    public void run() {
        //每个窗口卖票的操作
        // 窗口永远开启
        while(true)
        {
            if(ticket>0)// 还有票可以卖
            {
                // 出票操作
                // 使用sleep模拟一下出票时间
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                // 获取当前线程对象的名字
                System.out.println(Thread.currentThread().getName()+"正在卖:"+ticket--);
            }
        }
    }
}

模拟窗口

public class demo04 {
    public static void main(String[] args) {
        // 创建线程任务对象
        Runnable r = new Ticket();
        new Thread(r,"窗口1").start();
        new Thread(r,"窗口2").start();
        new Thread(r,"窗口3").start();
    }
}

模拟结果

image-20220912153319756

我们发现程序出现了两个问题:

  1. 相同的票数被卖了两回
  2. 出现了不存在的票,比如0和-1

这种问题,几个窗口票数不同步了,这种问题称为线程不安全。

出现这种问题的原因:

每一个线程卖票需要两种操作:

  1. 读票
  2. 票数减1

当某一个窗口执行完操作1正要执行操作2的时候,突然被别的线程抢占了处理机,导致当前线程处于就绪态。抢占的线程执行操作1票数就和之前那个线程读的票数一样,然后讲票数减1,这个线程处理完之后,原来那个线程抢占到了处理机,并把票数1减一,相当于一张票减了2次。所以造成了冲突问题,也就是线程不安全问题。

线程安全问题都是由全局变量及静态变量引起的。若每个线程中对全局变量、静态变量只有读操作,而无写 操作,一般来说,这个全局变量是线程安全的;若有多个线程同时执行写操作,一般都需要考虑线程同步, 否则的话就可能影响线程安全。

2.线程同步

当我们使用多个线程访问统一资源的时候,且多个线程对资源有写的操作,就容易出现线程安全问题。

要解决上诉多线程并发访问一个资源的安全性问题,也就是解决重复票和不存在票的问题,java中提供了同步机制(synchronized)来解决。

为了解决线程不安全问题,每一个线程要执行操作的时候,其他线程必须等待,直到当前线程执行完后才能进入。

窗口1线程进入操作的时候,窗口2和窗口3线程只能在外等着,窗口1操作结束,窗口1和窗口2和窗口3才有机会进入代码 去执行。也就是说在某个线程修改共享资源的时候,其他线程不能修改该资源,等待修改完毕同步之后,才能去抢夺CPU 资源,完成对应的操作,保证了数据的同步性,解决了线程不安全的现象。

实现同步的三种操作

同步代码块

同步代码块synchronized关键字可以用于方法中的某个区块中,表示只对这个区块的资源实现互斥访问。

格式:

synchronized(同步锁){
    需要同步的代码
}

同步锁:对象的同步锁只是一个概念,可以想象为在对象上标记了一个锁。

  • 锁对象可以是任意类型
  • 多个线程对象要使用同一把锁。

在任何时候,最多允许一个线程拥有同步锁,谁拿到锁就进入代码块,其他的线程只能在外等着。(阻塞)

代码

public class Ticket implements Runnable {
    private int ticket = 100;
    Object lock = new Object();

    /**
     * 执行卖票操作
     */
    @Override
    public void run() {
        //每个窗口卖票的操作
        // 窗口永远开启
        while (true) {
            synchronized (lock) {
                if (ticket > 0)// 还有票可以卖
                {
                    // 出票操作
                    // 使用sleep模拟一下出票时间
                    try {
                        Thread.sleep(100);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    // 获取当前线程对象的名字
                    System.out.println(Thread.currentThread().getName() + "正在卖:" + ticket--);
                }
            }

        }
    }
}

加入了同步锁之后就没有了线程不安全问题。

image-20220912155325055

同步方法

同步方法:使用synchronized修饰的方法,就叫做同步方法,保证A线程执行该方法的时候,其他线程只能在方法外 等着。

格式:

public synchronized void method()
{
    可能会产生线程安全问题的代码
}

同步锁是谁

  • 对于非static方法,同步锁就是this.
  • 对于static方法,我们使用当前方法所在类的字节码对象(类名.class).

代码

public class Ticket implements Runnable {
    private int ticket = 100;
    Object lock = new Object();

    /**
     * 执行卖票操作
     */
    @Override
    public void run() {
        //每个窗口卖票的操作
        // 窗口永远开启
        while (true) {
            sellTicket();
        }
    }
    // 锁对象是谁调用了这个方法就是谁
    // 这里隐含的锁对象就是this也就是创建Ticker类的这个对象
    public synchronized void sellTicket() {
        if (ticket > 0)// 还有票可以卖
        {
            // 出票操作
            // 使用sleep模拟一下出票时间
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            // 获取当前线程对象的名字
            System.out.println(Thread.currentThread().getName() + "正在卖:" + ticket--);
        }
    }
}

Lock锁

java.util.concurrent.locks.Lock 机制提供了比synchronized代码块和synchronized方法更广泛的锁定操作, 同步代码块/同步方法具有的功能Lock都有,除此之外更强大,更体现面向对象。

Lock锁也称为同步锁,加锁与释放锁方法话了,如下:

  • public void lock():加同步锁
  • public void unlock():释放同步锁
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

public class Ticket implements Runnable {
    private int ticket = 100;
    Lock lock = new ReentrantLock();

    /**
     * 执行卖票操作
     */
    @Override
    public void run() {
        //每个窗口卖票的操作
        // 窗口永远开启
        while (true) {
            lock.lock();
            if (ticket > 0)// 还有票可以卖
            {
                // 出票操作
                // 使用sleep模拟一下出票时间
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                // 获取当前线程对象的名字
                System.out.println(Thread.currentThread().getName() + "正在卖:" + ticket--);
            }
            lock.unlock();
        }
    }
}
相关文章
|
7天前
|
Java 开发者
Java多线程编程中的常见误区与最佳实践####
本文深入剖析了Java多线程编程中开发者常遇到的几个典型误区,如对`start()`与`run()`方法的混淆使用、忽视线程安全问题、错误处理未同步的共享变量等,并针对这些问题提出了具体的解决方案和最佳实践。通过实例代码对比,直观展示了正确与错误的实现方式,旨在帮助读者构建更加健壮、高效的多线程应用程序。 ####
|
6天前
|
安全 Java 开发者
Java 多线程并发控制:深入理解与实战应用
《Java多线程并发控制:深入理解与实战应用》一书详细解析了Java多线程编程的核心概念、并发控制技术及其实战技巧,适合Java开发者深入学习和实践参考。
|
6天前
|
Java 开发者
Java多线程编程的艺术与实践####
本文深入探讨了Java多线程编程的核心概念、应用场景及实践技巧。不同于传统的技术文档,本文以实战为导向,通过生动的实例和详尽的代码解析,引领读者领略多线程编程的魅力,掌握其在提升应用性能、优化资源利用方面的关键作用。无论你是Java初学者还是有一定经验的开发者,本文都将为你打开多线程编程的新视角。 ####
|
5天前
|
存储 安全 Java
Java多线程编程中的并发容器:深入解析与实战应用####
在本文中,我们将探讨Java多线程编程中的一个核心话题——并发容器。不同于传统单一线程环境下的数据结构,并发容器专为多线程场景设计,确保数据访问的线程安全性和高效性。我们将从基础概念出发,逐步深入到`java.util.concurrent`包下的核心并发容器实现,如`ConcurrentHashMap`、`CopyOnWriteArrayList`以及`BlockingQueue`等,通过实例代码演示其使用方法,并分析它们背后的设计原理与适用场景。无论你是Java并发编程的初学者还是希望深化理解的开发者,本文都将为你提供有价值的见解与实践指导。 --- ####
|
11天前
|
安全 Java 开发者
深入解读JAVA多线程:wait()、notify()、notifyAll()的奥秘
在Java多线程编程中,`wait()`、`notify()`和`notifyAll()`方法是实现线程间通信和同步的关键机制。这些方法定义在`java.lang.Object`类中,每个Java对象都可以作为线程间通信的媒介。本文将详细解析这三个方法的使用方法和最佳实践,帮助开发者更高效地进行多线程编程。 示例代码展示了如何在同步方法中使用这些方法,确保线程安全和高效的通信。
35 9
|
8天前
|
安全 Java 开发者
Java多线程编程中的常见问题与解决方案
本文深入探讨了Java多线程编程中常见的问题,包括线程安全问题、死锁、竞态条件等,并提供了相应的解决策略。文章首先介绍了多线程的基础知识,随后详细分析了每个问题的产生原因和典型场景,最后提出了实用的解决方案,旨在帮助开发者提高多线程程序的稳定性和性能。
|
14天前
|
存储 安全 Java
Java多线程编程的艺术:从基础到实践####
本文深入探讨了Java多线程编程的核心概念、应用场景及其实现方式,旨在帮助开发者理解并掌握多线程编程的基本技能。文章首先概述了多线程的重要性和常见挑战,随后详细介绍了Java中创建和管理线程的两种主要方式:继承Thread类与实现Runnable接口。通过实例代码,本文展示了如何正确启动、运行及同步线程,以及如何处理线程间的通信与协作问题。最后,文章总结了多线程编程的最佳实践,为读者在实际项目中应用多线程技术提供了宝贵的参考。 ####
|
11天前
|
监控 安全 Java
Java中的多线程编程:从入门到实践####
本文将深入浅出地探讨Java多线程编程的核心概念、应用场景及实践技巧。不同于传统的摘要形式,本文将以一个简短的代码示例作为开篇,直接展示多线程的魅力,随后再详细解析其背后的原理与实现方式,旨在帮助读者快速理解并掌握Java多线程编程的基本技能。 ```java // 简单的多线程示例:创建两个线程,分别打印不同的消息 public class SimpleMultithreading { public static void main(String[] args) { Thread thread1 = new Thread(() -> System.out.prin
|
14天前
|
Java
JAVA多线程通信:为何wait()与notify()如此重要?
在Java多线程编程中,`wait()` 和 `notify()/notifyAll()` 方法是实现线程间通信的核心机制。它们通过基于锁的方式,使线程在条件不满足时进入休眠状态,并在条件满足时被唤醒,从而确保数据一致性和同步。相比其他通信方式,如忙等待,这些方法更高效灵活。 示例代码展示了如何在生产者-消费者模型中使用这些方法实现线程间的协调和同步。
28 3
|
13天前
|
安全 Java
Java多线程集合类
本文介绍了Java中线程安全的问题及解决方案。通过示例代码展示了使用`CopyOnWriteArrayList`、`CopyOnWriteArraySet`和`ConcurrentHashMap`来解决多线程环境下集合操作的线程安全问题。这些类通过不同的机制确保了线程安全,提高了并发性能。
下一篇
无影云桌面