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);
            }
        }

    }
相关推荐
大数据编程之光12 分钟前
Flink Standalone集群模式安装部署全攻略
java·大数据·开发语言·面试·flink
爪哇学长25 分钟前
双指针算法详解:原理、应用场景及代码示例
java·数据结构·算法
ExiFengs29 分钟前
实际项目Java1.8流处理, Optional常见用法
java·开发语言·spring
paj12345678931 分钟前
JDK1.8新增特性
java·开发语言
繁依Fanyi42 分钟前
简易安卓句分器实现
java·服务器·开发语言·算法·eclipse
慧都小妮子1 小时前
Spire.PDF for .NET【页面设置】演示:打开 PDF 时自动显示书签或缩略图
java·pdf·.net
m51271 小时前
LinuxC语言
java·服务器·前端
IU宝1 小时前
C/C++内存管理
java·c语言·c++
瓜牛_gn1 小时前
依赖注入注解
java·后端·spring
hakesashou1 小时前
Python中常用的函数介绍
java·网络·python