笔记整理-多线程与高并发

多线程与高并发目录多线程与高并发 https://www.cnblogs.com/Zs-book1/p/14318992.html?share_token=641d3935-0525-44d5-a772-9764bf2fad2a一、了解多线程什么是进程?什么是线程?并发与并行的区别临界区学习线程必须知道的概念:二、 线程的使用三种方式的区别线程的方法线程的状态三、SynchronizedJMM模型volatilesynchronizedsyn
摘要由CSDN通过智能技术生成

多线程与高并发
目录
多线程与高并发 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();

    /**

    • 方式1: 使用this关键字,锁定当前对象
      */
      public void fun1() {
      synchronized (this) {
      // do something
      }

      synchronized (lock) {
      // do something
      }
      }

    /**

    • 方式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 1bit

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

不懂人情世故

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值