0%

查缺补漏-JavaCore

本章是整理知识内容,为强化知识长期更新。

面向对象思想

  • 面向对象是一个思想,时间万物皆可以被看做一个对象。

封装

  • 隐藏对象的属性和实现的具体细节,只对外暴露公共访问方式。

继承

  • 当多个类出现相同代码逻辑时,我们通常将相同的代码重构到一个类中,如果是绑定关系就可以使用继承。
  • Java中类是单继承。

多态

  • 一个事物的的多种状态,比如女人、男人都是人的性别。人的性别就分为女人、男人。

重写和重载

  • 重载:同一个类中,方法名相同,参数个数或者类型不相同,返回类型可以不相同。
  • 重写:类的继承关系中体现,子类重写父类的方法。

介绍下 Java 基本数据类型

  • Java中存在8个原生数据类型,同时又分成四种:整形、浮点型、char、Boolean。它们之间存在自动类型转换,规则是从小到大。并且都存在自动装箱拆箱特性,但是这种操作是隐式操作而且在某些情况会导致CG压力增大。
类型 存储需求 取值范围
int 4字节 -2 147 483 638 ~ 2 147 483 637
short 2字节 -32 768 ~ 32 767
long 8字节 -9 223 372 036 854 775 808 ~ 9 223 372 036 854 775 807
byte 1字节 - 128 ~ 127
  • 整型的范围与运行Java运行的硬件没有关系,所有的数据类型所占的字节数量与平台无关。
类型 存储需求 取值范围
float 4字节 大约 $\pm$ 3.402 823 37F + 38F (有效位数为7~8位)
double 8字节 大约 $\pm$ 1.797 693 134 862 315 70E + 308 (有效位数为15位)
  • double这种类型的精度是float的两倍。

  • 所有浮点数值计算都遵循IEEE 754规范,下面是溢出和出错的情况的三种特殊的浮点数值。

    • 正无穷大
    • 负无穷大
    • NaN ( 不是一个数字 )
    • 一个整整数除以0的结果为正无穷大,计算0/0或者负数的平方根结果为NaN。
  • char类型

    • char类型表示单个字符,属于Unicode编码表。因为历史原因,不建议在程序中使用。除非确实要对UTF-16代码单元进行操作。
    • char字节大小
      • Java中无论是汉字还是英文字母都是用Unicode编码来表示的,一个Unicode是16位,每字节是8位,所以一个Unicode码占两字节。但是英文字母比较特殊,源自于8位(1字节)的ASCII吗,于是在Unicode码仅使用了低8位(1字节)就可以表示。
  • boolean类型

    • 布尔类型,只有两个值false、true。基本用于判定条件。
    • boolean字节大小
      • Java规范中并未明确规定boolean类型的大小。
  • 自动类型转换

    • 整型、实型(常量)、字符型数据可以混合运算。运算中,不同类型的数据先转化为同一类型,然后进行运算。

      转换从低级到高级。

      1
      byte,short,char—> int —> long—> float —> double
    • 不能对boolean进行类型转换、不能把对象类型转换成不相关的对象、把大容量的对象转换成小容量对象时需要强制类型转换、转换过程中间可能出现精度损失。

  • 装箱和拆箱boxing or unboxing

原语 对应的JDK类
int java.lang.Integer
short java.lang.Short
long java.lang.Long
byte java.lang.Byte
char java.lang.Character
double java.lang.Double
float java.lang.Float
boolean java.lang.Boolean
  • Java中只有原生数据类型是特殊的,它们不是对象。其它的都是对象。那么就一个尴尬的问题,集合类都是存放的对象,JDK5之后考虑到这个问题就自动进行逆行拆箱装箱的操作。

    1
    2
    3
    //比如所在泛型中是不能存放原生数据类型的,如要要存放原生数据类型的数据,需要装箱。
    Collection<int> c = new ArrayList<int>(); //这是无法编译成功的。
    Collection<Integer> cc = new ArrayList<Integer>(); //这样才行。
  • 每个 JDK 类都提供了相应方法来解析它的内部表示,并将其转换为相应的原语类型。

  • 但是注意装箱拆箱操作其实是非常消耗内存的举动,在该过程中可能会生成生成无用对象增加GC压力。所以尽量避免这中操作。

    1
    2
    3
    4
    Integer sum = 0;
    for(int index = 1000; index < 5000; index ++){
    sum+=index;
    }

    比如这种,每次sum都需要自动拆箱。

  • 默认情况下整数的类型都是int、浮点型的数都是double。

    1
    float d = 1.1f; //在后面添加f,大小写不区分。隐式强制类型转换

