并发基础

线程与进程理论知识基础

1、并发与并行的区别:并发侧重于在同一实体上,如一台处理器上"同时"处理多个任务,不能脱离时间单位。而并行则侧重于在不同实体上,如多台处理器上同时处理多个任务。

2、申请内存是以进程来申请的。Java程序天生就是多线程的。

typescript 复制代码
public class OnlyMain {
    public static void main(String[] args) {
        //Java 虚拟机线程系统的管理接口
        ThreadMXBean threadMXBean = ManagementFactory.getThreadMXBean();
        // 不需要获取同步的monitor和synchronizer信息,仅仅获取线程和线程堆栈信息
        ThreadInfo[] threadInfos =
                threadMXBean.dumpAllThreads(false, false);
        // 遍历线程信息,仅打印线程ID和线程名称信息
        for (ThreadInfo threadInfo : threadInfos) {
            System.out.println("[" + threadInfo.getThreadId() + "] "
                    + threadInfo.getThreadName());
        }
    }
}

不到9行代码启动了6个线程。

finalize()方法不一定会被调用,因为他是在Finaizer这个线程触发的,如果主线程退出,该线程也退出,就不会执行了。

3、新开启线程的方式:只有两种。这是JDK官方说法,Thread源码上标注的。(1)通过Thread,线程的抽象;(2)实现Runnable,对任务的抽象。

4、线程调用stop()的存在不安全性,Stop()已被放弃,因为这些方法带有强制性,不建议使用,会强制回收线程,可能会导致线程占有的资源不会正常释放。这会导致不可预料的后果。如果你确认该情况强制回收没有问题,是可以的。Interrupt()方法可以使用。我们应该用这个,相当于告诉这个线程,你应该中断了,但是中断时间由线程自己做主。线程是协作式,而不是抢占式,也就是通知,而不是强制占用停止。另外还有isInterrupted()interrupted(),两者都是用来判断当前线程是否已经被置为了中断。需要注意调用interrupted()后会将中断标志位置为true,执行完之后又变成false,其他函数没有这个操作。不建议自定义一个setCancel()来做处理,因为一旦线程被挂起,例如调用sleep()wait()take()等方法,线程根本不会去执行cancel代码块。使用isInterrupted()还有个好处,就是调用sleep()wait()take()会抛出InterruptedException异常,一旦抛出这个异常,interrupted的标志位也会被改为false。为什么JDK在此时直接要改为true呢?因为线程被唤醒之后,可能已经获得了运行的资源,如果直接中断,可能会出现资源时间浪费的情况,如果想改为true,可以调用interrupt方法。 Future可以实现中断,和线程执行完成的回调。处于死锁状态的线程不会理会interrupt。

scss 复制代码
while(!isInterrupted()) {
    try {
       Thread.sleep(100);
    } catch (InterruptedException e) {
       System.out.println(Thread.currentThread().getName()
             +" in InterruptedException interrupt flag is "
             +isInterrupted());
       // 在这里做资源释放之后再改标识位。
       interrupt();
       e.printStackTrace();
    }
    System.out.println(Thread.currentThread().getName()
          + " I am extends Thread.");
}

5、如何在Runnable去中断一个线程?

csharp 复制代码
private static class UseRunnable implements Runnable{
    
    @Override
    public void run() {
       // 中断一个线程。
       while(!Thread.currentThread().isInterrupted()) {
          System.out.println(Thread.currentThread().getName()
                + " I am implements Runnable.");
       }
       System.out.println(Thread.currentThread().getName()
             +" interrupt flag is "+Thread.currentThread().isInterrupted());
    }
}

6、连续两次调用start方法会怎么样?会报错。线程只能启动一次。run()start()的区别:

csharp 复制代码
public void run() {
    if (target != null) {
        target.run();
    }
}

如果没有指定ruannable,那么Thread直接调用了run方法是没有效果的,直接调用run(),会在当前线程执行。run可以反复调用,就是一个普通的方法。

7、线程的6种状态:

8、Join方法:A线程正在执行,调用线程B的join()方法时,A会挂起,等待B执行完之后才会继续执行A,如果B调用C线程.Join(),这样需要等BC都执行完成之后才能继续A,就像栈一样。

