面经-多线程

https://blog.csdn.net/u013541140/article/details/95225769?ops_request_misc=%257B%2522request%255Fid%2522%253A%2522165984158916782391841634%2522%252C%2522scm%2522%253A%252220140713.130102334..%2522%257D&request_id=165984158916782391841634&biz_id=0&utm_medium=distribute.pc_search_result.none-task-blog-2~all~top_positive~default-1-95225769-null-null.142^v39^pc_rank_34_ecpm25,185^v2^control&utm_term=%E7%BA%BF%E7%A8%8B%E6%B1%A0&spm=1018.2226.3001.4187https://blog.csdn.net/u013541140/article/details/95225769?ops_request_misc=%257B%2522request%255Fid%2522%253A%2522165984158916782391841634%2522%252C%2522scm%2522%253A%252220140713.130102334..%2522%257D&request_id=165984158916782391841634&biz_id=0&utm_medium=distribute.pc_search_result.none-task-blog-2~all~top_positive~default-1-95225769-null-null.142%5Ev39%5Epc_rank_34_ecpm25,185%5Ev2%5Econtrol&utm_term=%E7%BA%BF%E7%A8%8B%E6%B1%A0&spm=1018.2226.3001.4187

Q:什么进程,什么线程

A:进程就是运作中的一个程序,线程就是程序内部的一条执行路径

Q:讲讲线程的状态/什么周期

A:创建、就绪、运行、阻塞、销毁。 new一个线程后就是创建状态,执行start()方法后处于就绪状态,处于就绪状态的线程获取了cpu之后,就开始执行run()方法,这时属于运行状态,当调用sleep()等方法时就进入了人阻塞状态,最后run()方法执行完或者抛出一个异常时就销毁了。

A:sleep和wait有什么区别?

1.相同点:一旦执行方法,都可以使得当前的线程进入阻塞状态。
2.不同点:

1)两个方法声明的位置不同:Thread类中声明sleep() , Object类中声明wait()
2)调用的要求不同:sleep()可以在任何需要的场景下调用。 wait()必须使用在同步代码块或同步方法中
3)关于是否释放同步监视器:如果两个方法都使用在同步代码块或同步方法中,sleep()不会释放锁,wait()会释放锁

Q:创建线程的方式

A:继承Thread   实现 Runnable 接口   实现Callable接口   使用线程池

注意:实现 Callable 接口 + FutureTask (可以拿到返回结果, 可以处理异常)

package site.zhourui.gilimall.search.thread;

import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;

/**
 * @author zr
 * @date 2021/11/22 22:46
 */
public class ThreadTest {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        System.out.println("main......start...");
//        Thread01 thread01 = new Thread01();
//        thread01.start();

//        Thread02 thread02 = new Thread02();
//        new Thread(thread02).start();

        FutureTask futureTask = new FutureTask<>(new Thread03());
        new Thread(futureTask).start();
        Integer i = (Integer) futureTask.get();
        System.out.println("main......end..."+i);
    }

    public static class Thread01 extends Thread{
        @Override
        public void run() {
            System.out.println("当前线程:"+Thread.currentThread().getName());
            Integer i=10/2;
            System.out.println("运行结果:"+i);
        }
    }

    public static class Thread02 implements Runnable{
        @Override
        public void run() {
            System.out.println("当前线程:"+Thread.currentThread().getName());
            Integer i=12/2;
            System.out.println("运行结果:"+i);
        }
    }

    public static class Thread03 implements Callable {

        @Override
        public Object call() throws Exception {
            System.out.println("当前线程:"+Thread.currentThread().getName());
            Integer i=14/2;
            System.out.println("运行结果:"+i);
            return i;
        }
    }
}

Q:synchronized底层实现

主要的三种使⽤⽅式

修饰实例⽅法: 作⽤于当前对象实例加锁,进⼊同步代码前要获得当前对象实例的锁

修饰静态⽅法: 也就是给当前类加锁,会作⽤于类的所有对象实例,因为静态成员不属于任何⼀个实例对象,是类成员。

修饰代码块: 指定加锁对象,对给定对象加锁,进⼊同步代码库前要获得给定对象的锁。

字节码角度分析synchronized实现

  • synchronized同步代码块实现使用的是monitorenter和monitorexit指令
  • synchronized普通同步方法(调用指令将会检查方法的ACC_SYNCHRONIZED访问标志是否被设置,如果设置了,执行线程会将先持有monitor然后再执行方法,最后再方法完成(无论是正常完成还是非正常完成)时释放minotor)
  • synchronized静态同步方法(ACC_STATIC、ACC_SYNCHRONIZED访问标志区分该方法是否静态同步方法)

Q:锁的升级过程

A:锁也分不同状态,JDK6之前只有两个状态:无锁、有锁(重量级锁),而在JDK6之后对synchronized进行了优化,新增了两种状态,总共就是四个状态:无锁状态、偏向锁、轻量级锁、重量级锁,其中无锁就是一种状态了。锁的类型和状态在对象头Mark Word中都有记录,在申请锁、锁升级等过程中JVM都需要读取对象的Mark Word数据。

总结:无锁–>偏向锁–>轻量级锁–>重量级锁

Q:线程同步的方法

A:

1、Java通过加锁实现线程同步,锁有两类:synchronized和Lock。
2、synchronized加在三个不同的位置,对应三种不同的使用方式,这三种方式的区别是锁对象不同:
(1.)加在普通方法上,则锁是当前的实例(this)。 (2.)加在静态方法上,锁是当前类的Class对象。 (3.)加在代码块上,则需要在关键字后面的小括号里,显式指定一个对象作为锁对象。
3、Lock支持的功能包括:支持响应中断、支持超时机制、支持以非阻塞的方式获取锁、支持多个条件变量(阻塞队列)。

Q;synchronized与Lock的区别

A:相同:二者都可以解决线性安全问题
不同:synchronized机制在执行完相应的同步代码以后,自动释放同步监视器
Lock需要手动的启动同步(Lock())。同时结束同步也需要手动的实现(unlock())

Lock只有代码块锁,synchronized既有代码块锁也有方法锁

Q:线程通信的方式

A:线程通信涉及到的三个方法:
wait():一旦执行此方法,当前线程就进入阻塞状态,并释放同步监视器。
notify():一旦执行此方法,就会唤醒被wait的一个线程。如果有多个线程被wait,就唤醒优先级高的那个。
notifyAll():一旦执行此方法,就会唤醒所有被wait的线程。
说明:
1.wait(),notify(),notifyAll()三个方法必须使用在同步代码块或同步方法中。
2.wait(),notify(),notifyAll()三个方法的调用者必须是同步代码块或同步方法中的同步监视器。否则,会出现IllegalMonitorStateException异常
3.wait(),notify(),notifyAll()三个方法是定义在java.lang.Object类中。

Q:介绍一下乐观锁和悲观锁

A:

  • ①. 悲观锁
  1. 什么是悲观锁?认为自己在操作数据的时候一定有别的线程来修改数据,因此在获取数据的时候会先加锁,确保数据不会被别的线程修改
  2. 适合写操作多的场景,synchronized关键字和Lock的实现类都是悲观锁
  • ②. 乐观锁
  1. 概念:乐观锁认为自己在操作数据时不会有别的线程修改数据,所以不会添加锁,只是在更新数据的时候去判断之前有没有别的线程更新了这个数据。如果这个数据没有被更新,当前线程将自己修改的数据成功写入。如果数据已经被其他线程更新,则根据不同的实现方式执行不同的操作
  2. 适合读操作多的场景,不加锁的特点能够使其读操作的性能大幅度提升
  3. 乐观锁一般有两种实现方式(采用版本号机制、CAS算法实现),乐观锁在Java中通过使用无锁编程来实现,最常采用的时CAS算法,Java原子类中的递增操作就通过CAS自旋实现的

Q:ReentrantLock 

使用方法:

private static final ReentrantLock LOCK = new ReentrantLock();
 
private static void m() {
    LOCK.lock();
    try {
        log.info("begin");
      	。。。。。。
    } finally {
        // 注意锁的释放
        LOCK.unlock();
    }
}

特点:需要手动加锁和释放锁,且unlock 释放锁的操作一定要放在 finally中,否者有可能会出现锁一直被占用,从而导致其他线程一直阻塞的问题

ReentrantLock既可以是公平锁也可以是非公平锁。默认情况下是非公平锁;ReentrantLock可以使用lockInterruptibly获取锁并响应中断指令;ReentrantLock是通过AQS (AbstractQueuedSynchronizer)程序级别的API实现

Q:公平锁和非公平锁

A:

公平锁:

公平锁自然是遵循FIFO(先进先出)原则的,先到的线程会优先获取资源,后到的会进行排队等待

优点:所有的线程都能得到资源,不会饿死在队列中。适合大任务

缺点:吞吐量会下降,队列里面除了第一个线程,其他的线程都会阻塞,cpu唤醒阻塞线程的开销大

非公平锁:

多个线程去获取锁的时候,会直接去尝试获取,获取不到,再去进入等待队列,如果能获取到,就直接获取到锁。

优点:可以减少CPU唤醒线程的开销,整体的吞吐效率会高点,CPU也不必取唤醒所有线程,会减少唤起线程的数量。

缺点:你们可能也发现了,这样可能导致队列中间的线程一直获取不到锁或者长时间获取不到锁

公平锁效率低原因:

公平锁要维护一个队列,后来的线程要加锁,即使锁空闲,也要先检查有没有其他线程在 wait,如果有自己要挂起,加到队列后面,然后唤醒队列最前面线程。这种情况下相比较非公平锁多了一次挂起和唤醒

Q:什么时候用公平?什么时候用非公平?
A:如果为了更高的吞吐量,很显然非公平锁是比较合适的,因为节省很多线程切换时间,吞吐量自然就上去了。否则那就用公平锁,大家公平使用)

Q:什么是可重入锁?有哪些可重入锁?底层原理是什么?

A:指的是可重复可递归调用的锁,在外层使用锁之后,在内层仍然可以使用,并且不发生死锁,这样的锁就叫做可重入锁。

  • 可重入锁的种类
  1. 隐式锁(即synchronized关键字使用的锁)默认是可重入锁,在同步块、同步方法使用
    (在一个synchronized修饰的方法或者代码块的内部调用本类的其他synchronized修饰的方法或代码块时,是永远可以得到锁的)
  2. 显示锁(即Lock)也有ReentrantLock这样的可重入锁
    (lock和unlock一定要一 一匹配,如果少了或多了,都会坑到别的线程)
  • 以Synchronized为例,重入的实现机理(为什么任何一个对象都可以成为一个锁)
  1. 每个锁对象拥有一个锁计数器和一个指向持有该锁的线程的指针(_owner)
  2. 当执行monitorenter时,如果目标锁对象的计数器为零,那么说明它没有被其他线程所持有,Java虚拟机会将该锁对象的持有线程设置为当前线程,并且将计数器加1
  3. 在目标锁对象的计数器不为零的情况下,如果锁对象的持有线程是当前线程,那么Java虚拟机可以将其计数器加1,否则需要等待,直到持有线程释放该锁
  4. 当执行monitorexit,Java虚拟机则需将锁对象的计数器减1。计数器为零代表锁已经释放

