线程安全和jvm的基础

start()和run()的区别

  • 一个问题:为什么我们调用start()方法时会执行run()方法,为什么我们不能直接调用run()方法?
  • start() 可以启动一个新线程,run()不能
  • start()不能被重复调用,run()可以
  • start()中的run代码可以不执行完就继续执行下面的代码,即进行了线程切换。直接调用run方法必须等待其代码全部执行完才能继续执行下面的代码。
  • start() 实现了多线程,run()没有实现多线程。

notify 和wait

  • 可以看看此博客 https://www.cnblogs.com/moongeek/p/7631447.html
  • 当线程执行wait()方法时候,会释放当前的锁,然后让出CPU,进入等待状态。
  • 只有当 notify/notifyAll() 被执行时候,才会唤醒一个或多个正处于等待状态的线程,然后继续往下执行,直到执行完synchronized 代码块的代码或是中途遇到wait() ,再次释放锁。

notify 和 notifyAll的区别

  • notify方法只唤醒一个等待(对象的)线程并使该线程开始执行。所以如果有多个线程等待一个对象,这个方法只会唤醒其中一个线程,选择哪个线程取决于操作系统对多线程管理的实现。notifyAll 会唤醒所有等待(对象的)线程,尽管哪一个线程将会第一个处理取决于操作系统的实现。如果当前情况下有多个线程需要被唤醒,推荐使用notifyAll 方法。比如在生产者-消费者里面的使用,每次都需要唤醒所有的消费者或是生产者,以判断程序是否可以继续往下执行。

  • notify唤醒沉睡的线程后,线程会接着上次的执行继续往下执行。所以在进行条件判断时候,可以先把 wait 语句忽略不计来进行考虑,显然,要确保程序一定要执行,并且要保证程序直到满足一定的条件再执行,要使用while来执行,以确保条件满足和一定执行。如下代码:

  public class K {
//状态锁
private Object lock;
//条件变量
private int now,need;
public void produce(int num){
    //同步
    synchronized (lock){
       //当前有的不满足需要,进行等待
        while(now < need){
            try {
                //等待阻塞
                wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("我被唤醒了!");
        }
       // 做其他的事情
    }
}
}

sleep 和 wait

  • sleep是Thread的静态方法,调用此方法,不会放开锁,当前线程一直处于同步控制,sleep不会让出系统资源,唤醒sleep需要使用interrupt()打扰方法
  • wait是Object的方法,wait方法进入线程池等待,并释放cpu资源,唤醒wait方法需要使用notify()通知方法

synchronized

三种使用方式
  • 同步普通方法,锁的是当前对象。
  • 同步静态方法,锁的是当前 Class 对象。
  • 同步块,锁的是 {} 中的对象

考虑一下为什么这个字段是解决并发问题常用解决方案 可以看一下这篇文章 https://www.jianshu.com/p/2ba154f275ea

  • 这里需要理解一下jvm的问题

  • JVM 是通过进入、退出对象监视器( Monitor )来实现对方法、同步块的同步的。

  • 具体实现是在编译之后在同步方法调用前加入一个 monitor.enter 指令,在退出方法和异常处插入 monitor.exit 的指令。

  • 其本质就是对一个对象监视器( Monitor )进行获取,而这个获取过程具有排他性从而达到了同一时刻只能一个线程访问的目的。

  • 而对于没有获取到锁的线程将会阻塞到方法入口处,直到获取锁的线程 monitor.exit 之后才能尝试继续获取锁。

代码演示

 public static void main(String[] args) {
    synchronized (Synchronize.class){
        System.out.println("Synchronize");
    }
}

使用javap -c synchronize

  public class com.crossoverjie.synchronize.Synchronize {
  public com.crossoverjie.synchronize.Synchronize();
Code:
   0: aload_0
   1: invokespecial #1                  // Method java/lang/Object."<init>":()V
   4: return

 public static void main(java.lang.String[]);
Code:
   0: ldc           #2                  // class com/crossoverjie/synchronize/Synchronize
   2: dup
   3: astore_1
   **4: monitorenter**
   5: getstatic     #3                  // Field java/lang/System.out:Ljava/io/PrintStream;
   8: ldc           #4                  // String Synchronize
  10: invokevirtual #5                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
  13: aload_1
  **14: monitorexit**
  15: goto          23
  18: astore_2
  19: aload_1
  20: monitorexit
  21: aload_2
  22: athrow
  23: return
Exception table:
   from    to  target type
       5    15    18   any
      18    21    18   any
}

可以看到在同步块的入口和出口分别有 monitorenter,monitorexit

#看到这里我想起了JVM,顺便了解一下吧 可以看看这个文章https://zhuanlan.zhihu.com/p/25511795

jvm

  • jvm的内部结构
  • 程序计数器
  • 堆内存
  • 虚拟机栈
  • 方法区
  • 本地方法栈

这里的堆内存和方法区是线程共享的,其他的都是线程私有的

一个一个了解一下

  • 堆内存:我们new出来的对象实例就放在这里面
  • 方法区:存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据 记住类信息、常量、静态变量就可以了吧
  • 程序计数器:当前线程所执行的字节码的行号指示器,比如说执行下一个命令是if 还是else
  • 虚拟机栈:存放基本类型数据,对象的地址这些,每一个方法被调用直至执行完成的过程,就对应着一个栈帧在虚拟机栈中从入栈到出栈的过程。
  • 本地方法区:与虚拟机栈相似,区别在于虚拟机栈执行字节码文件,本地方法区为native方法服务

#GC算法,垃圾回收

  • 垃圾回收,7月1号开始上海实施了垃圾回收,四个垃圾桶,放不同种类的垃圾
  • 对于jvm 垃圾回收,首先要判断当前对象是否存活,才去回收可回收的对象

判断哪些可以回收?即对象是否存活啊

判断一个对象是否存活有两种方式:

  • 1,每个对象有一个引用计数的属性,引用释放时减1,当计数为0即可回收,但是没办法解决对象循环调用的问题啊;
  • 2,可达性分析:从GC roots开始向下搜索,搜索时走过的路径称为引用链,当一个对象没有任何引用链相连时,则证明此对象不可用,称之为不可达对象

如何回收?回收的算法

  • 标记清除算法:先标记所有可以回收的对象,然后一次性回收;
  • 复制算法:内存分为两个大小相等的容量模块,每次使用其中的一块,当此块内存用完了,将还存活的对象复制到另一个空模块,然后把已经当前模块清除一次;
  • 标记压缩算法:与标记清除类似,但是后续步骤不是讲可回收对象清理,而是存活对象 往一边移动,可回收对象往一边移动,然后直接清除边界的可回收对象数据;
  • 分代收集算法:java堆分为新生代和旧生代,然后根据各个年代的特点进行最适当的收集算法;

找到可回收的对象,也有算法可以执行了,现在需要一个容器去干回收这件事情

  • Serial收集器 :串行收集器,串行收集器是最古老,最稳定以及效率高的收集器,可能会产生较长的停顿,只使用一个线程去回收。
  • ParNew收集器 :ParNew收集器其实就是Serial收集器的多线程版本。多线程的区别
  • Parallel收集器:Parallel Scavenge收集器类似ParNew收集器,Parallel收集器更关注系统的吞吐量,吞吐量的差别
  • Parallel Old 收集器:Parallel Old是Parallel Scavenge收集器的老年代版本,使用多线程和“标记-整理”算法 吞吐量的同时还有多线程,同事使用标记清除的算法;
  • CMS收集器:CMS(Concurrent Mark Sweep)收集器是一种以获取最短回收停顿时间为目标的收集器。 为了更快的收集垃圾
  • G1收集器:G1 (Garbage-First)是一款面向服务器的垃圾收集器,主要针对配备多颗处理器及大容量内存的机器. 以极高概率满足GC停顿时间要求的同时,还具备高吞吐量性能特征

cms收集器的垃圾回收流程 具体可以看这篇文章 https://my.oschina.net/hosee/blog/644618

  • 初始标记:会产生全局停顿,标记GC roots可以直接关联的对象,速度快
  • 并发标记:和用户线程一起,主要标记过程,标记全部对象
  • 重新标记:产生全局停顿,由于并发标记的时候,用户线程还在运行,因此正式清理前,在做修证
  • 并发清除:和用户线程一起,基于标记结果,直接清理对象
  • 特点:用户线程运行的过程中,分一半的cpu去运行GC,反应速度就会下降一半;清理阶段,用户线程又产生了大量对象,产生新的垃圾

GC分析 命令调优 这么没做过,没有实践过,感觉这件事情就很遥远,了解一下吧,不想去死记硬背

有了运行时数据区域 runtime data area ,有了垃圾的回收机制,我们来了解一个一个类从无到有的整个过程吧

  • 类加载过程:将字节码文件以二进制的方式读入内存中,将其放在运行时数据区域内,堆内存存class对象,方法区存类的数据包括常量,静态变量,类的信息等,并对数据进行验证,然后开始解析,初始化数据,new出对象,最后垃圾回收
  • 类的生命周期:加载、连接、初始化、使用和卸载
  • 了解了类的加载过程和生命周期,此时我们需要一个加载工具来加载类啊,名为类加载器
  • 类的加载机制:双亲委派模型

双亲委派模型

  • class文件交由上层ClassLoader加载,如果已经加载,直接返回,如果没有加载,找到上层加载器,如果存在上层加载器,交由上层加载器加载,如果不存在,继续向上委派,交个BootStrapClassLoader加载,所有上层加载器都无法加载,抛出ClassNotFound异常
  • java提供三个默认的类加载器 ,BootStrapClassLoader:由C++编写,加载JRE/lib/rt.jar文件;ExtensionClassLoader 和ApplicationClassLoader由java编写;Extension加载JRE/lib/ext orjava.ext.dirs; Application 加载classParh -cp

通过java.lang.classloader下面的方法loadClass了解

protected Class<?> loadClass(String name, boolean resolve)
    throws ClassNotFoundException
{
    synchronized (getClassLoadingLock(name)) {
        // First, check if the class has already been loaded
        // 首先,检查该Class是否已经被加载,如果已加载直接返回。
        Class c = findLoadedClass(name);
        // 没有被加载
        if (c == null) {
            long t0 = System.nanoTime();
            try {
                //是否存在上层加载器,如果存在交由上层加载器加载
                if (parent != null) {
                    c = parent.loadClass(name, false);
                } else {//如果不存在继续向上委派给BootstarapClassLoader加载
                    c = findBootstrapClassOrNull(name);
                }
            } catch (ClassNotFoundException e) {
                // ClassNotFoundException thrown if class not found
                // from the non-null parent class loader
            }
               //所有上层加载器都无法加载,由当前加载器进行加载
            if (c == null) {
                // If still not found, then invoke findClass in order
                // to find the class.
                long t1 = System.nanoTime();
                c = findClass(name);

                // this is the defining class loader; record the stats
                sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
                sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
                sun.misc.PerfCounter.getFindClasses().increment();
            }
        }
        if (resolve) {
            resolveClass(c);
        }
        return c;
    }
}

看完这些,在来详细了解一下jvm内存结构,不要浅尝辄止,来吧,可以看看这篇文章 https://www.cnblogs.com/ityouknow/p/5610232.html

堆内存

  • 堆内存有年轻代和老年代组成;年轻代又分为三部分:Eden区(伊甸区,亚当和夏娃居住的地方,称之为初始区域没毛病吧);From survivor 来自生存区; To survivor 去生存区 默认按照8:1:1分配

  • 新生代主要使用复制算法

  • 老年代使用标记压缩算法

  • 新建(New)或者短期的对象存放在Eden区域;

  • 幸存的或者中期的对象将会从Eden区域拷贝到Survivor区域;

  • 始终存在或者长期的对象将会从Survivor拷贝到Old Generation;

控制参数 jvmn内存结构图可以看链接里面的文章

  • -Xms设置堆的最小空间大小。

  • -Xmx设置堆的最大空间大小。

  • -XX:NewSize设置新生代最小空间大小。

  • -XX:MaxNewSize设置新生代最大空间大小。

  • -XX:PermSize设置永久代最小空间大小。

  • -XX:MaxPermSize设置永久代最大空间大小。

  • -Xss设置每个线程的堆栈大小。

一个问题String 常量池存在哪个内存区域呢?

  • String 常量池存在于方法区
  • 为什么放在方法区? 1:方法区放的是永远唯一的元素,class,static等;2.被所有线程共享,String 是不可变的,不用担心数据冲突进行共享
  • 具体关于String常量池的设计思想请看这篇文章:https://segmentfault.com/a/1190000009888357#articleHeader0

言归正传

  • 线程锁是由jvm monitor监视器监控 的,只有拿到 moitor.exit,下一个线程才能进入,否则线程阻塞

大部分面试都会问到的题目

Lock和synchronized 的使用原理和区别

  • lock 是一个接口,synchronized 是一个关键字,由内置语言实现
  • synchronized 发生异常会自动放开锁,而 lock不会,所以需要在finally中调用lock.unlock()方法释放锁
  • lock可以使用lock.lockInterruptibly()方法使线程响应中断,而synchronized会使线程一直等待下去
  • 通过lock的lock.tryLock()方法可以知道有没有成功获取锁,而synchroized不可以
  • ReentrantReadWriteLock允许多个读线程同时访问

synchronized锁升级 偏向锁到轻量级锁再到重量级锁,它们分别是怎么实现的,解决的是哪些问题,什么时候会发生锁升级 我是根据这篇文章总结的https://mp.weixin.qq.com/s/p1YnY486IAUrtgEhFzdtWA

  • 偏向锁:认为只有一个线程;当线程第一次进入方法时,使用CAS机制来标记当前方法有线程在执行,并记录当前线程的ID,记录当前是哪个线程执行当前方法,方法执行结束,直接退出,当该线程第二次进入方法,判断Id与自己想等,直接进入;
  • 然而现实残酷,当多线程时,另一个线程发现当前Id不是自己,偏向锁就不在适用了,这个时候偏向锁升级为轻量级锁
  • 轻量级锁:为了避免动不动就加锁,因为加锁也是需要消耗操作系统资源的,于是出现了轻量级锁;轻量级锁认为,当一个线程进入一个方法,我们做一个标记,表示当前方法有线程在使用,其他线程就无法进入,当线程退出就去掉标记
  • 采用CAS机制进行标记将方法标记为已经在执行,退出时,表示已经没人在执行;使用CAS的原因是因为状态的改变需要是一个原子性的操作,而CAS就保证了原子性,轻量级锁适用于竞争较少的情况下,也就是多个线程错开时间获取锁的情况当真的遇到了竞争,需要将轻量级锁升级为重量级锁
  • 重量级锁:当进入一个线程安全的,同步的方法时,需要获取方法的锁,退出时,需要释放锁,如果获取不到锁,会进入阻塞状态,当持有锁的线程释放锁,然后我们把阻塞的线程唤醒,这种获取不到锁的进入阻塞状态的,称为重量级锁

这里的以后在补充吧,还有几个问题等待学习

https://liuzhengyang.github.io/2017/05/12/aqs/

说说 CountDownLatch 原理
  • countDownLatch 这个是为了说我们在开发是有多个线程同时运行,最后我们需要一个线程来等待上面的多个线程结束才执行,countDownlatch 倒计时门栓就是解决这个问题的
说说 CyclicBarrier 原理
  • cyclic 周期,循环 barrier 屏障 这里举一个通俗的例子,运动员在奔跑之前会做每个人不同的动作,比如喝水,拉伸等,但是比赛快开始时,他们都会在跑道起跑线前开始准备奔跑,起跑线就是一道屏障,这里要注意一点任何一个运动员没有准备好之前,起跑线屏障都是没用的,所以CyclicBarrier的同步机制就是,每个线程都会等待别的线程到达屏障,因为这个屏障可以重复利用,所以又叫做循环屏障。cyclic 周期,循环
说说 Semaphore 原理
  • Semaphore管理着一组许可(permit) 控制资源能够被并发访问的线程数量 ,举一个例子,公司有10个厕所,早上高峰时期,大家要上厕所,此时信号量管理者10个厕所,但是却有12个人要上,此时另外两个人只能阻塞等待,如果走了两个人,后面的两个人才会进来 https://juejin.im/post/5a38d2046fb9a045076fcb1f
说说 Exchanger 原理
  • 线程间交换数据的工具,两个线程交换数据,举个栗子:青春时代,男女交换情书就是,男孩等待女孩从教室出来,然后交换情书,此时教室就是一个同步点 https://juejin.im/post/5aeec49b518825673614d183
  • 说说 CountDownLatch 与 CyclicBarrier 区别

#线程池的实现原理 看这两篇 https://www.jianshu.com/p/125ccf0046f3 https://mp.weixin.qq.com/s/QRp-y-YnMLZJkiG6XH7eGQ

  • 创建线程池的四种方式
  • 在java.util.current包中Executors调用四个静态方法实例化四个不同的线程池
  • 1.newCachedThreadPool可缓存线程池,如果线程池长度超过处理需要,可灵活回收空闲线程,若无可回收,则新建线程
  • 2.newFixedThreadPool 创建一个定长线程池,可控制线程最大并发数,超出的线程会在队列中等待。
  • 3.newShecudledThreadPool 创建一个定长线程池,支持定时及周期性任务执行.
  • 4.newSingleThreadExecutor 创建一个单线程化的线程池,它只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序(FIFO, LIFO, 优先级)执行。

自定义的线程池

  • ThreadPoolExecutor threadPoolExecutor=new ThreadPoolExecutor()
  • 它的构造方法一般需要重载方法 如下:

                   `                
                    ThreadPoolExecutor(int corePoolSize,
                          int maximumPoolSize,
                          long keepAliveTime,
                          TimeUnit unit,
                          BlockingQueue<Runnable> workQueue,
                          ThreadFactory threadFactory,
                          RejectedExecutionHandler handler)`
  • 一个一个解释一下构造方法里面的属性的意思
  • corePoolSize 顾名思义 :核心线程池大小
  • maximumPoolSize 最大线程池数量
  • keepAliveTime 空闲线程存活时间
  • unit 空闲线程存活时间单位
  • workQueue 阻塞队列 一般使用ArrayBlockingQueue, LinkedBlockingQueue, SynchronousQueue, PriorityBlockingQueue方式来初始化
  • threaFactory 创建线程的工厂类 一般情况下 会调用另一个重载方法,会有 Executors.defaultThreadFactory()默认
  • handler 饱和策略 当前队列已满,线程都已经开启,当前线程池都已经处于饱和状态,需要策略处理当前情况;以下几种解决策略:
  • 1.AbortPolicy: 直接拒绝所提交的任务,并抛出RejectedExecutionException异常;
  • 2.CallerRunsPolicy:只用调用者所在的线程来执行任务;
  • 3.DiscardPolicy:不处理直接丢弃掉任务
  • 4.DiscardOldestPolicy:丢弃掉阻塞队列中存放时间最久的任务,执行当前任务

接下来从源码角度看看线程池执行逻辑

public void execute(Runnable command) {
if (command == null)
    throw new NullPointerException();
/*
 * Proceed in 3 steps:
 *
 * 1. If fewer than corePoolSize threads are running, try to
 * start a new thread with the given command as its first
 * task.  The call to addWorker atomically checks runState and
 * workerCount, and so prevents false alarms that would add
 * threads when it shouldn't, by returning false.
 *
 * 2. If a task can be successfully queued, then we still need
 * to double-check whether we should have added a thread
 * (because existing ones died since last checking) or that
 * the pool shut down since entry into this method. So we
 * recheck state and if necessary roll back the enqueuing if
 * stopped, or start a new thread if there are none.
 *
 * 3. If we cannot queue task, then we try to add a new
 * thread.  If it fails, we know we are shut down or saturated
 * and so reject the task.
 */
int c = ctl.get();
//如果线程池的线程个数少于corePoolSize则创建新线程执行当前任务
if (workerCountOf(c) < corePoolSize) {
    if (addWorker(command, true))
        return;
    c = ctl.get();
}
//如果线程个数大于corePoolSize或者创建线程失败,则将任务存放在阻塞队列workQueue中
if (isRunning(c) && workQueue.offer(command)) {
    int recheck = ctl.get();
    if (! isRunning(recheck) && remove(command))
        reject(command);
    else if (workerCountOf(recheck) == 0)
        addWorker(null, false);
}
//如果当前任务无法放进阻塞队列中,则创建新的线程来执行任务
else if (!addWorker(command, false))
    reject(command);

}

具体逻辑流程

  • 1.判断核心线程池是否都在执行任务,如果不是,创建新的线程执行刚提交的任务,否则,核心线程池已满,都在执行任务,进入第2步
  • 2.判断当前阻塞队列是否已满,若未满,将当前线程放入队列中,否则进行第3步
  • 3.判断当前是否已经达到最大线程池数量,如果没有,创建新的线程执行任务,否则,按照饱和策略进行处理,目前我们使用的是 DiscardOldestPolicy:丢弃掉阻塞队列中存放时间最久的任务,执行当前任务

###关闭线程

  • shutdown() 执行后停止接受新任务,会把队列的任务执行完毕。

  • shutdownNow() 也是停止接受新任务,但会中断所有的任务,将线程池状态变为 stop。

合理配置参数

  • 任务的性质:CPU密集型任务,IO密集型任务和混合型任务。
  • 任务的优先级:高,中和低。
  • 任务的执行时间:长,中和短。
  • 任务的依赖性:是否依赖其他系统资源,如数据库连接。

#ThreadLocal的实现原理 请看文章https://www.jianshu.com/p/98b68c97df9b
#ThreadLocal内存泄漏真因探究,看文章 https://www.jianshu.com/p/a1cd61fa22da
#这篇线程总结 可以看看https://www.cnblogs.com/wxd0108/p/5479442.html

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值