精华内容
下载资源
问答
  • Java基础--多线程排序
    2019-06-22 16:58:00
    public class TestSrot implements Runnable {
    
    	private int number;
    	
    	public int getNumber() {
    		return number;
    	}
    
    	public void setNumber(int number) {
    		this.number = number;
    	}
    	
    	public TestSrot(int number) {
    		super();
    		this.number = number;
    	}
    
    	@Override
    	public void run() {
    		// TODO Auto-generated method stub
    		try {
    			Thread.sleep(this.number);
    			System.out.println(this.number);
    		} catch (InterruptedException e) {
    			// TODO Auto-generated catch block
    			e.printStackTrace();
    		}
    	}
    	
    	public static void main(String[] args) {
    		int[] numbers = {10,2,5,12,1,50};
    		for(int num : numbers) {
    			new Thread(new TestSrot(num)).start();
    		}
    	}
    
    }
    

      

    转载于:https://www.cnblogs.com/mxh-java/p/11069518.html

    更多相关内容
  • java多线程排序

    热门讨论 2008-07-02 09:37:17
    java多线程排序源程序,三种排序算法。希尔排序,快速排序,堆排序。
  • 这是一个简单的运用多线程的程序 主要对快速排序等多个算法进行比较
  • 成绩 面向对象原理与Java实践课程实验报告 实验5多线程 姓 名 _ _ _ 班 级 _ 学 号 _ 实验地点 _ 实验时间 _ 指导教师 _ _ _ 一实验目的 了解线程调度机制 理解线程同步机制 掌握线程设计方法 二实验要求 掌握线程...
  • 为实现多线程快速排序,提出基于Fork/Join框架的多线程快速排序,同时对排序算法进行优化。该算法主要用于大量数据需要进行排序处理的应用。
  • 主要介绍了了解Java多线程的可见性与有序性,在Java内存模型中,允许编译器和处理器对指令进行重排序,但是重排序过程不会影响到单线程程序的执行,却会影响到多线程并发执行的正确性。,需要的朋友可以参考下
  • ????使用 ThreadGroup,首先创建一个线程组,把创建的子线程都放到这个线程组里,然后循环判断这个线程组的活跃线程数量 是否等于0,不等于0继续,否则代表子线程全部执行完了,进行下一步。

    不多啰嗦了,直接上代码。

    方式一:使用 ThreadGroup 控制

    IntStream.range(0, N)
                    .forEach(i -> new Thread(() -> ShellSort.sort(bucketsArray.get(i)), "子线程--桶-" + i).start());
    return bucketsArray.stream().flatMapToLong(arr -> LongStream.of(arr)).toArray();

    比如这段代码,bucketsArray 是一个 List<long[]> 对象,ShelloSort.sort 对一个 long[] 进行排序,bucketsArray 里面存储的各个桶 的 long[],第二个桶的 long 都比 第一个的大。

    代码的目的就是分别对每个桶的 long[] 进行排序,利用多线程的方式加快了速度,之后再把 bucketsArrayList<long[]>)转化long[]  返回。因为是多线程同时运行,所以当 return 语句中得到值时,子线程都还运行结束,那么得出的值肯定是错误的,因为每个桶的 long[] 还没排序完。结果如下图

    修正方法,我们需要等到多个子线程都运行结束后,再执行 return 语句就可以了,因此可以加个 Thread.sleep() 等一会,问题只是时间等的早了还是等的晚了。

    😍使用 ThreadGroup,首先创建一个线程组,把创建的子线程都放到这个线程组里,然后循环判断这个线程组的活跃线程数量

    是否等于0,不等于0继续,否则代表子线程全部执行完了,进行下一步

    ThreadGroup group = new ThreadGroup("排序组");       
    IntStream.range(0, N)
                .forEach(i -> new Thread(group, () -> ShellSort.sort(bucketsArray.get(i)), "子线程--桶-" + i).start());
            
    while(group.activeCount() != 0){ }
    return bucketsArray.stream().flatMapToLong(arr -> LongStream.of(arr)).toArray();

    测试如下

    还不错,多线程的希尔排序比快速排序更快!😁

    方式二:

    等发现了新方式再更 😋😋😋

    展开全文
  • java多线程安全性基础介绍 线程安全 正确性 什么是线程安全性 原子性 竞态条件 i++ 读i ++ 值写回i 可见性 JMM 由于cpu和内存加载速度的差距,在两者之间增加了多级缓存导致,内存并不能直接对cpu可见。 ...
  • 万字图解Java多线程

    万次阅读 多人点赞 2020-09-06 14:45:07
    java多线程我个人觉得是javaSe中最难的一部分,我以前也是感觉学会了,但是真正有多线程的需求却不知道怎么下手,实际上还是对多线程这块知识了解不深刻,不知道多线程api的应用场景,不知道多线程的运行流程等等,...

    前言

    授权Java面试者精选独家原创发布

    java多线程我个人觉得是javaSe中最难的一部分,我以前也是感觉学会了,但是真正有多线程的需求却不知道怎么下手,实际上还是对多线程这块知识了解不深刻,不知道多线程api的应用场景,不知道多线程的运行流程等等,本篇文章将使用实例+图解+源码的方式来解析java多线程。

    文章篇幅较长,大家也可以有选择的看具体章节,建议多线程的代码全部手敲,永远不要相信你看到的结论,自己编码后运行出来的,才是自己的。

    什么是java多线程?

    进程与线程

    进程

    • 当一个程序被运行,就开启了一个进程, 比如启动了qq,word
    • 程序由指令和数据组成,指令要运行,数据要加载,指令被cpu加载运行,数据被加载到内存,指令运行时可由cpu调度硬盘、网络等设备

    线程

    • 一个进程内可分为多个线程
    • 一个线程就是一个指令流,cpu调度的最小单位,由cpu一条一条执行指令

    并行与并发

    并发:单核cpu运行多线程时,时间片进行很快的切换。线程轮流执行cpu

    并行:多核cpu运行 多线程时,真正的在同一时刻运行

    java提供了丰富的api来支持多线程。

    为什么用多线程?

    多线程能实现的都可以用单线程来完成,那单线程运行的好好的,为什么java要引入多线程的概念呢?

    多线程的好处:

    1. 程序运行的更快!快!快!

    2. 充分利用cpu资源,目前几乎没有线上的cpu是单核的,发挥多核cpu强大的能力

    多线程难在哪里?

    单线程只有一条执行线,过程容易理解,可以在大脑中清晰的勾勒出代码的执行流程

    多线程却是多条线,而且一般多条线之间有交互,多条线之间需要通信,一般难点有以下几点

    1. 多线程的执行结果不确定,受到cpu调度的影响
    2. 多线程的安全问题
    3. 线程资源宝贵,依赖线程池操作线程,线程池的参数设置问题
    4. 多线程执行是动态的,同时的,难以追踪过程
    5. 多线程的底层是操作系统层面的,源码难度大

    有时候希望自己变成一个字节穿梭于服务器中,搞清楚来龙去脉,就像无敌破坏王一样(没看过这部电影的可以看下,脑洞大开)。

    java多线程的基本使用

    定义任务、创建和运行线程

    任务: 线程的执行体。也就是我们的核心代码逻辑

    定义任务

    1. 继承Thread类 (可以说是 将任务和线程合并在一起)
    2. 实现Runnable接口 (可以说是 将任务和线程分开了)
    3. 实现Callable接口 (利用FutureTask执行任务)

    Thread实现任务的局限性

    1. 任务逻辑写在Thread类的run方法中,有单继承的局限性
    2. 创建多线程时,每个任务有成员变量时不共享,必须加static才能做到共享

    Runnable和Callable解决了Thread的局限性

    但是Runbale相比Callable有以下的局限性

    1. 任务没有返回值
    2. 任务无法抛异常给调用方

    如下代码 几种定义线程的方式

    @Slf4j
    class T extends Thread {
        @Override
        public void run() {
            log.info("我是继承Thread的任务");
        }
    }
    @Slf4j
    class R implements Runnable {
    
        @Override
        public void run() {
            log.info("我是实现Runnable的任务");
        }
    }
    @Slf4j
    class C implements Callable<String> {
    
        @Override
        public String call() throws Exception {
            log.info("我是实现Callable的任务");
            return "success";
        }
    }
    

    创建线程的方式

    1. 通过Thread类直接创建线程
    2. 利用线程池内部创建线程

    启动线程的方式

    • 调用线程的start()方法
    // 启动继承Thread类的任务
    new T().start();
    
    // 启动继承Thread匿名内部类的任务 可用lambda优化
    Thread t = new Thread(){
      @Override
      public void run() {
        log.info("我是Thread匿名内部类的任务");
      }
    };
    
    //  启动实现Runnable接口的任务
    new Thread(new R()).start();
    
    //  启动实现Runnable匿名实现类的任务
    new Thread(new Runnable() {
        @Override
        public void run() {
            log.info("我是Runnable匿名内部类的任务");
        }
    }).start();
    
    //  启动实现Runnable的lambda简化后的任务
    new Thread(() -> log.info("我是Runnable的lambda简化后的任务")).start();
    
    // 启动实现了Callable接口的任务 结合FutureTask 可以获取线程执行的结果
    FutureTask<String> target = new FutureTask<>(new C());
    new Thread(target).start();
    log.info(target.get());
    
    

    以上各个线程相关的类的类图如下

    上下文切换

    多核cpu下,多线程是并行工作的,如果线程数多,单个核又会并发的调度线程,运行时会有上下文切换的概念

    cpu执行线程的任务时,会为线程分配时间片,以下几种情况会发生上下文切换。

    1. 线程的cpu时间片用完
    2. 垃圾回收
    3. 线程自己调用了 sleep、yield、wait、join、park、synchronized、lock 等方法

    当发生上下文切换时,操作系统会保存当前线程的状态,并恢复另一个线程的状态,jvm中有块内存地址叫程序计数器,用于记录线程执行到哪一行代码,是线程私有的。

    idea打断点的时候可以设置为Thread模式,idea的debug模式可以看出栈帧的变化

    线程的礼让-yield()&线程的优先级

    yield()方法会让运行中的线程切换到就绪状态,重新争抢cpu的时间片,争抢时是否获取到时间片看cpu的分配。

    代码如下

    // 方法的定义
    public static native void yield();
    
    Runnable r1 = () -> {
        int count = 0;
        for (;;){
           log.info("---- 1>" + count++);
        }
    };
    Runnable r2 = () -> {
        int count = 0;
        for (;;){
            Thread.yield();
            log.info("            ---- 2>" + count++);
        }
    };
    Thread t1 = new Thread(r1,"t1");
    Thread t2 = new Thread(r2,"t2");
    t1.start();
    t2.start();
    
    // 运行结果
    11:49:15.796 [t1] INFO thread.TestYield - ---- 1>129504
    11:49:15.796 [t1] INFO thread.TestYield - ---- 1>129505
    11:49:15.796 [t1] INFO thread.TestYield - ---- 1>129506
    11:49:15.796 [t1] INFO thread.TestYield - ---- 1>129507
    11:49:15.796 [t1] INFO thread.TestYield - ---- 1>129508
    11:49:15.796 [t1] INFO thread.TestYield - ---- 1>129509
    11:49:15.796 [t1] INFO thread.TestYield - ---- 1>129510
    11:49:15.796 [t1] INFO thread.TestYield - ---- 1>129511
    11:49:15.796 [t1] INFO thread.TestYield - ---- 1>129512
    11:49:15.798 [t2] INFO thread.TestYield -             ---- 2>293
    11:49:15.798 [t1] INFO thread.TestYield - ---- 1>129513
    11:49:15.798 [t1] INFO thread.TestYield - ---- 1>129514
    11:49:15.798 [t1] INFO thread.TestYield - ---- 1>129515
    11:49:15.798 [t1] INFO thread.TestYield - ---- 1>129516
    11:49:15.798 [t1] INFO thread.TestYield - ---- 1>129517
    11:49:15.798 [t1] INFO thread.TestYield - ---- 1>129518
    

    如上述结果所示,t2线程每次执行时进行了yield(),线程1执行的机会明显比线程2要多。

    线程的优先级

    ​ 线程内部用1~10的数来调整线程的优先级,默认的线程优先级为NORM_PRIORITY:5

    ​ cpu比较忙时,优先级高的线程获取更多的时间片

    ​ cpu比较闲时,优先级设置基本没用

     public final static int MIN_PRIORITY = 1;
    
     public final static int NORM_PRIORITY = 5;
    
     public final static int MAX_PRIORITY = 10;
     
     // 方法的定义
     public final void setPriority(int newPriority) {
     }
    

    cpu比较忙时

    Runnable r1 = () -> {
        int count = 0;
        for (;;){
           log.info("---- 1>" + count++);
        }
    };
    Runnable r2 = () -> {
        int count = 0;
        for (;;){
            log.info("            ---- 2>" + count++);
        }
    };
    Thread t1 = new Thread(r1,"t1");
    Thread t2 = new Thread(r2,"t2");
    t1.setPriority(Thread.NORM_PRIORITY);
    t2.setPriority(Thread.MAX_PRIORITY);
    t1.start();
    t2.start();
    
    // 可能的运行结果
    11:59:00.696 [t1] INFO thread.TestYieldPriority - ---- 1>44102
    11:59:00.696 [t2] INFO thread.TestYieldPriority -             ---- 2>135903
    11:59:00.696 [t2] INFO thread.TestYieldPriority -             ---- 2>135904
    11:59:00.696 [t2] INFO thread.TestYieldPriority -             ---- 2>135905
    11:59:00.696 [t2] INFO thread.TestYieldPriority -             ---- 2>135906
    

    cpu比较闲时

    Runnable r1 = () -> {
        int count = 0;
        for (int i = 0; i < 10; i++) {
            log.info("---- 1>" + count++);
        }
    };
    Runnable r2 = () -> {
        int count = 0;
        for (int i = 0; i < 10; i++) {
            log.info("            ---- 2>" + count++);
    
        }
    };
    Thread t1 = new Thread(r1,"t1");
    Thread t2 = new Thread(r2,"t2");
    t1.setPriority(Thread.MIN_PRIORITY);
    t2.setPriority(Thread.MAX_PRIORITY);
    t1.start();
    t2.start();
    
    // 可能的运行结果 线程1优先级低 却先运行完
    12:01:09.916 [t1] INFO thread.TestYieldPriority - ---- 1>7
    12:01:09.916 [t1] INFO thread.TestYieldPriority - ---- 1>8
    12:01:09.916 [t1] INFO thread.TestYieldPriority - ---- 1>9
    12:01:09.916 [t2] INFO thread.TestYieldPriority -             ---- 2>2
    12:01:09.916 [t2] INFO thread.TestYieldPriority -             ---- 2>3
    12:01:09.916 [t2] INFO thread.TestYieldPriority -             ---- 2>4
    12:01:09.916 [t2] INFO thread.TestYieldPriority -             ---- 2>5
    12:01:09.916 [t2] INFO thread.TestYieldPriority -             ---- 2>6
    12:01:09.916 [t2] INFO thread.TestYieldPriority -             ---- 2>7
    12:01:09.916 [t2] INFO thread.TestYieldPriority -             ---- 2>8
    12:01:09.916 [t2] INFO thread.TestYieldPriority -             ---- 2>9
    
    

    守护线程

    默认情况下,java进程需要等待所有线程都运行结束,才会结束,有一种特殊线程叫守护线程,当所有的非守护线程都结束后,即使它没有执行完,也会强制结束。

    默认的线程都是非守护线程。

    垃圾回收线程就是典型的守护线程

    // 方法的定义
    public final void setDaemon(boolean on) {
    }
    
    Thread thread = new Thread(() -> {
        while (true) {
        }
    });
    // 具体的api。设为true表示未守护线程,当主线程结束后,守护线程也结束。
    // 默认是false,当主线程结束后,thread继续运行,程序不停止
    thread.setDaemon(true);
    thread.start();
    log.info("结束");
    

    线程的阻塞

    线程的阻塞可以分为好多种,从操作系统层面和java层面阻塞的定义可能不同,但是广义上使得线程阻塞的方式有下面几种

    1. BIO阻塞,即使用了阻塞式的io流
    2. sleep(long time) 让线程休眠进入阻塞状态
    3. a.join() 调用该方法的线程进入阻塞,等待a线程执行完恢复运行
    4. sychronized或ReentrantLock 造成线程未获得锁进入阻塞状态 (同步锁章节细说)
    5. 获得锁之后调用wait()方法 也会让线程进入阻塞状态 (同步锁章节细说)
    6. LockSupport.park() 让线程进入阻塞状态 (同步锁章节细说)

    sleep()

    ​ 使线程休眠,会将运行中的线程进入阻塞状态。当休眠时间结束后,重新争抢cpu的时间片继续运行

    // 方法的定义 native方法
    public static native void sleep(long millis) throws InterruptedException; 
    
    try {
       // 休眠2秒
       // 该方法会抛出 InterruptedException异常 即休眠过程中可被中断,被中断后抛出异常
       Thread.sleep(2000);
     } catch (InterruptedException异常 e) {
     }
     try {
       // 使用TimeUnit的api可替代 Thread.sleep 
       TimeUnit.SECONDS.sleep(1);
     } catch (InterruptedException e) {
     }
    

    join()

    ​ join是指调用该方法的线程进入阻塞状态,等待某线程执行完成后恢复运行

    // 方法的定义 有重载
    // 等待线程执行完才恢复运行
    public final void join() throws InterruptedException {
    }
    // 指定join的时间。指定时间内 线程还未执行完 调用方线程不继续等待就恢复运行
    public final synchronized void join(long millis)
        throws InterruptedException{}
    
    
    Thread t = new Thread(() -> {
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        r = 10;
    });
    
    t.start();
    // 让主线程阻塞 等待t线程执行完才继续执行 
    // 去除该行,执行结果为0,加上该行 执行结果为10
    t.join();
    log.info("r:{}", r);
    
    // 运行结果
    13:09:13.892 [main] INFO thread.TestJoin - r:10
    

    线程的打断-interrupt()

    // 相关方法的定义
    public void interrupt() {
    }
    public boolean isInterrupted() {
    }
    public static boolean interrupted() {
    }
    

    打断标记:线程是否被打断,true表示被打断了,false表示没有

    isInterrupted() 获取线程的打断标记 ,调用后不会修改线程的打断标记

    interrupt()方法用于中断线程

    1. 可以打断sleep,wait,join等显式的抛出InterruptedException方法的线程,但是打断后,线程的打断标记还是false
    2. 打断正常线程 ,线程不会真正被中断,但是线程的打断标记为true

    interrupted() 获取线程的打断标记,调用后清空打断标记 即如果获取为true 调用后打断标记为false (不常用)

    interrupt实例: 有个后台监控线程不停的监控,当外界打断它时,就结束运行。代码如下

    @Slf4j
    class TwoPhaseTerminal{
        // 监控线程
        private Thread monitor;
    
        public void start(){
            monitor = new Thread(() ->{
               // 不停的监控
                while (true){
                    Thread thread = Thread.currentThread();
                 	// 判断当前线程是否被打断
                    if (thread.isInterrupted()){
                        log.info("当前线程被打断,结束运行");
                        break;
                    }
                    try {
                        Thread.sleep(1000);
                    	// 监控逻辑中被打断后,打断标记为true
                        log.info("监控");
                    } catch (InterruptedException e) {
                        // 睡眠时被打断时抛出异常 在该处捕获到 此时打断标记还是false
                        // 在调用一次中断 使得中断标记为true
                        thread.interrupt();
                    }
                }
            });
            monitor.start();
        }
    
        public void stop(){
            monitor.interrupt();
        }
    }
    

    线程的状态

    上面说了一些基本的api的使用,调用上面的方法后都会使得线程有对应的状态。

    线程的状态可从 操作系统层面分为五种状态 从java api层面分为六种状态。

    五种状态

    1. 初始状态:创建线程对象时的状态
    2. 可运行状态(就绪状态):调用start()方法后进入就绪状态,也就是准备好被cpu调度执行
    3. 运行状态:线程获取到cpu的时间片,执行run()方法的逻辑
    4. 阻塞状态: 线程被阻塞,放弃cpu的时间片,等待解除阻塞重新回到就绪状态争抢时间片
    5. 终止状态: 线程执行完成或抛出异常后的状态

    六种状态

    Thread类中的内部枚举State

    public enum State {
    	NEW,
    	RUNNABLE,
    	BLOCKED,
    	WAITING,
    	TIMED_WAITING,
    	TERMINATED;
    }
    
    1. NEW 线程对象被创建
    2. Runnable 线程调用了start()方法后进入该状态,该状态包含了三种情况
      1. 就绪状态 :等待cpu分配时间片
      2. 运行状态:进入Runnable方法执行任务
      3. 阻塞状态:BIO 执行阻塞式io流时的状态
    3. Blocked 没获取到锁时的阻塞状态(同步锁章节会细说)
    4. WAITING 调用wait()、join()等方法后的状态
    5. TIMED_WAITING 调用 sleep(time)、wait(time)、join(time)等方法后的状态
    6. TERMINATED 线程执行完成或抛出异常后的状态

    六种线程状态和方法的对应关系

    线程的相关方法总结

    主要总结Thread类中的核心方法

    方法名称是否static方法说明
    start()让线程启动,进入就绪状态,等待cpu分配时间片
    run()重写Runnable接口的方法,线程获取到cpu时间片时执行的具体逻辑
    yield()线程的礼让,使得获取到cpu时间片的线程进入就绪状态,重新争抢时间片
    sleep(time)线程休眠固定时间,进入阻塞状态,休眠时间完成后重新争抢时间片,休眠可被打断
    join()/join(time)调用线程对象的join方法,调用者线程进入阻塞,等待线程对象执行完或者到达指定时间才恢复,重新争抢时间片
    isInterrupted()获取线程的打断标记,true:被打断,false:没有被打断。调用后不会修改打断标记
    interrupt()打断线程,抛出InterruptedException异常的方法均可被打断,但是打断后不会修改打断标记,正常执行的线程被打断后会修改打断标记
    interrupted()获取线程的打断标记。调用后会清空打断标记
    stop()停止线程运行 不推荐
    suspend()挂起线程 不推荐
    resume()恢复线程运行 不推荐
    currentThread()获取当前线程

    Object中与线程相关方法

    方法名称方法说明
    wait()/wait(long timeout)获取到锁的线程进入阻塞状态
    notify()随机唤醒被wait()的一个线程
    notifyAll();唤醒被wait()的所有线程,重新争抢时间片

    同步锁

    线程安全

    • 一个程序运行多个线程本身是没有问题的
    • 问题有可能出现在多个线程访问共享资源
      • 多个线程都是读共享资源也是没有问题的
      • 当多个线程读写共享资源时,如果发生指令交错,就会出现问题

    临界区: 一段代码如果对共享资源的多线程读写操作,这段代码就被称为临界区。

    注意的是 指令交错指的是 java代码在解析成字节码文件时,java代码的一行代码在字节码中可能有多行,在线程上下文切换时就有可能交错。

    线程安全指的是多线程调用同一个对象的临界区的方法时,对象的属性值一定不会发生错误,这就是保证了线程安全。

    如下面不安全的代码

    // 对象的成员变量
    private static int count = 0;
    
    public static void main(String[] args) throws InterruptedException {
      // t1线程对变量+5000次
        Thread t1 = new Thread(() -> {
            for (int i = 0; i < 5000; i++) {
                count++;
            }
        });
      // t2线程对变量-5000次
        Thread t2 = new Thread(() -> {
            for (int i = 0; i < 5000; i++) {
                count--;
            }
        });
    
        t1.start();
        t2.start();
    
        // 让t1 t2都执行完
        t1.join();
        t2.join();
        System.out.println(count);
    }
    
    // 运行结果 
    -1399
    

    上面的代码 两个线程,一个+5000次,一个-5000次,如果线程安全,count的值应该还是0。

    但是运行很多次,每次的结果不同,且都不是0,所以是线程不安全的。

    线程安全的类一定所有的操作都线程安全吗?

    开发中经常会说到一些线程安全的类,如ConcurrentHashMap,线程安全指的是类里每一个独立的方法是线程安全的,但是方法的组合就不一定是线程安全的

    成员变量和静态变量是否线程安全?

    • 如果没有多线程共享,则线程安全
    • 如果存在多线程共享
      • 多线程只有读操作,则线程安全
      • 多线程存在写操作,写操作的代码又是临界区,则线程不安全

    局部变量是否线程安全?

    • 局部变量是线程安全的
    • 局部变量引用的对象未必是线程安全的
      • 如果该对象没有逃离该方法的作用范围,则线程安全
      • 如果该对象逃离了该方法的作用范围,比如:方法的返回值,需要考虑线程安全

    synchronized

    同步锁也叫对象锁,是锁在对象上的,不同的对象就是不同的锁。

    该关键字是用于保证线程安全的,是阻塞式的解决方案。

    让同一个时刻最多只有一个线程能持有对象锁,其他线程在想获取这个对象锁就会被阻塞,不用担心上下文切换的问题。

    注意: 不要理解为一个线程加了锁 ,进入 synchronized代码块中就会一直执行下去。如果时间片切换了,也会执行其他线程,再切换回来会紧接着执行,只是不会执行到有竞争锁的资源,因为当前线程还未释放锁。

    当一个线程执行完synchronized的代码块后 会唤醒正在等待的线程

    synchronized实际上使用对象锁保证临界区的原子性 临界区的代码是不可分割的 不会因为线程切换所打断

    基本使用

    // 加在方法上 实际是对this对象加锁
    private synchronized void a() {
    }
    
    // 同步代码块,锁对象可以是任意的,加在this上 和a()方法作用相同
    private void b(){
        synchronized (this){
    
        }
    }
    
    // 加在静态方法上 实际是对类对象加锁
    private synchronized static void c() {
    
    }
    
    // 同步代码块 实际是对类对象加锁 和c()方法作用相同
    private void d(){
        synchronized (TestSynchronized.class){
            
        }
    }
    
    // 上述b方法对应的字节码源码 其中monitorenter就是加锁的地方
     0 aload_0
     1 dup
     2 astore_1
     3 monitorenter
     4 aload_1
     5 monitorexit
     6 goto 14 (+8)
     9 astore_2
    10 aload_1
    11 monitorexit
    12 aload_2
    13 athrow
    14 return
    

    线程安全的代码

    private static int count = 0;
    
    private static Object lock = new Object();
    
    private static Object lock2 = new Object();
    
     // t1线程和t2对象都是对同一对象加锁。保证了线程安全。此段代码无论执行多少次,结果都是0
    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(() -> {
            for (int i = 0; i < 5000; i++) {
                synchronized (lock) {
                    count++;
                }
            }
        });
        Thread t2 = new Thread(() -> {
            for (int i = 0; i < 5000; i++) {
                synchronized (lock) {
                    count--;
                }
            }
        });
     
        t1.start();
        t2.start();
    
        // 让t1 t2都执行完
        t1.join();
        t2.join();
        System.out.println(count);
    }
    

    重点:加锁是加在对象上,一定要保证是同一对象,加锁才能生效

    线程通信

    wait+notify

    线程间通信可以通过共享变量+wait()&notify()来实现

    wait()将线程进入阻塞状态,notify()将线程唤醒

    当多线程竞争访问对象的同步方法时,锁对象会关联一个底层的Monitor对象(重量级锁的实现)

    如下图所示 Thread0,1先竞争到锁执行了代码后,2,3,4,5线程同时来执行临界区的代码,开始竞争锁

    1. Thread-0先获取到对象的锁,关联到monitor的owner,同步代码块内调用了锁对象的wait()方法,调用后会进入waitSet等待,Thread-1同样如此,此时Thread-0的状态为Waitting
    2. Thread2、3、4、5同时竞争,2获取到锁后,关联了monitor的owner,3、4、5只能进入EntryList中等待,此时2线程状态为 Runnable,3、4、5状态为Blocked
    3. 2执行后,唤醒entryList中的线程,3、4、5进行竞争锁,获取到的线程即会关联monitor的owner
    4. 3、4、5线程在执行过程中,调用了锁对象的notify()或notifyAll()时,会唤醒waitSet的线程,唤醒的线程进入entryList等待重新竞争锁

    注意:

    1. Blocked状态和Waitting状态都是阻塞状态

    2. Blocked线程会在owner线程释放锁时唤醒

    3. wait和notify使用场景是必须要有同步,且必须获得对象的锁才能调用,使用锁对象去调用,否则会抛异常

    • wait() 释放锁 进入 waitSet 可传入时间,如果指定时间内未被唤醒 则自动唤醒
    • notify()随机唤醒一个waitSet里的线程
    • notifyAll()唤醒waitSet中所有的线程
    static final Object lock = new Object();
    new Thread(() -> {
        synchronized (lock) {
            log.info("开始执行");
            try {
              	// 同步代码内部才能调用
                lock.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            log.info("继续执行核心逻辑");
        }
    }, "t1").start();
    
    new Thread(() -> {
        synchronized (lock) {
            log.info("开始执行");
            try {
                lock.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            log.info("继续执行核心逻辑");
        }
    }, "t2").start();
    
    try {
        Thread.sleep(2000);
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
    log.info("开始唤醒");
    
    synchronized (lock) {
      // 同步代码内部才能调用
        lock.notifyAll();
    }
    // 执行结果
    14:29:47.138 [t1] INFO TestWaitNotify - 开始执行
    14:29:47.141 [t2] INFO TestWaitNotify - 开始执行
    14:29:49.136 [main] INFO TestWaitNotify - 开始唤醒
    14:29:49.136 [t2] INFO TestWaitNotify - 继续执行核心逻辑
    14:29:49.136 [t1] INFO TestWaitNotify - 继续执行核心逻辑
    

    wait 和 sleep的区别?

    二者都会让线程进入阻塞状态,有以下区别

    1. wait是Object的方法 sleep是Thread的方法
    2. wait会立即释放锁 sleep不会释放锁
    3. wait后线程的状态是Watting sleep后线程的状态为 Time_Waiting

    park&unpark

    LockSupport是juc下的工具类,提供了park和unpark方法,可以实现线程通信

    与wait和notity相比的不同点

    1. wait 和notify需要获取对象锁 park unpark不要
    2. unpark 可以指定唤醒线程 notify随机唤醒
    3. park和unpark的顺序可以先unpark wait和notify的顺序不能颠倒

    生产者消费者模型

    指的是有生产者来生产数据,消费者来消费数据,生产者生产满了就不生产了,通知消费者取,等消费了再进行生产。
    

    消费者消费不到了就不消费了,通知生产者生产,生产到了再继续消费。

      public static void main(String[] args) throws InterruptedException {
            MessageQueue queue = new MessageQueue(2);
    		
    		// 三个生产者向队列里存值
            for (int i = 0; i < 3; i++) {
                int id = i;
                new Thread(() -> {
                    queue.put(new Message(id, "值" + id));
                }, "生产者" + i).start();
            }
    
            Thread.sleep(1000);
    
    		// 一个消费者不停的从队列里取值
            new Thread(() -> {
                while (true) {
                    queue.take();
                }
            }, "消费者").start();
    
        }
    }
    
    
    // 消息队列被生产者和消费者持有
    class MessageQueue {
        private LinkedList<Message> list = new LinkedList<>();
    
        // 容量
        private int capacity;
    
        public MessageQueue(int capacity) {
            this.capacity = capacity;
        }
    
        /**
         * 生产
         */
        public void put(Message message) {
            synchronized (list) {
                while (list.size() == capacity) {
                    log.info("队列已满,生产者等待");
                    try {
                        list.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                list.addLast(message);
                log.info("生产消息:{}", message);
                // 生产后通知消费者
                list.notifyAll();
            }
        }
    
        public Message take() {
            synchronized (list) {
                while (list.isEmpty()) {
                    log.info("队列已空,消费者等待");
                    try {
                        list.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                Message message = list.removeFirst();
                log.info("消费消息:{}", message);
                // 消费后通知生产者
                list.notifyAll();
                return message;
            }
        }
    
    
    }
     // 消息
    class Message {
    
        private int id;
    
        private Object value;
    }
    

    同步锁案例

    为了更形象的表达加同步锁的概念,这里举一个生活中的例子,尽量把以上的概念具体化出来。

    这里举一个每个人非常感兴趣的一件东西。 钱!!!(马老师除外)。

    现实中,我们去银行门口的自动取款机取钱,取款机的钱就是共享变量,为了保障安全,不可能两个陌生人同时进入同一个取款机内取钱,所以只能一个人进入取钱,然后锁上取款机的门,其他人只能在取款机门口等待。

    取款机有多个,里面的钱互不影响,锁也有多个(多个对象锁),取钱人在多个取款机里同时取钱也没有安全问题。

    假如每个取钱的陌生人都是线程,当取钱人进入取款机锁了门后(线程获得锁),取到钱后出门(线程释放锁),下一个人竞争到锁来取钱。

    假设工作人员也是一个线程,如果取钱人进入后发现取款机钱不足了,这时通知工作人员来向取款机里加钱(调用notifyAll方法),取钱人暂停取钱,进入银行大堂阻塞等待(调用wait方法)。

    银行大堂里的工作人员和取钱人都被唤醒,重新竞争锁,进入后如果是取钱人,由于取款机没钱,还得进入银行大堂等待。

    当工作人员获得取款机的锁进入后,加了钱后会通知大厅里的人来取钱(调用notifyAll方法)。自己暂停加钱,进入银行大堂等待唤醒加钱(调用wait方法)。

    这时大堂里等待的人都来竞争锁,谁获取到谁进入继续取钱。

    和现实中不同的就是这里没有排队的概念,谁抢到锁谁进去取。

    ReentrantLock

    可重入锁 : 一个线程获取到对象的锁后,执行方法内部在需要获取锁的时候是可以获取到的。如以下代码

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

    synchronized 也是可重入锁,ReentrantLock有以下优点

    1. 支持获取锁的超时时间
    2. 获取锁时可被打断
    3. 可设为公平锁
    4. 可以有不同的条件变量,即有多个waitSet,可以指定唤醒

    api

    // 默认非公平锁,参数传true 表示未公平锁
    ReentrantLock lock = new ReentrantLock(false);
    // 尝试获取锁
    lock()
    // 释放锁 应放在finally块中 必须执行到
    unlock()
    try {
        // 获取锁时可被打断,阻塞中的线程可被打断
        LOCK.lockInterruptibly();
    } catch (InterruptedException e) {
        return;
    }
    // 尝试获取锁 获取不到就返回false
    LOCK.tryLock()
    // 支持超时时间 一段时间没获取到就返回false
    tryLock(long timeout, TimeUnit unit)
    // 指定条件变量 休息室 一个锁可以创建多个休息室
    Condition waitSet = ROOM.newCondition();
    // 释放锁  进入waitSet等待 释放后其他线程可以抢锁
    yanWaitSet.await()
    // 唤醒具体休息室的线程 唤醒后 重写竞争锁
    yanWaitSet.signal()
    
    

    实例:一个线程输出a,一个线程输出b,一个线程输出c,abc按照顺序输出,连续输出5次

    这个考的就是线程的通信,利用 wait()/notify()和控制变量可以实现,此处使用ReentrantLock即可实现该功能。

      public static void main(String[] args) {
            AwaitSignal awaitSignal = new AwaitSignal(5);
            // 构建三个条件变量
            Condition a = awaitSignal.newCondition();
            Condition b = awaitSignal.newCondition();
            Condition c = awaitSignal.newCondition();
            // 开启三个线程
            new Thread(() -> {
                awaitSignal.print("a", a, b);
            }).start();
    
            new Thread(() -> {
                awaitSignal.print("b", b, c);
            }).start();
    
            new Thread(() -> {
                awaitSignal.print("c", c, a);
            }).start();
    
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            awaitSignal.lock();
            try {
                // 先唤醒a
                a.signal();
            } finally {
                awaitSignal.unlock();
            }
        }
    
    
    }
    
    class AwaitSignal extends ReentrantLock {
    
        // 循环次数
        private int loopNumber;
    
        public AwaitSignal(int loopNumber) {
            this.loopNumber = loopNumber;
        }
    
        /**
         * @param print   输出的字符
         * @param current 当前条件变量
         * @param next    下一个条件变量
         */
        public void print(String print, Condition current, Condition next) {
    
            for (int i = 0; i < loopNumber; i++) {
                lock();
                try {
                    try {
                        // 获取锁之后等待
                        current.await();
                        System.out.print(print);
                    } catch (InterruptedException e) {
                    }
                    next.signal();
                } finally {
                    unlock();
                }
            }
        }
    

    死锁

    说到死锁,先举个例子,

    下面是代码实现

    static Beer beer = new Beer();
    static Story story = new Story();
    
    public static void main(String[] args) {
        new Thread(() ->{
            synchronized (beer){
                log.info("我有酒,给我故事");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                synchronized (story){
                    log.info("小王开始喝酒讲故事");
                }
            }
        },"小王").start();
    
        new Thread(() ->{
            synchronized (story){
                log.info("我有故事,给我酒");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                synchronized (beer){
                    log.info("老王开始喝酒讲故事");
                }
            }
        },"老王").start();
    }
    class Beer {
    }
    
    class Story{
    }
    

    死锁导致程序无法正常运行下去

    检测工具可以检查到死锁信息

    java内存模型(JMM)

    jmm 体现在以下三个方面

    1. 原子性 保证指令不会受到上下文切换的影响
    2. 可见性 保证指令不会受到cpu缓存的影响
    3. 有序性 保证指令不会受并行优化的影响

    可见性

    停不下来的程序

    static boolean run = true;
    
    public static void main(String[] args) throws InterruptedException {
        Thread t = new Thread(() -> {
            while (run) {
                // ....
            }
        });
        t.start();
        Thread.sleep(1000);
       // 线程t不会如预想的停下来
        run = false; 
    }
    

    如上图所示,线程有自己的工作缓存,当主线程修改了变量并同步到主内存时,t线程没有读取到,所以程序停不下来

    有序性

    JVM在不影响程序正确性的情况下可能会调整语句的执行顺序,该情况也称为 指令重排序

      static int i;
      static int j;
    // 在某个线程内执行如下赋值操作
            i = ...;
            j = ...;
      有可能将j先赋值
    

    原子性

    原子性大家应该比较熟悉,上述同步锁的synchronized代码块就是保证了原子性,就是一段代码是一个整体,原子性保证了线程安全,不会受到上下文切换的影响。

    volatile

    该关键字解决了可见性和有序性,volatile通过内存屏障来实现的

    • 写屏障

    会在对象写操作之后加写屏障,会对写屏障的之前的数据都同步到主存,并且保证写屏障的执行顺序在写屏障之前

    • 读屏障

    会在对象读操作之前加读屏障,会在读屏障之后的语句都从主存读,并保证读屏障之后的代码执行在读屏障之后

    注意: volatile不能解决原子性,即不能通过该关键字实现线程安全。

    volatile应用场景:一个线程读取变量,另外的线程操作变量,加了该关键字后保证写变量后,读变量的线程可以及时感知。

    无锁-cas

    cas (compare and swap) 比较并交换

    为变量赋值时,从内存中读取到的值v,获取到要交换的新值n,执行 compareAndSwap()方法时,比较v和当前内存中的值是否一致,如果一致则将n和v交换,如果不一致,则自旋重试。

    cas底层是cpu层面的,即不使用同步锁也可以保证操作的原子性。

    private AtomicInteger balance;
    
    // 模拟cas的具体操作
    @Override
    public void withdraw(Integer amount) {
        while (true) {
            // 获取当前值
            int pre = balance.get();
            // 进行操作后得到新值
            int next = pre - amount;
            // 比较并设置成功 则中断 否则自旋重试
            if (balance.compareAndSet(pre, next)) {
                break;
            }
        }
    }
    

    无锁的效率是要高于之前的锁的,由于无锁不会涉及线程的上下文切换

    cas是乐观锁的思想,sychronized是悲观锁的思想

    cas适合很少有线程竞争的场景,如果竞争很强,重试经常发生,反而降低效率

    juc并发包下包含了实现了cas的原子类

    1. AtomicInteger/AtomicBoolean/AtomicLong
    2. AtomicIntegerArray/AtomicLongArray/AtomicReferenceArray
    3. AtomicReference/AtomicStampedReference/AtomicMarkableReference

    AtomicInteger

    常用api

    new AtomicInteger(balance)
    get()
    compareAndSet(pre, next)
    //        i.incrementAndGet() ++i
    //        i.decrementAndGet() --i
    //        i.getAndIncrement() i++
    //        i.getAndDecrement() ++i
     i.addAndGet()
      // 传入函数式接口 修改i
      int getAndUpdate(IntUnaryOperator updateFunction)
      // cas 的核心方法
      compareAndSet(int expect, int update)
    

    ABA问题

    cas存在ABA问题,即比较并交换时,如果原值为A,有其他线程将其修改为B,在有其他线程将其修改为A。

    此时实际发生过交换,但是比较和交换由于值没改变可以交换成功

    解决方式

    AtomicStampedReference/AtomicMarkableReference

    上面两个类解决ABA问题,原理就是为对象增加版本号,每次修改时增加版本号,就可以避免ABA问题

    或者增加个布尔变量标识,修改后调整布尔变量值,也可以避免ABA问题

    线程池

    线程池的介绍

    线程池是java并发最重要的一个知识点,也是难点,是实际应用最广泛的。

    线程的资源很宝贵,不可能无限的创建,必须要有管理线程的工具,线程池就是一种管理线程的工具,java开发中经常有池化的思想,如 数据库连接池、Redis连接池等。

    预先创建好一些线程,任务提交时直接执行,既可以节约创建线程的时间,又可以控制线程的数量。

    线程池的好处

    1. 降低资源消耗,通过池化思想,减少创建线程和销毁线程的消耗,控制资源
    2. 提高响应速度,任务到达时,无需创建线程即可运行
    3. 提供更多更强大的功能,可扩展性高

    线程池的构造方法

    public ThreadPoolExecutor(int corePoolSize,
                              int maximumPoolSize,
                              long keepAliveTime,
                              TimeUnit unit,
                              BlockingQueue<Runnable> workQueue,
                              ThreadFactory threadFactory,
                              RejectedExecutionHandler handler) {
     
    }
    

    构造器参数的意义

    参数名参数意义
    corePoolSize核心线程数
    maximumPoolSize最大线程数
    keepAliveTime救急线程的空闲时间
    unit救急线程的空闲时间单位
    workQueue阻塞队列
    threadFactory创建线程的工厂,主要定义线程名
    handler拒绝策略

    线程池案例

    下面 我们通过一个实例来理解线程池的参数以及线程池的接收任务的过程

    如上图 银行办理业务。

    1. 客户到银行时,开启柜台进行办理,柜台相当于线程,客户相当于任务,有两个是常开的柜台,三个是临时柜台。2就是核心线程数,5是最大线程数。即有两个核心线程
    2. 当柜台开到第二个后,都还在处理业务。客户再来就到排队大厅排队。排队大厅只有三个座位。
    3. 排队大厅坐满时,再来客户就继续开柜台处理,目前最大有三个临时柜台,也就是三个救急线程
    4. 此时再来客户,就无法正常为其 提供业务,采用拒绝策略来处理它们
    5. 当柜台处理完业务,就会从排队大厅取任务,当柜台隔一段空闲时间都取不到任务时,如果当前线程数大于核心线程数时,就会回收线程。即撤销该柜台。

    线程池的状态

    线程池通过一个int变量的高3位来表示线程池的状态,低29位来存储线程池的数量

    状态名称高三位接收新任务处理阻塞队列任务说明
    Running111YY正常接收任务,正常处理任务
    Shutdown000NY不会接收任务,会执行完正在执行的任务,也会处理阻塞队列里的任务
    stop001NN不会接收任务,会中断正在执行的任务,会放弃处理阻塞队列里的任务
    Tidying010NN任务全部执行完毕,当前活动线程是0,即将进入终结
    Termitted011NN终结状态
    // 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;
    

    线程池的主要流程

    线程池创建、接收任务、执行任务、回收线程的步骤

    1. 创建线程池后,线程池的状态是Running,该状态下才能有下面的步骤
    2. 提交任务时,线程池会创建线程去处理任务
    3. 当线程池的工作线程数达到corePoolSize时,继续提交任务会进入阻塞队列
    4. 当阻塞队列装满时,继续提交任务,会创建救急线程来处理
    5. 当线程池中的工作线程数达到maximumPoolSize时,会执行拒绝策略
    6. 当线程取任务的时间达到keepAliveTime还没有取到任务,工作线程数大于corePoolSize时,会回收该线程

    注意: 不是刚创建的线程是核心线程,后面创建的线程是非核心线程,线程是没有核心非核心的概念的,这是我长期以来的误解。

    拒绝策略

    1. 调用者抛出RejectedExecutionException (默认策略)
    2. 让调用者运行任务
    3. 丢弃此次任务
    4. 丢弃阻塞队列中最早的任务,加入该任务

    提交任务的方法

    // 执行Runnable
    public void execute(Runnable command) {
        if (command == null)
            throw new NullPointerException();
        int c = ctl.get();
        if (workerCountOf(c) < corePoolSize) {
            if (addWorker(command, true))
                return;
            c = ctl.get();
        }
        if (isRunning(c) && workQueue.offer(command)) {
            int recheck = ctl.get();
            if (! isRunning(recheck) && remove(command))
                reject(command);
            else if (workerCountOf(recheck) == 0)
                addWorker(null, false);
        }
        else if (!addWorker(command, false))
            reject(command);
    }
    // 提交Callable
    public <T> Future<T> submit(Callable<T> task) {
      if (task == null) throw new NullPointerException();
       // 内部构建FutureTask
      RunnableFuture<T> ftask = newTaskFor(task);
      execute(ftask);
      return ftask;
    }
    // 提交Runnable,指定返回值
    public Future<?> submit(Runnable task) {
      if (task == null) throw new NullPointerException();
      // 内部构建FutureTask
      RunnableFuture<Void> ftask = newTaskFor(task, null);
      execute(ftask);
      return ftask;
    } 
    //  提交Runnable,指定返回值
    public <T> Future<T> submit(Runnable task, T result) {
      if (task == null) throw new NullPointerException();
       // 内部构建FutureTask
      RunnableFuture<T> ftask = newTaskFor(task, result);
      execute(ftask);
      return ftask;
    }
    
    protected <T> RunnableFuture<T> newTaskFor(Runnable runnable, T value) {
            return new FutureTask<T>(runnable, value);
    }
    

    Execetors创建线程池

    注意: 下面几种方式都不推荐使用

    1.newFixedThreadPool

    public static ExecutorService newFixedThreadPool(int nThreads) {
        return new ThreadPoolExecutor(nThreads, nThreads,
                                      0L, TimeUnit.MILLISECONDS,
                                      new LinkedBlockingQueue<Runnable>());
    }
    
    • 核心线程数 = 最大线程数 没有救急线程
    • 阻塞队列无界 可能导致oom

    2.newCachedThreadPool

    public static ExecutorService newCachedThreadPool() {
        return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
                                      60L, TimeUnit.SECONDS,
                                      new SynchronousQueue<Runnable>());
    }
    
    • 核心线程数是0,最大线程数无限制 ,救急线程60秒回收
    • 队列采用 SynchronousQueue 实现 没有容量,即放入队列后没有线程来取就放不进去
    • 可能导致线程数过多,cpu负担太大

    3.newSingleThreadExecutor

    public static ExecutorService newSingleThreadExecutor() {
        return new FinalizableDelegatedExecutorService
            (new ThreadPoolExecutor(1, 1,
                                    0L, TimeUnit.MILLISECONDS,
                                    new LinkedBlockingQueue<Runnable>()));
    }
    
    • 核心线程数和最大线程数都是1,没有救急线程,无界队列 可以不停的接收任务
    • 将任务串行化 一个个执行, 使用包装类是为了屏蔽修改线程池的一些参数 比如 corePoolSize
    • 如果某线程抛出异常了,会重新创建一个线程继续执行
    • 可能造成oom

    4.newScheduledThreadPool

    public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize) {
        return new ScheduledThreadPoolExecutor(corePoolSize);
    }
    
    • 任务调度的线程池 可以指定延迟时间调用,可以指定隔一段时间调用

    线程池的关闭

    shutdown()

    会让线程池状态为shutdown,不能接收任务,但是会将工作线程和阻塞队列里的任务执行完 相当于优雅关闭

    public void shutdown() {
        final ReentrantLock mainLock = this.mainLock;
        mainLock.lock();
        try {
            checkShutdownAccess();
            advanceRunState(SHUTDOWN);
            interruptIdleWorkers();
            onShutdown(); // hook for ScheduledThreadPoolExecutor
        } finally {
            mainLock.unlock();
        }
        tryTerminate();
    }
    

    shutdownNow()

    会让线程池状态为stop, 不能接收任务,会立即中断执行中的工作线程,并且不会执行阻塞队列里的任务, 会返回阻塞队列的任务列表

    public List<Runnable> shutdownNow() {
        List<Runnable> tasks;
        final ReentrantLock mainLock = this.mainLock;
        mainLock.lock();
        try {
            checkShutdownAccess();
            advanceRunState(STOP);
            interruptWorkers();
            tasks = drainQueue();
        } finally {
            mainLock.unlock();
        }
        tryTerminate();
        return tasks;
    }
    

    线程池的正确使用姿势

    线程池难就难在参数的配置,有一套理论配置参数

    cpu密集型 : 指的是程序主要发生cpu的运算

    ​ 核心线程数: CPU核心数+1

    IO密集型: 远程调用RPC,操作数据库等,不需要使用cpu进行大量的运算。 大多数应用的场景

    ​ 核心线程数=核数*cpu期望利用率 *总时间/cpu运算时间

    但是基于以上理论还是很难去配置,因为cpu运算时间不好估算

    实际配置大小可参考下表

    cpu密集型io密集型
    线程数数量核数<=x<=核数*2核心数*50<=x<=核心数 *100
    队列长度y>=1001<=y<=10

    1.线程池参数通过分布式配置,修改配置无需重启应用

    线程池参数是根据线上的请求数变化而变化的,最好的方式是 核心线程数、最大线程数 队列大小都是可配置的

    主要配置 corePoolSize maxPoolSize queueSize

    java提供了可方法覆盖参数,线程池内部会处理好参数 进行平滑的修改

    public void setCorePoolSize(int corePoolSize) {
    }
    

    2.增加线程池的监控

    3.io密集型可调整为先新增任务到最大线程数后再将任务放到阻塞队列

    代码 主要可重写阻塞队列 加入任务的方法

    public boolean offer(Runnable runnable) {
        if (executor == null) {
            throw new RejectedExecutionException("The task queue does not have executor!");
        }
    
        final ReentrantLock lock = this.lock;
        lock.lock();
        try {
            int currentPoolThreadSize = executor.getPoolSize();
           
            // 如果提交任务数小于当前创建的线程数, 说明还有空闲线程,
            if (executor.getTaskCount() < currentPoolThreadSize) {
                // 将任务放入队列中,让线程去处理任务
                return super.offer(runnable);
            }
    		// 核心改动
            // 如果当前线程数小于最大线程数,则返回 false ,让线程池去创建新的线程
            if (currentPoolThreadSize < executor.getMaximumPoolSize()) {
                return false;
            }
    
            // 否则,就将任务放入队列中
            return super.offer(runnable);
        } finally {
            lock.unlock();
        }
    }
    

    3.拒绝策略 建议使用tomcat的拒绝策略(给一次机会)

    // tomcat的源码
    @Override
    public void execute(Runnable command) {
        if ( executor != null ) {
            try {
                executor.execute(command);
            } catch (RejectedExecutionException rx) {
                // 捕获到异常后 在从队列获取,相当于重试1取不到任务 在执行拒绝任务
                if ( !( (TaskQueue) executor.getQueue()).force(command) ) throw new RejectedExecutionException("Work queue full.");
            }
        } else throw new IllegalStateException("StandardThreadPool not started.");
    }
    

    建议修改从队列取任务的方式: 增加超时时间,超时1分钟取不到在进行返回

    public boolean offer(E e, long timeout, TimeUnit unit){}
    

    结语

    工作三四年了,还没有正式的写过博客,自学一直都是通过笔记的方式积累,最近重新学了一下java多线程,想着周末把这部分内容认真的写篇博客分享出去。

    文章篇幅较长,给看到这里的小伙伴点个大大的赞!由于作者水平有限,加之第一次写博客,文章中难免会有错误之处,欢迎小伙伴们反馈指正。

    如果觉得文章对你有帮助,麻烦 点赞、评论、转发、在看 走起

    你的支持是我最大的动力!!!

    展开全文
  • java 多线程 面试题整理(更新......)

    千次阅读 2021-11-30 16:23:06
    3、什么是同步执行和异步执行4、Java中实现多线程有几种方法?(较难)(1)继承Thread类(2)实现runable接口(3)实现Callable接口(创建FutureTask(Callable)对象)5、Future接口,Callable接口,FutureTask实现类的...

    一、基础知识​​​​​​1、什么是线程和进程?什么是进程?进程的特点:什么是线程?区别与联系?2、什么是并行与并发?3、什么是同步执行和异步执行4、Java中实现多线程有几种方法?(较难)(1)继承Thread类(2)实现runable接口(3)实现Callable接口(创建FutureTask(Callable)对象)5、Future接口,Callable接口,FutureTask实现类的关系6、什么是Callable和Future?7、什么是线程的上下文切换?8、Thread类中的start()和run()方法有什么区别?9、Java中interrupted和isInterruptedd方法的区别?10、为何stop()和suspend()方法不推荐使用10、如何停止一个正在运行的线程?(重要)i:捕捉打断标记并且直接returnii:捕捉打断标记,并且抛出异常终止程序iii:当线程处于sleep,park,join,wait的时候需要在catch块处理异常时自行设置打断标记11、sleep和yield的区别?状态的区别:调度的区别:12、sleep,yield为什么是静态方法(重要)13、有三个线程T1,T2,T3,如何保证顺序执行?14、在 java中守护线程和本地线程区别15、sleep和wait的区别?16、线程创建到结束的几种状态?17、对线程优先级的理解?18、什么是后台线程?19、sleep,yiled,wait,join 对比21、Thread.sleep(0)有什么作用?二、锁知识20、什么是线程安全?21、什么是竞态条件?22、什么是临界区?22、什么是不可变对象,它对写并发应用有什么帮助?(重要)23、synchronized关键字最主要的三种使用方式1、修饰实例方法2、修饰静态方法3、修饰代码块24、讲讲你对synchronized的认识?1、无锁状态2、偏向锁状态偏向锁锁撤销:偏向锁存在的意义:偏向锁撤销的情况批量重偏向:批量撤销偏向锁3、轻量级锁锁重入:轻量级锁CAS4、自旋锁25、什么是重量级锁?为什么消耗很大?26、自旋锁的优缺点27、线程同步和互斥有几种实现方法,都是什么?(重要)28、wait的基本使用方法29、wait的相关问题(1)notify()和notifyAll()有什么区别?(2)为什么wait, notify和notifyAll这些方法不在thread类里面?(重要,记忆)(3)为什么wait和notify方法要在同步块中调用?(4)什么是阻塞队列?阻塞队列的实现原理是什么?如何使用阻塞队列来实现生产者-消费者模型?(5) join方法实现原理(6)如何实现线程间通信31、park和unpark(1)基本使用(2)先调用park再调用unpark(3)先调用unpark再调用park32、park和wait的区别33、park,wait,sleep,yield,join方法的区别34、什么是死锁,死锁发生的条件(重要)死锁的定义:死锁的四个条件:(重要,记忆)怎么预防死锁问题?怎么避免死锁问题怎么检测和解除死锁35、什么是活锁避免活锁的方法活锁与死锁的区别?36、什么是饥饿?37、什么是可重入锁?38、Reentrantlock39、ReentrantLock和Synchronized的相同点和区别(重要)40、ReentrantLock的实现原理41、lock、tryLock和lockInterruptibly的差別42Condition和Object类锁方法区别43、公平锁与非公平锁三、无锁机制(CAS,原子类)44、什么是java内存模型?45、什么是volitile?作用是什么46、什么是原子性?47、有序性48、volatile怎么保证可见性和有序性的?可见性有序性50、单例模式的双检锁是什么?51、synchronized 和 volatile 的区别是什么?为啥synchronized无法禁止指令重排,但可以保证有序性?30、乐观锁和悲观锁的理解及如何实现,有哪些实现方式(重要)52、CAS53、synchronized与CAS的区别(重要)54、CAS的缺点(重要)1) CPU开销过大2) 不能保证多个变量的原子性如何解决CAS只能保证一个变量的原子性操作问题?3)ABA问题49、volatile 变量和 atomic 变量有什么不同?50、什么是原子操作?在 Java Concurrency API 中有哪些原 子类(atomic classes)?

    一、基础知识

    1、什么是线程和进程?

    什么是进程?

    程序由指令和数据组成,但是这些指令要运行,数据要读写,就必须将指令加载到cpu,数据加载至内存。在指令运行过程中还需要用到磁盘,网络等设备,进程就是用来加载指令管理内存管理IO的。

    进程是指在系统中正在运行的一个应用程序,程序一旦运行就是进程。比如.exe文件运行,进程就可以视为程序的一个实例,大部分程序都可以运行多个实例进程

    总结:进程是把指令加载给CPU,数据加载到内存并执行的程序实例

    进程的特点:

    1、每个进程可以包括多个线程

    2、每个进程都有自己独立的内存空间,而其内部的线程可以共享这些内存空间,进程上下文切换的开销比较大,不同进程之间不共享内存

    什么是线程?

    线程是进程的一个子集,一个线程就是一个指令流的执行,线程按照一定的顺序把这些指令流交给CPU执行,就是线程的执行

    区别与联系?

    线程是进程的子集,一个进程可以有很多线程,每条线程并行执行不同的任务。

    不同的进程使用不同的内存空间,而线程共享同一进程的内存空间。别把它和栈内存搞混,每个线程都拥有单独的栈内存用来存储本地数据。

    线程作为操作系统能够进行运算调度的最小单位,进程作为资源分配的最小单位。

    线程更轻量,线程上下文切换成本一般上要比进程上下文切换低

    2、什么是并行与并发?

    并发:操作系统的任务调度器调度多个线程轮流使用某个CPU的操作(CPU的时间片为15ms),这个过程中会发生线程的上下文切换

    1583408729416

    并行:对于多核CPU来讲,每个核(core) 都可以调度运行线程,这时候线程可以是并行的,不同的线程同时使用不同的cpu在执行。

    1583408812725

    一般来说对于单核CPU的机器,线程执行是并发的,对于多核CPU来讲,线程执行是既有并行也有并发的

    3、什么是同步执行和异步执行

    以调用方的角度讲,如果需要等待结果返回才能继续运行的话就是同步,如果不需要等待就是异步

    也就是说一个程序需要运行完了有结果了才能进行下一个线程,这样这个程序就会堵塞其他的程序,这就是同步,异步就是这个程序在运行的时候我仍然可以不管他运行别的程序

    多线程可以将同步程序变为异步的,从而增加系统资源的利用率

    比如说读取磁盘文件时,假设读取操作花费了5秒,如果没有线程的调度机制,这么cpu只能等5秒,啥都不能做。

    4、Java中实现多线程有几种方法?(较难)

    Thread的构造方法参数可以传入Runnable接口和FutureTask对象

    Runnable缺少的一项功能是,当线程终止时(即run()完成时),我们无法使线程返回结果。为了支持此功能,Java中提供了Callable接口。

    (1)继承Thread类

    (1)定义Thread类的子类,并重写该类的run方法,该run方法的方法体就代表了线程要完成的任务。因此把run()方法称为执行体。

    (2)创建Thread子类的实例,即创建了线程对象。

    (3)调用线程对象的start()方法来启动该线程。

    public class MyThread extends Thread {
        public void run() {
            System.out.println("MyThread.run()");
        }
    }
    MyThread myThread1 = new MyThread();
    myThread1.start();

    (2)实现runable接口

    (1)定义runnable接口的实现类,并重写该接口的run()方法,该run()方法的方法体同样是该线程的线程执行体。

    (2)创建 Runnable实现类的实例,并依此实例作为Thread的target来创建Thread对象,该Thread对象才是真正的线程对象。

    (3)调用线程对象的start()方法来启动该线程

    public class MyThread extends OtherClass implements Runnable {
        public void run() {
            System.out.println("MyThread.run()");
        }
    }
    ​
    ​

    启动 MyThread,需要首先实例化一个 Thread,并传入自己的 MyThread 实例:

     MyThread myThread = new MyThread(); 
     Thread thread = new Thread(myThread); 
     thread.start(); 

    //事实上,当传入一个 Runnable target 参数给 Thread 后, Thread 的 run()方法就会调用 target.run() public void run() { if (target != null) { target.run(); } }

    (3)实现Callable接口(创建FutureTask(Callable)对象)

    1)创建Callable接口的实现类,并实现call()方法,该call()方法将作为线程执行体,并且有返回值。

    (2)创建Callable实现类的实例,使用FutureTask类来包装Callable对象,该FutureTask对象封装了该Callable对象的call()方法的返回值。

    (3)使用FutureTask对象作为Thread对象的target创建并启动新线程。

    (4)调用FutureTask对象的get()方法来获得子线程执行结束后的返回值

    public class SomeCallable<V> extends OtherClass implements Callable<V> {
    ​
        @Override
        public V call() throws Exception {
            // TODO Auto-generated method stub
            return null;
        }
    ​
    }
    Callable<V> oneCallable = new SomeCallable<V>();   
    //由Callable<Integer>创建一个FutureTask<Integer>对象:   
    FutureTask<V> oneTask = new FutureTask<V>(oneCallable);   
    //注释:FutureTask<Integer>是一个包装器,它通过接受Callable<Integer>来创建,它同时实现了Future和Runnable接口。 
      //由FutureTask<Integer>创建一个Thread对象:   
    Thread oneThread = new Thread(oneTask);   
    oneThread.start();   
    //至此,一个线程就创建完成了。

    5、Future接口,Callable接口,FutureTask实现类的关系

    Callable接口中就一个抽象方法call(),有返回值

    Future接口中定义了关于线程状态的方法,比如打断线程执行的cancel方法,判断该线程是否被取消的isCancelled()方法,返回线程是否执行完的isDone方法,以及重要的get方法获取返回值

    FutureTask实现类实现了Future接口,并且有构造函数,参数是传入一个Callable接口, 

    以此获得返回值

    其中Future接口的get方法是阻塞方法,没有得到get的值会阻塞主线程

    package TestFutureTask;
    ​
    import java.util.concurrent.ExecutionException;
    import java.util.concurrent.FutureTask;
    ​
    public class TestMain {
        public static void main(String[] args) {
            FutureTask<Integer> futureTask = new FutureTask<Integer>(()->{
                System.out.println("futureTask开始了");
                Thread.sleep(10000);
                return 100;
            }
            );
            Thread thread = new Thread(futureTask, "thread1");
            thread.start();
            try {
                System.out.println(futureTask.get());//会阻塞主线程使得主线程不能立刻输出语句
            } catch (InterruptedException e) {
                e.printStackTrace();
            } catch (ExecutionException e) {
                e.printStackTrace();
            }
            System.out.println("主线程运行!");
        }
    }

    6、什么是Callable和Future?

    Callable 接口类似于 Runnable,从名字就可以看出来了,但是 Runnable 不会返 回结果,并且无法抛出返回结果的异常,而 Callable 功能更强大一些,被线程执 行后,可以返回值,这个返回值可以被 Future 拿到,也就是说,Future 可以拿到 异步执行任务的返回值。可以认为是带有回调的 Runnable。Future 接口表示异步任务,是还没有完成的任务给出的未来结果。所以说 Callable用于产生结果,Future 用于获取结果

    Futuretask类通过传入一个Callable接口创建一个有返回值的线程任务,并且其实现了Future接口,可以通过其get方法拿到这个结果

    7、什么是线程的上下文切换?

    多线程的上下文切换是指 CPU 控制权由一个已经正在运行的线程切换到另外一个就绪并等待获取 CPU 执行权的线程的过程。

    可能有以下原因:

    • 线程的 cpu 时间片用完(每个线程轮流执行,看前面并发的概念)

    • 垃圾回收

    • 有更高优先级的线程需要运行

    • 线程自己调用了 sleepyieldwaitjoinparksynchronizedlock 等方法

    当 Context Switch 发生时,需要由操作系统保存当前线程的状态,并恢复另一个线程的状态

    8、Thread类中的start()和run()方法有什么区别?

    start方法是线程从就绪变为启动状态的方法,而run方法是线程启动之后需要执行的代码,如果直接调用run方法,相当于使用thread对象调用它的一个普通方法而已,调用者是线程对象,并且是在主线程中执行的。

    而start方法可以使得线程启动,之后再调用run方法便是在该线程中执行

    9、Java中interrupted和isInterruptedd方法的区别?

    一个清除一个不清除中断标记

    interrupted() 不仅返回当前Thread的中断状态,而且会清除当前Thread的中断状态**。所以如果当前Thread.interrupted()返回中断true,紧接着再call一次interrupted() 会返回“非中断false”,因为中断状态在第一次call的时候清除了。(源码中进行了操作)静态方法

    isInterrupted() 也会返回当前Thread的中断状态,但是不会主动清除当前Thread的中断状态

    10、为何stop()和suspend()方法不推荐使用

    用Thread.stop()方法来终止线程将会释放该线程对象已经锁定的所有监视器。如果以前受这些监视器保护的任何对象都处于不连贯状态,那么损坏的对象对其他线程可见,这有可能导致不安全的操作。

    suspend()方法 该方法已经遭到反对,因为它具有固有的死锁倾向。调用suspend()方法的时候,目标线程会停下来,并且不会释放锁资源,在目标线程重新开始以前,其他线程都不能访问该资源。除非被挂起的线程恢复运行。对任何其他线程来说,如果想恢复目标线程,同时又试图使用任何一个锁定的资源,就会造成死锁。

    10、如何停止一个正在运行的线程?(重要)

    (1)使用stop()来停止线程:stop()方法让线程立即停止运行, 这种暴力停止可能会破坏线程业务的原子性,不推荐使用

    (2)使用interrupt产生打断标志位来停止线程

    i:捕捉打断标记并且直接return

    由于run方法是一个void方法,可以在线程运行的时候用interrupt方法进行打断,此时产生一个打断标记位,捕捉到该标记位之后便可以优雅地结束该线程(可以直接return,也可以进行一些操作后return;)

    static class MyThread extends Thread {
        @Override
        public void run() {
            for (int i = 0; i < 500000; i++) {
                if (this.isInterrupted()) {
                    System.out.println("线程终止, 停止for循环.");
                    return;
                }
                System.out.println("i=" + (i + 1));
            }
        }
    }
    ​
    public static void main(String[] args) {
        MyThread thread = new MyThread();
        thread.start();
    ​
        try {
            Thread.sleep(200);
            thread.interrupt();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
    ​

    ii:捕捉打断标记,并且抛出异常终止程序

    捕捉到标记位之后,扔出异常来停止该线程

    static class MyThread extends Thread {
        @Override
        public void run() {
            try {
                for (int i = 0; i < 100000; i++) {
                    if (this.isInterrupted()) {
                        System.out.println("线程终止, 停止for循环.");
                        throw new InterruptedException();
                    }
                    System.out.println("i=" + (i + 1));
                }
            } catch (InterruptedException e) {
                System.out.println("MyThread抛出InterruptedException.");
                e.printStackTrace();
            }
        }
    }
    ​
    public static void main(String[] args) {
        MyThread thread = new MyThread();
        thread.start();
    ​
        try {
            Thread.sleep(200);
            thread.interrupt();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
    ​

    需要使用throw new Exception来打断

    iii:当线程处于sleep,park,join,wait的时候需要在catch块处理异常时自行设置打断标记

    当线程处于正常状态的时候,打断会产生打断的标记位,但是在线程处于sleep,join,wait,park等状态时,被打断将不会产生标记位,我们可以使用trycatch块来处理该情况,当程序被打断时,在程序catch并处理打断异常时候可以自己添加打断标记,从而设置打断标记。(两阶段终止模式)

    @Slf4j
    public class Test11 {
        public static void main(String[] args) throws InterruptedException {
            TwoParseTermination twoParseTermination = new TwoParseTermination();
            twoParseTermination.start();
            Thread.sleep(3000);  // 让监控线程执行一会儿
            twoParseTermination.stop(); // 停止监控线程
        }
    }
    ​
    ​
    @Slf4j
    class TwoParseTermination{
        Thread thread ;
        public void start(){
            thread = new Thread(()->{
                while(true){
                    if (Thread.currentThread().isInterrupted()){
                        log.debug("线程结束。。正在料理后事中");
                        break;
                    }
                    try {
                        Thread.sleep(500);
                        log.debug("正在执行监控的功能");
                    } catch (InterruptedException e) {
                        Thread.currentThread().interrupt();
                        e.printStackTrace();
                    }
                }
            });
            thread.start();
        }
        public void stop(){
            thread.interrupt();
        }
    }

    注:若程序是while循环,那么在捕捉到打断标记时,也可以用break结束循环从而结束线程

    11、sleep和yield的区别?

    状态的区别:

    调用 sleep 会让当前线程从 Running 进入 Timed Waiting 状态(阻塞)

    调用 yield 会让当前线程从 Running 进入 Runnable 就绪状态,然后调度执行其它线程

    调度的区别:

    调用sleep之后,该线程将进入阻塞状态,分不到CPU的时间片

    调用yield之后,该线程会让出CPU的使用权,但是任务调度器仍然可能分配给该线程时间片,从宏观上只是该线程被分配CPu的概率变低了

    12、sleep,yield为什么是静态方法(重要)

    Thread 类的 sleep()和 yield()方法将在当前正在执行的线程上运行。其他线程上调用这些方法是没有意义的。也就是说只有本线程才能执行休眠操作,如果sleep是成员方法,其他线程可以获得该线程的实例化对象,从而让此线程强制休眠(释放CPU的资源),这样会带来不可预估的后果。

    分析:wait,join为什么是成员方法

    join可以在其他线程中调用,因为其本身设计的意义就是其他线程等待该线程完成

    wait是本线程获取锁之后,锁对象调用的wait方法,实际上还是在本线程中使用

    sleep,yield不可以被其他线程调用!只能被自身线程调用,也是就是必须是自愿发生才可以!

    13、有三个线程T1,T2,T3,如何保证顺序执行?

    确保一个线程启动之后等待他执行完再进行下一个

    1. 1.          t1.start();
      2. •        t1.join();  
      3. •        t2.start();
      4. •        t2.join();
      5. •        t3.start();
      6. •        t3.join();

    2、现在可以用wait-notify实现线程间通信而达到顺序执行的目的

    14、在 java中守护线程和本地线程区别

    java中的线程分为两种:守护线程(Daemon)和用户线程(User)。

    任何线程都可以设置为守护线程和用户线程,通过方法Thread.setDaemon(bool);true则把该线程设置为守护线程,默认用户线程。Thread.setDaemon()必须在Thread.start()之前调用,否则运行时会抛出异常。

    守护线程的特点是,如果一个进程中的其他用户线程全部运行完毕,那么这时守护线程也会自动结束,比如垃圾回收线程

    比如JVM的垃圾回收线程是一个守护线程,当所有线程已经撤离,不再产生垃圾,守护线程自然就没事可干了

    15、sleep和wait的区别?

    sleep是Thread类的静态方法,在线程使用sleep方法之后会让出CPU的资源,但是不会释放锁资源

    wait方法是Object的方法,只能在同步代码块中被调用,某个线程使用锁对象的wait方法,会释放掉该线程的锁资源(同时还有CPU使用权),让其他线程去竞争

    16、线程创建到结束的几种状态?

    img

    1、线程刚被创建的时候是初始化状态New,这时候没有被分配CPU资源

    2、采用start方法之后,线程运行状态即RUNNABLE状态,这时可以被分配时间片资源进入RUNNING(RUNNING状态是包含在RUNABLE中的),也可以因为上下文切换暂时分配不到时间片资源

    3、当线程处于RUNABLE状态时,通过调用wait,join,park等方法会进入到WAITING状态,并且通过对应的唤醒操作,notify和unpark等操作(还得竞争锁成功)可以让线程从WAITING回到RUNABLE状态,join可以通过线程执行完,主线程便会变为RUNABLE

    join的底层原理是把thread对象看为一个对象锁,所以是主线程会进行wait,因此主线程会释放锁(thread锁,比较特殊)

    是主线程waiting了,而且锁是thread

    4、当线程处于RUNABLE状态时,通过调用wait(n),join(n),park(n),sleep(n)等方法会进入到TIMED_WAITING状态,可以通过等待时间结束(sleep),notify等操作回到原状态(竞争锁成功才能回到原状态)

    5、当某个线程与其他线程竞争同一把锁失败会进入BLOCKED状态,处于WAITING的线程被唤醒竞争锁失败也会进入BLOCKED状态,竞争锁成功可以回到RUNNABLE状态

    6、线程执行完毕会进入TERMINATED状态

    17、对线程优先级的理解?

    每一个线程都是有优先级的,一般来说,高优先级的线程在运行时会具有优先权,但这依赖于线程调度的实现,这个实现是和操作系统相关的(OSdependent)。可以定义线程的优先级,但是这并不能保证高优先级的线程会在低优先级的线程前执行线程优先级是一个int变量(从1-10),1代表最低优先级,10代表最高优先级

    18、什么是后台线程?

    就是守护线程,也可以叫做精灵线程

    19、sleep,yiled,wait,join 对比

    关于join的原理和这几个方法的对比:看这里

    补充:

    1. sleep,join,yield,interrupted是Thread类中的方法

    2. wait/notify是object中的方法

    sleep 不释放锁、释放cpu join 释放锁(主线程)、抢占cpu(被调用的线程) yiled 不释放锁、释放cpu wait 释放锁、释放cpu

    sleep和yield都不会释放锁,但是会释放该线程占用的CPU资源

    对于main{

    thread.join();

    }

    会使得主线程释放锁(相当于把thread作为锁对象进行wait),thread线程会占用CPUz资源

    21、Thread.sleep(0)有什么作用?

    触发操作系统立刻重新进行一次CPU竞争,竞争的结果可能是当前线程仍然获得CPU控制权,也可能是别的线程获得CPU控制权。

    二、锁知识

    20、什么是线程安全?

    线程安全概念:当多个线程访问某一个类(对象或方法)时,对象对应的公共数据区始终都能表现正确,那么这个类(对象或方法)就是线程安全的。

    21、什么是竞态条件?

    计算的正确性取决于多个线程的交替执行顺序时,就会发生竞态条件。

    多个线程在临界区执行,那么由于代码指令的执行不确定而导致的结果问题,称为竞态条件

    竞态条件不是某种条件,而是一种问题结果

    比如对全局变量的读写操作,A线程读取变量还未执行操作时候发生了上下文切换,另一个线程读取变量(由于A并未对变量操作,所以读取的还是原来的变量)并进行了操作,之后切换到A进行了操作,这时相当于只做了A的操作,B的操作被覆盖了

    22、什么是临界区?

    一段代码内如果存在对共享资源的多线程读写操作,那么称这段代码为临界区

    22、什么是不可变对象,它对写并发应用有什么帮助?(重要)

    答: 不可变对象(Immutable Objects)即对象一旦被创建它的状态(对象的数据,也即对象属性值)就不能改变,反之即为可变对象(MutableObjects)。不可变对象的类即为不可变类(Immutable Class)。

    Java平台类库中包含许多不可变类,如String、基本类型的包装类、BigInteger和BigDecimal等。不可变对象天生是线程安全的。它们的常量(域)是在构造函数中创建的。既然它们的状态无法修改,这些常量永远不会变。

    不可变对象永远是线程安全的。

    只有满足如下状态,一个对象才是不可变的;它的状态不能在创建后再被修改;所有域都是final类型;并且,它被正确创建(创建期间没有发生this引用的逸出)。 ———————————————— 版权声明:本文为CSDN博主「Java小叮当」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。 原文链接:【2021最新版】Java多线程&并发面试题总结(108道题含答案解析)_程序媛小琬的博客-CSDN博客_java多线程面试题2021

    23、synchronized关键字最主要的三种使用方式

    1、修饰实例方法

     class Test{
            public synchronized void test() {
    ​
            }
        }
        //等价于
        class Test{
            public void test() {
                synchronized(this) {
    ​
                }
            }
        }

    使用方法:

    Test test = new Test();
    test.test();

    synchronized加在实例方法上,需要创建该实例方法所属类的对象,某该线程使用对象引用该方法时候,就会给该线程加上此对象锁。

    • 修饰实例方法,作用于当前对象实例加锁,进入同步代码前要获得当前对象实例的锁

    2、修饰静态方法

    class Test{
            public synchronized static void test() {
            }
        }
       // 等价于
        class Test{
            public static void test() {
                synchronized(Test.class) {
    ​
                }
            }
        }

    使用方法:

    Test.test()

    修饰静态方法,作用于当前类对象加锁,进入同步代码前要获得当前类对象的锁

    如果一个线程A调用一个实例对象的非静态 synchronized 方法,而线程B需要调用这个实例对象所属类的静态 synchronized 方法,是允许的,不会发生互斥现象,因为访问静态 synchronized 方法占用的锁是当前类的锁,而访问非静态 synchronized 方法占用的锁是当前实例对象锁。

    package TestSynchorized;
    ​
    public class Lock {
        public  synchronized static  void test1(){
            while (true){
                System.out.println("静态方法锁");
            }
        }
        public synchronized void test2() {
                while (true){
                    System.out.println("-------------------------------------实例方法锁2");
                }
        }
            public synchronized void test3(){
                for (int i = 0; i < 10; i++) {
                    System.out.println("实例方法锁3");
                }
        }
    ​
    ​
        public static void main(String[] args) {
            Lock lock = new Lock();
            new Thread(()->{
                Lock.test1();
            }).start();
            new Thread(()->{
                lock.test2();
            }).start();
            new Thread(()->{
                lock.test3();
            }).start();
        }
    }
    ​

    访问静态 synchronized 方法占用的锁是当前类的锁,而访问非静态 synchronized 方法占用的锁是当前实例对象锁。两个线程占用的所对象不同,当然不会发生互斥。

    但是test2和test3的同步代码块由于使用的是一把锁,所以这两个线程会发生互斥,不解决互斥的方法是,创建两个对象,分别用这两个对象调用test2和test3这样就不会使用同一把锁了

    3、修饰代码块

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

    Thread t1 = new Thread(() -> {
             for (int i = 0; i < 5000; i++) {
                 synchronized (room) {
                 sout("锁住代码块")
                }
            }
        }, "t1");

    synchronized 关键字底层原理属于 JVM 层面。

    总结:synchronized锁住的同步代码块在执行之前需要获取对应的对象锁

    24、讲讲你对synchronized的认识?

    java中的对象是由对象头和实例数据组成的,对象头如下:

    img

    1、无锁状态

    当一个对象没有被加锁的时候,是无锁状态,对象头中记录了其hashcode以及无锁标志位001

    2、偏向锁状态

    0是可偏向状态,1是已偏向状态

    当一个对象被某个线程加锁,当进入临界区执行代码的时候,该对象从无锁状态变为偏向锁状态,该线程使用CAS操作将线程ID写入该对象的MarkWord,该对象对象头中记录了该线程的ID以自身的偏向锁标志101

    偏向锁锁撤销:

    如果有另外一个线程A也对该对象加锁,那么会引起锁撤销流程:

    该线程检查该对象的MakWork,如果检查到线程ID不是自己A,也就是偏向别的线程B,就发生了竞争现象

    就会执行偏向锁的撤销:

    过程:

    (1)偏向锁的撤销需要等待全局安全点(safe point,代表了一个状态,在该状态下所有线程都是暂停的,stop-the-world),到达全局安全点后,持有偏向锁的线程B也被暂停了。 (2)检查持有偏向锁的线程B的状态(会遍历当前JVM的所有线程,如果能找到线程B,则说明偏向的线程B还存活着): (3) 如果线程还存活,则检查线程是否还在执行同步代码块中的代码: (4) 如果是,则把该偏向锁升级为轻量级锁,且原持有偏向锁的线程B继续获得该轻量级锁。 (5)如果线程未存活,或线程未在执行同步代码块中的代码,将该对象设置为无锁状态,A线程再使用CAS操作使得该对象重新偏向自己

    总之:有线程竞争时,判断是不是偏向自己,不是,看看原线程是不是再执行临界区代码,不执行重偏向自己,执行,升级到轻量级锁(重偏向或者升级到重量级锁)

    偏向锁存在的意义:

    1、由于很多方法的临界区代码都只被一个线程所执行,使用偏向锁可以降低系统的开销(使用轻量级锁和重量级锁开销较大)

    2、少了轻量级锁可重入的开销(会检查线程ID是否为自己,如果是的话就不需要重新置换MArkWord了)

    偏向锁撤销的情况

    锁对象调用hashcode()方法会使得其进入无锁状态

    其他线程竞争。。。

    批量重偏向:

    批量重偏向:当一个线程创建了大量对象并执行了初始的同步操作,后来另一个线程也来将这些对象作为锁对象进行操作,会导偏向锁重偏向的操作。 批量撤销:在多线程竞争剧烈的情况下,使用偏向锁将会降低效率,于是乎产生了批量撤销机制。

    ListA listA = new ArrayList();
        Thread t1 = new Thread(() - {
            for (int i = 0; i  50; i++) {
                A a = new A();
                synchronized (a) {
                    listA.add(a);
                }
            }

    当撤销的对象个数达到二十个,JVM把其余其他对象都偏向给另一个线程

    批量撤销偏向锁

    • 当 撤销偏向锁的阈值超过39以后 ,就会将整个类的对象都改为不可偏向

      因为偏向锁的作用是为了偏向某个线程,然而过多的撤销会让JVM觉得这个类的对象锁不可以再偏向了,所以再new 该对象会将这个对象锁置为不可偏向的

    3、轻量级锁

    当锁升级到轻量级锁的时候,线程会在栈内存中创建一个锁记录对象,锁记录对象包括线程地址+轻量级锁标记00,以及对象指针

    锁对象此时是无锁状态,hashcode+01

    1583755737580

    经过CAS操作之后锁记录对象中的线程地址与锁对象的对象头互换,并且锁记录对象指向该锁对象

    此时锁对象中有线程的地址+00标记位(轻量级锁)

    1583755964276

    线程与锁对象分别记录对方的信息

    锁重入:

    广义上的可重入锁指的是可重复可递归调用的锁,在外层使用锁之后,在内层仍然可以使用,并且不发生死锁(前提得是同一个对象或者class),这样的锁就叫做可重入锁。ReentrantLock和synchronized都是可重入锁。

    比如轻量级锁,在执行某个临界区之前已经加了锁,之后在这段代码中继续加锁,就会发生锁重入

    1583756190177

    锁重入,线程栈内存会增加一条记录指向锁对象,会把锁记录地址置为null; 线程中有多少个锁记录, 就能表明该线程对这个对象加了几次锁 (锁重入计数)

    轻量级锁CAS

    轻量级锁CAS是指线程栈内存的锁记录对象地址和锁对象MArkWord互换的过程,这个过程互换成功,说明该线程竞争到了轻量级锁,互换失败的原因有两个:

    1、锁膨胀,该线程没有竞争到轻量级锁,进入锁膨胀过程,申请Monitor,并且进入EntryList等待

    1583757586447

    Monitor的Oewner指向竞争成功的线程。

    2、锁重入机制

    因此轻量级锁有其他线程竞争时就会进入锁膨胀,竞争成功的线程成为重量级锁的OWner,失败的线程进入EntryList进行等待(BLOCKED状态)

    4、自旋锁

    是指当一个线程在获取锁的时候,如果锁已经被其它线程获取,那么该线程将循环等待,然后不断的判断锁是否能够被成功获取,直到获取到锁才会退出循环。

    主要是在重量级锁的竞争过程

    也就是说在线程0获得重量级锁执行同步代码块的时候,线程一不断地用CAS操作去攻击对象锁的MarkWord看其是不是能交换成功,如果在这个不断攻击的过程中线程0执行完了同步代码块,这时候线程1就可以获得重量级锁,就不用进入EntryList等待了,而如果攻击了多次没有效果那么就会自旋失败

    特点没有竞争成功,可以不立马进入休眠状态,而是不断地使用CAS操作与对象锁进行MarkWord进行交换,交换成功则自旋成功,自旋一定次数之后就会失败,进入EntryList休眠

    1583758113724

    自旋会占用 CPU 时间,单核 CPU 自旋就是浪费,*多核 CPU 自旋才能发挥优势*

    25、什么是重量级锁?为什么消耗很大?

    Monitor:

    每个对象都有一个监视锁,或者叫管 程,他是为了该对象成为重量级锁对象准备的,此时锁对象的MarkWord指向该Monitor地址。

    重量级锁是轻量级锁在出现多线程竞争时膨胀得到的一种锁,其依靠锁对象的Monitor锁实现的,对于竞争到锁的线程,Monitor的Owner便是该线程,对于竞争失败的线程,经历了一定次数的自旋之后便会进入EntryList进行等待,锁对象的MarkWork地址是Monitor的地址。

    为什么重量级线程开销很大的?

    当系统检查到锁是重量级锁之后,会把等待想要获得锁的线程进行阻塞,被阻塞的线程不会消耗cpu。但是阻塞或者唤醒一个线程时,都需要操作系统来帮忙,这就需要从用户态转换到内核态,而转换状态是需要消耗很多时间的,有可能比用户执行代码的时间还要长。

    也被成为互斥锁,同步锁,悲观锁

    26、自旋锁的优缺点

    优点:在线程竞争不激烈或者临界区代码执行耗时不长的时候,自旋可以减少线程阻塞进入等待队列的操作,从而减少了操作系统挂起,唤醒线程的操作,降低系统消耗

    缺点:在线程竞争激烈或者临界区代码执行耗时长的时候,会出现自旋失败的情况,空耗CPU的资源

    27、线程同步和互斥有几种实现方法,都是什么?(重要)

    28、wait的基本使用方法

    thread1 --->
    synchronized(obj){
       while(condition is not satified){
       obj.wait;
       }
    }
    thread2--->
    sychronized(obj){
    while (condition is satified){
    obj.notify/notifyAll
    }
    }

    特点:

    当线程0获得到了锁, 成为Monitor的Owner, 但是此时它发现自己想要执行synchroized代码块的条件不满足; 此时它就调用obj.wait方法, 进入到Monitor中的WaitSet集合, 此时线程0的状态就变为WAITING。 处于BLOCKED和WAITING状态的线程都为阻塞状态,CPU都不会分给他们时间片。但是有所区别: BLOCKED状态的线程是在竞争锁对象时,发现Monitor的Owner已经是别的线程了,此时就会进入EntryList中,并处于BLOCKED状态 WAITING状态的线程是获得了对象的锁,但是自身的原因无法执行synchroized的临界区资源需要进入阻塞状态时,锁对象调用了wait方法而进入了WaitSet中,处于WAITING状态 处于BLOCKED状态的线程会在锁被释放的时候被唤醒(包括Owner线程执行了wait,以及owner执行完临界区代码释放锁) 处于WAITING状态的线程只有被锁对象调用了notify方法(obj.notify/obj.notifyAll),才会被唤醒。然后它会进入到EntryList, 重新竞争锁 (此时就将锁升级为重量级锁)

    注意:**obj.wait和obj.notify方法必须是拿到该锁的线程执行才可以

    总之:BLOCKED的线程是自身竞争不到锁,进入ENtryList等待OWner释放锁并竞争

    WAITING的线程是由于自身某些执行的条件不满足,自己进行wait,等待条件满足时,会被其他线程唤醒,进入到ENTryList进行竞争锁

    29、wait的相关问题

    (1)notify()和notifyAll()有什么区别?

    notify和notifyAll都可以同一把锁唤醒处于WAITING状态的线程,并且让它们进入EntryList去竞争锁,但是notify只能随即唤醒一个线程,而notifyALl会唤醒所有的线程

    (2)为什么wait, notify和notifyAll这些方法不在thread类里面?(重要,记忆)

    Java提供的锁是对象级的而不是线程级的,每个对象都有锁,通过线程获得。简单的说,由于wait,notify,notifyAll都是锁级别的操作,所以把他们定义在object类中因为锁属于对象

    线程为了进入临界区(也就是同步块内),需要获得锁并等待锁可用,它们并不知道也不需要知道哪些线程持有锁,它们只需要知道当前资源是否被占用,是否可以获得锁,所以锁的持有状态应该由同步监视器来获取,而不是线程本身

    如果wait()方法定义在Thread类中,线程正在等待哪个锁就不明显了

    (3)为什么wait和notify方法要在同步块中调用?

    在同步块中调用的意义是首先获得某个对象锁,如果不在同步块中调用,notify将无法获知唤醒的是哪个锁的等待线程,wait也无法获知等待那个锁

    notify(),notifyAll()是将锁交给含有wait()方法的线程,让其继续执行下去,如果自身没有锁,怎么叫把锁交给其他线程呢;(本质是让处于入口队列的线程竞争锁)

    (4)什么是阻塞队列?阻塞队列的实现原理是什么?如何使用阻塞队列来实现生产者-消费者模型?

    阻塞队列(BLOCKINGQUEUE)是一个在队列基础上又支持了两个附加操作的队列。

    2个附加操作:

    支持阻塞的插入方法:队列满时,队列会阻塞插入元素的线程(生产者线程),直到队列不满。

    支持阻塞的移除方法:队列空时,队列会阻塞获取元素的线程(消费者线程),直到队列变为非空。

    jdk1.5之前使用简单的wait和notify实现生产者消费者模式,之后使用rentreelock的await/singal实现阻塞队列并实现生产者消费者模式

    通知模式实现:所谓通知模式,就是当生产者往满的队列里添加元素时会阻塞住生产者,当消费者消费了一个队列中的元素后,会通知生产者当前队列可用。当消费者向空队列中取元素的时候会被阻塞,直到生产者生产了一个元素之后,会通知消费者当前队列可用

    (5) join方法实现原理

    public final synchronized void join(long millis)
        throws InterruptedException {
            long base = System.currentTimeMillis();
            long now = 0;
    ​
            if (millis < 0) {
                throw new IllegalArgumentException("timeout value is negative");
            }
    ​
            if (millis == 0) {
                while (isAlive()) {
                    wait(0);
                }
            } else {
                while (isAlive()) {
                    long delay = millis - now;
                    if (delay <= 0) {
                        break;
                    }
                    wait(delay);
                    now = System.currentTimeMillis() - base;
                }
            }
        }

    底层是通过wait实现的,主线程将thread作为锁对象,并调用wait方法实现阻塞,当阻塞超时,或者线程执行完毕(!isAlive)死亡,主线程便会唤醒,因此这是主线程的阻塞,等待线程执行完毕,释放锁,停止阻塞

    (6)如何实现线程间通信

    wait/notify机制

    对于A线程需要等待某个条件成立在执行,对于B线程可以生产该条件

    A可以wait,b可以生产该条件之后,notify A

    31、park和unpark

    (1)基本使用

    thread1----->
    {
    LockSupport.park();
    }

    park是LockSupport的一个静态方法,它在某个线程中被调用时,会暂停该线程的执行,并且该线程会进入WAITING状态

    LockSupport.unpark(thread1);

    unpark也是LockSupport的一个静态方法

    会把被暂停的线程重新唤醒

    (2)先调用park再调用unpark

    每个线程都有自己的一个 Parker 对象(底层,由c代码实现),由三部分组成 _counter, _cond和 _mutex

    先调用park,检查cond变量如果是0,则线程进入Parker对象锁的Waiting队列(这也解释了为什么park之后是WAITING对象)

    再调用unpark对象,cond变为1,线程获得锁对象,正常运行,cond再变回0

    (3)先调用unpark再调用park

    先调用unpark,检查cond是0,设置cond为1,再调用park发现cond是1,无需打断线程执行,把cond置为0

    unpark调用时会把cond变为1,park调用时会检查cond,为0才打断运行,为1则不打断并且重新置为0

    32、park和wait的区别

    park是静态方法属于LockSupport,执行层面上是属于线程的,wait是Object的一个方法,执行层面是属于锁对象的

    park可以先调用park再调用unpark,但是wait不能先调用notify

    park唤醒的线程比较精确,而notify不精确

    33、park,wait,sleep,yield,join方法的区别

    wait和join底层都是wait,WAITING

    sleep是属于Thread的静态方法,TIMED_WAITING

    yield属于thread对象,RUNABLE

    park,WAITING

    34、什么是死锁,死锁发生的条件(重要)

    public static void main(String[] args) {
        final Object A = new Object();
        final Object B = new Object();
        
        new Thread(()->{
            synchronized (A) {
                try {
                    Thread.sleep(2000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                synchronized (B) {
    ​
                }
            }
        }).start();
    ​
        new Thread(()->{
            synchronized (B) {
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                synchronized (A) {
    ​
                }
            }
        }).start();
    }
    ​

    死锁的定义:

    所谓死锁,是指多个线程在运行过程中因争夺资源而造成的一种僵局,当线程处于这种僵持状态时,若无外力作用,它们都将无法再向前推进。

    比如上述代码,线程1需要获得线程2所持有的锁B才能释放自己所持有的锁A,线程2需要获得线程1所持有的锁A才能释放自身持有的锁B,这样线程12均会一直处于等待状态无法推进,就发生了死锁

    死锁的四个条件:(重要,记忆)

    互斥条件:进程要求对所分配的资源进行排它性控制,即在一段时间内某资源仅为一进程所占用。 请求和保持条件:当进程因请求资源而阻塞时,对已获得的资源保持不放。 不剥夺条件:进程已获得的资源在未使用完之前,不能剥夺,只能在使用完时由自己释放。 环路等待条件:在发生死锁时,必然存在一个进程--资源的环形链。

    互斥条件(Mutual exclusion):资源不能被共享,只能由一个进程使用。

    请求与保持条件(Hold and wait):已经得到资源的进程可以再次申请新的资源。

    非剥夺条件(No pre-emption):已经分配的资源不能从相应的进程中被强制地剥夺。

    循环等待条件(Circular wait):系统中若干进程组成环路,改环路中每个进程都在等待相邻进程正占用的资源。

    处理死锁问题,可以从预防,避免,检测与恢复三个方面来进行

    怎么预防死锁问题?

    1〉破坏互斥条件。*即允许进程同时访问某些资源*。但是,有的资源是不允许被同时访问的,像打印机等等,这是由资源本身的属性所决定的。所以,这种办法并无实用价值。

    〈2〉破坏不可剥夺条件。*即允许进程强行从占有者那里夺取某些资源。就是说,当一个进程已占有了某些资源,它又申请新的资源,但不能立即被满足时,它必须释放所占有的全部资源,以后再重新申请*。它所释放的资源可以分配给其它进程。这就相当于该进程占有的资源被隐蔽地强占了。这种预防死锁的方法实现起来困难,会降低系统性能。

    比如使用rentreelock的 tryLock方法,当一个线程尝试获得某个锁资源一段时间后,就会放弃对该资源的请求,并且主动释放之前获得的锁,通过这种方式,可以避免死锁

    〈3〉破坏请求与保持条件。*可以实行资源预先分配策略。即进程在运行前一次性地向系统申请它所需要的全部资源。如果某个进程所需的全部资源得不到满足,则不分配任何资源,此进程暂不运行*。只有当系统能够满足当前进程的全部资源需求时,才一次性地将所申请的资源全部分配给该进程。由于运行的进程已占有了它所需的全部资源,所以不会发生占有资源又申请资源的现象,因此不会发生死锁。但是,这种策略也有如下缺点:

    (1)在许多情况下,一个进程在执行之前不可能知道它所需要的全部资源。这是由于进程在执行时是动态的,不可预测的;

    (2)资源利用率低。无论所分资源何时用到,一个进程只有在占有所需的全部资源后才能执行。即使有些资源最后才被该进程用到一次,但该进程在生存期间却一直占有它们,造成长期占着不用的状况。这显然是一种极大的资源浪费;

    (3)降低了进程的并发性。因为资源有限,又加上存在浪费,能分配到所需全部资源的进程个数就必然少了。

    < 4 >破坏循环等待条件,实行资源有序分配策略。采用这种策略,即把资源事先分类编号,按号分配,使进程在申请,占用资源时不会形成环路。所有进程对资源的请求必须严格按资源序号递增的顺序提出。进程占用了小号资源,才能申请大号资源,就不会产生环路,从而预防了死锁。这种策略与前面的策略相比,资源的利用率和系统吞吐量都有很大提高,但是也存在以下缺点:

    (1)限制了进程对资源的请求,同时给系统中所有资源合理编号也是件困难事,并增加了系统开销;

    (2)为了遵循按编号申请的次序,暂不使用的资源也需要提前申请,从而增加了进程对资源的占用时间。

    怎么避免死锁问题

    该方法同样是属于事先预防的策略,但它并不须事先采取各种限制措施去破坏产生死锁的的四个必要条件,而是在资源的动态分配过程中,用某种方法去防止系统进入不安全状态,从而避免发生死锁。

    预防死锁的几种策略,会严重地损害系统性能。因此在避免死锁时,要施加较弱的限制,从而获得 较满意的系统性能。由于在避免死锁的策略中,允许进程动态地申请资源。因而,系统在进行资源分配之前预先计算资源分配的安全性。若此次分配不会导致系统进入不安全的状态,则将资源分配给进程;否则,进程等待。其中最具有代表性的避免死锁算法是银行家算法。 银行家算法:首先需要定义状态和安全状态的概念。系统的状态是当前给进程分配的资源情况。因此,状态包含两个向量Resource(系统中每种资源的总量)和Available(未分配给进程的每种资源的总量)及两个矩阵Claim(表示进程对资源的需求)和Allocation(表示当前分配给进程的资源)。安全状态是指至少有一个资源分配序列不会导致死锁。当进程请求一组资源时,假设同意该请求,从而改变了系统的状态,然后确定其结果是否还处于安全状态。如果是,同意这个请求;如果不是,阻塞该进程直到同意该请求后系统状态仍然是安全的。

    对于线程申请某个资源,如果同意该请求,会不会导致死锁的发生,如果会发生死锁,则阻塞该线程,直到直到同意该请求后系统状态仍然是安全的,否则,分配给该线程资源

    怎么检测和解除死锁

    这种方式不需要对线程的资源做任何限定,只需要要求系统发生死锁的时候能够快速的检测并解决即可

    检测死锁 首先为每个进程和每个资源指定一个唯一的号码; 然后建立资源分配表和进程等待表。 解除死锁: 当发现有进程死锁后,便应立即把它从死锁状态中解脱出来,常采用的方法有:

    剥夺资源:从其它进程剥夺足够数量的资源给死锁进程,以解除死锁状态; 撤消进程:可以直接撤消死锁进程或撤消代价最小的进程,直至有足够的资源可用,死锁状态.消除为止;所谓代价是指优先级、运行代价、进程的重要性和价值等

    35、什么是活锁

    任务没有被阻塞,由于某些条件没有满足,导致一直重复尝试—失败—尝试—失败的过程。 处于活锁的实体是在不断的改变状态,活锁有可能自行解开。

    活锁出现在两个线程 互相改变对方的结束条件,谁也无法结束。

    避免活锁的方法

    • 在线程执行时,中途给予 不同的间隔时间, 让某个线程先结束即可。

    活锁与死锁的区别?

    处于活锁的线程并没有阻塞,状态·也在不停的改变,就是因为其他线程改变其终止条件而无法终止

    处于死锁的线程互相锁住了对象所需要的资源,从而导致了死锁线程的阻塞

    活锁和死锁的区别在于,处于活锁的实体是在不断的改变状态,所谓的“活”, 而处于死锁的实体表现为等待;活锁有可能自行解开,死锁则不能。

    36、什么是饥饿?

    饥饿:如果一个线程因为 CPU 时间全部被其他线程抢走而得不到 CPU 运行时间,这种状态被称之为“饥饿”;

    二、饥饿原因

    1. 高优先级线程吞噬所有的低优先级线程的 CPU 时间。(比如使用synchronized的时候,一直有大量的线程去竞争同一个锁)

    2. 线程被永久堵塞在一个等待进入同步块的状态,因为其他线程总是能在它之前持续地对该同步块进行访问。(比如某个线程设置了永远无法完成的条件进入wait状态,那么它就永远不会被唤醒)

    3. 线程在等待一个本身(在其上调用 wait())也处于永久等待完成的对象,因为其他线程总是被持续地获得唤醒。

    37、什么是可重入锁?

    广义上的可重入锁指的是可重复可递归调用的锁同一线程在外层使用锁之后,在内层仍然可以使用,并且不发生死锁(前提得是同一个对象或者class),这样的锁就叫做可重入锁。

    也就是说自身线程可以重复对一个对象上锁,并且不会出现异常现象

    synchronized:

    thread---->{
    synchronized(obj){
    //外部代码块
    synchronized(obj){
    //内部代码块
    }
    }
    }

    Rentreelock

    thread--->{
       lock.lock();
       try{
       //代码
          lock.lock();
       }
    }

    38、Reentrantlock

    (1)基本使用方法

    Reentrantlock lock = new Reentrantlock();
    thread--->{
        lock.lock();
           try{
           //代码
           }finally{
             lock.unlock();
           }
    }

    创建锁对象,使用lock获取锁,执行完代码之后释放锁

    (2)可打断性

    lockinterruptly

    Reentrantlock lock = new Reentrantlock();
    thread--->{
           try{
           lock.lockinterruptly();
           //代码
           }catch(Exception e){
           
           }finally{
             lock.unlock();
           }
    }

    其获得锁的过程是可以被打断的

    main{
       thread.interrupt();
    }
    • 如果某个线程处于阻塞状态,可以调用其interrupt方法让其停止阻塞,获得锁失败

    • 处于阻塞状态的线程,被打断了就不用阻塞了,直接停止运行

    • 可中断的锁, 在一定程度上可以被动的减少死锁的概率, 之所以被动, 是因为我们需要手动调用阻塞线程的interrupt方法;

    可打断的设计目的是为了放置某个线程阻塞等待获取某个锁而导致的死锁现象,当该线程处于阻塞状态等待获得锁的时候,可以用其他线程打断它,放置死锁

    (3)锁超时(获取不到锁不会停止运行)

    lock.trylock(time)

    如果在一定时间内没有获得到锁,那么就放弃该锁,以及已经有的资源,可以用在死锁的预防里

    放弃该锁并不是说以后就不竞争锁了,只是当前放弃

    注意这里会放弃已有的资源

    (4)公平锁

    synchronized锁中,在entrylist等待的锁在竞争时不是按照先到先得来获取锁的,所以说synchronized锁时不公平的;ReentranLock锁默认是不公平的,但是可以通过设置实现公平锁。本意是为了解决之前提到的饥饿问题,但是公平锁一般没有必要,会降低并发度,使用trylock也可以实现。

    实现方式是在创建reentrantlock的时候参数设置为(true)

    (5)条件变量

    package ReentrantLock;
    ​
    import lombok.extern.slf4j.Slf4j;
    ​
    import java.util.concurrent.TimeUnit;
    import java.util.concurrent.locks.Condition;
    import java.util.concurrent.locks.ReentrantLock;
    @Slf4j(topic = "Await")
    public class Await {
        //定义一个非公平Lock
        public static final ReentrantLock lock = new ReentrantLock();
        //定义两个等待变量
        private static Condition waityanRoom = lock.newCondition();
    ​
        private static Condition waitwaimaiRoom = lock.newCondition();
    ​
        private static Boolean hasyan = false;
    ​
        private static Boolean haswaimai = false;
    ​
        //定义一个执行干活的方法
        public void Dojob() throws InterruptedException {
            Thread t1 = new Thread(() -> {
                //由于不会出现死锁等问题,所以用正常的lock即可
                lock.lock();
                try{
                    log.info("烟送来了吗:{}",hasyan);
                    while (!hasyan){
                        try {
                            waityanRoom.await();//等待,锁释放给那些不需要烟的人用,给他加上
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                        //有烟的时候就执行干活
                        log.info("烟来了吗{},t1开始干活吧",hasyan);
                    }
                }catch (Exception e){
                    e.printStackTrace();
                }finally {
                    lock.unlock();
                }
            }, "t1");
            Thread t2 = new Thread(() -> {
                //由于不会出现死锁等问题,所以用正常的lock即可
                lock.lock();
                try{
                    log.info("外卖送来了吗:{}",haswaimai);
                    while (!haswaimai){
                        try {
                            waitwaimaiRoom.await();//等待,锁释放给那些不需要烟的人用,给他加上
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                        //有外卖的时候就执行干活
                        log.info("外卖来了吗{},t2开始干活吧",haswaimai);
                    }
                }catch (Exception e){
                    e.printStackTrace();
                }finally {
                    lock.unlock();
                }
            }, "t2");
            //剩下的是不需要等待烟或者外卖的线程
            Thread t3 = new Thread(() -> {
                //由于不会出现死锁等问题,所以用正常的lock即可
                lock.lock();
                try{
                    log.info("正常员工开始干活了......");
                }catch (Exception e){
                    e.printStackTrace();
                }finally {
                    lock.unlock();
                }
            }, "t3");
    ​
            Thread t4 = new Thread(() -> {
                lock.lock();
                try {
                    //先唤醒再送烟
                    waityanRoom.signal();
                    hasyan = true;
                } finally {
                    lock.unlock();
                }
            }, "t4");
    ​
            Thread t5 = new Thread(() -> {
                lock.lock();
                try {
                    //先唤醒再送烟
                    waitwaimaiRoom.signal();
                    haswaimai = true;
                } finally {
                    lock.unlock();
                }
            }, "t5");
            t3.start();
            t2.start();
            t1.start();
            Thread.sleep(1000);
            t4.start();
            Thread.sleep(1000);
            t5.start();
        }
    }
    ​

    使用条件变量可以唤醒指定的线程,而不是像notify/notifyAll随即唤醒或者唤醒所有的线程

    Reentrantlock lock;
    Condition condition1=lock.newCondition;
    Condition condition2=lock.newCondition;
    thread1--->{
       lock.lock();
       condition1.await();
    }
    thread2--->{
       lock.lock();
       condition2.await();
    }
    main--->{
       lock.lock();
       condition1.signal();
    }

    39、ReentrantLock和Synchronized的相同点和区别(重要)

    (1)相同点:

    都是同步锁,互斥锁,都是可重入锁

    两个都是可重入锁,它们都是加锁方式同步,而且都是阻塞式的同步,也就是说当如果一个线程获得了对象锁,进入了同步块,其他访问该同步块的线程都必须阻塞在同步块外面等待,而进行线程阻塞和唤醒的代价是比较高的(操作系统需要在用户态与内核态之间来回切换,代价很高,不过可以通过对锁优化进行改善)。

    (2)不同点:

    1、synchronized是关键字,是原生语言层面的互斥,需要JVM实现,Reentrantlock是API层面的互斥

    2、syn通过JVM加锁和解锁,Reen通过获得对象,并使用lock和unlock方法加锁解锁

    3、syn是JVM自动解锁,Reen是必须手动解锁,否则可能出现死锁现象,需要联合try/finally实现

    4、Reen在功能上比较丰富,可以进行获取锁中断,获取锁超时,以及设置公平锁等

    5、都可以设置条件从而让线程进入等待状态,不过REEn通过设置不同地Condition实现,可以唤醒具体的线程,而synchronized只能通过notify/notifyAll唤醒非具体特定的线程

    6、在线程竞争激烈的时候Reen的性能要比sychronized好一些,在基本没有线程竞争锁的时候,syn的性能比较好

    reentrantLock基于 AQS 实现。AQS 内部通过对 volatile 的 state 读写以及cas 操作 和在某些条件下让线程进入阻塞状态实现。

    性能:偏向锁 > 轻量级锁 > reentrantLock > 重量级锁

    为什么高并发时Reen的性能要好些

    因为在reen默认是非公平锁,在进行锁的竞争时使用的是队列首位线程CAS的操作去竞争锁,而syn

    40、ReentrantLock的实现原理

    Reen是通过AQS队列同步器实现的

    底层主要由三个组件构成:

    由votile修饰的变量state,当前获得锁的线程,以及阻塞排队队列

    img

    当某一个线程使用lock方法时,通过CAS操作查询变量state是否为0,为0代表当前锁没有被线程占有,因此可以通过CAS操作获取当前锁,否则,进入阻塞队列等待锁的释放,当持有锁的线程执行完时,通过unlock方法修改state,唤醒排队队列的第一个线程进行CAS操作,获取锁

    CAS操作:修改state=1,并把当前线程置为自身

    Reen默认是非公平锁,比如线程1执行完毕之后,本该到队列中的线程获取锁,这是如果竞争比较激烈,在队列之外出现别的线程进行CAS操作并成功,这时就是不公平的

    开启公平锁之后,队列之外的线程想要竞争该锁,首先要判断队列中是否有等待线程,如果有的话,那么该线程需要插入队列并等待队列唤醒

    也就是说公平的意思是不允许插队

    41、lock、tryLock和lockInterruptibly的差別

    lock():若lock被thread A取得,thread B会进入block状态,直到取得lock; tryLock():若当下不能取得lock,thread就会放弃,可以设置一个超时时间参数,等待多久获取不到锁就放弃; lockInterruptibly():跟lock()情況一下,但是thread B可以通过interrupt中断,放弃继续等待锁

    42Condition和Object类锁方法区别

      Condition是lock锁里面的类。

    1. Condition 类的 awiat 方法和 Object 类的 wait 方法等效

    2. Condition 类的 signal 方法和 Object 类的 notify 方法等效

    3. Condition 类的 signalAll 方法和 Object 类的 notifyAll 方法等效

    4. ReentrantLock 类可以唤醒指定条件的线程,而 object 的唤醒是随机的

    43、公平锁与非公平锁

      公平锁指的是锁的分配机制是公平的,通常是先到先得,RenntrantLock可以在构造函数中定义公平和非公平

      非公平锁,随机、就近原则分配锁的机制,线程过来后会先自旋,尝试直接获取到锁,获取不到再去排队。非公平锁的效率要更高。

    三、无锁机制(CAS,原子类)

    44、什么是java内存模型?

    JMM 即 Java Memory Model,它从java层面定义了主存、工作内存抽象概念,底层对应着 CPU 寄存器、缓存、硬件内存、CPU 指令优化等。JMM 体现在以下几个方面

    1. 原子性 - 保证指令不会受到线程上下文切换的影响

    2. 可见性 - 保证指令不会受 cpu 缓存的影响

    3. 有序性 - 保证指令不会受 cpu 指令并行优化的影响

    45、什么是volitile?作用是什么

    对于存在于主线程中的变量,线程会将其存储到自己工作内存的高速缓存中,这样线程读到的值可能不是该变量最新的值,volatile可以用来修饰成员变量和静态成员变量,他可以避免线程从自己的工作缓存中查找变量的值,必须到主存中获取它的值线程操作 volatile 变量都是直接操作主存

    从而保证了线程对变量的修改都是对其他线程可见的

    使用synchronized关键字也有相同的效果!在Java内存模型中,synchronized规定,线程在加锁时, 先清空工作内存→在主内存中拷贝最新变量的副本到工作内存 →执行完代码→将更改后的共享变量的值刷新到主内存中→释放互斥锁

    因此临界区使用的变量也是最新的

    不能保证原子性!可以保证有序性

    46、什么是原子性?

    保证指令不会受到线程上下文切换的影响,使用synchronized可以保证代码的原子性

    因为上下文切换的时候,由于互斥锁的原因,其他线程无法执行该临界区代码,所以不会发生错误

    Reentrantlock和actomic下的包都可以保证原子性

    47、有序性

    指令重排序是JVM为了优化指令,提高程序运行效率,在不影响单线程程序执行结果的前提下,会对一些指令的顺序进行重新排序

    重排序在单线程模式下是一定会保证最终结果的正确性,但是在多线程环境下,问题就出来了

    int num = 0;
    ​
    // volatile 修饰的变量,可以禁用指令重排 volatile boolean ready = false; 可以防止变量之前的代码被重排序
    boolean ready = false; 
    // 线程1 执行此方法
    public void actor1(I_Result r) {
     if(ready) {
        r.r1 = num + num;
     } 
     else {
        r.r1 = 1;
     }
    }
    // 线程2 执行此方法
    public void actor2(I_Result r) {
     num = 2;
     ready = true;
    }
    ​

    48、volatile怎么保证可见性和有序性的?

    可见性

    lock指令 对volatile修饰的变量,执行写操作的话,JVM会发送一条lock前缀指令给CPU,CPU在计算完之后会立即将这个值写回主内存,同时因为有MESI缓存一致性协议,所以各个CPU都会对总线进行嗅探,自己本地缓存中的数据是否被别人修改

    如果发现别人修改了某个缓存的数据,那么CPU就会将自己本地缓存的数据过期,标记位无效数据,然后这个CPU上执行的线程在读取那个变量的时候,就会从主内存重新加载最新的数据。

    lock前缀指令 + MESI缓存一致性协议

    由于只有volatile修饰的变量写的时候才会发送lock指令,所以没有写指令的时候,本地缓存的数据是有效的。

    在写的时候把其他线程本地缓存的数据置为无效数据,但是其他线程此时不更改,直到其他线程要读的时候才从主存中读取最新值

    有序性

    (2)内存屏障:禁止重排序

    底层就是插入了XX内存屏障,XX内存屏障,就可以保证指令不会重排

    对于volatile修改变量的读写操作,都会加入内存屏障

    每个volatile写操作前面,加StoreStore屏障,禁止上面的普通写和他重排;每个volatile写操作后面,加StoreLoad屏障,禁止跟下面的volatile读/写重排

    每个volatile读操作后面,加LoadLoad屏障,禁止下面的普通读和voaltile读重排;每个volatile读操作后面,加LoadStore屏障,禁止下面的普通写和volatile读重排

    在这里插入图片描述

    img

    50、单例模式的双检锁是什么?

    51、synchronized 和 volatile 的区别是什么?

    • volatile 是变量修饰符;synchronized 是修饰类、方法、代码段。

    • volatile 仅能实现变量的修改可见性,不能保证原子性;而 synchronized 则可以保证变量的修改可见性和原子性。

    • volatile 不会造成线程的阻塞;synchronized 可能会造成线程的阻塞。

    为啥synchronized无法禁止指令重排,但可以保证有序性?

    由于syn是互斥锁,加了锁之后,同一时间只能有一个线程获得到了锁,获得不到锁的线程就要阻塞。所以同一时间只有一个线程执行,相当于单线程,而单线程的指令重排是没有问题的。

    30、乐观锁和悲观锁的理解及如何实现,有哪些实现方式(重要)

    悲观锁:总是假设最坏的情况,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会阻塞直到它拿到锁。传统的关系型数据库里边就用到了很多这种锁机制,比如行锁,表锁等,读锁,写锁等,都是在做操作之前先上锁。再比如Java里面的同步原语synchronized关键字的实现也是悲观锁。

    乐观锁:顾名思义,就是很乐观,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,可以使用版本号等机制。乐观锁适用于多读的应用类型,这样可以提高吞吐量,像数据库提供的类似于write_condition机制,其实都是提供的乐观锁。在Java中java.util.concurrent.atomic包下面的原子变量类就是使用了乐观锁的一种实现方式CAS实现的。

    乐观锁的实现方式:

    1、使用版本标识来确定读到的数据与提交时的数据是否一致。提交后修改版本标识,不一致时可以采取丢弃和再次尝试的策略。

    2、java中的Compare and Swap即CAS ,当多个线程尝试使用CAS同时更新同一个变量时,只有其中一个线程能更新变量的值,而其它线程都失败,失败的线程并不会被挂起,而是被告知这次竞争中失败,并可以再次尝试。 CAS 操作中包含三个操作数 —— 需要读写的内存位置(V)、进行比较的预期原值(A)和拟写入的新值(B)。如果内存位置V的值与预期原值A相匹配,那么处理器会自动将该位置值更新为新值B。否则处理器不做任何操作。

    CAS缺点:

    1. ABA问题:

    比如说一个线程one从内存位置V中取出A,这时候另一个线程two也从内存中取出A,并且two进行了一些操作变成了B,然后two又将V位置的数据变成A,这时候线程one进行CAS操作发现内存中仍然是A,然后one操作成功。尽管线程one的CAS操作成功,但可能存在潜藏的问题。从Java1.5开始JDK的atomic包里提供了一个类AtomicStampedReference来解决ABA问题。

    2、循环时间长开销大:

    对于资源竞争严重(线程冲突严重)的情况,CAS自旋的概率会比较大,从而浪费更多的CPU资源,效率低于synchronized。

    3、只能保证一个共享变量的原子操作:

    当对一个共享变量执行操作时,我们可以使用循环CAS的方式来保证原子操作,但是对多个共享变量操作时,循环CAS就无法保证操作的原子性,这个时候就可以用锁。

    52、CAS

    CAS操作的流程:

    CAS是英文单词Compare and Swap的缩写,翻译过来就是比较并替换。

    底层通过Unsafe类提供的操作系统原生方法来实现

    CAS机制中使用了3个基本操作数:内存地址V,旧的预期值A,要修改的新值B。

    更新一个变量的时候,只有当变量的预期值A和内存地址V当中的实际值相同时,才会将内存地址V对应的值修改为B。

    假设有变量i目前的值是10,线程1想要做自增操作

    对于线程一来讲,变量的预期值是10,但是如果有其他线程2修改了内存(主存)中i的值为11,这时对于线程1来讲i就是被修改了(预期值与内存值不同),因此不能自增,需要把自身的预期值改为11,再与主存值比较是否相同(也就是判定该值是否被其他线程修改),这个过程称为自旋,直到比较成功,在进行更新(自增)

    CAS的自旋:当线程想要对某个变量进行更新操作时,需要先把自己缓存中的期望值与主存中的值进行比较(如果相同,说明没有其他线程对此变量进行修改),如果不相同,把自身缓存中的期望值更改为主存中的值,再次进行比较,这种过程叫做自旋,比较结果相同时候,自旋成功,该线程可以进行更新的操作。

    CAS:比较并更新,比较的过程就是自旋的过程,自旋成功才能够进行数值的更新

    Unsafe是CAS的核心类,由于Java方法无法直接访问底层系统,需要通过本地(Native)方法来访问,Unsafe相当于一个后门,基于该类可以直接操作特定的内存数据。Unsafe类存在sun.misc包中,其内部方法操作可以像C的指针一样直接操作内存,因为Java中的CAS操作的执行依赖于Unsafe类的方法。

    注意Unsafe类的所有方法都是native修饰的,也就是说unsafe类中的方法都直接调用操作系统底层资源执行相应的任务。

    java.util.concurrent.atomic并发包提供了一些并发工具类,这里把它分成五类:

    1. 使用原子的方式更新基本类型

      • AtomicInteger:整型原子类

      • AtomicLong:长整型原子类

      • AtomicBoolean :布尔型原子类

    原子类的包都使用了volatile来修饰变量,并且使用操作系统底层的CAS操作来进行无锁操作,保证方法的原子性

    53、synchronized与CAS的区别(重要)

    • 对于资源竞争较少的情况,使用synchronized同步锁进行线程阻塞和唤醒切换以及用户态内核态间的切换操作额外浪费消耗cpu资源;而CAS基于硬件实现,不需要进入内核,不需要切换线程,操作自旋几率较少,因此可以获得更高的性能。

    • 对于资源竞争严重的情况,CAS自旋的概率会比较大(比如getAndAddInt方法中的do-while循环),从而浪费更多的CPU资源,效率低于synchronized。

    54、CAS的缺点(重要)

    1) CPU开销过大

    在并发量比较高的情况下,如果许多线程反复尝试更新某一个变量,却又一直更新不成功,循环往复,会给CPU带来很到的压力。

    当线程比较多的时候,对于某个资源竞争激烈,可能会导致一些线程的CAS一直处于自旋状态,从而白白浪费CPU资源

    2) 不能保证多个变量的原子性

    CAS机制所保证的知识一个变量的原子性操作,而不能保证整个代码块的原子性。比如需要保证3个变量共同进行原子性的更新,就不得不使用synchronized了。

    如何解决CAS只能保证一个变量的原子性操作问题?

    使用引用类型包装需要保证原子性的变量,之后再用AtomicRefernce对该引用类型进行包装

    BankCard {
    ​
        private final String accountName;
        private final int money;
        }
    private static AtomicReference<BankCard> bankCardRef = new AtomicReference<>(new BankCard("cxuan",100));

    通过AtomicRefernce类中的CAS方法对该引用类型的包装属性进行原子性操作

    3)ABA问题

    什么是ABA问题?

    在线程进行自旋的时候,假设线程1的期望值是A,而此时主存存储的对象也是A,但是此时其他线程通过CAS操作将主存中的数值改成了B,再次又改回了A,这时线程1进行CAS操作,由于主存和线程1的期望值是相同的,这时线程1便认为该值没有被修改,但是实际上该值是经历了其他线程的两次修改的之后的值。

    这个过程看起来没有问题,结合实际有问题,比如剩余100有两个线程需要提取100,但是只能一个,假设线程1使用CAS提取了100,剩余0,本来线程2不应该再提取了,但是这时,线程3(老板)给打了100元过来,这时线程2便可以在提取100元,而误认为线程1没有提取到,是自己竞争到了,这与实际情况相悖,也就是说线程2被骗了!

    解决方法:使用AtomicStampedReference类,对于每个对象都采用版本号机制,线程对对象进行更新时,也会对其版本号进行更新,而CAS比较时需要比较版本号是否相同。

    或者使用锁(syn Reen)

    49、volatile 变量和 atomic 变量有什么不同?

    volatile修饰的变量可以保证其可见性和有序性,但是不能保证原子性

    atomic包下的类比如AtomicInteger等通过定义volatile的变量,并通过CAS操作实现变量读写的原子性

    50、什么是原子操作?在 Java Concurrency API 中有哪些原 子类(atomic classes)?

    原子操作(atomic operation)意为”不可被中断的一个或一系列操作” 。

    处理器使用基于对缓存加锁或总线加锁的方式来实现多处理器之间的原子操作。

    在 Java 中可以通过锁和循环 CAS 的方式来实现原子操作。 CAS 操作——

    Compare & Set,或是 Compare & Swap,现在几乎所有的 CPU 指令都支持 CAS

    的原子操作。

    原子操作是指一个不受其他操作影响的操作任务单元。原子操作是在多线程环境

    下避免数据不一致必须的手段。

    int++并不是一个原子操作,所以当一个线程读取它的值并加 1 时,另外一个线程

    有可能会读到之前的值,这就会引发错误。

    为了解决这个问题,必须保证增加操作是原子的,在 JDK1.5 之前我们可以使用同

    步技术来做到这一点。到 JDK1.5,java.util.concurrent.atomic 包提供了 int 和

    long 类型的原子包装类,它们可以自动的保证对于他们的操作是原子的并且不需

    要使用同步。

    java.util.concurrent 这个包里面提供了一组原子类。其基本的特性就是在多线程

    环境下,当有多个线程同时执行这些类的实例包含的方法时,具有排他性,即当

    某个线程进入方法,执行其中的指令时,不会被其他线程打断,而别的线程就像

    自旋锁一样,一直等到该方法执行完成,才由 JVM 从等待队列中选择一个另一个

    线程进入,这只是一种逻辑上的理解。

    原子类:AtomicBoolean,AtomicInteger,AtomicLong,AtomicReference

    原子数组:AtomicIntegerArray,AtomicLongArray,AtomicReferenceArray

    原子属性更新器:AtomicLongFieldUpdater,AtomicIntegerFieldUpdater,

    AtomicReferenceFieldUpdater

    解决 ABA 问题的原子类:AtomicMarkableReference(通过引入一个 boolean

    来反映中间有没有变过),AtomicStampedReference(通过引入一个 int 来累

    加来反映中间有没有变过)

    展开全文
  • 基于Java多线程快速排序设计与优化.pdf
  • 多线程带来的问题 为什么需要多线程 其实说白了,时代变了,现在的机器都是多核的了,为了榨干机器最后的性能我们引入单线程。 为了充分利用CPU资源,为了提高CPU的使用率,采用多线程的方式去同时完成几件事情而不...
  • Java多线程 - 锁

    千次阅读 2022-03-28 15:31:49
    Java多线程 - 锁 三性 可见性 指的是线程之间的可见性,一个线程对状态的修改,对其他线程是可见的。在 Java中 volatile、synchronized 和 final 实现可见性。 原子性 如果一个操作是不可分割的,我们则称之为...
  • Java 中用到的线程调度算法是什么?线程同步以及线程调度相关的方法。sleep() 和 wait() 有什么区别?线程的 sleep()方法和 yield()方法有什么区别?同步方法和同步块,哪个是更好的选择?如果你提交任务时,线程池...
  • 众所周知,多线程会造成线程安全问题,那么多线程为什么会导致线程安全问题呢? 一:首先了解jvm内存的运行时数据区 1.堆区:存储对象实例(和实例变量),数组等 2.java虚拟机栈(方法·栈),存放方法声明,...
  • 基于Java多线程快速排序设计与优化
  • 定义:多线程读写共享变量时出现不正确的行为 原因 原子性问题 CPU时钟中断带来的线程切换 可见性问题 多核CPU高速缓存之间不可见 重排序问题 CPU和编译器会进行重排序指令 典型问题:单例模式DCL ...
  • Java多线程---单例模式(有趣易懂版)

    千次阅读 多人点赞 2021-03-17 17:54:38
    多线程执行中,原本是要将数据从主内存拿到自己的私有工作区中去修改,然后再放回主内存中,但这个过程中,可能A线程正在改数据还没放回去,B线程又去拷贝这个数据去修改,导致数据不一致。 使用volatile关键字,...
  • 多线程冒泡法排序

    2019-12-05 22:20:01
    java课的课后作业,要求实现数组的多线程冒泡排序并实现可视化,自己写了一个奇偶排序多线程,但效率较低,现在回顾一下老师课上讲的程序。 生成的界面如上 首先实现Button1的功能,生成随机的指定大小的数组 ...
  • Java多线程——基本概念

    万次阅读 多人点赞 2019-10-23 10:36:25
    线程和多线程 程序:是一段静态的代码,是应用软件执行的蓝本 进程:是程序的一次动态执行过程,它对应了从代码加载、执行至执行完毕的一个完整过程,这个过程也是进程本身从产生、发展至消亡的过程 线程:是比...
  • Java多线程实现接口调用

    千次阅读 2019-11-23 19:37:24
    CustQueryOneThread.java 线程类 CustInfoOneServiceImpl.java 业务逻辑类 每次取数据的mybatis的 xml文件 < select id = " selectCustInfoList " resultMap = " BaseResultMap " > select < ...
  • java多线程创建创建线程的方式Runnable和Callable的区别Thread类中的start()和run()方法有什么区别?什么导致线程阻塞?3. 多线程同步和锁怎么检测一个线程是否持有对象监视器Condition?4. 线程池CyclicBarrier和...
  • Java 多线程总结笔记

    千次阅读 2018-09-19 10:21:56
    Java多线程总结笔记 实现多线程的方法 查看Thread类的源码,可以发现它实现了Runnable接口,然后在自己的run方法中调用了Runnable的run方法。这里其实就是静态代理这一设计模式,Thread是代理角色,而Runnable则是...
  • Java排序多线程的影响

    千次阅读 2017-06-17 14:53:58
    我们知道无论什么语言,最后驱动计算机的那一定是汇编,Java代码编译后会变成一段字节码,通过Java虚拟机的类加载机制ClassLoader加载到虚拟机里面,最后便是把它转化成汇编指令。通过JMM(Java内存模型)实现内存
  • Java多线程之线程安全问题

    千次阅读 多人点赞 2022-03-31 11:02:50
    本篇文章介绍的内容为Java多线程中的线程安全问题,此处的安全问题并不是指的像黑客入侵造成的安全问题,线程安全问题是指因多线程抢占式执行而导致程序出现bug的问题。
  • 史上最全Java多线程面试题及答案

    万次阅读 多人点赞 2018-08-20 11:17:08
    这篇文章主要是对多线程的问题进行总结的,因此罗列了40个多线程的问题。 这些多线程的问题,有些来源于各大网站、有些来源于自己的思考。可能有些问题网上有、可能有些问题对应的答案也有、也可能有些各位网友也都...
  • Java多线程及锁相关面试题

    千次阅读 2022-03-27 17:43:25
    JMM:为了明确定义在多线程场景下,什么时候可以重排序,什么时候不能重排序Java引入了JMM(Java Memory Model),也就是Java内存模型。 happen-before:这个模型就是一个规范,为了描述这个规范,JMM引入了...
  • java对大数据量文件内容的多线程读取和排序.doc
  • java多线程之多线程的三大特性

    千次阅读 2017-12-06 10:51:22
    多线程的三大特性:原子性、可见性、有序性 1.什么是原子性 即一个操作或者多个操作 要么全部执行并且执行的过程不会被任何因素打断,要么就都不执行。 一个很经典的例子就是银行账户转账问题: 比如从账户A向...
  • 多线程三大特性——重排序

    千次阅读 2021-12-01 20:12:24
    Java语言规范JVM线程内部维持顺序化语义,即只要程序的最终结果与它顺序化情况的结果相等,那么指令的执行顺序可以与代码逻辑顺序不一致,这个过程就叫做指令的重排序。 指令重排序的意义:在Java中,JVM能够根据...

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 224,268
精华内容 89,707
关键字:

java多线程排序

java 订阅