int 和 Integer 有什么区别。

  • Integer是int的包装类,它有一个int类型字段存储数据,并提供了基础的操作。关于Integer的缓存值,jdk5之后引入了一个静态工厂方法valueOf,在调用它的时候会有明显的性能提升,内部是一个缓存机制IntegerCache默认的长度是-128~127之间。
    • 这种缓存行为不仅适用于Integer对象。我们针对所有整数类型的类都有类似的缓存机制。
      • 有 ByteCache 用于缓存 Byte 对象。
      • 有 ShortCache 用于缓存 Short 对象。
      • 有 LongCache 用于缓存 Long 对象。
      • 有 CharacterCache 用于缓存 Character 对象。
      • Byte,Short,Long 有固定范围: -128 到 127。对于 Character, 范围是 0 到 127。除了 Integer 可以通过参数改变范围外,其它的都不行。

Java中访问类型有哪几种

访问类型 同一个类 同一个包 不同包的子类 不同包的非子类
public Y Y Y Y
protected Y Y Y
default Y Y
private Y

Java中重载和重写的区别

  • 重写(override)

    • 存在父类和子类之间。
    • 方法名、参数、返回值相同。
    • 方法被final修饰不能被重写。
    • 子类重写父类方法后,不能抛出比父类方法的异常。子类不能缩写父类的方法访问权限
  • 重载(overload)

    • 参数类型、个数、顺序至少有一个不相同。
    • 不能重载只有返回值不同的方法名。
    • 存在于父类和子类、同类中。
  • Constructor 不能被 override(重写),但是可以 overload(重载),所以你可以看到⼀个类中有多个构造函数的情况。

Java值传递和引用传递的区别

  • 其实只有传值

  • 值传递(call by value),对于基本型变量,传递的是该变量的副本,改变副本不影响变量。

  • 传递引用(call by reference),对于对象型变量,传递的该对象的地址的一个副本,并不是原对象本身。

instanceof 关键字的作用

  • instanceof 严格来说是Java中的一个双目运算符,用来测试一个对象是否为一个类的实例,用法为:
1
boolean result = obj instanceof Class
  • 其中 obj 为一个对象,Class 表示一个类或者一个接口,当 obj 为 Class 的对象,或者是其直接或间接子类,或者是其接口的实现类,结果result 都返回 true,否则返回false。

  • 注意:编译器会检查 obj 是否能转换成右边的class类型,如果不能转换则直接报错,如果不能确定类型,则通过编译,具体看运行时定。

1
2
3
4
5
6
7
8
9
int i = 0;
System.out.println(i instanceof Integer);//编译不通过 i必须是引用类型,不能是基本类型
System.out.println(i instanceof Object);//编译不通过

Integer integer = new Integer(1);
System.out.println(integer instanceof Integer);//true

//false ,在 JavaSE规范 中对 instanceof 运算符的规定就是:如果 obj 为 null,那么将返回 false。
System.out.println(null instanceof Object);

Java中的主线程

Java为多线程编程提供内置支持。多线程程序包含两个或多个可以并发运行的部分。这样的程序的每个部分称为线程,每个线程定义一个单独的执行路径。当Java程序启动时,一个线程立即开始运行。这通常被称为我们程序的线程,因为它是我们程序开始时执行的那个。

  • 它是生产启动其它线程的线程。
  • 它负责程序结束时候的收尾工作。

上面简单画图出一个主线程和子线程以及后台线程的关系。

在主线程中,这可以通过调用Thread类中的currentThread()方法来完成。此方法返回对其调用的线程的引用。主线程的默认优先级为5,对于所有剩余的用户线程,优先级将从父级继承到子级。同时虚拟机会启动其它的线程,比如垃圾回收器GC。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
package cn.z201.java.test.thread;

