多线程与高并发
目录
多线程与高并发 https://www.cnblogs.com/Zs-book1/p/14318992.html?share_token=641d3935-0525-44d5-a772-9764bf2fad2a
一、了解多线程
什么是进程?
什么是线程?
并发与并行的区别
临界区
学习线程必须知道的概念:
二、 线程的使用
三种方式的区别
线程的方法
线程的状态
三、Synchronized
JMM模型
volatile
synchronized
synchronized的使用
synchronized锁定的对象不能是基本类型和String类型
synchronized保证了可见性、原子性、有序性
synchroinzed是如何保证可见性的?
synchronized是如何保证原子性的?
什么是原子性?
synchronized如何保证原子性
synchronized保证有序性
synchronized锁升级过程
什么是重量级锁?
一个对象的内容
markword
锁升级
AtomicInteger
什么是CAS
CAS的工作原理
LongAdder
Unsafe
什么是Unsafe
Unsafe的使用
使用 Unsafe进行CAS操作
五、JUC中的锁
ReentrantLock
ReentrantLock的使用
ReentrantLock是可重入锁。
ReentrantLock的方法:
ReentrantLock和synchronized的区别:
Condition
condition方法
使用Condition
Object监视器和Condition的区别
使用Condition实现阻塞队列BlockingQueue
CountDownLatch
CyclicBarrier
CyclicBarrier与CountDownLatch的区别
Semaphore
Phaser
ReadWriteLock
LockSupport
AQS
ReentrantLock的实现:
CountDownLatch的实现:
六、强软弱虚四种引用以及ThreadLocal源码
强软弱虚引用
强引用
软引用
弱引用
虚引用
ThreadLocal源码
ThreadLocal造成的内存泄漏
七、集合容器
集合容器的分类
JUC下的容器
八、线程池
ThreadPoolExecutor
关于ThreadPoolExecutor
线程池的创建
线程工厂
ThreadGroup
自定义线程工厂
守护线程
线程池的拒绝策略RejectedExecutionHandler
自定义拒绝策略:
创建一个自定义线程工厂及拒绝策略的线程池
线程池的使用
线程池的大小设置
线程池的拓展
线程池源码阅读
ExecutorCompletionService
ForkJoinPool
九、 Disruptor
Disruptor的使用
定义事件
定义事件工厂
定义消费者
使用Disruptor
Disruptor的工作原理
disruptor的8种等待策略:
消费者处理异常
生产者类型
Disruptor对java8lambda的支持
一、了解多线程
什么是进程?
我们打开电脑上的qq时,点击qq.exe,电脑就会运行一个qq的程序,这个程序就叫做进程。
什么是线程?
当qq运行后, 我们可能会使用qq来打开多个聊天窗口进行聊天,那么每一个聊天窗口就算是一个线程。所以说,进程可以包括很多的线程。
线程和进程的区别?
进程是操作系统分配资源的最小单位,线程是CPU调度的最小单位。一个进程可以包括多个线程。
线程之间的执行是同时进行的,例如我们在qq聊天时,可以一边聊天,一边下载文件。此时下载文件和聊天这个两个操作就是两个线程。如果是一个线程的话,那么下载文件的过程中,我们就不能聊天了,只有等待文件下载完成之后我们才可以继续聊天,这就叫串形,而我们一边聊天一边下载就是并行。
说的再通俗一点:例如说我们现在想要打扫卫生,那么串形就是我自己一个人,就代表一个线程。我要先打扫卫生间,在打扫厨房,在打扫客厅,再打扫卧室… 因为我一个人,所以只能按照顺序先后执行,这样的话,就会非常的耗时间。那么还有一种方式就是我找几个朋友或者找几个家政保洁一起打扫。这样的话每一个人就相当于一个线程,大家一块打扫,你打扫你的,我打扫我的,互相之间并没有关联。此时打扫卫生总耗时就是耗时最多的一个人的时间,比如客厅空间比较大,那么打扫完整个房间的总耗时就是打扫客厅的时间,这就是多线程与单线程的区别。单线程也叫串形,多线程也叫并行。
并发与并行的区别
并发:电脑cpu是按照时间片来执行任务的,因此当电脑上运行着多个任务时,可能A任务执行一会儿,B任务执行一会儿,但是因为CPU的任务切换时间非常短,ns(纳秒)级别,因此在我们眼中看来,就像是A任务和B任务是一块执行的,也就是说A和B是并发执行的。并发说的是在同一个时间段内,多个事情在这个时间段内交替执行。
并行:当电脑上有多个核时,每个核都可以执行任务,因此假设电脑有两个核心的话,那么A任务在核心1上执行,B任务在核心2上执行,此时A和B任务是一块运行的,可以称为A和B是并行运行的。并行说的是多个事情在同一个时刻发生。
并行是并发的一种,都表示任务同步运行,因此也可以称为并发,但是并发不能称为并行。
下面以图片来理解并发和并行:
一个咖啡机就代表一个cpu核,上面的图一个咖啡机,排了两个队,那么这两个队交替到咖啡机接咖啡,交替前进就是并发。而下面两个咖啡机,每个咖啡机前都有一个队伍,这两个队伍是一起执行的,这两个队伍就叫做并行。
并发偏重于多个任务交替执行,这多个任务之间可能是串形执行的,而并行是真正意义上的同时执行。
临界区
临界区用来表示一种公共资源或者数据,可以被多个线程使用,但每次只能有一个线程使用它,一旦临界区被占用,那么其他线程过来想要获取这个资源就只能等待。
就比如办公室里的打印机,打印机就是一个公共资源。办公室里的每一个人都可以连接打印机打印文件,那么每一个电脑与打印机的连接就可以看成是一个线程。当需要打印东西时,如果一个人正在打印,那么此时他就独占了打印机这个资源,此时另一个人也想要打印东西,那么他就只能等待前一个人打印东西完成后,他就释放这个资源了,后面的人才可以连接打印自己的东西,只要前一个人没有打印完,就是还在用这个打印机,那么后面过来的人都要排队等着。等待获取这个公共资源。
学习线程必须知道的概念:
阻塞:阻塞的意思就是说线程在等待一个结果,在拿到这个结果前就在这等待着,CPU空转等待拿到结果后再继续,这个等待的过程叫做阻塞。例如:我们现在要去商店买一个玩具,但是这个玩具老板需要到仓库中找,我们就要在门口等着老板找到货后才能付款离开。等待的这个过程就是阻塞。
锁:加锁就是控制共享资源的使用权。例如一个8车道的高速公路,也就是说可以允许8辆车同时跑,这8个车道就可以看成是八个线程,而收费站就可以看成是共享资源,如果只有一个收费站的话,那么每次都只有一个车道的车辆可以通过这个收费站,在这辆车进入收费站出站之前,这辆车对这个收费站就是独占的,此时是不允许下一辆车进入的,那么这种情况就可以看成是这辆车获取了收费站这把锁。
其实,线程获取cpu也可以看成是获取锁,在一个线程获取CPU执行的过程中,其他的线程是等待的,只有当前线程的时间片用完了,那么释放锁,其他线程抢占CPU也就是获取锁。
死锁:多个线程之间互相挣抢锁,互不相让的过程。以下图理解:
A、B、C、D四辆小车都在这种情况下都无法继续行驶了。他们彼此之间相互占用了其他车辆的车道,如果大家都不愿意释放自己的车道,那么这个状况将永远持续下去,谁都不可能通过,这就是死锁。
二、 线程的使用
创建线程的三种方式:
方式1: 继承 Thread
import java.util.concurrent.TimeUnit;
/**
-
通过继承方式创建线程
-
-
@author 赵帅
-
@date 2021/1/1
*/public class CreateMyThreadByExtendThread extends Thread {
@Override
public void run() {
try {
TimeUnit.SECONDS.sleep(3);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(“通过继承方式实现自定义线程,当前线程为:” + Thread.currentThread().getName());
}
public static void main(String[] args) {
// 当前线程为主线程 main
System.out.println("Thread.currentThread().getName() = " + Thread.currentThread().getName());
// 创建一个新的线程并开启线程,打印新的线程名
CreateMyThreadByExtendThread thread = new CreateMyThreadByExtendThread();
thread.start();
}
}
方式2: 实现Runnable接口
import java.util.concurrent.TimeUnit;
/**
-
通过实现Runnable接口方式
-
-
@author 赵帅
-
@date 2021/1/1
*/public class CreateMyThreadByImplRunnable implements Runnable {
@Override
public void run() {
try {
TimeUnit.SECONDS.sleep(3);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(“通过实现Runnable接口方式实现自定义线程,当前线程为:” + Thread.currentThread().getName());
}
public static void main(String[] args) {
// 当前线程为主线程 main
System.out.println("Thread.currentThread().getName() = " + Thread.currentThread().getName());
// 创建一个新的线程并开启线程,打印新的线程名
Thread thread = new Thread(new CreateMyThreadByImplRunnable());
thread.start();
}
}
方式3: Callable+Feature
import java.util.concurrent.*;
/**
-
通过实现Callable接口方式
-
-
@author 赵帅
-
@date 2021/1/1
*/public class CreateMyThreadByImplCallable implements Callable {
@Override
public String call() throws Exception {
try {
TimeUnit.SECONDS.sleep(3);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(“通过实现Callable接口方式实现自定义线程,当前线程为:” + Thread.currentThread().getName());
return “hello”;
}
public static void main(String[] args) throws ExecutionException, InterruptedException {
// 当前线程为主线程 main
System.out.println("Thread.currentThread().getName() = " + Thread.currentThread().getName());
// callable接口实现的任务和Future接口或线程池一起使用
CreateMyThreadByImplCallable callable = new CreateMyThreadByImplCallable();
// 和Feature一起使用,Future表示未来,也就是说我执行这个任务,未来我去取任务执行的结果
FutureTask<String> task = new FutureTask<>(callable);
new Thread(task).start();
System.out.println("main线程继续运行");
// 等其他线程执行完了,再来拿task的结果,
System.out.println("task.get() = " + task.get());
// 使用线程池方式调用callable
Future<String> submit = Executors.newSingleThreadExecutor().submit(callable);
System.out.println("submit.get() = " + submit.get());
}
}
三种方式的区别
通过上面三种创建线程的方式,我们对线程的使用有了基本的了解 ,下面我们来分析这三种方式有什么区别:
1.继承Thread: 通过继承方式创建线程,因为java单继承的特性,使用的限制就非常多了,使用不方便。
2.实现Runnable: 因为单继承的限制,所以出现了Runnable,接口可以多实现,因此大大提高了程序的灵活性。但是无论是继承Thread还是实现Runnable接口,线程执行的方法都是 void 返回值。
3.实现Callable: Callable接口就是为了解决线程没有返回值的问题,Callable接口有一个泛型类型,这个泛型就代表返回值的类型,使用Callable接口就可以开启一个线程取执行, Callable一般和Future接口同时使用,返回值为Future类型,可以通过Future接口的get方法拿执行结果。get()方法是一个阻塞的方法。
相同点:都是通过Thread类的start()方法来开启线程。
线程的方法
start(): 开启线程,使线程从新建进入就绪状态
sleep(): 睡眠,使当前线程休息, 需要指定睡眠时间,当执行sleep方法后进入阻塞状态,
Join():加入线程,会将调用的线程加入当前线程。等待加入的线程执行完成后才会继续执行当前线程。
/**
-
线程方法示例
-
@author 赵帅
-
@date 2021/1/1
*/public class ThreadMethodDemo {
public static void main(String[] args) throws InterruptedException {
// 打印当前线程 main线程的线程名 main
System.out.println("当前主线程线程名 = " + Thread.currentThread().getName());
// 创建一个新的线程
Thread thread = new Thread(() -> {
// 线程进入睡眠状态
try {
Thread.sleep(1000L);
System.out.println("当前线程名:" + Thread.currentThread().getName());
} catch (InterruptedException e) {
e.printStackTrace();
}
});
// 开启线程 thread
thread.start();
// thread.join();
System.out.println(“主线程执行完毕”);
}
}
打开和关闭注释thread.join()可以发现输出结果是不一样的,使用join后会等待thread执行结束后再继续执行main方法。
wait(): 当前线程进入等待状态,让出CPU给其他线程,自己进入等待队列,等待被唤醒。
notify(): 唤醒等待队列中的一个线程,唤醒后会重新进入就绪状态,准备抢夺CPU。
notifyAll(): 唤醒等待队列中的所有线程,抢夺CPU。
yield(): 让出CPU。当前线程让出CPU给其他的线程执行,但是自己也会进入就绪状态参与CPU的抢夺,因此调用yield方法后,仍然可能继续获得CPU。
import java.util.concurrent.TimeUnit;
/**
-
线程方法示例
-
@author 赵帅
-
@date 2021/1/1
*/public class ThreadMethodDemo {
public static void main(String[] args) throws InterruptedException {
// 打印当前线程 main线程的线程名 main
System.out.println("当前主线程线程名 = " + Thread.currentThread().getName());
Object obj = new Object();
// 创建一个新的线程
Thread thread = new Thread(() -> {
// 线程进入睡眠状态
try {
synchronized (obj) {
obj.wait();
System.out.println("当前线程名:" + Thread.currentThread().getName());
}
} catch (InterruptedException e) {
e.printStackTrace();
}
});
// 开启线程 thread
thread.start();
System.out.println("主线程执行完毕");
// 等待thread进入等待状态释放锁,否则会产生死锁
TimeUnit.SECONDS.sleep(6);
synchronized (obj) {
obj.notify();
}
}
}
注意:wait和notify只能在synchronized同步代码块中调用,否则会抛异常。
Interrupt(): 中断线程,调用此方法后,会将线程的中断标志设置为true。
线程的状态
一个完整的线程的生命周期应该包含以下几个方面:
1.新建(New):创建了一个线程,但是还没有调用start方法
2.就绪(Runnable):调用了start()方法,线程准备获取CPU运行
3.运行(Running):线程获取CPU成功,正在运行
4.等待(Waiting):等待状态,和TimeWaiting一样都是等待状态, 不同的是Waiting是没有时间限制的等,而TimeWaiting会进入一个有时间限制的等。例如调用wait()方法后就会进入一个无限制的等,等待调用notify唤醒,而调用sleep( time)就会进入一个有时间限制的等。等待结束后(被唤醒或sleep时间到期)后就会重新进入就绪队列,等待获取CPU继续向下执行。
5.阻塞(Blocked):多个线程再等待临界区资源时,进入阻塞状态。
6.销毁(Teminated): 线程执行完毕,进入销毁状态,这个状态是不可逆的,是最终状态,当进入这个状态时,就代表线程执行结束了。
以一张图来理解这几个状态:
简单介绍一个线程的生命周期:
当我们使用 new Thread()创建一个线程时,那么这个线程就处于创建状态;当我们调用start()方法后,此时线程就处于就绪状态(进入就绪状态后就不可能再进入创建状态了),但是调用start()方法后并不是说立马就会被CPU执行,而是会参与CPU的抢夺,当这个线程拿到CPU后,就会被执行。那么拿到CPU后就进入了运行状态。当调用了sleep或wait方法后,线程就进入了等待状态, 当等待状态被唤醒后,就会重新进入就绪队列等待获取CPU,当访问同步资源时或其他阻塞式操作时就会进入阻塞状态,阻塞状态结束重新进入就绪状态获取CPU。当线程运行完成后进入Teminate状态后,就代表线程执行结束了。
sleep操作不释放锁,wait操作释放锁。
三、Synchronized
synchronized 是java中的关键字,通过synchronized加锁保证多线程情况下临界区资源的安全访问。那么synchronized是如何实现的?
JMM模型
要了解synchronized的底层原理,首先需要了解java的内存模型。JMM内存模型是围绕线程的可见性、原子性、有序性建立的。java的内存模型分为堆内存和线程内存,也就是说java会对每一个线程都分配一块内存空间,线程内存主要存放堆栈信息和临时变量等。当创建一个对象时,如果用到了主内存中的变量,那么会将这个变量拷贝一份副本,在这个线程中对这个变量的所有操作,都是对这个副本操作的。以下面这个程序来证明这一点。
import java.util.concurrent.TimeUnit;
/**
-
证明JMM模型中,线程对共享资源的操作,操作的是副本。
-
@author 赵帅
-
@date 2021/1/4
*/public class JMMTest {
/**
- 线程是否继续循环
*/
private static volatile boolean running = true;
public static void main(String[] args) throws InterruptedException {
new Thread(() -> {
while (running) {
}
}, "thread-0").start();
TimeUnit.SECONDS.sleep(1);
running = false;
System.out.println("main线程结束");
}
}
在上面这个程序中,我们定义了一个变量running,标记是否继续循环,在main方法中开启了一个线程,如果running=false时就会结束循环,结束这个线程。然后我们在main线程中将这个running更改为false。运行后发现线程并不会终止。说明在main线程中更改running的值后,thread-0线程中的running的值仍为true。这也就证明了main线程和thread-0线程的running变量并不是同一个变量。
总结一下上面的内容:
变量的定义都在主内存中。
每一个线程都有自己的工作内存,保存该线程使用到的变量的副本。
线程对共享变量的所有操作都在自己的工作内存中,不能直接操作主内存。
不同的线程之间不能访问其他线程工作内存中的变量,变量值的传递需要通过主内存来进行。
我们如何让main线程中的变量值修改后,thread-0就知道了呢?
对running变量添加volatile关键字修饰:private static volatile boolean running = true;
此时线程就能正常结束了。
volatile
volatile关键字有两个作用:
保证线程可见性: 通过缓存一致性协议实现。上面在running变量上添加volatile就是使用的保证线程可见性这个特性。当在变量上添加volatile之后,那么如果这个变量的值发生更改,就会将这个内存副本中变量的值,同步到主内存中去,主内存中的值发生更改,然后主内存将更改后的变量同步到其他的线程。这样就实现的线程间变量的可见性。
禁止指令重排序:通过内存屏障load-store实现。
指令重排序:一个类文件在编译后会转换成CPU能够识别的指令,CPU的速度很快,为了提升效率,会执行多条指令,CPU会对这多条指令进行优化。例如:
int a = 1;int b = 2;
变量a和变量b在定义上没有依赖关系,那么CPU在执行时可能会先执行b=2,再执行a=1。这都是CPU为了提升效率做的优化。当然发生重排序的概率非常小。但是这种情况是存在的。
经典问题:DCL单例是否需要添加volatile?
什么是DCL单例?
Double check lock 双重检查锁。
/**
-
@author 赵帅
-
@date 2021/1/5
*/public class DCLDemo {
private static volatile DCLDemo INSTANCE;
private String name = “hello”;
private DCLDemo(){}
public static DCLDemo getInstance(){
if (INSTANCE == null) {
synchronized (DCLDemo.class) {
if (INSTANCE == null) {
INSTANCE = new DCLDemo();
}
}
}
return INSTANCE;
}
}
上面代码就是DCL单例模式。那么INSTANCE要不要加volatile?
首先我们来了解对象的创建过程:Object obj = new Object();
创建这个对象会经历:
1.为这个对象开辟一个内存区域
2.复制对象引用
3.利用对象引用调用构造方法
4.将引用赋值给obj。
那么如果发生指令重排序,即3、4两条指令的顺序变了。先将引用指向了对象,此时还没有执行第3条指令,即对象的构造方法还没有被执行,此时如果第二个线程调用了这个方法,那么在第一个判断,if(INSTANCE == null)判断结果为false,那么直接就返回了这个对象,这个对象是有问题的,就会出异常。
所以DCL单例需要添加volatile。
volatile是如何实现禁止指令重排序的?
volatile内存屏障针对不同的操作系统会有不同的实现。只要是在每一个指令的前后添加内存屏障,前一条指令执行完才能执行后一条指令。主要通过lfence,sfence,mfence实现。在jvm层级的实现为:
loadload、loadstore、storeload、storestore。
更多的关于JMM内存模型会在后面的jvm中详细描述。
synchronized
在学习synchroinzed前,我们首先需要了解什么是线程安全性?
当多个线程操作共享资源时,如果最终的结果与我们预想的一致,那么就是线程安全的,否则就是线程不安全的。
看下面代码:
/**
-
@author 赵帅
-
@date 2021/1/6
*/public class ThreadDemo {
private int num = 0;
public void fun() {
for (int i = 0; i < 1000; i++) {
num++;
}
}
public static void main(String[] args) throws InterruptedException {
ThreadDemo demo = new ThreadDemo();
for (int i = 0; i < 10; i++) {
new Thread(demo::fun).start();
}
TimeUnit.SECONDS.sleep(5);
System.out.println("demo.num = " + demo.num);
}
}
我们定义了一个num值为0;在fun方法中我们对这个num自增1000次;在main方法中我们启动了十个线程来调用这个方法,那么最终num的值应该是10*1000=10000。期望值是10000,但是执行方法后,无论执行几次最终的结果都不是期望值,因此这个类是线程不安全的。
总结造成线程安全问题的主要原因:
1.存在共享资源。
2.存在多个线程同时操作共享资源。
上面的问题如何解决?
为了解决这个问题,我们需要保证在一个线程操作共享数据时,其他的线程不能操作这个数据。也就是保证同一时刻有且只有一个线程可以操作共享数据,其他线程必须等待这个线程处理完后再进行,这中方式叫做互斥锁。synchroinzed关键字可以实现这个操作。synchronized可以保证在同一时刻只有一个线程执行某个方法或某个代码块。
synchronized的使用
使用synchronzed解决上面的问题:
import java.util.concurrent.TimeUnit;
/**
-
@author 赵帅
-
@date 2021/1/6
*/public class ThreadDemo {
private int num = 0;
private final Object lock = new Object();
public void fun() {
synchronized (lock) {
for (int i = 0; i < 1000; i++) {
num++;
}
}
}
public static void main(String[] args) throws InterruptedException {
ThreadDemo demo = new ThreadDemo();
for (int i = 0; i < 10; i++) {
new Thread(demo::fun).start();
}
TimeUnit.SECONDS.sleep(5);
System.out.println("demo.num = " + demo.num);
}
}
运行结果与期望值一致。synchronized在使用时必须锁定一个对象,可以像上面代码一样自己定义锁的对象,也可以使用如下方式:
/**
-
@author 赵帅
-
@date 2021/1/7
*/public class SynchronizedDemo {
/**
- 加锁锁定的对象
*/
private final Object lock = new Object();
private static final Object STATIC_LOCK = new Object();
/**
/**
- 方式2:锁定方法
- 在方法上加锁,这种方式与上面一样,都是锁定的当前对象
*/
public synchronized void fun2(){}
/**
-
方式3:静态方法内加锁
-
静态方法时无法使用this,只能锁定static修饰的对象,或者使用 类对象。
-
Synchronized.class 是Class对象
*/
public static void fun3() {
synchronized (SynchronizedDemo.class) {
// do something
}
synchronized (STATIC_LOCK) {
// do something
}
}
/**
- 方式4:锁定静态方法
- 锁定静态方法时,与上面方式一样,锁定的是当前类对象
*/
public static synchronized void fun4() {}
}
多个线程必须竞争同一把锁,也就是说锁对象必须相同,下面这种方式是错误的:
public void fun1() {
final Object lock = new Object();
synchronized (lock) {
// do something
}
}
每个线程进来后都会创建一个新的锁对象,线程之间不存在锁竞争,那么锁就失去了作用,因此必须保证锁定同一个对象,多个线程竞争同一把锁。
synchronized锁定的对象不能是基本类型和String类型
使用如下代码做解释:
package com.xiazhi.thread;
import java.util.concurrent.TimeUnit;
/**
-
@author 赵帅
-
@date 2021/1/7
*/public class SynchronizedDemo2 {
public Integer lock = 1;
public void fun1() {
synchronized (lock) {
System.out.println(Thread.currentThread().getName() + “得到锁”);
try {
// 模拟执行业务代码耗时1秒
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + “释放锁”);
}
}
public static void main(String[] args) {
SynchronizedDemo2 demo = new SynchronizedDemo2();
for (int i = 0; i < 10; i++) {
new Thread(demo::fun1).start();
// demo.lock++; //1
}
}
}
上面代码中定义了一个Integer类型的锁,并启动了十个线程,来调用fun1方法。我们期待的输出是这样的。
Thread-0得到锁Thread-0释放锁Thread-9得到锁Thread-9释放锁Thread-8得到锁Thread-8释放锁Thread-7得到锁Thread-7释放锁Thread-6得到锁Thread-6释放锁Thread-5得到锁Thread-5释放锁Thread-4得到锁Thread-4释放锁Thread-3得到锁Thread-3释放锁Thread-2得到锁Thread-2释放锁Thread-1得到锁Thread-1释放锁
线程之间因为竞争同一把锁有序执行,此时程序是可以正常运行的。但是一旦我们打开 demo.lock++这个注释,那么程序的结果就会变成这样:
Thread-0得到锁Thread-2得到锁Thread-1得到锁Thread-3得到锁Thread-4得到锁Thread-6得到锁Thread-7得到锁Thread-8得到锁Thread-9得到锁Thread-1释放锁Thread-4释放锁Thread-6释放锁Thread-0释放锁Thread-5得到锁Thread-2释放锁Thread-3释放锁Thread-7释放锁Thread-8释放锁Thread-9释放锁Thread-5释放锁
每一个线程都能拿到锁,这说明线程之间并不是在竞争同一把锁了。这是因为demo.lock++实际上执行的是`demo.lock = new Integer(demo.lock+1)。可以看到,创建了一个新的对象。我们打印一下锁对象的内存地址:
package com.xiazhi.thread;
import java.util.concurrent.TimeUnit;
/**
-
@author 赵帅
-
@date 2021/1/7
*/public class SynchronizedDemo2 {
public Integer lock = 1;
public void fun1() {
synchronized (lock) {
System.out.println(Thread.currentThread().getName() + “得到锁”);
// 打印当前锁对象的内存地址
System.out.println(“当前锁对象:” + System.identityHashCode(lock));
try {
// 模拟执行业务代码耗时1秒
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + “释放锁”);
}
}
public static void main(String[] args) {
SynchronizedDemo2 demo = new SynchronizedDemo2();
for (int i = 0; i < 10; i++) {
new Thread(demo::fun1).start();
demo.lock++;
}
}
}
查看运行结果:
Thread-1得到锁
Thread-2得到锁
当前锁对象:245887817
Thread-0得到锁
当前锁对象:1443225064
当前锁对象:245887817
Thread-3得到锁
当前锁对象:1443225064
Thread-4得到锁
当前锁对象:1468479040
Thread-5得到锁
当前锁对象:774263507
Thread-6得到锁
当前锁对象:1022687942
Thread-7得到锁
当前锁对象:166950465
Thread-8得到锁
当前锁对象:1694292106
Thread-9得到锁
当前锁对象:1694292106
Thread-2释放锁
Thread-0释放锁
Thread-4释放锁
Thread-3释放锁
Thread-1释放锁
Thread-6释放锁
Thread-5释放锁
Thread-8释放锁
Thread-7释放锁
Thread-9释放锁
可以很明显的看到锁的对象一直在变化,而我们加锁的目的就是为了保证多个线程竞争同一把锁,现在是在竞争多把锁。线程之间就不存在竞争关系,都可以得到锁。所以不能使用Integer,其他的基本类型包装类型也是跟这个一样。所以说锁对象不能是基本类型包装类型。
如果只是因为i++这个原因的话,或许我们会想如果用final修饰为不可变对象不就可以了么。例如下面这样:
public final Integer lock = 1;
这样的话,就保证了lock对象是不可变的。这样是不是就可以了?
仍然不行。因为再Integer类内部维护着一个缓存池,缓存-128~127之间的值。
/**
-
@author 赵帅
-
@date 2021/1/9
*/public class IntegerTest {
public static void main(String[] args) {
Integer var1 = 127;
Integer var2 = 127;
System.out.println(var1 == var2);// true
Integer var3 = 128;
Integer var4 = 128;
System.out.println(var3 == var4);// false
}
}
可以看到如果Integer的值在 -128~127之间的话,无论创建多少次,实际上使用的都会是一个对象。那么再使用中就会造成如下问题:
我们首先来看不使用Integer做锁的时候, 程序的运行结果:
package com.xiazhi.thread;
import java.util.concurrent.TimeUnit;
/**
-
@author 赵帅
-
@date 2021/1/9
*/public class SynchronizedDemo3 {
static class A{
private final Object lock = new Object();
public void fun1() {
synchronized (lock) {
System.out.println(Thread.currentThread().getName() + "获取锁");
// do something
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "释放锁");
}
}
}
static class B{
private final Object lock = new Object();
public void fun1() {
synchronized (lock) {
System.out.println(Thread.currentThread().getName() + "获取锁");
// do something
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "释放锁");
}
}
}
public static void main(String[] args) {
A a = new A();
B b = new B();
for (int i = 0; i < 5; i++) {
new Thread(a::fun1, “Class A” + i).start();
new Thread(b::fun1, “Class B” + i).start();
}
}
}
此时程序的运行结果是这样的:
Class A0获取锁Class B0获取锁Class A0释放锁Class A4获取锁Class B0释放锁Class B4获取锁Class A4释放锁Class B4释放锁Class A3获取锁Class B3获取锁Class A3释放锁Class B3释放锁Class A2获取锁Class B2获取锁Class A2释放锁Class B2释放锁Class A1获取锁Class B1获取锁Class A1释放锁Class B1释放锁
可以看到,ClassA的和ClassB之间是没有锁竞争的,类A的lock和类B的lock是两把锁,这样的话,这也类关联的线程其实是两个并行的线程。A和B之间互不影响。但是如果我们将类A和类B的锁对象修改:
package com.xiazhi.thread;
import java.util.concurrent.TimeUnit;
/**
-
@author 赵帅
-
@date 2021/1/9
*/public class SynchronizedDemo3 {
static class A{
private final Integer lock = 1;
public void fun1() {
synchronized (lock) {
System.out.println(Thread.currentThread().getName() + "获取锁");
// do something
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "释放锁");
}
}
}
static class B{
private final Integer lock = 1;
public void fun1() {
synchronized (lock) {
System.out.println(Thread.currentThread().getName() + "获取锁");
// do something
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "释放锁");
}
}
}
public static void main(String[] args) {
A a = new A();
B b = new B();
for (int i = 0; i < 5; i++) {
new Thread(a::fun1, “Class A” + i).start();
new Thread(b::fun1, “Class B” + i).start();
}
}
}
此时再次运行程序,会发现A和B之间变成串形了,因为A和B都是用了Integer做锁,而且值一样,就变成了一把锁了。
通过上面的分析,我们知道了为什么不允许使用基本包装类型来做锁对象。那么为什么也不允许String呢?
原因与Integer缓存池一样,String创建的对象会进入常量池缓存。
synchronized保证了可见性、原子性、有序性
上面我们在讲volatile的可见性时的代码,如果我们讲代码这样更改:
import java.util.concurrent.TimeUnit;
/**
-
证明JMM模型中,线程对共享资源的操作,操作的是副本。
-
@author 赵帅
-
@date 2021/1/4
*/public class JMMTest {
/**
- 线程是否继续循环
*/
private static boolean running = true; //0
public static void main(String[] args) throws InterruptedException {
new Thread(() -> {
while (running) {
System.out.println("hello"); //1
}
}, "thread-0").start();
TimeUnit.SECONDS.sleep(1);
running = false;
System.out.println("main线程结束");
}
}
我们在1处添加代码,发现0处即使没有添加volatile,代码也是能正常结束的。为什么?
查看 System.out.println的源码:
public void println(String x) {
synchronized (this) {
print(x);
newLine();
}
}
发现使用了synchronized。synchronized是保证了可见性、原子性和有序性。
synchroinzed是如何保证可见性的?
synchronized获得锁之后会执行以下内容:
1.清空工作内存中共享变量的值
2.从主内存重新拷贝需要使用的共享变量的值
3.执行代码
4.将共享变量的最新值刷新到主内存数据
5.释放锁
从上面步骤可以看出,synchroinzed保证了线程可见性。
synchronized是如何保证原子性的?
什么是原子性?
原子性是指操作是不可分的,要么全部一起执行,要么都不执行。
synchronized如何保证原子性
查看synchronized的字节码原语,synchronized是通过monitorenter和monitorexit两个命令来操作的。线程1在执行monitorenter指令的时候,会对Monitor进行加锁,加锁后其他线程无法获得锁,除非线程1主动释放锁。即使在执行过程中,由于时间片用完,线程1放弃cpu,但是它并没有解锁,由于synchronized是可重入的,下一个时间片还是只能被他自己获取到,还是会继续执行代码,直到所有代码执行完,这就保证了原子性。
synchronized是可重入锁,什么是可重入锁?
查看下面一段代码:
package com.xiazhi.thread;
/**
-
@author 赵帅
-
@date 2021/1/9
*/public class SynchronizedDemo4 {
public synchronized void fun1() {
fun2();
}
public synchronized void fun2() {
// do something
}
public static void main(String[] args) {
SynchronizedDemo4 demo = new SynchronizedDemo4();
demo.fun1();
}
}
方法fun1和fun2都被synchronized修饰了,也就是说这两个方法都需要获得锁才可以执行,但是在fun1中调用了fun2方法,程序进入fun1时说明已经获得到this的锁了,之前我们说了,当锁被占用时,其他线程只有等待当前线程释放锁才可以拿到锁,但是现在线程已经拿到锁了,那么再次调用fun2是否能够调用成功?如果可以调用成功就说明这是个可重入锁。也就是说可重入锁就是指一个线程是否可以重复多次获得锁。
synchronized保证有序性
有序性是指程序执行的顺序按照代码的先后顺序执行。在并发时,程序的执行可能会出现乱序。给人的直观感觉就是:写在前面的代码,会在后面执行。但是synchronized提供了有序性保证,这其实和as-if-serial语义有关。
as-if-serial语义是指不管怎么重排序(编译器和处理器为了提高并行度),单线程程序的执行结果都不能被改变。编译器和处理器无论如何优化,都必须遵守as-if-serial语义。只要编译器和处理器都遵守了这个语义,那么就可以认为单线程程序是按照顺序执行的,由于synchronized修饰的代码,同一时间只能被同一线程访问。那么可以认为是单线程执行的。所以可以保证其有序性。
synchronized锁升级过程
synchronized在jdk早期是重量级锁。
什么是重量级锁?
要解释重量级锁和轻量级锁的概念首先需要理解用户态和内核态的。
一个对象的内容
在java中,一个对象的内容主要分为以下几个部分:
类型 jvm 32位长度 jvm64位长度
markword 64位,8字节 64位,8字节
class类指针长度 32位 64位,开启类指针压缩后为32位,默认开启
属性长度 32位 64位,开启属性指针压缩后为32位,默认开启
补齐 - -
java内存地址按照8字节对齐,因此当对象的长度不足8的倍数是,会补齐到8的倍数。例如:
Object obj = new Object();
obj对象的大小 = markword(8字节)+Object类指针长度(8字节)+属性指针长度(object无属性,0字节)==16字节,16为8的倍数,所以不需要补齐。
当开启类指针压缩时:
obj对象的大小 = markword(8字节)+Object类指针长度(4字节,开启指针压缩)+属性指针长度(object无属性,0字节)==12字节。12不是8的倍数,所以补齐4个字节,最后类大小仍为16字节。
markword
我们之前说synchronized必须锁定一个对象,那么多个线程如何判断这个对象是否已经被占用了呢?当锁定这个对象时,会对这个对象添加一个标记,标记这个对象是否加锁。这个标记就放在markword中。
锁升级
因为早期的synchronized太重,每次都要调用内核态进行操作,效率太低了,因此为了提升效率,在后来的版本中对synchronized进行了优化,添加了锁升级的过程,锁升级过程中锁的状态就记录在锁对象的markword中。整个锁升级过程如下:
无锁态:对象刚创建,还没有线程进来加锁。
偏向锁:第一个线程进来后,升级为偏向锁。
轻量级锁(自旋锁):当多个线程竞争这把锁时,升级为自旋锁。
重量级锁:当线程自旋超过10次或等待线程数超过10,升级为重量级锁。
锁升级过程与markword中内容对应关系如下:
锁状态 25bit 4bit 1b