java并发编程 JUC-基础篇 快速入门

1.进程与线程的概念

(1)进程

程序有指令与数据组成,指令要运行,数据要读写,就必须指令加载到CPU。数据加载到内容,指令运行需要用到磁盘。

当一个程序被运行时,从磁盘加载这个程序的代码至内存,这时开启了一个进程。

(2)线程

一个进程之内可以分为一个或多个线程。

一个线程就是一个指令流,将指令流中的一条条指令以一定的顺序交给CPU执行。

Java中的 最小调度单位是线程,最小资源分配单位是进程。

2.并发与并行的概念

并发:同一时间应对多件事情的能力。

并行:同一时间动手做多件事情的能力。

3.异步与同步的概念

异步:不需要等待结果返回,就能继续运行

同步:需要等待结果返回,才能继续运行

同步在多线程中是让多个线程步调一致

4.多线程提高效率的结论

(1)单核CPU下,多线程不能实际提高程序运行效率,只是为了能够在不同任务之间切换,不同

线程轮流使用CPU,不至于一个线程总是占用一个CPU,别的线程没法干活。

(2)多核CPU下,可以并行运行多个线程,能否提高效率要分情况:

一些任务可以通过设计,将任务拆分,并行执行,可以提高运行效率。

不是所有任务都需要拆分,任务的目的不同。

(3)IO操作不占用CPU,只是我们一般拷贝文件使用的是 阻塞IO ,相当于线程虽然不用CPU,但是需要等待IO结束,没能充分利用线程,才有了 非阻塞IO异步IO优化

5.创建线程

5.1 Thread的方式

复制代码
        Thread thread = new Thread(){

            @Override
            public void run(){
                System.out.println("Hello JUC");
            }
        };

        thread.start();

5.2 Runnable的方式

复制代码
        Runnable runnable = new Runnable(){

            @Override
            public void run(){
                System.out.println("Hello JUC Runnable");
            }
        };

        Thread thread = new Thread(runnable);
        thread.start();

5.3 Lambda的方式

5.3.1 Thread的Lambda写法

复制代码
        Thread thread = new Thread(()->{ System.out.println("Hello JUC Lambda");});

        thread.start();

5.3.2 Runnable的Lambda写法

复制代码
        Runnable runnable = ()->{
            System.out.println("Hello JUC Lambda");
        };

        Thread thread = new Thread(runnable);
        thread.start();

5.4 FutureTaks 配合 Thread

FutureTasks 能够接收Callable类型的参数,用于·处理有返回结果的情况

复制代码
    @Test
    void testFutureTask() throws ExecutionException, InterruptedException {

        FutureTask<Integer> task = new FutureTask<>(new Callable<Integer>() {
            @Override
            public Integer call() throws Exception {

                System.out.println("Running ...");
                Thread.sleep(3000);
                return 100;
            }
        });

        Thread thread = new Thread(task);
        thread.start();

        System.out.println(task.get());
    }

6.线程运行

线程运行现象:线程运行交替执行。谁先谁后,不由我们所控制。

查询线程的方式:

(1)windows:

可以通过任务管理器查看进程和线程数

tasklilst 查看进程

taskkill 杀死进程

(2)linux:

ps -fe 查看所有进程

ps -fT -p <PID> 这将显示PID进程及其所有线程的详细信息。

kill 杀死进程

top 按大写H切换是否显示线程

top -H -p <PID> 查看某个进程(PID)

(3)Java:

jps 命令查看所有Java进程

jstack <PID> 查看某个Java进程(PID)的所有线程状态

jconsole 来查看某个Java进程中线程的运行情况

7.线程运行的原理

7.1栈帧

JVM是由堆、栈、方法区所组成的,其中栈内存 存放就是线程,每个线程启动后,虚拟机就会分配一块栈内存。

每个栈有多个栈帧,对应着每次方法调用时所占用的内存。

每个线程只有一个活动栈帧,对应着当前正在执行的那个方法。

7.2上下文切换(Thread Context Switch)

可能因为

  • 线程的cpu时间用完
  • 垃圾回收
  • 有更高优先级的线程需要运行
  • 线程自己调用了sleep、yield、wait、join、park、synchronized、lock等方法

当Thread Context Switch发送时,需要操作系统保存当前线程的状态,并恢复另一个线程的状态,Java中对应的概念就是程序计数器,它的作用记录下一条jvm指令的执行地址,是线程私有的。

状态包括程序计数器,虚拟机中每个栈帧的信息,如局部变量,操作数栈,返回地址等

Thread Context Switch频繁发生会影响性能

8.线程中的常见方法

