<返回更多

深入理解与应用多线程技术

2024-01-09  微信公众号  一安未来
加入收藏
如果synchronized​作用于代码块,反编译可以看到两个指令:monitorenter、monitorexit,JVM​使用monitorenter和monitorexit​两个指令实现同步;如果作用synchronized​作用于方法,反编译可以看到ACCSYNCHRONIZED​标记,JVM​通过在方法访问标识符(flags​)中加入ACCSYNCHRONIZED来实现同步功能。

为什么要使用多线程

  1. 提高响应速度:对于耗时操作,使用线程可以避免阻塞主线程,提高应用程序的响应速度。
  2. 实现并行操作:在多CPU系统中,使用线程可以并行处理任务,提高CPU利用率。
  3. 改善程序结构:将一个既长又复杂的进程分为多个线程,可以使其成为几个独立或半独立的运行部分,这样有利于程序的修改和理解。
  4. 方便的通信机制:线程间可以通过共享内存等方式进行通信,比进程间通信更方便、高效。

创建线程有几种方式?

创建线程有四种方式:

  1. 通过继承Thread类来创建线程。
  2. 通过实现Runnable接口来创建线程。
  3. 通过实现Callable接口来创建线程。
  4. 使用Executor框架来创建线程池。

简单实现

public class ThreadTest {

    public static void mAIn(String[] args) {
        Thread thread = new MyThread();
        thread.start();
    }
}

class MyThread extends Thread {
    @Override
    public void run() {
        System.out.println("关注公众号:一安未来");
    }
}
 
public class ThreadTest {

    public static void main(String[] args) {
        MyRunnable myRunnable = new MyRunnable();
        Thread thread = new Thread(myRunnable);
        thread.start();
    }
}

class MyRunnable implements Runnable {
    @Override
    public void run() {
        System.out.println("关注公众号:一安未来");
    }
}
 
public class ThreadTest {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        MyThreadCallable mc = new MyThreadCallable();
        FutureTask<Integer> ft = new FutureTask<>(mc);
        Thread thread = new Thread(ft);
        thread.start();
        System.out.println(ft.get());
    }
}

class MyThreadCallable implements Callable {
    @Override
    public String call()throws Exception {
        return "关注公众号:一安未来";
    }
}
public class ThreadTest {

    public static void main(String[] args) throws Exception {

        ThreadPoolExecutor executorOne = new ThreadPoolExecutor(5, 5, 1,
                TimeUnit.MINUTES, new ArrayBlockingQueue<Runnable>(20), new CustomizableThreadFactory("Yian-Thread-pool"));
        executorOne.execute(() -> {
            System.out.println("关注公众号:一安未来");
        });

        //关闭线程池
        executorOne.shutdown();
    }
}

线程和进程的区别

线程和进程是操作系统中重要的概念,都是操作系统资源分配的基本单位,但它们有一些关键的区别。

  1. 地址空间和资源拥有:进程是执行中的一个程序,具有自己的地址空间和文件描述符等资源。线程是在进程中执行的一个单独的执行路径,共享进程的地址空间和资源。
  2. 开销:创建和销毁一个进程需要保存寄存器、栈信息以及进行资源分配和回收等操作,开销较大。而线程的创建和销毁只需保存寄存器和栈信息,开销较小。
  3. 通信切换:进程之间必须通过IPC(进程间通信)进行通信,切换开销相对较大。线程之间可以直接共享进程的地址空间和资源,切换开销相对较小。
  4. 并发性:进程是独立的执行单元,具有自己的调度算法,在并发条件下更加稳定可靠。而线程共享进程的资源,线程之间的调度和同步比较复杂,对并发条件的处理需要更多的注意。
  5. 一对多的关系:一个线程只能属于一个进程,而一个进程可以拥有多个线程。

Runnable和 Callable有什么区别

volatile作用,原理

主要用于声明变量,以指示该变量可能会被多个线程同时访问,从而防止编译器进行一些优化,确保线程之间能够正确地读写共享变量。volatile 提供了一种轻量级的同步机制,但它并不能替代 synchronized,因为它无法解决复合操作的原子性问题。

 

作用:

原理:

volatile 的实现原理涉及到 CPU 的缓存一致性和内存屏障(Memory Barrier)的概念。

synchronized 的实现原理以及锁优化

如果synchronized作用于代码块,反编译可以看到两个指令:monitorenter、monitorexit,JVM使用monitorenter和monitorexit两个指令实现同步;如果作用synchronized作用于方法,反编译可以看到ACCSYNCHRONIZED标记,JVM通过在方法访问标识符(flags)中加入ACCSYNCHRONIZED来实现同步功能。

monitor监视器

操作系统的管程(monitors)是概念原理,ObjectMonitor是它的原理实现。

图片图片

JAVA虚拟机(HotSpot)中,Monitor(管程)是由ObjectMonitor实现的,其主要数据结构如下:

ObjectMonitor() {
    _header       = NULL;
    _count        = 0; // 记录个数
    _waiters      = 0,
    _recursions   = 0;
    _object       = NULL;
    _owner        = NULL;
    _WaitSet      = NULL;  // 处于wait状态的线程,会被加入到_WaitSet
    _WaitSetLock  = 0 ;
    _Responsible  = NULL ;
    _succ         = NULL ;
    _cxq          = NULL ;
    FreeNext      = NULL ;
    _EntryList    = NULL ;  // 处于等待锁block状态的线程,会被加入到该列表
    _SpinFreq     = 0 ;
    _SpinClock    = 0 ;
    OwnerIsThread = 0 ;
  }

