多线程(三)

1 任务定时调度

1.1 JDK中的Timer类和TimerTask类

  Timer是jdk中提供的一个定时器工具,使用的时候会在主线程之外起一个单独的线程执行指定的计划任务,可以指定执行一次或者反复执行多次。
  TimerTask是一个实现了Runnable接口的抽象类,代表一个可以被Timer执行的任务

package com.tcxpz.others;
import java.util.Timer;
import java.util.TimerTask;
/**
 * 任务调度: Timer 和TimerTask类
 * @author jgp QQ:1072218420
 *
 */
public class TestTimer {   
    public static void main(String args[]){
        System.out.println("定时开始");
        //创建定时器对象
        final Timer timer = new Timer();
        //创建任务对象
        TimerTask task = new TimerTask() {            
            @Override
            public void run() {
                System.out.println("定时结束");
                timer.cancel();
            }
        };
        //调用定时器timer的schedule(...)按照指定方式方法启动任务
        timer.schedule(task, 1000);
        System.out.println("主线程结束");
    }   
}
/*result:
 * 定时开始
 * 主线程结束
 * 定时结束
 */

   Timer的构造方法:

  

  Timer的成员方法:

  

 schedule与scheduleAtFixedRate:

  这两个方法都是任务调度方法,他们之间区别是,schedule会保证任务的间隔是按照定义的period参数严格执行的,如果某一次调度时间比较长,那么后面的时间会顺延,保证调度间隔都是period,而scheduleAtFixedRate是严格按照调度时间来的,如果某次调度时间太长了,那么会通过缩短间隔的方式保证下一次调度在预定时间执行。举个栗子:你每个3秒调度一次,那么正常就是0,3,6,9s这样的时间,如果第二次调度花了2s的时间,如果是schedule,就会变成0,3+2,8,11这样的时间,保证间隔,而scheduleAtFixedRate就会变成0,3+2,6,9,压缩间隔,保证调度时间

1.2 Quarts

1)为什么要用quartz?

  在大家的工作过程中,多多少少都会需要在指定时刻或间隔多长时间就完成某项工作这种业务功能,在没有成熟的调度框架之前,我们都是使用JDK的Timer和TimerTask类完成,然而Timer是存在一些缺陷的:

  • Timer是基于绝对时间的,而不是相对时间,它对系统时间的改变很敏感;
  • Timer所有任务都是由同一个线程来调度,串行执行过程后续任务可能产生延时;
  • TimerTask任务中出现异常,Timer定时器会自动取消掉,影响其他的任务执行。

  quartz是一个任务调度框架,与Timer功能类似,他可以完成更加复杂的任务调度。quartz除了能解决上述说的问题,quartz还包含了其他的优点:

  • 可以使用更加复杂的任务触发策略;
  • 负载均衡,不同的节点可以执行不同的任务;
  • 容错,一个节点挂了不会影响其他的节点。

