java线程安全总结(一)

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

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

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是一个原子操做。看看下面代码:
jvm

Java代码          性能

  1. public class Account {  测试

  2.     private int balance;  this

 

  1.     public Account(int balance) {  

  2.         this.balance = balance;  

  3.     }  

  4.     public int getBalance() {  

  5.         return balance;  

  6.     }  

  7.     public void add(int num) {  

  8.         balance = balance + num;  

  9.     }  

 

  1.     public void withdraw(int num) {  

  2.         balance = balance - num;  

  3.     }  

 

  1.     public static void main(String[] args) throws InterruptedException {  

  2.         Account account = new Account(1000);  

  3.         Thread a = new Thread(new AddThread(account, 20), "add");  

  4.         Thread b = new Thread(new WithdrawThread(account, 20), "withdraw");  

  5.         a.start();  

  6.         b.start();  

  7.         a.join();  

  8.         b.join();  

  9.         System.out.println(account.getBalance());  

  10.     }  

  11.     static class AddThread implements Runnable {  

  12.         Account account;  

  13.         int     amount;  

  14.         public AddThread(Account account, int amount) {  

  15.             this.account = account;  

  16.             this.amount = amount;  

  17.         }  

  18.         public void run() {  

  19.             for (int i = 0; i < 200000; i++) {  

  20.                 account.add(amount);  

  21.             }  

  22.         }  

  23.     }  

  24.     static class WithdrawThread implements Runnable {  

  25.         Account account;  

  26.         int     amount;  

  27.         public WithdrawThread(Account account, int amount) {  

  28.             this.account = account;  

  29.             this.amount = amount;  

  30.         }  

  31.         public void run() {  

  32.             for (int i = 0; i < 100000; i++) {  

  33.                 account.withdraw(amount);  

  34.             }  

  35.         }  

  36.     }  

  37. }  

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.     }  

  76. }  

import java.util.ArrayList;
import java.util.List;

public class Plate {

    List<Object> eggs = new ArrayList<Object>();

    public synchronized Object getEgg() {
        while(eggs.size() == 0) {
            try {
                wait();
            } catch (InterruptedException e) {
            }
        }

        Object egg = eggs.get(0);
        eggs.clear();// 清空盘子
        notify();// 唤醒阻塞队列的某线程到就绪队列
        System.out.println("拿到鸡蛋");
        return egg;
    }

    public synchronized void putEgg(Object egg) {
        while(eggs.size() > 0) {
            try {
                wait();
            } catch (InterruptedException e) {
            }
        }
        eggs.add(egg);// 往盘子里放鸡蛋
        notify();// 唤醒阻塞队列的某线程到就绪队列
        System.out.println("放入鸡蛋");
    }
    
    static class AddThread extends Thread{
        private Plate plate;
        private Object egg=new Object();
        public AddThread(Plate plate){
            this.plate=plate;
        }
        
        public void run(){
            for(int i=0;i<5;i++){
                plate.putEgg(egg);
            }
        }
    }
    
    static class GetThread extends Thread{
        private Plate plate;
        public GetThread(Plate plate){
            this.plate=plate;
        }
        
        public void run(){
            for(int i=0;i<5;i++){
                plate.getEgg();
            }
        }
    }
    
    public static void main(String args[]){
        try {
            Plate plate=new Plate();
            Thread add=new Thread(new AddThread(plate));
            Thread get=new Thread(new GetThread(plate));
            add.start();
            get.start();
            add.join();
            get.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("测试结束");
    }
}

  执行结果:

Html代码   复制代码    收藏代码  

  1. 放入鸡蛋  

  2. 拿到鸡蛋  

  3. 放入鸡蛋  

  4. 拿到鸡蛋  

  5. 放入鸡蛋  

  6. 拿到鸡蛋  

  7. 放入鸡蛋  

  8. 拿到鸡蛋  

  9. 放入鸡蛋  

  10. 拿到鸡蛋  

  11. 测试结束  

放入鸡蛋
拿到鸡蛋
放入鸡蛋
拿到鸡蛋
放入鸡蛋
拿到鸡蛋
放入鸡蛋
拿到鸡蛋
放入鸡蛋
拿到鸡蛋
测试结束

 

 


声明一个Plate对象为plate,被线程A和线程B共享,A专门放鸡蛋,B专门拿鸡蛋。假设
1  开始,A调用plate.putEgg方法,此时eggs.size()为0,所以顺利将鸡蛋放到盘子,还执行了notify()方法,唤醒锁的阻塞队列 的线程,此时阻塞队列尚未线程。
2 又有一个A线程对象调用plate.putEgg方法,此时eggs.size()不为0,调用wait()方法,本身进入了锁对象的阻塞队列。
3  此时,来了一个B线程对象,调用plate.getEgg方法,eggs.size()不为0,顺利的拿到了一个鸡蛋,还执行了notify()方法,唤 醒锁的阻塞队列的线程,此时阻塞队列有一个A线程对象,唤醒后,它进入到就绪队列,就绪队列也就它一个,所以立刻获得锁,开始往盘子里放鸡蛋,此时盘子是 空的,所以放鸡蛋成功。
4 假设接着来了线程A,就重复2;假设来料线程B,就重复3。

整个过程都保证了放鸡蛋,拿鸡蛋,放鸡蛋,拿鸡蛋。


volatile关键字
       volatile是java提供的一种同步手段,只不过它是轻量级的同步,为何这么说,由于volatile只能保证多线程的内存可见性,不能保证多线 程的执行有序性。而最完全的同步要保证有序性和可见性,例如synchronized。任何被volatile修饰的变量,都不拷贝副本到工做内存,任何 修改都及时写在主存。所以对于Valatile修饰的变量的修改,全部线程立刻就能看到,可是volatile不能保证对变量的修改是有序的。什么意思 呢?假若有这样的代码:

Java代码          

  1. public class VolatileTest{  

  2.   public volatile int a;  

  3.   public void add(int count){  

  4.        a=a+count;  

  5.   }  

  6. }  

public class VolatileTest{
  public volatile int a;
  public void add(int count){
       a=a+count;
  }
}

 


        当一个VolatileTest对象被多个线程共享,a的值不必定是正确的,由于a=a+count包含了好几步操做,而此时多个线程的执行是无序的,因 为没有任何机制来保证多个线程的执行有序性和原子性。volatile存在的意义是,任何线程对a的修改,都会立刻被其余线程读取到,由于直接操做主存, 没有线程对工做内存和主存的同步。因此,volatile的使用场景是有限的,在有限的一些情形下可使用 volatile 变量替代锁。要使  volatile 变量提供理想的线程安全,必须同时知足下面两个条件:
1)对 变量的写操做不依赖于当前值。
2)该变量没有包含在具备其余变量的不变式中

volatile只保证了可见性,因此Volatile适合直接赋值的场景,如

Java代码          

  1. public class VolatileTest{  

  2.   public volatile int a;  

  3.   public void setA(int a){  

  4.       this.a=a;  

  5.   }  

  6. }  

public class VolatileTest{
  public volatile int a;
  public void setA(int a){
      this.a=a;
  }
}

 在没有volatile声明时,多线程环境下,a的最终值不必定是正确的,由于this.a=a;涉及到给a赋值和将a同步回主存的步骤,这个顺序可能被 打乱。若是用volatile声明了,读取主存副本到工做内存和同步a到主存的步骤,至关因而一个原子操做。因此简单来讲,volatile适合这种场 景:一个变量被多个线程共享,线程直接给这个变量赋值。这是一种很简单的同步场景,这时候使用volatile的开销将会很是小。

相关文章
相关标签/搜索