多线程、单例模式

目录

一、线程的创建方式

1、继承Thread类,重写run()方法

java 复制代码
public class Demo01 {
    public static void main(String[] args) {
        // 初始化自定义的线程
        MyThread01 myThread01 = new MyThread01();
        // 运行这个线程
        myThread01.start();

    }
}

// 自定义一个线程类,继承JDK中的Thread类
class MyThread01 extends Thread {

    // 定义线程的任务
    @Override
    public void run() {
        // 可以不停的处理任务
        while (true) {
            System.out.println("hello my thread...");
        }

    }
}

2、实现Runnable接口,重写run()方法

java 复制代码
public class Demo02 {
    public static void main(String[] args) {
        Thread02 thread02 = new Thread02();
        Thread thread = new Thread(thread02);
        
        thread.start();
    }
}

class Thread02 implements Runnable {
    @Override
    public void run() {
        while (true) {
            System.out.println("实现Runnable接口,创建多线程...");
        }
    }
}

3、通过匿名内部类的方式,创建Thread的子类

java 复制代码
public class Demo03 {
    public static void main(String[] args) {
        Thread thread = new Thread() {
            @Override
            public void run() {
                while (true) {
                    System.out.println("hello my thread...");
                }
            }
        };
        thread.start();
    }
}  

4、通过匿名内部类的方式,创建Runnable的子类

java 复制代码
public class Demo04 {
    public static void main(String[] args) {
        Thread thread = new Thread(new Runnable () {
            @Override
            public void run() {
                while (true) {
                    System.out.println("hello my thread...");
                }
            }
        });
        thread.start();
    }
}

5、通过Lambda表达式创建多线程

java 复制代码
public class Demo05 {
    public static void main(String[] args) {
        Thread thread = new Thread(()->{
            while (true) {
                System.out.println("hello my thread...");
            }
        });
        thread.start();
    }
}

如果在Lambda表达式中使用局部变量会触发变量捕获

原理:

解决方法:把这个局部变量变成全局变量(可看下面的代码,有涉及到)

二、线程中断

1、自定义一个标志位,通过修改这个标志位,通知线程中断

java 复制代码
public class Demo01 {
    static boolean isQuit = false;
    public static void main(String[] args) {
        Thread thread = new Thread(()-> {
            while (!isQuit) {
                System.out.println("hello my thread...");
            }
        });
        thread.start();
    }
}

2、调用interrupt()方法通知

java 复制代码
public class Demo03 {
    public static void main(String[] args) throws InterruptedException {
        Thread thread = new Thread(() -> {
            // 通过线程对象内部维护的中断标识,判断当前线程是否需要中断
            while (!Thread.currentThread().isInterrupted()) {
                // 线程中具体的任务是打印一句话
                System.out.println("hello thread...");

                // 线程大部分时间在sleep
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            System.out.println("线程已退出");
        });

        System.out.println("线程是否存活:" + thread.isAlive());
        // 启动线程
        thread.start();
        // 休眠一会
        Thread.sleep(1000);
        System.out.println("线程是否存活:" + thread.isAlive());
        // 中断线程,发出中断信号
        thread.interrupt();
        // 等待线程销毁
        Thread.sleep(100);
        // 查看是否存活
        System.out.println("线程是否存活:" + thread.isAlive());
    }
}

运行结果中hello thread...会一直打印下去,原因是因为子线程大部分在休眠,这就导致thread.interrupt()让子线程从sleep中醒了过来,抛出InterruptedException。而JVM在抛出异常时,会自动清除中断标志位(也就是设置为false),这样就使!Thread.currentThread().isInterrupted()变成了true,并且catch 块里只打印了异常,没有做任何退出操作;这就导致会一直循环下去。

所以,"一直打印" 的根本原因是:中断标志位被清除 + 异常处理没有退出循环。

想要避抛出异常,可以像下面这样修改代码:

java 复制代码
package com.example.class02;

public class Demo02 {
    public static void main(String[] args) throws InterruptedException {
        Thread thread = new Thread(() -> {
            // 通过线程对象内部维护的中断标识,判断当前线程是否需要中断
            while (!Thread.currentThread().isInterrupted()) {
                // 线程中具体的任务是打印一句话
                System.out.println("hello thread...");

                // 线程大部分时间在sleep
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                    System.out.println("休眠被中断");
                    // 处理中断逻辑
                    break;
                }
            }
            System.out.println("线程已退出");
        });