2)下载安装

  首先下载jar包,版本以2.2.3示例。官网下载需要借助VPN,这里提供一个百度云链接quarts2.2.3(提取码o5gt) 

  下载完解压后的目录:

  

  下面是lib目录中所有依赖的jar包,使用quarts时需添加到构建路径(还需在src路径下添加log4j.xml文件

   

package com.sxt.others;

import static org.quartz.DateBuilder.evenSecondDateAfterNow;
import static org.quartz.JobBuilder.newJob;
import static org.quartz.TriggerBuilder.newTrigger;
import static org.quartz.SimpleScheduleBuilder.simpleSchedule;
import org.quartz.JobDetail;
import org.quartz.Scheduler;
import org.quartz.SchedulerFactory;
import org.quartz.Trigger;
import org.quartz.impl.StdSchedulerFactory;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.util.Date;
/**
 * * quartz学习入门
 * @author jgp QQ:1072218420
 *
 */
public class QuartzTest {
  public void run() throws Exception {
    // 1、创建 Scheduler的工厂
    SchedulerFactory sf = new StdSchedulerFactory();
    //2、从工厂中获取调度器
    Scheduler sched = sf.getScheduler();  
    // 3、创建JobDetail
    JobDetail job = newJob(HelloJob.class).withIdentity("job1", "group1").build();
    // 时间
    Date runTime = evenSecondDateAfterNow();
    // 4、触发条件
    //Trigger trigger = newTrigger().withIdentity("trigger1", "group1").startAt(runTime).build();
    Trigger trigger  = newTrigger().withIdentity("trigger1", "group1").startAt(runTime)
            .withSchedule(simpleSchedule().withIntervalInSeconds(5).withRepeatCount(3)).build();
    // 5、注册任务和触发条件
    sched.scheduleJob(job, trigger);
    // 6、启动
    sched.start();
    try {
      // 100秒后停止
      Thread.sleep(100L * 1000L);
    } catch (Exception e) {
    }
    sched.shutdown(true);
  }
public static void main(String[] args) throws Exception {
    QuartzTest example = new QuartzTest();
    example.run();
  }
}

结果(每次间隔5秒,重复3次,一共打印了4次) 

2 指令重排

  Java语言中,你写的代码很可能根本没按你期望的顺序执行,因为编译器和CPU会尝试重排指令使得代码更快地运行。

2.1 Happen-Before规则

  它是Java内存模型中定义的两项操作之间的偏序关系,如果操作A先行发生于操作B,其意思就是说,在发生操作B之前,操作A产生的影响都能被操作B观察到,“影响”包括修改了内存中共享变量的值、发送了消息、调用了方法等,它与时间上的先后发生基本没有太大关系。这个原则特别重要,它是判断数据是否存在竞争、线程是否安全的主要依据。  举例来说,假设存在如下三个线程,分别执行对应的操作:  

-----------------------------------------------
  线程A中执行如下操作:i=1
  线程B中执行如下操作:j=i
  线程C中执行如下操作:i=2
-----------------------------------------------

   假设线程A中的操作“i=1”happen—before线程B中的操作“j=i”,那么就可以保证在线程B的操作执行后,变量j的值一定为1,即线程B观察到了线程A中操作“i=1”所产生的影响;现在,我们依然保持线程A和线程B之间的happen—before关系,同时线程C出现在了线程A和线程B的操作之间,但是C与B并没有happen—before关系,那么j的值就不确定了,线程C对变量i的影响可能会被线程B观察到,也可能不会,这时线程B就存在读取到不是最新数据的风险,不具备线程安全性。

  下面是Java内存模型中的八条保证happen—before的规则,它们无需任何同步器协助就已经存在,可以在编码中直接使用。如果两个操作之间的关系不在此列,并且无法从下列规则推导出来的话,它们就没有顺序性保障,虚拟机可以对它们进行随机地重排序。

  • 程序次序规则:在一个单独的线程中,按照程序代码的执行流顺序,(时间上)先执行的操作happen—before(时间上)后执行的操作。
  • 管理锁定规则:一个unlock操作happen—before后面(时间上的先后顺序,下同)对同一个锁的lock操作。
  • volatile变量规则:对一个volatile变量的写操作happen—before后面对该变量的读操作。
  • 线程启动规则:Thread对象的start()方法happen—before此线程的每一个动作。
  • 线程终止规则:线程的所有操作都happen—before对此线程的终止检测,可以通过Thread.join()方法结束、Thread.isAlive()的返回值等手段检测到线程已经终止执行。
  • 线程中断规则:对线程interrupt()方法的调用happen—before发生于被中断线程的代码检测到中断时事件的发生。
  • 对象终结规则:一个对象的初始化完成(构造函数执行结束)happen—before它的finalize()方法的开始。
  • 传递性:如果操作A happen—before操作B,操作B happen—before操作C,那么可以得出A happen—before操作C。

2.2 happen—before原则和时间上先后顺序

   “时间上执行的先后顺序”与“happen—before”之间有何关系呢?没有关系!

1)首先来看操作A在时间上先与操作B发生,是否意味着操作A happen—before操作B?一个常用来分析的例子如下:

private int value = 0;
public int get(){
    return value;
}
public void set(int value){
    this.value = value;
}

  假设存在线程A和线程B,线程A先(时间上的先)调用了setValue(3)操作,然后(时间上的后)线程B调用了同一对象的getValue()方法,那么线程B得到的返回值一定是3吗?

  对照以上八条happen—before规则,发现没有一条规则适合于这里的value变量,从而我们可以判定线程A中的setValue(3)操作与线程B中的getValue()操作不存在happen—before关系。因此,尽管线程A的setValue(3)在操作时间上先于操作B的getvalue(),但无法保证线程B的getValue()操作一定观察到了线程A的setValue(3)操作所产生的结果,也即是getValue()的返回值不一定为3(有可能是之前setValue所设置的值)。这里的操作不是线程安全的。 因此,“一个操作时间上先发生于另一个操作”并不代表“一个操作happen—before另一个操作”

  解决办法:可以将setValue(int)方法和getValue()方法均定义为synchronized方法,也可以把value定义为volatile变量(value的修改并不依赖value的原值,符合volatile的使用场景),分别对应happen—before规则的第2和第3条。注意,只将setValue(int)方法和getvalue()方法中的一个定义为synchronized方法是不行的,必须对同一个变量的所有读写同步,才能保证不读取到陈旧的数据,仅仅同步读或写是不够的 。

2)其次来看,操作A happen—before操作B,是否意味着操作A在时间上先与操作B发生?看有如下代码:

x = 1;
y = 2;

  假设同一个线程执行上面两个操作:操作A:x=1和操作B:y=2。根据happen—before规则的第1条,操作A happen—before 操作B,但是由于编译器的指令重排(Java语言规范规定了JVM线程内部维持顺序化语义,也就是说只要程序的最终结果等同于它在严格的顺序化环境下的结果,那么指令的执行顺序就可能与代码的顺序不一致。这个过程通过叫做指令的重排序。指令重排序存在的意义在于:JVM能够根据处理器的特性(CPU的多级缓存系统、多核处理器等)适当的重新排序机器指令,使机器指令更符合CPU的执行特点,最大限度的发挥机器的性能。在没有同步的情况下,编译器、处理器以及运行时等都可能对操作的执行顺序进行一些意想不到的调整)等原因,操作A在时间上有可能后于操作B被处理器执行,但这并不影响happen—before原则的正确性。因此,“一个操作happen—before另一个操作”并不代表“一个操作时间上先发生于另一个操作”。 

  最后,一个操作和另一个操作必定存在某个顺序,要么一个操作或者是先于或者是后于另一个操作,或者与两个操作同时发生。同时发生是完全可能存在的,特别是在多CPU的情况下。而两个操作之间却可能没有happen-before关系,也就是说有可能发生这样的情况,操作A不happen-before操作B,操作B也不happen-before操作A,用数学上的术语happen-before关系是个偏序关系。两个存在happen-before关系的操作不可能同时发生,一个操作Ahappen-before操作B,它们必定在时间上是完全错开的,这实际上也是同步的语义之一(独占访问)。

2.3 利用happen—before规则分析DCL

   DCL即双重检查加锁,下面是一个典型的在单例模式中使用DCL的例子:

public class LazySingleton {
    private int someField; 
    private static LazySingleton instance;
    private LazySingleton() {
        this.someField = new Random().nextInt(200)+1;         // (1)
    }
    public static LazySingleton getInstance() {
        if (instance == null) {                               // (2)
            synchronized(LazySingleton.class) {               // (3)
                if (instance == null) {                       // (4)
                    instance = new LazySingleton();           // (5)
                }
            }
        }
        return instance;                                      // (6)
    }
    public int getSomeField() {
        return this.someField;                                // (7)
    }
} 

  这里得到单一的instance实例是没有问题的,问题的关键在于尽管得到了Singleton的正确引用,但是却有可能访问到其成员变量的不正确值。具体来说Singleton.getInstance().getSomeField()有可能返回someField的默认值0。如果程序行为正确的话,这应当是不可能发生的事,因为在构造函数里设置的someField的值不可能为0。这是因为语句(1)和语句(7)并不存在happen-before关系。

  对DCL的分析也告诉我们一条经验原则:对引用(包括对象引用和数组引用)的非同步访问,即使得到该引用的最新值,却并不能保证也能得到其成员变量(对数组而言就是每个数组元素)的最新值。

   解决方案:

1)最简单而且安全的解决方法是使用static内部类的思想,它利用的思想是:一个类直到被使用时才被初始化,而类初始化的过程是非并行的,这些都有JLS保证。

public class Singleton {
    private Singleton() {}
    // Lazy initialization holder class idiom for static fields
    private static class InstanceHolder {
        private static final Singleton instance = new Singleton();
    }
    public static Singleton getSingleton() { 
        return InstanceHolder.instance; 
    }
}

2)可以将instance声明为volatile。

private volatile static LazySingleton instance; 

  这样我们便可以得到,线程Ⅰ的语句(5) -> 语线程Ⅱ的句(2),根据单线程规则,线程Ⅰ的语句(1) -> 线程Ⅰ的语句(5)和语线程Ⅱ的句(2) -> 语线程Ⅱ的句(7),再根据传递规则就有线程Ⅰ的语句(1) -> 语线程Ⅱ的句(7),这表示线程Ⅱ能够观察到线程Ⅰ在语句(1)时对someFiled的写入值,程序能够得到正确的行为。

3 ThreadLocal 

3.1 概述

  ThreadLocal是一个线程局部变量,我们都知道全局变量和局部变量的区别,拿Java举例就是定义在类中的是全局的变量,各个方法中都能访问得到,而局部变量定义在方法中,只能在方法内访问。那线程局部变量(ThreadLocal)就是每个线程都会有一个局部变量,独立于变量的初始化副本,而各个副本是通过线程唯一标识相关联的

3.2 用法实例

1)每个线程自身的存储空间、局部区域

 

package com.tcxpz.others;
/**
 * ThreadLocal:为每个线程自身的存储空间、局部区域
 * get/set/initialValue
 * @author jgp QQ:1072218420
 *
 */
public class ThreadLocalTest01 {
    private static ThreadLocal<Integer> threadLocal = new ThreadLocal<Integer>();
    public static void main(String[] args) {    
        //设置值
        threadLocal.set(99);
        //获取值
        System.out.println(Thread.currentThread().getName()+"-->"+threadLocal.get());
        new Thread(new MyRun()).start();
        new Thread(new MyRun()).start();
    }    
    public static  class MyRun implements Runnable{
        public void run() {
            threadLocal.set((int)(Math.random()*99));
            System.out.println(Thread.currentThread().getName()+"-->"+threadLocal.get());        
        }
    }    
}

2)每个线程自身的数据,更改不会影响其他线程

package com.tcxpz.others;
/**
 * ThreadLocal:每个线程自身的数据,更改不会影响其他线程
 * @author 裴新 QQ:3401997271
 *
 */
public class ThreadLocalTest02 {    
    private static ThreadLocal<Integer> threadLocal = new ThreadLocal<Integer>(){
        //threadLocal设置初始值为5
        protected Integer initialValue() {
            return 5;
        };
    };
    public static void main(String[] args) {
        for(int i=0;i<3;i++) {
            new Thread(new MyRun()).start();
        }
    }    
    public static  class MyRun implements Runnable{
        public void run() {
            Integer left =threadLocal.get();
            System.out.println(Thread.currentThread().getName()+"得到了-->"+left);        
            threadLocal.set(left -1);
            System.out.println(Thread.currentThread().getName()+"还剩下-->"+threadLocal.get());    
        }
    }
}

3)谁调用Thread.currentThread()方法,返回的就是谁的线程名,新开的线程的构造函数是由主线程调用的。

package com.tcxpz.others;
/**
 * ThreadLocal:分析上下文 环境  起点
 * 1、构造器: 哪里调用 就属于哪里 找线程体
 * 2、run方法:本线程自身的
 * @author jgp QQ:1072218420
 *
 */
