主页

桥模式

2020-10-20
设计模式

  • 模式定义:将抽象与实现分离,使它们都可以独立地变化。
  • 应用场景:两个非常强的变化维度。

案例 #

场景

  1. 分析一下,首先会创建1个电脑类,然后创建3个电脑的子类(即电脑的类型),再创建3x3个子类。假设有n种电脑类型,m种电脑品牌,那么产生的类的数量为1+n+nxm

  2. 如果再加一个电脑品牌acer,则需要再添加三个类,即acer台式机、acer笔记本、acer平板。显然这种方式产生的类的数量非常多。

  3. 此外,这个实现违背了单一职责原则,类中出现了两个变化(电脑类型和品牌)。

  4. 解决办法:将类型写成一个抽象类,将品牌写成一个抽象类。通过一个"桥"将他们联系起来。

//品牌类
interface Brand{
    void info();
}
class Lenovo implements Brand{
    public void info(){
        //...
    }
}
class Apple implements Brand{
    public void info(){
        //...
    }
}
class Dell implements Brand{
    public void info(){
        //...
    }
}

//电脑类
abstract class Computer{
    //通过类组合来替代类继承
    protected Brand brand;
    
    public Computer(Brand brand){this.brand=brand;}
    
    abstract void info();
}

class Desktop extends Computer{
    public Desktop(Brand brand){super(brand);}
    public void info(){
        //...
    }
}

class Laptop extends Computer{
    public Laptop(Brand brand){super(brand);}
    public void info(){
        //...
    }
}
class Pad extends Computer{
    public Pad(Brand brand){super(brand);}
    public void info(){
        //...
    }
}
  1. 通过上面的修改,类的数量变成了1+n+m
  2. 在这里我理解的抽象是Brand,实现指的是Computer和Computer的子类。

synchronized

2020-10-13
java

轻量级锁 #

  • 使用场景:如果一个对象虽然有多线程访问,但多线程的访问时间是错开的(也就是没有竞争),那么可以使用轻量级锁来优化。
  • 使用线程的栈中的锁记录充当锁。
  • 轻量级锁对使用者是透明的,即语法仍然是synchronized。
  • 轻量级锁在没有竞争时,每次重入仍需要执行CAS操作。

锁膨胀 #

  • 如果在尝试加轻量级锁的过程中,CAS操作无法成功,这时一种情况就是有其他线程为此对象加上了轻量级锁(有竞争),这时需要进行锁膨胀,将轻量级锁变为重量级锁。

自旋优化 #

  • 重量级锁竞争的时候,还可以使用自旋来优化,如果当前线程自旋成功(即这时候持锁线程已经退出了同步块,释放了锁),这时当前线程就可以避免阻塞。

偏向锁 #

  • 只有在第一次使用CAS将线程ID设置到对象头的Mark Word中,之后发现这个线程ID是自己的就表示没有竞争,不用重新CAS。以后只要不发生竞争,这个对象就归该线程所有。
  • 调用对象的hashCode()会禁用该对象的偏向锁;当有其他线程使用偏向锁对象时,会将偏向锁升级为轻量级锁;调用wait/notify会转化为重量级锁。
  • 批量重偏向:如果对象虽然被多个线程访问,但是没有竞争,这时候偏向了t1的线程的对象仍有机会重新偏向t2,重偏向会重置对象的ThreadID。当撤销偏向锁阈值超过20次后,jvm会觉得:我是不是偏向错了呢,于是会在给这些对象加锁时重新偏向至加锁线程。
  • 批量撤销:当撤销偏向锁阈值超过40次后,jvm会觉得:自己确实错了,根本就不应该偏向。于是整个类的所有对象都会变成不可偏向的,新建的对象也是不可偏向的,针对类的优化。

Monitor

2020-10-12
java

java对象头 #

32位虚拟机下

  • 普通对象

普通对象

  • 数组对象

数组对象

  • mark word结构

mark word结构

  • 64位虚拟机下mark word结构

64位虚拟机下mark word结构

Monitor #

  • 每个java对象都可以关联一个monitor对象,monitor对象由操作系统提供。如果使用synchronized给对象上锁(重量级)后,该对象对象头的mark word中的数据就会清空(不包括标志位,但标志位会发生改变),然后指向一个monitor对象。

java线程的6种状态

2020-10-08
java

  • 操作系统层面线程有5种状态

  • 在 java中,Thread.State 将线程分为六种状态

public enum State {
        // 线程刚被创建,但是还没调用start方法
        NEW,

        /**
         * 该状态的线程在jvm中是执行状态,但是在操作系统中可能是在等待其他的资源。
         * 此状态涵盖了操作系统中的 运行态、就绪态、阻塞态
         */
        RUNNABLE,

