Java点滴之相等比较、抽象类与接口、多线程与锁简介

​Java点滴之相等比较、抽象类与接口、多线程与锁简介

 

 

1、==和equals的区别:

JVM把内存分为栈内存和堆内存:基本类型会直接在栈上分配;而new创建的对象(包括基本类型的封装类:Interger、String、Double等)和数组,则在对上存放,栈上只存放引用地址。

默认情况下equal是通过==实现的:

public boolean equals(Object anObject) {

if (this == anObject) {

return true;

}

但String中做了override:

  • 若指向同一个对象直接返回true;

  • 否则,判断指向内容是否相等(是否指向同一个对象);

 

等号(==)比较:

  • 基本类型:比较值是否相同;

  • 引用类型:比较引用是否相同;

equals方法比较:equals是继承自Object类(默认为用==判断是否指向同一个对象),所以不能用于基本类型(可用于对应的封装类);但String、Date等都做了重写,为比较所指向对象的内容;

String x = “isequal”;

String y = “isequal”;  // 与x指向同一个对象(常量池中)

String z = new String(“isequal”);

System.out.println(x==y); // true

System.out.println(x==z); // false

System.out.println(x.equals(y)); // true

System.out.println(x.equals(z)); // true

 

hashCode()方法的主要作用是为了配合基于散列的集合(HashSet、HashMap、HashTable等)一起正常运行。设计hashCode()时最重要的因素就是:无论何时,对同一个对象调用hashCode()都应该产生同样的值。对于两个对象,

  • 如果调用equals方法得到的结果为true,则两个对象的hashcode值必定相等;

  • 如果equals方法得到的结果为false,则两个对象的hashcode值不一定不同;

  • 如果两个对象的hashcode值不等,则equals方法得到的结果必定为false;

  • 如果两个对象的hashcode值相等,则equals方法得到的结果未知。

 

2、抽象类及接口

抽象类(abstract)是定义子类通用特性的,不能直接实例化;

  • 抽象类可以有抽象方法(也可以没有),普通类中不能有抽象方法;

  • 抽象方法必须为public或者protected,且不能用final修饰;

  • 一个类继承于一个抽象类,则子类必须实现父类的抽象方法。

接口(interface)是抽象方法的集合:

  • 可以含有变量,但会被隐式地指定为public static final变量

  • 方法会被隐式地指定为public abstract;

 

抽象类与接口区别:

  • 抽象类可以提供成员方法的实现细节,而接口中只能存在public abstract 方法(java8中可以有默认的实现);

  • 抽象类中的成员变量可以是各种类型的,而接口中的成员变量只能是public static final类型的;

  • 接口中不能含有静态代码块以及静态方法,而抽象类可以有静态代码块和静态方法;

  • 一个类只能继承(extends)一个抽象类,而一个类却可以实现(implements)多个接口。

public class B extends A implements Ic, Ib {...}

 

3、多线程

多线程是指程序运行时产生不止一个线程:

  • 并行:多个处理器或多核处理器同时处理多个任务,真正的同时;

  • 并发:通过CPU调度算法,实现多个任务在同一个 CPU ,按时间片轮流(交替)执行,从逻辑上来看那些任务是同时执行。

  • 线程安全:指在并发的情况之下,一段代码经过多线程使用,线程的调度顺序不影响任何结果。反过来,线程不安全就意味着线程的调度顺序会影响最终结果;

  • 同步:指通过人为的控制和调度(如,@synchronized),保证共享资源的多线程访问成为线程安全,来保证结果的准确。

 

线程的状态转换:

Java点滴之相等比较、抽象类与接口、多线程与锁简介

 

基本线程类

三种创建线程的方法:

  • 最简单的方法,通过实现Runnable接口(实现run方法);

    class RunnableDemo implements Runnable {

       private Thread t;

       private String threadName;

       

       RunnableDemo( String name) {

          threadName = name;

          System.out.println("Creating " +  threadName );

       }

       

       public void run() {

          System.out.println("Running " +  threadName );

          try {

             for(int i = 4; i > 0; i--) {

                System.out.println("Thread: " + threadName + ", " + i);

                Thread.sleep(50);

             }

          }catch (InterruptedException e) {

             System.out.println("Thread " +  threadName + " interrupted.");

          }

          System.out.println("Thread " +  threadName + " exiting.");

       }

       

       public void start () {

          System.out.println("Starting " +  threadName );

          if (t == null) {

             t = new Thread (this, threadName);

             t.start ();

          }

       }

    }

     

    public class TestThread { 

       public static void main(String args[]) {

          RunnableDemo R1 = new RunnableDemo( "Thread-1");

          R1.start();      

          RunnableDemo R2 = new RunnableDemo( "Thread-2");

          R2.start();

       }   

    }                    

  • 通过继承Thread类本身(必须重写run方法,并通过start来启动线程);

    Thread 类其他相关方法:

    //当前线程可转让cpu控制权,让别的就绪状态线程运行(切换)

    public static Thread.yield() 

    //暂停一段时间

    public static Thread.sleep()  

    //调用other.join(),将等待other执行完后才继续本线程。    

    public join()

    //打断

    public interrupt()

    //检查当前线程是否发生中断,返回boolean

    public interrupted()

线程会不时地检测中断标识位,以判断线程是否应该被中断(中断标识值是否为true)。中断只会影响到wait状态、sleep状态和join状态。被打断的线程会抛出InterruptedException。

  • 通过Callable接口(一般配合ExecutorService接口中的submit方法使用)和Future接口(可以对Runnable或者Callable的任务执行取消、查询、获取结果的操作)创建线程:

    • 创建Callable接口的实现类,并实现call()方法,该call()方法将作为线程执行体,并且有返回值。

    • 创建Callable实现类的实例,使用FutureTask类来包装Callable对象,该FutureTask对象封装了该Callable对象的call()方法的返回值。

    • 使用FutureTask对象作为Thread对象的target创建并启动新线程。

    • 调用FutureTask对象的get()方法来获得子线程执行结束后的返回值。

public class Test {

  public static void main(String[] args) {

    ExecutorService executorService = Executors.newCachedThreadPool();

    Task task = new Task();

    Future<Integer> future = executorService.submit(task);

    executorService.shutdown();

     

    System.out.println("主线程在执行任务...");      

    try {

      Thread.sleep(2000);

      System.out.println("task运行结果:"+future.get());

    } catch (InterruptedException ex) {

      ex.printStackTrace();

    } catch (ExecutionException ex) {

      ex.printStackTrace();

    } 

    System.out.println("所有任务执行完毕");

  }

}

class Task implements Callable<Integer>{

  @Override

  public Integer call() throws Exception {

    System.out.println("子线程在执行任务...");

    //模拟任务耗时

    Thread.sleep(5000);

    return 1000;

  }

}

 

 

4、同步与锁

Java中主要有两种加锁机制:

  • synchronized关键字:通过一对字节码指令 monitorenter/monitorexit 实现;

  • java.util.concurrent.Lock接口(ReenterLock是常用的实现类):通过 Java 代码搭配sun.misc.Unsafe 中的本地调用实现的;

 

锁从宏观上分类,分为悲观锁与乐观锁:

  • 乐观锁:是一种乐观思想,认为读多写少,遇到并发写的可能性低;每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,采取在写时先读出当前版本号,然后加锁操作(比较跟上一次的版本号,如果一样则更新),如果失败则要重复读-比较-写的操作。java中的乐观锁基本都是通过CAS操作实现的。

  • 悲观锁:就是悲观思想,即认为写多,遇到并发写的可能性高,所以每次在读写数据的时候都会上锁;java中的悲观锁就是Synchronized。

 

JavaSE1.6为了改善性能,使得JVM会根据竞争情况,synchronized 代码块使用如下3种不同的锁机制:

  • 偏向锁(BiasedLock)

  • 轻量级锁(LightweightLock)

  • 重量级锁(HeavyweightLock)

这三种机制的切换是根据竞争激烈程度进行的,在几乎无竞争的条件下,会使用偏向锁,在轻度竞争的条件下,会由偏向锁升级为轻量级锁,在重度竞争的情况下,会升级到重量级锁。

Java点滴之相等比较、抽象类与接口、多线程与锁简介

 

每个java对象都有一个对象头存放‘Mark Word’(其最后两位为锁状态)和指向’class metadata‘类信息的指针;同时每个线程都有自己独立的内存空间, 栈帧就是其中的一部分。里面可以存储仅属于该线程的一些信息。 

  • CAS (Compare And Swap) 指令是一个CPU层级的原子性操作(也就是说 CPU 执行该指令时, 是不会被中断执行其他指令的)指令。在 Intel 处理器中, 其汇编指令为 cmpxchg。

  • 偏向锁:研究发现,大多数情况下,锁不仅不存在多线程竞争,而且总是由同一线程多次获得,为了让线程获得锁的代价更低而引入了偏向锁。

    • 当一个线程访问同步块并获取锁时,会在对象头和栈帧中的锁记录里存储锁偏向的线程ID,以后该线程在进入和退出同步块时不需要进行CAS操作来加锁和解锁,只需简单地测试一下对象头的Mark Word里是否存储着指向当前线程的偏向锁。如果测试成功,表示线程已经获得了锁。如果测试失败,则需要再测试一下Mark Word中偏向锁的标识是否设置成1(表示当前是偏向锁):如果没有设置,则使用CAS竞争锁;如果设置了,则尝试使用CAS将对象头的偏向锁指向当前线程。

    • 偏向锁的撤销:偏向锁使用了一种等到竞争出现才释放锁的机制,所以当其他线程尝试竞争偏向锁时,持有偏向锁的线程才会释放锁。偏向锁的撤销,需要等待全局安全点(在这个时间点上没有正在执行的字节码),它会首先暂停拥有偏向锁的线程,然后检查持有偏向锁的线程是否活着,如果线程不处于活动状态,则将对象头设置成无锁状态;如果线程仍然活着,拥有偏向锁的栈会被执行,遍历偏向对象的锁记录,栈中的锁记录和对象头的Mark Word要么重新偏向于其他线程,要么恢复到无锁或者标记对象不适合作为偏向锁,最后唤醒暂停的线程。

    • 在有锁的竞争时,偏向锁会多做很多额外操作,尤其是撤销偏向锁的时候会导致进入安全点(导致STW,导致性能下降),高并发的应用应禁用偏向锁。

  • 轻量级锁:原理非常简单,如果持有锁的线程能在很短时间内释放锁资源,那么那些等待竞争锁的线程就不需要做内核态和用户态之间的切换进入阻塞挂起状态,它们只需要等一等(自旋),等持有锁的线程释放锁后即可立即获取锁,这样就避免用户线程和内核的切换的消耗。

    • 加锁:线程在执行同步块之前,JVM会先在当前线程的栈桢中创建用于存储锁记录的空间,并将对象头中的Mark Word复制到锁记录中(官方称为Displaced Mark Word);然后线程尝试使用CAS将对象头中的Mark Word替换为指向锁记录的指针。如果成功,当前线程获得锁,如果失败,表示其他线程竞争锁,当前线程便尝试使用自旋来获取锁。

    • 解锁:轻量级解锁时,会使用原子的CAS操作将Displaced Mark Word替换回到对象头,如果成功,则表示没有竞争发生。如果失败,表示当前锁存在竞争,锁就会膨胀成重量级锁。

  • 重量级锁:被锁对象的 mark word 会被通过 CAS 操作尝试更新为一个数据结构的指针, 这个数据结构中进一步包含了指向操作系统互斥量(mutex) 和 条件变量(condition variable) 的指针。

 

synchronized是并发编程中最基本的同步工具,是java内置的同步机制,其三种用法:

  • 对象锁:当使用synchronized修饰类普通方法时,那么当前加锁的级别就是实例对象,当多个线程并发访问该对象的同步方法时,会进行同步。

  • 类锁:当使用synchronized修饰类静态方法时,那么当前加锁的级别就是类,当多个线程并发访问该类(所有实例对象)的同步方法时,会进行同步。

  • 同步代码块:当使用synchronized修饰代码块时,那么当前加锁的级别就是synchronized(X)中配置的x对象实例,当多个线程并发访问该同步代码块时,会进行同步(用String类型对象,要注意字符串常量池的存在,避免出现意外问题)。

除synchronized外,还有Lock接口提供了灵活的锁机制;其与synchronized的区别:

  • synchronized在发生异常时,会自动释放线程占有的锁,因此不会导致死锁现象发生;而Lock在发生异常时,如果没有主动通过unLock()去释放锁,则很可能造成死锁现象,因此使用Lock时需要在finally块中释放锁;

  • Lock可以让等待锁的线程响应中断,而synchronized却不行,使用synchronized时,等待的线程会一直等待下去,不能够响应中断;

  • 通过Lock可以知道有没有成功获取锁(tryLock),而synchronized却无法办到。

  • Lock可以提高多个线程进行读操作的效率。

class Service {

  synchronized public static void classLock() { ... }

  public void classLock() {

    synchronized(Service.class){

      ...

    }

  }

  

  synchronized public void instanceLock() { ... }

  public void instanceLock() {

    synchronized(this){

      ...

    }

  }

 

  public void blockLock(Object oLocker_, ...) {

  synchronized(oLocker_){

      ...

    }

  }

}