Java多线程与并发编程之基本方法(二)

本文详细探讨了Java中线程的结束方式,包括stop方法的弃用、使用共享变量和interrupt方法。此外,文章还涵盖了并发编程的三大特性:原子性(synchronized和CAS)、可见性(volatile和synchronized)以及有序性,解释了它们的概念和使用场景,以及ThreadLocal的工作原理和内存泄漏问题。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

1 线程的结束方式

1.1 stop方法(不用)

强制让线程结束,无论你再干嘛,不推荐。

1.2 使用共享变量(很少)

这种方式用的也不多,有的线程可能会通过死循环来保证一直运行。咱们可以通过修改共享变量在破坏死循环,让线程退出循环,结束run方法

1.3 interrupt方式

共享变量方式

public static void main(String[] args) throws InterruptedException {
    // 线程默认情况下, interrupt标记位:false
    System.out.println(Thread.currentThread().isInterrupted());
    // 执行interrupt之后,再次查看打断信息
    Thread.currentThread().interrupt();
    // interrupt标记位:ture
    System.out.println(Thread.currentThread().isInterrupted());
    // 返回当前线程,并归位为false interrupt标记位:ture
    System.out.println(Thread.interrupted());
    // 已经归位了
    System.out.println(Thread.interrupted());
    // =====================================================
    Thread t1 = new Thread(() -> {
        while(!Thread.currentThread().isInterrupted()){
            // 处理业务
        }
        System.out.println("t1结束");
    });
    t1.start();
    Thread.sleep(500);
    t1.interrupt();
}

通过打断WAITING或者TIMED_WAITNG状态的线程,从而抛出异常自行处理,这种停止线程是最常用的一种。

wait和sleep的区别

  1. sleep 属于Thread类中的static方法,wait属于Object方法
  2. sleep属于TIMED_WAITING,自动被唤醒,wait属于WAITING,需要手动唤醒
  3. sleep方法在持有锁时执行,不会释放锁。wait执行后,会释放锁资源
  4. sleep可以在持有锁或者不持有锁时,执行。 wait方法必须在只有锁时才可以执行。

2 并发编程的三大特性

2.1 原子性

2.1.1 什么是原子性

原子性指一个操作是不可分割的,不可中断的,一个线程在执行时,另一个线程不会影响到他。

导致的问题:多线程操作临界资源时,预期的结果与最终结果不一致

2.1.2 保证并发编程的原子性
2.1.2.1 synchronized
2.1.2.2 CAS

比较和交换。

**CAS缺点:**CAS只能保证一个变量操作是原子性的,无法实现对多行代码实现原子性

CAS问题:

  • ABA问题:问题如下,可以引入版本号的方式来解决ABA问题,Java中提供了一个类在CAS时,针对各个版本追加版本号的操作。 AtomicStampeReference

在这里插入图片描述

  • 自选时间过长问题:

    可以在指定CAS一共循环多少次,如果超过这个次数,直接失败或者挂起线程

    可以在CAS一次失败后,将这个操作暂存起来,后面需要获取结果时,将暂存的操作全部执行,在返回最后的结果

2.1.2.2 Lock锁

ReentrantLock底层是基于AQS实现的,有一个基于CAS维护的state变量来实现锁的操作。

2.12.3 ThreadLoal

Java中的四种引用类型:强,软,弱,虚

强引用:把一个对象赋给一个引用变量,这个引用变量就是强引用。这个引用变量就是强引用。当一个对象被强引用变量引用时,他始终处于可达状态,他是不可能被垃圾回收机制回收的。即使该对象永远都不会被用到jvm也不会回收,因此强引用是造成Java内存泄漏的主要原因之一。例如:User user =new User;

**软引用:**对于只有软引用的对象来说,当系统内存足够时他不会被回收,当系统内存空间足够时他会被回收。

软引用通常用在对内存敏感的程序中,作为缓存使用。

**弱引用:**他比软引用的生存周期更短,对于只有弱引用的对象来说,只要垃圾回收机制一运行,不管jvm一运行,该对象就会被回收,可以解决内存泄漏问题,ThreadLocal就是基于弱引用解决内存泄漏的问题

**虚引用:**他不能单独使用,必须和引用队列联合使用,虚引用的主要作用跟踪对象被垃圾回收的状态

开发中更多使用强引用

ThreadLocal保证原子性的方式,是不让多线程去操作临界资源,让每个线程去操作属于自己的数据

