java核心知识

目录

1 java中的锁

1.1 分类

1.2 synchronized

1.3 常见问题

2 线程

2.1 线程创建方式

2.2 生命周期

2.4 线程基本方法

2.3 线程池

3 工作内存与主存

4 原子性、可见性、有序性


1 java中的锁

1.1 分类

【乐观锁 VS 悲观锁】

  • 悲观锁认为自己在使用数据的时候一定有别的线程来修改数据,因此在获取数据的时候会先加锁,确保数据不会被别的线程修改。Java中,synchronized关键字和Lock的实现类都是悲观锁。

  • 乐观锁认为自己在使用数据时不会有别的线程修改数据,所以不会添加锁,只是在更新数据的时候去判断有没有别的线程更新了这个数据,没有则完成更新操作。乐观锁在Java中是通过使用无锁编程来实现,最常采用的是CAS算法,Java原子类中的递增操作就通过CAS自旋实现的。

【自旋锁 VS 适应性自旋锁】

背景:阻塞或唤醒一个Java线程需要操作系统切换CPU状态来完成,这种状态转换需要耗费处理器时间。所以状态转换消耗的时间有可能比用户代码执行的时间还要长。为了避免不必要的上下文切换,java支持自旋锁

  • 自旋锁:让当前线程进行自旋,如果在自旋完成后前面锁定同步资源的线程已经释放了锁,那么当前线程就可以直接获取同步资源,从而避免切换线程的开销。

  • 自适应自旋锁:意味着自旋的时间(次数)不再固定,而是由前一次在同一个锁上的自旋时间及锁的拥有者的状态来决定。比如在同一个锁对象上,自旋等待刚刚成功获得过锁,并且持有锁的线程正在运行中,那么虚拟机就会认为这次自旋也是很有可能再次成功,进而它将允许自旋等待持续相对更长的时间。

自旋等待虽然避免了线程切换的开销,但它要占用处理器时间。如果锁被占用的时间很短,自旋等待的效果就会非常好。反之,如果锁被占用的时间很长,那么自旋的线程只会白浪费处理器资源。

【无锁 VS 偏向锁 VS 轻量级锁 VS 重量级锁】

  • 重量级锁:synchronized依赖于操作系统Mutex Lock互斥量,会导致进程在用户态与内核态之间切换,相对开销大。

  • 轻量级锁:获取锁失败后,通过自旋的形式尝试获取锁,不会阻塞从而提高性能。

  • 偏向锁:偏向锁是指一段同步代码一直被一个线程所访问,那么该线程会自动获取锁,降低获取锁的代价。

【公平锁 VS 非公平锁】

  • 公平锁:是指多个线程按照申请锁的顺序来获取锁,线程直接进入队列中排队,队列中的第一个线程才能获得锁。公平锁的缺点是一个线程耗时特别长会导致其他所有线程等待。

  • 非公平锁:是多个线程加锁时直接尝试获取锁,获取不到才会到等待队列的队尾等待,所以非公平锁有可能出现后申请锁的线程先获取锁的场景。

【可重入锁 VS 非可重入锁】

  • 可重入锁 :一个线程获取锁后,后续该线程可重复获取该对象的锁。

  • 非可重入锁:一个线程获取锁后,后续该线程也无法重复获取该对象的锁。

【独享锁 VS 共享锁】

  • 独享锁也叫排他锁,是指该锁一次只能被一个线程所持有。

  • 共享锁是指该锁可被多个线程所持有。如果线程T对数据A加上共享锁后,则其他线程只能对A再加共享锁,不能加排它锁。

1.2 synchronized

https://www.cnblogs.com/summerday152/p/13703110.html

【介绍】

synchronized关键字用于解决并发时线程安全问题。是悲观锁、自旋锁、可重入锁、排他锁

【使用场景】

  1. 修饰非静态方法:作用于当前对象实例加锁,进入同步代码前要获得 当前对象实例的锁。

  2. 修饰静态方法: 也就是给当前类对象加锁,会作用于类的所有对象实例 ,进入同步代码前要获得 当前 class 的锁。

  3. 修饰代码块 :给括号内配置的对象加锁。

【JDK1.6的优化】

在JDK 1.6中,synchronized 的速度已经有了显著的提升,主要通过分级锁的方式。JVM 会根据使用情况对synchronized锁升级,会如此路径升级:偏向锁 -->轻量级锁--> 重量级锁。

  1. JVM在JDK 1.6中引入了分级锁机制来优化synchronized

  2. 当一个线程获取锁时,首先对象锁成为一个偏向锁

    1. 这是为了避免在同一线程重复获取同一把锁时,用户态和内核态频繁切换

  3. 如果有多个线程竞争锁资源,锁将会升级为轻量级锁

    1. 这适用于在短时间内持有锁,且分锁交替切换的场景

    2. 轻量级锁还结合了自旋锁来避免线程用户态与内核态的频繁切换

  4. 如果锁竞争太激烈(自旋锁失败),同步锁会升级为重量级锁

