zoukankan      html  css  js  c++  java
  • 线程安全的理解

    如果你的代码所在的进程中有多个线程在同时运行,而这些线程可能会同时运行这段代码。如果每次运行结果和单线程运行的结果是一样的,而且其他的变量的值也和预期的是一样的,就是线程安全的。
    比如一个 ArrayList 类,在添加一个元素的时候,它可能会有两步来完成:1. 在 Items[Size] 的位置存放此元素;2. 增大 Size 的值。

      在单线程运行的情况下,如果 Size = 0,添加一个元素后,此元素在位置 0,而且 Size=1;

      而如果是在多线程情况下,比如有两个线程,线程 A 先将元素存放在位置 0。但是此时 CPU 调度线程A暂停,线程 B 得到运行的机会。线程B也向此 ArrayList 添加元素,因为此时 Size 仍然等于 0 (注意哦,我们假设的是添加一个元素是要两个步骤哦,而线程A仅仅完成了步骤1),所以线程B也将元素存放在位置0。然后线程A和线程B都继续运行,都增加 Size 的值。

      那好,现在我们来看看 ArrayList 的情况,元素实际上只有一个,存放在位置 0,而 Size 却等于 2。这就是“线程不安全”了。
    如何做到线程安全:
    四种方式   sychronized关键字

       1. sychronized method(){}

       2. sychronized (objectReference) {/*block*/}

       3. static synchronized method(){}

       4. sychronized(classname.class)

        其中1和2是代表锁当前对象,即一个对象就一个锁,3和4代表锁这个类,即这个类的锁。要注意的是sychronized method()不是锁这个函数,而是锁对象,即:如果这个类中有两个方法都是sychronized,那么只要有两个线程共享一个该类的reference,每个调用这两个方法之一,不管是否同一个方法,都会用这个对象锁进行同步。
       注意:long 和double是简单类型中两个特殊的咚咚:java读他们要读两次,所以需要同步。

     java线程安全总结(二)请看http://www.iteye.com/topic/808550

      

       最近想将java基础的一些东西都整理整理,写下来,这是对知识的总结,也是一种乐趣。已经拟好了提纲,大概分为这几个主题: java线程安全,java垃圾收集,java并发包详细介绍,java profile和jvm性能调优慢慢写吧。本人jameswxx原创文章,转载请注明出处,我费了很多心血,多谢了。关于java线程安全,网上有很多资料,我只想从自己的角度总结对这方面的考虑,有时候写东西是很痛苦的,知道一些东西,但想用文字说清楚,却不是那么容易。我认为要认识java线程安全,必须了解两个主要的点:java的内存模型,java的线程同步机制。特别是内存模型,java的线程同步机制很大程度上都是基于内存模型而设定的。后面我还会写java并发包的文章,详细总结如何利用java并发包编写高效安全的多线程并发程序。暂时写得比较仓促,后面会慢慢补充完善。


    浅谈java内存模型
           不同的平台,内存模型是不一样的,但是jvm的内存模型规范是统一的。其实java的多线程并发问题最终都会反映在java的内存模型上,所谓线程安全无非是要控制多个线程对某个资源的有序访问或修改。总结java的内存模型,要解决两个主要的问题:可见性和有序性。我们都知道计算机有高速缓存的存在,处理器并不是每次处理数据都是取内存的。JVM定义了自己的内存模型,屏蔽了底层平台内存管理细节,对于java开发人员,要清楚在jvm内存模型的基础上,如果解决多线程的可见性和有序性。
           那么,何谓可见性? 多个线程之间是不能互相传递数据通信的,它们之间的沟通只能通过共享变量来进行。Java内存模型(JMM)规定了jvm有主内存,主内存是多个线程共享的。当new一个对象的时候,也是被分配在主内存中,每个线程都有自己的工作内存,工作内存存储了主存的某些对象的副本,当然线程的工作内存大小是有限制的。当线程操作某个对象时,执行顺序如下:
     (1) 从主存复制变量到当前工作内存 (read and load)
     (2) 执行代码,改变共享变量值 (use and assign)
     (3) 用工作内存数据刷新主存相关内容 (store and write)

    JVM规范定义了线程对主存的操作指令:read,load,use,assign,store,write。当一个共享变量在多个线程的工作内存中都有副本时,如果一个线程修改了这个共享变量,那么其他线程应该能够看到这个被修改后的值,这就是多线程的可见性问题。
            那么,什么是有序性呢 ?线程在引用变量时不能直接从主内存中引用,如果线程工作内存中没有该变量,则会从主内存中拷贝一个副本到工作内存中,这个过程为read-load,完成后线程会引用该副本。当同一线程再度引用该字段时,有可能重新从主存中获取变量副本(read-load-use),也有可能直接引用原来的副本 (use),也就是说 read,load,use顺序可以由JVM实现系统决定。
            线程不能直接为主存中中字段赋值,它会将值指定给工作内存中的变量副本(assign),完成后这个变量副本会同步到主存储区(store- write),至于何时同步过去,根据JVM实现系统决定.有该字段,则会从主内存中将该字段赋值到工作内存中,这个过程为read-load,完成后线程会引用该变量副本,当同一线程多次重复对字段赋值时,比如:

    Java代码 复制代码 收藏代码
    1. for(int i=0;i<10;i++)   
    2.  a++;  
     for(int i=0;i<10;i++)
      a++;


    线程有可能只对工作内存中的副本进行赋值,只到最后一次赋值后才同步到主存储区,所以assign,store,weite顺序可以由JVM实现系统决定。假设有一个共享变量x,线程a执行x=x+1。从上面的描述中可以知道x=x+1并不是一个原子操作,它的执行过程如下:
    1 从主存中读取变量x副本到工作内存
    2 给x加1
    3 将x加1后的值写回主

    如果另外一个线程b执行x=x-1,执行过程如下:
    1 从主存中读取变量x副本到工作内存
    2 给x减1
    3 将x减1后的值写回主存

    那么显然,最终的x的值是不可靠的。假设x现在为10,线程a加1,线程b减1,从表面上看,似乎最终x还是为10,但是多线程情况下会有这种情况发生:
    1:线程a从主存读取x副本到工作内存,工作内存中x值为10
    2:线程b从主存读取x副本到工作内存,工作内存中x值为10
    3:线程a将工作内存中x加1,工作内存中x值为11
    4:线程a将x提交主存中,主存中x为11
    5:线程b将工作内存中x值减1,工作内存中x值为9
    6:线程b将x提交到中主存中,主存中x为9

    同样,x有可能为11,如果x是一个银行账户,线程a存款,线程b扣款,显然这样是有严重问题的,要解决这个问题,必须保证线程a和线程b是有序执行的,并且每个线程执行的加1或减1是一个原子操作。看看下面代码:

    Java代码 复制代码 收藏代码
    1. public class Account {   
    2.   
    3.     private int balance;   
    4.   
    5.     public Account(int balance) {   
    6.         this.balance = balance;   
    7.     }   
    8.   
    9.     public int getBalance() {   
    10.         return balance;   
    11.     }   
    12.   
    13.     public void add(int num) {   
    14.         balance = balance + num;   
    15.     }   
    16.   
    17.     public void withdraw(int num) {   
    18.         balance = balance - num;   
    19.     }   
    20.   
    21.     public static void main(String[] args) throws InterruptedException {   
    22.         Account account = new Account(1000);   
    23.         Thread a = new Thread(new AddThread(account, 20), "add");   
    24.         Thread b = new Thread(new WithdrawThread(account, 20), "withdraw");   
    25.         a.start();   
    26.         b.start();   
    27.         a.join();   
    28.         b.join();   
    29.         System.out.println(account.getBalance());   
    30.     }   
    31.   
    32.     static class AddThread implements Runnable {   
    33.         Account account;   
    34.         int     amount;   
    35.   
    36.         public AddThread(Account account, int amount) {   
    37.             this.account = account;   
    38.             this.amount = amount;   
    39.         }   
    40.   
    41.         public void run() {   
    42.             for (int i = 0; i < 200000; i++) {   
    43.                 account.add(amount);   
    44.             }   
    45.         }   
    46.     }   
    47.   
    48.     static class WithdrawThread implements Runnable {   
    49.         Account account;   
    50.         int     amount;   
    51.   
    52.         public WithdrawThread(Account account, int amount) {   
    53.             this.account = account;   
    54.             this.amount = amount;   
    55.         }   
    56.   
    57.         public void run() {   
    58.             for (int i = 0; i < 100000; i++) {   
    59.                 account.withdraw(amount);   
    60.             }   
    61.         }   
    62.     }   
    63. }  
    public class Account {
    
        private int balance;
    
        public Account(int balance) {
            this.balance = balance;
        }
    
        public int getBalance() {
            return balance;
        }
    
        public void add(int num) {
            balance = balance + num;
        }
    
        public void withdraw(int num) {
            balance = balance - num;
        }
    
        public static void main(String[] args) throws InterruptedException {
            Account account = new Account(1000);
            Thread a = new Thread(new AddThread(account, 20), "add");
            Thread b = new Thread(new WithdrawThread(account, 20), "withdraw");
            a.start();
            b.start();
            a.join();
            b.join();
            System.out.println(account.getBalance());
        }
    
        static class AddThread implements Runnable {
            Account account;
            int     amount;
    
            public AddThread(Account account, int amount) {
                this.account = account;
                this.amount = amount;
            }
    
            public void run() {
                for (int i = 0; i < 200000; i++) {
                    account.add(amount);
                }
            }
        }
    
        static class WithdrawThread implements Runnable {
            Account account;
            int     amount;
    
            public WithdrawThread(Account account, int amount) {
                this.account = account;
                this.amount = amount;
            }
    
            public void run() {
                for (int i = 0; i < 100000; i++) {
                    account.withdraw(amount);
                }
            }
        }
    }


    第一次执行结果为10200,第二次执行结果为1060,每次执行的结果都是不确定的,因为线程的执行顺序是不可预见的。这是java同步产生的根源,synchronized关键字保证了多个线程对于同步块是互斥的,synchronized作为一种同步手段,解决java多线程的执行有序性和内存可见性,而volatile关键字之解决多线程的内存可见性问题。后面将会详细介绍。



    synchronized关键字
            上面说了,java用synchronized关键字做为多线程并发环境的执行有序性的保证手段之一。当一段代码会修改共享变量,这一段代码成为互斥区或临界区,为了保证共享变量的正确性,synchronized标示了临界区。典型的用法如下:

    Java代码 复制代码 收藏代码
    1. synchronized(锁){   
    2.      临界区代码   
    3. }   
    synchronized(锁){
         临界区代码
    } 


    为了保证银行账户的安全,可以操作账户的方法如下:

    Java代码 复制代码 收藏代码
    1. public synchronized void add(int num) {   
    2.      balance = balance + num;   
    3. }   
    4. public synchronized void withdraw(int num) {   
    5.      balance = balance - num;   
    6. }  
    public synchronized void add(int num) {
         balance = balance + num;
    }
    public synchronized void withdraw(int num) {
         balance = balance - num;
    }


    刚才不是说了synchronized的用法是这样的吗:

    Java代码 复制代码 收藏代码
    1. synchronized(锁){   
    2. 临界区代码   
    3. }  
    synchronized(锁){
    临界区代码
    }


    那么对于public synchronized void add(int num)这种情况,意味着什么呢?其实这种情况,锁就是这个方法所在的对象。同理,如果方法是public  static synchronized void add(int num),那么锁就是这个方法所在的class。
            理论上,每个对象都可以做为锁,但一个对象做为锁时,应该被多个线程共享,这样才显得有意义,在并发环境下,一个没有共享的对象作为锁是没有意义的。假如有这样的代码:

    Java代码 复制代码 收藏代码
    1. public class ThreadTest{   
    2.   public void test(){   
    3.      Object lock=new Object();   
    4.      synchronized (lock){   
    5.         //do something   
    6.      }   
    7.   }   
    8. }  
    public class ThreadTest{
      public void test(){
         Object lock=new Object();
         synchronized (lock){
            //do something
         }
      }
    }


    lock变量作为一个锁存在根本没有意义,因为它根本不是共享对象,每个线程进来都会执行Object lock=new Object();每个线程都有自己的lock,根本不存在锁竞争。
            每个锁对象都有两个队列,一个是就绪队列,一个是阻塞队列,就绪队列存储了将要获得锁的线程,阻塞队列存储了被阻塞的线程,当一个被线程被唤醒 (notify)后,才会进入到就绪队列,等待cpu的调度。当一开始线程a第一次执行account.add方法时,jvm会检查锁对象account 的就绪队列是否已经有线程在等待,如果有则表明account的锁已经被占用了,由于是第一次运行,account的就绪队列为空,所以线程a获得了锁,执行account.add方法。如果恰好在这个时候,线程b要执行account.withdraw方法,因为线程a已经获得了锁还没有释放,所以线程 b要进入account的就绪队列,等到得到锁后才可以执行。
    一个线程执行临界区代码过程如下:
    1 获得同步锁
    2 清空工作内存
    3 从主存拷贝变量副本到工作内存
    4 对这些变量计算
    5 将变量从工作内存写回到主存
    6 释放锁
    可见,synchronized既保证了多线程的并发有序性,又保证了多线程的内存可见性。


    生产者/消费者模式
            生产者/消费者模式其实是一种很经典的线程同步模型,很多时候,并不是光保证多个线程对某共享资源操作的互斥性就够了,往往多个线程之间都是有协作的。
            假设有这样一种情况,有一个桌子,桌子上面有一个盘子,盘子里只能放一颗鸡蛋,A专门往盘子里放鸡蛋,如果盘子里有鸡蛋,则一直等到盘子里没鸡蛋,B专门从盘子里拿鸡蛋,如果盘子里没鸡蛋,则等待直到盘子里有鸡蛋。其实盘子就是一个互斥区,每次往盘子放鸡蛋应该都是互斥的,A的等待其实就是主动放弃锁,B 等待时还要提醒A放鸡蛋。
    如何让线程主动释放锁
    很简单,调用锁的wait()方法就好。wait方法是从Object来的,所以任意对象都有这个方法。看这个代码片段:

    Java代码 复制代码 收藏代码
    1. Object lock=new Object();//声明了一个对象作为锁   
    2.    synchronized (lock) {   
    3.        balance = balance - num;   
    4.        //这里放弃了同步锁,好不容易得到,又放弃了   
    5.        lock.wait();   
    6. }  
    Object lock=new Object();//声明了一个对象作为锁
       synchronized (lock) {
           balance = balance - num;
           //这里放弃了同步锁,好不容易得到,又放弃了
           lock.wait();
    }


    如果一个线程获得了锁lock,进入了同步块,执行lock.wait(),那么这个线程会进入到lock的阻塞队列。如果调用 lock.notify()则会通知阻塞队列的某个线程进入就绪队列。
    声明一个盘子,只能放一个鸡蛋

    Java代码 复制代码 收藏代码
    1. import java.util.ArrayList;   
    2. import java.util.List;   
    3.   
    4. public class Plate {   
    5.   
    6.     List<Object> eggs = new ArrayList<Object>();   
    7.   
    8.     public synchronized Object getEgg() {   
    9.         while(eggs.size() == 0) {   
    10.             try {   
    11.                 wait();   
    12.             } catch (InterruptedException e) {   
    13.             }   
    14.         }   
    15.   
    16.         Object egg = eggs.get(0);   
    17.         eggs.clear();// 清空盘子   
    18.         notify();// 唤醒阻塞队列的某线程到就绪队列   
    19.         System.out.println("拿到鸡蛋");   
    20.         return egg;   
    21.     }   
    22.   
    23.     public synchronized void putEgg(Object egg) {   
    24.         while(eggs.size() > 0) {   
    25.             try {   
    26.                 wait();   
    27.             } catch (InterruptedException e) {   
    28.             }   
    29.         }   
    30.         eggs.add(egg);// 往盘子里放鸡蛋   
    31.         notify();// 唤醒阻塞队列的某线程到就绪队列   
    32.         System.out.println("放入鸡蛋");   
    33.     }   
    34.        
    35.     static class AddThread extends Thread{   
    36.         private Plate plate;   
    37.         private Object egg=new Object();   
    38.         public AddThread(Plate plate){   
    39.             this.plate=plate;   
    40.         }   
    41.            
    42.         public void run(){   
    43.             for(int i=0;i<5;i++){   
    44.                 plate.putEgg(egg);   
    45.             }   
    46.         }   
    47.     }   
    48.        
    49.     static class GetThread extends Thread{   
    50.         private Plate plate;   
    51.         public GetThread(Plate plate){   
    52.             this.plate=plate;   
    53.         }   
    54.            
    55.         public void run(){   
    56.             for(int i=0;i<5;i++){   
    57.                 plate.getEgg();   
    58.             }   
    59.         }   
    60.     }   
    61.        
    62.     public static void main(String args[]){   
    63.         try {   
    64.             Plate plate=new Plate();   
    65.             Thread add=new Thread(new AddThread(plate));   
    66.             Thread get=new Thread(new GetThread(plate));   
    67.             add.start();   
    68.             get.start();   
    69.             add.join();   
    70.             get.join();   
    71.         } catch (InterruptedException e) {   
    72.             e.printStackTrace();   
    73.         }   
    74.         System.out.println("测试结束");   
    75.     }   
  • 相关阅读:
    IO 单个文件的多线程拷贝
    day30 进程 同步 异步 阻塞 非阻塞 并发 并行 创建进程 守护进程 僵尸进程与孤儿进程 互斥锁
    day31 进程间通讯,线程
    d29天 上传电影练习 UDP使用 ScketServer模块
    d28 scoket套接字 struct模块
    d27网络编程
    d24 反射,元类
    d23 多态,oop中常用的内置函数 类中常用内置函数
    d22 封装 property装饰器 接口 抽象类 鸭子类型
    d21天 继承
  • 原文地址:https://www.cnblogs.com/kunpengit/p/2254280.html
Copyright © 2011-2022 走看看