- 什么是指令重排序
- volatile的使用与用处
- synchornized与Lock的使用
- Callable的使用
- 可重入锁,公平锁, 读写锁,
- 线程组,线程池,是什么
指令重排序
一般来说,处理器为了提高程序运行效率,可能会对输入代码进行优化,它不保证程序中各个语句的执行先后顺序同代码中的顺序一致,但是它会保证程序最终执行结果和代码顺序执行的结果是一致的。
在Java内存模型中,允许编译器和处理器对指令进行重排序,但是重排序过程不会影响到单线程程序的执行,却会影响到多线程并发执行的正确性。
volatile
volatile的深入介绍
一旦一个共享变量(类的成员变量、类的静态成员变量)被volatile修饰之后,那么就具备了两层语义:
- 保证了不同线程对这个变量进行操作时的可见性,即一个线程修改了某个变量的值,这新值对其他线程来说是立即可见的。
- 禁止进行指令重排序。
volatile原理
在java 的内存模型里面(JMM)。临时变量会被每个线程从主内存中读取一份放到自己的高速缓存中。单线程的时候不会出现问题,单当多个线程的时候就会出现可见性问题。
例如,线程A与线程B都想对变量o = 0进行加1, 结果应当是2。 但是由于这两个线程都把o复制到了自己的高速缓存中,都是在0的基础上加一, 导致最后的结果等于1。
实现原理
- 使用volatile关键字会强制将修改的值立即写入主存;
- 用volatile关键字的话,当线程2进行修改时,会导致线程1的工作内存中缓存变量stop的缓存行无效(反映到硬件层的话,就是CPU的L1或者L2缓存中对应的缓存行无效);
- 由于线程1的工作内存中缓存变量stop的缓存行无效,所以线程1再次读取变量stop的值时会去主存读取。
volatile保证原子性吗
public volatile int inc = 0;
volatile主要保证的是可见性,并不保证原子性。在多个线程进行inc++时,是保证不了inc执行正确的,因为累计并不是原子操作, 实现累计原子性的方法有:
- synchornized
- Lock
volatile能保证有序性吗?
volatile会禁止重排序,所有在一定程度上保证有序性。
volatile关键字禁止指令重排序有两层意思:
- 当程序执行到volatile变量的读操作或者写操作时,在其前面的操作的更改肯定全部已经进行,且结果已经对后面的操作可见;在其后面的操作肯定还没有进行;
- 在进行指令优化时,不能将在对volatile变量访问的语句放在其后面执行,也不能把volatile变量后面的语句放到其前面执行。
使用volatile关键字的场景
synchronized关键字是防止多个线程同时执行一段代码,那么就会很影响程序执行效率,而volatile关键字在某些情况下性能要优于synchronized,但是要注意volatile关键字是无法替代synchronized关键字的,因为volatile关键字无法保证操作的原子性。通常来说,使用volatile必须具备以下2个条件:
- 对变量的写操作不依赖于当前值
- 该变量没有包含在具有其他变量的不变式中
实际上,这些条件表明,可以被写入 volatile 变量的这些有效值独立于任何程序的状态,包括变量的当前状态。
事实上,我的理解就是上面的2个条件需要保证操作是原子性操作,才能保证使用volatile关键字的程序在并发时能够正确执行。
下面列举几个Java中使用volatile的几个场景。
1.状态标记量
volatile boolean flag = false;
while(!flag){
doSomething();
}
public void setFlag() {
flag = true;
}
volatile boolean inited = false;
//线程1:
context = loadContext();
inited = true;
//线程2:
while(!inited ){
sleep()
}
doSomethingwithconfig(context);
2.double check
class Singleton{
private volatile static Singleton instance = null;
private Singleton() {
}
public static Singleton getInstance() {
if(instance==null) {
synchronized (Singleton.class) {
if(instance==null)
instance = new Singleton();
}
}
return instance;
}
}
Synchronized 与ReenTrantLock的区别
- 「锁的实现:」 synchronized是Java语言的关键字,基于JVM实现。而ReentrantLock是基于JDK的API层面实现的(一般是lock()和unlock()方法配合try/finally 语句块来完成。)
- 「性能:」 在JDK1.6锁优化以前,synchronized的性能比ReenTrantLock差很多。但是JDK6开始,增加了适应性自旋、锁消除等,两者性能就差不多了。
- 「功能特点:」 ReentrantLock 比 synchronized 增加了一些高级功能,如等待可中断、可实现公平锁、可实现选择性通知。
- ReentrantLock提供了一种能够中断等待锁的线程的机制,通过lock.lockInterruptibly()来实现这个机制。
- ReentrantLock可以指定是公平锁还是非公平锁。而synchronized只能是非公平锁。所谓的公平锁就是先等待的线程先获得锁。
- synchronized与wait()和notify()/notifyAll()方法结合实现等待/通知机制,ReentrantLock类借助Condition接口与newCondition()方法实现。
- ReentrantLock需要手工声明来加锁和释放锁,一般跟finally配合释放锁。而synchronized不用手动释放锁。
线程组
线程组的作用是:可以批量管理线程或线程组对象,有效地对线程或线程组对象进行组织
public class ThreadGroupDemo {
public static void main(String[] args) {
Runnable run = new Runnable() {
@Override
public void run() {
String GroupAndNAme = Thread.currentThread().getThreadGroup().getName()
+ "-" + Thread.currentThread().getName();
while (true){
System.out.println("I am " + GroupAndNAme);
try {
Thread.sleep(3000);
}catch (InterruptedException e){
e.printStackTrace();
}
}
}
};
ThreadGroup tg = new ThreadGroup("printGroup");
new Thread(tg, run, "t1").start();
new Thread(tg, run, "t2").start();
System.out.println(tg.activeCount());
tg.list();
}
}
重入锁
这里的重入锁是指java.util.conurrent.locks.Reentrant。 Reentrant是可以完全替代synchronized的。
常用方法:
- lock(): 获得锁,如果锁以及被占领,则等待
- lockInterruptibly() : 获得锁,但优先响应中断。
- tryLock(): 尝试获得锁,如果超过,返回true,失败返回false.该方法不等待,立即返回。
- tryLock(long time, TimeUnit umit); 在给定时间内尝试获得锁。
- unlock(); 释放锁
公平锁
非公平锁,是哪个线程抢到就是哪个的,会产生饥饿问题,效率相对较高,
而公平锁是安装先来先得,保证先到着先得,后到者后得。不会产生饥饿问题,效率相对较低
线程池
什么是线程池
因为频繁的创建与销毁进程会很消耗性能, 所有用一个类似与集合的东西,把许多线程放进去,当我们要线程的时候,我们从线程池中取线程出来,而当我们不需要这个线程的时候,把线程还给线程池。
线程池的使用
JDK提供了一套Executor框架来控制多线程,在java.util.concurrent.包下面,也就是我们所说的JUC下面。 Executor就是一个工厂类,我们可以通过这个工程类获得各类型的线程池:
- newFixedThreadPool() ; 返回一个固定数量的线程池,当提交任务时,有空闲线程时,立即执行,而没有空闲线程时。并不会在线程池中添加新线程,而是等待线程执行完毕后,在执行
- newSingleThreadExecutor(); 其实这个就是newFixedPool(1);一样,就是内部只有一个线程的版本,只能等上个任务执行完毕后再执行。
- newCachedThreadPool(); 初始线程数量不确定,当有空闲线程时,使用空闲线程,而所有线程都在工作时,会新建线程,新建的线程执行完毕后,也会放入线程池。
- newScheduledThreadPool() 创建一个可定期或者延时执行任务的定长线程池,支持定时及周期性任务执行。
- newSingleScheduledThreadPool(), 这个是newScheduledThreadPool()线程池为1的版本。
计划任务
newScheduledThreadPool, 这个就是可以用来做定时任务的线程池,使用ScheduledExecutorService 类来接收,这里介绍三个定时参数
- schedule(commod,delay,unit)。 根据设定的时间执行一次。
- scheduleAtFixedRate(commod,initialDelay,period,unit), 周期性的执行, 不考虑任务时间,假如设定的任务间隔大于任务运行的时间,则按照设定的间隔时间执行,假如任务时间大于设定的任务间隔时间则每次任务结束后马上开始下一次任务。
- scheduleWithFixedDelay, 不管任务执行了多少时间,下一个任务都是以上一个任务执行完毕后开始计时。
Fork/Join框架
分而思想, 就是当有一个很大的问题,可以把他划分成很多个小问题来解决,最后汇总。
使用案例
class CountTask extends RecursiveTask<Long>{
private static final int THRESHOLD = 10000;
private long start;
private long end;
public CountTask(long start, long end){
this.start = start;
this.end = end;
}
@Override
protected Long compute() {
long sum = 0;
boolean canCpmpute = (end-start)<THRESHOLD;
if(canCpmpute){
for(long i = start; i <= end; i++){
sum += i;
}
}else{
long step = (start+end) / 100;
ArrayList<CountTask> subTasks = new ArrayList<CountTask>();
long pos = start;
for (int i = 0; i < 100; i++) {
long lastOne = pos+step;
if(lastOne > end){ lastOne = end;}
CountTask subTask = new CountTask(pos, lastOne);
pos += step + 1;
subTasks.add(subTask);
subTask.fork();
}
for (CountTask t : subTasks){
sum += t.join();
}
}
return sum;
}
}
public class ForkAndJoinDemo01 {
public static void main(String[] args) throws ExecutionException, InterruptedException {
ForkJoinPool forkJoinPool = new ForkJoinPool();
CountTask task = new CountTask(0, 20010L);
ForkJoinTask<Long> result = forkJoinPool.submit(task);
long res = result.get();
System.out.println("sum="+res);
}
}
可以向ForkJoinTask提交一个ForkJoinTask任务。而ForkJoinTask 有两个子类,RecursiveTask 有返回值与RecusiveAction没有返回值。