• Java并发编程(一) 基础知识


    1. 原子性

      操作的原子性是即不能被分割的操作,和数据库中事务的原子性概念一致,即要么不执行,要么全部完成。有很多看起来具有原子性操作的实际上并不具有“原子性”,例如:

      “读取-修改-写入”操作:形式上像 i++ 一样简单的语句实际上并不是一个具有“原子性”的操作,它实际上包含三步:

      读取原来的值

      修改值

      写入新的值

     并且这三步之间是可以被分割执行的。

    2. 竞态条件

      在多线程环境下,遇到不恰当的执行时序会出现不正确结果的情况称为竞态条件,竞态条件并不一定会导致错误,还需要某种不恰当的执行时序。常见的会导致竞态条件的语句结构有:

      “读取-修改-写入”结构:即更新变量成为一个新值,并且该新值依赖于旧值。

      “先检查后执行”结构:即执行操作之前要先验证一个条件,例如:if-else语句。

    3.加锁

      对于需要保证原子性的操作,我们可以通过加锁来实现多个线程之间的互斥访问。Java提供了内置锁机制来方便地给某个操作加锁,Java中每个对象都有一个内置的锁,我们可以通过synchronized关键字来使用内置锁机制,通常包括两种形式:synchronized块和synchronized方法。

    //synchronized块
    synchronized(obj)   //使用对象obj的内置锁
    {
      // .......
    }
    
    //synchronized方法
    public  synchronized method() 
    {
      // .......
    }
    //就相当于
    public method()
    { 
      synchronized(this)
      {
        // ....... 
      }
    }

       Java的内置锁是可重入的,即持有锁的单位是线程,而不是调用。因此,对于某个已经持有锁X的线程,再次请求锁X会成功,这种情况常见于递归调用:

    public void method()
    {
       synchronized(obj)
      {
         //.....
         method();
      }
    }

       一种常见的错误是认为只有在对共享变量进行写入时才需要加锁,事实上只要访问共享变量,无论是读操作还是写操作,都需要加锁。

    4. 可见性

      通常情况下,我们需要实现只有一个线程能执行一些互斥的操作,我们还可能需要在执行完这个操作后使其他线程知道发生的改变,这就需要同步机制来保证操作执行之后的内存可见性。

      Java中的内置锁和显示锁都支持同步机制,对于显示锁可以使用常用的notify方法和wait方法等。Java中还可以通过volatile关键字来实现轻量级的同步,通常用来实现变量的内存可见性。

      volatile修饰的变量上的操作不会被重排序,访问volatile变量时不会使用缓存中的值,总是会得到最新写入的值。需要注意的是和加锁不一样的是,volatile关键字只能确保内存可见性,但不能确保原子性。使用volatile变量需要满足以下条件:

      (1)对该变量的访问可以是非互斥的

      (2)更新该变量的值不依赖于旧值(如:volatile不能保证自增操作的原子性),如果依赖于旧值则需要确保只有一个线程更新该变量的值

      volatile变量通常用作状态变量,例如,常用作判断某个循环是否应该退出的状态标记:

    volatile  boolean  running = true;
    
    public void run()
    {
      while(running)
      {
        //.....
      }
    }
    
    public void stop()
    {
       running = false;
    }

    5.发布与逸出

      发布一个对象是指使对象能够在当前作用域之外的代码中使用,例如:将该对象的引用传递给其他方法,或者在public方法中返回对象的引用。这样,这个对象就可以在其他代码中被修改。当某个对象在不该被发布的时候发布了,就称为逸出。

      常见的逸出包括:

     (1)在public方法中返回私有成员(可变)的引用

    class  Test
    {
       private Date  birthday;
     
       public void getBirthday()
      {
         return birthday;
      }
    }

      Date类对象不是不可变对象,因此返回Date类对象的引用后,就可以在外部代码中修改它的值,说明这个类的封装性不够好。

    (2)在对象构造完成之前就传递this引用到其他地方

    class  A
    {
       private int num;   
    
       public A()
       {
            new B(this);                 //发布this引用到B类的对象中
            num++;                       //本意是希望num变成1,结果实际上是2
       }
    
       public void addNum()
      {
         num++;
      }
       
    }
    
    class  B
    {
       public B(A a)
      {
          a.addNum();
      }
    }

      上面的错误比较容易看出来,一种更加隐晦的错误情况是在构造函数中启动一个线程:

    class A
    {
        public A()
       {
           new Task(this).start();
    //..... } }
    class Task implements Runnable { A a; Task(A a) { this.a = a; } public void run() { //操作a } }

      上述情况可能会导致A类对象尚未创建完成时便在另一个线程中被修改。

    6. 线程封闭

      仅在单线程内访问数据,就不需要同步以及互斥,这种技术称为线程封闭。Swing中大量使用了线程封闭技术,在Swing技术中,对界面组件的操作都要在EDT(event dispath thread)线程中执行。如果需要在其他线程中更新Swing界面组件,Swing提供了invokeLater方法和invokeAndWait方法来提交更新Swing界面组件的请求。因此,使用Swing编写的程序的main函数形式应该如下:

    public static void main(String[] args)
    {
        EventQueue.invokeLater(new Runnable(){
            public void run()
            {
                JFrame mainFrame = new JFrame();
                mainFrame.setVisible(true);
            }
        });
    }        

    参考资料 《Java并发编程实战》

  • 相关阅读:
    HDU 2899 Strange fuction
    HDU 2899 Strange fuction
    HDU 2199 Can you solve this equation?
    HDU 2199 Can you solve this equation?
    Java实现 LeetCode 700 二叉搜索树中的搜索(遍历树)
    Java实现 LeetCode 700 二叉搜索树中的搜索(遍历树)
    Java实现 LeetCode 700 二叉搜索树中的搜索(遍历树)
    Java实现 LeetCode 699 掉落的方块(线段树?)
    Java实现 LeetCode 699 掉落的方块(线段树?)
    Java实现 LeetCode 699 掉落的方块(线段树?)
  • 原文地址:https://www.cnblogs.com/jqctop1/p/4771894.html
Copyright © 2020-2023  润新知