初阶并发编辑之——共享受限资源

不正确的访问资源
  我们先看一个例子:

class addOne implements Runnable{
    private static int value=0;
    @Override
    public void run() {
        //System.out.println(Thread.currentThread());
        try {
        Thread.sleep(150);
        value+=1;
        System.out.println(value);
    } catch (InterruptedException e) {
        // TODO Auto-generated catch block
        e.printStackTrace();
    }
      }
    }
public class ConcurrentTest4 {
   public static void main(String args[]){
       ExecutorService exec = Executors.newCachedThreadPool();
       AddOne a = new AddOne();
       for(int i=0;i<10;i++){
           exec.execute(a);
       }
       exec.shutdown();
   }
}

输出结果:

3
3
3
3
4
5
6
3
3

  我们可以看出,静态常量Value本来应该是每执行一次任务加1才对,输出结果即使不是递增那也应该没有相同的数才对,然而在多线程中共享资源Value时操作Value+=1不是原子操作,在编译器中他不是不可拆解的,他可以分为加1和赋值2个操作。当第一个线程进入运行到加1时,有可能第二个线程进入捕获Value值的时候第一个线程还没有进入赋值操作,所以他捕获到的仍是0,然后加一输出1,而第一个线程同样赋值输出1。当任务不能保证原子性时,共享资源就会同时被多个线程操作,就会混乱。比如你本来银行卡有1000,现在取走100元但还余额还未减去100的时候有人同时存了100,并且余额变为1100,那取钱的操作接着下去就会用1100去减100,变成1000,而不是900。所以多线程共享资源时一定要慎重,尤其时项目中那些任务耗时较长时就一定会出错。

解决共享资源竞争
  1)同步锁synchronized

  可以用sysnchonized修饰一个代码块,代码块中被封装成一个原子,一个线程执行完代码块中的代码才能有可能让别的线程进入代码块。代码块就像一个厕所,排在前面的人抢先进入后反锁住门,等他上完厕所打开门才会让第二个人上。将上面的run()方法改为下列:

public  void run() {
        synchronized(this){
            value+=1;
            System.out.println(Thread.currentThread()+" "+ value );
        }
      }

输出结果:

Thread[pool-1-thread-1,5,main] 1
Thread[pool-1-thread-10,5,main] 2
Thread[pool-1-thread-7,5,main] 3
Thread[pool-1-thread-9,5,main] 4
Thread[pool-1-thread-8,5,main] 5
Thread[pool-1-thread-6,5,main] 6
Thread[pool-1-thread-5,5,main] 7
Thread[pool-1-thread-4,5,main] 8
Thread[pool-1-thread-3,5,main] 9
Thread[pool-1-thread-2,5,main] 10


  sysnchronized代码块中的(this)可以指定对象,给对象加锁(obj)。当没有明确的对象作为锁,只是想让一段代码同步时,可以创建一个特殊的对象来充当锁(lock)。当()中是类的class对象时,表示锁住了整个类,该类所有的对象都是同一把所(ClassName.class)。另外sysnchronized可以修饰方法,使Synchronized作用于整个方法。该方法就可以锁住,只能同时被一个线程访问,并且整个方法具有原子性。

  当sysnchronized修饰静态方法时,所有Rnnable对象都遵从同步机制,比如将实例改为:

class AddOne implements Runnable{
    private static int value = 0;
    @Override
    public  void run() {
        a();
      }
    synchronized static void a(){
        value+=1;
        System.out.println(Thread.currentThread()+" "+ value );
    }
    }
public class ConcurrentTest4 {
   public static void main(String args[]) throws InterruptedException{
       ExecutorService exec = Executors.newCachedThreadPool();
       //AddOne a = new AddOne();
       for(int i=0;i<10;i++){

           exec.execute(new AddOne());
       }
       exec.shutdown();
   }
}

输出结果:

Thread[pool-1-thread-4,5,main] 1
Thread[pool-1-thread-8,5,main] 2
Thread[pool-1-thread-1,5,main] 3
Thread[pool-1-thread-9,5,main] 4
Thread[pool-1-thread-10,5,main] 5
Thread[pool-1-thread-7,5,main] 6
Thread[pool-1-thread-3,5,main] 7
Thread[pool-1-thread-6,5,main] 8
Thread[pool-1-thread-5,5,main] 9
Thread[pool-1-thread-2,5,main] 10

  注意上述有两处改动:第一:run()方法调用synchronzed修饰的静态方法a();第二:exec.execute()中不再是对象a,而是初始化了10个对象。run()中运行的是类AddOne的静态方法a(),每一个对象指向的都是同一个方法。


  2)使用显示的Lock对象

  Lock对象必须被显示的创建,锁定,释放。将上述的AddOne类修改为:

class AddOne implements Runnable{
    private static int value = 0;
    private Lock lock = new ReentrantLock();
    @Override
    public  void run() {
        lock.lock();
        value+=1;
        System.out.println(Thread.currentThread()+" "+ value );
        lock.unlock();
      }
    //synchronized static void a(){}
    }

  同样可以实现类似与synchronized的效果,虽然代码看起来没有使用synchronized来的优雅,但是在某些情况下却更加灵活。当时用Lock时上述的将lock()的调用放在finally自居中带有unlock()的try-finally语句的惯用法会显得非常重要。finally中的放置unlock确保不会过早释放锁,从而将数据暴露给第二个任务。synchronized中当任务失败会抛出一个异常,却没有机会去做任何清理工作,以维护系统的良好状态。当你尝试获取锁且获取锁失败,或者尝试获取锁一段时间,然后放弃他时就要用到Lock。同时Lock可以更加细致的去控制所得获取与释放。

原子性与易变性
  原子性
  原子操作时不能被线程机制中断的操作,一旦操作开始那么他一定可以在可能发生“上下文切换”之前(即线程之间的切换)执行完毕。对于读取和写入出long和double之外的基本类型都是原子操作,比如int i = 0。JVM将long和double变量(64位)的读取与写入当作两个分离的32位操作执行,因此只有用关键字volatile修饰该变量使简单的读取写入为原子操作。
  volatile关键字保证可见性
  当一个任务(比如一个共享的普通变量)被修改时,会暂时的存储在本地处理器的高速缓存中,其他线程的读取或写入就会在这个高速缓存中,当程序运行结束将高速缓存中的数据刷新到主存当中。上当一个共享变量被volatile修饰时,它会保证修改的值会立即被更新到主存,当有其他线程需要读取时,它会去内存中读取新值。而普通的共享变量不能保证可见性,因为普通共享变量被修改之后,什么时候被写入主存是不确定的,当其他线程去读取时,此时内存中可能还是原来的旧值,因此无法保证可见性。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值