9、线程优先级:在操作系统层面,线程优先级的作用并不大。

10、除了main 线程,其余的都是守护线程。通过 new thread()启动的都是用户线程,非守护线程,如果你想自己设置为一个守护线程,只需要调用 useThread.setDaemon(true);另外需要注意:守护线程中finally不一定起作用。用户线程先结束,守护线程后结束。

csharp 复制代码
try {
    while (!isInterrupted()) {
       System.out.println(Thread.currentThread().getName() 
             + " I am extends Thread.");
    }
    System.out.println(Thread.currentThread().getName() 
          + " interrupt flag is " + isInterrupted());
} finally {
    // 守护线程中finally不一定起作用,这取决于当前操作系统有没有给这个守护线程分配时间片。
    System.out.println(" .............finally");
}

11、Synchoronized 关键字,内置锁,可以做用于class对象锁(也有说类锁,每个类对应的一个class对象)和对象。对象头里边会有几个标志位,可以用来标记是否获取了锁。实现原理:

可以看一下字节码:

这两个指令是由JVM虚拟机添加的。注释掉之后,这两个指令就没有了。 每一个monitor是有一个对象与之对应的,叫Monitor对象,所以Synchoronized实际上就是对这个monitor对象进行加锁,执行完之后再释放锁。详情可以参考:blog.csdn.net/JAYU_37/art... 如果给方法添加Synchoronized关键字,那么方法还会多一个ACC_SYNCHRONIZED的flag

另外需要注意,如果Synchoronized是加载方法上的,那么编译后的class 代码不会有monitorenter 和monitorexit指令,但是在运行时,JVM还是会把这两个指令加进去的。下边就是Synchoronized锁对象和锁方法的不同之处。

对象头里边包含,GC年龄,属于哪个类(类型指针),如果是数组,还会多一个长度的记录。随着对象的运行,对象头的数据也会发生变化。

一个线程的挂起和恢复,会发生两次上下文切换,大概3~5ms。第一次是从运行态到挂起,挂起之后恢复又是一次切换。这些切换很耗费时间。为了解决这些问题,就引入了偏向锁,轻量级锁,重量级锁。

12、Volatile 关键字,最轻量的同步机制;保证可见性,但是不能保证原子性,无法保证多线程安全性。在一写多读的情况下,用该方式比较合适。

13、 ThreadLocal,为每一个线程提供一个变量的副本,实现了线程的隔离。ThreadLocal以ThreadLocal为键, 以设置的值为Value,

scala 复制代码
static class ThreadLocalMap {

    /**
     * The entries in this hash map extend WeakReference, using
     * its main ref field as the key (which is always a
     * ThreadLocal object).  Note that null keys (i.e. entry.get()
     * == null) mean that the key is no longer referenced, so the
     * entry can be expunged from table.  Such entries are referred to
     * as "stale entries" in the code that follows.
     */
    static class Entry extends WeakReference<ThreadLocal<?>> {
        /** The value associated with this ThreadLocal. */
        Object value;