        /**
         * 此状态的线程会等待一个monitor lock。
         * A thread in the blocked state is waiting for a monitor lock 
         * to enter a synchronized block/method or
         * reenter a synchronized block/method after calling Object.wait
         */
        BLOCKED,

        /**
         * Thread state for a waiting thread.
         * 以下方法可使线程进入此状态:
         * 	 Object.wait with no timeout
         * 	 Thread.join with no timeout 
         * 	 LockSupport.park
         * 该状态的线程会等待其他线程通过特定的动作唤醒。
         */
        WAITING,

        /**
         * Thread state for a waiting thread with a specified(特定的) waiting time.
         * 以下方法可使线程进入此状态:
         * 	 Thread.sleep
         * 	 Object.wait with timeout
         * 	 Thread.join with timeout
         * 	 LockSupport.parkNanos
         * 	 LockSupport.parkUntil
         */
        TIMED_WAITING,

        // 线程已经完成了执行,终止了的状态。
        TERMINATED;
    }

并发相关信息

2020-10-08
java

Thread方法 #

  • sleep:让当前线程休眠n毫秒,休眠时让出cpu的时间片给其他线程。

  • join:等待某个线程运行结束。

  • yield:提示线程调度器让出当前线程对 CPU的使用。

  • interrupt():打断线程,可能会产生打断标记(看下面的介绍)。

    • 可以使用isInterrupted()判断线程是否被打断。
    • 如果线程正在sleep、wait、join会导致被打断的线程抛出InterruptedException,并清除打断标记。
    • 如果打断正在运行的线程,则会设置打断标记。
    • interrupted()方法能返回打断标记的状态,并将打断标记设置为假。

同步 #

  • 同步:需要等待结果返回,才能继续运行就是同步。
  • 异步:不需要等待结果返回,就能继续运行就是异步。

常见线程安全类 #

String Integer StringBuffer Random Vector Hashtable java.util.concurrent

wait和notify

2020-10-06
java

  • 在调用wait方法时,线程必须要持有被调用对象的锁,当调用wait方法之后,线程就会释放掉该对象的锁。
  • 在调用Thread类的sleep方法时,线程是不会释放掉对象的锁的。

  1. 当调用wait方法时,首先要确保调用了wait方法的线程已经持有了对象的锁。
  2. 当调用了wait后,该线程就会释放掉这个对象的锁,然后进入等待状态,该线程进入对象的等待集合中(wait set)。
  3. 当线程调用了wait后进入到等待状态时,它就等待其他线程调用相同对象的notify和notifyAll方法来使得自己被唤醒。
  4. 调用wait方法的代码片段需要放在一个synchronized块或者被synchronized修饰的方法中。
  5. 当调用了对象的notify方法时,它会随机唤醒该对象等待集合中(wait set)的任意一个线程,当某个线程被唤醒后,它就会与其他线程一同竞争对象的锁。
  6. 当调用对象的notifyAll方法时,它会唤醒该对象等待集合中(wait set)中所有的线程,这些线程被唤醒后,又会开始竞争对象的锁。
  7. 某一时刻,只有唯一的一个线程拥有对象的锁。

具体案例 #

Demon对象有一个int类型的属性counter,该值初始为0; 创建四个线程,两个线程对该值增1,两个线程对该值减1; 输出counter每次变化后的结果,要求输出结果为1010101010…。