【相比ReentrantLock】

共同点

  1. 都是重入锁:

  2. 都是悲观锁:

  3. 都是排他锁:

  4. 都是自旋锁:

不同点

  1. Synchronized依赖于JVM实现,ReentrantLock依赖于API。ReentrantLock需要手动加锁与释放。

  2. Synchronized为非公平锁,ReentrantLock默认非公平锁,可选择为公平锁。

  3. ReentrantLock支持终止等待,与tryLock。

【实现机制】

对象头:Hotspot虚拟机的对象头主要包括两部分数据:Mark Word(标记字段)、Klass Pointer(类型指针)。

  • 类型指针是指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例。

  • 标记字段用于存储对象自身的运行时数据,如哈希码(HashCode)、GC分代年龄、锁状态标志、线程持有的锁、偏向线程 ID等,同时会有指向Monitor的指针。

Monitor:我们可以把它理解为一个同步工具,也可以描述为一种同步机制,它通常被描述为一个对象。每一个被锁住的对象都会和一个monitor关联(对象头的MarkWord中的LockWord指向monitor的起始地址),同时monitor中有一个Owner字段存放拥有该锁的线程的唯一标识,表示该锁被这个线程占用。

1.3 常见问题

有哪些锁?

常用的有比如Synchronized、ReentrantLock、Semaphore(共享锁)

2 线程

2.1 线程创建方式

  1. 继承Thread类:创建类继承Thread,实例化后调用start方法。

  2. 实现Runnable接口:如果类已经有继承关系,那么无法再继承Thread类,可以通过该方式。创建类A实现Runnable接口,新建Thread类并将A的实例传入,调用start方法。

  3. 实现Callable接口:当需要知道线程的执行结果是使用该方法。创建类实现Callable接口并实现call方法

public class TestCallable {

	public static void main(String[] args) throws ExecutionException, InterruptedException {
		ExecutorService executorService = Executors.newFixedThreadPool(10);
		Future<String> rt = executorService.submit(new MyCallable());
		while (true) {
			System.out.println("没完呢");
			if (rt.isDone()) {
				System.out.println(rt.get());
				break;
			}
		}

	}
}

class MyCallable implements Callable<String> {
	@Override
	public String call() throws Exception {
		return "请问OK吗?";
	}
}

2.2 生命周期

  1. 新建:就是刚使用new方法,new出来的线程;

  2. 就绪:就是调用的线程的start()方法后,这时候线程处于等待CPU分配资源阶段,谁先抢的CPU资源,谁开始执行;

  3. 运行:当就绪的线程被调度并获得CPU资源时,便进入运行状态,run方法定义了线程的操作和功能;

  4. 阻塞:在运行状态时主动或被动放弃CPU并暂停运行,比如sleep()、wait()、IO阻塞等。只有重新进入就绪状态才有机会再次竞争到CUP进入运行状态;

  5. 销毁:如果线程正常执行完毕后或线程被提前强制性的终止或出现异常导致结束,那么线程就要被销毁,释放资源;

2.4 线程基本方法

方法

作用

注意

线程等待(wait )

调用该方法的线程进入等待状态,只有等待另外线程的通知或被中断才会返回。

调用 wait()方法后,会释放对象的锁,因此wait 方法一般用在同步方法或同步代码块中。

线程睡眠(sleep )

sleep 导致当前线程休眠,并可指定睡眠时间。

与 wait 方法不同的是sleep 不会释放当前占有的锁。

线程让步(yield )

yield 会使当前线程让出CPU时间片进入就绪状态,与其他线程一起重新竞争。

可能出现让出后立刻重新获取到时间片。

线程加入(Join)

join() 调用一个线程的 join() 方法,则当前线程转为阻塞状态,等待加入线程结束,转为就绪状态。

线程唤醒(notify)

Object 类中的 notify() 方法,唤醒在此对象监视上等待的单个线程,如果多个线程在等待会随机唤醒一个。类似的方法还有 notifyAll() ,唤醒再次监视器上等待的所有线程。

线程中断(interrupt)

中断一个线程,其本意是给这个线程一个通知信号,会修改这个线程内部的一个中断标识位。

这个线程本身并不会因此而改变状态(如阻塞,终等)。

