1、什么是线程
Java monitor机制,写的很详细,本文不介绍了https://www.cnblogs.com/qingshan-tang/p/12698705.html
com.lang.Thread类,感兴趣同学自行阅读该源码
1.1 进程
进程(Process)是计算机中的程序关于某数据集合上的一次运行活动,是系统进行资源分配和调度的基本单位,是操作系统结构的基础。在早期面向进程设计的计算机结构中,进程是程序的基本执行实体;在当代面向线程设计的计算机结构中,进程是线程的容器。程序是指令、数据及其组织形式的描述,进程是程序的实体。进程参考资料
1.1.1 进程切换
进程从硬盘读取我们的程序代码,这个时候是比较费时的,CPU不会阻塞在这里等着,而是切换到其他进程,当数据加载完成是,CPU会收到个中断,继续执行这个请求。
对于单核CPU而言,短时间内会执行多少进程,造成并行的错觉,实际可以理解为是并发的操作。
1.1.2 内核态与用户态
进程分为用户进程和内核进程两种。为了安全,用户进程是受限的,它不能随意访问资源、获取资源。所以,由内核进程负责管理和分配资源,它具有最高权限,而用户进程使用被分配的资源。且,操作系统必须能够在有需要的时候能立即切换回内核进程(通过中断),只有这样,操作系统才能有安全感。
1.1.3 用户态向内核态切换
1、发生系统调用时
2、产生异常时
3、外设产生中断时
这里不做过多解释了,感兴趣同学自行学习linux原理。
1.3 线程
线程(英语:thread)是操作系统能够进行运算调度的最小单位。它被包含在进程之中,是进程中的实际运作单位。一条线程指的是进程中一个单一顺序的控制流,一个进程中可以并发多个线程,每条线程并行执行不同的任务。百度百科
1.3.1 进程与线程的关系
1、一个进程中可以同时存在多个线程;
2、各个线程之间可以并发执行;
3、各个线程之间可以共享当前线程中的地址空间和文件等资源;
4、线程是调度的基本单位,而进程则是资源拥有的基本单位;
5、当进程只有一个线程时,可以认为进程就等于线程;
1.3.2 线程内存模型
(1) 在看线程内存模型之前,我们首先看下计算机的硬件内存模型。
寄存器:寄存器部件,包括通用寄存器、专用寄存器和控制寄存器。通用寄存器又可分定点数和浮点数两类,它们用来保存指令执行过程中临时存放的寄存器操作数和中间(或最终)的操作结果。 通用寄存器是中央处理器的重要部件之一。
高速多级缓存:用于解决CPU核心与内存的速度差异问题,CPU核心速度快,内存相比要慢很多,而缓存要比内存速度快。
缓存协议:多级缓存的引入在多核CPU时代导致了缓存不一致的问题,这里需要引入MESI协议缓存解决这一问题,保证缓存中的信息与内存中的信息一致。MESI协议缓存这里不做讲解。
(2)下一步我们看下java内存模型与线程模型的关系
java程序的一次编写到处运行如何体现的?jvm内存模型屏蔽了不同硬件的内存模型。jvm内存模型规定,所有的变量都存储在jvm主内存中,即堆内存,这里指可共享的变量(new出来的实例化对象,数组等)。这里提出主内存与工作内存的概念,每个线程有自己的工作内存,保存自己私有的变量,线程从主内存获取变量的副本作为自己的私有变量,不允许直接操作主内存的变量。线程的工作内存也是独立的,无法操作其他线程的变量。
(3)硬件内存模型与jvm内存模型的关系
如下图所示,jvm内存模型和硬件内存模型相似,但并不完全相同。但是jvm的数据大部分会存储到硬件主内存中,部分会存储到寄存器或缓存中,这里只做简单说明。
2、线程实现
2.1 继承Thread类
static class ExtendThread extends Thread {
@Override
public void run() {
System.out.println("继承Thread类方式");
}
}
public static void main(String[] args) {
//实例化一个对象
ExtendThread extendThread = new ExtendThread();
//start()执行
extendThread.start();
}
结果:
继承Thread类方式
2.2 实现runnable接口
static class ImplRunnable implements Runnable {
@Override
public void run() {
System.out.println("实现Runnable接口方式");
}
}
public static void main(String[] args) {
new Thread(new ImplRunnable()).start();
}
结果:
实现Runnable接口方式
2.3 实现callable接口
static class ImplCallable implements Callable {
private String str;
public ImplCallable(String str) {
this.str = str;
}
@Override
public Object call() throws Exception {
System.out.println("实现callable接口方式");
return str;
}
}
public static void main(String[] args) throws ExecutionException, InterruptedException {
//创建目标对象
ImplCallable mc = new ImplCallable("实现callable接口方式");
//创建执行服务
ExecutorService ser = Executors.newFixedThreadPool(2);
//提交执行
Future<String> result = ser.submit(mc);
//获取结果
String msg = result.get();
//关闭服务
ser.shutdownNow();
System.out.println("线程执行结果:" + msg);
}
结果:
实现callable接口方式
线程执行结果:实现callable接口方式
2.4 lambda表达式简化实现
提供一个jdk8的方式, lambda表达式。
public static void main(String[] args) {
new Thread(() -> {
System.out.println("lambda表达式简化实现");
}).start();
}
结果:
lambda表达式简化实现
3、常用方法
3.1 start()
用于启动线程,具体请看以下源码内容
public synchronized void start() {
//判断线程状态是否为0,默认初始化为0
if (threadStatus != 0)
throw new IllegalThreadStateException();
//通知组此线程即将启动,以便可以将其添加到组的线程列表中,修改未启动和已启动数量
group.add(this);
//启动状态默认为false
boolean started = false;
try {
//调用native方法,底层jvm开启异步线程,并调用run方法
start0();
//线程已启动
started = true;
} finally {
try {
if (!started) {
//移除线程组中的该线程,并修改未启动线程数
group.threadStartFailed(this);
}
} catch (Throwable ignore) {
//将start0方法抛出的异常加入堆栈
}
}
}
3.2 run()
通常需要重写该方法,线程需要执行的具体内容。
public void run() {
if (target != null) {
//这里执行我们自己重写的run方法
target.run();
}
}
3.3 currentThread()
返回当前执行线程的Thread对象
public static native Thread currentThread();
3.4 getName()
获取当前线程名称
public final String getName() {
return name;
}
3.5 setName()
给线程设置自定义名称,一定要在start方法前执行。
public final synchronized void setName(String name) {
//健康检查,这不做过多研究
checkAccess();
//非空验证
if (name == null) {
throw new NullPointerException("name cannot be null");
}
this.name = name;
//当状态不为0,表示线程已启动,不能设置名字
if (threadStatus != 0) {
//调用底层方法,不做研究
setNativeName(name);
}
}
3.6 yield()
放弃当前处理器的占用
public static native void yield()
3.7 join()
在当前执行线程a中,另一个线程b调用该方法,则线程a进入阻塞状态,直到线程b执行完毕,线程a继续执行。参照下面示例:
public class Join extends Thread {
public static void main(String[] args) throws InterruptedException {
Join join = new Join();
join.setName("我是join线程");
join.start();
//调用join方法
join.join();
System.out.println("我是main线程");
}
@Override
public void run() {
for (int i=0; i<3; i++) {
System.out.println(Thread.currentThread().getName() + "i=" + i) ;
}
}
}
执行结果:
我是join线程i=0
我是join线程i=1
我是join线程i=2
我是main线程
3.8 sleep()
阻塞当前线程指定的毫秒数
public static native void sleep(long millis) throws InterruptedException;
3.9 isAlive()
判断当前线程是否存活
public final native boolean isAlive();
4、线程状态
Thread类中定义了state枚举,可以通过getState()获取
public enum State {
/**
* 初始
*/
NEW,
/**
* 可运行
*/
RUNNABLE,
/**
* 阻塞
*/
BLOCKED,
/**
* 等待
*/
WAITING,
/**
* 超时等待
*/
TIMED_WAITING,
/**
* 终止
*/
TERMINATED;
}
下图是通过idea集成plantuml插件画的,无法控制布局,请各位忍受,后面的图使用visio。
5、线程同步
多个线程同时调用同一个对象,或者修改同一个变量,为了保证安全性与准确性,引入了同步的概念。关于线程同步大概归纳整理以下几种方式。
举个例子:
有一个库存类,原本100个库存,现在启动多线程增加100个库存,在没有线程同步的情况下。
/**
* 库存
*/
static class Inventory {
//库存数量
private int num = 100;
//增加库存
public void add(int n) {
num += n;
System.out.println("增加库存后的数量=" + num);
}
}
public static void main(String[] args) {
//初始化库存
Inventory inventory = new Inventory();
for (int i = 0; i < 100; i++) {
new Thread(() -> {
inventory.add(1);
}).start();
}
}
取最后几条结果,发现库存最终并没有达到预想的200个:
增加库存后的数量=179
增加库存后的数量=177
增加库存后的数量=177
增加库存后的数量=175
增加库存后的数量=174
增加库存后的数量=174
增加库存后的数量=171
增加库存后的数量=169
增加库存后的数量=167
Process finished with exit code 0
5.1 synchronized同步方法
使用同步方法的方式看看能否解决这个问题,在add方法增加synchronized,锁住该方法。
/**
* 库存
*/
static class Inventory {
//库存数量
private int num = 100;
//增加库存
public synchronized void add(int n) {
num += n;
System.out.println("增加库存后的数量=" + num);
}
}
public static void main(String[] args) {
//初始化库存
Inventory inventory = new Inventory();
for (int i = 0; i < 100; i++) {
new Thread(() -> {
inventory.add(1);
}).start();
}
}
查看结果,发现此时结果按照顺序展示,达到预期。
...
增加库存后的数量=192
增加库存后的数量=193
增加库存后的数量=194
增加库存后的数量=195
增加库存后的数量=196
增加库存后的数量=197
增加库存后的数量=198
增加库存后的数量=199
增加库存后的数量=200
Process finished with exit code 0
5.2 synchronized同步代码块
使用同步代码块的方式解决该问题,在add方法内部加入同步代码块,如下代码所以
/**
* 库存
*/
static class Inventory {
//库存数量
private int num = 100;
//增加库存
public void add(int n) {
synchronized (this) {
num += n;
System.out.println("增加库存后的数量=" + num);
}
}
}
public static void main(String[] args) {
//初始化库存
Inventory inventory = new Inventory();
for (int i = 0; i < 100; i++) {
new Thread(() -> {
inventory.add(1);
}).start();
}
}
取部分结果,能够得到预期结果
...
增加库存后的数量=196
增加库存后的数量=197
增加库存后的数量=198
增加库存后的数量=199
增加库存后的数量=200
5.3 volatile关键字
网上很多说通过volatile关键字解决线程同步的问题,但是我想说,这个关键字虽说能够阻止指令重排序,让变量线程间可见,但并不能完全实现线程同步,代码来看下。
/**
* 库存
*/
static class Inventory {
//库存数量
private volatile int num = 100;
//增加库存
public void add(int n) {
num += n;
System.out.println("增加库存后的数量=" + num);
}
//减少库存
public void sub(int n) {
num -= n;
System.out.println("减少库存后的数量=" + num);
}
}
public static void main(String[] args) {
Inventory inventory = new Inventory();
for(int i = 0;i<100;i++) {
new Thread(() -> {
inventory.add(1);
}).start();
}
for(int i = 0;i<100;i++) {
new Thread(() -> {
inventory.sub(1);
}).start();
}
}
结果,后面会专门针对volatile进行讲解:
...
减少库存后的数量=110
减少库存后的数量=111
减少库存后的数量=105
减少库存后的数量=103
减少库存后的数量=101
减少库存后的数量=100
减少库存后的数量=102
5.4 重入锁
/**
* 库存
*/
static class Inventory {
//初始化ReentrantLock实例
Lock lock = new ReentrantLock();
//库存数量
private int num = 100;
//增加库存
public void add(int n) {
//加锁
lock.lock();
try {
num += n;
System.out.println("增加库存后的数量=" + num);
} finally {
//释放锁
lock.unlock();
}
}
//减少库存
public void sub(int n) {
//加锁
lock.lock();
try {
num -= n;
System.out.println("减少库存后的数量=" + num);
} finally {
//释放锁
lock.unlock();
}
}
}
public static void main(String[] args) {
Inventory inventory = new Inventory();
for (int i = 0; i < 100; i++) {
new Thread(() -> {
inventory.add(1);
}).start();
}
for (int i = 0; i < 100; i++) {
new Thread(() -> {
inventory.sub(1);
}).start();
}
}
5.5 阻塞队列
定义LinkedBlockingQueue阻塞队列,简单实现增加一个,消费一个的线程同步功能
/**
* 定义一个元素的队列,通过阻塞达到新增一个消费一个的效果
* 数组元素定义多个时,因消费线程是100个独立线程,执行时间顺序存在差异,不能保证完全顺序执行
*/
final static LinkedBlockingQueue<String> queue = new LinkedBlockingQueue<>(1);
/**
* 库存
*/
static class Inventory {
//增加库存
public void add(int n) {
try {
System.out.println("开始增加第" + n + "库存。。。。。");
//压入队列
queue.put("第" + n + "库存。。。。。");
} catch (Exception e) {
e.printStackTrace();
}
}
//减少库存
public void sub() {
try {
//取出数据
System.out.println("[取出]====" + queue.take());
} catch (Exception e) {
e.printStackTrace();
}
}
}
public static void main(String[] args) {
Inventory inventory = new Inventory();
new Thread(() -> {
for (int i = 1; i <= 100; i++) {
inventory.add(i);
}
}).start();
for (int i = 0; i < 100; i++) {
new Thread(() -> {
inventory.sub();
}).start();
}
}
结果:
...
增加库存后的数量=101
取出队列中的数量=101
减少库存后的数量=100
增加库存后的数量=101
取出队列中的数量=101
减少库存后的数量=100
增加库存后的数量=101
5.6 原子类
使用原子类的形式实现线程同步,内部实现使用了CAS(compareAndSwapInt自旋锁)机制。
/**
* 库存
*/
static class Inventory {
private AtomicInteger num = new AtomicInteger(100);
//增加库存
public void add(int n) {
//增加库存
num.incrementAndGet();
System.out.println("增加库存后的数量=" + num);
}
}
public static void main(String[] args) {
Inventory inventory = new Inventory();
for (int i = 0; i < 100; i++) {
new Thread(() -> {
inventory.add(1);
}).start();
}
}
结果:
...
增加库存后的数量=196
增加库存后的数量=197
增加库存后的数量=198
增加库存后的数量=199
增加库存后的数量=200
Process finished with exit code 0
5.7 局部变量
使用局部限量形式,模拟两个线程分别处理,相互之间不进行影响。
/**
* 库存
*/
static class Inventory {
private ThreadLocal<Integer> num = ThreadLocal.withInitial(() -> 100);
//增加库存
public void add(int n, String threadName) {
//增加库存
num.set(num.get() + n);
System.out.println("线程:" + threadName + ",增加库存后的数量=" + num.get());
}
}
public static void main(String[] args) {
for (int i = 0; i < 2; i++) {
new Thread(() -> {
Inventory inventory = new Inventory();
for (int j = 0; j < 10; j++) {
inventory.add(1,Thread.currentThread().getName());
}
}).start();
}
}
结果:
线程:Thread-0,增加库存后的数量=101
线程:Thread-0,增加库存后的数量=102
线程:Thread-0,增加库存后的数量=103
线程:Thread-0,增加库存后的数量=104
线程:Thread-0,增加库存后的数量=105
线程:Thread-0,增加库存后的数量=106
线程:Thread-0,增加库存后的数量=107
线程:Thread-0,增加库存后的数量=108
线程:Thread-0,增加库存后的数量=109
线程:Thread-0,增加库存后的数量=110
线程:Thread-1,增加库存后的数量=101
线程:Thread-1,增加库存后的数量=102
线程:Thread-1,增加库存后的数量=103
线程:Thread-1,增加库存后的数量=104
线程:Thread-1,增加库存后的数量=105
线程:Thread-1,增加库存后的数量=106
线程:Thread-1,增加库存后的数量=107
线程:Thread-1,增加库存后的数量=108
线程:Thread-1,增加库存后的数量=109
线程:Thread-1,增加库存后的数量=110
6、synchronized锁升级
在jdk1.6之前,synchronized的锁只有一种方式,即重量级锁;在之后引入了偏向锁,轻量级锁来缓解锁竞争问题。从此以后锁的状态就有了四种(无锁、偏向锁、轻量级锁、重量级锁),并且四种状态会随着竞争的情况逐渐升级,而且是不可逆的过程,即不可降级。
6.1 java对象
在讲锁之前,先简单介绍下java对的概念。如下图,java对象分为对象头,对象体,对齐字段。
6.1.1 对象头
当对象为普通对象时,对象头只包含Mark Word 和Klass Word,当时数组对象时,会包含数组长度。
Mark Word:先看下面一张图,图片来源:https://www.cnblogs.com/ZoHy/p/11313155.html
从上图看到,在Mark Word中,包含以下主要信息:
1.locked:两位二进制的锁状态标志位,配合biased_lock表示java对象各阶段不同锁状态。
2.biased_lock:只占一位的二进制偏向锁标记,1表示启用偏向锁,0表示未启用。
3.identity_hashcode:31位的对象标识hashCode,采用延迟加载技术。调用方法System.identityHashCode()计算,并会将结果写到该对象头中。当对象加锁后(偏向、轻量级、重量级),MarkWord的字节没有足够的空间保存hashCode,因此该值会移动到管程Monitor(也可称为监视器)中。
4.thread:54位的持有偏向锁的线程id
5.epoch:偏向锁的时间戳。
6.ptr_to_lock_record:轻量级锁状态下,指向栈中锁记录的指针。
7.ptr_to_heavyweight_monitor:重量级锁状态下,指向对象监视器Monitor的指针。
8:age:4位的Java对象年龄。
Klass Word:存储一个地址,长度取决于系统位数,32位和64位,该地址指向方法区中类的元数据信息。
这里放个jdk1.8的jvm模型图,看一下堆与方法区的关系。
可参考后面的链接学习jvm模型知识:https://www.cnblogs.com/paddix/p/5309550.html
数组长度:32位的JVM上,长度为32位;64位JVM则为64位。64位JVM如果开启+UseCompressedOops选项,该区域长度也将由64位压缩至32位。
6.1.2对象体
主要存放对象的属性值。
6.1.3对齐字节
也可称为补齐区域,如果对象总大小不是4字节的整数倍,会填充上一段内存地址使之成为整数倍。
6.3 锁升级
大概了解什么是对象头以及Mark Word后,我们来讨论下锁升级的过程。在jdk1.6之前,只有重量级锁,通过阻塞和唤醒的形式,需要cpu不停地切换状态,这个过程有可能比用户代码执行的时间还长,这即是1.6之前效率低下的原因。1.6之后引入偏向锁和轻量级锁解决效率低下这一问题。
6.3.1 四种锁Mark Word
6.3.2 四种锁的优缺点对比
6.3.3 锁升级过程分析
图片来源https://tech.souyunku.com/?p=37386
偏向锁
1.当线程访问同步块,首先检查当前对象头是否包含线程1的id,如果没有,则通过CAS替换Mark Word,替换成功则升级为偏向锁。
2.升级为偏向锁时,会将Mark Word中的内容设置为当前线程id,继续执行同步代码块。这是线程2也来访问同步块,首先检查对象头是否包含线程2的id,如果没有,则会将Mark Word的内容替换为线程2的id,失败的话则意味着存在锁竞争。
3.存在竞争意味着需要升级偏向锁为轻量级锁,在此之前需要先撤销对象的偏向锁(在安全点,没有线程操作字节码),变为无锁的状态,然后升级为轻量级锁。
轻量级锁
1、在偏向锁撤销后,需要升级为轻量级锁,线程1和线程2同时访问同步代码块,通过CAS修改对象的Mark Word,将其修改成每个线程自己的LockRecord,成功则得到锁。失败则表示已经被其他线程占用,继续通过自旋获取锁。
2、当自旋满足以下两个条件之一时,发生锁膨胀,升级为重量级锁。
1)在jdk1.6前,默认10次,可通过-XX:PreBlockSpin来修改,或者自旋线程数超过CPU核数的一半。
2)jdk1.6之后,引入了自适应自旋锁,次数并非一成不变。根据获取锁的成功率来决定是否能有更长的等待时间。
升级重量级锁后,会修改对象Mark Word,同时阻塞,并等待占有锁线程释放锁并唤醒其他线程,阻塞线程被唤醒后,继续开始争夺锁访问同步块。
7、synchronized静态同步方法与非静态同步方法
1.同步锁和对象锁是否互斥
同步静态方法,是类锁
同步非静态方法,是对象所
这两种锁是不同的,所以相互之前不会产生互斥。
通过一段代码进行验证:
public class SynchronizedStaticAndNonStatic {
public static int count = 0;
public static synchronized void inc() throws InterruptedException {
count++;
//这里将静态方法阻塞,验证是否能访问到inc2()
Thread.sleep(10000);
System.out.println("结果1: " + count);
}
public synchronized void inc2() {
count++;
System.out.println("结果2: " + count);
}
public static void main(String[] args) throws InterruptedException {
//调用静态同步方法
for (int i = 0; i < 5; i++) {
new Thread(() -> {
//同步静态方法
try {
SynchronizedStaticAndNonStatic.inc();
} catch (InterruptedException e) {
e.printStackTrace();
}
}).start();
}
//调用非静态同步方法前先进行阻塞一秒,确保静态同步方法在执行阻塞过程中
Thread.sleep(1000);
for (int i = 0; i < 5; i++) {
new Thread(() -> {
//同步动态方法
SynchronizedStaticAndNonStatic t = new SynchronizedStaticAndNonStatic();
t.inc2();
}).start();
}
}
}
结果分析:
结果2: 3
结果2: 4
结果2: 3
结果2: 5
结果2: 6
结果1: 6
结果1: 7
结果1: 8
结果1: 9
结果1: 10
当结果一进入sleep后,并没有对非静态方法inc2进行阻塞,inc2率先执行完毕。得出在静态方法加同步锁,并不会影响其他对象的非静态同步方法。类锁和对象锁并不互斥。
2.对象锁的同步方法是否互斥
public synchronized void inc3(String tName) throws InterruptedException {
System.out.println("结果3: " + tName);
Thread.sleep(10000);
}
public synchronized void inc4(String tName) {
System.out.println("结果4: " + tName);
}
public static void main(String[] args) {
SynchronizedStaticAndNonStatic t = new SynchronizedStaticAndNonStatic();
for (int i = 0; i < 3; i++) {
new Thread(() -> {
try {
t.inc3(Thread.currentThread().getName());
} catch (InterruptedException e) {
e.printStackTrace();
}
}).start();
}
for (int i = 0; i < 3; i++) {
new Thread(() -> {
t.inc4(Thread.currentThread().getName());
}).start();
}
}
结果分析:在第一条结果3出来之前,休眠了10秒,结果4才进行打印,同一个对象在两个线程中分别访问该对象的两个同步方法是互斥的。
结果3: Thread-0
sleep 10s...
结果4: Thread-3
结果4: Thread-5
结果4: Thread-4
结果3: Thread-2
结果3: Thread-1
3.非静态锁方法,不同对象在两个线程中调用同一个同步方法
public synchronized void inc5(String tName) throws InterruptedException {
System.out.println("结果5: " + tName);
Thread.sleep(10000);
System.out.println("结果5操作完成: " + tName);
}
public static void main(String[] args) {
for (int i = 0; i < 5; i++) {
new Thread(() -> {
//同步动态方法
SynchronizedStaticAndNonStatic t = new SynchronizedStaticAndNonStatic();
try {
t.inc5(Thread.currentThread().getName());
} catch (InterruptedException e) {
e.printStackTrace();
}
}).start();
}
}
结果分析:“结果5”的提示几乎同时输出,然后sleep了10秒后,才出现操作完成,证明一个对象阻塞后,并不会与其他对象的同步方法互斥。
结果5: Thread-0
结果5: Thread-4
结果5: Thread-3
结果5: Thread-2
结果5: Thread-1
结果5操作完成: Thread-3
结果5操作完成: Thread-4
结果5操作完成: Thread-0
结果5操作完成: Thread-1
结果5操作完成: Thread-2
4.用类直接在两个线程中调用两个不同的同步方法
public static synchronized void inc() throws InterruptedException {
count++;
Thread.sleep(10000);
System.out.println("结果1: " + count);
}
public static synchronized void inc6() throws InterruptedException {
count++;
Thread.sleep(10000);
System.out.println("结果6: " + count);
}
public static void main(String[] args) {
for (int i = 0; i < 5; i++) {
new Thread(() -> {
try {
SynchronizedStaticAndNonStatic.inc();
} catch (InterruptedException e) {
e.printStackTrace();
}
}).start();
}
for (int i = 0; i < 5; i++) {
new Thread(() -> {
try {
SynchronizedStaticAndNonStatic.inc6();
} catch (InterruptedException e) {
e.printStackTrace();
}
}).start();
}
}
结果分析:无论结果1还是结果6,每条结果间隔都是10秒,用类直接在两个线程中调用两个不同的同步方法是互斥的。
结果1: 1
结果6: 2
结果6: 3
结果6: 4
结果6: 5
结果6: 6
结果1: 7
结果1: 8
结果1: 9
结果1: 10
8、synchronized锁重入
问题提出:当一个线程获取了一个对象的锁,在方法内部是否能够获取其他方法的锁?答案是肯定的,Synchronized是可重入锁。
实现原理:通过monitor机制,monitor通过维护一个计数器来记录锁的获取,重入,释放情况。
看个例子:
static class Inventory {
//库存数量
private int num = 100;
//增加库存
public synchronized void add(int n) {
num += n;
System.out.println("增加库存后的数量=" + num);
sub(n);
}
//增加库存
public synchronized void sub(int n) {
num -= n;
System.out.println("减少库存后的数量=" + num);
}
}
public static void main(String[] args) {
//初始化库存
Inventory inventory = new Inventory();
new Thread(() -> {
inventory.add(1);
}).start();
}
结果:线程调用同步方法add,其内部又调用了同步方法sub,看到结果正常返回,仍然能够获取到锁。
增加库存后的数量=101
减少库存后的数量=100
还有另外一种情况,当子类继承父类时,子类也是可以通过可重入锁调用父类的同步方法。
static class Inventory extends InventoryParent{
//增加库存
public synchronized void add(int n) {
super.num += n;
System.out.println("增加库存后的数量=" + super.num);
super.sub(n);
}
}
static class InventoryParent {
//库存数量
private int num = 100;
//增加库存
public synchronized void sub(int n) {
num -= n;
System.out.println("减少库存后的数量=" + num);
}
}
public static void main(String[] args) {
//初始化库存
Inventory inventory = new Inventory();
new Thread(() -> {
inventory.add(1);
}).start();
}
结果:
增加库存后的数量=101
减少库存后的数量=100
9、volatile
参考https://baijiahao.baidu.com/s?id=1663045221235771554&wfr=spider&for=pc
很详细
volatile 的特性
1、保证了不同线程对这个变量进行操作时的可见性,即一个线程修改了某个变量的值,这新值对其他线程来说是立即可见的。(实现可见性)
2、禁止进行指令重排序。(实现有序性)
3、volatile 只能保证对单次读/写的原子性。i++ 这种操作不能保证原子性。
10、wait notify (面试高频)
是java.lang.Object下提供的方法,其中有五个相关方法
//随机唤醒等待队列中等待同一共享资源的一个线程,并使该线程退出等待队列,进入可运行状态,也就是notify()方法仅通知一个线程。
public final native void notify();
//使所有正在等待队列中等待同一共享资源的全部线程退出等待队列,进入可运行状态。此时,优先级最高的那个线程最先执行,但也有可能是随机执行,这取决于JVM虚拟机的实现。
public final native void notifyAll();
//超时等待一段时间,这里的参数时间是毫秒,也就是等待长达n毫秒,如果没有通知就超时返回
public final native void wait(long timeout) throws InterruptedException;
//对于超时时间更细力度的控制,单位为纳秒
public final void wait(long timeout, int nanos) throws InterruptedException {
if (timeout < 0) {
throw new IllegalArgumentException("timeout value is negative");
}
if (nanos < 0 || nanos > 999999) {
throw new IllegalArgumentException(
"nanosecond timeout value out of range");
}
if (nanos > 0) {
timeout++;
}
wait(timeout);
}
//使调用该方法的线程释放共享资源锁,然后从运行状态退出,进入等待队列,直到被再次唤醒
public final void wait() throws InterruptedException {
wait(0);
}
使用wait和notify实现数字和英文字母的交替打印
static class Print {
/**
* 当值为1时打印数字,当值为2时打印字母
*/
private int flag = 1;
private int count = 1;
public synchronized void printNum() {
if (flag != 1) {
try {
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.print(count);
flag = 2;
notify();
}
public synchronized void printChar() {
if (flag != 2) {
//打印字母
try {
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.print((char) (count - 1 + 'A'));
count++;
flag = 1;
notify();
}
}
public static void main(String[] args) {
Print print = new Print();
new Thread(() -> {
for (int i = 0; i < 26; i++) {
print.printNum();
}
}).start();
new Thread(() -> {
for (int i = 0; i < 26; i++) {
print.printChar();
}
}).start();
}
结果:
1A2B3C4D5E6F7G8H9I10J11K12L13M14N15O16P17Q18R19S20T21U22V23W24X25Y26Z