static ThreadLocal tl1 = new ThreadLocal();
static ThreadLocal tl2 = new ThreadLocal();
public static void main(String[] args) {
    tl1.set("123");
    tl2.set("456");
    Thread t1 = new Thread(() -> {
        System.out.println("t1:" + tl1.get());
        System.out.println("t1:" + tl2.get());
    });
    t1.start();
    System.out.println("main:" + tl1.get());
    System.out.println("main:" + tl2.get());
}

ThreadLocal实现原理:

  • 每个ThreadLocal中存储着一个成员变量,ThreadLocalMap
  • ThreadLocal本身不存储数据,像是一个工具类,基于ThreadLocal操作ThreadLocalMap
  • ThreadLocalMap本身基于Entry[]实现的,因此一个线程可以绑定多个ThreadLocal,这样依赖,可能需要存储多个数据,所以采用Entry[]的形式实现
  • 每一个线程都有自己独立的ThreadLocalMap,在基于ThreadLocal对象本身作为key,对value进行存储
  • ThreadLocalMap的key是一个弱引用,弱引用的特点是,即便有弱引用,在GC时,也必须被回收。这里是为了在ThreadLocal对象失去引用后,如果key的引用是强引用,会导致ThreadLocal对象无法被回

ThreadLocal内存泄漏问题:

  • 如果ThreadLocal引用丢失,key因为弱引用会被GC回收掉,如果同时线程还没有被回收,就会导致内存泄漏,内存中的value无法被回收,同时也无法获取到
  • 只需要使用完毕ThreadLocal对象之后,及时调用remove方法移除Entry即可

2.2 可见性

可见性问题是基于CPU位置出现的,CPU处理速度非常快,相对CPU来说,去主内存获取数据这个事情太慢了,CPU就提供了L1,L2,L3的三级缓存,每次去主内存拿完数据后,就会存储到CPU的三级缓存,每次去三级缓存拿数据,效率肯定会提升。

这就带来了问题,现在CPU都是多核,每个线程的工作内存(CPU三级缓存)都是独立的,会告知每个线程中做修改时,只改自己的工作内存,没有及时的同步到主内存,导致数据不一致问题。

2.2.1 解决可见性办法
2.2.1.1 volatile

volatile是一个关键字,用了修饰成员变量。

如果属性被volatile修饰,相当于告诉CPU,对当前属性的操作,不允许使用CPU的缓存,必须去和主存内操作

volatile的内存语义:

  • volatile属性被写:当写一个volatile变量,JMM(内存模型)会将当前线程对应CPU缓存及时的刷新到主存。
  • volatile属性被读:当读一个volatile变量,JMM会将对应的CPU缓存中的内存设置为无效,必须去主存中重新读取共享变量

volatile修饰的属性转化为汇编之后,追加了一个lock前缀

2.2.1.2 synchronizaed

如果涉及到了synchronized的同步代码块或者是同步方法,获取锁资源之后,将内部涉及到的变量从CPU缓存中移除,必须去主内存中重新拿数据,而且在释放锁之后,会立即将CPU缓存中的数据同步到主内存

2.2.1.3 Lock

Lock锁是基于volatile实现的,Lock锁内部在进行加锁和释放锁,会对一个由volatile修饰的state属性进行加减操作

2.2.1.4 final

final修饰的属性,在运行期间是不允许修改的,这样一来,就间接的保证了可见性,所有多线程读取final属性,值肯定是一样。

final并不是说每次取数据从主内存读取,他没有这个必要,而且final和volatile是不允许同时修饰一个属性的

final修饰的内容已经不允许再次被写了,而volatile是保证每次读写数据去主内存读取,并且volatile会影响一定的性能,就不需要同时修饰。

2.3 有序性

2.3.1 什么是有序性

.java文件中的内容会被编译,在执行前需要再次转为CPU可以识别的指令,CPU在执行这些指令时,为了提升执行效率,在不影响最终结果的前提下(满足一些要求),会对指令进行重排。

2.3.2 as-if-serial

b不论指令如何重排序,需要保证单线程的程序执行的结果不变。而且如果存在依赖关系,那么也不可以做指令重排

2.3.3 volatile

volatile如何实现的禁止指令重排?

内存屏障概念。将内存屏障看成一条指令。

会在两个操作之间,添加上一道指令,这个指令就可以避免上下执行的其他指令进行重排序。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值