        System.out.println("线程是否存活:" + thread.isAlive());
        // 启动线程
        thread.start();
        // 休眠一会
        Thread.sleep(1000);
        System.out.println("线程是否存活:" + thread.isAlive());
        // 中断线程,发出中断信号
        thread.interrupt();
        // 等待线程销毁
        Thread.sleep(100);
        // 查看是否存活
        System.out.println("线程是否存活:" + thread.isAlive());
    }
}

三、synchronized、volatile、wait、notify

1、关于synchronized

1、被synchronized修饰的代码会变成串行执行。

2、synchronized可以修饰方法,也可以修饰代码块(有关共享变量的)。

3、被synchronized修饰的代码并不是一次性在CPU上执行完,而是中途可能会被CPU调度走。

4、只给一个线程加锁,也会出现线程安全问题。

2、volitile

多个线程之间涉及的共享变量,如果存在修改的逻辑,就要加volatile

3、wait、notify

1、当一个线程调用了wait之后,就是放掉当前持有的锁,等待被其他线程唤醒

2、当另一个线程调用了notify后,之前调用了wait的线程被唤醒,需要重新去竞争锁(有可能会没竞争到,那就继续等待),拿到锁之后,会从wait的位置继续执行逻辑。

小结:

1、wait和notify必须搭配synchronized一起使用

2、wait和notify使用的锁对象必须是同一个

3、notify执行多少次都没有关系(即使没有线程再wait)

四、单例模式

饿汉模式

java 复制代码
public class Singleton {
    //定义一个类的成员变量,用static修饰,保证全局唯一
    private static Singleton singleton = new Singleton();

    //构造方法私有化
    private Singleton() {}

    //提供一个公开的方法返回instance对象
    public static Singleton getInstance() {
        return singleton;
    }
}

懒汉模式

单例类不一定在程序启动时就初始化,为了节省计算机资源,提升程序启动速度,可以在实际使用的时候再new 也就是延时加载

java 复制代码
public class Singleton {
    //定义一个类的成员变量,用static修饰,保证全局唯一
    private static Singleton singleton = null;

    //构造方法私有化
    private Singleton() {}

    //提供一个公开的方法返回instance对象
    public static Singleton getInstance() {
        if(singleton == null) {
            singleton = new Singleton();
        }
        return singleton;
    }
}

双重检查锁DCL

java 复制代码
public class SingletonDCL {
    private static volatile SingletonDCL singletonDCL = null;

    private SingletonDCL () {}

    public static SingletonDCL getInstance() {
        // 非同步判空:规避DCL模式下的锁竞争,提升高并发场景性能
        if (singletonDCL == null) {
            synchronized (SingletonDCL.class) {
                if (singletonDCL == null) {
                    singletonDCL = new SingletonDCL();
                }
            }
        }
        return singletonDCL;
    }
}

五、锁策略

1、乐观锁 vs 悲观锁

乐观锁:在执行任务之前预期竞争不激烈,那就先不加锁,等到竞争激烈的时候,再加锁

悲观锁:在执行任务之前预期竞争激烈,必须先加锁再执行任务

2、轻量级锁 vs 重量级锁

轻量级锁:加锁的过程比较简单,用到的资源比较少,典型就是用户态的一些操作(Java层面就可以完成加锁)

重量级锁:加锁的过程比较复杂,用到的资源比较多,典型就是内核态的一些操作

3、自旋锁 vs 挂起等待锁

自旋锁:不停的检查所是否被释放,如果锁一旦被释放就去竞争锁资源

挂起等待锁:会被操作系统挂起,进入阻塞状态,不再参与 CPU 调度,不消耗 CPU 资源,直到被唤醒,再去竞争锁资源