Q:什么是死锁?死锁产生的原因?手写一下?如何排查死锁

A:

  • ①. 什么是死锁?

死锁是指两个或两个以上的线程在执行过程中,因争夺资源而造成的一种互相等待的现象,

  • ②. 产生死锁的原因
  1. 互斥条件:进程对所分配到的资源不允许其他进程访问,若其他进程访问该资源,只能等待至占有该资源的进程释放该资源;
  2. 请求与保持条件:进程获得一定的资源后,又对其他资源发出请求,阻塞过程中不会释放自己已经占有的资源
  3. 非剥夺条件:进程已获得的资源,在未完成使用之前,不可被剥夺,只能在使用后自己释放
  4. 循环等待条件:系统中若干进程组成环路,环路中每个进程都在等待相邻进程占用的资源
  • ③手写一个死锁
public class DeadLockDemo{
 
    static Object lockA = new Object();
    static Object lockB = new Object();
    public static void main(String[] args){
        Thread a = new Thread(() -> {
            synchronized (lockA) {
                System.out.println(Thread.currentThread().getName() + "\t" + " 自己持有A锁,期待获得B锁");
                synchronized (lockB) {
                    System.out.println(Thread.currentThread().getName() + "\t 获得B锁成功");
                }
            }
        }, "a");
        a.start();
 
        new Thread(() -> {
            synchronized (lockB){
                System.out.println(Thread.currentThread().getName()+"\t"+" 自己持有B锁,期待获得A锁");
                synchronized (lockA){
                    System.out.println(Thread.currentThread().getName()+"\t 获得A锁成功");
                }
            }
        },"b").start();
 
 
    }
}

  • ④ 如何排查死锁
  • 方法一:使用命令
  • 第一步:在控制台输入jps -l(输出主类全名或jar路径和进程号)

  •  第二步:在控制台输入jstack+空格+进程号 (查看进程的堆栈信息)

  •  方法二:图形化界面-:jconsole(输入cmd,输入jconsole,点击检测死锁按钮)

Q:ThreadLocal简单介绍一下,他为什么会出现内存泄漏

A:首先 ThreadLocal 是一个泛型类,保证可以接受任何类型的对象。因为一个线程内可以存在多个 ThreadLocal 对象,所以其实是 ThreadLocal 内部维护了一个 Map ,是 ThreadLocal 实现的一个叫做 ThreadLocalMap 的静态内部类。我们使用的 get()、set() 方法其实都是调用了这个ThreadLocalMap类对应的 get()、set() 方法。

实际上 ThreadLocalMap 中使用的 key 为 ThreadLocal 的弱引用,⽽ value 是强引⽤。弱引用的特点是,如果这个对象持有弱引用,那么在下一次垃圾回收的时候必然会被清理掉。所以如果 ThreadLocal 没有被外部强引用的情况下,在垃圾回收之后,会出现 key 为 null 的 value。 假如我们不做任何措施的话,value 永远⽆法被GC 回收,如果线程长时间不被销毁,可能会产⽣内存泄露。因此使⽤完ThreadLocal ⽅法后,最好⼿动调⽤ remove() ⽅法

Q:什么是JMM?

A:JMM(Java内存模型Java Memory Model,简称JMM)本身是一种抽象的概念并不真实存在它仅仅描述的是一组约定或规范,通过这组规范定义了程序中(尤其是多线程)各个变量的读写访问方式并决定一个线程对共享变量的写入何时以及如何变成对另一个线程可见,关键技术点都是围绕多线程的原子性、可见性和有序性展开的。

Q:什么是happens-before原则

A:

①. 如果一个操作happens-before(先行发生于)另一个操作,那么第一个操作的执行结果对第二个操作可见,而且第一个操作的执行顺序排在第二个操作之前(可见性,有序性)

②. 两个操作之间存在happens-before关系,并不意外着一定要按照happens-before原则制定的顺序来执行。如果重排序之后的执行结果与按照happens-before关系来执行的结果一致,那么这种重排序并不非法(可以指令重排)
(值日:周一张三周二李四,假如有事情调换班可以的1+2+3=3+2+1)

Q:什么是volatile关键词

A:被volatile修改的变量有2大特点

保证数据的“可见性”:

当写一个volatile变量时,JMM会把该线程对应的本地内存中的共享变量值立即刷新回主内存中。
当读一个volatile变量时,JMM会把该线程对应的本地内存设置为无效,直接从主内存中读取共享变量
所以volatile的写内存语义是写完直接刷新到主内存中,读的内存语义是直接从主内存中读取

保证数据的“有序性”:

禁止指令重排:在多线程操作情况下,指令重排会导致计算结果不一致 

Q:哪些地方使用了volatile?

A:

  • ①. 状态标志,判断业务是否结束-这样T2将flag修改后,T1立马就知道,立刻就会停止while循环
  • public class UseVolatileDemo{
        private volatile static boolean flag = true;
        public static void main(String[] args){
            new Thread(() -> {
                while(flag) {
                    //do something......
                }
            },"t1").start();
     
            //暂停几秒钟线程
            try { TimeUnit.SECONDS.sleep(2L); } catch (InterruptedException e) { e.printStackTrace(); }
     
            new Thread(() -> {
                flag = false;
            },"t2").start();
        }
    }

    ②开销较低的读,写锁策略

  • public class UseVolatileDemo{
        /**
         * 使用:当读远多于写,结合使用内部锁和 volatile 变量来减少同步的开销
         * 理由:利用volatile保证读取操作的可见性;利用synchronized保证复合操作的原子性
         */
        public class Counter{ 
            private volatile int value;
            public int getValue(){
                return value;   //利用volatile保证读取操作的可见性
             }
            public synchronized int increment(){
                return value++; //利用synchronized保证复合操作的原子性
             }
        }
    }

  • ③. 单列模式 DCL双端锁的发布

原始:

public class SafeDoubleCheckSingleton{
    //通过volatile声明,实现线程安全的延迟初始化。
    private static SafeDoubleCheckSingleton singleton;---------Ⅰ
    //私有化构造方法
    private SafeDoubleCheckSingleton(){
    }
    //双重锁设计
    public static SafeDoubleCheckSingleton getInstance(){
        if (singleton == null){
            //1.多线程并发创建对象时,会通过加锁保证只有一个线程能创建对象
            synchronized (SafeDoubleCheckSingleton.class){
                if (singleton == null){
                    //隐患:多线程环境下,由于重排序,该对象可能还未完成初始化就被其他线程读取
    
                    singleton = new SafeDoubleCheckSingleton();--------Ⅱ
                }
            }
        }
        //2.对象创建完毕,执行getInstance()将不需要获取锁,直接返回创建对象
        return singleton;
    }
}

假如重排序,导致Ⅰ发生在Ⅱ后面,这样就会导致singleton为null,所以我们要使用volatile禁止重排序

public class SafeDoubleCheckSingleton{
    //通过volatile声明,实现线程安全的延迟初始化。
    private volatile static SafeDoubleCheckSingleton singleton;
    //私有化构造方法
    private SafeDoubleCheckSingleton(){
    }
    //双重锁设计
    public static SafeDoubleCheckSingleton getInstance(){
        if (singleton == null){
            //1.多线程并发创建对象时,会通过加锁保证只有一个线程能创建对象
            synchronized (SafeDoubleCheckSingleton.class){
                if (singleton == null){
                    //隐患:多线程环境下,由于重排序,该对象可能还未完成初始化就被其他线程读取
                                      //原理:利用volatile,禁止 "初始化对象"(2) 和 "设置singleton指向内存空间"(3) 的重排序
                    singleton = new SafeDoubleCheckSingleton();
                }
            }
        }
        //2.对象创建完毕,执行getInstance()将不需要获取锁,直接返回创建对象
        return singleton;
    }
}

Q:你在哪里使用过volatile

A:

①单列模式 DCL双端锁

②我在高并发JUC编程的时候使用原子类AtomiclntegerFieldUpdater
AtomicLongFieldUpdater
AtomicReferenceFieldUpdater

Q:说说什么是CAS

A:详见下

Q:说说AQS

A:抽象队列同步器

是用来实现锁或者其它同步器组件的公共基础部分的抽象实现,
是重量级基础框架及整个JUC体系的基石,主要用于解决锁分配给"谁"的问题
整体就是一个抽象的FIFO队列来完成资源获取线程的排队工作,并通过一个int类变量表示持有锁的状态
 

Q:说说线程池

A:

7大参数

  • corePoolSize:核心线程数,一直存在,一开始只是new 并没有start
  • maximumPoolSize:最大线程数量,控制资源
  • keepAliveTime: 最大空闲时间
  • TimeUnitunit:时间单位
  • workQueue: 阻塞队列,只要有线程空闲,就会去队列取出新的任务执行
    • new LinkedBlockingDeque()默认是Integer的最大值
  • threadFactory:线程的创建工厂【可以自定义】
    • Executors.defaultThreadFactory(),
  • RejectedExecutionHandler handler:拒绝策略
    • 1、丢弃最老的 Rejected
    • 2、调用者同步调用,直接调用run方法,不创建线程了 Caller
    • 3、直接丢弃新任务 Abort 【默认使用这个】
    • 4、丢弃新任务,并且抛出异常 Discard

执行流程

  1. 当线程池小于corePoolSize,新提交任务将创建一个新线程执行任务,即使此时线程池中存在空闲线程。

  2. 当线程池达到corePoolSize时,新提交任务将被放入 workQueue 中,等待线程池中任务调度执行。

  3. 当workQueue已满,且 maximumPoolSize 大于 corePoolSize 时,新提交任务会创建新线程执行任务。

  4. 当提交任务数超过 maximumPoolSize 时,新提交任务由 RejectedExecutionHandler 处理。

  5. 当线程池中超过corePoolSize 线程,空闲时间达到 keepAliveTime 时,关闭空闲线程 。

四种拒绝策略(handler)

当线程池的线程数达到最大线程数时,需要执行拒绝策略。拒绝策略需要实现 RejectedExecutionHandler 接口,并实现 rejectedExecution(Runnable r, ThreadPoolExecutor executor) 方法。不过 Executors 框架已经为我们实现了 4 种拒绝策略:

  • AbortPolicy(默认):丢弃任务并抛出 RejectedExecutionException 异常。
  • CallerRunsPolicy:由调用线程处理该任务。
  • DiscardPolicy:丢弃任务,但是不抛出异常。可以配合这种模式进行自定义的处理方式。
  • DiscardOldestPolicy:丢弃队列最早的未处理任务,然后重新尝试执行任务。

三种创建方式