|------------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------|
| 方法 | 描述 |
| start() | 启动新线程并执行该线程的run()方法。线程开始执行时,它的run()方法会被调用。 |
| run() | 当线程启动时,run()方法会被调用。这个方法应该包含线程的执行代码。 |
| join() | 等待调用join()方法的线程终止。例如,thread.join()会使当前线程等待thread线程终止。 |
| sleep(long millis) | 使当前线程暂停执行指定的毫秒数。线程不会失去任何监视器的所有权。 |
| interrupt() | 中断线程。如果线程在调用Object类的wait()wait(long)wait(long, int)join()join(long, int)或者sleep(long, int)方法时被阻塞,那么它的中断状态将被清除,并且它将接收到InterruptedException。 |
| isAlive() | 测试线程是否处于活动状态。如果线程已经启动且尚未终止,则线程处于活动状态。 |
| setName(String name) | 改变线程的名称,可以通过getName()方法获取线程的名称。 |
| getPriority() | 返回线程的优先级。线程的优先级可以设置为MIN_PRIORITY(1)、NORM_PRIORITY(5)或MAX_PRIORITY(10)。 |
| setPriority(int newPriority) | 变线程的优先级。线程优先级只是建议给调度器,实际调度可能会忽略它。 |
| yield() | 暂停当前正在执行的线程对象,并执行其他线程。 |
| getState() | 返回线程的状态。线程的状态可以是NEWRUNNABLEBLOCKEDWAITINGTIMED_WAITINGTERMINATED。 |
| isInterrupted() | 测试线程是否已经中断。不同于interrupted()方法,这个方法不会改变线程的中断状态。 |

8.1 run 与 start 的区别

(1)run() 方法:

  • run() 方法是线程的执行主体,它包含了线程要执行的任务。
  • 当线程被启动时,run() 方法会被自动调用。
  • run() 方法可以直接调用,就像调用普通方法一样,它在当前线程中执行,而不是在新线程中执行。
  • 如果直接调用 run() 方法,程序不会创建新的线程,而是在当前线程中顺序执行 run() 方法中的代码。

(2)start() 方法:

  • start() 方法用于启动一个新线程,并执行该线程的 run() 方法。
  • start() 方法被调用时,Java 虚拟机会创建一个新的线程,并执行 run() 方法中的代码。
  • start() 方法只能被调用一次,多次调用会抛出 IllegalThreadStateException
  • 调用 start() 方法后,线程可能会立即开始执行,也可能因为线程调度器的安排而在稍后执行。

8.2 Sleep和yield的区别

(3)Sleep

  • 调用sleep让当前线程从Running进入Timed Waiting(阻塞)状态
  • 其他线程可以通过interrupt方法打断正在睡眠的线程,这时sleep方法会抛出InterruptedException
  • 睡眠解释后的线程未必会立即执行
  • 建议使用Timeunit的sleep代替Thread的sleep,有更好的可读性

(4)yield

  • 调用yield 会使当前线程从Running进入Runnable就绪状态,然后调度执行其他线程
  • 具体的实现依赖操作系统的任务调度器
  • 在Java中,Thread.yield()也是一个静态方法,它使当前线程从运行状态转到可运行状态,但不会释放所占有的任何资源。
  • 通常,yield的使用并不频繁,因为它对线程调度提供的信息有限,且线程调度器可能会忽略这个提示。

8.3 线程优先级

  • 线程优先级会提示调度器优先调度线程
  • 如果CPU比较忙,那么优先级高的线程会获得更多的时间片;如果CPU比较空闲,优先级几乎没有什么作用。

8.4 Sleep实现

在没有利用CPU实现计算时,不要让while(true)空转浪费CPU,这时可以使用yield或sleep使CPU的使用权让给其他的程序

8.5 join实现

等待thread线程运行结束或终止。

如果join(long n)带参数的话,就是等待线程运行结束的最多等待n毫秒

8.6 打断阻塞

阻塞

打断sleep的线程,清空打断状态

示例:

复制代码
  @Test
    void test() throws ExecutionException, InterruptedException {

       Thread t1 = new Thread(()->{

           try{
               TimeUnit.SECONDS.sleep(12);
           }catch (Exception e){
               e.printStackTrace();
           }

       },"t1");

        t1.start();
        TimeUnit.SECONDS.sleep(2);
        t1.interrupt();
        System.out.println(t1.isInterrupted());

    }

注意: 使用 isInterrupted()来判断是否打断成功,对于打断sleep 的结果为false则表示打断成功,同理wait和join 都是false标记为打断成功。

8.7 打断正常

打断正常的则isInterrupted()为true则表示打断成功

示例:

复制代码
    @Test
    void test() throws  InterruptedException {

       Thread t1 = new Thread(()->{

           try{

              while (true){
                  boolean interrupted = Thread.currentThread().isInterrupted();
                  if (interrupted){
                      log.info("打斷...");
                      break;
                  }

              }
           }catch (Exception e){
               e.printStackTrace();
           }

       },"t1");

        t1.start();
        TimeUnit.SECONDS.sleep(3);
        log.info("interrupt");
        t1.interrupt();


    }

9.两阶段终止

问题:在一个线程T1中如何优雅的终止线程T2呢?

错误的思路:

  • 使用stop方法终止线程,这种如果此时线程锁住了公共享资源,将其杀死后就再也没有机会释放资源,导致其他线程永远无法获取锁。
  • 使用System.exit(int)方法停止线程,目的是仅停止一个线程,但是会让整个程序都停止

正确的思路:

复制代码
    @Test
    void test() throws  InterruptedException {
        Thread t1 = new Thread(()->{
           while (true){
               Thread current = Thread.currentThread();
               if(current.isInterrupted()){
                   log.info("打断....");
                   break;
               }
               try {
                   Thread.sleep(800);
                   log.info("正在记录中.....");
               }catch (InterruptedException e){
                   e.printStackTrace();
                   //重置设置打断标识,因为sleep过程中进行interrupt是标识false,那么需要正常的interrupt使其标识为true
                   current.interrupt();
               }

           }
        });

        t1.start();

        TimeUnit.SECONDS.sleep(3);
        t1.interrupt();

    }

10.打断park线程

打断线程后不会影响标记

11.不推荐的方法

这些方法过时,容易破坏同步代码块,造成线程死锁

|-----------|------------|
| 方法名 | 说明 |
| stop() | 停止线程运行 |
| suspend() | 挂起(暂停)线程运行 |
| resume() | 恢复线程运行 |

12.主线程与守护线程

默认情况下,Java进程需要等待线程运行都结束,才会结束。有一种特殊的线程是守护线程,只要其他的线程运行结束了,即使守护线程的代码还没有执行结束,也会强制结束。

示例:设置t1线程为守护线程:

复制代码
t1.setDaemon(true);

运用:

  • 垃圾回收器线程就是一种守护线程
  • 接收shutdown命令后,不会等待Acceptor和Poller守护线程处理完当前请求

13.线程的五种状态

五种状态:初始状态、可运行状态、运行状态、终止状态、阻塞状态

  • 初始状态:仅在语言层面创建了线程对象,还未与操作系统线程关联
  • 可运行状态;也称(就绪状态),指线程已经被创建(与操作系统线程未关联),可以由CPU调度执行
  • 运行状态:指获取CPU时间片运行中的状态
  • 阻塞状态:如果调用了阻塞API,这时线程实际不会用到CPU,会导致线程上下文切换,进入阻塞状态
  • 终止状态:表示线程已经执行完毕,生命周期已经结束,不会转换为其他状态

14.共享资源的线程安全(synchronized)

竞态条件:发生在至少两个线程竞争同一资源时,而最终的结果取决于这些线程的执行顺序。在大多数情况下,竞态条件的出现是由于程序设计上的缺陷,例如没有适当的同步机制来控制对共享资源的访问。

为了避免临界的竞态的条件发生,可以使用以下的手段实现:

  • 阻塞式的解决方案:Synchronized,Lock
  • 非阻塞式的解决方案:原子变量

使用阻塞式的解决方案:Synchronized(对象锁),采用互斥的方式让同一时刻至多只有一个线程能持有 对象锁 ,其他线程再想获取这个 对象锁就会被阻塞住。这样可以保证线程可以安全的执行临界区内的代码,不用担心线程的上下文切换

  • 互斥是保证临界区的竞态条件发生,同一时刻只有一个线程执行临界区代码
  • 同步是由于线程执行的先后,顺序不同,需要一个线程等待其他线程运行到某个点

示例代码:

复制代码
    int counter = 0;

    Object lock = new Object();

    @Test
    void test() throws  InterruptedException {
        Thread t1 = new Thread(()->{
            for (int i = 0;i<=50000;i++){
                synchronized(lock){
                    counter++;
                }
            }

        });

        Thread t2 = new Thread(()->{
            for (int i = 0;i<=50000;i++){
                synchronized(lock){
                    counter--;
                }

            }

        });

        t1.setName("t1");
        t2.setName("t2");
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        log.info("Counter:{}",counter);

    }

synchronized用对象锁保证了临界区内代码的原子性,临界区的代码对外不可分割,不会被线程切换所打断

思考:

如果synchronized(obj)放在for循环外面,如何理解? --原子性

如果 t1 synchronized(obj1) 而 t2 synchronized(obj2) 会怎么运行?--锁对象

如果 t1 synchronized(obj)而 t2没有加会怎么样?--锁对象

14.1 通过面向对象的方式实现:

创建实体:

复制代码
public class Room {

    private int counter = 0;

    public void increment(){
        synchronized (this){
            counter++;
        }
    }

    public void decrement(){
        synchronized (this){
            counter--;
        }
    }

    public int getCounter(){
        synchronized (this){
            return counter;
        }
    }
}

实现代码:

复制代码
    Room room = new Room();

    @Test
    void test() throws  InterruptedException {
        Thread t1 = new Thread(()->{
            room.increment();

        });

        Thread t2 = new Thread(()->{
            room.decrement();

        });

        t1.setName("t1");
        t2.setName("t2");
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        log.info("Counter:{}",room.getCounter());

    }

14.2 synchronized加在方法上

(1)

复制代码
public class Test {
    public synchronized void test(){
        
    }
    
}

相当于:

复制代码
public class Test {
    public  void test(){
        synchronized(this){
            
        }

    }

}

(2)static方法

复制代码
public class Test {
    public synchronized static void test(){
        
    }
    
}

相当于:

复制代码
public class Test {
    public  static void test(){
        synchronized(Test.class){
            
        }
    }

}

15.变量线程安全分析

15.1 成员变量和静态变量是否线程安全?

(1)如果没有共享,则线程安全

(2)如果共享,根据他们的状态是否能够改变,分为两种情况:

  • 如果只有读操作,则线程安全
  • 如果有读写操作,则这段代码是临界区,需要考虑线程安全

15.2 局部变量是否线程安全?

(1)局部变量是线程安全的

(2)但是局部变量引用的对象则未必

  • 如果该对象没有逃离方法的作用访问,它是线程安全的
  • 如果该对象逃离方法的作用范围,则需要考虑线程安全

16.常用的线程安全类

  • String
  • Integer
  • StringBuffer
  • Random
  • Vector
  • Hashtable
  • java.utils.concurrent包下的类

这里的线程安全是多线程调用它们同一个实例的某个方法时,线程是安全的。可以理解为

  • 它们的每个方法是原子
  • 但是它们的多个方法的组合不是原子的(不是线程安全的)

不可改变线程安全

String、Integer等都是不可变类,因为其内部的状态不可改变,因此它们的方法都是线程安全的

虽然String有replace、substring等方法可以改变值、但是根据源码可知都是创新一个新的对象,没有改变原有的对象的值,所以线程安全

17.买票问题习题

(1)线程安全问题示例

复制代码
package org.example;

import java.util.ArrayList;
import java.util.List;
import java.util.Random;
import java.util.Vector;

// 主类,用于执行售票模拟
public class ExerciseSell {

    // 执行售票模拟的入口点
    public static void main(String[] args) throws InterruptedException {
        // 创建一个 TicketWindow 实例,初始票数为1000
        TicketWindow ticket = new TicketWindow(1000);

        // 创建一个线程列表,用于存放即将创建的线程
        List<Thread> threadList = new ArrayList<>();

        // 创建一个Integer类型的列表,用于存放每个线程卖出的票数
        List<Integer> amountList = new Vector<>();

        // 创建80000个线程,每个线程将模拟卖出一定数量的票
        for(int i = 0; i < 80000; i++) {
            Thread thread = new Thread(() -> {
                // 每个线程卖出的票数是随机生成的
                int amount = ticket.sell(randomAmount());
                try {
                    // 模拟售票操作后的延迟
                    Thread.sleep(10);
                } catch (InterruptedException e) {
                    // 如果线程在睡眠中被中断,抛出运行时异常
                    throw new RuntimeException(e);
                }
                // 将卖出的票数添加到amountList中
                amountList.add(amount);
            });
            // 将新创建的线程添加到线程列表中
            threadList.add(thread);
            // 启动线程
            thread.start();
        }

        // 等待所有线程完成
        for (Thread thread : threadList) {
            thread.join();
        }

        // 打印剩余票数
        System.out.println("余票:" + ticket.getAmount());
        // 计算所有线程卖出的票数总和并打印
        System.out.println("卖出的票数:" + amountList.stream().mapToInt(i -> i).sum());
    }

    // 生成随机售票数的方法
    static Random random = new Random();

    public static int randomAmount() {
        // 返回1到5之间的随机整数,包括1和5
        return random.nextInt(5) + 1;
    }
}

// 表示售票窗口的类
class TicketWindow {
    // 私有属性,表示售票窗口的票数
    private int amount;

