CAS详解首页投稿(暂停使用,暂停投稿)开源工具技巧

Java进阶 - 并发探索

2017-01-10  本文已影响143人  Goo_Yao

前言

深入浅出,对Java多线程的探索 - 笔者的一段学习笔记,如果错漏,恳请指教。

前提概念

原子性

可见性

顺序性

并行、并发以及线程安全

java内存模型

保证原子性

x = 10;//1
y = x;//2
x++;//3
x = x + 1;//4

只有语句1是原子性操作 - 语句1直接将10赋值给x(将数值10写入工作内存);语句2包含两个原子性操作(读x值,将x写入内存),合起来就不是原子性操作了(因为可能中间会被打断,造成一个有效,一个无效的情况,语句3,4也是同理);语句3包含3个操作(读x值,加1,写入内存)

总结:只有简单的读取、赋值才是原子操作(必须是将具体数值赋值给某个变量,变量之间的赋值不是原子操作);Java内存模型只保证了基本读取和赋值是原子性操作,如果要实现更大范围的原子性,需要通过synchronized和lock来实现(保证任意时刻只有一个线程执行对应代码块)

保证可见性

volatile关键字可保证可见性:当一个共享变量被volatile修饰时,它会保证修改的值会立即更新到主存,当有其他线程需要读取,它会去内存中读取新值。(当然,synchronized、Lock 也可以保证)

有序性

synchronized、lock通过保证线程同步,自然保证了有序性。另外,通过volatile关键字也可保证一定的“有序性”(具体原理稍候再描述)

注意:Java内存模型中,如果操作遵循“先行发生原理”(happens-before),则不需要通过任何手段就可以保证有序性。

先行发生原则(happens-before):

前四条主要规则解释:

探讨Volatile关键字

文章及书本推荐:

volatile 关键字两层含义

** 1. 保证了不同线程对该变量的可见性**
注意:volatile 变量在各个线程的工作内存中,可以存在不一致的情况,但由于每次使用都要先刷新,执行引擎看不到不一致的情况,因此可认为不存在一致性问题,但java中运算操作并不是原子性操作,导致volatile变量的运算在并发状态下一样是不安全的,下面尝试用代码说明。

public class LearningVolatile {
    public static volatile int race = 0;
    public static void increase() {
        race++;
    }
    private static final int THREADS_COUNT = 20;
    public static void main(String[] args) {
        Thread[] threads = new Thread[THREADS_COUNT];
        for (int i = 0; i < THREADS_COUNT; i++) {
            threads[i] = new Thread(new Runnable() {
                @Override
                public void run() {
                    for (int i = 0; i < 10000; i++) {
                        increase();
                    }
                }
            });
            threads[i].start();
        }
        // 等待所有累加线程结束
        while (Thread.activeCount() > 1) {
            Thread.yield();
        }
        System.out.println(race);
    }
}
//每次运行结果不同,总为一个小于200000的数字
//笔者数次输出结果为:73000+

代码剖析:
如果正确并发,理论结果应为200000,显然,这段程序并没有正确并发,使用javap 命令反编译得到字节码。可以看到,race++ 一行代码包含:取值(getstatic-volatile保证,此时取race值的正确性)、将常量压入栈(iconst_1)、(将两个栈顶int值相加并压入栈顶)iadd,写入(putstatic),就如上面解析的一样,这一系列操作合起来,就不再符合原子性了,当取值之后,其他线程可能已经把race加大了,本线程的race值则变成过期数据,最后写入错误的race值到主内存中。
另:其实采用字节码来分析,也是欠缺严谨的,即使编译出来只有一条字节码,也并不代表该指令就是一个原子操作(因为一条字节码执行时候,解析器还是要运行多行代码才可以实现它的语义,使用 -XX:+PrintAssembly参数输出反汇编来分析会更加严谨),但这里字节码已经能够说明问题,可不必再深入细究。

//反编译字节码(increase方法)
  public static void increase();
    Code:
       0: getstatic     #2                  // Field race:I
       3: iconst_1
       4: iadd
       5: putstatic     #2                  // Field race:I
       8: return

volatile 主要适用场景:

  1. 运算结果不依赖变量当前值,能够确保只有单一线程修改变量的值
  2. 变量不需要与其他的状态变量共同参与不变约束

** 2. 保证了不同线程对该变量的可见性**
关键需要理解:为何指令重排序会干扰程序的并发执行?
例子:初始化完成的标识,如果指令重排,可能会导致 initialized = true提前执行,使B线程运行出现问题

A线程中
volatile boolean initialized = false;
XXX x = new XXX();//模拟初始化
...
initialized = true;//说明初始化完成
B线程中
while(!initialized){
sleep();
}
doSomethingWithAConfig();//接下来,就可以利用A中初始好的配置信息进行操作啦

Java monitor(同步机制)

线程类相关

主要的线程相关类:Thread类、Runnable接口、Callable接口、Future类

Thread

Thread类实现了Runnable接口,常用的Thread类相关方法:

start();//启动线程
yield();//让出CPU,让其他就绪状态(RUNNABLE)的线程运行
sleep();//停滞,使线程进入阻塞状态,但不能改变对象的机锁(仍持有对象锁,其他线程不可访问该对象),注意与wait()方法区分
wait();//等待,释放对象锁(其他线程可访问),因此必须要放到 synchronized 代码块中,否则会抛出“java.lang.IllegalMonitorStateException”异常,使用notify或者noyifyAll方法来唤醒当前等待池中的线程
join();//阻塞当前执行的线程,直到调用该方法的线程执行完毕才释放
interrupte();//检查当前线程是否被打断(返回boolean类型)
interrupted();//将中断状态标识置为true

