首先我先介绍关于对java并发的理解:在保证线程安全的情况下 尽可能的利用多核cpu的优势 缩短程序的运行耗时 提高程序的性能;
基本的方法我就不过多涉及了,下面我就讲解一下我自己对并发中各个难点的认识;
这个是知乎某篇关于并发编程的个人图表总结原文章地址
关于多线程不安全的理解:
1.多线程时,当线程的cpu时间片用完时,线程就中断了,此时cpu会发生线程上下文切换,而在这个过程中,如果刚好被中断的是这俩个线程的中的共享变量的读写操作,那么会发生错误;
2.多线程时,如果同时执行对共享变量的读写操作,那么有可能会发生指令交错,从而导致结果错误;
如何判断线程是否安全?——关于happens-before原理的介绍:
当一个线程的写,对另外一个线程的读可见,这就是线程安全,反之,线程不安全;
就像下面的代码,只要你能保证线程t1对x的写,对线程t2可见,线程就安全;具体实现有好多种(无锁,有锁,volatile等等)
intx;
new Thread(()->{
x= 5;
},"t1").start();
new Thread(()->{
System.out.println(x);
},"t2").start();
关于变量的线程安全理解:
1.成员变量和静态变量的线程安全
(1)如果它们没有被多个线程所共享,那么它们是线程安全的;
(2)如果它们被多个线程所共享:
1.如果只有读操作,那么是线程安全的
2.如果含有读写操作,那么就要考虑线程是否安全了
2.局部变量的线程安全
(1)局部变量是线程安全的;
(2)但局部变量引用的对象则未必线程安全 例如下面代码,如果同时调用了method1,2那么就不能保证局部变量x对i的引用是否是线程安全的了;
int i = 5;
public void method1(){
int x = i;
System.out.println(x);
}
public void method2(){
new Thread(()->{
this.i = 4;
}).start;
}
关于volatile关键字的原理:
这个原理首先由几个问题引出
1.有序性
Object obj = new Object();
这是一个再熟悉不过的java语句了,如果将其翻译成电脑看的懂的字节码之后是这样的
1: new //表示创建对象,将对象引用入栈2: dup //表示复制一份对象引用3: invokespecial //表示利用一个对象引用,调用构造方法4: putstatic //表示利用一个对象引用,赋值给obj
所以这个语句并不是原子性的,有可能当jvm中的JIT(Java即时编译器)对其字节码的指令做出优化(即重排序后)将3,4行指令执行顺序调换之后,会先执行第4行也就是先执行赋值的话,那么得到的obj对象就为null;
volatile Object obj = new Object();
而volatile这个关键字能保证这句话的有序性,在加入后,就能使jvm不再对其字节码进行重排序;
2.可见性
static boolean run = true;
public static void main(String[] args) throws InterruptedException {
Thread t = new Thread(()->{
while(run){ // ....
}
}); t.start();
sleep(1);
run = false; // 线程t不会如预想的停下来 }
这段代码当执行到run = false时,按理说,线程t会停下来,但线程t并不会停下来,这是为什么呢?
分析一下 刚开始t线程将从主内存un (run = true)读到了自己的工作内存中;然后经过一秒的sleep,main线程将run的值改成false,但此时t线程并不会从主存中再次获取run的值,是因为jvm中的jit为了提高效率避免每次读取从主存中读取run的值,而将run的值保存在了自己工作内存的高速缓存中(这样以后每次获取run的值就不用从主存中获取,提高了性能);所以t线程读取到的值仍是true(高速缓存中的值);
加了volatile的变量就能避免这个问题,volatile能使每次读取变量的值,都从主存中获取;
* volatile的底层原理是实现内存屏障
1.对 volatile 变量的写指令后会加入写屏障(1.可见性:写屏障保证在该屏障之前的,对共享变量的改动,都同步到主存当中 2.有序性:写屏障会确保指令重排序时,不会将写屏障之前的代码排在写屏障之后)
2.对 volatile 变量的读指令前会加入读屏障(1.可见性: 读屏障保证在该屏障之后,对共享变量的读取,加载的是主存中新数据 2.有序性:读屏障会确保指令重排序时,不会将读屏障之后的代码排在读屏障之前 )
*关于CAS(CompareAndSet或者ComareAndSwap)的实现原理及介绍:
为什么要实现CAS?以及它的用途?
为了避免多线程修改变量时,其读写操作对线程的不可见,所以在CAS中会直接从主存中得到变量的值,而不是在缓存中;
我们可以利用CAS与volatile关键字来使线程安全;
多线程运行环境下,关于其共享变量的修改是不安全的,例如
class TestUnsafe{
int i;
public TestUnsafe(int i){
this.i = i;
}
public void increase (int i){
this.i += i;
}
}
当多线程调用TestUnsafe类中的increase方法时,线程不安全,变量被多个线程所共用,线程不安全;
因为在变量被多个线程所修改时其字节码指令可能发生交错,导致最终得到结果不正确,这时候我们如果使用AtomicInteger类替换int,用CAS实现increase方法时,如下,代码就会被得线程安全了
class Testsafe{
private AtomicInteger AI = new AtomicInteger();
public TestUnsafe(int i){
this.i = i;
}
public int get(){
return this.AI.get();
}
public void increase (int i){
while(true){
int previous = this.get();//获取当前最新的值
int update = previous + i;//计算更新之后的值
if(AI.compareAndSet(previous,update)){//将更新之后的值与现在的值做比较,如果是则返回真,此循环结束,如果否,则一直循环到真为止;
return true;
}
}
}
}
我们从jdk AtomicInteger上的实现上来看
可以看出AtomicInteger对象中的CAS方法是通过底层Unsafe类来实现的,而Unsafe类不能直接得到,只能通过java反射机制得到;
由此可以设计出自己的AtomicInteger(这里只实现了线程安全的减方法)
public class MyAtomicInteger {
private volatile int value;
private static final long valueOffset;
private static final Unsafe unsafe;
static {
unsafe = UnsafeAccessor.getUnsafe();
try {
valueOffset = unsafe.objectFieldOffset(MyAtomicInteger.class.getDeclaredField("value"));
}catch(NoSuchFieldException e) {
e.printStackTrace();
throw new RuntimeException(e);
}
}
public int getValue() {
return value;
}
public void decrement(int amount) {
while(true) {
int prev = this.value;
int next = prev - amount;
unsafe.compareAndSwapInt(this, valueOffset, prev, next);
}
}
}
关于synchronized的实现原理:
Object对象的结构 :Mark Word就是第一行,其中的数字代表加锁的类型;
其中01代表无锁或偏向锁 00代表轻量级锁 10代表重量级锁
先介绍Monitor对象,其中有三个部分分别是WaitSet EntryList Owner ,当线程拿到这个锁之后,也就是进入Owner,此时其他线程就获得不到了锁,即进入EntryList等待在Owner中的线程释放锁,再与其他线程竞争锁(非公平竞争,其中Thread可以设置执行优先级,每个系统的优先级设置范围不一样),拿到锁的线程可以调用wait方法进入WaitSet休息室进行等待,可以用interrupt,notify,notifyall来打断(唤醒)WaitSet中的线程使其进入EntryList中再次与其他线程竞争锁;
当使用synchronized为对象上锁(重量级)时,对象头的Mark Word 就会指向Monitor(类似c语言的指针),执行其中的代码时就如上面所述;
如果一个对象的多线程加锁时间是错开的(非竞争关系的),这时候JVM会对synchronized的加锁进行优化,就会加上轻量级锁,以提高程序的性能,在JDK6以后的版本,
只有第一次使用 CAS 将线程 ID 设置到对象的 Mark Word 头,之后发现 这个线程 ID 是自己的就表示没有竞争,不用重新 CAS。以后只要不发生竞争,这个对象就归该线程所有
即变成偏向锁,当其他线程对此对象加锁时,偏向锁又会变成轻量级锁;
在尝试加轻量级锁的过程中,如果有另外的线程已经给该对象加了轻量级锁时,那么此时会加锁失败(CAS失败),进行锁膨胀过程,将锁升级成重量级锁;