    // 构造函数,初始化票数为传入的参数
    public TicketWindow(int amount) {
        this.amount = amount;
    }

    // 获取当前票数的方法
    public int getAmount() {
        return amount;
    }

    // 售票方法,尝试从窗口卖出指定数量的票
    public int sell(int amount) {
        if (this.amount >= amount) {
            // 如果票数足够,减少票数并返回卖出的票数
            this.amount -= amount;
            return amount;
        } else {
            // 如果票数不足,返回0
            return 0;
        }
    }
}

(2)解决线程安全问题方法

这段代码存在线程安全问题。具体来说,TicketWindow 类的 amount 成员变量在被多个线程访问和修改时,没有使用任何同步机制,这可能导致多个线程同时修改 amount,从而引发数据不一致的问题。

所以要给amount 进行共享资源读写的时进行加锁操作(这段代码,只需要对sell方法加上synchronized即可加锁)

复制代码
 public synchronized int sell(int amount){
        if(this.amount >= amount){
            this.amount -= amount;
            return amount;
        }else {
            return 0;
        }

    }

18. 转账习题

(1)线程安全问题示例

复制代码
package org.example;

import java.util.Random;

public class ExerciseTransfer {

    public static void main(String[] args) throws InterruptedException {

        Amount a = new Amount(1000);
        Amount b = new Amount(1000);
        Thread t1 = new Thread(()->{
            for(int i = 0;i < 100;i++){
                a.transfer(b,randomAmount());
            }

        });

        Thread t2 = new Thread(()->{
            for(int i = 0;i < 100;i++){
                b.transfer(a,randomAmount());
            }

        });

        t1.start();
        t2.start();
        t1.join();
        t2.join();
        System.out.println("总金额:"+a.getMoney() + b.getMoney());
    }

    static Random random = new Random();

    public static int randomAmount(){
        return random.nextInt(100) + 1;
    }


}

class Amount{
    private int money;

    public int getMoney() {
        return money;
    }

    public void setMoney(int money) {
        this.money = money;
    }

    public Amount(int money) {
        this.money = money;
    }

    public void transfer(Amount amount,int transferMoney){
        if(this.money >= transferMoney){
            this.setMoney(this.getMoney() - transferMoney);
            amount.setMoney(amount.getMoney() + transferMoney);
        }
    }
}

(2)解决线程安全问题方法(两个共享变量)

复制代码
    public void transfer(Amount amount,int transferMoney){
        synchronized(Amount.class){
            if(this.money >= transferMoney){
                this.setMoney(this.getMoney() - transferMoney);
                amount.setMoney(amount.getMoney() + transferMoney);
            }
        }

    }

因为如果

复制代码
public synchronized  void transfer(Amount amount,int transferMoney){
        if(this.money >= transferMoney){
            this.setMoney(this.getMoney() - transferMoney);
            amount.setMoney(amount.getMoney() + transferMoney);
        }

    }

相当于保护this.money,但是不保护amount的money

复制代码
    public void transfer(Amount amount,int transferMoney){
        synchronized(this){
            if(this.money >= transferMoney){
                this.setMoney(this.getMoney() - transferMoney);
                amount.setMoney(amount.getMoney() + transferMoney);
            }
        }

    }
相关推荐
腥臭腐朽的日子熠熠生辉18 分钟前
解决maven失效问题(现象:maven中只有jdk的工具包,没有springboot的包)
java·spring boot·maven
ejinxian19 分钟前
Spring AI Alibaba 快速开发生成式 Java AI 应用
java·人工智能·spring
杉之25 分钟前
SpringBlade 数据库字段的自动填充
java·笔记·学习·spring·tomcat
圈圈编码1 小时前
Spring Task 定时任务
java·前端·spring
俏布斯1 小时前
算法日常记录
java·算法·leetcode
27669582921 小时前
美团民宿 mtgsig 小程序 mtgsig1.2 分析
java·python·小程序·美团·mtgsig·mtgsig1.2·美团民宿
爱的叹息1 小时前
Java 连接 Redis 的驱动(Jedis、Lettuce、Redisson、Spring Data Redis)分类及对比
java·redis·spring
程序猿chen1 小时前
《JVM考古现场(十五):熵火燎原——从量子递归到热寂晶壁的代码涅槃》
java·jvm·git·后端·java-ee·区块链·量子计算
松韬2 小时前
Spring + Redisson:从 0 到 1 搭建高可用分布式缓存系统
java·redis·分布式·spring·缓存
绝顶少年2 小时前
Spring Boot 注解:深度解析与应用场景
java·spring boot·后端