4、读写锁 vs 普通互斥锁

读写锁:分为读锁和写锁

读操作的时候加读锁(共享锁),多个读锁可以共存,同时加多个读锁互不影响

写操作的时候加写锁(排他锁),只允许有一个写锁执行任务,和其他的锁是冲突的

普通互斥锁:有竞争关系,只能一个线程释放了锁之后,其他线程才可以来抢,之前用到的锁基本上都是互斥锁

5、公平锁 vs 非公平锁

公平锁:先来后到,先排队的线程先拿到锁,后排队的线程后拿到锁

非公平锁:大家去抢,谁先抢到就是谁的

6、可重入锁 vs 不可重入锁

可重入锁:对一把锁可以连续加多次,不造成死锁

不可重入锁:对一把锁可以重复加多次,造成死锁

六、自旋锁(CAS)

CAS 是实现自旋锁的核心底层技术

自旋锁通过循环执行 CAS 操作来实现 "不停检查锁状态、竞争锁资源" 的核心逻辑

CAS 操作本身是一个原子指令,它会一次性完成「读取(LOAD)→ 比较 → 交换」三个步骤

锁消除

锁消除:JVM 在即时编译(JIT)时,通过逃逸分析发现某些锁对象不会被多个线程共享,因此可以直接消除这些锁,避免不必要的同步开销。

锁粗化

锁粗化:JVM 检测到连续多次对同一个对象加锁和解锁时,会把这些锁合并成一个更大范围的锁,减少频繁加锁 / 解锁的性能损耗。

七、JUC的常见类

Callable

java 复制代码
public class Demo_1101_Callable {
    public static void main(String[] args) {
        // 实现Callable接口,定义任务
        Callable<Integer> callable  = new Callable<Integer>() {
            @Override
            public Integer call() throws Exception {
                System.out.println("执行运算...");
                int sum = 0;
                // 执行累加操作
                for (int i = 1; i <= 1000; i++) {
                    sum += i;
                }
                // 休眠3秒,模拟业务处理的时间
                TimeUnit.SECONDS.sleep(3);
                throw new Exception("执行过程中出现了异常...");
//                System.out.println("执运算完成...");
//                return sum;
            }
        };

        // Callable要配合FutureTask一起使用,FutureTask用来获取Callable的执行结果
        FutureTask<Integer> futureTask = new FutureTask<>(callable);
        // FutureTask当做构造参数传入到Thread构造方法中
        Thread thread = new Thread(futureTask);
        // 启动线程
        thread.start();

        try {
            // 等待结果, 的时间可能被中断,会抛出InterruptedException
            Integer result = futureTask.get();
            // 打印结果
            System.out.println("执行结果是:" + result);
        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (ExecutionException e) {
            e.printStackTrace();
            // 打印异常信息
            System.out.println("打印日志:" + e.getMessage());
        }
    }
}
相关推荐
Nuopiane2 小时前
MyPal3(7)
java·开发语言
不光头强2 小时前
object所有方法及知识点
java·开发语言·jvm
予枫的编程笔记2 小时前
【面试专栏|JVM虚拟机】CMS vs 其他垃圾收集器:核心差异+适用场景
java·jvm·java面试·后端开发·垃圾回收机制·cmv垃圾回收器·jvm性能优化
渡过晚枫2 小时前
[第十六届蓝桥杯/java/算法]1.偏蓝
java·算法·蓝桥杯
zhaoyin19942 小时前
JavaScript面试题笔记
java·javascript·笔记
计算机学姐2 小时前
基于SpringBoot的宠物诊所管理系统
java·vue.js·spring boot·后端·spring·elementui·宠物
2501_940315262 小时前
【无标题】1302 层数最深叶子节点的和
java·数据结构·算法
悟能不能悟2 小时前
idea默认的快捷键和eclipse配置快捷键对比,列出一些常用的
java·eclipse·intellij-idea
晨晖22 小时前
java容器类的博客
java·开发语言