sleep和wait的区别?

  1. sleep方法属于 Thread 类,wait方法属于Object 类。

  2. sleep方法线程不会释放对象锁,而调用 wait()方法线程会释放对象锁。

  3. sleep会让出cpu并休眠指定时间,时间结束会自动恢复到就绪状态。wait让出cpu后等待被唤醒才行重新进入就绪状态。

2.3 线程池

【线程池优势】

  1. 降低资源消耗:通过重用已存在的线程,降低线程创建和销毁造成的消耗;

  2. 提高响应速度:当有任务到达时,通过复用已存在的线程,无需等待新线程的创建便能立即执行;

  3. 方便线程管理:因为线程若是无限制的创建,可能会导致内存占用过多而产生OOM,并且会造成cpu过度切换(cpu切换线程是有时间成本的(需要保持当前执行线程的现场,并恢复要执行线程的现场))。

【构成组件】

一个线程池包括以下四个基本组成部分:

  1. 线程池管理器(ThreadPool):用于创建并管理线程池,包括 创建线程池,销毁线程池,添加新任务;

  2. 工作线程(WorkThread):线程池中线程,在没有任务时处于等待状态,可以循环的执行任务;

  3. 任务接口(Task):每个任务必须实现的接口,以供工作线程调度任务的执行,它主要规定了任务的入口,任务执行完后的收尾工作,任务的执行状态等;

  4. 任务队列(taskQueue):用于存放没有处理的任务。提供一种缓冲机制。

【线程池七个参数】

  1. corePoolSize 核心线程数:这些线程即使处理空闲状态,他们也不会被销毁,除非设置了allowCoreThreadTimeOut。

  2. maximumPoolSize 最大线程数量:即线程池中允许存在的最大线程数。

  3. keepAliveTime 空闲线程存活时间:指定超过核心线程数的线程最大空闲时间,超过这个时间将被销毁。

  4. unit 空闲线程存活时间单位:3中的单位。

  5. workQueue 工作队列:用于存放等待线程的任务队列。有四种队列可供选择

    1. ArrayBlockingQueue:基于数组的有界阻塞队列,按FIFO排序。新任务进来后,会放到该队列的队尾,有界的数组可以防止资源耗尽问题。当线程池中线程数量达到corePoolSize后,再有新任务进来,则会将任务放入该队列的队尾,等待被调度。如果队列已经是满的,则创建一个新线程,如果线程数量已经达到maxPoolSize,则会执行拒绝策略。

    2. LinkedBlockingQuene:基于链表的无界阻塞队列(其实最大容量为Interger.MAX)。

    3. SynchronousQuene 不缓存任务的阻塞队列:生产者放入一个任务必须等到消费者取出这个任务。也就是说新任务进来时,不会缓存,而是直接被调度执行该任务,如果没有可用线程,则创建新线程,如果线程数量达到maxPoolSize,则执行拒绝策略。

    4. PriorityBlockingQueue 具有优先级的无界阻塞队列:优先级通过参数Comparator实现。

  6. threadFactory 线程工厂:创建一个新线程时使用的工厂,可以用来设定线程名、是否为daemon线程等等。

  7. handler 拒绝策略:当工作队列中的任务已到达最大限制,并且线程池中的线程数量也达到最大限制,这时如果有新任务提交进来,将使用拒绝策略进行处理。

    1. CallerRunsPolicy:调用者线程中直接执行被拒绝任务的run方法。

    2. AbortPolicy:直接丢弃任务,并抛出RejectedExecutionException异常。

    3. DiscardPolicy:直接丢弃任务,什么都不做。

    4. DiscardOldestPolicy:抛弃进入队列最早的那个任务,然后尝试把这次拒绝的任务放入队列。

【工作流程】

  1. 当一个任务通过submit或者execute方法提交到线程池的时候,如果当前池中线程数小于coolPoolSize,则创建一个线程执行该任务。

  2. 如果当前线程池中线程数已经达到coolPoolSize,则将任务放入等待队列。

  3. 等待队列情况分为三种情况:

    1. 等待队列未满,直接加入队列。

    2. 等待队列已满,若当前池中线程数小于maximumPoolSize,则创建一个临时线程(非核心线程)执行该任务。

    3. 等待队列已满,且池中线程数已经等于maximumPoolSize,此时无法执行该任务,根据拒绝执行策略处理。