包含counter的Demon类 #
//该对象提供加1和减1的操作
class Demon{
    //counter
    private int counter=0;
	//对方法加锁,当一个线程要调用该方法时,需要先获取该对象的锁
    public synchronized void inc(){//counter加1
        //此处必须使用while而不是if,防止被其他不相关的线程唤醒
        while(counter!=0){
            try {
                wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        counter++;
        System.out.print(counter);
        /*此处必须使用notifyAll,notify会从等待队列中
        随机选择一个线程唤醒,可能会导致程序一直阻塞*/
        notifyAll();
    }
	//counter减1
    public synchronized void dec(){
        while(counter!=1){
            try {
                wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        counter--;
        System.out.print(counter);
        notifyAll();
    }
}

关键代码如上,剩余的代码也都比较简单,就省略了。如果你需要所有代码,可以通过ctrl+u查看网页源代码,并使用ctrl+f快捷键搜索"黑魔仙变身"即可找到完整代码。手机需要使用能查看网页源代码的浏览器,如via等。

垃圾回收器

2020-09-30
java

-XX:+PrintCommandLineFlags:查看命令行参数(可打印出使用的是哪个垃圾回收器)。

垃圾收集器

Serial回收器:串行回收 #

  • Client模式下默认的新生代垃圾收集器。

  • Serial收集器采用复制算法、串行回收和“Stop-The-World”机制的方式执行内存回收。

  • Serial Old-对应的老年代垃圾收集器,同样采用串行回收和“Stop-The-World”机制,只不过内存回收算法使用的是标记-压缩算法。

    • Serial Old是Client模式下默认的老年代的垃圾收集器。
    • Serial Old在Server模式下主要有两个用途:①与新生代的Parallel Scavenge配合使用;②作为老年代CMS收集器的后备垃圾收集方案。
  • 这个收集器是一个单线程的收集器,在它进行垃圾收集时,必须停掉其他所有的工作线程,直到它收集结束。

  • -XX:+UseSerialGC:指定新生代Serial,老年代Serial Old GC。

ParNew回收器:并行回收 #

  • 多线程的Serial。

  • -XX:+UseParNewGC:指定年轻代ParNew,不影响老年代。

  • -XX:ParallelGCThreads:限制线程数量,默认开启与cpu数据相同的线程数。

Parallel Scavenge回收器:吞吐量优先 #

  • 同样采用并行回收、复制算法、STW机制。

  • Parallel Scavenge收集器的目标是达到一个可控的吞吐量,它也被称为吞吐量优先的垃圾收集器。

  • 自适应调节机制也是Parallel Scavenge与ParNew的一个重要区别。

  • 高吞吐量可以高效的利用cpu时间,尽快的完成程序的运算任务。主要用于在后台运算而不需要太多交互的任务

  • 应用场景:执行批量处理、订单处理、工资支付、科学计算的应用程序。

  • Parallel Old:老年代垃圾收集器,采用标记-压缩算法、并行回收、STW机制。

  • -XX:+UseParallelGC:手动指定年轻代使用Parallel;-XX:+UserParallelOldGC:手动指定老年代。这两个参数,当指定一个,另一个也会被开启。

  • -XX:ParallelGCThreads:设置年轻代并行的线程数。

  • -XX:+UseAdaptiveSizePolicy:设置Parallel收集器具有自适应调节策列。

CMS回收器:低延迟(Concurrent-Mark-Sweep) #

  • HotSpot虚拟机中第一款真正意义上的并发收集器,实现了让垃圾收集线程与用户线程同时执行。

  • 采用标记清除算法,也会导致stw。

  • 运行过程:

    • 初始标记:仅标记GC Roots能直接关联到的对象。速度很快,需要stw。
    • 并发标记:从GC Roots的直接关联对象开始遍历整个对象图的过程,这个过程耗时较长但不需要停顿用户线程。
    • 重新标记:修正并发标记期间因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录,这个阶段的stw一般会比初始标记稍长一些。
    • 并发清除:清理标记阶段判断已经死亡的对象,释放内存空间。这个阶段可与用户线程并发。

cms

  • 由于在垃圾收集阶段用户线程仍在执行,所以在CMS回收过程中,要保证应用程序线程有足够的内存可用。因此,CMS收集器不能像别的收集器一样等到老年代几乎完全填满了才进行垃圾回收,而是在堆内存达到某一个阈值时,便开始进行回收。当CMS运行期间,内存无法满足程序要求,这时虚拟机会启用预备方案:临时使用Serial Old进行垃圾回收。

  • CMS采用标记-清除算法,只能采用空闲列表进行内存分配。

  • 优点:低延迟,并发收集。

  • 缺点:产生内存碎片;对cpu资源非常敏感,因为占用了一部分线程,会导致吞吐量降低;无法处理浮动垃圾(并发标记阶段如果产生新的垃圾,cms无法对这些垃圾进行标记,会导致这些垃圾无法及时的被回收)。

  • -XX:+UseConcMarkSweepGC:手动指定使用CMS收集器。

G1回收器:区域分代化 #

  • -XX:+UseG1GC:启用G1。

  • 目标:延迟可控的情况下获取尽可能高的吞吐量。

    • G1是一个并行回收器,它把堆内存分割为很多不相关的区域。使用不同的region来表示Eden区、幸存者0区、幸存者1区、老年代等。
    • G1有计划的避免在java堆中进行全区域的垃圾回收,G1跟踪各个region里面的垃圾堆积的价值大小(回收所获得的空间大小以及回收所需时间的经验值),在后台维护一个优先列表,每次根据允许的收集时间,优先回收价值最大的region。
    • 由于这种方式的侧重点在于回收垃圾最大量的区间,所以给G1取一个名字:垃圾优先(Garbage First)。
  • 优点:

    • 并行性:G1在回收期间,可以多个GC线程同时工作。此时用户线程stw。
    • 并发性:G1部分工作能与用户线程同时执行。
    • 分代收集:G1仍属于分代型垃圾收集器。但是不再要求整个Eden、年轻代、或者老年代都是连续的,也不再坚持固定大小和数量。它同时兼顾年轻代和老年代。
    • 空间整合:内存是以region为基本单位的。region之间是复制算法,整体上看可以看作是标记-压缩算法。可以避免产生内存碎片。
    • 可预测的时间停顿模型:能让使用者明确指定在一个长度为M毫秒的时间片段内,消耗在垃圾收集上的时间不得超过N毫秒。
  • -XX:MaxGCPauseMillis 设置期望达到的最大Gc停顿时间指标(JVM会尽力实现,但不保证达到)。默认值是200ms

  • Remembered Set:一个对象可能被不同区域region所引用。当有其它region指向本region时,记录其他region的引用。

  • G1垃圾回收的过程

    • 年轻代GC:①扫描根(包含RSet);②更新RSet;③处理RSet;④复制对象;⑤处理引用。
    • 并发标记过程:①初始标记(标记根节点直接可达的对象,会触发一次年轻代GC);②根区域扫描(扫描survivor区能直达老年代的对象并标记被引用的对象);③并发标记(若发现一个region区域全是垃圾,则直接回收);④再次标记;⑤独占清理;⑥并发清理阶段。
    • 混合回收:回收整个Yong Region和一部分Old Region。
    • 如果上述方式不能正常工作,就会触发Full GC。

现状

垃圾回收相关概念

2020-09-29
java

System.gc()的理解 #

  • 在默认情况下,通过System.gc()或者Runtime.getRuntime().gc()的调用会显示触发Full GC,但是该方法可能不是立刻就执行。

内存溢出 #

  • 产生原因:没有空闲内存,并且垃圾收集器也无法提供更多内存。

内存泄漏 #

  • 严格定义:对象不再被程序使用了,但是GC又不能回收他们的情况(仍然存在引用链)。
  • 宽泛定义:一些不太好的实践会导致对象的生命周期变得很长,甚至导致OOM,也叫宽泛意义上的内存泄漏。

安全点 #

  • 程序执行时并非在所有的地方都能停下来开始GC,只有在特定的位置才能停顿下来开始GC,这些位置称为“安全点”(safepoint)。
  • 如何在GC发生时,检测所有的程序都跑到最近的安全点停顿下来了呢 –> 主动式中断:设置一个中断标志,各个线程运行到safe point的时候主动轮询这个标志,如果中断标志为真,则将自己进行中断挂起。

安全区域 #

  • 指在一段代码片段中,对象的引用关系不会发生变化,在这个区域中的任何位置开始GC都是安全的。
  • 可以应对处于sleep或者blocked状态的线程,这时候线程无法响应jvm的中断请求,“走”到安全点去中断挂起。
  • 当程序运行到safe region的代码时,首先标识已经进入了safe region,如果这段时间发生了gc,jvm会忽略标识为safe region状态的线程。
  • 当线程即将离开safe region时,会检测jvm是否已经完成了gc,如果完成了,则继续运行,否则线程必须等待直到收到可以安全离开safe region的信号为止。

引用 #

  • 强引用:类似“Object object=new Object()”这类的引用,只要强引用还存在,就永不回收。

  • 软引用:内存不足才回收。

  • 弱引用:发现即回收。

  • 虚引用:对象回收跟踪。

    • 如果一个对象仅持有虚引用,那么它和没有引用几乎是一样的,随时都可能被垃圾回收器回收。
    • 它不能单独使用,也无法通过虚引用来获取被引用的对象。当试图通过虚引用的get方法来获取对象时,总是null。
    • 为一个对象设置虚引用关联的唯一目的就是能够在这个对象被收集器回收时收到一个系统通知。
    • 虚引用必须和引用队列一起使用。虚引用在创建时必须提供一个引用队列作为参数。当垃圾回收器准备回收一个对象时,如果发现它还有虚引用,就会在回收对象后,将这个虚引用加入引用队列,以通知引用程序对象的回收情况。
    • 由于虚引用可以跟踪对象的回收时间,因此,也可以将一些资源释放操作放置在虚引用中执行和记录。
//声明强引用
Object obj=new Object();
//声明软引用
SoftReference<Object> sf=new SoftReference<Object>(new Object());
//声明弱引用
WeakReference<Object> wr=new WeakReference<Object>(new Object());
//虚引用
Object obj=new Object();
ReferenceQueue<Object> rq=new ReferenceQueue<>();
PhantomReference<Object> pr=new PhantomReference<>(obj,rq);

评估性能指标 #

  • 吞吐量:运行用户代码时间占总运行时间的比例。
  • 垃圾收集开销:吞吐量的补数,垃圾收集所用时间与总运行时间的比例。
  • 暂停时间:执行垃圾收集时,程序的工作线程被暂停的时间。
  • 收集频率:相对于应用程序的执行,收集操作发生的频率。
  • 内存占用:Java堆区所占的内存大小。
  • 快速:一个对象从诞生到被回收所经历的时间。