073-JAVA线程安全的原子变量方案【非阻塞】

要想并发程序正确地执行,必须要保证原子性、可见性以及有序性。

可见性

可见性是指当多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即看得到修改的值。由于java的内存模型+JIT的原因,会导致线程对变量的不可见性。

主内存,存放共享数据。工作内存,存放线程自有数据。
JIT会将变量run放入本线程缓存中,导致线程t没有及时感知到run的变更

解决可见性问题

  • synchronized规定,线程在加锁时,先清空工作内存 → 在主内存中拷贝最新变量的副本到工作内存 → 执行完代码 → 将更改后的共享变量的值刷新到主内存中 → 释放互斥锁。
  • 给变量增加volatile关键字,这种方式可以保证每次取数直接从主存取。
  • AtomicXXX jdk 提供了很多原子类型,这种类型的基本原理总结起来,volatile + unsafe 的 Compare and Swap,这种 Unsafe 操作并不推荐在自己的代码中使用,因为各 JDK 版本在这里变化较大,有可能升级 JDK 时造成各种问题。而且也要保证自己能够用好。

volatile示例:优化两阶段终止优化前

public class Demo {
    public static void main(String[] args) throws InterruptedException {
      TwoPhsaeTermination  twoPhsaeTermination = new TwoPhsaeTermination();
      twoPhsaeTermination.start();
      Thread.sleep(2000);
      twoPhsaeTermination.stop();
    }
}
class  TwoPhsaeTermination{
    private Thread   monitor;
    public  void start(){
        monitor = new Thread(()->{
            while (true){
                Thread thread = Thread.currentThread();
                if(thread.isInterrupted()){
                    System.out.println("料理后事");
                    break;
                }
                try {
                    Thread.sleep(1000);
                    System.out.println("执行监控");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                    // 如果是在sleep过程中被打断,打断标记会被清除,需要重新进行打断标记。
                  // 这里较繁琐,需要特别处理
                    thread.interrupt();
                }
            }
        });
        monitor.start();
    }
    public  void  stop(){
        monitor.interrupt();
    }
}

两阶段终止,优化后