方式一:通过构造方法实现

        ThreadPoolExecutor executor = new ThreadPoolExecutor(
                CORE_POOL_SIZE,
                MAX_POOL_SIZE,
                KEEP_ALIVE_TIME,
                TimeUnit.SECONDS,
                new ArrayBlockingQueue<>(QUEUE_CAPACITY),
                Executors.defaultThreadFactory(),
                new ThreadPoolExecutor.CallerRunsPolicy());

方式二:通过 Executor 框架的工具类 Executors 来实现

 四种常见线程池

1、newCachedThreadPool:核心线程数是0,如果空闲会回收所有线程【缓存线程池】

​ 创建一个可缓存线程池, 如果线程池长度超过处理需要, 可灵活回收空闲线程, 若无可回收, 则新建线程。

2、newFixedThreadPool:核心线程数 = 最大线程数,【不回收】

​ 创建一个定长线程池, 可控制线程最大并发数, 超出的线程会在队列中等待。

3、newScheduledThreadPool:定时任务线程池,多久之后执行【可提交核心线程数,最大线程数是Integer.Max】

​ 创建一个定时线程池, 支持定时及周期性任务执行。

4、newSingleThreadPool:核心与最大都只有一个【不回收】,后台从队列中获取任务

​ 创建一个单线程化的线程池, 它只会用唯一的工作线程来执行任务, 保证所有任务按照指定顺序(FIFO, LIFO, 优先级)执行。

线程池优点

  • 降低资源消耗。通过重复利用已创建的线程降低线程创建和销毁造成的消耗。
  • 提高响应速度。当任务到达时,任务可以不需要等到线程创建就能立即执行。
  • 提高线程的可管理性。线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一的分配,调优和监控。

 线程池execute和submit区别(向线程池提交任务)

execute:参数只能是Runnable,没有返回值
submit:参数可以是Runnable、Callable,返回值是FutureTask

线程调度

1.线程

在这里插入图片描述

线程是cpu任务调度的最小执行单位,每个线程拥有自己独立的程序计数器、虚拟机栈、本地方法栈

2.线程的生命周期

线程状态:创建、就绪、运行、阻塞、死亡

3.线程状态切换

4.sleep() 和 wait()的异同?


1.相同点:一旦执行方法,都可以使得当前的线程进入阻塞状态。
2.不同点:1)两个方法声明的位置不同:Thread类中声明sleep() , Object类中声明wait()
2)调用的要求不同:sleep()可以在任何需要的场景下调用。 wait()必须使用在同步代码块或同步方法中
3)关于是否释放同步监视器:如果两个方法都使用在同步代码块或同步方法中,sleep()不会释放锁,wait()会释放锁

5、创建线程方式