Java Monitor 的工作机理

图片图片

对象与monitor关联

图片图片

Mark Word 是用于存储对象自身的运行时数据,如哈希码(HashCode)、GC分代年龄、锁状态标志、线程持有的锁、偏向线程 ID、偏向时间戳等。

图片图片

重量级锁,指向互斥量的指针。其实synchronized是重量级锁,也就是说Synchronized的对象锁,Mark Word锁标识位为10,其中指针指向的是Monitor对象的起始地址。

 

在JDK1.6之前,synchronized的实现直接调用ObjectMonitor的enter和exit,这种锁被称之为重量级锁。从JDK6开始,HotSpot虚拟机开发团队对Java中的锁进行优化,如增加了适应性自旋、锁消除、锁粗化、轻量级锁和偏向锁等优化策略,提升了synchronized的性能。

线程有哪些状态

图片图片

CountDownLatch与CyclicBarrier 区别

CountDownLatch和CyclicBarrier都用于让线程等待,达到一定条件时再运行。主要区别是:

图片图片

多线程环境下的伪共享

 

CPU的缓存是以缓存行(cache line)为单位进行缓存的,当多个线程修改相互独立的变量,而这些变量又处于同一个缓存行时就会影响彼此的性能。这就是伪共享

现代计算机计算模型:

图片图片

也正是因为缓存行的存在,就导致了伪共享问题,如图所示:

图片图片

假设数据a、b被加载到同一个缓存行。

解决伪共享问题的一种方法是通过填充(Padding)来确保共享的变量独立存储于不同的缓存行中。填充的思想是在变量之间插入一些无关的数据,使它们分布到不同的缓存行,从而避免多个变量共享同一个缓存行。

在Java中,可以使用@Contended注解来避免伪共享。这个注解可以在字段上使用,它会在字段的前后插入填充,使得字段单独占据一个缓存行。

Fork/Join框架

Fork/Join框架是Java7提供的一个用于并行执行任务的框架,是一个把大任务分割成若干个小任务,最终汇总每个小任务结果后得到大任务结果的框架。

Fork/Join框架需要理解两个点,「分而治之」和「工作窃取」。

分而治之

图片图片

工作窃取

图片图片

一般就是指做得快的线程(盗窃线程)抢慢的线程的任务来做,同时为了减少锁竞争,通常使用双端队列,即快线程和慢线程各在一端。

ThreadLocal原理

ThreadLocal的内存结构图:

图片图片

 

内存泄露问题:指程序中动态分配的堆内存由于某种原因没有被释放或者无法释放,造成系统内存的浪费,导致程序运行速度减慢或者系统奔溃等严重后果。内存泄露堆积将会导致内存溢出。

ThreadLocal的内存泄露问题一般考虑和Entry对象有关,ThreadLocal::Entry被弱引用所修饰。JVM会将弱引用修饰的对象在下次垃圾回收中清除掉。这样就可以实现ThreadLocal的生命周期和线程的生命周期解绑。但实际上并不是使用了弱引用就会发生内存泄露问题,考虑下面几个过程:

图片图片

当ThreadLocal Ref被回收了,由于在Entry使用的是强引用,在Current Thread还存在的情况下就存在着到达Entry的引用链,无法清除掉ThreadLocal的内容,同时Entry的value也同样会被保留;也就是说就算使用了强引用仍然会出现内存泄露问题。

图片图片

当ThreadLocal Ref被回收了,由于在Entry使用的是弱引用,因此在下次垃圾回收的时候就会将ThreadLocal对象清除,这个时候Entry中的KEY=null。但是由于ThreadLocalMap中任然存在Current Thread Ref这个强引用,因此Entry中value的值任然无法清除。还是存在内存泄露的问题。

AQS实现原理

 

AbstractQueuedSynchronizer(AQS)是Java中用于构建同步器的基础框架。它提供了一个灵活的、可重用的同步器实现,可以用来构建各种同步工具,如ReentrantLock、Semaphore、CountDownLatch等。AQS的核心思想是基于FIFO等待队列,通过状态(state)来管理线程的同步。

核心原理:

ReentrantLock 解析:

图片图片

图片图片

上下文切换

 

CPU上下文:CPU 寄存器,是CPU内置的容量小、但速度极快的内存。而程序计数器,则是用来存储 CPU 正在执行的指令位置、或者即将执行的下一条指令位置。它们都是 CPU 在运行任何任务前,必须的依赖环境,因此叫做

CPU上下文切换:把前一个任务的CPU上下文(也就是CPU寄存器和程序计数器)保存起来,然后加载新任务的上下文到这些寄存器和程序计数器,最后再跳转到程序计数器所指的新位置,运行新任务。

图片图片

关键词:多线程技术      点击(7)
声明:本站部分内容来自互联网,如有版权侵犯或其他问题请与我们联系,我们将立即删除或处理。
▍相关推荐
更多多线程技术相关>>>