《深入理解Java虚拟机》这本书真是神书,百看不厌,每次会看都会有不一样的收获,这次暑假放假也打算把这本书的部分内容再重新看一下,顺便整理些笔记在博客上,以前笔记都是记录在有道云上,找时间也要把笔记整理一下,搬到博客上。

一、面向过程编程与面向对象编程

    什么是面向过程编程?

    面向过程编程思想:程序的编写都是以算法为核心的,程序会把数据和过程分别作为独立的部分来考虑,数据是代表问题空间的客体,程序代码则用于处理这些数据,这种编程方式是站在计算机的角度去抽象问题和解决问题的。这就是POP面向过程编程,它是以功能为中心来进行思考和组织的一种编程方式,强调的是系统的数据被加工和处理的过程,说白了就是注重功能性的实现,效果达到就好了。
    比如:在这里我们暂且把程序设计比喻为房子的布置,一间房子的布局中,需要各种功能的家具和洁具(类似方法),如马桶、浴缸、天然气灶,床、桌子等,对于面向过程的程序设计更注重的是功能的实现(即功能方法的实现),效果符合预期就好,因此面向过程的程序设计会更倾向图中设置结构,各种功能都已实现,房子也就可以正常居住了。


    什么是面向对象编程?

    面向对象编程思想:站在现实世界的角度去抽象和解决问题,它把数据和行为都看做对象的一部分,可以将数据和行为封装到一个类当中,作为一个独立的部分。这就是OOP面向对象编程,OOP则注重封装,强调整体性的概念,以对象为中心,将对象的内部组织与外部环境区分开来。
    同样以房子的布置为例,面向对象的程序设计便采用了图2的布局,对于面向对象程序设计来说这样设置好处是显而易见的,房子中的每个房间都有各自的名称和相应功能(在java程序设计中一般把类似这样的房间称为类,每个类代表着一种房间的抽象体),如卫生间是大小解和洗澡梳妆用的,卧室是休息用的,厨房则是做饭用的,每个小房间都各司其职并且无需时刻向外界暴露内部的结构,整个房间结构清晰,外界只需要知道这个房间并使用房间内提供的各项功能即可(方法调用),同时也更有利于后期的拓展了,毕竟哪个房间需要添加那些功能,其范围也有了限制,也就使职责更加明确了(单一责任原则)。

二、线程安全

    线程安全就是,当多个线程并发访问一个对象时,如果不用考虑这些线程在运行时环境下的调度和交替执行,也不需要进行额外的同步操作,或者在调用方进行任何其他的协调操作,调用这个对象的行为都可以获得正确的结果,那么这个对象就是线程安全的。

    Java中的线程安全

    在Java语言中各种操作共享的数据可以分为5类:不可变、绝对线程安全、相对线程安全、线程兼容和线程对立

    不可变

    在Java语言中(JDK1.5以后),不可变的对象一定是线程安全的,也就是用final修饰的对象,只要一个不可变的对象被正确地构建出来(没有发生this引用逃逸的情况),那其外部的可见状态永远也不会发生改变,永远不会看到它在多个线程当中处于不一致的状态。典型的例子就是java.lang.String类的对象,它的subString()、replace()和concat()等方法都不会影响它原来的值,只会返回一个新的对象。
    在Java API中,不可变类型除了String以外,还有枚举类、java.lang.Number的部分子类,如Long和Double等数值包装类型,还有BigInteger和BigDecimal等大数据类型,但除了同为Number子类下的原子类AtomicInteger和AtomicLong并非是不可变的。

    绝对线程安全

    通过下面一个例子来理解绝对线程安全中的绝对是什么意思
import java.util.Vector;

/**
 * @Author ChoiBin
 * @Date 2019-07-27 17:06
 * @Version 1.0
 */
public class VectorTest {

    private static Vector<Integer> vector = new Vector<>();