继承Thread(java不支持多线程

public class ExtendsThread extends Thread {
    @Override
    public void run() {System.out.println('用Thread类实现线程');}
}

实现 Runnable 接口(优先使用)

public class RunnableThread implements Runnable {
    @Override
    public void run() {System.out.println('用实现Runnable接口实现线程');}
}

实现Callable接口(有返回值可抛出异常)

class CallableTask implements Callable<Integer> {
    @Override
    public Integer call() throws Exception { return new Random().nextInt();}
}

继承Thread类(java不支持多继承)

public class ExtendsThread extends Thread {
    @Override
    public void run() {System.out.println('用Thread类实现线程');}
}

使用线程池(底层都是实现run方法)

static class DefaultThreadFactory implements ThreadFactory {
    DefaultThreadFactory() {
        SecurityManager s = System.getSecurityManager();
        group = (s != null) ? s.getThreadGroup() : Thread.currentThread().getThreadGroup();
        namePrefix = "pool-" + poolNumber.getAndIncrement() +"-thread-";
    }
    public Thread newThread(Runnable r) {
        Thread t = new Thread(group, r,namePrefix + threadNumber.getAndIncrement(),0);
        if (t.isDaemon()) t.setDaemon(false);  //是否守护线程
        if (t.getPriority() != Thread.NORM_PRIORITY) t.setPriority(Thread.NORM_PRIORITY); //线程优先级
        return t;
    }
}

6、synchronized底层实现

使用方法:主要的三种使⽤⽅式

修饰实例⽅法: 作⽤于当前对象实例加锁,进⼊同步代码前要获得当前对象实例的锁

修饰静态⽅法: 也就是给当前类加锁,会作⽤于类的所有对象实例,因为静态成员不属于任何⼀个实例对象,是类成员。

修饰代码块: 指定加锁对象,对给定对象加锁,进⼊同步代码库前要获得给定对象的锁。

// 加在方法上 实际是对this对象加锁
private synchronized void a() {
}

// 同步代码块,锁对象可以是任意的,加在this上 和a()方法作用相同
private void b(){
    synchronized (this){

    }
}

假设下面两种方法都是在类TestSynchronized中

// 加在静态方法上 实际是对类本身(TestSynchronized.class)加锁
private synchronized static void c() {

}

// 同步代码块 实际是对类对象加锁 和c()方法作用相同
private void d(){
    synchronized (TestSynchronized.class){
        
    }
}

//由反射可知,一个对象.class 只会有一个,从而达到加锁的效果

总结:synchronized锁住的资源只有两类:一个是对象,一个是加锁加在对象上,一定要保证是同一对象,加锁才能生效

底层实现:

对象头是我们需要关注的重点,它是synchronized实现锁的基础,因为synchronized申请锁、上锁、释放锁都与对象头有关。对象头主要结构是由Mark Word 组成,其中Mark Word存储对象的hashCode、锁信息或分代年龄或GC标志等信息

锁也分不同状态,JDK6之前只有两个状态:无锁、有锁(重量级锁),而在JDK6之后对synchronized进行了优化,新增了两种状态,总共就是四个状态:无锁状态、偏向锁、轻量级锁、重量级锁,其中无锁就是一种状态了。锁的类型和状态在对象头Mark Word中都有记录,在申请锁、锁升级等过程中JVM都需要读取对象的Mark Word数据。

同步代码块是利用 monitorenter 和 monitorexit 指令实现的,而同步方法则是利用 flags 实现的。

字节码角度分析synchronized实现

  • ②. synchronized有三种应用方式

  • 作用于实例方法,当前实例加锁(this),进入同步代码前要获得当前实例的锁
  • 作用于代码块,对括号里配置的对象加锁
  • 作用于静态方法,当前类加锁,进去同步代码前要获得当前类对象的锁
  • ③. synchronized同步代码块
  1. 实现使用的是monitorenter和monitorexit指令
    在这里插入图片描述
  2. 一定是一个enter和两个exit吗?
    (不一定,如果方法中直接抛出了异常处理,那么就是一个monitorenter和一个monitorexit)
    在这里插入图片描述
  • ④. synchronized普通同步方法
    (调用指令将会检查方法的ACC_SYNCHRONIZED访问标志是否被设置,如果设置了,执行线程会将先持有monitor然后再执行方法,最后再方法完成(无论是正常完成还是非正常完成)时释放minotor)
    在这里插入图片描述
  • ⑤. synchronized静态同步方法
    (ACC_STATIC、ACC_SYNCHRONIZED访问标志区分该方法是否静态同步方法)
    在这里插入图片描述

8.线程同步的三个方法

 方式一:同步代码块
synchronized(同步监视器){
//需要被同步的代码
}

方式二:同步方法
1.同步方法仍然涉及到同步监视器,只是不需要我们显示的声明
2.非静态的同步方法:同步监视器:this
3.静态的同步方法,同步监视器是:当前类本身

方式三:Lock锁
面试题:synchronized与Lock的区别
相同:二者都可以解决线性安全问题
不同:synchronized机制在执行完相应的同步代码以后,自动释放同步监视器
Lock需要手动的启动同步(Lock())。同时结束同步也需要手动的实现(unlock())

 8.5 线程的通信
线程通信涉及到的三个方法:
wait():一旦执行此方法,当前线程就进入阻塞状态,并释放同步监视器。
notify():一旦执行此方法,就会唤醒被wait的一个线程。如果有多个线程被wait,就唤醒优先级高的那个。
notifyAll():一旦执行此方法,就会唤醒所有被wait的线程。
说明:
1.wait(),notify(),notifyAll()三个方法必须使用在同步代码块或同步方法中。
2.wait(),notify(),notifyAll()三个方法的调用者必须是同步代码块或同步方法中的同步监视器。否则,会出现IllegalMonitorStateException异常
3.wait(),notify(),notifyAll()三个方法是定义在java.lang.Object类中。
 

public class ThreadTest {
    public static void main(String[] args) {
        thr th1 = new thr();
        thr th2 = new thr();
        Thread thread1 = new Thread(th1);
        Thread thread2 = new Thread(th2);
        thread1.start();
        thread2.start();

    }


}
class thr implements Runnable{
    private int num=0;
    private Object obj=new Object();
    @Override
    public void run() {
        while (true) {
            //synchronized (this) {
            //    notify(); //this.notify();
            synchronized (obj) {
                //    this.notify();  error
                System.out.println(Thread.currentThread().getName()+":"+"notify");
                obj.notify();
                if(num<=100){
                    System.out.println(Thread.currentThread().getName()+":"+num);
                    num++;
                }else{break;}
                try {
                    //wait();//this.wait();
                    System.out.println(Thread.currentThread().getName()+":"+"wait");
                    obj.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }

    }
}

 直接进入死循环,可见三个方法的调用者必须是同步代码块或同步方法中的同步监视器,且必须是同一个

 修改一下

    public static void main(String[] args) {
        thr th = new thr();
        Thread thread1 = new Thread(th);
        Thread thread2 = new Thread(th);
        thread1.start();
        thread2.start();
    }

 

或者这样子

public class ThreadTest {
    public static String qwer="lock";
    public static void main(String[] args) {
        thr th = new thr();
        thr th1 = new thr();
        thr th2 = new thr();
        Thread thread1 = new Thread(th1);
        Thread thread2 = new Thread(th2);
        thread1.start();
        thread2.start();
    }

}
class thr implements Runnable{
    private int num=0;
    private Object obj=new Object();
    @Override
    public void run() {
        while (true) {
            synchronized (ThreadTest.qwer) {
                ThreadTest.qwer.notify();
                if(num<=100){
                    System.out.println(Thread.currentThread().getName()+":"+num);
                    num++;
                }else{break;}
                try {
                    ThreadTest.qwer.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }

    }
}

乐观锁和悲观锁

  • ①. 悲观锁(synchronized关键字和Lock的实现类都是悲观锁)
  1. 什么是悲观锁?认为自己在使用数据的时候一定有别的线程来修改数据,因此在获取数据的时候会先加锁,确保数据不会被别的线程修改
  2. 适合写操作多的场景,先加锁可以保证写操作时数据正确(写操作包括增删改)、显式的锁定之后再操作同步资源
  3. synchronized关键字和Lock的实现类都是悲观锁
  • ②. 乐观锁
  1. 概念:乐观锁认为自己在使用数据时不会有别的线程修改数据,所以不会添加锁,只是在更新数据的时候去判断之前有没有别的线程更新了这个数据。如果这个数据没有被更新,当前线程将自己修改的数据成功写入。如果数据已经被其他线程更新,则根据不同的实现方式执行不同的操作
  2. 适合读操作多的场景,不加锁的特点能够使其读操作的性能大幅度提升
  3. 乐观锁一般有两种实现方式(采用版本号机制、CAS算法实现),乐观锁在Java中通过使用无锁编程来实现,最常采用的时CAS算法,Java原子类中的递增操作就通过CAS自旋实现的

ReentrantLock 

区别一:用法不同

Synchronized 可用来修饰普通方法、静态方法和代码块;

ReentrantLock 只能用在代码块上

基础使用:

Synchronized见上

ReentrantLock

private static final ReentrantLock LOCK = new ReentrantLock();

private static void m() {
    LOCK.lock();
    try {
        log.info("begin");
      	。。。。。。
    } finally {
        // 注意锁的释放
        LOCK.unlock();
    }
}

区别二:获取锁和释放锁方式不同

Synchronized会自动加锁和释放锁

ReentrantLock需要手动加锁和释放锁 

 

 注意

unlock 释放锁的操作一定要放在 finally中,否者有可能会出现锁一直被占用,从而导致其他线程一直阻塞的问题
 

区别三:锁的类型不同

synchronized属于非公平锁;

ReentrantLock既可以是公平锁也可以是非公平锁。默认情况下是非公平锁

区别四:响应中断不同
ReentrantLock可以使用lockInterruptibly获取锁并响应中断指令;

synchronized 不能响应中断,也就是如果发生了死锁,使用synchronized 会一直等待下去

而使用ReentrantLock可以响应中断并释放锁,从而解决死锁的问题

区别五:底层实现不同

Synchronized是JVM 层面通过监视器(Monitor)实现的;

ReentrantLock是通过AQS (AbstractQueuedSynchronizer)程序级别的API实现

4、公平锁和非公平锁区别

注意:synchronized 和 ReentrantLock 默认是非公平锁

公平锁:

公平锁自然是遵循FIFO(先进先出)原则的,先到的线程会优先获取资源,后到的会进行排队等待

优点:所有的线程都能得到资源,不会饿死在队列中。适合大任务

缺点:吞吐量会下降,队列里面除了第一个线程,其他的线程都会阻塞,cpu唤醒阻塞线程的开销大

非公平锁:

多个线程去获取锁的时候,会直接去尝试获取,获取不到,再去进入等待队列,如果能获取到,就直接获取到锁。

优点:可以减少CPU唤醒线程的开销,整体的吞吐效率会高点,CPU也不必取唤醒所有线程,会减少唤起线程的数量。

缺点:你们可能也发现了,这样可能导致队列中间的线程一直获取不到锁或者长时间获取不到锁

公平锁效率低原因:

公平锁要维护一个队列,后来的线程要加锁,即使锁空闲,也要先检查有没有其他线程在 wait,如果有自己要挂起,加到队列后面,然后唤醒队列最前面线程。这种情况下相比较非公平锁多了一次挂起和唤醒

线程切换的开销,其实就是非公平锁效率高于公平锁的原因,因为非公平锁减少了线程挂起的几率,后来的线程有一定几率逃离被挂起的开销。

Q :为什么会有公平锁、非公平锁的设计?为什么默认非公平?非公平锁效率高的原因

A:

  1. 恢复挂起的线程到真正锁的获取还是有时间差的,从开发人员来看这个时间微乎其微,但是从CPU的角度来看,这个时间存在的还是很明显的,所以非公平锁能更充分的利用CPU的时间片,尽量减少CPU空闲状态时间
  2. 使用多线程很重要的考量点是线程切换的开销,当采用非公平锁时,当一个线程请求锁获取同步状态,然后释放同步状态,因为不需要考虑是否还有前驱节点,所以刚释放锁的线程在此刻再次获取同步状态的概率就变得非常大了,所以就减少了线程的开销线程的开销

 Q:什么时候用公平?什么时候用非公平?
A:如果为了更高的吞吐量,很显然非公平锁是比较合适的,因为节省很多线程切换时间,吞吐量自然就上去了。否则那就用公平锁,大家公平使用)

5、可重入锁

  • ①. 什么是可重入锁?
  1. 指的是可重复可递归调用的锁,在外层使用锁之后,在内层仍然可以使用,并且不发生死锁,这样的锁就叫做可重入锁。
    简单的来说就是:在一个synchronized修饰的方法或代码块的内部调用本类的其他synchronized修饰的方法或代码块时,是永远可以得到锁屑
  2. 如果是1个有synchronized修饰得递归调用方法,程序第2次进入被自己阻塞了岂不是天大的笑话,出现了作茧自缚
  3. 所以Java中ReentrantLockSynchronized都是可重入锁,可重入锁的一个优点是可在一定程度避免死锁
  • ②. 可重入锁这四个字分开解释
    可: 可以 | 重: 再次 | 入: 进入 | 锁: 同步锁 | 进入什么:进入同步域(即同步代码块、方法或显示锁锁定的代码)

  • ④. 可重入锁的种类
  1. 隐式锁(即synchronized关键字使用的锁)默认是可重入锁,在同步块、同步方法使用
    (在一个synchronized修饰的方法或者代码块的内部调用本类的其他synchronized修饰的方法或代码块时,是永远可以得到锁的)
  2. 显示锁(即Lock)也有ReentrantLock这样的可重入锁
    (lock和unlock一定要一 一匹配,如果少了或多了,都会坑到别的线程)
  • ③. 代码验证synchronsyized和ReentrantLock是可重入锁

隐式锁synchronsyized用于同步代码块

 隐式锁synchronsyized用于同步方法

执行m1()方法:

  • ⑤. Synchronized的重入的实现机理(为什么任何一个对象都可以成为一个锁)
  1. 每个锁对象拥有一个锁计数器和一个指向持有该锁的线程的指针(_owner)
  2. 当执行monitorenter时,如果目标锁对象的计数器为零,那么说明它没有被其他线程所持有,Java虚拟机会将该锁对象的持有线程设置为当前线程,并且将计数器加1
  3. 在目标锁对象的计数器不为零的情况下,如果锁对象的持有线程时当前线程,那么Java虚拟机可以将其计数器加1,否则需要等待,直到持有线程释放该锁
  4. 当执行monitorexit,Java虚拟机则需将锁对象的计数器减1。计数器为零代表锁已经释放
    在这里插入图片描述

 6、死锁

  • ①. 什么是死锁?

死锁是指两个或两个以上的线程在执行过程中,因争夺资源而造成的一种互相等待的现象,

  • ②. 产生死锁的原因
  1. 互斥条件:进程对所分配到的资源不允许其他进程访问,若其他进程访问该资源,只能等待至占有该资源的进程释放该资源;
  2. 请求与保持条件:进程获得一定的资源后,又对其他资源发出请求,阻塞过程中不会释放自己已经占有的资源
  3. 非剥夺条件:进程已获得的资源,在未完成使用之前,不可被剥夺,只能在使用后自己释放
  4. 循环等待条件:系统中若干进程组成环路,环路中每个进程都在等待相邻进程占用的资源

  • ③手写一个死锁
public class DeadLockDemo{

    static Object lockA = new Object();
    static Object lockB = new Object();
    public static void main(String[] args){
        Thread a = new Thread(() -> {
            synchronized (lockA) {
                System.out.println(Thread.currentThread().getName() + "\t" + " 自己持有A锁,期待获得B锁");
                synchronized (lockB) {
                    System.out.println(Thread.currentThread().getName() + "\t 获得B锁成功");
                }
            }
        }, "a");
        a.start();

        new Thread(() -> {
            synchronized (lockB){
                System.out.println(Thread.currentThread().getName()+"\t"+" 自己持有B锁,期待获得A锁");
                synchronized (lockA){
                    System.out.println(Thread.currentThread().getName()+"\t 获得A锁成功");
                }
            }
        },"b").start();


    }
}
  • ④ 如何排查死锁

方法一:使用命令

第一步:在控制台输入jps -l(输出主类全名或jar路径和进程号)

 第二步:在控制台输入jstack+空格+进程号 (查看进程的堆栈信息)

 方法二:图形化界面-:jconsole(输入cmd,输入jconsole,点击检测死锁按钮)

在这里插入图片描述在这里插入图片描述
在这里插入图片描述

7、ThreadLocal原理

ThreadLocal简介:

通常情况下,我们创建的变量是可以被任何⼀个线程访问并修改的。如果想实现每⼀个线程都有⾃⼰的 专属本地变量该如何解决呢? JDK中提供的 ThreadLocal 类正是为了解决这样的问题。类似操作系统中的TLAB

原理:

首先 ThreadLocal 是一个泛型类,保证可以接受任何类型的对象。因为一个线程内可以存在多个 ThreadLocal 对象,所以其实是 ThreadLocal 内部维护了一个 Map ,是 ThreadLocal 实现的一个叫做 ThreadLocalMap 的静态内部类。

最终的变量是放在了当前线程的 ThreadLocalMap 中,并不是存在 ThreadLocal 上,ThreadLocal 可以理解为只是ThreadLocalMap的封装,传递了变量值。

我们使用的 get()、set() 方法其实都是调用了这个ThreadLocalMap类对应的 get()、set() 方法。例如下面的

如何使用:

1)存储用户Session

private static final ThreadLocal threadSession = new ThreadLocal();

2)解决线程安全的问题

private static ThreadLocal<SimpleDateFormat> format1 = new ThreadLocal<SimpleDateFormat>()

ThreadLocal内存泄漏的场景

实际上 ThreadLocalMap 中使用的 key 为 ThreadLocal 的弱引用,⽽ value 是强引⽤。弱引用的特点是,如果这个对象持有弱引用,那么在下一次垃圾回收的时候必然会被清理掉。

所以如果 ThreadLocal 没有被外部强引用的情况下,在垃圾回收的时候会被清理掉的,这样一来 ThreadLocalMap中使用这个 ThreadLocal 的 key 也会被清理掉。但是,value 是强引用,不会被清理,这样一来就会出现 key 为 null 的 value。 假如我们不做任何措施的话,value 永远⽆法被GC 回收,如果线程长时间不被销毁,可能会产⽣内存泄露。

ThreadLocalMap实现中已经考虑了这种情况,在调用 set()、get()、remove() 方法的时候,会清理掉 key 为 null 的记录。如果说会出现内存泄漏,那只有在出现了 key 为 null 的记录后,没有手动调用 remove() 方法,并且之后也不再调用 get()、set()、remove() 方法的情况下。因此使⽤完ThreadLocal ⽅法后,最好⼿动调⽤ remove() ⽅法

8、HashMap线程安全

面试突击15:说一下HashMap底层实现?及元素添加流程?_哔哩哔哩_bilibili

死循环造成 CPU 100%

HashMap 有可能会发生死循环并且造成 CPU 100% ,这种情况发生最主要的原因就是在扩容的时候,也就是内部新建新的 HashMap 的时候,扩容的逻辑会反转散列桶中的节点顺序,当有多个线程同时进行扩容的时候,由于 HashMap 并非线程安全的,所以如果两个线程同时反转的话,便可能形成一个循环,并且这种循环是链表的循环,相当于 A 节点指向 B 节点,B 节点又指回到 A 节点,这样一来,在下一次想要获取该 key 所对应的 value 的时候,便会在遍历链表的时候发生永远无法遍历结束的情况,也就发生 CPU 100% 的情况。

所以综上所述,HashMap 是线程不安全的,在多线程使用场景中推荐使用线程安全同时性能比较好的 ConcurrentHashMap。

9、String不可变原因

  1. 可以使用字符串常量池,多次创建同样的字符串会指向同一个内存地址

  2. 可以很方便地用作 HashMap 的 key。通常建议把不可变对象作为 HashMap的 key

  3. hashCode生成后就不会改变,使用时无需重新计算

  4. 线程安全,因为具备不变性的对象一定是线程安全的

JUC

JMM(Java Memory Model) Java内存模型

为什么会推导出JMM模型呢?

1.因为cpu和物理主内存的速度不一致的,cpu运行速度快,内存运行速度慢,会导致cpu需要等待内存运行,造成资源浪费,所以有了多级缓存。

CPU的运行并不是直接操作内存而是先把内存里的数据读到缓存,而内存的读和写操作的时候就会造成不一致的问题
2.Java虚拟机规范中试图定义一种Java内存模型(java Memory Model,简称JMM)来屏蔽掉各种硬件和操作系统的内存访问差异,以实现让Java程序在各种平台下都能达到一致的内存访问效果。

什么是JMM?

JMM(Java内存模型Java Memory Model,简称JMM)本身是一种抽象的概念并不真实存在它仅仅描述的是一组约定或规范,通过这组规范定义了程序中(尤其是多线程)各个变量的读写访问方式并决定一个线程对共享变量的写入何时以及如何变成对另一个线程可见,关键技术点都是围绕多线程的原子性、可见性和有序性展开的。

线程是如何操作主内存的? 

每个线程都是将主线程中的数据,拿到自己的工作内存(线程私有)中,进行读写操作,写操作结束之后,再保存到主内存中   ——大概流程

加亿点点细节,就是这样

在这里插入图片描述

JMM三大特性

一. JVMM规范下 - 可见性

①. 定义:是指当一个线程修改了某一个共享变量的值,其他线程是否能够立即知道该变更,JVMM规定了所有的变量都存储在主内存中
(假设有A、B两个线程同时去操作主物理内存的共享数据number=0,A抢到CPU执行权,将number刷新到自己的工作内存,这个时候进行number++的操作,这个时候number=1,将A中的工作内存中的数据刷新到主物理内存,这个时候,马上通知B,B重新拿到最新值number=1刷新B的工作内存中)
②. Java中普通的共享变量不保证可见性,因为数据修改被写入内存的时机是不确定的,多线程并发很可能出现"脏读",所以每个线程都有自己的工作内存,线程自己的工作内存中保存了该线程使用到的变量的主内存副本拷贝,线程对变量的所有操作(读取、赋值等)都必需在线程自己的工作内存中进行,而不能直接读写主内存中的变量。不同线程之间也无法直接访问对工作内存中的变量,线程间变量值的传递均需要通过主内存来完成

③. 如何解决:volatile可以解决可见性(能否及时看到)

二. JVMM规范下 - 原子性


指一个操作是不可中断的,即多线程坏境下,操作不能被其他线程干扰


三. JVMM规范下 - 有序性


①. 计算机在执行程序时,为了提高性能,编译器和处理器常常会做指令重排,一把分为以下3种

在这里插入图片描述
②. 单线程坏境里面确保程序最终执行结果和代码顺序执行的结果一致

③. 处理器在进行重新排序是必须要考虑指令之间的数据依赖性

在这里插入图片描述

语句4 对语句2有数据依赖性


④. 多线程坏境中线程交替执行,由于编译器优化重排的存在,两个线程使用的变量能否保持一致是无法确认的,结果无法预测

在这里插入图片描述

 如何解决:volatile可以解决有序(能禁止重排)

小总结
我们定义的所有的共享变量都存储在物理主内存中
每个线程都有自己独立的工作内存,里面保存该线程使用到的变量的副本(主内存中该变量的一份拷贝)
线程对共享变量所有的操作都必须先在自己的工作内存中进行后写回主内存,不能直接从主内存中读写(不能越级)
不同线程之间也无法直接访问其他线程的工作内存中的变量,线程间变量值的传递需要通过主内存来进行(同级不能相互访问)

多线程先行发生原则之happens-before

一、先行发生原则说明


①. 如果Java内存模型中有序性仅靠volatile和synchronized来完成,那么有很多操作都将会变得非常啰嗦,但是我们在编写Java并发代码的时候并没有察觉到这一点

②. 我们没有时时、处处、次次,添加volatile和synchronized来完成程序,这是因为Java语言中JMM原则下,有一个"先行发生"(Happens-Before)的原则限制和规则

③. 在JMM中,如果一个操作执行的结果需要对另一个操作可见或者代码重排序,那么这两个操作之间必须存在happens-before关系

④. x、y案例说明

二、happens-before总原则-粗略总结


①. 如果一个操作happens-before(先行发生于)另一个操作,那么第一个操作的执行结果对第二个操作可见,而且第一个操作的执行顺序排在第二个操作之前(可见性,有序性)

②. 两个操作之间存在happens-before关系,并不意外着一定要按照happens-before原则制定的顺序来执行。如果重排序之后的执行结果与按照happens-before关系来执行的结果一致,那么这种重排序并不非法(可以指令重排)
(值日:周一张三周二李四,假如有事情调换班可以的1+2+3=3+2+1)

三、八大原则-细说

(1) 单线程happen-before原则:在同一个线程中,书写在前面的操作happen-before后面的操作。

  一个线程内,按照代码顺序,写在前面的操作先行发生于写在后面的操作,前一个操作的结果可以被后续的操作获取。将白点就是前面一个操作把变量X赋值为1,那后面一个操作肯定能知道X已经变成了1

(2) 锁的happen-before原则:同一个锁的unlock操作happen-before此锁的lock操作。

 一个unlock操作happen-before(先行发生于)后面(这里的"后面"是指时间上的先后)对同一个锁的lock操作(上一个线程unlock了,下一个线程才能获取到锁,进行lock)

(3) volatile的happen-before原则: 对一个volatile变量的写操作happen-before对此变量的任意操作。

对一个volatile变量的写操作先行发生于后面对这个变量的读操作,前面的写对后面的读是可见的,这里的"后面"同样是指时间是的先后

(4) happen-before的传递性原则: 如果A操作 happen-before B操作,B操作happen-before C操作,那么A操作happen-before C操作。

如果操作A先行发生于操作B,而操作B又先行发生于操作C,则可以得出A先行发生于操作C

(5) 线程启动的happen-before原则:同一个线程的start方法happen-before此线程的其它方法。

Thread对象的start( )方法先行发生于线程的每一个动作,必须先启动线程,才能执行线程中的内容

(6) 线程中断的happen-before原则:对线程interrupt方法的调用happen-before被中断线程的检测到中断发送的代码。

  1. 对线程interrupt( )方法的调用先发生于被中断线程的代码检测到中断事件的发生
  2. 可以通过Thread.interrupted( )检测到是否发生中断

(7) 线程终结的happen-before原则:线程中的所有操作都happen-before线程的终止检测。

线程中的所有操作都先行发生于对此线程的终止检测

(8)对象创建的happen-before原则:一个对象的初始化完成先于他的finalize方法调用。

象没有完成初始化之前,是不能调用finalized( )方法的

 案例说明

  • ①. 代码展示、问题暴露、解决方案
  • 	private int value=0;
    	public void setValue(int value){
    	    this.value=value;
    	}
    	public int getValue(){
    	    return value;
    	}
    

    在这里插入图片描述

    ②. 解决方案
    把getter/setter方法都定义synchronized方法(某一时刻只能有一个线程进入)
    把value定义为volatile变量,由于setter方法对value的修改不依赖value的原值,满足volatile关键字的使用
    (对一个volatile变量的写操作先行发生于后面对这个变量的读操作,前面的写对后面的读是可见的,这里的"后面"同样是指时间是的先后)

  • Volatile

前面我们讲过的JMM、Happen-before,JMM是规范,有个细则叫happen-before,用来保证有序性的是volatile、synchronized关键字来捍卫
volatile凭什么可以保证有序性和可见性,靠的是内存屏障,内存屏障分为 loadload、StoreLoad、LoadStore、StoreStore
 

被volatile修改的变量有2大特点

保证数据的“可见性”:

当写一个volatile变量时,JMM会把该线程对应的本地内存中的共享变量值立即刷新回主内存中
当读一个volatile变量时,JMM会把该线程对应的本地内存设置为无效,直接从主内存中读取共享变量
所以volatile的写内存语义是写完直接刷新到主内存中,读的内存语义是直接从主内存中读取

保证数据的“有序性”:

禁止指令重排:在多线程操作情况下,指令重排会导致计算结果不一致 

 在哪些地方可以使用volatile?

  • ①. 单一赋值可以,but含复合运算赋值不可以(i++之类)
volatile int a = 10
volatile boolean flag = false
  • ②. 状态标志,判断业务是否结束
public class UseVolatileDemo{
    private volatile static boolean flag = true;
    public static void main(String[] args){
        new Thread(() -> {
            while(flag) {
                //do something......
            }
        },"t1").start();

        //暂停几秒钟线程
        try { TimeUnit.SECONDS.sleep(2L); } catch (InterruptedException e) { e.printStackTrace(); }

        new Thread(() -> {
            flag = false;
        },"t2").start();
    }
}

 这样T2将flag修改后,T1立马就知道,立刻就会停止while循环

  • ③. 开销较低的读,写锁策略
public class UseVolatileDemo{
    /**
     * 使用:当读远多于写,结合使用内部锁和 volatile 变量来减少同步的开销
     * 理由:利用volatile保证读取操作的可见性;利用synchronized保证复合操作的原子性
     */
    public class Counter{ 
        private volatile int value;
        public int getValue(){
            return value;   //利用volatile保证读取操作的可见性
         }
        public synchronized int increment(){
            return value++; //利用synchronized保证复合操作的原子性
         }
    }
}
  • ④. 单列模式 DCL双端锁的发布

public class SafeDoubleCheckSingleton{
    //通过volatile声明,实现线程安全的延迟初始化。
    private static SafeDoubleCheckSingleton singleton;---------Ⅰ
    //私有化构造方法
    private SafeDoubleCheckSingleton(){
    }
    //双重锁设计
    public static SafeDoubleCheckSingleton getInstance(){
        if (singleton == null){
            //1.多线程并发创建对象时,会通过加锁保证只有一个线程能创建对象
            synchronized (SafeDoubleCheckSingleton.class){
                if (singleton == null){
                    //隐患:多线程环境下,由于重排序,该对象可能还未完成初始化就被其他线程读取
    
                    singleton = new SafeDoubleCheckSingleton();--------Ⅱ
                }
            }
        }
        //2.对象创建完毕,执行getInstance()将不需要获取锁,直接返回创建对象
        return singleton;
    }
}

为什么会有隐患? 

假如重排序,导致Ⅰ发生在Ⅱ后面,这样就会导致singleton为null,所以我们要使用volatile禁止重排序

public class SafeDoubleCheckSingleton{
    //通过volatile声明,实现线程安全的延迟初始化。
    private volatile static SafeDoubleCheckSingleton singleton;
    //私有化构造方法
    private SafeDoubleCheckSingleton(){
    }
    //双重锁设计
    public static SafeDoubleCheckSingleton getInstance(){
        if (singleton == null){
            //1.多线程并发创建对象时,会通过加锁保证只有一个线程能创建对象
            synchronized (SafeDoubleCheckSingleton.class){
                if (singleton == null){
                    //隐患:多线程环境下,由于重排序,该对象可能还未完成初始化就被其他线程读取
                                      //原理:利用volatile,禁止 "初始化对象"(2) 和 "设置singleton指向内存空间"(3) 的重排序
                    singleton = new SafeDoubleCheckSingleton();
                }
            }
        }
        //2.对象创建完毕,执行getInstance()将不需要获取锁,直接返回创建对象
        return singleton;
    }
}

八股文小总结

  • 凭什么我们java写了一个volatile关键字系统底层加入内存屏障?两者关系怎么勾搭上的?

字节码层面javap -c xx.class

它其实添加了一个ACC_VOLATILE

在这里插入图片描述

  • 内存屏障是什么?

内存屏障是一种屏障指令,它使得CPU或编译器对屏障指令的前和后所发出的内存操作执行一个排序的约束。也叫内存栅栏或栅栏指令

  • 内存屏障能干嘛?

1. 阻止屏障两边的指令重排序

2. 写数据时假如屏障,强制将线程私有工作内存的数据刷回主物理内存

3. 读数据时加入屏障,线程私有工作内存的数据失效,重新到主物理内存中获取最新数据

  • 内存屏障的四大指令

在这里插入图片描述

面试官:你在哪里使用过volatile

单列模式 DCL双端锁

②我在高并发JUC编程的时候使用原子类AtomiclntegerFieldUpdater
AtomicLongFieldUpdater
AtomicReferenceFieldUpdater

 

CAS

CAS五连问—知道CAS吗?CAS原理是什么?有什么缺点(循环+ABA)?如何解决ABA问题?(AtomicStampedReference)那你知道AtomicMarkableReference吗

1.为什么会有CAS

回答CAS思想第一步:为什么会有CAS

如果没有cas我们如何处理i++?使用volatile修饰 i    , 给写操作加锁synchronized,缺点就是性能低

有了CAS,使用AtomicInteger 定义  i ,可以直接调用 getAndIncrement()方法进行自增

这样既不用加synchronized,又能保证原子性 (重点!!!!!!)

2、首先回答什么是CAS

 十一、CAS | 小薛博客

回答CAS思想第二步:什么是CAS
compare and swap的缩写,是比较并交换,实现并发算法时常用到的一种技术。它包含三个操作数——内存位置、预期原值及更新值。
执行CAS操作的时候,将内存位置的值与预期原值比较:
如果相匹配,那么处理器会自动将该位置值更新为新值,
如果不匹配,处理器不做任何操作,多个线程同时执行CAS操作只有一个会成功。也可重整旗鼓,重头再来,这种行为叫做自旋

跟着例子理解一下

 

例子:内存中有一个数据5,A B C三个线程对数据进行操作,A想进行++操作,A将数据5读入自己的内存空间,此时旧的预期值就是5,位置内存值是5,A更新之后5+1=6,

更新值是6。A线程就想更新内存中的数据,更新前,线程A比较内存值旧的预期值是否相等——compare。可惜,在A线程进行++操作时,B,C已经抢先一步(注意CAS是没有synchronized)将内存值5修改为7,线程A就发现内存值(7)旧的预期值(5)不相等,此时就有两个选择:

选择①:什么也不做,摆烂,更新失败

选择②:重整旗鼓,继续去读数据,在自己内存改数据,内存值旧的预期值比较,相等就成功,不等就继续重试,这种行为就是自旋。

如果线程A发现旧的预期值和内存值是相等的,就交换两个值——swap,就是更新成功

3.其次解释CAS原理 

 回答第三步:CAS底层原理

CAS操作,我们都是通过方法compareAndSet()来实现的(见图左,即使是++——getAndIncrement()操作,最后也是调用compareAndSet),根据参数的不同,会调用Unsafe类的三个方法(见下图右),Unsafe 是CAS的核心类,由于Java方法无法直接访问底层系统,需要通过本地(native)方法来访问,Unsafe相当于一个后门,基于该类可以直接操作特定内存的数据。Unsafe类存在于sun.misc包中,其内部方法操作可以像C的指针一样直接操作内存,因为Java中CAS操作的执行依赖于Unsafe类的方法。 也就是说Unsafe类中的方法都直接调用操作系统底层资源执行相应任务。

综上,CAS是靠硬件实现的从而在硬件层面提升效率,最底层还是交给硬件来保证原子性和可见性的,实现方式是基于硬件平台的汇编指令,在intel的CPU中(X86机器上),使用的是汇编指令cmpxchg指令

 

4、最后说一下CAS的缺点 

回答第四步:CAS的缺点 

①如果发生自旋,会给CPU带来很大的开销。

②无法保证代码块的原子性

功能限制CAS是能保证单个变量的操作是原子性的,在Java中要配合使用volatile关键字来保证线程的安全;当涉及到多个变量的时候CAS无能为力

③CAS会导致“ABA问题”。
 CAS算法实现一个重要前提需要取出内存中某时刻的数据并在当下时刻比较并替换,那么在这个时间差类会导致数据的变化。
 比如说一个线程one从内存位置V中取出A,这时候另一个线程two也从内存中取出A,并且线程two进行了一些操作将值变成了B,
然后线程two又将V位置的数据变成A,这时候线程one进行CAS操作发现内存中仍然是A,然后线程one操作成功。
 尽管线程one的CAS操作成功,但是不代表这个过程就是没有问题的。

回答第五步:如何解决ABA问题

类似于乐观锁,加上版本号(又叫戳记流水)。利用AtomicStampedReference类来解决  stamp的意思就是戳记

一些API说明:

   AtomicStampedReference 的初始化一定需要带两个参数,第一个是初始化对象,一个是初始化流水号initialstamp

static AtomicStampedReference atomicStampedReference = new AtomicStampedReference(100,1);

 获取对象

atomicStampedReference.getReference()

 获取流水号

atomicStampedReference.getStamp()

 更新操作,需要传入四个参数,①初始对象②更新后的对象③期望流水号④如果更新成功,也更新流水号,一般是期望流水号+1

atomicStampedReference.compareAndSet()

如果不使用 

public class ABADemo {
     static AtomicInteger atomicInteger=new AtomicInteger(100);

    public static void main(String[] args) {
        new Thread(new Runnable() {
            @Override
            public void run() {
                atomicInteger.compareAndSet(100,101);
                System.out.println(atomicInteger.get());
                atomicInteger.compareAndSet(101,100);
            }
        },"t1").start();
        new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println(atomicInteger.compareAndSet(100, 2022));
                System.out.println(atomicInteger.get());
            }
        },"t2").start();

    }
}

 

