参考:volatile关键字解析 另外:深入JVM知识
内存模型
- 由于CPU执行速度很快,而从内存读取数据和向内存写入数据的过程跟CPU执行指令的速度比起来要慢的多,因此如果任何时候对数据的操作都要通过和内存的交互来进行,会大大降低指令执行的速度。因此在CPU里面就有了高速缓存
- 当程序在运行过程中,会将运算需要的数据从主存复制一份到CPU的高速缓存当中,那么CPU进行计算时就可以直接从它的高速缓存读取数据和向其中写入数据,当运算结束之后,再将高速缓存中的数据刷新到主存当中。
- 如果一个变量在多个CPU中都存在缓存(一般在多线程编程时才会出现),那么就可能存在缓存不一致的问题
- 内存模型 为了保证共享内存的正确性(可见性、有序性、原子性),内存模型定义了共享内存系统中多线程程序读写操作行为的规范
Java内存模型
- 规定了所有的变量都存储在主内存中,每条线程还有自己的工作内存
- 线程的工作内存中保存了该线程中是用到的变量的主内存副本拷贝,
- 线程对变量的所有操作都必须在工作内存中进行,而不能直接读写主内存。
- 不同的线程之间也无法直接访问对方工作内存中的变量,线程间变量的传递均需要自己的工作内存和主存之间进行数据同步进行
Java 堆栈
- 栈:
- 函数中定义的基本类型变量,对象的引用变量都在函数的栈内存中分配。
- 栈内存特点,数据一执行完毕,变量会立即释放,节约内存空间。
- 栈内存中的数据,没有默认初始化值,需要手动设置。
- 堆:
- 堆内存用来存放new创建的对象和数组。
- 堆内存中所有的实体都有内存地址值。
- 堆内存中的实体是用来封装数据的,这些数据都有默认初始化值。
- 堆内存中的实体不再被指向时,JVM启动垃圾回收机制,自动清除 Sttring在堆还是栈存储
- String a ="123" 不创建对象,在栈中。new String("123") 在堆中创建对象
原子性
- 原子性 即一个操作或者多个操作 要么全部执行并且执行的过程不会被任何因素打断,要么就都不执行。
- 在Java中可以使用synchronized来保证方法和代码块内的操作是原子性的
可见性
- 可见性 是指当多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即看得到修改的值
- 可以使用volatile来保证多线程操作时变量的可见性。
- 除了volatile,Java中的synchronized和final两个关键字也可以实现可见性
有序性
- 有序性:即程序执行的顺序按照代码的先后顺序执行
synchronized
- Synchronized 是Java的一个关键字,使用于多线程并发环境下,可以用来修饰实例对象和类对象,确保在同一时刻只有一个线程可以访问被Synchronized修饰的对象,并且能确保线程间的共享变量及时可见性,还可以避免重排序,从而保证线程安全。
- Synchronized 修饰静态方法 中,锁住的是类对象,所以在多线程中,尽管new了多个实例对象,但是本质上是属于同一个类对象,所以还是存在同步关系
- Synchronized 修饰普通方法 中,锁住的是类的实例对象,所以在多线程中,如果多个线程执行同一个runnable,就存在同步关系,而如果new了多个实例对象,且线程间各自执行不同的runnable,线程之间就不存在同步关系了。
volatile
一个共享变量(类的成员变量、类的静态成员变量)被volatile修饰之后
- 保证了不同线程对这个变量进行操作时的可见性,即一个线程修改了某个变量的值,这新值对其他线程来说是立即可见的。
- 禁止进行指令重排序。
volatile与synchronized 区别
- volatile本质是告诉JVM当前变量在寄存器中的值是不确定的,需要从主存中读取。synchronized则是锁定当前变量,只有当前线程可以访问该变量,其它线程被阻塞。
- volatile仅能使用在变量级别,synchronized则可以使用在变量、方法。
- volatile仅能实现变量修改的可见性,而synchronized则可以保证变量修改的可见性和原子性。
- volatile不会造成线程阻塞,synchronized会造成线程阻塞。
- 使用volatile而不是synchronized的唯一安全情况是 类中只有一个可变的域。 当一个域的值依赖于它之前的值时,volatile就无法工作了,如n=n+1,n++等
线程
-
多线程中sleep和wait的区别?参考连接
- sleep是Thread的静态方法;wait是Object中的方法;
- sleep过程中不会释放锁,不会让出系统资源;wait会释放锁资源,将其放入等待池中,让出系统资源,让cpu可以执行其他线程;
- sleep之后可以主动释放锁;wait需要手动去notify
- sleep方法不依赖于同步器synchronized,但是wait需要依赖synchronized关键字。
-
线程池
- 重用已存在线程,减小创建、销毁的消耗。
- 复用已存在线程,可提高相应速度
- 管控线程数,防止线程太多导致OOM
- newFixedThreadPool:它返回了一个corePoolSize和maximumPoolSize大小一样的,并且使用了LinkedBlockingQueue任务队列的线程池。同时,它使用无界队列存放无法立即执行的任务,当任务提交非常频繁的时候,该队列可能迅速膨胀,从而耗尽系统资源。
- newSingleThreadExecutor:简单的将线程池线程数量设置为1,操作一个无界的工作队列,所以他能保证了所有任务都是被顺序执行。
- newCacheThreadPool:返回corePoolSize为0,maximumPoolSize无穷大的线程池。它是一种用来处理大量短时间工作任务的线程池。
-
自定义线程池
public ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue) { // corePoolSize(核心线程) // maximumPoolSize(线程池最大大小) // keepAliveTime(线程存活保持时间) // workQueue(任务队列) // threadFactory(线程工厂) // handler(线程饱和策略) this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue, Executors.defaultThreadFactory(), defaultHandler); }- 线程数小于 CorePoolSize 则创建新的,否则加入队列。
- 队列是否已满,未满继续放入队列,满了若小于maxPoolSize则创建新线程。
- 若大于maxPoolSize则 执行饱和策略。
-
为什么使用阻塞队列
- 有了阻塞队列就不一样了,它会对当前线程产生阻塞
- 比如一个线程从一个空的阻塞队列中取元素,此时线程会被阻塞直到阻塞队列中有了元素。当队列中有元素后,被阻塞的线程会自动被唤醒(不需要我们编写代码去唤醒)。这样提供了极大的方便性。
- 配置线程池
- CPU密集型任务 : 尽量使用较小的线程池,一般为CPU核心数+1。 因为CPU密集型任务使得CPU使用率很高,若开过多的线程数,会造成CPU过度切换。
- IO密集型任务:可以使用稍大的线程池,一般为2*CPU核心数。 IO密集型任务CPU使用率并不高,因此可以让CPU在等待IO的时候有其他线程去处理别的任务,充分利用CPU时间
- execute()和submit()区别
- execute(),执行一个任务,没有返回值。
- submit(),提交一个线程任务,有返回值。
- 饱和策略
- Abort策略:默认策略,新任务提交时直接抛出未检查的异常RejectedExecutionException
- CallerRuns策略:为调节机制,既不抛弃任务也不抛出异常,而是将某些任务回退到调用者。不会在线程池的线程中执行新的任务,而是在调用exector的线程(可能是主线程)中运行新的任务。
- Discard策略:新提交的任务被抛弃。