Java并发-1
进程
进程是操作系统进行资源资源分配的单位,进程中包含若干线程
线程
线程是CPU进行调度和执行的基本单位
方法区 堆 虚拟机栈 本地方法栈 程序计数器
每个线程拥有自己的栈和PC
多个线程共享方法去和堆
并行和并发
并行
单位时间内,任务同时执行
并发
在一定时间内,任务都执行了
并行是一起执行。并发是宏观上一起执行的,实际上是CPU时间片的轮询
创建线程的方式
继承Thread
继承thread
重写run方法
没有返回值
实现Runnable
重写run方法
没有返回值
相对于继承thread。更新灵活,避免了单继承的局限性
实现Callable
实现callable接口
重写call方法,此方法带有返回值,且有抛出异常
thread构造器无法直接传入,需要通过Runnable的子类,futureTask进行传入
获取数据时,使用futureTask.get()方法获取。此方法是阻塞的
总结
继承thread,每启动一个线程,都是一个新的资源类,不能使用内部非静态变量作为锁。实现Runnable,避免了单继承,多个线程可以共享一个资源类,在使用时,只能使用主线程被final修饰的变量。
Thread线程只能调用一次start()。多次会抛出异常
run()方法的调用是在start0()里面调用的是,是本地native c++方法
线程的生命周期
新建
初始化状态,线程被构建,但是还没有调用start()方法
运行
运行状态,运行状态包含运行态和就绪态。
阻塞
阻塞状态,表示线程阻塞于锁
等待
等待状态,,表示线程进入了等待状态,进入改状态的线程需要等待其他线程通知或中断
超时等待
超时等待,她可以在指定时间后,自行返回
销毁
终止状态,表示当前线程已经执行完毕
线程创建之后处于新建态,调用start()之后处于ready(可运行态),线程获取到了cpu时间片后处于running(运行态)。
调用sleep方法后,线程会进入阻塞态,sleep结束后,恢复到ready,等待cpu时间片的调用
一个java程序启动,至少包含2个线程 ,一个main线程,一个gc线程
守护线程
//设置为守护线程
setDaemon(true);
如果用户线程结束了,守护线程也会结束
join方法
join方法底层是wait()方法,调用join方法后,调用者就会被阻塞。直到线程运行完后,调用者所在的线程才会继续执行
yield方法
使当前线程由运行态--进入到就绪态 (Ready)。交互cpu时间片
并发编程中的三个问题
可见性
可加性:是指一个线程对共享变量进行修改,其他线程可立即得到修改后的值
案例:
假设新开一个线程,线程里面操作资源类的内容变更,主线程采用while循环判断内容是否更改(采用while可以避免虚假唤醒)。
未加volatile关键字修饰变量。则会一直循环,因为其他线程无法感知到内容实际已修改,即未从工作内存同步到主存中。
使用volatile关键字后,如果变量被线程修改,其他线程可感知到,则案例死循环将终止。
volatile可以保证内存可见性,不能保证原子性。可以防止指令重排
原子性
原子性:在一次或多次操作中,要么操作全部成功,要么全部失败。不会存在中间状态。
解决原子性方案:synchronized 、 cas原子类工具、lock锁机制
synchronized 、AtomicInteger、【lock.lock、lock.unlock】
有序性
有序性:指的是程序中代码的执行顺序,Java在编译和运行时会对代码进行优化(指令重排)来加快程序的速度,但是这样会导致程序的执行顺序和我们编写的代码顺序不同。在并发下,就会产生问题。
初始化一个对象
instance=new Demo(); 是被分为三部分
memory=allocate(); 分配内存空间 步骤1
instance(memory) 初始化对象 步骤2
instance=memory; 设置instance指向刚刚分配的内存空间地址。此时instance!=null 步骤3
步骤2和3不存在依赖关系,是否发送重排,单线程下,结果都一样。但是如果步骤3在步骤2前,这个时候instance不为null,但实际初始化工作还没有完成。就会返回一个null的getInstance。这时候数据就出现了问题。
解决方案,加volatile,防止指令重排
CAS
-
CAS:cas是比较并交换compareAndSet,是一条CPU并发原语,功能是判断内存某个位置的值是否为预期值,如果是则更新为新的值,这个过程是原子性的。
-
采用Cas+volatile可以保证内存可见性、防止指令重排、以及原子性。解决了并发问题。
-
cas底层原理:
-
调用Unsafe类中的Cas方法,jvm帮我们是先出cas汇编指令,它是依赖于硬件的功能,通过她实现原子操作,Cas是CPU的一条原子指令。
-
cas的思想就是乐观锁的思想.。java中,采用while循环进行自旋,尝试获取锁或者满足期望值。
CAS三大问题
- 如果cas长时间不成功,会给cpu带来开销,java中,是采用while进行一直循环的
- 只能保证一个共享变量的原子性
- 存在ABA问题
ABA问题
ABA问题指的就是,初始化值为A,如果线程1修改A为B。然后线程2修改A值为C。但是线程1修改成功后,又将B修改为了A。此时,线程2去修改,发现原来的值还是A,她就认为数据没有被修改过,然后就将A修改为了C。但是实际情况,A是被修改过后的,并不是原来默认的A了。
解决方案
增加版本号。通过版本号机制,每次修改,都将版本号+1,修改时,判断版本号是否和自己获取前的一致,一致则修改,否则修改失败。
java juc下的AtomicStampedReference类就是ABA的一种解决方案。
只能保证一个共享变量的原子性
java中采用AtomicReference、AtomicStampedReference 原子引用来保证引用对象之间的原子性
Synchronized优化
synchronized可以同时保证可见性,有序性,原子性
锁消除
锁消除是JIT编译器对synchronized做的优化,在编译的时候,jit通过逃逸分析技术,来分析synchronized锁对象,判断她是不是只可能被一个线程来加锁,没有其他线程来竞争加锁,这个时候编译就不加入monitorenter和monitorexit的指令。仅仅一个线程争用锁的时候,就可以消除这个锁了,提示了代码的执行效率,因为只有一个线程来加锁,就不会涉及到锁的竞争
锁粗化
synchronized(this) { } synchronized(this) { } synchronized(this) { } ---------------------------------------------------- 最终会优化成一个 synchronized(this) { }
锁粗化的意思是,如果JIT编译器发现了代码中连续多次加锁释放锁的代码,会结合成一个锁,这就是锁粗化,避免多次加锁释放锁的开销
偏向锁(我偏心)
monitorenter和monitorexit是要使用CAS操作加锁和释放锁的,开销较大。因此,如果发现大概率只有一个线程会主要竞争一个锁,那么就会给这个锁一个偏好,后面她加锁和释放锁,都基于偏好来执行,不需要通过cas,性能会有提升。但是如果有偏好之外的线程来竞争锁,就会回收之前的偏好,但是其他线程来竞争锁的记录交小。
轻量级锁
如果偏向锁没有成功,说明锁的竞争很激烈。那么这个时候就会尝试采用轻量级锁来加锁,就是将对象头的MarkWord里面有一个轻量级锁的指针,尝试指向持有锁的线程,然后判断一下是不是自己加的锁,如果是自己加的锁,那就指向代码。如果不是自己加的锁,那就是加锁失败,说明以及有其他人加锁了,这个时候就会升级为重量级锁
适应性锁
JIT编译器的优化,如果每个线程持有锁的时间非常短,那么一个线程获取不到锁,就会暂停,发生上下文切换,让其他线程来执行,但其他线程很快就释放了锁,然后唤醒暂停的线程,加入锁的竞争,这样线程会频繁的上下文切换,导致开销过大。针对这种情况,可以采用忙等策略,线程没有获取到锁,就进入while循环不停等待,不会暂停发生上下文切换,等到机会获取到锁继续执行就好了。