public class Demo {
    public static void main(String[] args) throws InterruptedException {
      TwoPhsaeTermination  twoPhsaeTermination = new TwoPhsaeTermination();
      twoPhsaeTermination.start();
      Thread.sleep(2000);
      twoPhsaeTermination.stop();
    }
}
class  TwoPhsaeTermination{
    private Thread   monitor;
    private volatile boolean stop = false;
    public  void start(){
        monitor = new Thread(()->{
            while (true){
                Thread thread = Thread.currentThread();
                if(stop){
                    System.out.println("料理后事");
                    break;
                }
                try {
                    Thread.sleep(1000);
                    System.out.println("执行监控");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });
        monitor.start();
    }
    public  void  stop(){
        stop = true;
        monitor.interrupt();
    }
}
volatile保证可见性

原子性

在Java中,对基本数据类型的变量的读取和赋值操作是原子性操作,即这些操作是不可被中断的,要么执行,要么不执行。

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

注意:其实只有语句1是原子性操作,其他三个语句都不是原子性操作。

  • 语句1是直接将数值10赋值给x,也就是说线程执行这个语句的会直接将数值10写入到工作内存中。
  • 语句2实际上包含2个操作,它先要去读取x的值,再将x的值写入工作内存,虽然读取x的值以及将x的值写入工作内存,这2个操作都是原子性操作,但是合起来就不是原子性操作了。
  • 同样的,x++和 x = x+1包括3个操作:读取x的值,进行加1操作,写入新的值也就是说,只有简单的读取、赋值(而且必须是将数字赋值给某个变量,变量之间的相互赋值不是原子操作)才是原子操作。

解决原子性问题

  • synchronizedLock来实现。

有序性

有序性:即程序执行的顺序按照代码的先后顺序执行。

int i = 0;
boolean flag = false;
i = 1; //语句1
flag = true; //语句2

上面代码定义了一个int型变量,定义了一个boolean类型变量,然后分别对两个变量进行赋值操作。从代码顺序上看,语句1是在语句2前面的,那么JVM在真正执行这段代码的时候会保证语句1一定会在语句2前面执行吗?不一定,为什么呢?这里可能会发生指令重排序Instruction Reorder

指令重排序:一般来说,处理器为了提高程序运行效率,可能会对输入代码进行优化,它不保证程序中各个语句的执行先后顺序同代码中的顺序一致,但是它会保证程序最终执行结果和代码顺序执行的结果是一致的。

比如上面的代码中,语句1和语句2谁先执行对最终的程序结果并没有影响,那么就有可能在执行过程中,语句2先执行而语句1后执行。但是要注意,虽然处理器会对指令进行重排序,但是它会保证程序最终结果会和代码顺序执行结果相同,那么它靠什么保证的呢?再看下面一个例子:

int a = 10; //语句1
int r = 2; //语句2
a = a + 3; //语句3
r = a * a; //语句4

这段代码有4个语句,那么可能的一个执行顺序是:那么可不可能是这个执行顺序呢: 语句2 >语句1> 语句4 > 语句3。
不可能,因为处理器在进行重排序时是会考虑指令之间的数据依赖性,如果一个指令Instruction 2必须用到Instruction 1的结果,那么处理器会保证Instruction 1会在Instruction 2之前执行。

虽然重排序不会影响单个线程内程序执行的结果,但是多线程呢?下面看一个例子:

//线程1:
context = loadContext(); //语句1
inited = true; //语句2
//线程2:
while(!inited ){
sleep()
}
doSomethingwithconfig(context);

上面代码中,由于语句1和语句2没有数据依赖性,因此可能会被重排序。假如发生了重排序,在线程1执行过程中先执行语句2,而此是线程2会以为初始化工作已经完成,那么就会跳出while循环,去执行doSomethingwithconfig(context)方法,而此时context并没有被初始化,就会导致程序出错。从上面可以看出,指令重排序不会影响单个线程的执行,但是会影响到线程并发执行的正确性。

解决有序性问题

  • 使用volatile关键字修饰变量就可以禁止重排序。
  • 使用synchronized防止“指令重排”,其本质是让多个线程在调用synchronized修饰的方法时,由并行(并发)变成串行调用,谁获得锁谁执行;非真正的禁止指令重排。由多线程变为单线程。
volatile保证有序性

Happens-Before规则

规定了对共享变量的写操作对其他线程的读操作可见,它是可见性和有序性的一套规则总结。

  • 程序次序规则:一个线程内,按照代码顺序,书写在前面的操作先行发生于书写在后面的操作;
  • 管程锁定规则:一个unLock操作先行发生于后面对同一个锁的lock操作;(此处后面指时间的先后)
  • volatile变量规则:对一个变量的写操作先行发生于后面对这个变量的读操作;(此处后面指时间的先后)
  • 线程启动规则:Thread对象的start()方法先行发生于此线程的每个一个动作;
  • 线程终结规则:线程中所有的操作都先行发生于线程的终止检测,我们可以通过Thread.join()方法结束、Thread.isAlive()的返回值手段检测到线程已经终止执行;
  • 线程中断规则:对线程interrupt()方法的调用先行发生于被中断线程的代码检测到中断事件的发生;
  • 对象终结规则:一个对象的初始化完成先行发生于他的finalize()方法的开始;
  • 传递性:如果操作A先行发生于操作B,而操作B又先行发生于操作C,则可以得出操作A先行发生于操作C;

一、CAS保证线程安全

传统方式:

public class Demo {
    public static void main(String[] args) throws InterruptedException {
        Account account = new AccountSafe(10000);
        Account.test(account);
    }
}
class  AccountSafe implements  Account{
    private  Integer balance;
    public  AccountSafe(Integer balance){
        this.balance = balance;
    }
    @Override
    public Integer getBalance() {
        synchronized (this) {
            return this.balance;
        }
    }
    @Override
    public void withdraw(Integer amount) {
        synchronized (this){
            this.balance -=  amount;
        }
    }
}

interface  Account{
    Integer getBalance();
    void withdraw(Integer amount);
    static  void test(Account account){
        List<Thread>  ts = new ArrayList<>();
        for (int i = 0; i < 1000; i++) {
            ts.add(new Thread(()->{
                account.withdraw(10);
            }));
        }
        long start = System.nanoTime();
        ts.forEach(Thread::start);
        ts.forEach( t ->{
            try {
                t.join();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });
        long end = System.nanoTime();
        System.out.println(account.getBalance()+"cost = " + (end-start)/1000_0000+"ms");
    }
}

CAS方式:

public class Demo {
    public static void main(String[] args) throws InterruptedException {
        Account account = new AccountSafe(10000);
        Account.test(account);
    }
}

class  AccountSafe implements  Account{
    private AtomicInteger balance;
    public  AccountSafe(int account){
        this.balance = new AtomicInteger(account);
    }
    @Override
    public Integer getBalance() {
        return  balance.get();
    }
    @Override
    public void withdraw(Integer amount) {
        while(true){
            int pre = balance.get();
            int next = pre - amount;
            if(balance.compareAndSet(pre,next)){
                break;
            }
        }
    }
}
interface   Account{
    Integer getBalance();
    void withdraw(Integer amount);
    static  void test(Account account){
        List<Thread>  ts = new ArrayList<>();
        for (int i = 0; i < 1000; i++) {
            ts.add(new Thread(()->{
                account.withdraw(10);
            }));
        }
        long start = System.nanoTime();
        ts.forEach(Thread::start);
        ts.forEach( t ->{
            try {
                t.join();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });
        long end = System.nanoTime();
        System.out.println(account.getBalance()+"cost = " + (end-start)/1000_0000+"ms");
    }
}

CAS与volatile关系;以AtomicInteger为例:

  • 数据存在volatile修饰的变量中,能够保证线程的可见和有序性。
  • compareAndSet()能保证线程的原子性。
  • 如果共享变量是引用类型,避免出现ABA问题,使用可以AtomicStampedReferencreAtomicMarkableReferencre类。
    原子操作类

CAS适用场景:多核CPU且线程数不超过CPU核心数。

CAS与synchronized区别:

  • CAS基于乐观锁:不担心别人修改共享变量。无锁、不阻塞。
  • synchronized基于悲观锁:总担心别人修改共享变量。有锁,阻塞。

二、享元模式

除了给共享变量加锁、使用原子性的类保证共享变量的安全外,也可使用不可变类,如JDK自带的 DateTimeFormatterString,其主要思想是保护性拷贝机制,以通过创建新的对象来保证变量的不共享,从而保证变量的安全性。但这样的机制弊端就是重复创建对象。

享元模式就是解决重复创建对象的设计模式,比如经常使用的Long对象的内部类,在初始化时就创建 -128 ~ 127的Long数组;valueOf方法在获取Long时,会从数组中获取现有的数,从而避免重复创建Long对象。

 private static class LongCache {
        private LongCache(){}
        static final Long cache[] = new Long[-(-128) + 127 + 1];

        static {
            for(int i = 0; i < cache.length; I++)
                cache[i] = new Long(i - 128);
        }
    }

   public static Long valueOf(long l) {
        final int offset = 128;
        if (l >= -128 && l <= 127) { // will cache
            return LongCache.cache[(int)l + offset];
        }
        return new Long(l);
    }

享元模式示例:自定义数据库连接池

public class PoolDemo {
    // 1.连接数大小
    private  final int poolsize;
    // 2. 连接对象数组
    private Connection[]  connects;
    // 3.连接状态数组 0 空闲  1 繁忙
    private AtomicIntegerArray state;
    // 4.初始化
    public PoolDemo(int poolsize) {
        this.poolsize = poolsize;
        this.connects = new Connection[poolsize];
        this.state = new AtomicIntegerArray(new int[poolsize]);
        for (int i = 0; i < poolsize ; i++) {
            connects[i] = new MockConnect();
        }
    }
    // 5. 获取连接
    public  Connection borrow(){
     while (true){
         for (int i = 0; i <  poolsize; i++) {
             // 获取空闲连接
              if(state.get(i) == 0){
                 if(state.compareAndSet(i,0,1)){
                     System.out.println(Thread.currentThread().getName()+"创建连接" +connects[i]);
                     return  connects[i];
                 }
              }
         }
         // 无空闲连接,当前线程则等待
         synchronized (this){
             try {
                 System.out.println(Thread.currentThread().getName()+"等待" );
                 this.wait();
             } catch (InterruptedException e) {
                 e.printStackTrace();
             }
         }
     }
    }
    // 6. 归还连接
    public  void  free(Connection conn){
        for (int i = 0; i < poolsize ; i++) {
            if(connects[i] == conn){
                state.set(i,0);
                synchronized (this){
                    System.out.println(Thread.currentThread().getName()+"归还 = " + conn);
                    this.notifyAll();
                }
                break;
            }
        }
    }
}

class MockConnect implements  Connection{...}

比较其他方式的连接池方式:

public class ConnectionUtils {
   //解决并发问题,每个线程只取各自的Connect
    private ThreadLocal <Connection> tl = new ThreadLocal<Connection>();
    private DataSource  dataSource;
    public void setDataSource(DataSource dataSource) {
        this.dataSource = dataSource;
    }
    public  Connection getThreadConnection(){
        try {
        //获取当前线程绑定的局部变量
        Connection con = tl.get();
        if(con == null){
           con = dataSource.getConnection();
        //设置当前线程绑定的局部变量
           tl.set(con);
          }
        return  con;
        }catch (Exception e) {
            throw  new RuntimeException(e);
        }
    }

    public void removeConnection(){
      //移除当前线程绑定的局部变量
        tl.remove();
    }
}
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 219,589评论 6 508
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 93,615评论 3 396
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 165,933评论 0 356
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,976评论 1 295
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,999评论 6 393
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,775评论 1 307
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,474评论 3 420
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,359评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,854评论 1 317
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 38,007评论 3 338
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 40,146评论 1 351
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,826评论 5 346
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,484评论 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 32,029评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,153评论 1 272
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,420评论 3 373
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 45,107评论 2 356

推荐阅读更多精彩内容