public class ABADemo
{    

    //第一个线程测试不带戳记流水号
    static AtomicInteger atomicInteger = new AtomicInteger(100);
    
    static AtomicStampedReference atomicStampedReference = new AtomicStampedReference(100,1);

    public static void main(String[] args)
    {
        new Thread(() -> {
            atomicInteger.compareAndSet(100,101);
            atomicInteger.compareAndSet(101,100);
        },"t1").start();

        new Thread(() -> {
            //暂停一会儿线程
            try { Thread.sleep( 500 ); } catch (InterruptedException e) { e.printStackTrace(); };            System.out.println(atomicInteger.compareAndSet(100, 2019)+"\t"+atomicInteger.get());
        },"t2").start();

        //暂停一会儿线程,main彻底等待上面的ABA出现演示完成。
        try { Thread.sleep( 2000 ); } catch (InterruptedException e) { e.printStackTrace(); }

        System.out.println("============以下是ABA问题的解决=============================");

        new Thread(() -> {
            int stamp = atomicStampedReference.getStamp();
            System.out.println(Thread.currentThread().getName()+"\t 首次版本号:"+stamp);//1
            //暂停一会儿线程,
            try { Thread.sleep( 1000 ); } catch (InterruptedException e) { e.printStackTrace(); }
            atomicStampedReference.compareAndSet(100,101,atomicStampedReference.getStamp(),atomicStampedReference.getStamp()+1);
            System.out.println(Thread.currentThread().getName()+"\t 2次版本号:"+atomicStampedReference.getStamp());
            atomicStampedReference.compareAndSet(101,100,atomicStampedReference.getStamp(),atomicStampedReference.getStamp()+1);
            System.out.println(Thread.currentThread().getName()+"\t 3次版本号:"+atomicStampedReference.getStamp());
        },"t3").start();

        new Thread(() -> {
            int stamp = atomicStampedReference.getStamp();
            System.out.println(Thread.currentThread().getName()+"\t 首次版本号:"+stamp);//1
            //暂停一会儿线程,获得初始值100和初始版本号1,故意暂停3秒钟让t3线程完成一次ABA操作产生问题
            try { Thread.sleep( 3000 ); } catch (InterruptedException e) { e.printStackTrace(); }
            boolean result = atomicStampedReference.compareAndSet(100,2019,stamp,stamp+1);
            System.out.println(Thread.currentThread().getName()+"\t"+result+"\t"+atomicStampedReference.getReference());
        },"t4").start();
    }
}

 

 4.自旋锁(spinlock)

