1.JUC简介

在JDK1.5版本里,提供了java.util.concurrent(简称JUC)这个包,在此包中增加了许多有关于并发编程中常用的工具类。

2.基础概念

进程:进程是资源分配的最小单位(工厂)

线程:线程是CPU调度的最小单位(工人)

并发:CPU单核、多个线程操作同一资源

并行:CPU多核、多个线程同时执行

wait/sleep的区别:

  1. 来自不同的类:wait => Object
  2. sleep => Thread
  3. 关于锁的释放:wait会释放锁,而sleep不会

java创建多线程的三种方式:继承Thread类、实现Runnable接口、匿名内部类

3.最常见的锁:synchronized(经典的可重入锁)

synchronized的三种应用方式:

  1. 普通同步方法(实例方法),锁是当前实例对象 ,进入同步代码前要获得当前实例的锁
  2. 静态同步方法,锁是当前类的class对象 ,进入同步代码前要获得当前类对象的锁
  3. 同步方法块,锁是括号里面的对象,对给定对象加锁,进入同步代码库前要获得给定对象的锁。

6.png
作用:Synchronized是Java中解决并发问题的一种最常用最简单的方法 ,他可以确保线程互斥的访问同步代码

  1. //修饰方法
  2. public synchronized void increase(){
  3. i++;
  4. }
  5. //代码块
  6. public class test{
  7. public void test(){
  8. synchronized(this){
  9. }
  10. }
  11. }

1、 无论synchronized关键字加在方法上还是对象上,如果它作用的对象是非静态的,则它取得的锁是对象;如果synchronized作用的对象是一个静态方法或一个类,则它取得的锁是对类,该类所有的对象同一把锁。
2、每个对象只有一个锁(lock)与之相关联,谁拿到这个锁谁就可以运行它所控制的那段代码。
3、实现同步是要很大的系统开销作为代价的,甚至可能造成死锁,所以尽量避免无谓的同步控制

synchronized是会自己升级的:偏向锁 → 轻量级锁 → 重量级锁

初次执行到synchronized代码块的时候,锁对象变成偏向锁(通过CAS修改对象头里的锁标志位),字面意思是“偏向于第一个获得它的线程”的锁。执行完同步代码块后,线程并不会主动释放偏向锁。当第二次到达同步代码块时,线程会判断此时持有锁的线程是否就是自己(持有锁的线程ID也在对象头里),如果是则正常往下执行。由于之前没有释放锁,这里也就不需要重新加锁。如果自始至终使用锁的线程只有一个,很明显偏向锁几乎没有额外开销,性能极高。

一旦有第二个线程加入锁竞争,偏向锁就升级为轻量级锁(自旋锁)。这里要明确一下什么是锁竞争:如果多个线程轮流获取一个锁,但是每次获取锁的时候都很顺利,没有发生阻塞,那么就不存在锁竞争。只有当某线程尝试获取锁的时候,发现该锁已经被占用,只能等待其释放,这才发生了锁竞争。

在轻量级锁状态下继续锁竞争,没有抢到锁的线程将自旋,即不停地循环判断锁是否能够被成功获取。获取锁的操作,其实就是通过CAS修改对象头里的锁标志位。先比较当前锁标志位是否为“释放”,如果是则将其设置为“锁定”,比较并设置是原子性发生的。这就算抢到锁了,然后线程将当前锁的持有者信息修改为自己。

长时间的自旋操作是非常消耗资源的,一个线程持有锁,其他线程就只能在原地空耗CPU,执行不了任何有效的任务,这种现象叫做忙等(busy-waiting)。如果多个线程用一个锁,但是没有发生锁竞争,或者发生了很轻微的锁竞争,那么synchronized就用轻量级锁,允许短时间的忙等现象。这是一种折衷的想法,短时间的忙等,换取线程在用户态和内核态之间切换的开销。

显然,此忙等是有限度的(有个计数器记录自旋次数,默认允许循环10次,可以通过虚拟机参数更改)。如果锁竞争情况严重,某个达到最大自旋次数的线程,会将轻量级锁升级为重量级锁(依然是CAS修改锁标志位,但不修改持有锁的线程ID)。当后续线程尝试获取锁时,发现被占用的锁是重量级锁,则直接将自己挂起(而不是忙等),等待将来被唤醒。在JDK1.6之前,synchronized直接加重量级锁,很明显现在得到了很好的优化。

一个锁只能按照 偏向锁、轻量级锁、重量级锁的顺序逐渐升级(也有叫锁膨胀的),不允许降级。

经典面试题:手写一个单例模式

然后你心里想:简单,饿汉式、懒汉式都给你写一个

面试官:一般水平

那么如何展示我们的水平高呢,我们写一个双重校验锁的单例:

  1. public class Singleton {
  2. //采用volatile修饰
  3. private volatile static Singleton singleton;
  4. //构造方法私有化
  5. private Singleton(){}
  6. //双重校验锁
  7. public static Singleton getInstance(){
  8. //先判断对象是否已经实例过,没有实例化过才进入加锁代码
  9. if(singleton == null){
  10. //类对象加锁
  11. synchronized(Singleton.class){
  12. //再次判断
  13. if (singleton == null){
  14. singleton = new Singleton();
  15. }
  16. }
  17. }
  18. return singleton;
  19. }
  20. }

使用volatile 的原因:避免jvm指令重排

因为 singleton = new Singleton() 这句话可以分为三步:

  1. 为 singleton 分配内存空间;
  2. 初始化 singleton;
  3. 将 singleton 指向分配的内存空间。