/**
* @author z201.coding@gmail.com
**/
public class ChildThread extends Thread{

public ChildThread(String name) {
super(name);
}

@Override
public void run() {
for (int i = 0; i < 3 ; i++) {
System.out.println(Thread.currentThread().getName() + ": " + i);
}
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
package cn.z201.java.test.thread;

/**
* @author z201.coding@gmail.com
**/
public class MasterThread {

public static void main(String[] args) {

Thread master = Thread.currentThread();
// getting name of Main thread
System.out.println("Current thread: " + master.getName());

// changing the name of Main thread
master.setName("Master");
System.out.println("After name change: " + master.getName());

// getting priority of Main thread
System.out.println("Main thread priority: "+ master.getPriority());

// setting priority of Main thread to MAX(10)
master.setPriority(Thread.MAX_PRIORITY);

System.out.println("Main thread new priority: "+ master.getPriority());

for (int i = 0; i < 3; i++) {
System.out.println("MasterThread " + i);
}

ChildThread childThread1 = new ChildThread("childThread1");
// getting priority of child thread
// which will be inherited from Main thread
// as it is created by Main thread
System.out.println("Child1 thread priority: "+ childThread1.getPriority());
// setting priority of Main thread to MIN(1)
childThread1.setPriority(Thread.MIN_PRIORITY);
System.out.println("Child1 thread new priority: "+ childThread1.getPriority());

ChildThread childThread2 = new ChildThread("childThread2");
System.out.println("Child2 thread priority: "+ childThread2.getPriority());
childThread2.setPriority(Thread.NORM_PRIORITY);
System.out.println("Child2 thread new priority: "+ childThread2.getPriority());
// starting child thread
childThread1.start();
childThread2.start();

}
}

输出
Current thread: main
After name change: Master
Main thread priority: 5
Main thread new priority: 10
MasterThread 0
MasterThread 1
MasterThread 2
Child1 thread priority: 10
Child1 thread new priority: 1
Child2 thread priority: 10
Child2 thread new priority: 5
childThread1: 0
childThread1: 1
childThread1: 2
childThread2: 0
childThread2: 1
childThread2: 2

主线程默认优先级是5经过修改为10,启动的子线程就继承了主线程的级别。

什么是进程

  • 进程是程序运行和资源分配的基本单位,一个程序至少一个进程,一个进程至少一个线程。

  • 多个进程的*内部数据和状态是完全独立的*,而多个线程是共享一个内存空间和一组系统资源,有可能互相影响


什么是线程

  • 线程是程序内部的控制流,只能使用分配给线程 的资源和环境
  • 线程本身的数据通常只有寄存器数据,以及一个程序执行时使用的堆栈,所以线程的切换比进程切换负担要小。
  • 线程是进程的一个实体,是cpu调度和分派的基本单位,是比程序更小能独立运行的基本单位。

多线程编程的目的

  • 多线程编程的目的,就是 最大限度地利用CPU资源,当某一线程的处理不需要占用CPC而和I/O等资源打交道时,让需要占用CPU资源的其他线程有机会获得CPU资源,从根本上,这就是多线程编程的最终目的。
  • 线程(Thread)也是程序的最小单元,它依托进程而存在。
  • 多个线程可以共享一块内存空间和一组系统资源因此线程之前的切换更加节省资源、更加轻量化,也因此被称为轻量级的进程。

多线程概念介绍

  • 一个进程可以包含多个线程。
  • 一个程序实现多个代码同时交替运行就需要产生多个线程。
  • CPU随机的抽出时间,让我们的程序一会做这件事情,一会做另外一件事情。
  • 同期其他大多数编程语言不同,Java内置支持多线程编程(multithreaded progranmming)。多线程程序包包含两个或两条以上并发运作的部分,吧程序中每个这种的部分都叫做一个线程(thread),每个线程都独立的执行路经,因此多线程是多任务处理的一种特殊形式。
  • 多任务处理被所有的现代操作系统所支持。然后,多任务处理有两种截然不同的类型,基于进程和基于线程。
    • 基于进程
      • 基于进程的多任务处理是更熟悉的形式,进程(process)本质上是一个执行的程序。因此基于进程的多线程任务处理的特点是允许你的计算机同时运行两个或更多的程序。
    • 基于线程
      • 基于线程(thread-based)的多任务处理环境中,线程是最小的执行单位。这意味着一个程序可以执行两个或则多个任务的功能。
    • 多线程程序比多进程程序需要更少的管理资源。
      • 进程是重量级的任务,需要分配给他们的独立空间地址。进程之间通信是昂贵和受限的。进程之间的转换也是很需要花费的。另一方面,线程是轻量级的选手,它们共享相同的地址空间并且共享同一进程,线程之间通信是轻量级的,线程的转换也是轻量级的。
    • 线程的实现
      • 1.两种方法均需要执行线程start方法作为线程分配必须的系统资源、调度线程运行并执行线程的run方法。
      • 2.在具体应用中,采用哪种方法来构造线程体要试情况而定。通常,当一个线程继承了另一个类,应该应该使用第二种方法,即使实现runnable接口。
      • 3.线程的消亡不能通过调用一个Trread.stop方法,而是让线程自然消亡。

线程的生命周期

一个线程的消亡过程。

  • 新建(New)

    • 创建后尚未启动。
  • 可运行(Runnable)

    • 可能正在运行,也可能正在等待 CPU 时间片。
    • 包含了操作系统线程状态中的 运行(Running ) 和 就绪(Ready)。
  • 无限期等待(Waiting)

    • 等待其它线程显式地唤醒,否则不会被分配 CPU 时间片。

      进入方法 退出方法
      没有设置 Timeout 参数的 Object.wait() 方法 Object.notify() / Object.notifyAll()
      没有设置 Timeout 参数的 Thread.join() 方法 被调用的线程执行完毕
      LockSupport.park() 方法 -
  • 限期等待(Timed Waiting)

    • 无需等待其它线程显式地唤醒,在一定时间之后会被系统自动唤醒。

    • 调用 Thread.sleep() 方法使线程进入限期等待状态时,常常用 “使一个线程睡眠” 进行描述。

    • 调用 Object.wait() 方法使线程进入限期等待或者无限期等待时,常常用 “挂起一个线程” 进行描述。

    • 睡眠和挂起是用来描述行为,而阻塞和等待用来描述状态

    • 阻塞和等待的区别在于,阻塞是被动的,它是在等待获取一个排它锁。而等待是主动的,通过调用 Thread.sleep() 和 Object.wait() 等方法进入。

      进入方法 退出方法
      Thread.sleep() 方法 时间结束
      设置了 Timeout 参数的 Object.wait() 方法 时间结束 / Object.notify() / Object.notifyAll()
      设置了 Timeout 参数的 Thread.join() 方法 时间结束 / 被调用的线程执行完毕
      LockSupport.parkNanos() 方法 -
      LockSupport.parkUntil() 方法 -
  • 阻塞(Blocking)

    • 这个状态下,是在多个线程有同步操作的场景,比如正在等待另一个线程的 synchronized 块的执行释放,或者可重入的 synchronized 块里别人调用 wait() 方法,也就是线程在等待进入临界区。
    • 阻塞可以分为:等待阻塞,同步阻塞,其他阻塞
  • 死亡(Terminated)

    • 线程因为 run 方法正常退出而自然死亡。
    • 因为一个没有捕获的异常终止了 run 方法而意外死亡。

线程的状态

线程的基本方法

  • wait线程等待

    • 调用该方法的线程进入 WAITING 状态,只有等待另外线程的通知或被中断才会返回,需要注意的是调用 wait()方法后,会释放对象的锁。因此,wait 方法一般用在同步方法或同步代码块中。
  • sleep线程睡眠

    • sleep 导致当前线程休眠,与 wait 方法不同的是 sleep 不会释放当前占有的锁,sleep(long)会导致线程进入 TIMED-WATING 状态,而 wait()方法会导致当前线程进入 WATING 状态。
  • yield线程让步

    • yield 会使当前线程让出 CPU 执行时间片,与其他线程一起重新竞争 CPU 时间片。一般情况下,优先级高的线程有更大的可能性成功竞争得到 CPU 时间片,但这又不是绝对的,有的操作系统对线程优先级并不敏感。
  • interrupt线程中断

    • 中断一个线程,其本意是给这个线程一个通知信号,会影响这个线程内部的一个中断标识位。这个线程本身并不会因此而改变状态(如阻塞,终止等)。

      • 调用 interrupt()方法并不会中断一个正在运行的线程。也就是说处于 Running 状态的线程并不会因为被中断而被终止,仅仅改变了内部维护的中断标识位而已。

      • 若调用 sleep()而使线程处于 TIMED-WATING 状态,这时调用 interrupt()方法,会抛出InterruptedException,从而使线程提前结束 TIMED-WATING 状态。

        许多声明抛出 InterruptedException 的方法(如 Thread.sleep(long mills 方法)),抛出异常前,都会清除中断标识位,所以抛出异常后,调用 isInterrupted()方法将会返回 false。

      • 中断状态是线程固有的一个标识位,可以通过此标识位安全的终止线程。比如,你想终止一个线程 thread 的时候,可以调用 thread.interrupt()方法,在线程的 run 方法内部可以根据 thread.isInterrupted()的值来优雅的终止线程。

  • join等待其它线程终止

    • join() 方法,等待其他线程终止,在当前线程中调用一个线程的 join() 方法,则当前线程转为阻塞状态,回到另一个线程结束,当前线程再由阻塞状态变为就绪状态,等待 cpu 的宠幸。
    • 很多情况下,主线程生成并启动了子线程,需要用到子线程返回的结果,也就是需要主线程需要在子线程结束后再结束,这时候就要用到 join() 方法。
  • notify线程唤醒

    • Object 类中的 notify() 方法,唤醒在此对象监视器上等待的单个线程,如果所有线程都在此对象上等待,则会选择唤醒其中一个线程,选择是任意的,并在对实现做出决定时发生,线程通过调用其中一个 wait() 方法,在对象的监视器上等待,直到当前的线程放弃此对象上的锁定,才能继续执行被唤醒的线程,被唤醒的线程将以常规方式与在该对象上主动同步的其他所有线程进行竞争。类似的方法还有 notifyAll() ,唤醒再次监视器上等待的所有线程。
  • 其它方法

    • isAlive(): 判断一个线程是否存活。
    • activeCount(): 程序中活跃的线程数。
    • enumerate(): 枚举程序中的线程。
    • currentThread(): 得到当前线程。
    • isDaemon(): 一个线程是否为守护线程。
    • setDaemon(): 设置一个线程为守护线程。(用户线程和守护线程的区别在于,是否等待主线程依赖于主线程结束而结束)
    • setName(): 为线程设置一个名称。
    • setPriority(): 设置一个线程的优先级。
    • getPriority()::获得一个线程的优先级。

线程优先级

  • 设置优先级是为了在多线程环境中便于系统对线程调度,优先级高的线程将优先执行。

    • 一个线程的优先级遵从以下原则。
      • 线程创建时,子继承父的优先级。
      • 线程创建后,可用过调用setPriority()方法改变优先级。
      • 线程的优先级是1-10之间的正整数。
        • 1 - MIN_PRIORITY
        • 10 - MAX_PRIORITY
        • 5 - NORM_PRIORITY
  • 线程的优先级策略

    • 线程调度器选择优先级最高的线程运行.但是,如果发生以下情况,就会终止线程的运行.
      • 线程体中调用了yieid()方法,让出了对CPU的占用权。
      • 线程体中调用sleep()方法,使线程进入了睡眠状态。
      • 线程由I/O操作而受阻塞。
      • 另一个更高优先级的线程出现。
      • 在支持时间片段系统中,该线程的时间片用完。

上下文切换

  • 是指某一时间点 CPU 寄存器和程序计数器的内容。

    • 寄存器
      • 是 CPU 内部的数量较少但是速度很快的内存(与之对应的是 CPU 外部相对较慢的 RAM 主内存)。寄存器通过对常用值(通常是运算的中间值)的快速访问来提高计算机程序运行的速度。
    • 程序计数器
      • 是一个专用的寄存器,用于表明指令序列中 CPU 正在执行的位置,存的值为正在执行的指令的位置或者下一个将要被执行的指令的位置,具体依赖于特定的系统。
    • 上下文切换的活动
    1. 挂起一个进程,将这个进程在 CPU 中的状态(上下文)存储于内存中的某处。
    2. 在内存中检索下一个进程的上下文并将其在 CPU 的寄存器中恢复。
    3. 跳转到程序计数器所指向的位置(即跳转到进程被中断时的代码行),以恢复该进程在程序中。
    • 引起线程上下文切换的原因
    1. 当前执行任务的时间片用完之后,系统 CPU 正常调度下一个任务。
    2. 当前执行任务碰到 IO 阻塞,调度器将此任务挂起,继续下一任务。
    3. 多个任务抢占锁资源,当前任务没有抢到锁资源,被调度器挂起,继续下一任务。
    4. 用户代码挂起当前任务,让出 CPU 时间。
    5. 硬件中断。

线程的同步

  • 在多线程环境中,可能会出现两个甚至多个线程试图同时访问一个有限的资源。必须对这种潜在的资源冲突进行预防。
    • 解决方案:在线程使用一个资源时为其加锁即可。访问资源的第一个线程为其上锁以后,其他线程比便不能在使用那个资源。除非被解锁。
  • 在线程环境中,关于成员变量与局部变量;如果一个变量是成员变量,那么多个线程对同一个对象的成员变量进行操作时候,他们对该成员变量是彼此影响到(也就是说一个线程对成员变量的改变会影响另一个到另一线程)。
  • 不能依靠线程优先级来决定线程的执行。
  • 同步到实现方式
    • synchronized 关键字;当synchronized关键字修饰一个方法的时候,该方法叫做同步方法。
    • Java中方锁。
      • java中每个对象都有一个锁(lock)或者叫监视器(monitor),当访问一个对象synchronized方法时,表示将该对象上锁,此时其他任何线程在去访问该synchronized方法了,直到之前那个线程执行方法完毕后(或者抛出异常),那么该对象的锁释放掉。其他线程才有可能再去访问synchronized方法。
      • 如果一个对象中有多个synchronized方法,某一时刻某个线程进入了该对象中的synchronized方法,那么在该方法没有执行完成之前或者抛出异常之前,其他线程是无法访问该对象的任何synchronized方法。
      • 被synchronized保护的变量应该是private修饰的。
      • 如果某个synchronized方法被static修饰的,那么当线程访问该方法时候,它锁定并不是对象(实例),而是synchronized方法所以在对象所对应的Class对象,因为Java中无论一个类有多少个对象,这些对象都会对应唯一一个Class对象,因此当线程分别访问一个类的两个对象的两个static synchronized方法时,他们的执行顺序是顺序的,也就是说一个线程先执行方法,执行完毕后另一个线程才开始执行。
      • synchronized方法是一个粗粒度的控制,某一个时刻只能有一个方法执行synchronized方法;sysnchronized块则是一种细粒度的控制方;只会将代码块同步。位于方法内、synchronized块之外的代码是可以被多个线程同时访问的。

volatile关键字

volatile 关键字的主要作⽤就是保证变量的可⻅性然后还有⼀个作⽤是防⽌指令重排序。

  • 并发编程的三个重要特性

    • 原⼦性 : ⼀个的操作或者多次操作,要么所有的操作全部都得到执⾏并且不会收到任何因素的

      ⼲扰⽽中断,要么所有的操作都执⾏,要么都不执⾏。 synchronized 可以保证代码⽚段的原

      ⼦性。

    • 可⻅性 :当⼀个变量对共享变量进⾏了修改,那么另外的线程都是⽴即可以看到修改后的最新

      值。 volatile 关键字可以保证共享变量的可⻅性。

    • 有序性 :代码在执⾏的过程中的先后顺序,Java 在编译器以及运⾏期间的优化,代码的执⾏顺

      序未必就是编写代码时候的顺序。 volatile 关键字可以禁⽌指令进⾏重排序优化。

使用场景

  • 读写锁

    • 如果需要实现一个读写锁,每次只能一个线程去写数据,但是有多个线程来读数据,就synchronize同步锁来对set方法加锁,get方法不加锁, 使用volatile来修饰变量,保证内存可见性,不然多个线程可能会在变量修改后还读到一个旧值。
  • 状态位

    • 用于做状态位标志,如果多个线程去需要根据一个状态位来执行一些操作,使用volatile修饰可以保证内存可见性。

      用于单例模式用于保证内存可见性,以及防止指令重排序。

synchronized关键字

synchronized关键字解决的是多个线程之间访问资源的同步性,synchronized关键字可以保证被它修饰

的⽅法或者代码块在任意时刻只能有⼀个线程执⾏。

synchronized 关键字加到 static 静态⽅法和 synchronized(class)代码块上都是是给 Class

类上锁。synchronized 关键字加到实例⽅法上是给对象实例上锁。尽量不要使⽤

synchronized(String a) 因为JVM中,字符串常量池具有缓存功能!

常用场景

1. 同步一个代码块
1
2
3
4
5
public void func() {
synchronized (this) {
// ...
}
}

它只作用于同一个对象,如果调用两个对象上的同步代码块,就不会进行同步。

对于以下代码,使用 ExecutorService 执行了两个线程,由于调用的是同一个对象的同步代码块,因此这两个线程会进行同步,当一个线程进入同步语句块时,另一个线程就必须等待。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class SynchronizedExample {

public void func1() {
synchronized (this) {
for (int i = 0; i < 10; i++) {
System.out.print(i + " ");
}
}
}
}
public static void main(String[] args) {
SynchronizedExample e1 = new SynchronizedExample();
ExecutorService executorService = Executors.newCachedThreadPool();
executorService.execute(() -> e1.func1());
executorService.execute(() -> e1.func1());
}
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9

对于以下代码,两个线程调用了不同对象的同步代码块,因此这两个线程就不需要同步。从输出结果可以看出,两个线程交叉执行。

1
2
3
4
5
6
7
8
public static void main(String[] args) {
SynchronizedExample e1 = new SynchronizedExample();
SynchronizedExample e2 = new SynchronizedExample();
ExecutorService executorService = Executors.newCachedThreadPool();
executorService.execute(() -> e1.func1());
executorService.execute(() -> e2.func1());
}
0 0 1 1 2 2 3 3 4 4 5 5 6 6 7 7 8 8 9 9
2. 同步一个方法
1
2
3
public synchronized void func () {
// ...
}

它和同步代码块一样,作用于同一个对象。

3. 同步一个类
1
2
3
4
5
public void func() {
synchronized (SynchronizedExample.class) {
// ...
}
}

作用于整个类,也就是说两个线程调用同一个类的不同对象上的这种同步语句,也会进行同步。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class SynchronizedExample {

public void func2() {
synchronized (SynchronizedExample.class) {
for (int i = 0; i < 10; i++) {
System.out.print(i + " ");
}
}
}
}
public static void main(String[] args) {
SynchronizedExample e1 = new SynchronizedExample();
SynchronizedExample e2 = new SynchronizedExample();
ExecutorService executorService = Executors.newCachedThreadPool();
executorService.execute(() -> e1.func2());
executorService.execute(() -> e2.func2());
}
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9
4. 同步一个静态方法
1
2
3
public synchronized static void fun() {
// ...
}

作用于整个类。

Java实现多线程的方式

有三种使用线程的方法:

  • 实现 Runnable 接口;
  • 实现 Callable 接口;
  • 继承 Thread 类。

实现 Runnable 和 Callable 接口的类只能当做一个可以在线程中运行的任务,不是真正意义上的线程,因此最后还需要通过 Thread 来调用。可以说任务是通过线程驱动从而执行的。

实现 Runnable 接口

需要实现 run() 方法。

通过 Thread 调用 start() 方法来启动线程。

1
2
3
4
5
6
7
8
9
10
public class MyRunnable implements Runnable {
public void run() {
// ...
}
}
public static void main(String[] args) {
MyRunnable instance = new MyRunnable();
Thread thread = new Thread(instance);
thread.start();
}

实现 Callable 接口

与 Runnable 相比,Callable 可以有返回值,返回值通过 FutureTask 进行封装。

1
2
3
4
5
6
7
8
9
10
11
12
public class MyCallable implements Callable<Integer> {
public Integer call() {
return 123;
}
}
public static void main(String[] args) throws ExecutionException, InterruptedException {
MyCallable mc = new MyCallable();
FutureTask<Integer> ft = new FutureTask<>(mc);
Thread thread = new Thread(ft);
thread.start();
System.out.println(ft.get());
}

继承 Thread 类

同样也是需要实现 run() 方法,因为 Thread 类也实现了 Runable 接口。

1
2
3
4
5
6
7
8
9
public class MyThread extends Thread {
public void run() {
// ...
}
}
public static void main(String[] args) {
MyThread mt = new MyThread();
mt.start();
}

实现接口 VS 继承 Thread

实现接口会更好一些,因为:

  • Java 不支持多重继承,因此继承了 Thread 类就无法继承其它类,但是可以实现多个接口;
  • 类可能只要求可执行就行,继承整个 Thread 类开销过大。

三种方式的区别

  • 实现 Runnable 接口可以避免 Java 单继承特性而带来的局限;增强程序的健壮性,代码能够被多个线程共享,代码与数据是独立的;适合多个相同程序代码的线程区处理同一资源的情况。
  • 继承 Thread 类和实现 Runnable 方法启动线程都是使用 start() 方法,然后 JVM 虚拟机将此线程放到就绪队列中,如果有处理机可用,则执行 run() 方法。
  • 实现 Callable 接口要实现 call() 方法,并且线程执行完毕后会有返回值。其他的两种都是重写 run() 方法,没有返回值。

Lock

Lock对象也可以实现同步,在使用上更加方便。

再Java多线程中,可以使用synchronized关键字来实现线程之间同步互斥,但是再jdk1.5以后增加了ReentrantLock类。并且功能更加强大、灵活。

ReentrantLock

ReentrantLock 是 java.util.concurrent(J.U.C)包中的锁。和synchronized也是互斥锁

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class LockExample {

private Lock lock = new ReentrantLock();

public void func() {
lock.lock();
try {
for (int i = 0; i < 10; i++) {
System.out.print(i + " ");
}
} finally {
lock.unlock(); // 确保释放锁,从而避免发生死锁。
}
}
}
public static void main(String[] args) {
LockExample lockExample = new LockExample();
ExecutorService executorService = Executors.newCachedThreadPool();
executorService.execute(() -> lockExample.func());
executorService.execute(() -> lockExample.func());
}
// 最后输出 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
public class ReentrantLockDemo {

private Lock lock = new ReentrantLock();

public void test(){
// 调用ReentrantLock的lock方法获取锁
lock.lock();
for (int i = 0; i < 3; i++) {
System.out.println("ThreadName " + Thread.currentThread().getName() + " number " + i);
}
// 调用ReentrantLock的lock方法释放锁
lock.unlock();
}
}

public class ReentrantLockThread extends Thread{
private ReentrantLockDemo reentrantLockDemo;

public ReentrantLockThread(ReentrantLockDemo reentrantLockDemo) {
this.reentrantLockDemo = reentrantLockDemo;
}

@Override
public void run() {
reentrantLockDemo.test();
}
}

public class ReentrantLockTests {

public static void main(String[] args) {
ReentrantLockDemo reentrantLockDemo = new ReentrantLockDemo();
ReentrantLockThread reentrantLockThread1 = new ReentrantLockThread(reentrantLockDemo);
ReentrantLockThread reentrantLockThread2 = new ReentrantLockThread(reentrantLockDemo);
ReentrantLockThread reentrantLockThread3 = new ReentrantLockThread(reentrantLockDemo);
ReentrantLockThread reentrantLockThread4 = new ReentrantLockThread(reentrantLockDemo);
reentrantLockThread1.start();
reentrantLockThread2.start();
reentrantLockThread3.start();
reentrantLockThread4.start();
}

}

输出结果

1
2
3
4
5
6
7
8
9
10
11
12
ThreadName Thread-0 number 0
ThreadName Thread-0 number 1
ThreadName Thread-0 number 2
ThreadName Thread-2 number 0
ThreadName Thread-2 number 1
ThreadName Thread-2 number 2
ThreadName Thread-3 number 0
ThreadName Thread-3 number 1
ThreadName Thread-3 number 2
ThreadName Thread-1 number 0
ThreadName Thread-1 number 1
ThreadName Thread-1 number 2
  • 当前的线程执行完成之后才能执行其他线程,但是其他线程执行的顺序是随机的。

公平锁 VS 非公平锁

  • 公平锁的含义是线程需要按照请求的顺序来获得锁;而非公平锁则允许“插队”的情况存在,所谓的“插队”指的是,线程在发送请求的同时该锁的状态恰好变成了可用,那么此线程就可以跳过队列中所有排队的线程直接拥有该锁。
  • 而公平锁由于有挂起和恢复所以存在一定的开销,因此性能不如非公平锁,所以 ReentrantLock 和 synchronized 默认都是非公平锁的实现方式。