回答:是指尝试获取锁的线程不会立即阻塞,而是采用循环的方式去尝试获取锁, 当线程发现锁被占用时,会不断循环判断锁的状态,直到获取。这样的好处是减少线程上下文切换的消耗,缺点是循环会消耗CPU

这就是一个典型的自旋锁

/**
 * 题目:实现一个自旋锁
 * 自旋锁好处:循环比较获取没有类似wait的阻塞。
 *
 * 通过CAS操作完成自旋锁,A线程先进来调用myLock方法自己持有锁5秒钟,B随后进来后发现
 * 当前有线程持有锁,不是null,所以只能通过自旋等待,直到A释放锁后B随后抢到。
 */
public class SpinLockDemo
{
    AtomicReference<Thread> atomicReference = new AtomicReference<>();

    public void myLock()
    {
        Thread thread = Thread.currentThread();
        System.out.println(Thread.currentThread().getName()+"\t come in");
        while(!atomicReference.compareAndSet(null,thread))
        {

        }
    }

    public void myUnLock()
    {
        Thread thread = Thread.currentThread();
        atomicReference.compareAndSet(thread,null);
        System.out.println(Thread.currentThread().getName()+"\t myUnLock over");
    }

    public static void main(String[] args)
    {
        SpinLockDemo spinLockDemo = new SpinLockDemo();

        new Thread(() -> {
            spinLockDemo.myLock();
            try { TimeUnit.SECONDS.sleep( 5 ); } catch (InterruptedException e) { e.printStackTrace(); }
            spinLockDemo.myUnLock();
        },"A").start();

        //暂停一会儿线程,保证A线程先于B线程启动并完成
        try { TimeUnit.SECONDS.sleep( 1 ); } catch (InterruptedException e) { e.printStackTrace(); }

        new Thread(() -> {
            spinLockDemo.myLock();
            spinLockDemo.myUnLock();
        },"B").start();

    }
}

 原子类-应用于项目

