同步队列:处于running状态的线程,有资格竞争锁的线程组成的等待获取同步资源的队列。
等待队列:调用wait方法或await方法后线程进入等待队列,等待被唤醒后进入同步队列。
如何创建和结束多线程?
进程线程概念,线程创建的几种方式,线程的几种状态,如何优雅结束线程。
如何保证线程正确执行?
并发编程三大特性
保证原子性的锁
如何控制线程执行顺序?
掌握一些线程控制的关键类,关键方法
高并发编程常用的一些容器
程序可以只使用多线程不使用锁和线程执行控制。
public class ManyThread {
public static void main(String[] args) {
int m =10000;
Thread[] threads = new Thread[m];
for (int i = 0; i < m; i++) {
threads[i] = new Thread(()-> System.out.println("不需要使用锁和线程控制"));
threads[i].start();
}
}
}
程序只使用多线程和线程控制(代码演示了使用线程和不使用线程的区别),(m2和m4做对比,m3和m5做对比)
package com.mashibing.juc.c_000_threadbasic;
import java.util.concurrent.CountDownLatch;
/**
* @title: T_01_exercise
* @Author Tan
* @Date: 2022/12/31 15:39
* @Version 1.0
*/
//计算1到5 0000 0000的和
public class T_01_exercise {
long total = 800000000;
long l =0;
long l1 = 0;
long l2 = 0;
// long result1;
//long result2;
private void m1(){
for (long i = 1; i <=total; i++) {
l += i ;
//i++;
}
System.out.println(l);
}
private void m2() throws InterruptedException {
Thread t1 =new Thread(){
public void run(){
for (long i = 1; i
不需要进行线程执行顺序控制,使用多线程和锁,竞争使用互斥代码块示例,有公共变量(创建的线程的run方法要操作这个变量,这个变量就是公共变量,要保证并发编程三性)。
public class Shoupiao{
public static void main(String[] args) {
MaiPiao maiPiao = new MaiPiao();
new Thread(maiPiao).start();
new Thread(maiPiao).start();
new Thread(maiPiao).start();
}
static class MaiPiao implements Runnable{
int tickets = 10;
Object o = new Object();
@Override
public void run() {
synchronized (o){
while (tickets>0){
System.out.println(Thread.currentThread().getName()+"余票"+tickets+"张,可以卖票");
try {
Thread.sleep(1);
tickets--;
//o.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
}
使用AtomicInteger卖票的失败尝试
public class 多线程和锁 {
public static void main(String[] args) {
int t = 3;
卖票 m = new 卖票();
Thread[] threads = new Thread[t];
for (int i = 0; i < t; i++) {
threads[i] = new Thread(m);
}
for (int i = 0; i < t; i++) {
threads[i].start();
}
}
static class 卖票 implements Runnable {
volatile AtomicInteger ticket1 =new AtomicInteger(10);
int ticket = ticket1.get();
@Override
public void run() {
// int ticket = ticket1.get();
while (ticket>0){
ticket = ticket1.get();
System.out.println(Thread.currentThread().getName()+"余票数:"+ ticket);
ticket = ticket1.decrementAndGet();
}
}
}
}
进程:资源分配的基本单位
线程:任务调度的基本单位
继承Thread类,重写run方法。
实现Runable接口,重写run方法,作为任务参数传递给new Thread
实现Callable接口,重写call方法,得到带返回值的线程方法。
匿名创建线程,本质同2、3,匿名类,实现接口或继承父类。
使用线程池创建线程
package com.mashibing.juc.c_000_threadbasic;
import java.util.concurrent.*;
/**
* @title: T_02_exercise_createThread
* @Author Tan
* @Date: 2023/1/1 18:57
* @Version 1.0
*/
public class T_02_exercise_createThread {
static class Mythread1 extends Thread{
public void run(){
System.out.println("thread1");
}
}
static class Mythread2 implements Runnable{
@Override
public void run() {
System.out.println("thread2");
}
}
static class Mythread3 implements Callable{
Mythread3(){
String name = Thread.currentThread().getName();
System.out.println(name);
}
@Override
public String call() throws Exception {
return "thread3";
}
}
public static void main(String[] args) throws ExecutionException, InterruptedException {
new Mythread1().start();
new Thread(new Mythread2()).start();
FutureTask task = new FutureTask<>(new Mythread3());
new Thread(task).start();
String s = task.get();
System.out.println(s);
new Thread(() -> System.out.println("thread4")).start(); //lambda表达式的本质是匿名内部类的缩写,new Runable
new Thread(new Runnable() {
@Override
public void run() {
System.out.println("thread4");
}
}).start();
ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(2,10,60, TimeUnit.SECONDS,new PriorityBlockingQueue(),new 自定义线程工厂("zhao"));
//,new PriorityBlockingQueue()
threadPoolExecutor.submit(new Mythread2());
Future future = threadPoolExecutor.submit(new Mythread3());
String s1 = future.get();
System.out.println(s1);
ExecutorService executorService = Executors.newCachedThreadPool();
executorService.submit(()-> System.out.println("newCachedThreadPool"));
ExecutorService executorService1 = Executors.newFixedThreadPool(6);
executorService1.submit(()-> System.out.println("newFixedThreadPool"));
ExecutorService executorService2 = Executors.newSingleThreadExecutor();
executorService2.submit(()-> System.out.println("newSingleThreadExecutor"));
ScheduledExecutorService scheduledExecutorService = Executors.newScheduledThreadPool(6);
scheduledExecutorService.submit(()-> System.out.println("newScheduledThreadPool"));
scheduledExecutorService.schedule(()-> System.out.println("newScheduledThreadPool"),3,TimeUnit.SECONDS);
scheduledExecutorService.scheduleAtFixedRate(()-> System.out.println("newScheduledThreadPool"),6,1,TimeUnit.SECONDS);
}
}
线程池作用在于减少了创建和销毁线程的次数,可被重复利用。
ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(2,10,60, TimeUnit.SECONDS,new PriorityBlockingQueue(),new 自定义线程工厂("zhao"));
参数一:核心线程数
参数二:最大线程数,核心线程用完且任务队列占满才会创建新线程。
参数三:线程空闲时最大的等待时间,超时则销毁线程
参数四:等待时间的单位(TimeUnit.SECONDS)
参数五:线程池使用的任务队列(WorkingQueue)
有界队列:
ArrayBlockingQueue:数组容量不可变,FIFO,满时增,空时取会阻塞
SynchronousQueue:不存储元素,只保证每一个新增插入操作(put)伴随另一个线程调用移除操作(take),没有空闲线程时会创建线程,造成OOM。
无界队列
LinkedBlockingQueue:可设定容量也可以不设定,实际最大值为Integer的最大值。
PriorityBlockingQueue:按照优先级排序的无界队列,元素必须实现Comparable接口,相同优先级不保证顺序
参数六:使用的线程工厂,可不填,默认defaultThreadFactory,可自定义
public class 自定义线程工厂 implements ThreadFactory {
//线程组命名标识
private final String namePrefix;
private final AtomicInteger nextId = new AtomicInteger(1);
创建的线程要调用的run方法要操作这个变量,这个变量就是公共变量,要保证并发编程三性。
public 自定义线程工厂(String namePrefix) {
this.namePrefix = "来自线程组"+namePrefix+"-Worker-";
}
@Override
public Thread newThread(Runnable r) {
String name = namePrefix+nextId.getAndIncrement();
Thread t = new Thread(r,name);//前面的所有都是为了重定义这个线程名字。
return t;
}
}
参数七:异常处理策略
ThreadPoolExecutor.AbortPolicy(),抛出RejectExecutionException异常,默认策略。
ThreadPoolExecutor.CallerRunsPolicy(),由提交任务的线程处理异常
ThreadPoolExecutor.DiscardPolicy(),丢弃当前任务
ThreadPoolExecutor.DiscardOldestpolicy(),丢弃最先提交但还没有执行的任务。
这里的定长,无界说的时线程的数量,不是任务的数量
都可以使用submit方法提交执行,execute()方法只接受runnable任务,不常用。
都可以接收Runnable或者Callable类型的任务参数
定时周期线程池的特殊方法:schedule()、scheduleAtFixedRate()、scheduleWithFixedDelay()
创建线程数定长线程池(LinkedBlockingQueue),Executors.newFixedThreadPool();因为采用了无界任务队列,队列永远不可能满,所有实际只有核心线程数工作。可能因为任务队列过多造成OOM.
创建线程数无界线程池(synchronousQueue),Executors.newCachedThreadPool();可能因为创建线程过多造成OOM。
创建单一任务线程池:Executors.newSingleThreadPool();用唯一的工作线程来执行任务,保证所有任务按照指定顺序执行。线程池作用在于减少了创建和销毁线程的次数,可被重复利用。
创建定时或周期执行线程池:Executors.newScheduledThreadPool();
只有深刻了解三大特性,才不会编写出漏洞百出的并发程序。意味着使用多线程的程序中,必须提前考虑好三大特性使用的注意事项;
可见性:一个线程对变量进行了修改,另外一个线程能够立刻读取到此变量的最新值。
原子性:所有操作要么全部成功,要么全部失败。
有序性:代码在运行期间按照编写的顺序执行。
总结:
共享变量使用volatile实现可见性
共享变量使用@sun.misc.Contended注解减少因缓存一致性产生的性能影响。
假如类中有一个线程共用变量,CPU通过指令将该变量从内存经三级缓存放入L1之后,线程1会拷贝一份到自己的指令空间,线程2也会拷贝一份到自己的指令空间,假如线程1修改了该变量的值,如何保证该变化对线程2可见,使线程2使用这个改变了之后的值。
volatile(同时需要底层缓存一致性的支持)
1.并发编程时,在程序中分析清楚哪个变量需要保证在线程间的可见性(共享且变量会被修改),使用volatile关键字修饰。
2.如果想共享的变量是位于对象内部或者数组内部,则需要在对象或数组内部使用volatile修饰,不能直接使用volatile修饰对象或数组,因为即使使用volatile修饰,对象内部属性值仍是对其他线程不可见的。
3.有些语句也会触发从内存中重新加载变量值,造成类似可见性的效果,但是比较影响性能,不建议使用,例如使用system.out输出一句话,本质是加锁的过程,会降低性能
4.有些变量是共享的,有些变量值要绝对保证线程独有,如果想要确保线程独有,请使用threadlocal
此图示意,两颗CPU,每颗CPU2核,三级缓存,L1,L2位于核内部,L3CPU共享,对于单核CPU,三级缓存都位于CPU内部。
加载数据进缓存以缓存行为基本单位。
Intel的缓存一致性协议MESI协议,表示缓存行数据的四种状态
Modified:数据被修改了,缓存行是脏的,与主存值不同。
Exclusive:独占的,复制的只属于当前核缓存的数据,是干净的,等同于主存数据。
Shared:缓存行也位于其他核缓存中且都是干净的。
Invalid:无效的。
缓存一致性协议保证了不同核缓存间数据的一致性,在一定程度上实现了可见性。
volatile和MESI协议之间的关系
刚接触MESI协议时容易产生误解,以为是volatile关键字触发MESI机制来保证变量的可见性,实际没有这一层因果关系。下面简单总结下我对两者关系的一些理解:
1.MESI保障了多核场景的缓存一致性,是一套固有机制,无论是否声明volatile,只要变量在CPU缓存中就能通过这个协议保障可见性。但反过来,如果没有MESI协议,即使声明了volatile也无法保证变量的可见性;
2.既然有了MESI协议,为什么还要volatile?这是因为MESI本身存在一些性能问题(例如修改一个存在于其他CPU缓存的数据时,需要等待其他CPU的invalidate ack),因此额外引入了store buffer和invalidate queue等存储结构来优化性能。当数据仅存在于L1/L2 cache中时,MESI足可以保证volatile的内存语义,但如果数据存在于store buffer或者invalidate queue中时,仅靠MESI无法保证数据的一致性,这时需要引入内存屏障。
使用缓存一致性协议需要进行频繁的缓存行状态更改,可使用左右两侧加空字节的方式避免因不需要的数据被别的核缓存修改而引起的反复状态更改,能在一定程度上提升性能。
Java8之前可以给变量前后分别填充8个long类型的字段解决。(为什么总共是8个?因为一个不同CPU架构的缓存行是不一样的,最常见的缓存行大小是64字节,而8个long类型刚好是64字节)
Java 8 中已经提供了官方的解决方案,Java 8 中新增了一个注解: @sun.misc.Contended。加上这个注解的类会自动补齐缓存行,需要注意的是此注解默认是无效的,需要在 jvm 启动时设置 -XX:-RestrictContended 才会生效。
总结,有序性的存在单线程任务中能提高效率,但在多线程任务中可能会导致程序出错,需要使用方法来避免在多线程任务中的指令重排。
禁止方法:对需要共享的变量使用volatile关键字修饰,原理,对于volatile关键字修饰的变量,会使多线程在对变量操作时使用内存屏障,禁止指令重排。
为了提高单线程任务的执行效率,JVM以及CPU有可能对任务指令进行重排序。
以下程序证明了指令重排的存在
例一:
import java.util.concurrent.CountDownLatch;
public class T01_Disorder {
private static int x = 0, y = 0;
private static int a = 0, b = 0;
public static void main(String[] args) throws InterruptedException {
for (long i = 0; i < Long.MAX_VALUE; i++) {
x = 0;
y = 0;
a = 0;
b = 0;
CountDownLatch latch = new CountDownLatch(2);
Thread one = new Thread(new Runnable() {
public void run() {
a = 1;
x = b;
latch.countDown();
}
});
Thread other = new Thread(new Runnable() {
public void run() {
b = 1;
y = a;
latch.countDown();
}
});
one.start();
other.start();
latch.await();
String result = "第" + i + "次 (" + x + "," + y + ")";
if (x == 0 && y == 0) {
System.err.println(result);
break;
}
}
}
}
1,2,3分别表示了线程1,2顺序徐执行可能出现的几种情况,紫色部分为线程内部指令重排才会出现,x=0,y=0的结果。
例二:this溢出现象证明指令重排序
package com.mashibing.juc.c_001_03_Ordering;
public class T03_ThisEscape {
private int num = 8;
public T03_ThisEscape() {
new Thread(() -> System.out.println(this.num)
).start();
}
public static void main(String[] args) throws Exception {
new T03_ThisEscape();
System.in.read();
}
}
this溢出的意思是,在类的构造方法里new了新线程并启动,导致在构造方法中使用的类变量因指令重排未初始化完成就被启动的线程使用,这里如果m为初始化赋值为8,则会打印输出0。(4,7颠倒)
对于单线程任务来说,可以保证结果的最终一致性,提高效率。
对于多线程任务来说,会产生不希望看到的效果,如上。
两句指令没有关系。
禁止指令重排,在cpu层面有读屏障Lfence,写屏障sfence,以及读写屏障mfence。java语言为了屏蔽底层CPU架构的不一致性,也实现了自己的内存屏障方式,共分为四种,使用volatile 修饰的数据就会在程序执行时自动添加内存屏障,禁止指令重排。
避免直接在类的构造函数里new线程并启动,可以new线程,但不要启动。
要使用的公共类变量使用volatile关键字修饰,上例指m
深入源码发现volatile关键字的底层是一个c语言写的lock方法,该方法既保证了可见性,又保证了有序性。
DCL单例:懒汉模式下的程序优化。
记住几个问题:
DCL单例模式是用来在创建对象单例的时候用的
DCL单例:懒汉模式下的程序优化。只有懒汉模式才需要规避多线程情况下获得非单例的情况。
为什么要双重检查double check null?减小锁粒度同时避免线程一上来就抢锁。
为什么要使用volatile关键字,防止第一个线程因指令重排判断为null但没来得及赋值new Singleton()就丢失时间片导致第二个线程创建另一个单例,第一个线程获得时间片后又创建另一个单例。
借用一篇写的特别好的文章
所谓的DCL 就是 Double Check Lock,即双重锁定检查,在了解DCL在单例模式中如何应用之前,我们先了解一下单例模式。单例模式通常分为“饿汉”和“懒汉”,先从简单入手
饿汉
所谓的“饿汉”是因为程序刚启动时就创建了实例,通俗点说就是刚上菜,大家还没有开始吃的时候就先自己吃一口。
private volatile static final Singleton singleton = null;
public class Singleton {
private static final Singleton singleton = new Singleton();
private Singleton(){}
public static Singleton getInstance(){
return singleton;
}
}
第3行 通过一个私有构造方法限制了创建此类对象的途径(反射忽略)。这种方法很安全,但从某种程度上有点浪费资源,比方说从一开始就创建了Singleton实例,但很少去用它,这就造成了方法区资源的浪费,因此出现了另外一种单例模式,即懒汉单例模式
懒汉
之所以叫“懒汉”是因为只有真正叫它的时候,才会出现,不叫它它就不理,跟它没关系。也就是说真正用到它的时候才去创建实例,并不是一开始就创建实例。如下代码所示:
public class Singleton {
private static Singleton singleton = null;
private Singleton(){}
public static Singleton getInstance(){
if(null == singleton){
singleton = new Singleton();
}
return singleton;
}
}
看似很简单的一段代码,但存在一个问题,就是线程不安全的问题。例如,现在有1000个线程,都需要这一个Singleton的实例,验证一下是否拿到同一个实例,代码如下所示:
public class Singleton {
private static Singleton singleton = null;
private Singleton(){}
public static Singleton getInstance(){
if(null == singleton){
try {
Thread.sleep(1);//象征性的睡了1ms
} catch (InterruptedException e) {
e.printStackTrace();
}
singleton = new Singleton();
}
return singleton;
}
public static void main(String[] args) {
for (int i=0;i<1000;i++){
new Thread(()-> System.out.println(Singleton.getInstance().hashCode())).start();
}
}
}
为什么会这样?第一个线程过来了,执行到第7行,睡了1ms,正在睡的同时第二个线程来了,第二个线程执行到第5行时,结果肯定为空,因此接下来将会有两个线程各自创建一个对象,这必然会导致Singleton.getInstance().hashCode()结果不一致。可以通过给整个方法加上一把锁改进如下:
改进1
public class Singleton {
private static Singleton singleton = null;
private Singleton(){}
public static synchronized Singleton getInstance(){
if(null == singleton){
try {
Thread.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
singleton = new Singleton();
}
return singleton;
}
public static void main(String[] args) {
for (int i=0;i<1000;i++){
new Thread(()-> System.out.println(Singleton.getInstance().hashCode())).start();
}
}
}
通过给getInstance()方法加上synchronized来解决线程一致性问题,结果分析虽然显示所有实例的hashcode都一致,但是synchronized的粒度太大了,即锁的临界区太大了,有点影响效率,例如如果第4行和第5行之间有业务处理逻辑,不会涉及共享变量,那么每次对这部分业务逻辑加锁必然会导致效率低下。为了解决粗粒度的问题,可以对代码进一步改进:
改进2
public class Singleton {
private static Singleton singleton = null;
private Singleton(){}
public static Singleton getInstance(){
/*
一堆业务处理代码
*/
if(null == singleton){
synchronized(Singleton.class){//锁粒度变小
try {
Thread.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
singleton = new Singleton();
}
}
return singleton;
}
public static void main(String[] args) {
for (int i=0;i<1000;i++){
new Thread(()-> System.out.println(Singleton.getInstance().hashCode())).start();
}
}
}
部分运行结果 :
391918859
391918859
391918859
1945023194
通过分析运行结果发现,虽然锁的粒度变小了,但线程不安全了。为什么会这样呢?因为有种情况,线程1执行完if判断后还没有拿到锁的时候时间片用完了,此时线程2来了,执行if判断时发现对象还是空的,继续往下执行,很顺利的拿到锁了,因此线程2创建了一个对象,当线程2创建完之后释放掉锁,这时线程1激活了,顺利的拿到锁,又创建了一个对象。所以代码还需要再一步的改进。
改进3
public class Singleton {
private static Singleton singleton = null;
private Singleton(){}
public static Singleton getInstance(){
/*
一堆业务处理代码
*/
if(null == singleton){
synchronized(Singleton.class){//锁粒度变小
if(null == singleton){//DCL
try {
Thread.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
singleton = new Singleton();
}
}
}
return singleton;
}
public static void main(String[] args) {
for (int i=0;i<1000;i++){
new Thread(()-> System.out.println(Singleton.getInstance().hashCode())).start();
}
}
}
通过在第10行又加了一层if判断,也就是所谓的Double Check Lock。也就是说即便拿到锁了,也得去作一步判断,如果这时判断对像不为空,那么就不用再创建对象,直接返回就可以了,很好的解决了“改进2”中的问题。但这里第8行是不是可以去了,我个人觉得都行,保留第8行的话,是为了提升效率,因为如果去了,每个线程过来就直接抢锁,抢锁本身就会影响效率,而if判断就几ns,且大部分线程是不需要抢锁的,所以最好保留。
到这DCL 单例的原理就介绍完了,但是还是有一个问题。就是需要考虑指令重排序的问题,因此得加入volatile来禁止指令重排序。继续分析代码,为了分析方便这里将Singleton代码简化:
public class Singleton {
int a = 5;//考虑指令重排序的问题
}
singleton = new Singleton()的字节码如下:
0: new #2 // class com/reasearch/Singleton
3: dup
4: invokespecial #3 // Method com/reasearch/Singleton."":()V
7: astore_1
先不管dup指令。这里补充一个知识点,创建对象的时候,先分配空间,类里面的变量先有一个默认值,等调用了构造方法后才给变量赋值。例如int a = 5 刚开始的时候 a = 0。字节码指令执行过程如下,
new 分配空间,a=0
invokespecial 构造方法 a=5
astore_1将对象赋给singleton
这是理想的状态,2和3语义和逻辑上没有什么关联,因此jvm可以允许这些指令乱序执行,即先执行3再执行2 。回到改进3,假如线程1再执行第16行代码时,指令的执行顺序是1,3,2,当执行完3时,时间片用完了,此时a=0,也就是说初始化到一半时就挂起了。这时线程2 来了,第8行判断,singleton肯定不为空,因此直接返回一个Singleton的对象,但其实这个对象是一个问题对象,是一个半初始化的对象,即a=0 。这就是指令重排序造成的,因此为了防止这种现象的发生加上关键字volatile就可以了。因而,最终DCL之单例模式的代码完整版如下:
完整版
public class Singleton {
private volatile static Singleton singleton = null;//加上volatile
private Singleton(){}
public static Singleton getInstance(){
/*
一堆业务处理代码
*/
if(null == singleton){
synchronized(Singleton.class){//锁粒度变小
if(null == singleton){//DCL
try {
Thread.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
singleton = new Singleton();
}
}
}
return singleton;
}
}
一条高级语句或执行一个功能的代码片段在低层被分成多个CPU指令,在指令未完全执行成功之前发生了线程切换,中间态暴露造成原子性问题。
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class T00_00_IPlusPlus {
private static volatile long n = 0L;
public static void main(String[] args) throws Exception {
//Lock lock = new ReentrantLock();
Thread[] threads = new Thread[100];
CountDownLatch latch = new CountDownLatch(threads.length);
for (int i = 0; i < threads.length; i++) {
threads[i] = new Thread(() -> {
for (int j = 0; j < 10000; j++) {
// synchronized (T00_00_IPlusPlus.class) {
//lock.lock();
n++;
//lock.unlock();
// }
}
latch.countDown();
});
}
for (Thread t : threads) {
t.start();
}
latch.await();
System.out.println(n);
}
}
在未上锁但使用了volatile关键字的情况下计算结果远小于100万(逻辑是想让等于100万的),说明volatile关键字解决不了这种问题,这种问题就是原子性问题。
n++一共有三个操作,读取n, n+1以及回写n赋值。那么如果线程1已经执行到n+1=2, 并未进行第三步赋值。而此时线程2已经完成了n++的三个步骤并写回主存,线程1的n虽然是失效了,但是线程1再去读也对结果没影响了,因为也是2,而且线程1没有运算步骤了,直接再把运算结果2赋值给n(n=2)写回主存就可以。
核心思想是保证原子性,上锁,上各种锁。
乐观锁,CAS(除了Synchronized锁外的各种锁,例:使用Atomic类)底层是底层CPU指令支持的cas操作(底层lock+cmpchx指令)。LongAdder(分段锁,底层是CAS锁,当使用的线程比较多的时候比较有优势)。具体使用哪一种建议在项目中实验一下)
下面三种方式保证m++的原子性。
atomicLong.incrementAndGet() //AtomicLong类
synchronized(o){
count++; //synchronzied方式
}
longAdder.increment() //LongAdder类
try{
lock.lock();
}finally{
lock.unLock();
}
悲观锁 synchronized ,synchronized 还能保证可见性。
将需要原子性执行的操作上锁,即同一时刻只能有一个线程执行。我们将这一段需要互斥执行的代码称之为临界区。
CAS存在什么问题,重要吗? 如何解决?
1、ABA问题,基本类型影响不大,引用类型里面值可能被修改,对于ABA问题,可以通过加版本好解决,相同的A不同的版本号。
2、原子性问题,假如比对成功后丢失时间片被修改,所以必须保证CAS操作的原子性,因此实现 CAS的底层操作指令是lock+cmpchx指令,前面加了lock。
3,自旋等待,消耗CPU资源,仅适用于临界区执行较快且等待线程数较少的情况。
几种比synchronized更灵活的方法
Lock lock = new ReentrantLock();
locked = lock.tryLock(5, TimeUnit.SECONDS); //true or false
lock.lockInterruptibly();
公平锁和非公平锁
Condition(生产者和消费者的例子中有用到)
最方便的是使用阻塞队列blockingQueue,如LinkedBlockingQueue的put和take方法,这里未作演示。
package com.mashibing.zhao.TaoBaoInterView;
import java.util.LinkedList;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
/**
* @title: TaoBaoTwo_ReenTrentLock
* @Author Tan
* @Date: 2023/1/28 15:49
* @Version 1.0
*/
public class TaoBaoTwo_ReenTrentLock {
static class BlockQueue {
final int MAX = 10;
LinkedList list = new LinkedList();
Lock lock = new ReentrantLock();
Condition producer = lock.newCondition();
private Condition consumer = lock.newCondition();
private void put() {
try {
lock.lock();
while (list.size()==MAX){
try {
producer.await();//=============================================================================
} catch (InterruptedException e) {
e.printStackTrace();
}
}
list.add(new Object());
// Thread.sleep(300);
System.out.println(Thread.currentThread().getName()+"生产一个包子,现在包子数为 "+list.size());
consumer.signalAll();//=================================================================================
} finally {
lock.unlock();
}
}
private void get(){
try {
lock.lock();
while (list.size()==0){
try {
consumer.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
// Thread.sleep(300);
list.removeFirst();
System.out.println(Thread.currentThread().getName()+"消费一个包子,现在包子数为 "+list.size());
producer.signalAll();
} finally {
lock.unlock();
}
}
}
public static void main(String[] args) {
BlockQueue blockQueue = new BlockQueue();
new Thread(() -> {
while (true){
blockQueue.put();
try {
Thread.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}).start();
new Thread(() -> {
while (true){
blockQueue.put();
try {
Thread.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}).start();
new Thread(() -> {
while (true){
blockQueue.put();
try {
Thread.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}).start();
new Thread(() -> {
while (true){
blockQueue.put();
try {
Thread.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}).start();
new Thread(() -> {
while (true){
blockQueue.put();
try {
Thread.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}).start();
new Thread(() -> {
while (true){
blockQueue.put();
try {
Thread.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}).start();
new Thread(() -> {
while (true){
blockQueue.put();
try {
Thread.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}).start();
new Thread(() -> {
while (true){
blockQueue.put();
try {
Thread.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}).start();
new Thread(() -> {
while (true){
blockQueue.put();
try {
Thread.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}).start();
new Thread(() -> {
while (true){
blockQueue.put();
try {
Thread.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}).start();
new Thread(() -> {
while (true){
blockQueue.get();
try {
Thread.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}).start();
new Thread(() -> {
while (true){
blockQueue.put();
try {
Thread.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}).start();
new Thread(() -> {
while (true){
blockQueue.get();
try {
Thread.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}).start();
new Thread(() -> {
while (true){
blockQueue.get();
try {
Thread.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}).start();
new Thread(() -> {
while (true){
blockQueue.get();
try {
Thread.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}).start();
new Thread(() -> {
while (true){
blockQueue.get();
try {
Thread.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}).start();
new Thread(() -> {
while (true){
blockQueue.get();
try {
Thread.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}).start();
new Thread(() -> {
while (true){
blockQueue.get();
try {
Thread.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}).start();
new Thread(() -> {
while (true){
blockQueue.get();
try {
Thread.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}).start();
new Thread(() -> {
while (true){
blockQueue.get();
try {
Thread.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}).start();
new Thread(() -> {
while (true){
blockQueue.get();
try {
Thread.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}).start();
new Thread(() -> {
while (true){
blockQueue.get();
try {
Thread.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}).start();
new Thread(() -> {
while (true){
blockQueue.get();
try {
Thread.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}).start();
}
}
public static void main(String[] args) {
Lock lock = new ReentrantLock();
Thread t1 = new Thread(() -> {
try {
lock.lock();
System.out.println("t1 start");
TimeUnit.SECONDS.sleep(Integer.MAX_VALUE);
System.out.println("t1 end");
} catch (InterruptedException e) {
System.out.println("interrupted!");
} finally {
lock.unlock();
}
});
t1.start();
Thread t2 = new Thread(() -> {
try {
//lock.lock();
lock.lockInterruptibly(); //============================================
System.out.println("t2 start");
TimeUnit.SECONDS.sleep(5);
System.out.println("t2 end");
} catch (InterruptedException e) {
System.out.println("interrupted!");
} finally {
lock.unlock();
}
});
t2.start();
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
t2.interrupt();
}
}
import com.mashibing.util.SleepHelper;
import java.util.Random;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
public class T10_TestReadWriteLock {
static Lock lock = new ReentrantLock();
private static int value;
static ReadWriteLock readWriteLock = new ReentrantReadWriteLock();
static Lock readLock = readWriteLock.readLock();
static Lock writeLock = readWriteLock.writeLock();
public static void read(Lock lock) {
lock.lock();
try {
SleepHelper.sleepSeconds(1);
System.out.println("read over!");
//模拟读取操作
} finally {
lock.unlock();
}
}
public static void write(Lock lock, int v) {
lock.lock();
try {
SleepHelper.sleepSeconds(1);
value = v;
System.out.println("write over!");
//模拟写操作
} finally {
lock.unlock();
}
}
public static void main(String[] args) {
Runnable readR = () -> read(lock);
//Runnable readR = ()-> read(readLock);
Runnable writeR = () -> write(lock, new Random().nextInt());
//Runnable writeR = ()->write(writeLock, new Random().nextInt());
for (int i = 0; i < 18; i++) new Thread(readR).start();
for (int i = 0; i < 2; i++) new Thread(writeR).start();
}
}
读写锁读的时候不能写,写的时候不能读。
读写互斥,写写互斥,读读不互斥。
读锁的存在保证了读锁不释放的情况下写锁不能进行修改
持有锁的线程即便时间片用完了,也会继续执行,临界区执行完成才会将时间片设置为可可抢占状态。
假如没有读锁的话,有一个事务A先读取了数据是10,然后没有加锁,那么事务B修改完数据之后变成了20,这个时候事务A还没有提交事务,那么事务A再次读取数据的时候就变成了20,这样在同一个事务读取两次的数据不就不一致了吗?
java对象的内存布局,参考文章:https://blog.csdn.net/uuqaz/article/details/123340729
(java object layout),可在maven中引入坐标:
org.openjdk.jol
jol-core
0.14
在代码中使用jol提供的方法查看jvm信息:
public class User {
}
public static void main(String[] args) {
User user=new User();
//查看对象的内存布局
System.out.println(ClassLayout.parseInstance(user).toPrintable());
}
执行代码,查看打印信息:
OFFSET:偏移地址,单位为字节
SIZE:占用内存大小,单位为字节
TYPE:Class中定义的类型
DESCRIPTION:类型描述,Obejct header 表示对象头,alignment表示对齐填充
VALUE:对应内存中存储的值
组成:对象头(两部分组成,8字节的标记字markword加默认开启压缩后4字节的类型指针)、示例数据(成员变量,包括父类的)+对齐填充字节(java对象大小被要求是8字节的整数倍)。
对象头markword不同锁阶段中包含了哪些信息?
在mark word中,锁(lock)标志位占用2个bit,结合1个bit偏向锁(biased_lock)标志位,这样通过倒数的3位,就能用来标识当前对象持有的锁的状态,并判断出其余位存储的是什么信息。
偏向锁不会自动撤销,只有当其他线程来抢时会被动撤销升级为自旋锁。
升级成重量级锁时mark word中的指针指向的是monitor对象(也被称为管程或监视器锁)的起始地址。
静态变量并不在对象的内存布局中,它的大小是不计算在对象中的,因为静态变量属于类而不是属于某一个对象的,猜测方法信息也是。
Class Pointer 类型指针,dk6之后的版本中,指针压缩是被默认开启的,可通过启动参数开启或关闭该功能:
#开启指针压缩:
-XX:+UseCompressedOops
#关闭指针压缩:
-XX:-UseCompressedOops
epoch:偏向锁的时间戳
对象头markword不同锁阶段中包含了哪些信息,无锁时调用未被重写的hashcode后的hash值,
分代年龄(age):在jvm的垃圾回收过程中,每当对象经过一次Young GC,年龄都会加1,这里4位来表示分代年龄最大值为15,这也就是为什么对象的年龄超过15后会被移到老年代的原因。在启动时可以通过添加参数来改变年龄阈值:
-XX:MaxTenuringThreshold 超过15会报错
idea maven项目设置JVM启动参数
可以通过设置JVM参数设置是否使用偏向锁,如果明确知道会有多线程争抢互斥锁,设置偏向锁开启反而会降低性能,偏向锁撤销消耗资源。在jdk1.6之后,默认是开启的,如果要禁用掉,设置JVM启动参数,-XX:-useBiasedLocking.
只有偏向锁开启并启动(默认BiasedLockingStartupDelay=4000),处于可偏向状态,线程才有可能获取偏向锁。
修饰实例方法,对当前实例对象this加锁,反编译没有看到monitorenter和monitorexit指令但是
可以看到方法被标识为ACC_SYNCHRONIZED,表明这是一个同步方法。
修饰静态方法,对当前类的Class对象加锁,
修饰代码块,指定加锁对象,对给定对象加锁,反编译可以看到进入同步代码块,执行monitorenter指令,退出同步代码块,执行monitorexit指令,可以看到有2个monitorexit指令,第一个是正常退出执行的,第二个是当异常发生时执行的
在jdk6 之前,通过synchronized关键字加锁时使用无差别的的重量级锁,重量级锁会造成线程的串行执行,并且使cpu在用户态和核心态之间频繁切换。随着对synchronized的不断优化,提出了锁升级的概念,并引入了偏向锁、轻量级锁、重量级锁。在mark word中,锁(lock)标志位占用2个bit,结合1个bit偏向锁(biased_lock)标志位,这样通过倒数的3位,就能用来标识当前对象持有的锁的状态,并判断出其余位存储的是什么信息。
以下总结可能是jdk1.6之后自适应调整的结果
如果获得锁的线程执行出现异常,将会释放锁,进而使其他线程执行,一定要注意。
只要其他线程没执行完,其他线程来争抢时就升级为重量级锁。
多线程轮流获得锁执行的情况,其他线程在执行过程不会去抢锁,才会使用自旋锁。
线程全部执行完成后,自旋锁会恢复到无锁状态,再次有线程来用锁时会直接使用自旋锁,不再使用偏向锁。
根据生产实际情况,考虑是否使用偏向锁。
Monitor对象中三个关键物,entrylist,waitset,owner
检查锁状态
检查线程id
检查线程是否存活且正在执行同步代码块中的代码
检查是否允许重偏向
偏向锁获取
当JVM启动了偏向锁模式(java6之后是默认启动的),启动之后程序的每一个对象的对象头markword中的thread为0,此时没有偏向任何线程,称为可偏向状态(匿名偏向)
此时线程A来了之后会先检查锁对象的状态,是无锁,还是偏向锁,还是自旋锁,或者是重量级锁。如果是无锁状态,则表明不能使用偏向锁,直接升级成轻量级锁。如果是101偏向锁状态,则检查markword中的线程id是否是线程A的线程id,如果是,表示线程A已经获得过该对象的偏向锁,此时,线程A进入同步代码块时,不需要进行CAS加锁,只需要继续往线程A的线程A中添加一条Displaced Markword为空的LR,用来统计重入的次数。
如果不是当前线程A的线程id,则尝试使用CAS操作。此时需要判断锁对象markword的threadId是否为0,如果为0,则会直接替换成功,锁对象将会找到当前线程栈中的内存地址最高的可用的LockRecord,将当前线程A的线程id替换进markword,获得锁,执行同步代码块。
如果不为0,表示该对象锁已经被其他线程(线程B)占用,则会替换失败,开始进行偏向锁撤销。
偏向锁撤销
偏向锁的撤销需要等到一个全局安全点才可以进行,此时stop-the-world,所有的线程暂停。此时开始检查线程B是否存活,遍历当前JVM的所有线程,如果能找到线程B,说明线程B是否还活着。
如果线程B还活着且还正在执行同步代码块中的代码,则把该偏向锁升级为轻量级锁,且原持有偏向锁的线程B继续获得该轻量级锁。
如果线程B已不存在或者不再执行同步代码块中的代码,则进行校验是否允许重偏向。
如果允许重偏向,将锁对象重设为匿名偏向状态(即线程B进行锁撤销),然后进行线程A进行CAS判断线程id为0,线程A获得偏向锁。
如果不允许重偏向,则将对象markword设置为无锁状态,然后升级为轻量级锁,进行CAS竞争。竞争完毕,唤醒暂停的线程,从安全点继续执行代码。
如何判断线程还在执行同步代码块中的内容?
每次进入同步代码块的时候都会从高往低在栈中找到第一个可用的LR,并设置偏向锁id,每次解锁则从低到高移除LR(可重入),所以遍历的时候如果能找到对于的LR则说明还在执行,在底层层面表现为未执行monitorExit指令。
何时升级为重量级锁?
如果锁竞争情况严重, 达到某个最大自旋次数的线程,或者自旋线程数超过CPU核数的一半,会将轻量级锁升级为重量级锁(依然是通过 CAS 修改锁标志位,但不修改持有锁的线程 ID,在jdk1.6之后,加入自适应自旋机制,升级为重量级锁的时机由JVM自行控制)。当后续线尝试获取锁时,发现被占用的锁是重量级锁,则直接将自己挂起(而不是上面说的忙等,即不会自旋),等待释放锁的线程去唤醒。
重量级锁实现原理
对于一个synchronized修饰的方法(代码块)来说:
当多个线程同时访问该方法,那么这些线程会先被放进_EntryList队列,此时线程处于blocked状态
当一个线程获取到了对象的monitor后,那么就可以进入running状态,执行方法,此时,ObjectMonitor对象的/_owner指向当前线程,_count加1表示当前对象锁被一个线程获取
当running状态的线程调用wait()方法,那么当前线程释放monitor对象,进入waiting状态,ObjectMonitor对象的/_owner变为null,_count减1,同时线程进入_WaitSet队列,直到有线程调用notify()方法唤醒该线程,则该线程进入_EntryList队列,竞争到锁再进入_Owner区
如果当前线程执行完毕,那么也释放monitor对象,ObjectMonitor对象的/_owner变为null,_count减1
每个 Java 对象都可以关联一个 Monitor 对象,如果使用 synchronized 给对象上锁(重量级)之后,该对象头的 Mark Word 中就被设置指向 Monitor 对象的指针,进入重量级解锁流程后,按照 Monitor 地址找到 Monitor 对象,设置 Owner 为 null,唤醒 EntryList 中 BLOCKED 线程。
刚开始 Monitor 中 Owner 为 null
当 Thread-2 执行 synchronized(obj) 就会将 Monitor 的所有者 Owner 置为 Thread-2,Monitor中只能有一 个 Owner
在 Thread-2 上锁的过程中,如果 Thread-3,Thread-4,Thread-5 也来执行 synchronized(obj),就会进入 EntryList BLOCKED
Thread-2 执行完同步代码块的内容,然后唤醒 EntryList 中等待的线程来竞争锁,竞争的时是非公平的
图中 WaitSet 中的 Thread-0,Thread-1 是之前获得过锁,但条件不满足进入, WAITING 状态的线程
1、锁对象刚创建时,没有任何线程竞争,对象处于无锁状态。在上面打印的空对象的内存布局中,根据大小端,得到最后8位是00000001,表示处于无锁态,并且处于不可偏向状态。这是因为在jdk中偏向锁存在延迟4秒启动,也就是说在jvm启动后4秒后创建的对象才会开启偏向锁,我们通过jvm参数取消这个延迟时间:
-XX:BiasedLockingStartupDelay=0 (匿名偏向),程序刚启动时会有大量的线程创建,不建议。
这时最后3位为101,表示当前对象的锁没有被持有,并且处于可被偏向状态。
此时如果有线程来执行互斥任务,此线程会先在自己的线程栈里创建一个lock record空间。
LR(LockRecord简称,下同),主要包含两部分,第一步部分可以用于存放MW的副本;第二部分让锁记录中 Object reference 指向锁对象。
下图为当对象所处于偏向锁时,当前线程重入3次,线程栈帧中Lock Record记录:
偏向锁关联线程
public class BiasedLockingTest {
public static void main(String[] args) throws InterruptedException {
TimeUnit.SECONDS.sleep(6);
ObjectUse objectUse = new ObjectUse();
//System.out.println(ClassLayout.parseInstance(objectUse).toPrintable());
//处于可偏向状态,但未偏向
synchronized (objectUse){
System.out.println(ClassLayout.parseInstance(objectUse).toPrintable());
//主线程获得锁,记录主线程的线程id
}
}
static class ObjectUse{
}
}
调用未被改写的hashcode()方法偏向锁撤销
import org.openjdk.jol.info.ClassLayout;
import java.util.concurrent.TimeUnit;
/**
* @title: BiasedExit
* @Author Tan
* @Date: 2023/1/11 16:10
* @Version 1.0
*/
public class BiasedExit {
public static void main(String[] args) throws InterruptedException {
TimeUnit.SECONDS.sleep(6);
Lock lock = new Lock();//这两行位置不能调换
System.out.println("1--"+ClassLayout.parseInstance(lock).toPrintable());
lock.hashCode();
System.out.println("2--"+ClassLayout.parseInstance(lock).toPrintable());
}
static class Lock{}
}
线程一获得偏向锁-->执行完后其他线程来抢(轻量级锁)-->第二个线程执行完毕,恢复为无锁状态
import org.openjdk.jol.info.ClassLayout;
import java.util.concurrent.TimeUnit;
/**
* @title: BiasedLockingTest
* @Author Tan
* @Date: 2023/1/11 10:28
* @Version 1.0
*/
public class BiasedLockingTest {
public static void main(String[] args) throws InterruptedException {
TimeUnit.SECONDS.sleep(6);
ObjectUse objectUse = new ObjectUse();
System.out.println("1==========="+ClassLayout.parseInstance(objectUse).toPrintable());
//处于可偏向状态,但未偏向,此行输出若注释掉,将会全部使用重量级锁,很奇怪
Thread t1 = new Thread(()->{
synchronized (objectUse){
//主线程获得锁,记录主线程的线程id
try {
System.out.println("2==========="+ClassLayout.parseInstance(objectUse).toPrintable());
Thread.sleep(1);把sleep时间延长,6000ms,两任务同时执行,就会升级程重量级锁
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
Thread t2= new Thread(()->{
synchronized (objectUse){
//主线程获得锁,记录主线程的线程id
try {
System.out.println("5==========="+ClassLayout.parseInstance(objectUse).toPrintable());
Thread.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
t1.start();
Thread.sleep(2000);
synchronized (objectUse){
System.out.println("3==========="+ClassLayout.parseInstance(objectUse).toPrintable());
}
System.out.println("4========"+ClassLayout.parseInstance(objectUse).toPrintable());
t2.start();
}
static class ObjectUse{
}
}
异常情况,若注释掉,全部为重量级锁(下图2),所以使用synchronized关键字的时候,最好加一句y同样内容的输出。(哪位大佬如果研究明白原因麻烦评论区告诉我)
线程一获得偏向锁-->没有执行完其他线程来抢(重量级锁)-->全部线程执行完毕,仍为重量级锁。
import org.openjdk.jol.info.ClassLayout;
import java.util.concurrent.TimeUnit;
/**
* @title: BiasedLockingTest
* @Author Tan
* @Date: 2023/1/11 10:28
* @Version 1.0
*/
public class BiasedLockingTest {
public static void main(String[] args) throws InterruptedException {
TimeUnit.SECONDS.sleep(6);
ObjectUse objectUse = new ObjectUse();
System.out.println(ClassLayout.parseInstance(objectUse).toPrintable());
//处于可偏向状态,但未偏向
Thread t = new Thread(()->{
synchronized (objectUse){
//主线程获得锁,记录主线程的线程id
try {
System.out.println("1==========="+ClassLayout.parseInstance(objectUse).toPrintable());
Thread.sleep(6000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
t.start();
Thread.sleep(2000);
synchronized (objectUse){
System.out.println("3==========="+ClassLayout.parseInstance(objectUse).toPrintable());
}
System.out.println("4========"+ClassLayout.parseInstance(objectUse).toPrintable());
}
static class ObjectUse{
}
}
sleep:线程类的静态方法,让出CPU,不释放锁。
wait:Object类的方法,让出锁,一般也会让出CPU,使用受限于synchronized关键字,可以传参数也可以不传参数。
yeild:让出CPU时间片,但继续参与时间片的争抢。
join:用于让其他线程先执行,保证线程执行顺序。
package com.mashibing.juc.c_000_threadbasic;
/**
* @title: Interupt_isinterupted
* @Author Tan
* @Date: 2023/1/3 21:00
* @Version 1.0
*/
public class T_05_Interrupt_isinterrupted_interrupted {
public static void main(String[] args) throws InterruptedException {
Thread t = null;
t = new Thread(() -> {
for (int i = 0; i < 1000; i++) {
System.out.println("666");
if (Thread.currentThread().isInterrupted()){//isInterrupted()方法不会重置中断标志位为false
//if (Thread.interrupted()){ //interrupted()方法会重置重置中断标志位为false
System.out.println("16888");
System.out.println(Thread.interrupted());
break;
}
}
});
t.start();
Thread.sleep(1);
t.interrupt();
}
}
import com.mashibing.util.SleepHelper;
public class T03_VolatileFlag {
private static volatile boolean running = true;
public static void main(String[] args) {
Thread t = new Thread(() -> {
long i = 0L;
while (running) {
//wait recv accept
i++;
}
System.out.println("end and i = " + i); //4168806262 4163032200
});
t.start();
SleepHelper.sleepSeconds(1);
running = false;
}
}
public class T_01_exercise {
long total = 800000000;
long l =0;
long l1 = 0;
long l2 = 0;
// long result1;
//long result2;
private void m1(){
for (long i = 1; i <=total; i++) {
l += i ;
//i++;
}
System.out.println(l);
}
private void m2() throws InterruptedException {
Thread t1 =new Thread(){
public void run(){
for (long i = 1; i
package com.mashibing.zhao;
import java.util.LinkedList;
/**
* @title: TaoBaoOne_Wait_Notify
* @Author Zhao
* @Date: 2023/1/18 15:22
* @Version 1.0
*/
public class TaoBaoOne_Wait_Notify {
static Thread t1 =null,t2 = null;
public static void main(String[] args) {
Rongqi_3 rongqi_3 = new Rongqi_3();
t1=new Thread(()->{
synchronized (rongqi_3){
for (int i = 0; i < 10; i++) {
rongqi_3.add();
System.out.println(rongqi_3.size());
if (rongqi_3.size()==5){
rongqi_3.notify();
try {rongqi_3.wait();} catch (InterruptedException e) {e.printStackTrace();}}}}});
t2=new Thread(()->{
if(rongqi_3.size()!=5){
synchronized (rongqi_3){
try {
rongqi_3.wait();
rongqi_3.notify();
System.out.println("美丽的黑天鹅,see you!!");
} catch (InterruptedException e) {e.printStackTrace();}}}});
t2.start();
t1.start();}
static class Rongqi_3{
LinkedList linkedList = new LinkedList();
private void add(){
linkedList.add(new Object());
}
private int size(){return linkedList.size();}}}
package com.mashibing.zhao;
import java.util.ArrayList;
import java.util.concurrent.CountDownLatch;
/**
* @title: TaoBaoOne_2
* @Author ZhaoWh
* @Date: 2023/1/18 14:27
* @Version 1.0
*/
public class TaoBaoOne_CountdownLatch {
static Thread t1=null,t2=null;
public static void main(String[] args) {
Rongqi_2 rongqi_2 = new Rongqi_2();
CountDownLatch countDownLatch = new CountDownLatch(1);
CountDownLatch latch = new CountDownLatch(1);
t1 = new Thread(()->{
for (int i = 0; i < 10; i++) {
rongqi_2.add();
System.out.println(i);
if(rongqi_2.size()==5){
countDownLatch.countDown();//双重latch,不等于5的时候wait,等于5的时候countdownLatch
try {latch.await();} catch (InterruptedException e) {e.printStackTrace();}}}});
t2 = new Thread(()->{
if (rongqi_2.size()!=5){
try {countDownLatch.await();
System.out.println("恭喜你,线程结束");
latch.countDown();
} catch (InterruptedException e) {
e.printStackTrace();}}});
t2.start();
t1.start();}
static class Rongqi_2{
ArrayList arrayList = new ArrayList();
private void add(){arrayList.add(new Object());}
private int size(){return arrayList.size();}}}
import java.util.concurrent.BrokenBarrierException;
import java.util.concurrent.CyclicBarrier;
public class T07_TestCyclicBarrier {
public static void main(String[] args) {
//CyclicBarrier barrier = new CyclicBarrier(20);
CyclicBarrier barrier = new CyclicBarrier(20, () -> System.out.println("满人"));
/*CyclicBarrier barrier = new CyclicBarrier(20, new Runnable() {
@Override
public void run() {
System.out.println("满人,发车");
}
});*/
for (int i = 0; i < 100; i++) {
new Thread(() -> {
try {
barrier.await();
System.out.println("谁说的清楚谁爱谁");
} catch (InterruptedException e) {
e.printStackTrace();
} catch (BrokenBarrierException e) {
e.printStackTrace();
}
}).start();
}
}
}
import java.util.Random;
import java.util.concurrent.Phaser;
import java.util.concurrent.TimeUnit;
public class T09_TestPhaser2 {
static Random r = new Random();
static MarriagePhaser phaser = new MarriagePhaser();
static void milliSleep(int milli) {
try {
TimeUnit.MILLISECONDS.sleep(milli);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public static void main(String[] args) {
phaser.bulkRegister(7);
for (int i = 0; i < 5; i++) {
new Thread(new Person("p" + i)).start();
}
new Thread(new Person("新郎")).start();
new Thread(new Person("新娘")).start();
}
static class MarriagePhaser extends Phaser {
@Override
protected boolean onAdvance(int phase, int registeredParties) {
switch (phase) {
case 0:
System.out.println("所有人都到齐了!" + registeredParties);
System.out.println();
return false;
case 1:
System.out.println("所有人都吃完了!" + registeredParties);
System.out.println();
return false;
case 2:
System.out.println("所有人都离开了!" + registeredParties);
System.out.println();
return false;
case 3:
System.out.println("婚礼结束!新郎新娘抱抱!" + registeredParties);
return true;
default:
return true;
}
}
}
static class Person implements Runnable {
String name;
public Person(String name) {
this.name = name;
}
public void arrive() {
milliSleep(r.nextInt(1000));
System.out.printf("%s 到达现场!\n", name);
phaser.arriveAndAwaitAdvance();
}
public void eat() {
milliSleep(r.nextInt(1000));
System.out.printf("%s 吃完!\n", name);
phaser.arriveAndAwaitAdvance();
}
public void leave() {
milliSleep(r.nextInt(1000));
System.out.printf("%s 离开!\n", name);
phaser.arriveAndAwaitAdvance();
}
private void hug() {
if (name.equals("新郎") || name.equals("新娘")) {
milliSleep(r.nextInt(1000));
System.out.printf("%s 洞房!\n", name);
phaser.arriveAndAwaitAdvance();
} else {
phaser.arriveAndDeregister();
//phaser.register()
}
}
@Override
public void run() {
arrive();
eat();
leave();
hug();
}
}
}
package com.mashibing.zhao;
import java.util.LinkedList;
import java.util.concurrent.locks.LockSupport;
/**
* @title: TaoBaoOne
* @Author ZhaoWh
* @Date: 2023/1/18 8:52
* @Version 1.0
*/
//实现一个容器,提供两个方法,add,size
//写两个线程,线程1添加10个元素到容器中,线程2实现监控元素的个数,当个数到5个时,线程二给出提示并结束线程二,线程一继续执行
public class TaoBaoOne_LockSupport {
static Thread t1 =null;
static Thread t2 =null;
public static void main(String[] args) {
RongQiOne rongQiOne = new RongQiOne();
t1=new Thread(()->{
for (int i = 0; i < 10; i++) {
rongQiOne.add();
System.out.println(i);
if (rongQiOne.size()==5){
LockSupport.unpark(t2);//要先
LockSupport.park();
}}});
t2=new Thread(()->{
LockSupport.park();
System.out.println("线程二结束");
LockSupport.unpark(t1);
});
t2.start();
t1.start();}
static class RongQiOne{
LinkedList linkedList = new LinkedList();
private void add(){
linkedList.add(new Object());
}
private int size(){
return linkedList.size();
}
}}
interrupt会使park失效
import java.util.concurrent.locks.LockSupport;
public class T05_InterruptAndPark {
public static void main(String[] args) {
Thread t = new Thread(() -> {
System.out.println("1");
LockSupport.park();
System.out.println("2");
});
t.start();
SleepHelper.sleepSeconds(1);
t.interrupt(); //interrupt会使park失效
}
}
import java.util.concurrent.Exchanger;
public class T12_TestExchanger {
static Exchanger exchanger = new Exchanger<>();
public static void main(String[] args) {
new Thread(() -> {
String s = "T1";
try {
s = exchanger.exchange(s);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + " " + s);
}, "t1").start();
new Thread(() -> {
String s = "T2";
try {
s = exchanger.exchange(s);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + " " + s);
}, "t2").start();
}
}
package com.mashibing.zhao;
import java.util.LinkedList;
import java.util.concurrent.Semaphore;
/**
* @title: TaoBaoOne_Semaphore_Join
* @Author ZhaoWh
* @Date: 2023/1/18 16:10
* @Version 1.0
*/
public class TaoBaoOne_Semaphore_Join {
static Thread t1 = null,t2 = null;
public static void main(String[] args) {
Rongqi_4 rongqi_4 = new Rongqi_4();
Semaphore semaphore = new Semaphore(1);
t1 = new Thread(()->{
try {
semaphore.acquire();
} catch (InterruptedException e) {
e.printStackTrace();
}
for (int i = 0; i < 10; i++) {
rongqi_4.add();
System.out.println(rongqi_4.size());
if (rongqi_4.size()==5){
semaphore.release();
t2.start();
try {
t2.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
try {
semaphore.acquire();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
});
t2 = new Thread(()->{
try {
semaphore.acquire();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("线程2执行,结束哟");
semaphore.release();
});
t1.start();
}
static class Rongqi_4{
LinkedList linkedList = new LinkedList();
private void add(){
linkedList.add(new Object());
}
private int size(){return linkedList.size();}}}
AQS是一个用来构建锁和同步器的框架,使用AQS能简单且高效地构造出应用广泛的大量的同步器,比如我们提到的ReentrantLock,Semaphore,其他的诸如ReentrantReadWriteLock,SynchronousQueue,FutureTask等等皆是基于AQS的。当然,我们自己也能利用AQS非常轻松容易地构造出符合我们自己需求的同步器。
学习AQS需要大家对同步锁有一定的概念。同时大家要知道LockSupport的使用,可以参考我这篇文章。(LockSupport从入门到深入理解)
AQS核心思想是,如果被请求的共享资源空闲,则将当前请求资源的线程设置为有效的工作线程,并且将共享资源设置为锁定状态。如果被请求的共享资源被占用,那么就需要一套线程阻塞等待以及被唤醒时锁分配的机制,这个机制AQS是用CLH队列锁实现的,即将暂时获取不到锁的线程加入到队列中。 CLH(Craig,Landin,and Hagersten)队列是一个虚拟的双向队列(虚拟的双向队列即不存在队列实例,仅存在结点之间的关联关系)。AQS是将每条请求共享资源的线程封装成一个CLH锁队列的一个结点(Node)来实现锁的分配。 AQS使用一个int成员变量来表示同步状态,通过内置的FIFO队列来完成获取资源线程的排队工作。AQS使用CAS对该同步状态进行原子操作实现对其值的修改。 (图一为节点关系图)
private volatile int state;//共享变量,使用volatile修饰保证线程可见性
上面讲述的原理还是太抽象了,那我我们上示例,结合案例来分析AQS 同步器的原理。以ReentrantLock使用方式为例。
代码如下:
public class AQSDemo {
private static int num;
public static void main(String[] args) {
ReentrantLock lock = new ReentrantLock();
new Thread(new Runnable() {
@Override
public void run() {
lock.lock();
try {
Thread.sleep(1000);
num += 1000;
System.out.println("A 线程执行了1秒,num = "+ num);
} catch (InterruptedException e) {
e.printStackTrace();
}
finally {
lock.unlock();
}
}
},"A").start();
new Thread(new Runnable() {
@Override
public void run() {
lock.lock();
try {
Thread.sleep(500);
num += 500;
System.out.println("B 线程执行了0.5秒,num = "+ num);
} catch (InterruptedException e) {
e.printStackTrace();
}
finally {
lock.unlock();
}
}
},"B").start();
new Thread(new Runnable() {
@Override
public void run() {
lock.lock();
try {
Thread.sleep(100);
num += 100;
System.out.println("C 线程执行了0.1秒,num = "+ num);
} catch (InterruptedException e) {
e.printStackTrace();
}
finally {
lock.unlock();
}
}
},"C").start();
}
}
执行的某一种结果! 这个代码超级简单,但是执行结果却是可能不一样,大家可以自行实验。
对比一下三种结果,大家会发现,无论什么样的结果,num最终的值总是1600,这说明我们加锁是成功的。
使用方法很简单,线程操纵资源类就行。主要方法有两个lock() 和unlock().我们深入代码去理解。我在源码的基础上加注释,希望大家也跟着调试源码。其实非常简单。
AQS 主要有三大属性分别是 head ,tail, state,其中state 表示同步状态,head为等待队列的头结点,tail 指向队列的尾节点。
/**
* Head of the wait queue, lazily initialized. Except for
* initialization, it is modified only via method setHead. Note:
* If head exists, its waitStatus is guaranteed not to be
* CANCELLED.
*/
private transient volatile Node head;
/**
* Tail of the wait queue, lazily initialized. Modified only via
* method enq to add new wait node.
*/
private transient volatile Node tail;
/**
* The synchronization state.
*/
private volatile int state;
class Node{
//节点等待状态
volatile int waitStatus;
// 双向链表当前节点前节点
volatile Node prev;
// 下一个节点
volatile Node next;
// 当前节点存放的线程
volatile Thread thread;
// condition条件等待的下一个节点
Node nextWaiter;
}
waitStatus 只有特定的几个常量,相应的值解释如下:
本次源码讲解,我们一ReentranLock的非公平锁为例。我们主要关注的方法是lock(),和unlock()。
首先我们看一下lock()方法源代码,直接进入非公平锁的lock方法:
final void lock() {
//1、判断当前state 状态, 没有锁则当前线程抢占锁
if (compareAndSetState(0, 1))
// 独占锁
setExclusiveOwnerThread(Thread.currentThread());
else
// 2、锁被人占了,尝试获取锁,关键方法了
acquire(1);
}
进入 AQS的acquire() 方法:
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
总-分-总
lock方法主要由tryAquire()尝试获取锁,addWaiter(Node.EXCLUSIVE) 加入等待队列,acquireQueued(node,arg)等待队列尝试获取锁。示意图如下:
5.3.1 tryAquire 方法源码
既然是非公平锁,那么我们一进来就想着去抢锁,不管三七二一,直接试试能不能抢到,抢不到再进队列。
final boolean nonfairTryAcquire(int acquires) {
//1、获取当前线程
final Thread current = Thread.currentThread();
// 2、获取当前锁的状态,0 表示没有被线程占有,>0 表示锁被别的线程占有
int c = getState();
// 3、如果锁没有被线程占有
if (c == 0) {
// 3.1、 使用CAS去获取锁, 为什么用case呢,防止在获取c之后 c的状态被修改了,保证原子性
if (compareAndSetState(0, acquires)) {
// 3.2、设置独占锁
setExclusiveOwnerThread(current);
// 3.3、当前线程获取到锁后,直接发挥true
return true;
}
}
// 4、判断当前占有锁的线程是不是自己
else if (current == getExclusiveOwnerThread()) {
// 4.1 可重入锁,加+1
int nextc = c + acquires;
if (nextc < 0) // overflow
throw new Error("Maximum lock count exceeded");
// 4.2 设置锁的状态
setState(nextc);
return true;
}
return false;
}
5.3.2 addWaiter() 方法的解析
private Node addWaiter(Node mode),当前线程没有货得锁的情况下,进入CLH队列。
private Node addWaiter(Node mode) {
// 1、初始化当前线程节点,虚拟节点
Node node = new Node(Thread.currentThread(), mode);
// Try the fast path of enq; backup to full enq on failure
// 2、获取尾节点,初始进入节点是null
Node pred = tail;
// 3、如果尾节点不为null,怎将当前线程节点放到队列尾部,并返回当前节点
if (pred != null) {
node.prev = pred;
if (compareAndSetTail(pred, node)) {
pred.next = node;
return node;
}
}
// 如果尾节点为null(其实是链表没有初始化),怎进入enq方法
enq(node);
return node;
}
// 这个方法可以认为是初始化链表
private Node enq(final Node node) {
// 1、入队 : 为什么要用循环呢?
for (;;) {
// 获取尾节点
Node t = tail;
// 2、尾节点为null
if (t == null) { // Must initialize
// 2.1 初始话头结点和尾节点
if (compareAndSetHead(new Node()))
tail = head;
}
// 3、将当前节点加入链表尾部
else {
node.prev = t;
if (compareAndSetTail(t, node)) {
t.next = node;
return t;
}
}
}
}
有人想明白为什么enq要用for(;;)吗? 咋一看最多只要循环2次啊! 答疑来了,这是对于单线程来说确实是这样的,但是对于多线程来说,有可能在第2部完成之后就被别的线程先执行入链表了,这时候第3步cas之后发现不成功了,怎么办?只能再一次循环去尝试加入链表,直到成功为止。
5.3.3 acquireQueued()方法详解
addWaiter 方法我们已经将没有获取锁的线程放在了等待链表中,但是这些线程并没有处于等待状态。acquireQueued的作用就是将线程设置为等待状态。
final boolean acquireQueued(final Node node, int arg) {
// 失败标识
boolean failed = true;
try {
// 中断标识
boolean interrupted = false;
for (;;) {
// 获取当前节点的前一个节点
final Node p = node.predecessor();
// 1、如果前节点是头结点,那么去尝试获取锁
if (p == head && tryAcquire(arg)) {
// 重置头结点
setHead(node);
p.next = null; // help GC
// 获得锁
failed = false;
// 返回false,节点获得锁,,,然后现在只有自己一个线程了这个时候就会自己唤醒自己
// 使用的是acquire中的selfInterrupt();
return interrupted;
}
// 2、如果线程没有获得锁,且节点waitStatus=0,shouldParkAfterFailedAcquire并将节点的waitStatus赋值为-1
//parkAndCheckInterrupt将线程park,进入等待模式,
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
int ws = pred.waitStatus;
if (ws == Node.SIGNAL)
/*
* This node has already set status asking a release
* to signal it, so it can safely park.
*/
return true;
if (ws > 0) {
/*
* Predecessor was cancelled. Skip over predecessors and
* indicate retry.
*/
do {
node.prev = pred = pred.prev;
} while (pred.waitStatus > 0);
pred.next = node;
} else {
/*
* waitStatus must be 0 or PROPAGATE. Indicate that we
* need a signal, but don't park yet. Caller will need to
* retry to make sure it cannot acquire before parking.
*/
compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
}
return false;
}
我用白话给大家串起来讲一下吧! 我们以reentrantLock的非公平锁结合我们案例4来讲解。
当线程A 到lock()方法时,通过compareAndSetState(0,1)获得锁,并且获得独占锁。当B,C线程去争抢锁时,运行到acquire(1),C线程运行tryAcquire(1),接着运行nonfairTryAcquire(1)方法,未获取锁,最后返回false,运行addWaiter(),运行enq(node),初始化head节点,同时C进入队列;再进入acquireQueued(node,1)方法,初始化waitStatus= -1,自旋并park()进入等待。
接着B线程开始去抢锁,B线程运行tryAcquire(1),运行nonfairTryAcquire(1)方法,未获得锁最后返回false,运行addWaiter(),直接添加到队尾,同时B进入队列;在进入acquireQueued(node,1)方法,初始化waitStatus= -1,自旋并park()进入等待。
5.3 unlock源码分析
unlock释放锁。主要利用的是LockSupport
public final boolean release(int arg) {
// 如果成功释放独占锁,
if (tryRelease(arg)) {
Node h = head;
// 如果头结点不为null,且后续有入队结点
if (h != null && h.waitStatus != 0)
//释放当前线程,并激活等待队里的第一个有效节点
unparkSuccessor(h);
return true;
}
return false;
}
// 如果释放锁着返回true,否者返回false
// 并且将sate 设置为0
protected final boolean tryRelease(int releases) {
int c = getState() - releases;
if (Thread.currentThread() != getExclusiveOwnerThread())
throw new IllegalMonitorStateException();
boolean free = false;
if (c == 0) {
free = true;
setExclusiveOwnerThread(null);
}
setState(c);
return free;
}
private void unparkSuccessor(Node node) {
/*
* If status is negative (i.e., possibly needing signal) try
* to clear in anticipation of signalling. It is OK if this
* fails or if status is changed by waiting thread.
*/
int ws = node.waitStatus;
if (ws < 0)
// 重置头结点的状态waitStatus
compareAndSetWaitStatus(node, ws, 0);
/*
* Thread to unpark is held in successor, which is normally
* just the next node. But if cancelled or apparently null,
* traverse backwards from tail to find the actual
* non-cancelled successor.
*/
// 获取头结点的下一个节点
Node s = node.next;
// s.waitStatus > 0 为取消状态 ,结点为空且被取消
if (s == null || s.waitStatus > 0) {
s = null;
// 获取队列里没有cancel的最前面的节点
for (Node t = tail; t != null && t != node; t = t.prev)
if (t.waitStatus <= 0)
s = t;
}
// 如果节点s不为null,则获得锁
if (s != null)
LockSupport.unpark(s.thread);
}
原文链接:https://blog.csdn.net/u010445301/article/details/125590758
首先,双向链表的特点是它有两个指针,一个指针指向前置节点,一个指针指向后继节点。
所以,双向链表可以支持 常量O(1) 时间复杂度的情况下找到前驱结点,基于这样的特点。
双向链表在插入和删除操作的时候,要比单向链表简单、高效。
因此,从双向链表的特性来看,我认为AQS使用双向链表有三个方面的考虑。
第一个方面,没有竞争到锁的线程加入到阻塞队列,并且阻塞等待的前提是,当前线程所在节点的前置节点是正常状态,这样设计是为了避免链表中存在异常线程导致无法唤醒后续线程的问题。所以线程阻塞之前需要判断前置节点的状态,如果没有指针指向前置节点,就需要从head节点开始遍历,性能非常低。
第二个方面,在Lock接口里面有一个,lockInterruptibly()方法,这个方法表示处于锁阻塞的线程允许被中断。也就是说,没有竞争到锁的线程加入到同步队列等待以后,是允许外部线程通过interrupt()方法触发唤醒并中断的。这个时候,被中断的线程的状态会修改成CANCELLED。被标记为CANCELLED状态的线程,是不需要去竞争锁的,但是它仍然存在于双向链表里面。意味着在后续的锁竞争中,需要把这个节点从链表里面移除,否则会导致锁阻塞的线程无法被正常唤醒。在这种情况下,如果是单向链表,就需要从Head节点开始往下逐个遍历,找到并移除异常状态的节点。同样效率也比较低,还会导致锁唤醒的操作和遍历操作之间的竞争。
第三个方面,为了避免线程阻塞和唤醒的开销,所以刚加入到链表的线程,首先会通过自旋的方式尝试去竞争锁。但是实际上按照公平锁的设计,只有头节点的下一个节点才有必要去竞争锁,后续的节点竞争锁的意义不大。否则,就会造成羊群效应,也就是大量的线程在阻塞之前尝试去竞争锁带来比较大的性能开销。所以为了避免这个问题,加入到链表中的节点在尝试竞争锁之前,需要判断前置节点是不是头节点,如果不是头节点,就没必要再去触发锁竞争的动作。所以这里会涉及到前置节点的查找,如果是单向链表,那么这个功能的实现会非常复杂。
原文链接: https://blog.csdn.net/q331464542/article/details/126123092
公平锁:多个线程按照申请锁的顺序去获得锁,线程会直接进入队列去排队,永远都是队列的第一位才能得到锁。
优点:所有的线程都能得到资源,不会饿死在队列中。
缺点:吞吐量会下降很多,队列里面除了第一个线程,其他的线程都会阻塞,cpu唤醒阻塞线程的开销会很大。
非公平锁:多个线程去获取锁的时候,会直接去尝试获取,获取不到,再去进入等待队列,如果能获取到,就直接获取到锁。
优点:可以减少CPU唤醒线程的开销,整体的吞吐效率会高点,CPU也不必取唤醒所有线程,会减少唤起线程的数量。
缺点:你们可能也发现了,这样可能导致队列中间的线程一直获取不到锁或者长时间获取不到锁,导致饿死。
我举个例子给他家通俗易懂的讲一下的,想了好几天终于在前天跟三歪去肯德基买早餐排队的时候发现了怎么举例了。
现在是早餐时间,敖丙想去kfc搞个早餐,发现有很多人了,一过去没多想,就乖乖到队尾排队,这样大家都觉得很公平,先到先得,所以这是公平锁咯。
那非公平锁就是,敖丙过去买早餐,发现大家都在排队,但是敖丙这个人有点渣的,就是喜欢插队,那他就直接怼到第一位那去,后面的鸡蛋,米豆都不行,我插队也不敢说什么,只能默默忍受了。
但是偶尔,鸡蛋也会崛起,叫我滚到后面排队,我也是欺软怕硬,默默到后面排队,就插队失败了。
介绍完简单的例子,大家可能会说,渣丙,这个我也知道的啊。
我们是不是应该回归真正的实现了,其实在大家经常使用的ReentrantLock中就有相关公平锁,非公平锁的实现了。
大家还记得我在乐观锁、悲观锁章节提到的Sync类么,是ReentrantLock他本身的一个内部类,他继承了AbstractQueuedSynchronizer,我们在操作锁的大部分操作,都是Sync本身去实现的。
Sync呢又分别有两个子类:FairSync和NofairSync
他们子类的名字就可以见名知意了,公平和不公平那又是怎么在代码层面体现的呢?
公平锁:
你可以看到,他加了一个hasQueuedPredecessors的判断,那他判断里面有些什么玩意呢?
代码的大概意思也是判断当前的线程是不是位于同步队列的首位,是就是返回true,否就返回false。
我总觉得写到这里就应该差不多了,但是我坐下来,静静的思考之后发现,还是差了点什么。
上次聊过ReentrantLock了,但是AQS什么的我都只是提了一嘴,一个线程进来,他整个处理链路到底是怎样的呢?
公平锁到底公平不公平呢?让我们一起跟着丙丙走进ReentrantLock的内心世界。
上面提了这么多,我想你应该是有所了解了,那一个线程进来ReentrantLock这个渣男是怎么不公平的呢?(默认是非公平锁)
我先画个图,帮助大家了解下细节:
ReentrantLock的Sync继承了AbstractQueuedSynchronizer也就是我们常说的AQS
他也是ReentrantLock加锁释放锁的核心,大致的内容我之前一期提到了,我就不过多赘述了,他们看看一次加锁的过程吧。
A线程准备进去获取锁,首先判断了一下state状态,发现是0,所以可以CAS成功,并且修改了当前持有锁的线程为自己。
这个时候B线程也过来了,也是一上来先去判断了一下state状态,发现是1,那就CAS失败了,真晦气,只能乖乖去同步队列等待,等着唤醒了,先去睡一觉吧。
A持有久了,也有点腻了,准备释放掉锁,给别的仔一个机会,所以改了state状态,抹掉了持有锁线程的痕迹,准备去叫醒B。
这个时候有个带绿帽子的仔C过来了,发现state怎么是0啊,果断CAS修改为1,还修改了当前持有锁的线程为自己。
B线程被A叫醒准备去获取锁,发现state居然是1,CAS就失败了,只能失落的继续回去同步队列等待,路线还不忘骂A渣男,怎么骗自己,欺骗我的感情。
诺以上就是一个非公平锁的线程,这样的情况就有可能像B这样的线程长时间无法得到资源,优点就是可能有的线程减少了等待时间,提高了利用率。
现在都是默认非公平了,想要公平就得给构造器传值true。
ReentrantLock lock = new ReentrantLock(true);
说完非公平,那我也说一下公平的过程吧:
线A现在想要获得锁,先去判断下state,发现也是0,去看了看队列,自己居然是第一位,果断修改了持有线程为自己。
线程b过来了,去判断一下state,嗯哼?居然是state=1,那cas就失败了呀,所以只能乖乖去排队了。
线程A暖男来了,持有没多久就释放了,改掉了所有的状态就去唤醒线程B了,这个时候线程C进来了,但是他先判断了下state发现是0,以为有戏,然后去看了看队列,发现前面有人了,作为新时代的良好市民,果断排队去了。
线程B得到A的召唤,去判断state了,发现值为0,自己也是队列的第一位,那很香呀,可以得到了。
总结我不说话了,但是去获取锁判断的源码,箭头所指的位置,现在是不是都被我合理的解释了,当前线程,state,是否是0,是否是当前线程等等,都去思考下。
现在默认非公平锁
公平锁:多个线程按照申请锁的顺序去获得锁,线程会直接进入队列去排队,永远都是队列的第一位才能得到锁。
优点:所有的线程都能得到资源,不会饿死在队列中。
缺点:吞吐量会下降很多,队列里面除了第一个线程,其他的线程都会阻塞,cpu唤醒阻塞线程的开销会很大。
非公平锁:多个线程去获取锁的时候,会直接去尝试获取,获取不到,再去进入同步队列等待,如果能获取到,就直接获取到锁。
优点:可以减少CPU唤醒线程的开销,整体的吞吐效率会高点,CPU也不必取唤醒所有线程,会减少唤起线程的数量。
缺点:你们可能也发现了,这样可能导致队列中间的线程一直获取不到锁或者长时间获取不到锁,导致饿死。
线程私有的线程本地变量,实现了线程隔离。与定义在方法内部的变量不同,方法内部的局部变量不能被其他方法访问。类似于类的成员变量,但类的成员变量能够被所有线程访问。
ThreadLocal 适用于如下两种场景
1、每个线程需要有自己单独的实例
2、实例需要在多个方法中共享,但不希望被多线程共享
一句话理解ThreadLocal,threadlocl是作为当前线程中属性ThreadLocalMap集合中的某一个Entry的key值Entry(threadlocl,value),虽然不同的线程之间threadlocal这个key值是一样,但是不同的线程所拥有的ThreadLocalMap是独一无二的,也就是不同的线程间同一个ThreadLocal(key)对应存储的值(value)不一样,从而到达了线程间变量隔离的目的,但是在同一个线程中这个value变量地址是一样的。
set方法:set方法实际是将值存入了当前线程内部类ThreadLocalMap中,map的key值是threadlocal,value值是存入的值。是存入了当前线程的内部。
get方法
import org.apache.ibatis.io.Resources;
import org.apache.ibatis.session.SqlSession;
import org.apache.ibatis.session.SqlSessionFactory;
import org.apache.ibatis.session.SqlSessionFactoryBuilder;
import java.io.IOException;
import java.io.Reader;
/**
* ClassName: SessionUtil
* 连接操作的工具类
* @author wanglina
* @version 1.0
*/
public class MybatisUtil {
private static ThreadLocal threadLcoal = new ThreadLocal();
private static SqlSessionFactory SqlSessionFactory;
/**
*
* 加载配置文件
*/
static{
try{
Reader reader = Resources.getResourceAsReader("mybatis.xml");
SqlSessionFactory = new SqlSessionFactoryBuilder().build(reader);
}catch(IOException e){
e.printStackTrace();
throw new RuntimeException(e);
}
}
/**
* 获取SqlSession
* @return
*/
public static SqlSession getSqlSession(){
//从当前线程获取
SqlSession sqlSession = threadLcoal.get();
if(sqlSession == null){
sqlSession = SqlSessionFactory.openSession();
//将sqlSession与当前线程绑定
threadLcoal.set(sqlSession);
}
return sqlSession;
}
/**
* 关闭Session
*/
public static void closeSqlSession(){
//从当前线程获取
SqlSession sqlSession = threadLcoal.get();
if(sqlSession != null){
sqlSession.close();
threadLcoal.remove();
}
}
}
在ThreadLocalMap中是使用Entry类型的数组实现的 ,Entry 如下
static class Entry extends WeakReference> {
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal> k, Object v) {
super(k);
value = v;
}
}
可以看到有个Entry内部静态类,它继承了WeakReference,总之它记录了两个信息,一个是ThreadLocal类型,一个是Object类型的值。getEntry方法则是获取某个ThreadLocal对应的值,set方法就是更新或赋值相应的ThreadLocal对应的值。
ThreadLocal 在 ThreadLocalMap 中是以一个弱引用身份被Entry中的Key引用的,因此如果ThreadLocal没有外部强引用来引用它,那么ThreadLocal会在下次JVM垃圾收集时被回收。这个时候就会出现Entry中Key已经被回收,出现一个null Key的情况,外部读取ThreadLocalMap中的元素是无法通过null Key来找到Value的。因此如果当前线程的生命周期很长,一直存在,那么其内部的ThreadLocalMap对象也一直生存下来,这些null key就存在一条强引用链的关系一直存在:Thread --> ThreadLocalMap-->Entry-->Value,这条强引用链会导致Entry不会回收,Value也不会回收,但Entry中的Key却已经被回收的情况,造成内存泄漏。
但是JVM团队已经考虑到这样的情况,并做了一些措施来保证ThreadLocal尽量不会内存泄漏:在ThreadLocal的get()、set()、remove()方法调用的时候会清除掉线程ThreadLocalMap中所有Entry中Key为null的Value,并将整个Entry设置为null,最好的办法就是每次调用完之后及时使用remove方法。
生命周期长的线程可以理解为:线程池的核心线程
ThreadLocal在没有外部对象强引用时如Thread,发生GC时弱引用Key会被回收,而Value是强引用不会回收,如果创建ThreadLocal的线程一直持续运行如线程池中的线程,那么这个Entry对象中的value就有可能一直得不到回收,发生内存泄露。
key 使用强引用: 引用的ThreadLocal的对象被回收了,但是ThreadLocalMap还持有ThreadLocal的强引用,如果没有手动删除,ThreadLocal不会被回收(弱引用是value不会被回收),导致Entry内存泄漏。
key 使用弱引用: 引用的ThreadLocal的对象被回收了,由于ThreadLocalMap持有ThreadLocal的弱引用,即使没有手动删除,ThreadLocal也会被回收。value在下一次ThreadLocalMap调用set,get,remove的时候会被清除。
强引用: 如果一个对象具有强引用,它就不会被垃圾回收器回收。即使当前内存空间不足,JVM也不会回收它,而是抛出 OutOfMemoryError 错误,使程序异常终止。如果想中断强引用和某个对象之间的关联,可以显式地将引用赋值为null,这样一来的话,JVM在合适的时间就会回收该对象
软引用: 在使用软引用时,如果内存的空间足够,软引用就能继续被使用,而不会被垃圾回收器回收,只有在内存不足时,软引用才会被垃圾回收器回收。(软引用可用来实现内存敏感的高速缓存,比如网页缓存、图片缓存等。使用软引用能防止内存泄露,增强程序的健壮性)
弱引用: 具有弱引用的对象拥有的生命周期更短暂。因为当 JVM 进行垃圾回收,一旦发现弱引用对象,无论当前内存空间是否充足,都会将弱引用回收。不过由于垃圾回收器是一个优先级较低的线程,所以并不一定能迅速发现弱引用对象
虚引用: 虚引用并不会决定对象的生命周期。如果一个对象仅持有虚引用,那么它就和没有任何引用一样,在任何时候都可能被垃圾回收器回收。虚引用必须和引用队列(ReferenceQueue)联合使用。当垃圾回收器准备回收一个对象时,如果发现它还有虚引用,就会在回收对象的内存之前,把这个虚引用加入到与之关联的引用队列中。(注意哦,其它引用是被JVM回收后才被传入ReferenceQueue中的。由于这个机制,所以虚引用大多被用于引用销毁前的处理工作。可以使用在对象销毁前的一些操作,比如说资源释放等。)
作用:
管理堆外内存
当某一对象需要回收时,该对象关联一个堆外内存。在jvm中所有关联堆外内存的对象都会挂一个虚引用,当这种对象需要回收的时候,这个对象会放在一个队列中,有一个专门的GC监视这个队列,一旦队列中有了数据就会去清理对应的堆外内存。
java容器全览
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Random;
import java.util.Vector;
import java.util.concurrent.CopyOnWriteArrayList;
public class T02_CopyOnWriteList {
public static void main(String[] args) {
List lists =
//new ArrayList<>();
//new Vector();
new CopyOnWriteArrayList<>();
Random r = new Random();
Thread[] ths = new Thread[100];
for (int i = 0; i < ths.length; i++) {
Runnable task = new Runnable() {
@Override
public void run() {
for (int i = 0; i < 1000; i++) lists.add("a" + r.nextInt(10000));
}
};
ths[i] = new Thread(task);
}
runAndComputeTime(ths);
System.out.println(lists.size());
}
static void runAndComputeTime(Thread[] ths) {
long s1 = System.currentTimeMillis();
Arrays.asList(ths).forEach(t -> t.start());
Arrays.asList(ths).forEach(t -> {
try {
t.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
});
long s2 = System.currentTimeMillis();
System.out.println(s2 - s1);
}
}
其他队列
Queue的一些方法
存
add 添加不进去报错
offer 添加不进去返回false
取
peek 取了之后不删除
poll 取了之后删除
如果是blockingQueue,还有另外两个阻塞存阻塞取的方法
put 阻塞存
take 阻塞取
定长线程池就使用了它
import java.util.Random;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.TimeUnit;
public class T05_LinkedBlockingQueue {
static BlockingQueue strs = new LinkedBlockingQueue<>();
static Random r = new Random();
public static void main(String[] args) {
new Thread(() -> {
for (int i = 0; i < 100; i++) {
try {
strs.put("a" + i);
TimeUnit.MILLISECONDS.sleep(r.nextInt(1000));
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}, "p1").start();
for (int i = 0; i < 5; i++) {
new Thread(() -> {
for (; ; ) {
try {
System.out.println(Thread.currentThread().getName() + " take -" + strs.take()
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}, "c" + i).start();
}
}
}
import java.util.Random;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.TimeUnit;
public class T06_ArrayBlockingQueue {
static BlockingQueue strs = new ArrayBlockingQueue<>(10);
static Random r = new Random();
public static void main(String[] args) throws InterruptedException {
for (int i = 0; i < 10; i++) {
strs.put("a" + i);
}
//strs.put("aaa");
//strs.add("aaa");
//strs.offer("aaa");
strs.offer("aaa", 1, TimeUnit.SECONDS);
System.out.println(strs);
}
}
import java.util.PriorityQueue;
public class T07_01_PriorityQueque {
public static void main(String[] args) {
PriorityQueue q = new PriorityQueue<>();
q.add("c");
q.add("e");
q.add("a");
q.add("d");
q.add("z");
for (int i = 0; i < 5; i++) {
System.out.println(q.poll());
}
}
}
import java.util.Calendar;
import java.util.Random;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.DelayQueue;
import java.util.concurrent.Delayed;
import java.util.concurrent.TimeUnit;
public class T07_DelayQueue {
static BlockingQueue tasks = new DelayQueue<>();
static Random r = new Random();
static class MyTask implements Delayed {
String name;
long runningTime;
MyTask(String name, long rt) {
this.name = name;
this.runningTime = rt;
}
@Override
public int compareTo(Delayed o) {
if (this.getDelay(TimeUnit.MILLISECONDS) < o.getDelay(TimeUnit.MILLISECONDS))
return -1;
else if (this.getDelay(TimeUnit.MILLISECONDS) > o.getDelay(TimeUnit.MILLISECONDS))
return 1;
else
return 0;
}
@Override
public long getDelay(TimeUnit unit) {
return unit.convert(runningTime - System.currentTimeMillis(), TimeUnit.MILLISECONDS);
}
@Override
public String toString() {
return name + " " + runningTime;
}
}
public static void main(String[] args) throws InterruptedException {
long now = System.currentTimeMillis();
MyTask t1 = new MyTask("t1", now + 1000);
MyTask t2 = new MyTask("t2", now + 2000);
MyTask t3 = new MyTask("t3", now + 1500);
MyTask t4 = new MyTask("t4", now + 2500);
MyTask t5 = new MyTask("t5", now + 500);
tasks.put(t1);
tasks.put(t2);
tasks.put(t3);
tasks.put(t4);
tasks.put(t5);
System.out.println(tasks);
for (int i = 0; i < 5; i++) {
System.out.println(tasks.take());
}
}
}
一个线程使用put给另一个线程使用take获取
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.SynchronousQueue;
public class T08_SynchronusQueue {
public static void main(String[] args) throws InterruptedException {
BlockingQueue strs = new SynchronousQueue<>();
new Thread(() -> {
try {
System.out.println(strs.take());
} catch (InterruptedException e) {
e.printStackTrace();
}
}).start();
strs.put("aaa");
//strs.put("bbb");
//strs.add("aaa");
System.out.println(strs.size());
}
}
Tranfer的任务必须被取走后才会继续执行,put则不会这样阻塞。有付出必须得有回报。
import java.util.concurrent.LinkedTransferQueue;
public class T09_TransferQueue {
public static void main(String[] args) throws InterruptedException {
LinkedTransferQueue strs = new LinkedTransferQueue<>();
new Thread(() -> {
try {
System.out.println(strs.take());
} catch (InterruptedException e) {
e.printStackTrace();
}
}).start();
strs.transfer("aaa");
//strs.put("aaa");
/*new Thread(() -> {
try {
System.out.println(strs.take());
} catch (InterruptedException e) {
e.printStackTrace();
}
}).start();*/
}
}