但是由于JVM具有指令重排的特性,执行顺序有可能变成 1-3-2。 指令重排在单线程下不会出现问题,但是在多线程下会导致一个线程获得一个未初始化的实例。例如:线程T1执行了1和3,此时T2调用 getInstance() 后发现 singleton 不为空,因此返回 singleton, 但是此时的 singleton 还没有被初始化。
使用 volatile 会禁止JVM指令重排,从而保证在多线程下也能正常执行

4.volatile关键字、内存可见性、原子性、有序性

Java 内存模型中的可见性、原子性和有序性。

内存可见性(Memory Visibility)
1.jpg
什么叫可见性?一个线程对共享变量值的修改,能够被其它线程看到

代码示例:

  1. public class volatileTest {
  2. public static void main(String[] args) {
  3. ThreadDemo td = new ThreadDemo();
  4. new Thread(td).start();
  5. while(true){
  6. if(td.isFlag()){
  7. System.out.println("------------------");
  8. break;
  9. }
  10. }
  11. }
  12. }
  13. class ThreadDemo implements Runnable {
  14. private volatile boolean flag = false;
  15. @Override
  16. public void run() {
  17. try {
  18. Thread.sleep(200);
  19. } catch (InterruptedException e) {
  20. }
  21. flag = true;
  22. System.out.println("flag=" + isFlag());
  23. }
  24. public boolean isFlag() {
  25. return flag;
  26. }
  27. public void setFlag(boolean flag) {
  28. this.flag = flag;
  29. }
  30. }

当我们去掉volatile关键字时:
2.png
我们可以发现,程序并未执行完成,说明了isFlag一直为false。

原因:
上面的程序中共有两个线程,一个是td ,一个是主线程,此时td线程修改了值,但是主线程就是没有获得到,这里可以根据上面的JMM得到答案,就是说此时有一块主存,线程td从主存中读取的flag=false,此时睡眠0.2秒,主线程从主存中也读取了flag=false。0.2秒过后,td线程中flag=true,但是main线程中while(true)执行的速度特别快,是计算机比较底层的代码,所以main线程一直都没有机会再次从主存中读取数据(此时他也不知道主存的数据被更改)。这两个线程之间操作共享数据彼此是不可见的。

volatile不具备原子性:

  1. public class volatileTest2 {
  2. public static void main(String[] args) {
  3. myData myData=new myData();
  4. for (int i = 0; i <20 ; i++) {
  5. new Thread(()->{
  6. for (int j = 0; j <1000 ; j++) {
  7. myData.addPlusPlus();
  8. }
  9. },String.valueOf(i)).start();
  10. }
  11. while (Thread.activeCount()>2){ //java最起码有一个main线程,和一个垃圾回收线程,所以这里是2
  12. Thread.yield();//当前线程由执行态变为就绪态,让出cpu
  13. }
  14. System.out.println(myData.number);
  15. }
  16. }
  17. class myData{
  18. volatile int number=0;
  19. public void addPlusPlus(){
  20. this.number++;
  21. }
  22. }

我们运行三次看结果:

第一次:
3.png
第二次:
4.png
第三次:
5.png
这里我们得出一个结论 num++ 在多线程下是不安全的

因为num++ 实际上是分三步的,

第一步:栈中取出i
第二步:i自增1
第三步:将i存到栈

尽管用了volatile 第三步能够及时写入到内存。但是它不具备原子性,比如线程A从栈中取出i,此时完成了自增,发生了线程调度,此时线程B取出栈的值,尽管线程A里的值发生了更改,但是还未写到栈里,此时线程B操作的还是之前的值。这就证明了volatile不具备原子性。

如何具有原子性呢?

1、synchronized同步锁(不推荐、太重量级且效率低)

2、Atomic包(Compare And Swap(CAS))

  1. public class TestVolatile1 {
  2. public static void main(String[] args) {
  3. myData myData=new myData();
  4. for (int i = 0; i <20 ; i++) {
  5. new Thread(()->{
  6. for (int j = 0; j <1000 ; j++) {
  7. myData.addAtomic();
  8. }
  9. },String.valueOf(i)).start();
  10. }
  11. while (Thread.activeCount()>2){ //java最起码有一个main线程,和一个垃圾回收线程
  12. Thread.yield();//当前线程由执行态变为就绪态,让出cpu
  13. }
  14. System.out.println(Thread.currentThread().getName()+"\t" +myData.atomicInteger);
  15. }
  16. }
  17. class myData{
  18. AtomicInteger atomicInteger=new AtomicInteger(); // 不用赋值,默认就是0
  19. public void addAtomic(){
  20. atomicInteger.getAndIncrement();// 表示i++
  21. }
  22. }

volatile禁止指令重排序

前提条件:计算机在执行程序的时候,为了提高性能,编译器和处理器会对指令进行重排序。

举例:

  1. //线程一
  2. a = 1;
  3. flag = true;
  4. //线程二
  5. if(flag){
  6. a = a + 5;
  7. System.out.println(a);
  8. }

此时线程如果线程1的执行顺序a=1,flag=true,则线程2输出的结果为6,如果此时线程1排序后,先执行了flag=true,还未执行a=1,那么此时恰巧线程2获取了flag=true,最终结果就是5了。解决这个问题 变量前用volatile关键字就可以解决。

重排序对单线程无影响,只影响多线程。因为处理器在进行重排序时是会考虑指令之间的数据依赖性,如果一个指令Instruction 2必须用到Instruction 1的结果,那么处理器会保证Instruction 1会在Instruction 2之前执行。

参考文章:

https://zhuanlan.zhihu.com/p/71156910

https://blog.csdn.net/yinbucheng/article/details/71305951/