• java之多线程


    目录

    程序、进程、线程

    进程的特点

    线程注意事项

    线程与进程的关系​编辑

    主内存与本地内存的关系

    JMM有以下规定

    volatile关键字

    作用解释:

    串行,并行和并发

    时间片

    上下文切换

    OS底层执行线程规则

    线程的生命周期

    线程经历的阶段

    阻塞分类

    创建线程的方式

    三种方式

    继承Thread类

    总结:

    实现Runnable接口

    总结:

    实现callable接口

    实现callable接口与实现runnable接口的区别

    获取返回值

    具体代码

    理解第三种线程创建方式

    线程池

    线程池优点

    ExecutorService

    常见方法

    案例 

    线程池的5种状态

    Lamda表达式

    Lamda表达式优点

    转换过程

    线程的的常用方法

    构造方法

    普通方法

    线程的停止

    线程休眠

    线程礼让

    join方法

    观测线程状态

    属性值

    获取线程状态

    线程的优先级

    前言:

    优先级属性 

    优先级方法 

    守护线程

    设置守护线程

    线程安全 

    线程安全问题条件

    java中线程安全的三方面体现

    同步与异步

    同步机制

    使用同步锁的问题

    同步代码块

     案例

    注意:

    同步方法

    具体案例 

    总结:

    Lock锁

    前言

    lock与synchronized的区别

    Lock锁的使用

    乐观锁与悲观锁

    乐观锁案例

    锁的状态 

    锁升级原理

    死锁

    死锁的必要条件

    线程通信

    常用方法(Object类中)

    线程wait和notify工作原理

    经典案例

    Threadlocal

    常用方法 

    Semaphore

    常用方法

    程序、进程、线程

    程序:为完成特定任务,用某种语言编写的一组指令的集合,是一段静态代码

    进程(process):其是程序的一次执行过程,正在运行的一个程序,进程作为资源分配的单位,在内存中会为每个进程分配不同的内存区域。进程是一个动的过程,进程的生命周期:有它自身的产生,存在和消亡的过程。

    线程(thread):线程是操作系统OS能够进行运算调度的最小单位,它被包含在进程之中,是进程中的实际运作单位

    注意:我们平时执行main方法时一般伴随着3个线程的执行,分别为main主线程,异常处理线程,垃圾回收线程 

    进程的特点

    • 独立性:进程是系统中独立存在的实体,它可以拥有自己独立的资源,每个进程都拥有自己私有的地址空间,在没有经过进程本身允许的情况下,一个用户进程不可以直接访问其他进程的地址空间
    • 动态性:进程与程序的区别在于,程序只是一个静态的指令集合,而进程是一个正在系统中活动的指令集合,程序加入了时间的概念以后,称为进程,具有自己的生命周期和各种不同的状态,这些概念都是程序所不具备的.
    • 并发性:多个进程可以在单个处理器CPU上并发执行,多个进程之间不会互相影响.

    线程注意事项

    • 一个进程可以开启多个线程,其中有一个主线程来调用本进程中的其他线程。
    • 我们看到的进程的切换,切换的也是不同进程的主线程
    • 多线程可以让同一个进程同时并发处理多个任务,相当于扩展了进程的功能。
    • 一个操作系统中可以有多个进程,一个进程中可以包含一个线程(单线程程序),也可以包含多个线程(多线程程序)
    • 每个线程在共享同一个进程中的内存的同时,又有自己独立的内存空间

    线程与进程的关系进程与线程的关系

    主内存与本地内存的关系

    java作为高级语言屏蔽了cpu多层缓存这些细节,用jmm定义了一套读写内存的数据规范,虽然不需要关心一级缓存和二级缓存这些问题,但jmm抽象了主内存和本地内存的概念

    JMM有以下规定

    • 所有变量都储存在主内存中,同时每个线程也有自己独立的工作内存,工作内存的变量内容是主内存中的拷贝
    • 线程不能直接读写主内存中的变量,而是只能操作自己工作内存的变量,然后同步到主内存中
    • 主内存是多线程共享的,但线程间不共享工作内存,如果线程间需要通信,必须借助主内存中转来完成

    volatile关键字

    前言:其仅能使用在变量级别,使用该关键字的变量,一个线程修改则其他线程可见,并且避免了指令的重排序

    作用解释:

    • 保证线程可见性:一个线程对该变量的修改会马上由工作内存写回到主内存,所以会马上反映在其他线程的读取操作中。
    • 禁止指令重排序:若创建对象会有很多步骤,但一般情况下JVM会按照自己的一个逻辑来打乱上述步骤的执行,单线程情况下结果与之前相同,多线程就不一定了,禁止指令重排序就是取消JVM内部指令优化,按照一般步骤执行 

    串行,并行和并发

    并行:同一时刻有多个线程同时执行

    并发:同一个对象被多个线程同时操作

    串行:同一时刻一个线程只执行一个任务,这个任务执行完才执行下一个

    并发编程的目的:充分利用处理器的每一个核,以达到最高的处理性能 

    时间片

    时间片,即CPU分配给各个线程的一个时间段,称作它的时间片,即该线程被允许运行的时间,如果在时间片用完时线程还在执行,那CPU将被剥夺并分配给另一个线程,将当前线程挂起,如果线程在时间片用完之前阻塞或结束,则CPU当即进行切换,从而避免CPU资源浪费,当再次切换到之前挂起的线程,恢复现场,继续执行。

    上下文切换

    当前任务执行完CPU时间片切换到另一个任务之前会先保存自己的状态以便下次切换回这个任务时可以加载这个任务的状态。任务从保存到再加载的过程就是一次上下文切换

    注意:CPU以纳秒的级别进行时间片的分配,高速完成着线程的切换

    OS底层执行线程规则

    • FCFS(First Come First Service 先来先服务算法)
    • SJS(Short Job Service短服务算法)

    线程的生命周期

    含义:线程从开始到消亡的过程

    线程经历的阶段

    • 创建状态:需要先申请PCB,然后为该线程运行分配必须的资源,并将该线程转为就绪状态插入到就绪队列中
    • 就绪状态:当调用线程对象的start()方法,线程即为进入就绪状态,但不意味着立即调度执行
    • 运行状态:当CPU调度了处于就绪状态的线程时,此线程才是真正的执行,即进入到运行状态
    • 阻塞状态:处于运状态中的线程由于某种原因,暂时放弃对CPU的使用权,停止执行,此时进入阻塞状态,直到其进入就绪状态才有机会被CPU选中再次执行.
    • 终止状态:线程执行完了或者因异常退出了run()方法,该线程结束生命周期

    注意:

    • 为了保证参与并发执行的每个线程都能独立运行,OS配置了特有的数据结构PCB(进程控制模块)来描述线程的基本情况和活动过程,进而控制和管理线程 
    • 处于就绪(可运行)状态的线程,只是说明线程已经做好准备,随时等待CPU调度执行,并不是执行了t.start()此线程立即就会执行
    • 就绪状态是进入运行状态的唯一入口,也就是线程想要进入运行状态状态执行,先得处于就绪状态
    • 死亡之后的线程不能够再次启动

    阻塞分类

    • 等待阻塞:运行状态中的线程执行wait()方法,本线程进入到等待阻塞状态
    • 同步阻塞:线程在获取synchronized同步锁失败(因为锁被其他线程占用),它会进入同步阻塞状态
    • 其他阻塞:调用线程的sleep()或者join()或发出了I/O请求时,线程会进入到阻塞状态.当sleep()状态超时.join()等待线程终止或者超时或者I/O处理完毕时线程重新转入就绪状态

    创建线程的方式

    三种方式

    • 继承Thread类
    • 实现Runnable接口
    • 实现Callable接口

    继承Thread类

    1. //继承Thread类
    2. public class MyThread extends Thread{
    3. //重写run方法,里面放执行逻辑
    4. @Override
    5. public void run() {
    6. //run方法线程体
    7. for (int i=0;i<9;i++){
    8. System.out.println("我是线程"+getName()+"\t"+i);
    9. }
    10. }
    11. }
    12. class Test1{
    13. public static void main(String[] args) {
    14. new MyThread().start();
    15. new MyThread().start();
    16. new MyThread().start();
    17. }
    18. }

    注意:

    • 这里面的run方法不能直接调用,如果直接调用就会被当作一个普通方法,想要线程真正起作用,必须启动线程
    • 启动线程的方法为线程对象.start(),start()方法为原生方法(Thread类中的),当调用时,底层会去调用其他语言接口来完成线程的功能
    • Thread类实现了Runnable接口

    总结:

    1. 定义Thread类的子类并重写Thread类的run方法该run方法的方法体就代表了线程要完成的任务,因此把run方法称为执行体
    2. 创建Thread子类的实例,即创建线程的对象
    3. 调用线程的start方法,将本线程加入到就绪状态,等待CPU调度

    实现Runnable接口

    1. public class TestThread implements Runnable{
    2. int count=30;
    3. @Override
    4. public void run() {
    5. for(int i=0;i<10;i++){
    6. System.out.println(Thread.currentThread().getName()+"——"+i);
    7. count--;
    8. }
    9. System.out.println(Thread.currentThread().getName()+"\t"+count);
    10. }
    11. }
    12. class Test2{
    13. public static void main(String[] args) {
    14. TestThread target = new TestThread();
    15. //因为公用1个target所以共享count
    16. new Thread(target,"线程1").start();
    17. new Thread(target,"线程2").start();
    18. new Thread(target,"线程3").start();
    19. }
    20. }

    注意:实现Runnable接口方式创建线程避免了单继承的局限性,可以多个线程跑一个对象

    总结:

    • 定义Runnable接口实现类,并重写该接口的run方法,该run方法同样是该线程的线程执行体
    • 创建Runnable实现类的实例,并将此实例传入Thread构造参数中来创建Thread对象,该Thread对象才是真正的线程对象
    • 调用线程对象的start方法来启动线程

    实现callable接口

    实现callable接口与实现runnable接口的区别

    • 执行逻辑为run方法的线程不可以有返回值,不可以抛出异常
    • callable接口实现线程的call方法可以有返回值类型
    • callable接口实现线程的call方法可以抛出异常
    • Runnable接口实现需要重写run方法,Callable接口实现需要重写call方法

    获取返回值

    语法:Object o = FutureTask适配器对象.get();

    注意:该方法可能产生阻塞,因为它要等待返回值返回

    具体代码

    1. public class TestRandomNum implements Callable {
    2. //上面的泛型参数对应下面方法的返回值参数,如果不写,那么下面的返回值默认为Object类型
    3. @Override
    4. public Integer call() throws Exception {
    5. System.out.println("线程执行体"+Thread.currentThread().getName());
    6. return new Random().nextInt(10);
    7. }
    8. }
    9. class Test06{
    10. public static void main(String[] args) throws Exception {
    11. //定义一个线程对象
    12. TestRandomNum num = new TestRandomNum();
    13. //新建适配类
    14. FutureTask task = new FutureTask(num);
    15. FutureTask task1 = new FutureTask(num);
    16. new Thread(task).start();
    17. new Thread(task1).start();
    18. //获取线程得到的返回值
    19. Object o = task.get();
    20. Object n = task1.get();
    21. System.out.println("task:"+o+"\ttask1:"+n);
    22. }
    23. }

    注意:

    • 实现Callable接口可以不带泛型,如果不带泛型,那么call方式的返回值就是Object类型
    • 如果实现的Callable接口带泛型,那么call方法的返回值就是泛型对应的类型
    • 从call方法上可以看到,方法有返回值,并且可以抛出异常

    理解第三种线程创建方式

    • 线程启动方式:new Thread(Runnable a,String name).start();
    • 如今来个第三者Callable,因此只能将Runnable和Callable链接起来
    • 我们看到FutureTask为Runnable接口实现类,并且其构造参数有Callable:FutureTask(Callable callable)
    • 如此这般FutureTask就作为一个适配类将Callable和Runnable连接起来

    线程池

    前言:经常创建和销毁,使用量特别大的资源,对性能影响很大,因此可以提前创建好多个线程,放入线程池中,使用时直接获取,使用完放回池中。可以避免频繁的创建和销毁,实现重复利用。

    线程池优点

    • 提高响应速度(减少了创建新线程的时间)
    • 降低资源消耗(重复利用线程池中的线程,不需要每次都创建)
    • 便于线程管理

    ExecutorService

    ExecutorService:真正的线程池接口,常见子类ThreadPoolExecutor

    常见方法

    void execute(Runnable command):执行任务或命令,没有返回值

    Future submit(Callable task):执行任务,有返回值

    void shutdown():关闭线程池

    Executors:工具类,线程池的工厂类,用于创建并返回不同类型的线程池

    static ExecutorService newFixedThreadPool(int n) 最多n个线程的线程池

    static ExecutorService newCachedThreadPool() 足够多的线程,使任务不必等待

    static ExecutorService newSingleThreadExecutor() 只有一个线程的线程池 

    案例 

    1. public class TestPool {
    2. public static void main(String[] args) {
    3. //创建线程池
    4. ExecutorService pool = Executors.newFixedThreadPool(10);
    5. //上面创建最多10个线程,但是只用了6个
    6. for (int i = 0; i < 6; i++) {
    7. pool.execute(new RThread());
    8. }
    9. //关闭连接
    10. pool.shutdown();
    11. }
    12. }
    13. class RThread implements Runnable{
    14. @Override
    15. public void run() {
    16. System.out.println(Thread.currentThread().getName());
    17. }
    18. }

    线程池的5种状态

    Lamda表达式

    Lamda表达式优点

    • 避免匿名内部类定义过多
    • 可以让代码看起来更简洁
    • 去掉一堆没有意义的代码

    函数式接口:任何接口,如果只包含唯一一个抽象方法,那么他就是一个函数式接口

    注意:对于函数式接口,我们可以通过Lamda表达式来创建该接口的对象

    转换过程

    1. public class Test {
    2. public static void main(String[] args) {
    3. //局部内部类
    4. class Have implements OneMethod{
    5. @Override
    6. public void fly(int a) {
    7. System.out.println("我飞了"+a+"米");
    8. }
    9. }
    10. Have have = new Have();
    11. have.fly(3);
    12. //OneMethod的匿名内部类写法
    13. OneMethod one=new OneMethod(){
    14. @Override
    15. public void fly(int a) {
    16. System.out.println("我飞了"+a+"米");
    17. }
    18. };
    19. one.fly(3);
    20. //OneMethod的lambda表达式写法
    21. OneMethod oneMethod=(int a)->{
    22. System.out.println("我飞了"+a+"米");
    23. };
    24. oneMethod.fly(3);
    25. }
    26. }
    27. //定义一个函数时接口
    28. interface OneMethod{
    29. void fly(int a);
    30. }

    注意:

    • lambda表达式中若函数参数只有一个,则()可以省略
    • lambda表达式若方法体里只有一句代码,则{}可省略
    • lambda表达式的使用前提是该接口必须是函数式接口
    • lambda表达式的方法参数类型可以省略,多个参数的时候参数类型也可以省略,不过,要省略就都省略

    线程的的常用方法

    前言:因为线程的创建方式有3种:继承Thread、实现Runnable接口、实现Callable接口,Runnable接口只有运行方法run,Callable接口只有运行方法call,而线程的启动都需要借助Thread类对象,所以线程的方法也可以理解为Thread类的方法

    构造方法

    Thread():分配新的Thread对象

    Thread(String name):分配新的Thread对象并为线程起名

    Thread(Runnable target):分配新的Thread对象

    Thread(Runnable target,String name):分配新的Thread对象并为线程起名

    普通方法

    static Thread currentThread( ):返回对当前正在执行的线程对象的引用
    long getId():返回该线程的标识
    String getName():返回该线程的名称
    static void sleep(long millions):在指定的毫秒数内让指定的线程休眠
    void start():使该线程开始执行(Java虚拟机调用该线程的run())

    int getPriority():获取线程优先级

    void setPriority(int newPriority):设置线程优先级别

    void join():等待该线程终止(插入该线程)

    static void yield():暂停当前正在执行的线程对象,并执行其他线程

    void interrupt():中断线程,别用这个方式

    boolean isAlive():测试线程是否处于活动状态

    Thread.State getState():返回该线程状态

    线程的停止

    1. public class TestStop implements Runnable{
    2. //设置一个标志位
    3. private boolean flag=true;
    4. @Override
    5. public void run() {
    6. int i=0;
    7. while (flag){
    8. System.out.println("run……Thread"+i++);
    9. }
    10. }
    11. public void stop(){
    12. this.flag=false;
    13. }
    14. public static void main(String[] args) {
    15. TestStop testStop = new TestStop();
    16. new Thread(testStop).start();
    17. for (int i=0;i<1000;i++){
    18. System.out.println("main"+i);
    19. if (i==900){
    20. //调用stop方法切换标志位,让线程停止
    21. testStop.stop();
    22. System.out.println("线程停止了");
    23. }
    24. }
    25. }
    26. }

    线程休眠

    • sleep指定当前线程阻塞的毫秒数
    • sleep存在异常interruptedException
    • sleep时间达到后线程进入到就绪状态
    • sleep可以模拟网络延时,倒计时等
    • 每个对象都有一个锁,sleep不会释放锁

    线程礼让

    • 就是让当前正在执行的线程暂停,但不阻塞
    • 让线程从运行状态转变为就绪状态
    • 让CPU重新调度,礼让不一定成功!看CPU心情
    1. public class TestYield {
    2. public static void main(String[] args) {
    3. MyYield myYield = new MyYield();
    4. new Thread(myYield,"a线程").start();
    5. new Thread(myYield,"b线程").start();
    6. }
    7. }
    8. class MyYield implements Runnable{
    9. @Override
    10. public void run() {
    11. System.out.println(Thread.currentThread().getName()+"线程开始执行");
    12. //线程礼让
    13. Thread.yield();
    14. System.out.println(Thread.currentThread().getName()+"线程停止执行");
    15. }
    16. }

    join方法

    • join合并线程,待此线程执行完后,再执行其他线程,其他线程阻塞
    • 可以想象成插队
    1. public class TestJoin implements Runnable{
    2. @Override
    3. public void run() {
    4. for (int i=0;i<100;i++){
    5. try {
    6. //为了能有机会插队
    7. Thread.sleep(100);
    8. } catch (InterruptedException e) {
    9. e.printStackTrace();
    10. }
    11. System.out.println("我是VIP");
    12. }
    13. }
    14. public static void main(String[] args) throws InterruptedException {
    15. TestJoin testJoin = new TestJoin();
    16. Thread thread = new Thread(testJoin);
    17. thread.start();
    18. for (int i=0;i<300;i++){
    19. if (i==200){
    20. //此线程执行完,主线程才能继续执行
    21. thread.join();
    22. }
    23. System.out.println("main"+i);
    24. }
    25. }
    26. }

    观测线程状态

    语法:Thread.State.属性

    注意:Thread.State为嵌套类

    属性值

    获取线程状态

    语法:线程.getState()

    1. public class TestState {
    2. public static void main(String[] args) throws InterruptedException {
    3. Thread thread = new Thread(() -> {
    4. for (int i=0;i<5;i++){
    5. try {
    6. Thread.sleep(1000);
    7. } catch (InterruptedException e) {
    8. e.printStackTrace();
    9. }
    10. }
    11. System.out.println("我是标记");
    12. });
    13. //观察状态
    14. Thread.State state = thread.getState();
    15. System.out.println(state);
    16. //启动后
    17. thread.start();
    18. //只要线程不处于退出状态就一直打印状态
    19. while (state!=Thread.State.TERMINATED){
    20. Thread.sleep(100);
    21. state=thread.getState();
    22. System.out.println(state);
    23. }
    24. }
    25. }

    线程的优先级

    前言:

    • 线程的执行并不一定优先级高的就会先跑,只是更可能先跑
    • java提供了一个线程调度器来监控程序中启动后进入就绪状态的所有线程,线程调度器按照优先级决定应该调度哪个线程来执行
    • 应该先设置优先级再启动,一般线程优先级默认为5来公平竞争
    • 线程的优先级用数字表示,范围从1-10

    优先级属性 

    • Thread.MIN_PRIORITY=1
    • Thread.MAX_PRIORITY=10
    • Thread.NORM_PRIORITY=5

    优先级方法 

    获取线程优先级:int getPriority()

    设置线程优先级:void setPriority(int xxx)

    1. public class TestPriority {
    2. public static void main(String[] args) {
    3. MyPriority myPriority = new MyPriority();
    4. Thread t1 = new Thread(myPriority);
    5. Thread t2 = new Thread(myPriority);
    6. Thread t3 = new Thread(myPriority);
    7. //设置优先级再启动
    8. t1.setPriority(1);
    9. t2.setPriority(5);
    10. t3.setPriority(10);
    11. t1.start();
    12. t2.start();
    13. t3.start();
    14. }
    15. }
    16. class MyPriority implements Runnable{
    17. @Override
    18. public void run() {
    19. System.out.println(Thread.currentThread().getName()+"优先级为"+Thread.currentThread().getPriority());
    20. }
    21. }

    守护线程

    • 线程分为用户线程和守护线程
    • 虚拟机必须确保用户线程执行完毕
    • 虚拟机不用等待守护线程执行完毕

    设置守护线程

    语法:线程.setDaemon(true);

    注意:默认值为false表示他为普通线程

    1. public class TestDaemon {
    2. public static void main(String[] args) throws InterruptedException {
    3. //守护线程
    4. God god = new God();
    5. //用户线程
    6. You you = new You();
    7. Thread thread = new Thread(god);
    8. thread.setDaemon(true);//默认为false,表示是用户线程
    9. thread.start();
    10. new Thread(you).start();//用户线程启动
    11. }
    12. }
    13. class God implements Runnable{
    14. @Override
    15. public void run() {
    16. while (true){
    17. System.out.println("主与你同在");
    18. }
    19. }
    20. }
    21. class You implements Runnable{
    22. @Override
    23. public void run() {
    24. for (int i = 0; i < 36500; i++) {
    25. System.out.println("你一生都开心的活着");
    26. }
    27. System.out.println("goodbye world");
    28. }
    29. }

    线程安全 

    线程安全问题条件

    • 在多线程程序中
    • 有共享数据
    • 多条语句操作共享数据

    java中线程安全的三方面体现

    1. 原子性:提供互斥访问,同一个时刻只有一个线程对数据操作
    2. 可见性:一个线程对主内存的修改可以及时的被其他线程看到
    3. 有序性:一个线程观察其他线程中指令的执行顺序,由于指令重排序,观察结果一般杂乱无序

    同步与异步

    同步:体现了排队的效果,同一时刻只能有一个线程独占资源,其他没有权利的线程排队。

    异步:体现了多线程抢占资源的效果,线程间互相不等待,互相抢占资源。

    注意:同步执行效率低但安全,异步执行效率高但不安全 

    同步机制

    前言:处理多线程问题时,多个线程访问同一个对象,并且,某些线程还想修改这个对象,此时我们就需要线程同步,线程同步实际上就是一种等待机制,多个需要同时访问此对象的线程进入这个对象的等待池形成队列,等待前面的线程使用完毕,下一个线程再使用。

    线程同步条件:队列+锁

    注意:由于同一进程的多个线程共享同一块存储空间,在带来方便的同时也带来了冲突问题,为了保证数据在方法中被访问的正确性,在访问时加入锁机制synchronized,当一个线程获得对象的排他锁,独占资源,其他线程必须等待,使用后释放锁即可

    使用同步锁的问题

    • 一个线程持有锁会导致其他所需要此锁的线程挂起
    • 在多线程竞争下,加锁,释放锁会导致比较多的上下文切换和调度延时,引起性能问题
    • 如果一个优先级高的线程等待一个优先级低的线程释放锁会导致优先级倒置,引起性能问题

    同步代码块

    1. synchronized (同步监视器){
    2. ……需要同步的代码……
    3. }

     案例

    1. public class Test08 {
    2. public static void main(String[] args) {
    3. ByTicketThread t = new ByTicketThread();
    4. new Thread(t,"小明").start();
    5. new Thread(t,"小红").start();
    6. new Thread(t,"小兰").start();
    7. }
    8. }
    9. class ByTicketThread implements Runnable{
    10. int ticketNum=100;
    11. @Override
    12. public void run() {
    13. for (int i = 1; i <= 100; i++) {
    14. synchronized (this) {
    15. if (ticketNum > 0) {
    16. System.out.println(Thread.currentThread().getName() + "买到第" + ticketNum-- + "张");
    17. }
    18. }
    19. }
    20. }
    21. }

    注意:

    • synchronized修饰的代码块叫同步代码块
    • 锁对象(同步监视器)任意,但必须唯一,一个线程拿到这个唯一的锁对象就可以操作代码块里面的资源了
    • 锁对象必须是引用数据类型,不能是基本数据类型
    • 锁对象建议使用final修饰
    • 多个代码块使用同一个锁对象,一个线程获得锁的情况下,所有的被锁对象修饰的代码块都被锁住了

    同步方法

    1. //给方法加同步修饰符
    2. public synchronized void method(int args){}

    具体案例 

    1. public class Test08 {
    2. public static void main(String[] args) {
    3. ByTicketThread t = new ByTicketThread();
    4. new Thread(t,"小明").start();
    5. new Thread(t,"小红").start();
    6. new Thread(t,"小兰").start();
    7. }
    8. }
    9. class ByTicketThread implements Runnable{
    10. int ticketNum=100;
    11. @Override
    12. public void run() {
    13. for (int i = 1; i <= 100; i++) {
    14. get();
    15. }
    16. }
    17. //如果是继承Thread类,则同步方法需加static
    18. public synchronized void get(){
    19. if (ticketNum > 0) {
    20. System.out.println(Thread.currentThread().getName() + "买到第" + ticketNum-- + "张");
    21. }
    22. }
    23. }

    总结:

    • 被synchronized修饰的方法是同步方法
    • synchronized关键字也可以修饰静态方法,此时如果调用静态方法将会锁住整个类
    • synchronized修饰的非静态方法其实是给当前的this对象加锁
    • synchronized修饰的非静态方法:操作本对象的线程进入了该同步方法锁住了本对象,那么下一个操作本对象所有同步方法的线程必须等待
    • synchronized修饰的静态方法:操作本类的线程进入了该同步方法锁住了本类,那么下一个操作本类所有同步方法的线程必须等待 

    Lock锁

    前言

    synchronized是java中的关键字,这个关键字的识别是靠jvm来识别完成的,是虚拟机级别,但是lock锁是API级别,提供了相应的接口与对应的实现类,这个方式更灵活,表现出来的性能优于之前的方式

    lock与synchronized的区别

    • lock是显式锁(手动开启和关闭锁),synchronized是隐式的
    • lock只有代码块锁,synchronized有代码块锁和方法锁
    • 使用Lock锁,JVM将花费较少的时间来调度线程,性能更好。并且具有更好的扩展性(提供更多子类)

    Lock锁的使用

    拿锁:Lock lock=new ReentrantLock();

    打开锁:lock.lock();

    关闭锁:lock.unlock();

    注意:

    • 打开锁与关闭锁之间放代码块,并且关闭锁尽量放到finally种
    • Lock为一个接口,不可以直接创建对象
    1. public class Test08 {
    2. public static void main(String[] args) {
    3. ByTicketThread t = new ByTicketThread();
    4. new Thread(t,"小明").start();
    5. new Thread(t,"小红").start();
    6. new Thread(t,"小兰").start();
    7. }
    8. }
    9. class ByTicketThread implements Runnable{
    10. int ticketNum=100;
    11. //拿来一把锁
    12. Lock lock=new ReentrantLock();
    13. @Override
    14. public void run() {
    15. for (int i = 1; i <= 100; i++) {
    16. //打开锁
    17. lock.lock();
    18. try {
    19. if (ticketNum > 0) {
    20. System.out.println(Thread.currentThread().getName() + "买到第" + ticketNum-- + "张");
    21. }
    22. }catch (Exception e){
    23. e.printStackTrace();
    24. }finally {
    25. //关闭锁
    26. lock.unlock();
    27. }
    28. }
    29. }
    30. }

    乐观锁与悲观锁

    • 乐观锁:持有比较乐观态度的锁,就是在操作数据时非常乐观,认为别的线程不会同时修改数据,所以不会上锁,但在更新时会判断在此期间别的线程有没有更新过这个数据,若更新过了,则此次更新失败
    • 悲观锁:操作数据时比较悲观,每次拿数据时都会认为别人会同时修改数据,所以每次拿数据时都会上锁,这样别人想拿到这个数据就会阻塞,直到它拿到锁 

    乐观锁案例

    CAS(compare and swap)

    cas(&i,0,1) 

    CAS操作包含三个操作数:(内存位置V,期望原值A,和新值B)

    注意:当且仅当内存地址的V的值和预期的值都相等时,cpu才会更新到新值B,否则失效

    ABA问题:一个线程将数值改成了b,接着又改成了a,此时cas会认为其没变化,其实已经变化过了

    解决办法:使用版本号标识,每操作一次version+1来解决

    自旋锁:就是让线程等待一段时间,不会被立即挂起,看持有锁的线程是否很快的释放锁,其不会改变以前的旧值,当判断另一个线程释放锁后再将其改为约定的新值

    锁的状态 

    锁的四种状态:无锁状态、偏向锁状态、轻量级锁状态、重量级锁状态

    1. 偏斜锁:当没有竞争出现时,默认会使用偏斜锁。JVM 会利用 CAS 操作(compare and swap),在对象头上的 Mark Word 部分设置线程 ID,以表示这个对象偏向于当前线程,所以并不涉及真正的互斥锁。
    2. 轻量级锁:当关闭偏向锁功能或者多个线程竞争偏向锁导致偏向锁升级为轻量级锁,则会尝试获取轻量级锁。(JVM自己处理线程与线程间的同步关系)
    3. 重量级锁:重量级锁通过对象内部的监视器(monitor)实现,其中monitor的本质是依赖于底层操作系统的Mutex Lock实现,操作系统实现线程之间的切换需要从用户态到内核态的切换,切换成本非常高。(JVM管理不了,由操作系统管理)

    注意:

    • 锁的状态会随着竞争的激烈而逐渐升级(锁可以升级,不可以降级)
    • 重量级锁降级发生在STW阶段,降级对象仅仅能被VMThread访问而没有其他javaThread访问对象
    • 引入偏向锁目的:为了在无多线程竞争的情况下尽量减少不必要的轻量级锁执行路径。
    • 偏向锁启动机制:默认虚拟机启动后4s,原因:确定是单/多线程启动
    • 锁升级目的:降低锁带来的性能消耗

    锁升级原理

    在锁对象的对象头里面有一个 threadid 字段,在第一次访问的时候threadid 为空,jvm 让其持有偏向锁,并将 threadid 设置为其线程 id,再次进入的时候会先判断threadid 是否与其线程 id 一致,如果一致则可以直接使用此对象,如果不一致,则升级偏向锁为轻量级锁,通过自旋循环一定次数来获取锁,执行一定次数之后,如果还没有正常获取到要使用的对象,此时就会把锁从轻量级升级为重量级锁,此过程就构成了锁的升级。

    死锁

    含义:两个线程互相抱着对方需要的资源,互相等待对方的执行结果,形成僵持,并且两个线程均不会释放各自的资源

    死锁的必要条件

    • 互斥条件:一个资源每次只能被一个进程使用
    • 请求和保持条件:一个进程因请求资源而阻塞,对已获得的资源保持不放
    • 不可剥夺条件:进程已获得资源,在未使用完之前不可被剥夺
    • 环路等待条件:若干进程之间形成了一种头尾相接的环路等待资源关系
    1. public class DeadLock {
    2. public static void main(String[] args) {
    3. Makeup g1 = new Makeup(0, "灰姑娘");
    4. Makeup g2 = new Makeup(1, "白雪公主");
    5. g1.start();
    6. g2.start();
    7. }
    8. }
    9. class Lipstick{}
    10. class Mirror{}
    11. class Makeup extends Thread{
    12. //需要的资源只有一份
    13. static Lipstick lipstick=new Lipstick();
    14. static Mirror mirror=new Mirror();
    15. int choice;
    16. String girlName;
    17. Makeup(int choice,String girlName){
    18. this.choice=choice;
    19. this.girlName=girlName;
    20. }
    21. @Override
    22. public void run() {
    23. //化妆
    24. try {
    25. makeup();
    26. } catch (InterruptedException e) {
    27. e.printStackTrace();
    28. }
    29. }
    30. private void makeup() throws InterruptedException {
    31. if (choice==0){
    32. synchronized (lipstick){
    33. System.out.println(this.girlName+"获得口红的锁");
    34. Thread.sleep(1000);
    35. synchronized (mirror){
    36. System.out.println(this.girlName+"获得镜子的锁");
    37. }
    38. }
    39. }else {
    40. synchronized (mirror){
    41. System.out.println(this.girlName+"获得镜子的锁");
    42. synchronized (lipstick){
    43. System.out.println(this.girlName+"获得口红的锁");
    44. }
    45. }
    46. }
    47. }
    48. }

    线程通信

    常用方法(Object类中)

    void wait():表示线程一直等待,直到其他线程通知(等待时释放锁)

    void wait(long timeout):指定等待的毫秒数

    void notify():随机唤醒一个处于等待状态的线程

    void notifyAll():唤醒同一对象上所有调用了wait方法的线程争抢资源

    线程wait和notify工作原理

    当对象Object调用wait方法后,线程进入Object对象的等待队列,多个线程等待同一个对象,当对象Object调用了notify方法后,就会从等待队列队列里随机选取一个线程并将其唤起,完全随机

    注意:

    • wait方法和notify方法都必须先获取目标对象的监视器,这样才能进行后续操作
    • wait方法不是随便能调用的,其会包含在synchronized中

    经典案例

    1. public class Message {
    2. final static Object OBJECT=new Object();
    3. public static class T1 extends Thread{
    4. @Override
    5. public void run() {
    6. synchronized (OBJECT){
    7. System.out.println(System.currentTimeMillis()+"——t1 start");
    8. try {
    9. System.out.println(System.currentTimeMillis()+"——t1 wait for object");
    10. OBJECT.wait();
    11. }catch (Exception e){
    12. e.printStackTrace();
    13. }
    14. System.out.println(System.currentTimeMillis()+"——t1 end");
    15. }
    16. }
    17. }
    18. public static class T2 extends Thread{
    19. @Override
    20. public void run() {
    21. synchronized (OBJECT){
    22. System.out.println(System.currentTimeMillis()+"——t2 start notify one thread");
    23. OBJECT.notify();
    24. System.out.println(System.currentTimeMillis()+"——t2 notify end");
    25. try {
    26. Thread.sleep(5000);
    27. System.out.println(System.currentTimeMillis()+"——t2 end");
    28. }catch (Exception e){
    29. e.printStackTrace();
    30. }
    31. }
    32. }
    33. }
    34. public static void main(String[] args) {
    35. new T1().start();
    36. new T2().start();
    37. }
    38. }

    Threadlocal

    Threadlocal属于线程自身所有,不会在多个线程间共享,java提供了Threadlocal类支持线程局部变量,是一种实现线程安全的方式

    注意:在管理环境下使用线程局部变量时要特别小心,在这种情况下,工作线程的生命周期比任何应用变量的生命周期都要长,任何线程局部变量一旦在工作完成后没有释放,java应用就会存在内存泄露的风险(避免前世记忆影响今生行为,使用前最好remove掉)

    创建对象:ThreadLocal threadLocal=new ThreadLocal<>();

    常用方法 

    T get():返回此线程局部变量当前值

    void set(T value):为此线程局部变量设置指定的值

    void remove():移除此线程局部变量在该线程当前的值

    1. public class Current {
    2. static ThreadLocal threadLocal=new ThreadLocal<>();
    3. public static void main(String[] args) throws Exception {
    4. Thread t1 = new Thread(() -> {
    5. System.out.println(threadLocal.get());
    6. threadLocal.set(0);
    7. System.out.println(threadLocal.get());
    8. });
    9. Thread t2 = new Thread(() -> {
    10. System.out.println(threadLocal.get());
    11. threadLocal.set(1);
    12. System.out.println(threadLocal.get());
    13. });
    14. t1.start();
    15. Thread.sleep(1000);
    16. t2.start();
    17. }
    18. }

    Semaphore

    含义:Semaphore就是一个信号量,他的作用是限制某代码段中的并发数

    工作原理:Semaphore计数信号量会初始化指定数量的许可,每调用一次acquire方法,一个许可被调用的线程取走,每调用一个release方法,一个许可会被返还给信号量,因此在没有任何release调用时最多n个线程能通过acquire方法,n是该信号量初始化时指定许可的数量,这些许可只是简单的计数器

    创建对象:Semaphore semaphore = new Semaphore(int n,boolean flag);

    注意:第一个值为int类型,表示许可数量,第二个值为boolean类型,表示是否为公平策略,默认为false(非公平);若为公平策略,则按照请求事件获取许可,即先发送的请求先获得许可,若为非公平策略,则先发送的请求未必先获得许可,这有助于提高程序的吞吐量,但有可能导致某些请求始终获取不到许可

    常用方法

    void acquire():从此信号量中获取一个许可,若无许可获得,则会一直等待

    void release():释放一个许可,来返还给信号量

    1. public class Sign {
    2. public static void main(String[] args) {
    3. Semaphore semaphore = new Semaphore(2);
    4. new SignThread(semaphore, "线程1").start();
    5. new SignThread(semaphore, "线程2").start();
    6. new SignThread(semaphore, "线程3").start();
    7. }
    8. }
    9. class SignThread extends Thread{
    10. private Semaphore semaphore;
    11. public SignThread(Semaphore semaphore,String s) {
    12. this.semaphore = semaphore;
    13. //为线程起名
    14. setName(s);
    15. }
    16. @Override
    17. public void run() {
    18. try {
    19. //取走许可
    20. semaphore.acquire();
    21. System.out.println(getName()+"取走了许可");
    22. sleep(2000);
    23. //释放许可
    24. semaphore.release();
    25. } catch (InterruptedException e) {
    26. e.printStackTrace();
    27. }
    28. }
    29. }

  • 相关阅读:
    招投标系统简介 企业电子招投标采购系统源码之电子招投标系统 —降低企业采购成本
    PostgreSQL插入大量数据:pg_testgen插件
    VUE+VScode+elementUI开发环境
    【小程序】微信小程序设置globalData全局数据
    Java模板方法模式源码剖析及使用场景
    改变思维,让你弯道超车的好习惯!
    React Hook - useState函数的详细解析
    传输机房的基本结构
    如何抓住元宇宙中机遇
    webpack打包TypeScript代码
  • 原文地址:https://blog.csdn.net/m0_60027772/article/details/126212534