【常用线程池】

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

        public static ExecutorService newCachedThreadPool() {
            return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
                                          60L, TimeUnit.SECONDS,
                                          new SynchronousQueue<Runnable>());
        }
  2. newFixedThreadPool:创建一个定长线程池,可控制线程最大并发数,超出的线程会在无界队列中等待。

        public static ExecutorService newFixedThreadPool(int nThreads) {
            return new ThreadPoolExecutor(nThreads, nThreads,
                                          0L, TimeUnit.MILLISECONDS,
                                          new LinkedBlockingQueue<Runnable>());
        }
  3. newScheduledThreadPool:创建一个任务调度线程池,支持定时及周期性任务执行。线程数无限制,使用DelayedWorkQueue实现。

        public ScheduledThreadPoolExecutor(int corePoolSize) {
            super(corePoolSize, Integer.MAX_VALUE, 0, NANOSECONDS,
                  new DelayedWorkQueue());
        }
  4. newSingleThreadExecutor:单个线程的线程池,能保证执行顺序,先提交的先执行,当线程执行中出现异常,去创建一个新的线程替换之。

        public static ExecutorService newSingleThreadExecutor() {
            return new FinalizableDelegatedExecutorService
                (new ThreadPoolExecutor(1, 1,
                                        0L, TimeUnit.MILLISECONDS,
                                        new LinkedBlockingQueue<Runnable>()));
        }

3 工作内存与主存

CPU有多级缓存,导致读的数据过期。由于CPU的处理速度很快,相比之下,内存的速度就显得很慢,所以为了提高 CPU 的整体运行效率,减少空闲时间,在 CPU 和内存之间会有缓存层的存在。虽然缓存的容量比内存小,但是缓存的速度却比内存的速度要快得多。结构示意图如下所示:

Java 作为高级语言,屏蔽了CPU多层缓存的这些底层细节,用 JMM 定义了一套读写数据的规范。我们不再需要关心 多层缓存的问题,我们只需要关心 JMM 抽象出来的主内存和工作内存的概念。

每个线程只能够直接接触到工作内存,无法直接操作主内存,而工作内存中所保存的正是主内存的共享变量的副本,主内存和工作内存之间的通信是由 JMM 控制的。

JMM 有以下规定:

  1. 所有的变量都存储在主内存中,同时每个线程拥有自己独立的工作内存,而工作内存中的变量的内容是主内存中该变量的拷贝;

  2. 线程不能直接读 / 写主内存中的变量,但可以操作自己工作内存中的变量,然后再同步到主内存中,这样,其他线程就可以看到本次修改;

  3. 主内存是由多个线程所共享的,但线程间不共享各自的工作内存,如果线程间需要通信,则必须借助主内存中转来完成。

【与内存模型关系】

这里的主存和工作内存与Java内存结构中的堆、栈、元空间等并不是同一个层次的内存划分,无法直接类比。

《深入理解Java虚拟机》中认为,如果一定要勉强对应起来的话,从变量、主内存、工作内存的定义来看,主内存主要对应于Java堆中的对象实例数据部分。工作内存则对应于虚拟机栈中的部分区域。

【可见性来源】

线程间对于共享变量的可见性问题,并不是直接由多核引起的,而是由CPU多级缓存引起的:每个核心在获取数据时,都会将数据从内存一层层往上读取,同样后续对于数据的修改也是逐层往下同步,直到最终刷回内存,这期间就会出现缓存与内存数据不一致的场景。

4 原子性、可见性、有序性

并发三要素:可见性、原子性和有序性

【问题背景】

  1. 可见性问题:CPU多级缓存机制,以均衡与内存的速度差异导致

  2. 原子性问题:操作系统增加了进程、线程,以分时复用 CPU,进而均衡 CPU 与 I/O 设备的速度差异。

  3. 有序性问题:编译程序优化指令执行次序,使得缓存能够得到更加合理地利用。

【问题定义】

  1. 原子性:一个操作具有原子操作,那么我们称它具有原子性。

  2. 可见性:一个线程修改的状态对另一个线程是可见的。

  3. 有序性:Java 语言提供了 volatile 和 synchronized 两个关键字来保证线程之间操作的有序性

    1. volatile 是因为其本身包含“禁止指令重排序”的语义

    2. synchronized 是由“一个变量在同一个时刻只允许一个线程对其进行 lock 操作”这条规则获得的。

【volatile不能保证原子性】

volatile修饰的变量不允许线程内部缓存和重排序,即直接修改内存。所以对其他线程是可见的。但是这里需要注意一个问题,volatile只能让被他修饰内容具有可见性,但不能保证它具有原子性。

【volatile怎么保证可见性】

1. 使用Lock前缀的指令让线程工作内存中的值写回主内存中;
2. 通过缓存一致性协议,其他线程如果工作内存中存了该共享变量的值,就会失效;
3. 其他线程会重新从主内存中获取最新的值;

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值