    public static void main(String[] args) {
        while (true){
            for(int i = 0;i < 10;i++){
                vector.add(i);
            }

            Thread removeThread = new Thread(new Runnable() {
                @Override
                public void run() {
                    for(int i = 0;i < vector.size();i++){
                        vector.remove(i);
                    }
                }
            });

            Thread printThread = new Thread(new Runnable() {
                @Override
                public void run() {
                    for(int i = 0;i < vector.size();i++){
                        System.out.println(vector.get(i));
                    }
                }
            });

            removeThread.start();
            printThread.start();

            while(Thread.activeCount() > 20);
        }
    }
}    
        运行结果:
        
    虽然vector是一个线程安全的容器,里面所有的方法都通过synchronized修饰,尽管这样效率很低,但确实是安全的。
    但是由上面的测试中可以看出,当在多线程环境下,如果不在方法调用端做额外的同步操作的话,使用这段代码仍然是不安全的,因为如果另外一个线程刚好在错误的时间里删除了一个元素,导致i位置不可用,其他线程在去访问i上的位置是就会抛出ArrayIndexOutOfBoundException异常,如果要保证线程安全,就必须把相应的代码改为如下:
           Thread removeThread = new Thread(new Runnable() {
                @Override
                public void run() {
                    synchronized (vector) {
                        for (int i = 0; i < vector.size(); i++) {
                            vector.remove(i);
                        }
                    }
                }
            });


            Thread printThread = new Thread(new Runnable() {
                @Override
                public void run() {
                    synchronized (vector){
                        for(int i = 0;i < vector.size();i++){
                            System.out.println(vector.get(i));
                        }
                    }
                }
            });     

    相对线程安全

    相对线程安全就是通常意义上所说的线程安全,它需要保证对这个对象单独的操作是线程安全的,我们在调用的情况不需要额外的保障措施,但是对于一些特点顺序的连续调用,就可能需要在调用端进行额外的同步操作来保证调用的正确性。如上面所示的两个例子都是相对线程安全的,在Java语言中,HashTable、Vector等都是相对线程安全的。

    线程兼容

    线程兼容指的是对象本身不是线程安全的,但是可以通过在调用端正确使用同步手段来保证对象在多线程环境下可以安全的使用,通常一个对象线程不安全,通常就是这一种情况。比如ArrayList、HashMap在并发环境就需要进行额外的同步操作。

    线程对立

    线程对立是指无论调用端是否采取了同步措施,都无法保证在多线程环境下并发的使用代码。比如:Thread类中得到suspend()方法和resume()方法,如果两个线程同时持有一个线程对象,一个去中断线程,另一个尝试去恢复线程,如果在并发环境下,无论是否进行了同步操作,目标线程都存在死锁的风险,如果suspend()中断的线程就是即将要执行resume()的那个线程,那就肯定会产生死锁。因此,这两个方法已经被JDK声明抛弃了。
    
    线程安全的实现方法

     互斥同步

    同步是指多个线程并发访问共享数据时,保证共享数据再同一时刻只被一个线程(或者是一些,当使用信号量时)使用。
    互斥是实现同步一种手段,常见的实现方式有信号量、互斥量和临界区。因此互斥是因,同步是果;互斥是方法,同步是目的。
    在Java中最常用的互斥同步手段就是使用synchronized关键字,这个关键字是通过monitorenter和monitorexit两个字节码指令来实现的,这两个字节码指令需要一个reference类型的参数来指明要锁定或者解锁的对象。如果指明了对象参数,那么就是这个对象的reference;如果没有指明,那么就要看synchronized修饰的是实例方法还是类方法,实例方法对应对象实例,而类方法对应Class对象。
    为什么synchronized在Java中是一个重量级的操作?
    原因主要是Java线程是要映射到操作系统的原生线程上的,如果要阻塞或者唤醒一个线程,都需要操作系统来帮忙完成,这就需要从用户态切换到内核态,这个状态转换的过程需要耗费大量的处理器时间,这就可能导致比用户代码执行的时间还要长。
    在Java中除了可以使用synchronized来实现同步,还可以用JUC包下的重入锁,也就是ReentrantLock来实现,相比synchronized,两者有如下区别:
  • synchronized是基于原生语法层面的互斥锁,也就是要依赖于操作系统;而ReentrantLock是依靠API来实现的互斥锁;
  • ReentrantLock支持等待可中断,当持有锁的线程长时间不释放锁时,正在等待的线程可以选择放弃等待,该做其他事情。
  • ReentrantLock可以实现公平锁,默认的是非公平锁,可以在构造函数中通过传入布尔值来实现公平锁。
  • ReentrantLock可以同时绑定多个condition对象,只需要多次调用newCondiition()方法,从而实现多路通知,而synchronized只能绑定一个。
    在JDK1.6以后,synchronized进行了很多优化操作,性能上基本都与ReentrantLock持平。

    非阻塞同步

    互斥同步是典型的阻塞同步,是一种悲观的并发策略,总是认为如果不采取正确的同步措施,就会产生问题,无论共享数据是否会出现竞争,都会进行加锁操作,发生竞争了,又会产生用户态与内核态之间的转换,维护锁计数器等等操作。
    而非阻塞同步策略,提供了一种乐观的并发策略,也就是可以先进行操作,如果没有其他线程去争用这些共享数据,那么操作就执行成功;否则发生冲突了,就会采取相应补偿措施(常见的就是不断地重试,直到成功为止),这种乐观的并发策略正是因为避免了线程的切换而产出的开销,而性能上会相对不错。
    在JDK1.5以后,Java也支持了这种非阻塞式的同步方式,也就是CAS操作,该操作是有sun.mic.Unsafe类里面的compareAndSwapInt()和compareAndSwapLong()等几个方法包来提供的,即时编译出来的结果也就是一条平台相关的处理器CAS指令。CAS指令需要3个操作数,分别是内存位置(变量的内存地址,用V表示),旧的预期值(A表示)新值(B表示)。当这条指令执行时,仅当V符合旧预期值A时,才会将值更新为B,否则不更新,无论更新是否成功,都会返回V的旧值,该处理过程是一个原子操作。
    在JUC包下,Atomic类就是基于CAS实现的,比如AtomicInteger类中的incrementAndGet()方法
    
    底层调用的就是Unsafe包下的方法,通过循环不断进行CAS操作,直到成功

    当然这种非阻塞同步并不是完美的,它也有缺陷,比如它会导致ABA问题的产生,ABA问题就是如果一个变量V在初次使用时是A值,并在准备赋值检查时它还是A值,我们能保证这中间没有发生变化吗?比如在期间它的值被改成B后,有改为A,那么CAS操作就会误认为它一直没发生变化。在JUC包下,也提供解决这种问题的办法,JUC中提供了一个带有标记的原子引用类"AtomicStampedReference",来通过控制变量值的版本保证CAS操作的正确性。

    无同步方案

    当一个方法没有涉及共享数据,那么它自然就无需要进行同步操作,去保证它的正确性。比如可重入代码和线程本地存储(ThreadLocal)。