public class ThreadLocalTest03 {    
    private static ThreadLocal<Integer> threadLocal = new ThreadLocal<Integer>();
    public static void main(String[] args) {
        threadLocal.set(5);
        new Thread(new MyRun()).start();
        new Thread(new MyRun()).start();
    }    
    public static  class MyRun implements Runnable{
        public MyRun() {
            System.out.println(Thread.currentThread().getName()+"-->"+threadLocal.get());    
        }
        public void run() {
            threadLocal.set(100);
            System.out.println(Thread.currentThread().getName()+"-->"+threadLocal.get());    
        }
    }    
} 

4)InheritableThreadLocal继承了ThreadLocal,此类扩展了ThreadLocal以提供从父线程到子线程的值的继承:当创建子线程时,子线程接收父线程具有的所有可继承线程局部变量的初始值。 

package com.tcxpz.others;
/**
 * InheritableThreadLocal:继承上下文 环境的数据 ,拷贝一份给子线程
 * @author jgp QQ:1072218420
 *
 */
public class ThreadLocalTest04 {    
    private static ThreadLocal<Integer> threadLocal = new InheritableThreadLocal<Integer>();
    public static void main(String[] args) {
        threadLocal.set(5);
        System.out.println(Thread.currentThread().getName()+"-->"+threadLocal.get());    
        
        //线程由main线程开辟
        new Thread(new Runnable(){
            @Override
            public void run() {
                System.out.println(Thread.currentThread().getName()+"-->"+threadLocal.get());    
                threadLocal.set(10);
                System.out.println(Thread.currentThread().getName()+"-->"+threadLocal.get());    
            }            
        }).start();        
    }        
}

3.3 实际用途

  ThreadLocal最常用的地方就是为每个线程绑定一个数据库连接, HTTP请求,用户身份信息等,这样一个线程的所有调用到的方法都可以非常方便地访问这些资源。

  • 连接数据库的连接管理类,保证每个线程获得独立的连接对象;
  • Hibernate的Session工具类HibernateUtil;
  • 通过不同的线程对象设置Bean属性,保证各个线程Bean对象的独立性。

例:在数据库管理中的连接管理类是下面这样的

public class ConnectionManager {
    private static Connection connect = null;

    public static Connection getConnection() {
        if(connect == null){
            connect = DriverManager.getConnection();
        }
        return connect;
    }
    ...
} 

  在单线程的情况下这样写并没有问题,但如果在多线程情况下回出现线程安全的问题。你可能会说用同步关键字或锁来保障线程安全,这样做当然是可行的,但考虑到性能的问题所以这样子做并是很优雅。 下面是改造后的代码:

public class ConnectionManager {
    private static ThreadLocal<Connection> connThreadLocal = new ThreadLocal<Connection>();

    public static Connection getConnection() {
        if(connThreadLocal.get() != null)
            return connThreadLocal.get();        
        //获取一个连接并设置到当前线程变量中
        Connection conn = getConnection();
        connThreadLocal.set(conn);
        return conn;
    }  
    ...
}

4 可重入锁

  所谓重入锁,指的是以线程为单位,当一个线程获取对象锁之后,这个线程可以再次获取本对象上的锁,而其他的线程是不可以获取该对象上的锁。synchronized和ReentrantLock都是可重入锁。可重入锁的意义在于防止死锁。

  实现原理是通过为每个锁关联一个请求计数器和一个占有它的线程。当计数为0时,认为锁是未被占有的;线程请求一个未被占有的锁时,JVM将记录锁的占有者,并且将请求计数器置为1 。如果同一个线程再次请求这个锁,计数将递增;每次占用线程退出同步块,计数器值将递减。直到计数器为0,锁被释放。 

4.1 synchronized可重入锁

package com.tcxpz.others;
/**
 * 可重入锁: 锁可以延续使用
 * @author jgp QQ:1072218420
 *
 */