        Entry(ThreadLocal<?> k, Object v) {
            super(k);
            value = v;
        }
    }
// 为何定义为数组?
private Entry[] table;

这个数组的初始大小是16,为何将Entry定义为数组?请看以下的代码:

typescript 复制代码
private static ThreadLocal<Integer> s1 = new ThreadLocal<Integer>() {
    @Override
    protected Integer initialValue() {
        return 1;
    }
};
private static ThreadLocal<Integer> s2 = new ThreadLocal<Integer>() {
    @Override
    protected Integer initialValue() {
        return 2;
    }
};
private static ThreadLocal<String> s3 = new ThreadLocal<String>() {
    @Override
    protected String initialValue() {
        return "3";
    }
};

线程中我们可以定义多个ThreadLocal变量,这也是我们使用Entry数组的原因。另外,不是多个Map,而是多个ThreadLocal都可以放到一个Map中去。一个线程拥有的ThreadLocal的变量副本可能有多个需要保存,就如上边的代码所示。每一个线程都有一个自己的ThreadLocalMap,以ThreadLocal为键,传入的值为Value进行保存的。对应的数量比例,线程:ThreadLocalMap:ThreadLocal = 1 : 1 : n,但是要注意ThreadLocal可以是多个不同线程的Map的键,不过由于ThreadLocalMap是每个线程独有的,因此不会有影响。

ThreadLocal导致的一定的内存泄漏问题分析(为什么说一定的?这个后续会分析):

如图所示,在ThreadLocal中,ThreadLocalMap的entry的key是弱引用,在每次GC的时候都会回收。但是我们知道,每一个线程都有一个ThreadLocalMap,而Map中有Entry, 由于ThreadLocal这个很容易被回收,导致我们无法通过ThreadLocal.get()来获取对应的value。造成了内存泄漏。这里的GCroot就是CurrentThreadRef。只有当Thread本身被回收,才能将引用链切断。因此,使用ThreadLocal是一定要注意使用remove() 方法。

为什么说ThreadLocal是发生一定的内存泄漏? 因为他的get和set方法,实际上里边也有可能调用:

java 复制代码
private int expungeStaleEntry(int staleSlot)

这个方法就是用来清除key为null的value的。

为什么ThreadLocal使用弱引用来作为Map的key?如果是用强引用,这个内存泄漏是必然的。会导致expungeStaleEntry必然无法执行。此情况下,如果不调用remove,一定内存泄漏。

ThreadLocal 的线程不安全问题分析:

typescript 复制代码
public static Number number = new Number(0);

对于上边的变量,因为使用了static进行修饰,会导致各个线程的ThreadLocal获取到的副本无法隔离,只是隔离了引用,但是对象都是同一个。那么如何解决呢?一是直接去掉static关键字,二是在每一个ThreadLocal初始化的时候给一个初值,这样他们就能够拷贝自己的副本了。

14、线程的协作: wait,notify,notifyAll必须要在 synchronized 代码块中执行,他们都是在object中的方法,是在对象上等待和唤醒。JDK规定的,没得选,不写编译不会报错,运行必定报错。 等待和通知的标准范式 等待:synchronized(对象) { while(条件不满足){ 对象.wait() } //业务逻辑 } 通知: synchronized(对象){ //业务逻辑,改变条件 对象.notify/notifyA11(); // so some things. } 对象.wait(),会释放锁,对象.notify()/notifyAll()// 不会释放锁,直到synchronized代码块执行完成后才释放。 ,如果只有一个条件,可以用notifyAll,多个条件不可以用。如果一个对象包含了多个条件,就不能用notifyAll,因为唤醒的线程有可能不满足对应的条件。notify,随机唤起其中一个,notifyAll, 让所有的线程都来检查条件。

15、各个同步方法对锁的影响: yield():让出CPU的执行权,不会释放锁。sleep()不会释放锁;wait()会释放锁,唤醒的时候会重新竞争锁,锁到手之后才会继续执行wait()之后的代码。notifynotifyAll对锁没有影响。

相关推荐
归于尽7 分钟前
async/await 从入门到精通,解锁异步编程的优雅密码
前端·javascript
陈随易8 分钟前
Kimi k2不行?一个小技巧,大幅提高一次成型的概率
前端·后端·程序员
猩猩程序员14 分钟前
Rust 动态类型与类型反射详解
前端
杨进军15 分钟前
React 实现节点删除
前端·react.js·前端框架
yanlele37 分钟前
【实践篇】【01】我用做了一个插件, 点击复制, 获取当前文章为 Markdown 文档
前端·javascript·浏览器
爱编程的喵40 分钟前
React useContext 深度解析:告别组件间通信的噩梦
前端·react.js
望获linux2 小时前
【实时Linux实战系列】多核同步与锁相(Clock Sync)技术
linux·前端·javascript·chrome·操作系统·嵌入式软件·软件
魂祈梦2 小时前
rsbuild的环境变量
前端
赫本的猫2 小时前
告别生命周期!用Hooks实现更优雅的React开发
前端·react.js·面试
LaoZhangAI2 小时前
Browser MCP完全指南:5分钟掌握AI浏览器自动化新范式(2025最新)
前端·后端