十二、原子操作类之18罗汉增强 | 小薛博客

  1. AtomicBoolean
  2. AtomicInteger
  3. AtomicIntegerArray
  4. AtomicIntegerFieldUpdater
  5. AtomicLong
  6. AtomicLongArray
  7. AtomicLongFieldUpdater
  8. AtomicMarkableReference
  9. AtomicReference
  10. AtomicReferenceArray
  11. AtomicReferenceFieldUpdater
  12. AtomicStampedReference
  13. DoubleAccumulator
  14. DoubleAdder
  15. LongAccumulator
  16. LongAdder

1、基本类型原子类

  • AtomicInteger
  • AtomicBoolean
  • AtomicLong

#1、常用API简介

public final int get() //获取当前的值
public final int getAndSet(int newValue)//获取当前的值,并设置新的值
public final int getAndIncrement()//获取当前的值,并自增
public final int getAndDecrement() //获取当前的值,并自减
public final int getAndAdd(int delta) //获取当前的值,并加上预期的值
boolean compareAndSet(int expect, int update) //如果输入的数值等于预期值,则以原子方式将该值设置为输入值(update)

 1.1.测试

一定要有个想法就是多线程并发,是由时间片切换的

下面的例子,其实是有51个线程,50个new的加上一个main线程,有可能50个线程还没有全部执行完,mian线程就执行,所以输出大概率不可能是500000

public class AutomicIntegerTest {
    static AtomicInteger atomicInteger = new AtomicInteger(0);
    private static final int SIZE = 50;
    public static void main(String[] args) {
        for (int i = 0; i < SIZE; i++) {
            new Thread(()->{
                for (int j = 0; j < 10000; j++) {
                    atomicInteger.incrementAndGet();
                }
            }).start();
        }
        System.out.println(Thread.currentThread().getName()+" 执行结果"+ atomicInteger.get();
    }
}

 需要一个操作,确保上面50个线程全部执行完成,那就是CountDownLatch,确保50个线程全部执行完成

CountDownLatch countDownLatch = new CountDownLatch(50);

public class AutomicIntegerTest {
    static AtomicInteger atomicInteger = new AtomicInteger(0);
    private static final int SIZE = 50;

    public static void main(String[] args) throws InterruptedException {
        CountDownLatch countDownLatch = new CountDownLatch(SIZE);
        for (int i = 0; i < SIZE; i++) {
            new Thread(()->{
                try {
                    for (int j = 0; j < 10000; j++) {
                        atomicInteger.incrementAndGet();
                    }
                }finally {
                    countDownLatch.countDown();
                }

            }).start();
        }
        countDownLatch.await();
        System.out.println(Thread.currentThread().getName()+" 执行结果"+ atomicInteger.get());
    }
}

2、数组类型原子类-详见标题连接

  • AtomicIntegerArray
  • AtomicLongArray
  • AtomicReferenceArray
public class AtomicIntegerArrayDemo
{
    public static void main(String[] args)
    {
        AtomicIntegerArray atomicIntegerArray = new AtomicIntegerArray(new int[5]);
        //AtomicIntegerArray atomicIntegerArray = new AtomicIntegerArray(5);
        //AtomicIntegerArray atomicIntegerArray = new AtomicIntegerArray(new int[]{1,2,3,4,5});

        for (int i = 0; i <atomicIntegerArray.length(); i++) {
            System.out.println(atomicIntegerArray.get(i));//全是0
        }
        System.out.println();
        System.out.println();
        System.out.println();
        int tmpInt = 0;

        tmpInt = atomicIntegerArray.getAndSet(0,1122);
        System.out.println(tmpInt+"\t"+atomicIntegerArray.get(0)); //0     1122
        atomicIntegerArray.getAndIncrement(1);//下标1的数++
        atomicIntegerArray.getAndIncrement(1);//下标1的数++
        tmpInt = atomicIntegerArray.getAndIncrement(1);//类似i++,先赋值在++
        System.out.println(tmpInt+"\t"+atomicIntegerArray.get(1));//2      3
    }
}

3、引用类型原子类-详见标题连接

  • AtomicReference
  • AtomicStampedReference
    • 携带版本号的引用类型原子类,可以解决ABA问题
    • 解决修改过几次
    • 状态戳原子引用
  • AtomicMarkableReference
    • 原子更新带有标记位的引用类型对象
    • 解决是否修改过 它的定义就是将状态戳简化为true|false -- 类似一次性筷子,不再是+1+1,而是true|false 循环变
@Getter
@ToString

class User
{
    String userName;
    int    age;

    public User(String userName, int age) {
        this.userName = userName;
        this.age = age;
    }
}

public class AtomicIntegerArrayDemo
{
    public static void main(String[] args)
    {
        User z3 = new User("z13",24);
        User li4 = new User("li14",26);

        AtomicReference<User> atomicReferenceUser = new AtomicReference<>();

        atomicReferenceUser.set(z3);
        System.out.println(atomicReferenceUser.compareAndSet(z3,li4)+"\t"+atomicReferenceUser.get().toString());//true	User(userName=li14, age=26)
        System.out.println(atomicReferenceUser.compareAndSet(z3,li4)+"\t"+atomicReferenceUser.get().toString());//false	User(userName=li14, age=26)
    }
}

4、对象的属性修改原子类

84_原子类之对象的属性修改原子类理论_哔哩哔哩_bilibili

  • AtomicIntegerFieldUpdater
    • 基于反射的实用程序,可对指定类的指定volatile int字段进行原子更新。
  • AtomicLongFieldUpdater
    • 基于反射的实用程序,可以对指定类的指定volatile long字段进行原子更新。
  • AtomicReferenceFieldUpdater
    • 基于反射的实用程序,可以对指定类的指定volatile 引用字段进行原子更新。

即  粒度更细的原子类

①. 使用目的:
以一种线程安全的方式操作非线程安全对象内的某些字段
是否可以不要锁定整个对象,减少锁定的范围,只关注长期、敏感性变化的某一个字段,而不是整个对象,以达到精确加锁+节约内存的目的
②. 使用要求
更新的对象属性必须使用public volatile修饰符
这种原子类型,是抽象类,所以每次使用都必须使用静态方法newUpdater( )创建一个更新器,并且需要设置想要更新的类和属性

AtomicIntegerFieldUpdater

package com.bilibili.juc.atomics;


import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicIntegerFieldUpdater;

class BankAccount//资源类
{
    String bankName = "CCB";

    //更新的对象属性必须使用 public volatile 修饰符。
    public volatile int money = 0;//钱数

    public void add()
    {
        money++;
    }

    //因为对象的属性修改类型原子类都是抽象类,所以每次使用都必须
    // 使用静态方法newUpdater()创建一个更新器,并且需要设置想要更新的类和属性。

    AtomicIntegerFieldUpdater<BankAccount> fieldUpdater =
            AtomicIntegerFieldUpdater.newUpdater(BankAccount.class,"money");

    //不加synchronized,保证高性能原子性,局部微创小手术
    public void transMoney(BankAccount bankAccount)
    {
        fieldUpdater.getAndIncrement(bankAccount);
    }


}

/**
 * @auther zzyy
 * 以一种线程安全的方式操作非线程安全对象的某些字段。
 *
 * 需求:
 * 10个线程,
 * 每个线程转账1000,
 * 不使用synchronized,尝试使用AtomicIntegerFieldUpdater来实现。
 */
public class AtomicIntegerFieldUpdaterDemo
{
    public static void main(String[] args) throws InterruptedException
    {
        BankAccount bankAccount = new BankAccount();
        CountDownLatch countDownLatch = new CountDownLatch(10);

        for (int i = 1; i <=10; i++) {
            new Thread(() -> {
                try {
                    for (int j = 1; j <=1000; j++) {
                        //bankAccount.add();
                        bankAccount.transMoney(bankAccount);
                    }
                } finally {
                    countDownLatch.countDown();
                }
            },String.valueOf(i)).start();
        }

        countDownLatch.await();

        System.out.println(Thread.currentThread().getName()+"\t"+"result: "+bankAccount.money);
    }
}

AtomicReferenceFieldUpdater


class MyVar //资源类
{
    public volatile Boolean isInit = Boolean.FALSE;

    AtomicReferenceFieldUpdater<MyVar,Boolean> referenceFieldUpdater =
            AtomicReferenceFieldUpdater.newUpdater(MyVar.class,Boolean.class,"isInit");

    public void init(MyVar myVar)
    {
        if (referenceFieldUpdater.compareAndSet(myVar,Boolean.FALSE,Boolean.TRUE))
        {
            System.out.println(Thread.currentThread().getName()+"\t"+"----- start init,need 2 seconds");
            //暂停几秒钟线程
            try { TimeUnit.SECONDS.sleep(3); } catch (InterruptedException e) { e.printStackTrace(); }
            System.out.println(Thread.currentThread().getName()+"\t"+"----- over init");
        }else{
            System.out.println(Thread.currentThread().getName()+"\t"+"----- 已经有线程在进行初始化工作。。。。。");
        }
    }
}


/**
 * @auther zzyy
 * 需求:
 * 多线程并发调用一个类的初始化方法,如果未被初始化过,将执行初始化工作,
 * 要求只能被初始化一次,只有一个线程操作成功
 */
public class AtomicReferenceFieldUpdaterDemo
{
    public static void main(String[] args)
    {
        MyVar myVar = new MyVar();

        for (int i = 1; i <=5; i++) {
            new Thread(() -> {
                myVar.init(myVar);
            },String.valueOf(i)).start();
        }
    }
}
//打印:

1	----- start init,need 2 seconds
3	----- 已经有线程在进行初始化工作。。。。。
2	----- 已经有线程在进行初始化工作。。。。。
4	----- 已经有线程在进行初始化工作。。。。。
5	----- 已经有线程在进行初始化工作。。。。。
1	----- over init


ThreadLocal

