并发基础

线程与进程理论知识基础

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对锁没有影响。

相关推荐
小小小小宇4 小时前
TS泛型笔记
前端
小小小小宇4 小时前
前端canvas手动实现复杂动画示例
前端
codingandsleeping4 小时前
重读《你不知道的JavaScript》(上)- 作用域和闭包
前端·javascript
小小小小宇5 小时前
前端PerformanceObserver使用
前端
zhangxingchao6 小时前
Flutter中的页面跳转
前端
烛阴6 小时前
Puppeteer入门指南:掌控浏览器,开启自动化新时代
前端·javascript
全宝7 小时前
🖲️一行代码实现鼠标换肤
前端·css·html
小小小小宇7 小时前
前端模拟一个setTimeout
前端
萌萌哒草头将军7 小时前
🚀🚀🚀 不要只知道 Vite 了,可以看看 Farm ,Rust 编写的快速且一致的打包工具
前端·vue.js·react.js
芝士加8 小时前
Playwright vs MidScene:自动化工具“双雄”谁更适合你?
前端·javascript