public class LockTest01 {
    public void test() {
    //  第一次获得锁
        synchronized(this) {
            while(true) {
                //  第二次获得同样的锁
                synchronized(this) {
                    System.out.println("ReentrantLock!");
                }
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }
    public static void main(String[] args) {
        new LockTest01().test();
    }
}

4.2 自己手动实现一个不可重入锁

package com.tcxpz.others;
/**
 * 不可重入锁: 锁不可以延续使用
 * @author jgp QQ:1072218420
 *
 */
public class LockTest02 {
    Lock lock = new Lock();
    public void a() throws InterruptedException {
        lock.lock();
        doSomething();
        lock.unlock();
    }
    //不可重入
    public void doSomething() throws InterruptedException {
        lock.lock();
        //...................
        lock.unlock();
    }
    public static void main(String[] args) throws InterruptedException {
        LockTest02 test = new LockTest02();
        test.a();
        test.doSomething();
    }
}
// 不可重入锁
class Lock{
    //是否占用
    private boolean isLocked = false;
    //使用锁
    public synchronized void lock() throws InterruptedException {
        while(isLocked) {
            wait();
        }        
        isLocked = true;
    }
    //释放锁
    public synchronized void unlock() {
        isLocked = false;
        notify();        
    }
}
/*
*result:死锁状态
*/

4.3 自己手动实现一个可重入锁

package com.tcxpz.others;
/**
 * 可重入锁: 锁可以延续使用 + 计数器
 * @author jgp QQ:1072218420
 *
 */
public class LockTest03 {
    ReLock lock = new ReLock();
    public void a() throws InterruptedException {
        lock.lock();
        System.out.println("可重入锁计数器:"+lock.getHoldCount());
        doSomething();
        lock.unlock();
        System.out.println("可重入锁计数器:"+lock.getHoldCount());
    }
    //不可重入
    public void doSomething() throws InterruptedException {
        lock.lock();
        System.out.println("可重入锁计数器:"+lock.getHoldCount());
        //...................
        lock.unlock();
        System.out.println("可重入锁计数器:"+lock.getHoldCount());
    }
    public static void main(String[] args) throws InterruptedException {
        LockTest03 test = new LockTest03();
        test.a();            
        Thread.sleep(1000);        
        System.out.println("可重入锁计数器:"+test.lock.getHoldCount());
    }
}
// 可重入锁
class ReLock{
    //是否占用
    private boolean isLocked = false;
    private Thread lockedBy = null; //存储线程
    private int holdCount = 0;
    //使用锁
    public synchronized void lock() throws InterruptedException {
        Thread t = Thread.currentThread();
        while(isLocked && lockedBy != t) {
            wait();
        }        
        isLocked = true;
        lockedBy = t;
        holdCount ++;
    }
    //释放锁
    public synchronized void unlock() {
        if(Thread.currentThread() == lockedBy) {
            holdCount --;
            if(holdCount ==0) {
                isLocked = false;
                notify();
                lockedBy = null;
            }        
        }        
    }
    public int getHoldCount() {
        return holdCount;
    }
}

4.4 ReentrantLock可重入锁

  ReentrantLock可重入锁的大体设计思想就是4.3。

package com.tcxpz.others;

import java.util.concurrent.locks.ReentrantLock;

/**
 * 可重入锁: 锁可以延续使用 + 计数器
 * @author jgp QQ:1072218420
 *
 */
public class LockTest04 {
    ReentrantLock lock = new ReentrantLock();
    public void a() throws InterruptedException {
        lock.lock();
        System.out.println(lock.getHoldCount());
        doSomething();
        lock.unlock();
        System.out.println(lock.getHoldCount());
    }
    //不可重入
    public void doSomething() throws InterruptedException {
        lock.lock();
        System.out.println(lock.getHoldCount());
        //...................
        lock.unlock();
        System.out.println(lock.getHoldCount());
    }
    public static void main(String[] args) throws InterruptedException {
        LockTest04 test = new LockTest04();
        test.a();            
        Thread.sleep(1000);        
        System.out.println(test.lock.getHoldCount());
    }
}

ReentrantLock与synchronized比较:

  • 前者使用灵活,但是必须手动开启和释放锁;
  • 前者扩展性好,有时间锁等候(tryLock( )),可中断锁等候(lockInterruptibly( )),锁投票等,适合用于高度竞争锁和多个条件变量的地方;
  • 前者提供了可轮询的锁请求,可以尝试去获取锁(tryLock( )),如果失败,则会释放已经获得的锁。有完善的错误恢复机制,可以避免死锁的发生。

5 CAS(Compare and Swap)比较并交换

  在介绍CAS之前我们先简单介绍一下悲观锁和乐观锁。

  悲观锁:总是假设最坏的情况,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会阻塞直到它拿到锁。传统的关系型数据库里边就用到了很多这种锁机制,比如行锁,表锁等,读锁,写锁等,都是在做操作之前先上锁。再比如Java里面的同步原语synchronized关键字的实现也是悲观锁。

   乐观锁:顾名思义,就是很乐观,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,可以使用版本号等机制。乐观锁适用于多读的应用类型,这样可以提高吞吐量,像数据库提供的类似于write_condition机制,其实都是提供的乐观锁。在Java中java.util.concurrent.atomic包下面的原子变量类就是使用了乐观锁的一种实现方式CAS实现的。

  相对悲观锁而言,乐观锁假设认为数据一般情况下不会产生并发冲突,所以在数据进行提交更新的时候,才会正式对数据是否产生并发冲突进行检测,如果发现并发冲突了,则让返回用户错误的信息,让用户决定如何去做。它的具体实现细节主要就是两个步骤:冲突检测和数据更新。

  CAS是乐观锁技术,当多个线程尝试使用CAS同时更新同一个变量时,只有其中一个线程能更新变量的值,而其它线程都失败,失败的线程并不会被挂起,而是被告知这次竞争中失败,并可以再次尝试。

  CAS 操作中包含三个操作数 —— 需要读写的内存位置(V)、进行比较的预期原值(A)和拟写入的新值(B)。如果内存位置V的值与预期原值A相匹配,那么处理器会自动将该位置值更新为新值B。否则处理器不做任何操作。无论哪种情况,它都会在 CAS 指令之前返回该位置的值。(在 CAS 的一些特殊情况下将仅返回 CAS 是否成功,而不提取当前值。)CAS 有效地说明了“ 我认为位置V的值应该值是A;如果是A,则将 B 放到这个位置;否则,不要更改该值,只告诉我这个位置现在的值即可。 ”这其实和乐观锁的冲突检查+数据更新的原理是一样的。

  这里再强调一下:乐观锁是一种思想,CAS是这种思想的一种实现方式。

  在JDK1.5 中新增 java.util.concurrent (J.U.C)就是建立在CAS之上的。相对于synchronized这种阻塞算法,CAS是非阻塞算法的一种常见实现。所以J.U.C在性能上有了很大的提升。以 java.util.concurrent 中的 AtomicInteger为例,看一下在不使用锁的情况下是如何保证线程安全的。主要理解 decrementAndGet 方法,该方法的作用相当于 --i 操作。

package com.tcxpz.others;

import java.util.concurrent.atomic.AtomicInteger;

/**
 *  * CAS:比较并交换
 * @author jgp QQ:1072218420
 *
 */
public class CAS {
    //库存
    private static AtomicInteger stock = new AtomicInteger(5);
    public static void main(String[] args) {
        for(int i=0;i<5;i++) {
            new Thread(new Runnable(){
                @Override
                public void run() {
                    //模拟网络延时
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    Integer left = stock.decrementAndGet();
                    if(left<1) {
                        System.out.println("抢完了...");
                        return ;
                    }
                    System.out.println(Thread.currentThread().getName()+"抢了一件商品,还剩"+left+"件商品。");
                }                
            }) .start();
        }
    }
}

posted @ 2019-03-23 10:34  糖醋小瓶子  阅读(320)  评论(0编辑  收藏  举报