 为什么会有ThreadLocal

threadLocal和Thread和ThreadLocalMap的区别

thread包含threadLocal,ThreadLocalMap是threadLocal的静态内部类


线程池

Java 多线程:彻底搞懂线程池_孙强 Jimmy的博客-CSDN博客_java 多线程池

7大参数,3种创建方法,4种拒绝策略

7大参数

  • corePoolSize:核心线程数,一直存在,一开始只是new 并没有start
  • maximumPoolSize:最大线程数量,控制资源
  • keepAliveTime: 最大空闲时间
  • TimeUnitunit:时间单位
  • workQueue: 阻塞队列,只要有线程空闲,就会去队列取出新的任务执行
    • new LinkedBlockingDeque()默认是Integer的最大值
  • threadFactory:线程的创建工厂【可以自定义】
    • Executors.defaultThreadFactory(),
  • RejectedExecutionHandler handler:拒绝策略
    • 1、丢弃最老的 Rejected
    • 2、调用者同步调用,直接调用run方法,不创建线程了 Caller
    • 3、直接丢弃新任务 Abort 【默认使用这个】
    • 4、丢弃新任务,并且抛出异常 Discard

源码分析:

一,详细解释一下参数

private final AtomicInteger ctl = new AtomicInteger(ctlOf(RUNNING, 0));
private static final int COUNT_BITS = Integer.SIZE - 3;//Integer.SIZE=32,即integer的最大值有32位
private static final int CAPACITY   = (1 << COUNT_BITS) - 1;

// runState is stored in the high-order bits
private static final int RUNNING    = -1 << COUNT_BITS;
private static final int SHUTDOWN   =  0 << COUNT_BITS;
private static final int STOP       =  1 << COUNT_BITS;
private static final int TIDYING    =  2 << COUNT_BITS;
private static final int TERMINATED =  3 << COUNT_BITS;

// Packing and unpacking ctl
private static int runStateOf(int c)     { return c & ~CAPACITY; }//得到线程池状态
private static int workerCountOf(int c)  { return c & CAPACITY; }//得到正在工作的线程数
private static int ctlOf(int rs, int wc) { return rs | wc; }

 AtomicInteger可以理解成Integer,表达了两个意思

1、声明当前线程池的状态

 2.声明线程池中的线程数

一个变量如何表达这两个意思的?int一共有32位字符 

高3位表示线程池的状态        底29位表示线程池的线程个数


 SIZE=32     32-3=29  即一共有29位表示线程个数,方便后面做位运算

private static final int COUNT_BITS = Integer.SIZE - 3;

 1的二进制   0000 0000 0000 0000 0000 0000 0000 0001

                左移29位

                    0010 0000 0000 0000 0000 0000 0000 0000

                减去1 

                   0000 1111 1111 1111 1111 1111 1111 1111

CAPACITY的意思就是最大能创建多少个线程

private static final int CAPACITY = (1 << COUNT_BITS) - 1;

-1的二进制表示 1111 1111 1111 1111 1111 1111 1111 1111

                          左移29位

                          1110 0000 0000 0000 0000 0000 0000 0000

即   高3位  111   表示正常接受任务

private static final int RUNNING    = -1 << COUNT_BITS;

      高3位   000  表示SHUTDOWN状态,不接受新任务,但是内部还会处理阻塞队列中的任务和正在进行的任务

    调用executor.shutdown() ;触发

private static final int SHUTDOWN   =  0 << COUNT_BITS;

      高3位 001 表示STOP状态,不接受任务,也不处理阻塞队列中的任务,同时中断正在进行的任务

        调用executor.shutDownNow();触发

private static final int STOP       =  1 << COUNT_BITS;

     高3位  010  表示TIDYING状态,过度状态,代表当前线程池即将销毁 

private static final int TIDYING    =  2 << COUNT_BITS;

    高3位   011  代表TERMINATED,线程池已经销毁

private static final int TERMINATED =  3 << COUNT_BITS;

 二、线程池状态变化

三、execute方法 

  1. 当线程池小于corePoolSize,新提交任务将创建一个新线程执行任务,即使此时线程池中存在空闲线程。

  2. 当线程池达到corePoolSize时,新提交任务将被放入 workQueue 中,等待线程池中任务调度执行。

  3. 当workQueue已满,且 maximumPoolSize 大于 corePoolSize 时,新提交任务会创建新线程执行任务。

  4. 当提交任务数超过 maximumPoolSize 时,新提交任务由 RejectedExecutionHandler 处理。

  5. 当线程池中超过corePoolSize 线程,空闲时间达到 keepAliveTime 时,关闭空闲线程 。

public void execute(Runnable command) {
    //健壮性判断
    if (command == null)
        throw new NullPointerException();
    //获得32位的int
    int c = ctl.get();       
    // 工作线程数  < 核心线程数
    if (workerCountOf(c) < corePoolSize) {
        //boolean addWorker(Runnable firstTask, boolean core) arg0:线程   arg1:是否属于核心线程。即如果是true,创建核心线程
        if (addWorker(command, true))
            return;
        //上一个if没进去的原因就是发现了并发。如果核心线程只剩下一个,A,B同时进行if (workerCountOf(c) < corePoolSize),但是A执行的快,创建了核心线程,但是B没来得及,线程数已经达到了corePoolSize
        //所以这里需要重新获取一下ct1,因为并发,数据产生了 变化,线程数会+1
        c = ctl.get();
    }
    //if(线程池是否是RUNNING状态 && 进入阻塞队列是否成功)
    if (isRunning(c) && workQueue.offer(command)) {
        //再次获取ctl
        int recheck = ctl.get();
        //如果不是RUNNING状态 执行Remove,移除任务(仔细考虑一下)
        if (! isRunning(recheck) && remove(command))
            reject(command);//拒绝策略
        else if (workerCountOf(recheck) == 0)//如果线程池处在RUNNING状态,但是工作线程数为0。没有加锁,一定要考虑并发情况!!!!!!!!是不是因为给核心线程添加了过期时间等等原因?TODO
            addWorker(null, false);//阻塞队列有任务,但是没有工作线程,添加一个任务为空的工作线程
    }
    //创建非核心线程,否则拒绝策略
    else if (!addWorker(command, false))
        reject(command);
}


addWorker()方法

private boolean addWorker(Runnable firstTask, boolean core) {
    retry://标记for循环,用于return到标记点
    for (;;) {
        int c = ctl.get();
        //获取状态
        int rs = runStateOf(c);

        // Check if queue empty only if necessary.
        //rs >= SHUTDOWN,除了RUNNING,其他状态都>= SHUTDOWN
        //! (rs == SHUTDOWN && firstTask == null && ! workQueue.isEmpty())换一种形式就是
        // rs != SHUTDOWN || firstTask != null ||  workQueue.isEmpty()
        //也就是说如果状态是非RNUNNING 并且 状态是STOP/TIDYING/TERMINATED 或者 任务不为空 或者 阻塞队列是空,那就return false
不是运行状态的直接失败,除非这个线程池是SHUTDOWN 并且 FirstTask是空 并且工作线程不是空,这种情况下可以继续。
        if (rs >= SHUTDOWN && ! (rs == SHUTDOWN && firstTask == null && ! workQueue.isEmpty()))
            
            return false;
        for (;;) {
            int wc = workerCountOf(c);
            if (wc >= CAPACITY || 
                wc >= (core ? corePoolSize : maximumPoolSize))
                return false;
            //将工作线程数加1   采取CAS操作
            if (compareAndIncrementWorkerCount(c))
                break retry;
            c = ctl.get();  // Re-read ctl
            //判断状态,如果有变化:结束这次外侧循环,继续下一次外侧循环。如果没有变化,重新执行内侧循环
            if (runStateOf(c) != rs)
                continue retry;
            // else CAS failed due to workerCount change; retry inner loop
        }
    }

         //真正创建线程

    boolean workerStarted = false;
    boolean workerAdded = false;
   //worker就是工作线程
    Worker w = null;
    try {
        w = new Worker(firstTask);
        final Thread t = w.thread;
        if (t != null) {
            拿到全局锁,避免我在添加任务时,其他线程干掉线程池,因为干掉线程池需要获取到这个锁
            final ReentrantLock mainLock = this.mainLock;
            mainLock.lock();//加锁
            try {
                // Recheck while holding lock.
                // Back out on ThreadFactory failure or if
                // shut down before lock acquired.
                int rs = runStateOf(ctl.get());
                // 两种情况允许添加工作线程
                if (rs < SHUTDOWN || // 判断线程池是否是RUNNING状态
                    (rs == SHUTDOWN && firstTask == null)) {
                    if (t.isAlive()) // precheck that t is startable   // 如果线程池状态为SHUTDOWN并且任务为空
                        throw new IllegalThreadStateException();
                    workers.add(w);//把w-工作线程放在workers中,private final HashSet<Worker> workers = new HashSet<Worker>();
                    int s = workers.size();
                    if (s > largestPoolSize)//largestPoolSize:线程池创建之后,存在的线程数最大是多少-不同于最大线程数
                        largestPoolSize = s;
                    workerAdded = true;//创建成功
                }
            } finally {
                mainLock.unlock();
            }
            if (workerAdded) {
                t.start();//启动工作线程
                workerStarted = true;//启动工作线程成功
            }
        }
    } finally {
        if (! workerStarted)
            addWorkerFailed(w);
    }
    return workerStarted;
}
 
}

Worker

6、Worker的封装_哔哩哔哩_bilibili

四、拒绝策略(handler)
当线程池的线程数达到最大线程数时,需要执行拒绝策略。拒绝策略需要实现 RejectedExecutionHandler 接口,并实现 rejectedExecution(Runnable r, ThreadPoolExecutor executor) 方法。不过 Executors 框架已经为我们实现了 4 种拒绝策略:

AbortPolicy(默认):丢弃任务并抛出 RejectedExecutionException 异常。
CallerRunsPolicy:由调用线程处理该任务。
DiscardPolicy:丢弃任务,但是不抛出异常。可以配合这种模式进行自定义的处理方式。
DiscardOldestPolicy:丢弃队列最早的未处理任务,然后重新尝试执行任务。
 

五、功能线程池

参考:Java 多线程:彻底搞懂线程池_孙强 Jimmy的博客-CSDN博客_java 多线程池
嫌上面使用线程池的方法太麻烦?其实Executors已经为我们封装好了 4 种常见的功能线程池,如下:

定长线程池(FixedThreadPool)
定时线程池(ScheduledThreadPool )
可缓存线程池(CachedThreadPool)
单线程化线程池(SingleThreadExecutor)
 

①定长线程池(FixedThreadPool)

  • 特点:只有核心线程,线程数量固定,执行完立即回收,任务队列为链表结构的有界队列。
  • 应用场景:控制线程最大并发数。

②定时线程池(ScheduledThreadPoolweiwei

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值