注意:Thread的异常处理,需要在run方法中,使用try/catch来处理,另外有方法setUncaughtExceptionHandler来处理 uncheck exception

Runnable接口

推荐通过实现Runnable接口,而非继承Thread,从而避免单继承的局限性。

Callable接口、Future

Callable接口与Runnable接口相似,但是Runnable不会返回结果,并且无法抛出返回结果的异常,而Callable功能更强大一些,被线程执行后,可以返回值,这个返回值可以被Future拿到,基本使用方法:

//Future的两个方法
future.isDone() //return  true,false 无阻塞 
future.get() //  return 返回值,阻塞直到该线程运行结束

// 方法1:FutureTask实现了两个接口,Runnable和Future,所以它既可以作为Runnable被线程执行,又可以作为Future得到Callable的返回值
public static void main(String[] args) {
        Callable<Integer> callable = new Callable<Integer>() {
            public Integer call() throws Exception {
                // 返回码
                return new Random().nextInt(100);
            }
        };
        FutureTask<Integer> future = new FutureTask<Integer>(callable);
        new Thread(future).start();
        try {
            Thread.sleep(5000);// 模拟业务逻辑
            System.out.println(future.get());
        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (ExecutionException e) {
            e.printStackTrace();
        }
    }
// 方法2:通过ExecutorService(继承Executor,管理Thread,简化并发编程)的submit方法执行Callable
public static void main(String[] args) {
        ExecutorService threadPool = Executors.newCachedThreadPool();
        Future<Integer> future = threadPool.submit(new Callable<Integer>() {
            public Integer call() throws Exception {
                // 返回码
                return new Random().nextInt(100);
            }
        });
        try {
            Thread.sleep(5000);// 模拟业务逻辑
            System.out.println(future.get());
        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (ExecutionException e) {
            e.printStackTrace();
        }
    }

锁与同步

锁与同步方法是常用于保证Java操作原子性,使用锁,可以保证同一时间只有一个线程拿到锁,因此保证了同一时间只有一个线程能够执行申请锁和释放锁之间的代码。
Java Lock 实现方式:Lock详解 - Raven's Blog

//使用lock来实现synchronized的效果,主要区别:使用synchronized修饰的方法或代码块,在执行完之后会自动释放锁;而Lock则需要手动释放。
public class LockTest {
    public static void main(String[] args) {
        final Outputter1 output = new Outputter1();
        new Thread() {
            public void run() {
                output.output("zhangsan");
            };
        }.start();
        new Thread() {
            public void run() {
                output.output("lisi");
            };
        }.start();
    }
}
class Outputter1 {
    private Lock lock = new ReentrantLock();// 锁对象
    public void output(String name) {
        lock.lock();// 得到锁
        try {
            //互斥区
            for(int i = 0; i < name.length(); i++) {
                System.out.print(name.charAt(i));
            }
        } finally {
            //为了保证能被释放,因此需要放在finall
            lock.unlock();// 释放锁
        }
    }
}

相对synchronized,锁机制更具灵活性,例如,使用读写锁(ReadWriteLock - 读与写互斥、写与写互斥、但读与读不互斥,以此提高性能)

//读写锁
public class ReadWriteLockTest {
    public static void main(String[] args) {
        final Data data = new Data();
        for (int i = 0; i < 3; i++) {
            new Thread(new Runnable() {
                public void run() {
                    for (int j = 0; j < 5; j++) {
                        data.set(new Random().nextInt(30));
                    }
                }
            }).start();
        }        
        for (int i = 0; i < 3; i++) {
            new Thread(new Runnable() {
                public void run() {
                    for (int j = 0; j < 5; j++) {
                        data.get();
                    }
                }
            }).start();
        }
    }
}
class Data {
    private int data;// 共享数据
    private ReadWriteLock rwl = new ReentrantReadWriteLock();    
    public void set(int data) {
        rwl.writeLock().lock();// 取到写锁
        try {
            System.out.println(Thread.currentThread().getName() + "准备写入数据");
            try {
                Thread.sleep(20);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            this.data = data;
            System.out.println(Thread.currentThread().getName() + "写入" + this.data);
        } finally {
            rwl.writeLock().unlock();// 释放写锁
        }
    }    
    public void get() {
        rwl.readLock().lock();// 取到读锁
        try {
            System.out.println(Thread.currentThread().getName() + "准备读取数据");
            try {
                Thread.sleep(20);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName() + "读取" + this.data);
        } finally {
            rwl.readLock().unlock();// 释放读锁
        }
    }
}

CAS(compare and swap)

Java并发包(java.util.concurrent.atomic)下,提供了原子操作类来实现原子性操作方法,从而保证原子性,而其本质是利用了CPU级别的CAS指令。(下面以AtomicInteger为例说明)

//源码中的两个有代表性的方法
//相当于原子性的++i
public final int incrementAndGet() {
    for (;;) {
        int current = get();
        int next = current + 1;
        if (compareAndSet(current, next))
            return next;
    }
}
//相当于原子性的--i
public final int decrementAndGet() {
    for (;;) {
        int current = get();
        int next = current - 1;
        if (compareAndSet(current, next))
            return next;
    }
}
//两种方法都没有使用阻塞式方法来保证原子性,而是通过了CAS指令实现

待扩展的知识点

探究线程知识过程中,发现其与许多其他知识或多或少有着联系,仍需继续努力,嗯,加油!
待扩展:Java类加载机制、JVM内存模型、Java异常分析

上一篇 下一篇

猜你喜欢

热点阅读