volatile
volatile关键字的作用是保证变量在多线程之间的可见性,他也可以保证有序性。它是java并发开发的核心基础。
-
1.valatile的两层语义 一旦一个共享变量被volatile修饰后, 1) 不同线程对这个变量进行操作后,都会立即可见。(修改后,会立即写入主存,会使其他线程的缓存无效)–>可见性 2) 禁止进行指令重排序。(重排序时,在volatile变量之前的操作已经完成,在volatile之后的没有进行)–>有序性
-
2.实现
有valatile修饰的变量,在其汇编代码之前可以看到 lock,该lock(内存屏障)会触发两件事
- 将当前处理器缓存行的数据立即写回到系统内存。(一般是写到缓存后,是不确定什么时候会写到内存)
- 这个写回内存的操作会引起在其他CPU里缓存了该内存地址的数据无效。
- 它确保指令重排序时不会把其后面的指令排到内存屏障之前的位置,也不会把前面的指令排到内存屏障的后面;即在执行到内存屏障这句指令时,在它前面的操作已经全部完成
- 3.使用条件 对变量的写操作不依赖于当前值 该变量没有包含在具有其他变量的不变式中
volatile应用场景
状态标记量,如果flag没有用volatile来修饰,就存在某个线程的stop()
方法修改了flag值,但是不保证确定性,并不会立即刷新到主内存中,导致其他的线程不会立即停止。
private volatile boolean flag ;
private void run(){
new Thread(new Runnable() {
@Override
public void run() {
while (flag) {
doSomeThing();
}
}
});
}
private void stop(){
flag = false ;
}
双重检查锁的单例模式
public class Singleton {
private static volatile Singleton singleton;
private Singleton() {
}
public static Singleton getInstance() {
if (singleton == null) {
synchronized (Singleton.class) {
if (singleton == null) {
singleton = new Singleton();
}
}
}
return singleton;
}
}
我们注意这里volatile的用法是为了避免指令重排,对于singleton=new Singleton();
其代码执行其实分为3个步骤:
- 为singleton分配内存空间
- 初始化singleton
- 将singleton指向分配的内存地址
当多线程并发调用getInstance()时,如果不使用volatile,会出现T1执行了1,3,2。T2访问时发现singleton!=null,从而返回singleton,然后此时singleton并未初始化的问题。
线程
Thread是操作系统能够进行调度的最小单元。
线程与进程相似,但线程是一个比进程更小的执行单位。一个进程在其执行的过程中可以产生多个线程。
与进程不同的是同类的多个线程共享同一块内存空间和一组系统资源,所以系统在产生一个线程,或是在各个线程之间作切换工作时,负担要比进程小得多,也正因为如此,线程也被称为轻量级进程。
另外,也正是因为共享资源,所以线程中执行时一般都要进行同步和互斥
。
线程的状态转换
线程具备多个状态:
- 新建(New):创建后尚未启动。
- 可运行(Runnable):对于jvm是正在执行状态,但是对于CPU而言肯定还在等待(ready+running)。
- 阻塞(Blocked):线程被阻塞,访问的资源锁被别人获取到,一直等待到锁释放。synchronized I/O block
- 无限期等待 object对象.wait() thread对象.join() LockSupport.park();
- 有限期等待 Thread.sleep(1000) object对象.wait() thread对象.join() LockSupport.parkNanos LockSupport.parkUntil
- 死亡(Terminated):可以是线程结束任务之后自己结束,或者产生了异常而结束。
通过 Object.notify()或Object.notifyAll() 睡眠时间结束 Thread.yield() 被调用的线程执行完、LockSupport.unpark(Thread) 结束等待
其中我们需要分别阻塞和等待。
阻塞是被动的,它在等待一个排它锁;而等待是主动的,由使用者调用启动。
使用线程
- 实现Runnable接口,无返回值
- 实现Callable接口,有返回值
FutureTask<T>
- 继承Thread类,实现run()方法。(并不推荐,Java 不支持多重继承,因此继承了 Thread 类就无法继承其它类;类可能只要求可执行就行,继承整个 Thread 类开销过大。)
使用匿名类实现:
public static void main(String[] args) {
Thread thread1=new Thread(new Runnable() {
@Override
public void run() {
System.out.println(1);
}
});
FutureTask<Integer> ft=new FutureTask<Integer>(new Callable<Integer>() {
@Override
public Integer call() throws Exception {
return 1;
}
});
Thread thread2=new Thread(ft);
thread2.start();
System.out.println(ft.get());
}
在java8中我们可以使用Lambda:
public static void main(String[] args) {
Thread threadLambda1=new Thread(() -> System.out.println(1);
FutureTask<Integer> ft=new FutureTask<Integer>(() -> 1);
Thread threadLambda2=new Thread(ft);
threadLambda2.start();
System.out.println(ft.get());
}
Future 和 FutureTask
Future
就是对于具体的Runnable或者Callable任务的执行结果进行取消、查询是否完成、获取结果等操作。
Future
接口 声明了5个方法,分别是:
1) cancel() 用来取消任务,如果取消任务成功则返回true,如果取消任务失败则返回false。 2) isCancelled() 表示任务是否被取消成功。 3) isDone() 表示任务是否完成。 4) get() 获取任务的执行结果,这个方法会产生 阻塞 ,一直等到任务执行完毕才返回。 5) get(long timeout,TimeUnit unit) 获取任务执行结果,如果在timeout时间内没有获取到结果,就直接返回null。
Future可以判断任务是否完成,能够中断任务,能够获取任务执行结果。但是future只是接口,是无法直接用来创建对象使用的,因此有了FutureTask。
FutureTask
实现了 RunnableFuture
1)FutureTask是Future接口的一个唯一实现类。 2)FutureTask实现了Runnable,因此它既可以通过Thread包装来直接执行,也可以提交给ExecuteService来执行。
public interface ExecutorService extends Executor {
<T> Future<T> submit(Callable<T> task);//常用
<T> Future<T> submit(Runnable task, T result);
Future<?> submit(Runnable task);//常用
<T> List<Future<T>> invokeAll(Collection<? extends Callable<T>> tasks);
<T> T invokeAny(Collection<? extends Callable<T>> tasks);
}
3)FutureTask实现了Futrue可以直接通过get()函数获取执行结果,该函数会阻塞,直到结果返回。
那么我们的使用流程是什么:
1.创建Runnable实现类对象或者Callable实现类对象。
2.创建FutureTask对象,将Runnable实现类对象或者Callable实现类对象作为参数、
3.创建Thread Thread thread=new Thread(futureTask)
,利用thread对象.start()启动,或者创建线程池ExecutorService executor = Executors.newCachedThreadPool()
,
利用excutor.submit(futureTask)启动针对Callable 有返回值, excutor.execute(futureTask)针对Runnable,无返回值。
4.利用futureTask.get()获取真正的结果。
当然可以参考上面的做法,使用lambda简化你的代码。
线程的方法
Daemon
在线程启动之前使用 setDaemon() 方法可以将一个线程设置为守护线程
public static void main(String[] args) {
Thread thread = new Thread(new MyRunnable());
thread.setDaemon(true);
}
守护线程是服务其他线程的线程。垃圾回收线程就是一个典型的守护线程。
Main函数是一个典型的用户线程,当用户线程全部结束,其守护线程会自动结束。 用户自定义的线程,默认都是用户线程。
sleep()
Thread.sleep(millisec) 方法会休眠当前正在执行的线程,millisec 单位为毫秒。需要处理打断的异常
public void run() {
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
sleep(时间)方法需要指定等待的时间,它可以让当前正在执行的线程在指定的时间内暂停执行,进入阻塞状态。可以让其他同优先级或者高优先级的线程得到执行的机会,也可以让低优先级的线程得到执行机会。
但是sleep()方法不会释放“锁标志”,也就是说如果有synchronized同步块,其他线程仍然不能访问共享数据。
yield()
对静态方法 Thread.yield() 的调用声明了当前线程已经完成了生命周期中最重要的部分,可以切换给其它线程来执行。 该方法只是对线程调度器的一个建议,而且也只是建议具有相同优先级的其它线程可以运行。
yield()
让线程从正在运行,进行runnale状态。
public void run() {
Thread.yield();
}
yield()方法和sleep()方法类似,也不会释放“锁标志”,区别在于,它没有参数,即yield()方法只是使当前线程重新回到可执行状态,所以执行yield()的线程有可能在进入到可执行状态后马上又被执行,另外yield()方法只能使同优先级或者高优先级的线程得到执行机会
,这也和sleep()方法不同。
join()
在线程中调用另一个线程的 join() 方法,会将当前线程挂起,而不是忙等待,直到目标线程结束。
Object的方法
wait()方法需要和notify()及notifyAll()两个方法一起介绍,这三个方法用于协调多个线程对共享数据的存取,所以必须在synchronized
语句块内使用,也就是说,调用wait(),notify()和notifyAll()的任务在调用这些方法前必须拥有对象的锁。
注意,它们都是Object类的方法,而不是Thread类的方法。
wait()
调用对象的wait()方法,会让当前线程阻塞,并释放锁。调用wait方法将会将调用者的线程挂起,直到其他的线程调用同一个对象的notify()防范才会重新激活。
notify()
调用某个对象的notify()方法能够唤醒一个正在等待这个对象的monitor的线程,如果有多个线程都在等待这个对象的monitor,则只能唤醒其中一个线程;
notifyAll()
调用notifyAll()方法能够唤醒所有正在等待这个对象的monitor的线程;
public class WaitNotifyExample {
public synchronized void before() {
System.out.println("before");
notifyAll();
}
public synchronized void after() {
try {
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("after");
}
}
public static void main(String[] args) {
ExecutorService executorService = Executors.newCachedThreadPool();
WaitNotifyExample example = new WaitNotifyExample();
executorService.execute(() -> example.after());//wait
executorService.execute(() -> example.before());//notifyall()
}
before
after
被notify()唤醒的线程可以立即得到执行吗?
被notify唤醒的线程不是立刻可以得到执行的,因为notify()不会立刻释放锁,wait()状态的线程也不能立刻获得锁;等到执行notify()的线程退出同步块后,才释放锁,此时其他处于wait()状态的线程才能获得该锁。
wait()和sleep()的区别
- wait() 是 Object 的方法,而 sleep() 是 Thread 的静态方法;
- wait() 会释放锁,sleep() 不会。
wait和notify实现交替打印
public class Main {
private static Object object = new Object();
public static void main(String arg[]) {
Thread t1 = new Thread(() -> {
String[] string = {"a", "b", "c", "d"};
for (int i = 0; i < string.length; i++) {
synchronized (object) {
object.notify();
try {
object.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.print(string[i]);
object.notify();
}
}
});
Thread t2 = new Thread(() -> {
String[] string = {"1", "2", "3", "4"};
for (int i = 0; i < string.length; i++) {
synchronized (object) {
//线程2开始执行
object.notify();
try {
//线程2开始等待
object.wait();
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
//线程2继续执行
System.out.print(string[i]);
//线程2运行结束
object.notify();
}
}
});
t1.start();
t2.start();
}
}
await() signal() signalAll()
wait(),notify()及notifyAll()只能在synchronized语句中使用; 但是如果使用的是ReenTrantLock实现同步,该如何达到这三个方法的效果呢? 解决方法是使用java.util.concurrent.ReenTrantLock.newCondition()获取一个Condition类对象, 然后Condition的await(),signal()以及signalAll()分别对应上面的三个方法。
public class AwaitSignalExample {
private Lock lock = new ReentrantLock();
private Condition condition = lock.newCondition();
public void before() {
lock.lock();
try {
System.out.println("before");
condition.signalAll();
} finally {
lock.unlock();
}
}
public void after() {
lock.lock();
try {
condition.await();
System.out.println("after");
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
}
实现生产者-消费者模型?
可以有几种方式实现生产者-消费者模型:
-
wait()/notify()
-
await()/signal()
-
BlockingQueue
生产者-消费者问题的关键在于:
- 没有“产品”时,消费者不能消费
- “产品”线满时,生产者不能生产
-
如果用队列来存放“产品”:
- 队列为空时,消费者需要一直等待,不为空时消费者才能取走。
- 队列为满时,生产者需要一直等待,不为满时生产者才能进行生产。
线程的打断
一个线程执行完毕之后会自动结束,如果在运行过程中发生异常也会提前结束。
thread.interrupt()
ThreadLocal
Thread类中有一个成员变量属于ThreadLocalMap类(一个定义在ThreadLocal类中的内部类),它是一个Map,他的key是ThreadLocal实例对象。
当为ThreadLocal类的对象set值时,首先获得当前线程的ThreadLocalMap类属性,然后以ThreadLocal类的对象为key,设定value。get值时则类似。
ThreadLocal变量的活动范围为某线程,是该线程“专有的,独自霸占”的,对该变量的所有操作均由该线程完成!也就是说,ThreadLocal 不是用来解决共享对象的多线程访问的竞争问题的,因为ThreadLocal.set() 到线程中的对象是该线程自己使用的对象,其他线程是不需要访问的,也访问不到的。当线程终止后,这些值会作为垃圾回收。
由ThreadLocal的工作原理决定了:每个线程独自拥有一个变量,并非是共享的。
ThreadLocal的内存泄露
由于ThreadLocalMap的key是弱引用,而Value是强引用。这就导致了一个问题,ThreadLocal在没有外部对象强引用时,发生GC时弱引用Key会被回收,而Value不会回收,如果创建ThreadLocal的线程一直持续运行,那么这个Entry对象中的value就有可能一直得不到回收,发生内存泄露。 如何避免泄漏 既然Key是弱引用,那么我们要做的事,就是在调用ThreadLocal的get()、set()方法时完成后再调用remove方法,将Entry节点和Map的引用关系移除,这样整个Entry对象在GC Roots分析后就变成不可达了,下次GC的时候就可以被回收。 如果使用ThreadLocal的set方法之后,没有显示的调用remove方法,就有可能发生内存泄露,所以养成良好的编程习惯十分重要,使用完ThreadLocal之后,记得调用remove方法。
ThreadLocal<Session> threadLocal = new ThreadLocal<Session>();
try {
threadLocal.set(new Session(1, "Misout的博客"));
// 其它业务逻辑
} finally {
threadLocal.remove();
}
应用
数据库连接、Session管理、HttpRequest
线程池
线程应该通过线程池提供,不允许在应用中自行显式创建线程。
说明:使用线程池的好处是减少在创建和销毁线程上所花的时间以及系统资源的开销,解决资源不足的问题。如果不使用线程池,有可能造成系统创建大量同类线程而导致消耗完内存或者“过度切换”的问题。
线程资源必须通过线程池提供,不能在应用中自行显示的创建线程。线程过多会调来调度的开销,进而影响缓存局部性和整体功能。
线程池维护多个线程,等待着监督管理者分配可并发执行的任务,这避免了在处理短时间任务时创建与销毁线程的代价。
线程池一般有以下的几个目的:
- 线程是稀缺资源,不能频繁创建。
- 解耦的作用:线程的创建于执行完全分开,方便维护。
- 线程可以为其他任务进行复用。
当然我们在配置线程池的时候,并不是线程池越大越好,通常我们需要根据任务的性质来确定参数:
- IO密集型的任务:由于线程不是一直在运行,所以可以尽可能的多配置线程。
- CPU密集型的任务: 由于线程一直在运行(CPU进行大量的计算),应当分配较少的线程。
原理
线程池的核心在于 预先创建线程,在任务到来之前,创建一定数量的线程,放入空闲队列中。
当请求来到时,线程池为这次请求分配一个空闲线程,并在此线程中运行。
如果当前线程数为corePoolSize,继续提交的任务会被保存到阻塞队列,等待执行。
如果阻塞队列满了,就创建新的进程执行任务,直到线程池中的线程数达到maxPoolSize,这时如果还有任务,则由饱和策略来处理提交的任务。
拒绝策略包括:
ThreadPoolExecutor.AbortPolicy:丢弃任务并抛出RejectedExecutionException异常。
ThreadPoolExecutor.DiscardPolicy:也是丢弃任务,但是不抛出异常。
ThreadPoolExecutor.DiscardOldestPolicy:丢弃队列最前面的任务,然后重新尝试执行任务(重复此过程)
ThreadPoolExecutor.CallerRunsPolicy:由调用线程处理该任务
Executors
因此Executors类里面提供了一些静态工厂,以生成一些常用的线程池。包括:
- newSingleThreadExecutor() 单线程的线程池,容量为1,也就是相当于单线程串行执行所有任务。队列为LinkedBlockingQueue 无界。
- newCachedThreadPool() 创建一个可缓存的线程池,容量大小为0~Integer.Max_Value,来一个任务创建一个线程,当线程空闲超过一段时间后,就销毁线程。 SynchronousQueue队列 无界。
- newFixedThreadPool(int nThreads) 创建固定大小的线程池,corePoolSize==maxPoolSize==nThreads,可以控制最大的并发数为 nThreads,多余的任务会放置到队列中等待,队列为LinkedBlockingQueue 无界。
- newScheduledThreadPool(int size) 创建size~Integer.MaxValue的线程池,支持定时及周期性任务执行,比如延时1S操作和每1S执行一次。队列DelayedWorkQueue
使用
- execute(Runnable) 接收一個 java.lang.Runnable 对象作为参数,并且以异步的方式执行它。使用这种方式没有办法获取执行 Runnable 之后的结果,如果你希望获取运行之后的返回值。
- submit(Runnable) 方法 submit(Runnable) 同样接收1个 Runnable 的实现作为参数,但是会返回壹1个Future 对象。这個 Future 对象可以用于判断 Runnable 是否结束执行。
ThreadPoolExecutor
Executors类
创建的线程池,为你屏蔽了部分构建细节,因此你会忽略一些可能存在的潜在问题如:
- 针对newSingThreadExecutor 会出现堆积的请求处理队列过大,出现OOM;
- 针对newCachedThreadPool会创建Integer.MAX_VALUE个线程,出现OOM。
因此不建议使用Executors去创建,而是通过ThreadPoolExecutor
的方式,这样的处理方式能让写的同学更加明确线程池的运行规则,规避资源耗尽的风险。
我们在配置线程池的时候,需要思考这么几个问题?
- 线程池里面的线程什么时候创建;
- 线程池大小怎么确定;
- 线程要定时执行吗
- 线程的优先级呢.
public ThreadPoolExecutor(
int corePoolSize,
int maximumPoolSize,
long keepAliveTime,//非核心线程能够空闲的最长时间,超过时间,线程终止。这个参数默认只有在线程数量超过核心线程池大小时才会起作用。
TimeUnit unit,//时间单位
BlockingQueue<Runnable> workQueue//缓存阻塞队列 阻塞队列的类型有 LinkedBlockingQueue(无界) SynchronousQueue(默认,无界) ArrayBlockingQueue(有界)
ThreadFactory threadFactory,//线程工厂
RejectedExecutionHandler handler//拒绝策略
);
线程互斥同步
Java 提供了两种锁机制来控制多个线程对共享资源的互斥访问,第一个是 JVM 实现的 synchronized,而另一个是 JDK 实现的 ReentrantLock。
synchronized
synchronized
是一种同步锁,是JVM的实现,被synchronized修饰的东西,在同一个瞬间,只能有一个线程可以进行访问,其中线程会受到阻塞,只有待当前线程执行完以后才能执行。
我们这里称之为线程是互斥的,因为在执行synchronized代码块时会锁定当前的对象,只有执行完该代码块才能释放该对象锁,下一个线程才能执行并锁定该对象。 他可以修饰一下的对象:
- 代码块,被修饰的代码称为同步代码块,作用的是调用这个代码块的对象。 当两个并发线程(thread1和thread2)访问同一个对象(syncThread)中的synchronized代码块时,在同一时刻只能有一个线程得到执行,另一个线程受阻塞,必须等待当前线程执行完这个代码块以后才能执行该代码块。Thread1和thread2是互斥的,因为在执行synchronized代码块时会锁定当前的对象,只有执行完该代码块才能释放该对象锁,下一个线程才能执行并锁定该对象。(如果是不同的对象,那么会产生两个锁,从而并不会互斥)
- 方法,被修饰的代码称为同步方法.
其与代码块synchronized是一致的
public synchronized void method() { // todo } public void method() { synchronized(this) { // todo } }
- 对象 这时,当一个线程访问对象时,其他试图访问对象的线程将会阻塞,直到该线程访问对象结束。也就是说谁拿到那个锁谁就可以运行它所控制的那段代码。
- 静态方法,作用对象是这个类的所有对象。 我们知道静态方法是属于类的而不是属于对象的。因此所有的对象共有一个锁
- 类,作用对象是这个类的所有对象。
class ClassName { public void method() { synchronized(ClassName.class) { // todo } } }
与静态方法一致。
总结: A. 无论synchronized关键字加在方法上还是对象上,如果它作用的对象是非静态的,则它取得的锁是对象;如果synchronized作用的对象是一个静态方法或一个类,则它取得的锁是对类,该类所有的对象同一把锁。 B. 每个对象只有一个锁(lock)与之相关联,谁拿到这个锁谁就可以运行它所控制的那段代码。 C. 实现同步是要很大的系统开销作为代价的,甚至可能造成死锁,所以尽量避免无谓的同步控制。
synchronized
是解决并发问题常用的解决方案,有以下三种使用方式:
- 同步普通方法,锁的是当前对象,进入同步代码前要获取当前对象实例的锁。
- 同步静态方法,锁的当前
Class
对象,进入同步代码前要获取当前类对象的锁。对类进行加锁,会作用于类的所有对象实例。 - 同步块,锁的是代码块内的对象,对给定对象加锁,进入同步代码库前要获得给定对象的锁。
对给定对象加锁,进入同步代码库前要获得给定对象的锁。
同步方法实现原理:为方法添加 ACC_SYNCHRONIZED 标识,该标识指明了该方法是一个同步方法,JVM 通过该 ACC_SYNCHRONIZED 访问标志来辨别一个方法是否声明为同步方法,从而执行相应的同步调用。
同步语句块实现原理:JVM通过进入和退出对象监视器(Monitor)来实现同步的。代码在编译后,会在同步方法调用前面加入一个monitor.enter命令,然后在退出方法和异常处插入monitor.exit的指令。其本质就是对一个对象监视器(Monitor)进行获取,而这个获取过程具有排他性从而达到了同一时刻只能一个线程访问的目的。
我们要认识到synchronized的实现原理属于JVM层面。
在 Java 早期版本中,synchronized 属于重量级锁,效率低下,因为监视器锁(monitor)是依赖于底层的操作系统的 Mutex Lock 来实现的,Java 的线程是映射到操作系统的原生线程之上的。如果要挂起或者唤醒一个线程,都需要操作系统帮忙完成,而操作系统实现线程之间的切换时需要从用户态转换到内核态,这个状态之间的转换需要相对比较长的时间,时间成本相对较高,这也是为什么早期的 synchronized 效率低的原因。庆幸的是在 Java 6 之后 Java 官方对从 JVM 层面对synchronized 较大优化,所以现在的 synchronized 锁效率也优化得很不错了。JDK1.6对锁的实现引入了大量的优化,如自旋锁、适应性自旋锁、锁消除、锁粗化、偏向锁、轻量级锁等技术来减少锁操作的开销。
锁主要存在四种状态,依次是:无锁状态、偏向锁状态、轻量级锁状态、重量级锁状态,他们会随着竞争的激烈而逐渐升级。注意锁可以升级不可降级,这种策略是为了提高获得锁和释放锁的效率。
ReentrantLock
ReentrantLock 是 JDK 实现的,是 java.util.concurrent(J.U.C)包中的锁。是可以中断的。
public class LockExample {
private Lock lock = new ReentrantLock();
public void func() {
lock.lock();
try {
for (int i = 0; i < 10; i++) {
System.out.print(i + " ");
}
} finally {
lock.unlock(); // 确保释放锁,从而避免发生死锁。
}
}
}
public static void main(String[] args) {
LockExample lockExample = new LockExample();
ExecutorService executorService = Executors.newCachedThreadPool();
executorService.execute(() -> lockExample.func());
executorService.execute(() -> lockExample.func());
}
使用原则
除非需要使用 ReentrantLock 的高级功能,否则优先使用 synchronized。这是因为 synchronized 是 JVM 实现的一种锁机制,JVM 原生地支持它,而 ReentrantLock 不是所有的 JDK 版本都支持。并且使用 synchronized 不用担心没有释放锁而导致死锁问题,因为 JVM 会确保锁的释放。
线程通信
线程间通信的主要目的是用于线程同步,所以线程没有象进程通信中用于数据交换的通信机制。
等待通知机制
线程A和线程B都对同一个对象获取锁,A线程调用了同步对象的wait()方法释放了锁,并进入WAITING状态。 B线程调用了notify()方法,这样A线程就能从wait()方法中返回。
有一些注意点:
- wait() 、notify()、notifyAll() 调用的前提都是获得了对象的锁
- 调用 wait() 方法后线程会释放锁,进入 WAITING 状态,该线程也会被移动到等待队列中。
- 调用 notify() 方法会将等待队列中的线程移动到同步队列中
- 从 wait() 方法返回的前提是调用 notify() 方法的线程释放锁,wait() 方法的线程获得锁。
等待通知有一个经典范式:
线程 A 作为消费者: 1.获取对象的锁。 2.进入 while(判断条件),并调用 wait() 方法。 3.当条件满足跳出循环执行具体处理逻辑。
synchronized(Object){
while(条件){
Object.wait();
}
//do something
}
线程 B 作为生产者:
1.获取对象锁。 2.更改与线程 A 共用的判断条件。 3.调用 notify() 方法。
synchronized(Object){
条件=false;//改变条件
Object.notify();
}
线程响应中断
可以采用中断线程的方式来通信,调用了 thread.interrupt() 方法其实就是将 thread 中的一个标志属性置为了 true。
并不是说调用了该方法就可以中断线程,只是标志属性变换。
线程池 awaitTermination() 方法
如果是用线程池来管理线程,可以使用以下方式来让主线程等待线程池中所有任务执行完毕:
1.使用这个 awaitTermination() 方法的前提需要关闭线程池,如调用了 shutdown() 方法。 2.调用了 shutdown() 之后线程池会停止接受新任务,并且会平滑的关闭线程池中现有的任务。
管道通信
Java 虽说是基于内存通信的,但也可以使用管道通信。
需要注意的是,输入流和输出流需要首先建立连接。这样线程 B 就可以收到线程 A 发出的消息了。
//面向于字符 PipedInputStream 面向于字节
PipedWriter writer = new PipedWriter();s
PipedReader reader = new PipedReader();
//输入输出流建立连接
writer.connect(reader);
🔒
🔒啊~~~~
自旋锁和自适应自旋
轻量级锁失败后,虚拟机为了避免线程真实地在操作系统层面挂起,还会进行一项称为自旋锁的优化手段。
互斥同步对性能最大的影响就是阻塞的实现,因为挂起线程/恢复线程的操作都需要转入内核态中完成(用户态转换到内核态会耗费时间)。
一般线程持有锁的时间都不是太长,所以仅仅为了这一点时间去挂起线程/恢复线程是得不偿失的。 所以,虚拟机的开发团队就这样去考虑:“我们能不能让后面来的请求获取锁的线程等待一会而不被挂起呢?看看持有锁的线程是否很快就会释放锁”。为了让一个线程等待,我们只需要让线程执行一个忙循环(自旋),这项技术就叫做自旋。
而自适应的自旋锁带来的改进就是:自旋的时间不在固定了,而是和前一次同一个锁上的自旋时间以及锁的拥有者的状态来决定,虚拟机变得越来越“聪明”了。
锁消除
锁消除理解起来很简单,它指的就是虚拟机即使编译器在运行时,如果检测到那些共享数据不可能存在竞争,那么就执行锁消除。锁消除可以节省毫无意义的请求锁的时间。
锁粗化
原则上,我们再编写代码的时候,总是推荐将同步快的作用范围限制得尽量小——只在共享数据的实际作用域才进行同步,这样是为了使得需要同步的操作数量尽可能变小,如果存在锁竞争,那等待线程也能尽快拿到锁。
偏向锁
JDK 1.6 引入了偏向锁和轻量级锁,从而让锁拥有了四个状态:无锁状态(unlocked)、偏向锁状态(biasble)、轻量级锁状态(lightweight locked)和重量级锁状态(inflated)。
在没有多线程竞争的前提下,减少传统的重量级锁使用操作系统互斥量产生的性能消耗。偏向锁在无竞争的情况下会把整个同步都给消除掉。
偏向锁的”偏”,是偏向于第一个获得它的线程的意思。如果在接下来的执行中,该锁没有被其他线程获取,那么持有偏向锁的线程就不需要进行同步!
但是对于锁竞争比较激烈的场合,偏向锁就失效了,因为这样场合极有可能每次申请锁的线程都是不相同的,因此这种场合下不应该使用偏向锁,否则会得不偿失,需要注意的是,偏向锁失败后,并不会立即膨胀为重量级锁,而是先升级为轻量级锁。
轻量级锁
轻量级锁不是为了代替重量级锁,它的本意是在没有多线程竞争的前提下,减少传统的重量级锁使用操作系统互斥量产生的性能消耗,因为使用轻量级锁时,不需要申请互斥量。另外,轻量级锁的加锁和解锁都用到了CAS操作。
轻量级锁能够提升程序同步性能的依据是“对于绝大部分锁,在整个同步周期内都是不存在竞争的”,这是一个经验数据。如果没有竞争,轻量级锁使用 CAS 操作避免了使用互斥操作的开销。但如果存在锁竞争,除了互斥量开销外,还会额外发生CAS操作,因此在有锁竞争的情况下,轻量级锁比传统的重量级锁更慢!如果锁竞争激烈,那么轻量级将很快膨胀为重量级锁!
重入锁
重入锁:一个线程获取了锁之后,仍然可以反复的加锁,不会出现自身阻塞自己的情况。同一个线程每次获取锁,锁的计数器都自增1,所以要等到锁的计数器下降为0时才能释放锁。
同一个线程可以多次获得同一个锁,即一个线程获取到锁之后可以无限次地进入该临界区 (对于ReentrantLock来说,通过调用lock.lock());当然锁的释放也需要相同次数的unlock()操作。注意:除了ReentrantLock,synchronized的锁也是可重入的。
ReentrantLock
ReentrantLock
获取锁的时候会判断当前线程是否为获取锁的线程,如果是,则将同步的状态+1,释放锁的时候将状态-1。只有同步状态的次数为0时,才会最终释放锁。
ReentrantLock 就是一个普通的类,它是基于 AQS(AbstractQueuedSynchronizer)来实现的。需要lock(),unlock()方法配合try/finally语句块来实现。
ReenTrantLock 比 synchronized 增加了一些高级功能:
- 等待可中断,通过lock.lockInterruptibly()来实现这个机制。也就是说正在等待的线程可以选择放弃等待,改为处理其他事情。
- 公平锁和非公平锁,ReenTrantLock可以指定是公平锁还是非公平锁。而synchronized只能是非公平锁。所谓的公平锁就是先等待的线程先获得锁。
- 可实现选择性通知,线程对象可以注册在指定的Condition中,从而可以有选择性的进行线程通知,在调度线程上更加灵活。 在使用notify/notifyAll()方法进行通知时,被通知的线程是由 JVM 选择的,用ReentrantLock类结合Condition实例可以实现“选择性通知” 。
synchronized
使用 synchronized 来做同步处理时,锁的获取和释放都是隐式的,实现的原理是通过编译后加上不同的机器指令来实现。
非公平锁
//默认非公平锁
public ReentrantLock() {
sync = new NonfairSync();
}
公平锁
//公平锁
public ReentrantLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
}
公平锁和非公平锁的区别
公平锁就相当于买票,后来的人需要排到队尾依次买票,不能插队
。
而非公平锁则没有这些规则,是抢占模式
,每来一个人不会去管队列如何,直接尝试获取锁。
由于公平锁需要关心队列的情况,得按照队列里的先后顺序来获取锁(会造成大量的线程上下文切换),而非公平锁则没有这个限制。 所以也就能解释非公平锁的效率会被公平锁更高。
ReenTrantLock 可以实现公平锁,而synchronized只能是非公平锁。
JUC
java.util.concurrent(J.U.C)大大提高了并发性能,AQS 被认为是 J.U.C 的核心。
AQS
AQS(AbstractQueueSynchronizer),位于java.util.concurrent.locks包下。
AQS是一个用来构建锁和同步器的框架,使用AQS能简单且高效地构造出应用广泛的大量的同步器,比如我们提到的ReentrantLock,Semaphore,其他的诸如ReentrantReadWriteLock,SynchronousQueue,FutureTask等等皆是基于AQS的。当然,我们自己也能利用AQS非常轻松容易地构造出符合我们自己需求的同步器。
AQS原理
AQS核心思想是,如果被请求的共享资源空闲,则将当前请求资源的线程设置为有效的工作线程,并且将共享资源设置为锁定状态。如果被请求的共享资源被占用,那么就需要一套线程阻塞等待以及被唤醒时锁分配的机制,这个机制AQS是用CLH队列锁实现的,即将暂时获取不到锁的线程加入到队列中。
CLH(Craig,Landin,and Hagersten)队列是一个虚拟的双向队列(虚拟的双向队列即不存在队列实例,仅存在结点之间的关联关系)。AQS是将每条请求共享资源的线程封装成一个CLH锁队列的一个结点(Node)来实现锁的分配。
CountDownLatch
用来控制一个或者多个线程等待多个线程。
维护了一个计数器 cnt,每次调用 countDown() 方法会让计数器的值减 1,减到 0 的时候,那些因为调用 await() 方法而在等待的线程就会被唤醒。
public class CountdownLatchExample {
public static void main(String[] args) throws InterruptedException {
final int totalThread = 10;
CountDownLatch countDownLatch = new CountDownLatch(totalThread);
ExecutorService executorService = Executors.newCachedThreadPool();
for (int i = 0; i < totalThread; i++) {
executorService.execute(() -> {
System.out.print("run..");
countDownLatch.countDown();
});
}
countDownLatch.await();
System.out.println("end");
executorService.shutdown();
}
}
结果:run..run..run..run..run..run..run..run..run..run..end
countDownLatch
会await
,只有在其他的10个线程完成之后才会执行。
CountDownLatch 也是基于 AQS(AbstractQueuedSynchronizer) 实现的
1.初始化一个 CountDownLatch 时告诉并发的线程,然后在每个线程处理完毕之后调用 countDown() 方法。 2.该方法会将 AQS 内置的一个 state 状态 -1 。 3.最终在主线程调用 await() 方法,它会阻塞直到 state == 0 的时候返回。
CyclicBarrier
用来控制多个线程互相等待,只有当多个线程都到达时,这些线程才会继续执行。
和 CountdownLatch 相似,都是通过维护计数器来实现的。线程执行 await() 方法之后计数器会减 1,并进行等待,直到计数器为 0,所有调用 await() 方法而在等待的线程才能继续执行。
CyclicBarrier 和 CountdownLatch 的一个区别是,CyclicBarrier 的计数器通过调用 reset() 方法可以循环使用,所以它才叫做循环屏障。
CyclicBarrier 有两个构造函数,其中 parties 指示计数器的初始值,barrierAction 在所有线程都到达屏障的时候会执行一次。
public class CyclicBarrierExample {
public static void main(String[] args) {
final int totalThread = 10;
CyclicBarrier cyclicBarrier = new CyclicBarrier(totalThread);
ExecutorService executorService = Executors.newCachedThreadPool();
for (int i = 0; i < totalThread; i++) {
executorService.execute(() -> {
System.out.print("before..");
try {
cyclicBarrier.await();
} catch (InterruptedException | BrokenBarrierException e) {
e.printStackTrace();
}
System.out.print("after..");
});
}
executorService.shutdown();
}
}
结果:
before..before..before..before..before..before..before..before..before..before..after..after..after..after..after..after..after..after..after..after..
CyclicBarrier 中文名叫做屏障或者是栅栏,也可以用于线程间通信。
它可以等待 N 个线程都达到某个状态后继续运行的效果。
1.首先初始化线程参与者。 2.调用 await() 将会在所有参与者线程都调用之前等待。 3.直到所有参与者都调用了 await() 后,所有线程从 await() 返回继续后续逻辑。
Semaphore
Semaphore 类似于操作系统中的信号量,可以控制对互斥资源的访问线程数。
以下代码模拟了对某个服务的并发请求,每次只能有 3 个客户端同时访问,请求总数为 10。
public class SemaphoreExample {
public static void main(String[] args) {
final int clientCount = 3;
final int totalRequestCount = 10;
Semaphore semaphore = new Semaphore(clientCount);
ExecutorService executorService = Executors.newCachedThreadPool();
for (int i = 0; i < totalRequestCount; i++) {
executorService.execute(()->{
try {
semaphore.acquire();
System.out.print(semaphore.availablePermits() + " ");
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
semaphore.release();
}
});
}
executorService.shutdown();
}
}
结果:
2 1 2 2 2 2 2 1 2 2
Atomic原子类
Atomic就是原子性的意思,体现在一个操作是不可中断的。即使是在多个线程一起执行的时候,一个操作一旦开始,就不会被其他线程干扰。
java.util.concurrent.atomic 包含了java并发使用的所有原子类。包括四类:
-
基本类型,使用原子的方式更新基本类型
- AtomicInteger:整形原子类
- AtomicLong:长整型原子类
- AtomicBoolean :布尔型原子类
-
数组类型,使用原子的方式更新数组里的某个元素
- AtomicIntegerArray:整形数组原子类
- AtomicLongArray:长整形数组原子类
- AtomicReferenceArray :引用类型数组原子类
-
引用类型
- AtomicReference:引用类型原子类
- AtomicStampedRerence:原子更新引用类型里的字段原子类
- AtomicMarkableReference :原子更新带有标记位的引用类型
-
对象的属性修改类型
- AtomicIntegerFieldUpdater:原子更新整形字段的更新器
- AtomicLongFieldUpdater:原子更新长整形字段的更新器
- AtomicStampedReference :原子更新带有版本号的引用类型。该类将整数值与引用关联起来,可用于解决原子的更新数据和数据的版本号,可以解决使用 CAS 进行原子更新时可能出现的 ABA 问题。
原子类的优势
①多线程环境不使用原子类保证线程安全
class NoAtomic {
//使用volatile是内存可见
private volatile int count = 0;
//若要线程安全执行执行count++,需要加锁
public synchronized void increment() {
count++;
}
public int getCount() {
return count;
}
}
②多线程环境使用原子类保证线程安全
class Atomic {
private AtomicInteger count = new AtomicInteger();
//使用AtomicInteger之后,不需要加锁,也可以实现线程安全。
public void increment() {
count.incrementAndGet();
}
public int getCount() {
return count.get();
}
}
原子类,主要利用了CAS (compare and swap) + volatile 和 native 方法来保证原子操作,从而避免 synchronized 的高开销,执行效率大为提升。
FutureTask
在介绍 Callable 时我们知道它可以有返回值,返回值通过 Future 进行封装。FutureTask 实现了 RunnableFuture 接口,该接口继承自 Runnable 和 Future 接口,这使得 FutureTask 既可以当做一个任务执行,也可以有返回值。
FutureTask 可用于异步获取执行结果或取消执行任务的场景。当一个计算任务需要执行很长时间,那么就可以用 FutureTask 来封装这个任务,主线程在完成自己的任务之后再去获取结果。
BlockingQueue
java.util.concurrent.BlockingQueue 接口有以下阻塞队列的实现:
- FIFO 队列 :LinkedBlockingQueue、ArrayBlockingQueue(固定长度)
- 优先级队列 :PriorityBlockingQueue
提供了阻塞的 take() 和 put() 方法:如果队列为空 take() 将阻塞,直到队列中有内容;如果队列为满 put() 将阻塞,直到队列有空闲位置。
利用阻塞队列可以实现生产者消费者
问题
public class ProducerConsumer {
private static BlockingQueue<String> queue = new ArrayBlockingQueue<>(5);
private static class Producer extends Thread {
@Override
public void run() {
try {
queue.put("product");
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.print("produce..");
}
}
private static class Consumer extends Thread {
@Override
public void run() {
try {
String product = queue.take();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.print("consume..");
}
}
}
public static void main(String[] args) {
for (int i = 0; i < 2; i++) {
Producer producer = new Producer();
producer.start();
}
for (int i = 0; i < 5; i++) {
Consumer consumer = new Consumer();
consumer.start();
}
for (int i = 0; i < 3; i++) {
Producer producer = new Producer();
producer.start();
}
}
结果:
produce..produce..consume..consume..produce..consume..produce..consume..produce..consume..
ArrayBlockingQueue和LinkedBlockingQueue的区别?
- ArrayBlockingQueue基于数组,是有界的阻塞队列,初始化时需要指定容量且不可扩容;LinkedBlockingQueue基于链表,是无界的阻塞队列,容量无限制。
- ArrayBlockingQueue读写共用一把锁,因此put和take是互相阻塞的;而LinkedBlockingQueue使用了两把锁,一把putLock和一把takeLock,实现了锁分离,使得put和take写数据和读数据可以并发的进行。
ForkJoin
主要用于并行计算中,和 MapReduce 原理类似,都是把大的计算任务拆分成多个小任务并行计算。
public class ForkJoinExample extends RecursiveTask<Integer> {
private final int threshold = 5;
private int first;
private int last;
public ForkJoinExample(int first, int last) {
this.first = first;
this.last = last;
}
@Override
protected Integer compute() {
int result = 0;
if (last - first <= threshold) {
// 任务足够小则直接计算
for (int i = first; i <= last; i++) {
result += i;
}
} else {
// 拆分成小任务
int middle = first + (last - first) / 2;
ForkJoinExample leftTask = new ForkJoinExample(first, middle);
ForkJoinExample rightTask = new ForkJoinExample(middle + 1, last);
leftTask.fork();
rightTask.fork();
result = leftTask.join() + rightTask.join();
}
return result;
}
}
public static void main(String[] args) throws ExecutionException, InterruptedException {
ForkJoinExample example = new ForkJoinExample(1, 10000);
ForkJoinPool forkJoinPool = new ForkJoinPool();
Future result = forkJoinPool.submit(example);
System.out.println(result.get());
}
Java内存模型
Java内存模型(Java Memory Model ,JMM)就是一种符合内存模型规范的,屏蔽了各种硬件和操作系统的访问差异的,保证了Java程序在各种平台下对内存的访问都能得到一致效果的机制及规范。
Java内存模型很容易会和Java内存结构混淆,但是他们是两个东西。
主内存和工作内存
CPU处理器上的寄存器的读写的速度比内存快几个数量级,为了解决这种速度矛盾,在它们之间加入了高速缓存。
加入高速缓存带来了一个新的问题:缓存一致性。如果多个缓存共享同一块主内存区域,那么多个缓存的数据可能会不一致,需要一些协议来解决这个问题。
那么我们定义这样一个协议,所有的变量都存储在主内存中,每个线程还有自己的工作内存,工作内存存储在高速缓存或者寄存器中,保存了该线程使用的变量的主内存副本拷贝。 线程只能直接操作工作内存中的变量,不同线程之间的变量值传递需要通过主内存和工作内存之间的数据同步来进行。
由此Java内存模型就出现了,Java内存模型是根据英文Java Memory Model(JMM)翻译过来的。其实JMM并不像JVM内存结构一样是真实存在的。他只是一个抽象的概念。JSR-133: Java Memory Model and Thread Specification中描述了,JMM是和多线程相关的,他描述了一组规则或规范,这个规范定义了一个线程对共享变量的写入时对另一个线程是可见的。
Java的线程之间是通过共享内存进行通信的。在利用共享内存通信的过程中,一定是存在可见性、原子性、顺序性
的问题,而JMM就是围绕着多线程通信以及与其相关的一系列特性而建立的模型。
Java内存模型就定义了一些语法集,用于规定了共享内存系统中多线程程序读写操作规范,,这些语法集映射到java语言中,就是我们说的volatile,synchronized
等关键字。
内存间交互操作
Java 内存模型定义了 8 个操作来完成主内存和工作内存的交互操作。
- read:把一个变量的值从主内存传输到工作内存中
- load:在 read 之后执行,把 read 得到的值放入工作内存的变量副本中
- use:把工作内存中一个变量的值传递给执行引擎
- assign:把一个从执行引擎接收到的值赋给工作内存的变量
- store:把工作内存的一个变量的值传送到主内存中
- write:在 store 之后执行,把 store 得到的值放入主内存的变量中
- lock:作用于主内存的变量
- unlock:作用于主内存的变量
Java 内存模型保证了 read、load、use、assign、store、write、lock 和 unlock 操作具有原子性。
内存模型的三大特性
原子性
线程是CPU调度的基本单位。CPU有时间片的概念,会根据不同的调度算法进行线程调度。所以在多线程场景下,就会发生原子性问题。因为线程在执行一个读改写操作时,在执行完读改之后,时间片耗完,就会被要求放弃CPU,并等待重新调度。
保证原子性就是保证,一个操作或者多个操作 要么全部执行并且执行的过程不会被任何因素打断,要么就都不执行。
在java中,对基本数据类型的变量的读取和赋值操作都是原子性操作,即这些操作是不可中断的。
Java内存模型只保证了基本读取和赋值
是原子性操作,如果要实现更大范围操作的原子性,可以通过synchronized和Lock来实现。
由于synchronized和Lock能够保证任一时刻只有一个线程执行该代码块,那么自然就不存在原子性问题了,从而保证了原子性。
当然我们可以使用JUC提供的原子类来保证 原子性。
可见性
现代计算机中,由于 CPU 直接从主内存中读取数据的效率不高,所以都会对应的 CPU 高速缓存,先将主内存中的数据读取到缓存中,线程修改数据之后首先更新到缓存,之后才会更新到主内存。如果此时还没有将数据更新到主内存其他的线程此时来读取就是修改之前的数据。
在CPU和主存之间增加缓存,在多线程场景下就可能存在缓存一致性问题,也就是说,在多核CPU中,每个核的自己的缓存中,关于同一个数据的缓存内容可能不一致。
可见性是指当多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即看得到修改的值。
Java提供了volatile关键字来保证可见性。当一个共享变量被volatile修饰时,它会保证修改的值会立即被更新到主存,当有其他线程需要读取时,它会去内存中读取新值。而普通的共享变量不能保证可见性,因为普通共享变量被修改之后,什么时候被写入主存是不确定的,当其他线程去读取时,此时内存中可能还是原来的旧值,因此无法保证可见性。
另外,通过synchronized和Lock也能够保证可见性,synchronized和Lock能保证同一时刻只有一个线程获取锁然后执行同步代码,并且在释放锁之前会将对变量的修改刷新到主存当中。因此可以保证可见性。
final,被 final 关键字修饰的字段在构造器中一旦初始化完成,并且没有发生 this 逃逸(其它线程通过 this 引用访问到初始化了一半的对象),那么其它线程就能看见 final 字段的值。
有序性
有序性:即程序执行的顺序按照代码的先后顺序执行。在本线程内观察,所有操作都是有序的。在一个线程观察另一个线程,所有操作都是无序的,无序是因为发生了指令重排序。
为了CPU最大限度的使用,进行处理器优化。
Java虚拟机的即时编译器(JIT),会进行指令重排。
在JDK中,JAVA语言为了维持顺序内部的顺序化语义,也就是为了保证程序的最终运行结果需要和在单线程严格意义的顺序化环境下执行的结果一致,程序指令的执行顺序有可能和代码的顺序不一致,这个过程就称之为指令的重排序。指令重排序的意义在于:JVM能根据处理器的特性,充分利用多级缓存,多核等进行适当的指令重排序,使程序在保证业务运行的同时,充分利用CPU的执行特点,最大的发挥机器的性能!
从java源代码到最终执行的指令序列,会经过一下的三种重排序:
源代码->编译器优化重排序->指令级并行重排序->内存系统重排序->最终执行的指令序列
int a=100;//step1
int b=200;//step2
int c=a+b;//step3
对于以上的代码,正常的执行顺序是1>>2>>3
。但是JVM有时为了提高整体效率,可能会对指令进行重排序导致最终顺序为2>>1>>3
。当然JVM不会随意重排,重排的前提是最终结果和代码执行顺序结果应该一致。
重排在单线程中不会存在问题,在多线程会出现数据不一致的问题。
在Java里面,可以通过volatile关键字来保证一定的“有序性”。
另外可以通过synchronized和Lock来保证有序性,很显然,synchronized和Lock保证每个时刻是有一个线程执行同步代码,相当于是让线程顺序执行同步代码,自然就保证了有序性。
先行发生原则
Java内存模型具备一些先天的“有序性”,即不需要通过任何手段就能够得到保证的有序性,这个通常也称为 happens-before 原则,以确保并发情况下的数据正确性。如果两个操作的执行次序无法从happens-before原则推导出来,那么它们就不能保证它们的有序性,虚拟机可以随意地对它们进行重排序。
单一线程原则
在一个线程内,在程序前面的操作先行发生于后面的操作。
管程锁定规则
一个 unlock 操作先行发生于后面对同一个锁的 lock 操作。
volatile 变量规则
对一个 volatile 变量的写操作先行发生于后面对这个变量的读操作
线程启动规则
Thread 对象的 start() 方法调用先行发生于此线程的每一个动作。
线程加入规则
Thread 对象的结束先行发生于 join() 方法返回。
对象终结规则
一个对象的初始化完成(构造函数执行结束)先行发生于它的 finalize() 方法的开始。
传递性
如果操作 A 先行发生于操作 B,操作 B 先行发生于操作 C,那么操作 A 先行发生于操作 C。
线程安全
什么是线程安全?多个线程不管以何种方式访问某个类,并且在主调代码中不需要进行同步,都能表现正确的行为。
那么如何实现线程安全呢?
不可变
不可变(Immutable)的对象一定是线程安全的,不需要再采取任何的线程安全保障措施。只要一个不可变的对象被正确地构建出来,永远也不会看到它在多个线程之中处于不一致的状态。多线程环境下,应当尽量使对象成为不可变,来满足线程安全。
常见的不可变类型:
- final 关键字修饰的基本数据类型
- String
- Enum
- Number 部分子类,如 Long 和 Double 等数值包装类型,BigInteger 和 BigDecimal 等大数据类型。但同为 Number 的原子类 AtomicInteger 和 AtomicLong 则是可变的。
对于集合类型,可以使用 Collections.unmodifiableXXX()
方法来获取一个不可变的集合。
互斥和同步(阻塞同步)
利用synchronized 和 ReentrantLock。
非阻塞同步
互斥同步最主要的问题就是线程阻塞和唤醒所带来的性能问题,因此这种同步也称为阻塞同步。
互斥同步属于一种悲观的并发策略,总是认为只要不去做正确的同步措施,那就肯定会出现问题。无论共享数据是否真的会出现竞争,它都要进行加锁(这里讨论的是概念模型,实际上虚拟机会优化掉很大一部分不必要的加锁)、用户态核心态转换、维护锁计数器和检查是否有被阻塞的线程需要唤醒等操作。
CAS
随着硬件指令集的发展,我们可以使用基于冲突检测的乐观并发策略:先进行操作,如果没有其它线程争用共享数据,那操作就成功了,否则采取补偿措施(不断地重试,直到成功为止)。这种乐观的并发策略的许多实现都不需要将线程阻塞,因此这种同步操作称为非阻塞同步。
乐观锁需要操作和冲突检测这两个步骤具备原子性,这里就不能再使用互斥同步来保证了,只能靠硬件来完成。
硬件支持的原子性操作最典型的是:比较并交换(Compare-and-Swap,CAS)。CAS 指令需要有 3 个操作数,分别是内存地址 V、旧的预期值 A 和新值 B。当执行操作时,只有当 V 的值等于 A,才将 V 的值更新为 B。
AtomicInteger
J.U.C 包里面的整数原子类 AtomicInteger 的方法调用了 Unsafe 类的 CAS 操作。
public final int getAndAddInt(Object var1, long var2, int var4) {
int var5;
do {
var5 = this.getIntVolatile(var1, var2);
} while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));
return var5;
}
var1 是对象的内存地址 var2 指示该字段相对对象内存地址的偏移 var4 指示操作需要加的数值,这里为 1。 var5 是旧的预期值 getIntVolatile(var1, var2) 得到旧的预期值 compareAndSwapInt(var1, var2, var5, var5 + var4) 进行比较, 如果该字段内存地址中的值等于 var5,那么就更新内存地址为 var1+var2 的变量为 var5+var4。
ABA
CAS存在一个问题:A和V一致,就能说明A的值没有被修改过吗?是可能存在A被修改成了B,再从B变回A的情况; 这就是ABA的问题,针对这种情况,需要对其添加一个标记,通过版本来识别CAS的正确性。
J.U.C 包提供了一个带有标记的原子引用类 AtomicStampedReference 来解决这个问题,它可以通过控制变量值的版本来保证 CAS 的正确性。大部分情况下 ABA 问题不会影响程序并发的正确性,如果需要解决 ABA 问题,改用传统的互斥同步可能会比原子类更高效。
无同步方案
要保证线程安全,并不是一定就要进行同步。如果一个方法本来就不涉及共享数据,那它自然就无须任何同步措施去保证正确性。
栈封闭
多个线程访问同一个方法的局部变量时,不会出现线程安全问题,因为局部变量存储在虚拟机栈中,属于线程私有的。
线程本地存储(ThreadLocal Storage)
如果一段代码中所需要的数据必须与其他代码共享,那就看看这些共享数据的代码是否能保证在同一个线程
中执行。如果能保证,我们就可以把共享数据的可见范围限制在同一个线程之内,这样,无须同步也能保证线程之间不出现数据争用的问题。
符合这种应用的典型案列 就是web交互中的 一个请求对应一个线程
,这种处理方式的广泛应用使得很多 Web 服务端应用都可以使用线程本地存储来解决线程安全问题。
可以使用 java.lang.ThreadLocal 类来实现线程本地存储功能。
对于共享变量,一般采取同步的方式保证线程安全。而ThreadLocal是为每一个线程都提供了一个线程内的局部变量,每个线程只能访问到属于它的副本。
可重入代码(Reentrant Code)
这种代码也叫做纯代码(Pure Code),可以在代码执行的任何时刻中断它,转而去执行另外一段代码(包括递归调用它本身),而在控制权返回后,原来的程序不会出现任何错误。
可重入代码有一些共同的特征,例如不依赖存储在堆上的数据和公用的系统资源、用到的状态量都由参数中传入、不调用非可重入的方法等
多线程常见问题
上下文切换
多线程并不一定是要在多核处理器才支持的,就算是单核也是可以支持多线程的。 CPU 通过给每个线程分配一定的时间片,由于时间非常短通常是几十毫秒,所以 CPU 可以不停的切换线程执行任务从而达到了多线程的效果。
但是由于在线程切换的时候需要保存本次执行的信息(在JVM的程序计数器中记录状态,位置),在该线程被 CPU 剥夺时间片后又再次运行恢复上次所保存的信息的过程就称为上下文切换。
上下文切换是非常耗效率的。
通常为了避免上下文切换,我们采取了这些方案:
- 采用无锁编程
- 采用CAS算法
- 合理的创建线程,避免创建了一些线程但其中大部分都是处于 waiting 状态,因为每当从 waiting 状态切换到 running 状态都是一次上下文切换。
死锁
死锁的场景一般是:线程 A 和线程 B 都在互相等待对方释放锁,或者是其中某个线程在释放锁的时候出现异常如死循环之类的。这时就会导致系统不可用。 常用的解决方案如下:
- 尽量一个线程只获取一个锁。
- 一个线程只占用一个资源。
- 尝试使用定时锁,至少能保证锁最终会被释放。
分布式、高并发、多线程的区别
分布式=高并发=多线程?然而并不是,他们三者是相伴而生,但是侧重点又有不同
什么是分布式
分布式,为了解决单个物理服务器容量和性能瓶颈问题而采用的优化手段。这种手段衍生出了各种系统,包括:分布式文件系统、分布式缓存、分布式数据库以及分布式计算。 特别是经常遇到的那些名词:Hadoop、zookeeper、MQ等都和分布式有关。 从理念上讲,分布式的实现有两种方式:
- 水平扩展,当单个服务器扛不住访问时,可以通过添加机器的方式,将流量分解到所有的服务器上,所有的服务器提����的是一样的功能。
- 垂直拆分,前端有不同的访问请求,将不同的请求分发到不同的机器上,所有的服务器提供各自服务器承载的功能。
什么是高并发
高并发,反映了同时访问的数量。高并发可以通过分布式的技术取解决,将并发流量分发到不同的服务器进行处理。除此之外我们也可以采用另外的优化手段:比如使用缓存系统,使用CDN;也可以使用多线程技术使服务器的性能最大化。
什么是多线程
多线程,是指从单个计算机来说,实现多个线程并发执行的技术。多线程最需要关心的问题就是线程安全
的问题。
总结
- 分布式是从物理资源的角度去将不同的机器组成一个整体对外服务,技术范围非常管且难度非常大,有了这个基础,高并发、高吞吐等系统很容易构建;
- 高并发是从业务角度去描述系统的能力,实现高并发的手段可以采用分布式,也可以采用诸如缓存、CDN等,当然也包括多线程;
- 多线程则聚焦于如何使用编程语言将CPU调度能力最大化。