Java 并发编程__内存模型、线程同步机制



要认识java的线程安全,必须了解两个主要的点:java的内存模型、java的线程同步机制

参考多线程编程学习博客栏目:

http://blog.csdn.net/axman/article/category/894627

深入 Java 内存模型查看博客:

http://blog.csdn.net/ccit0519/article/details/11241403

http://developer.51cto.com/art/200906/131393.htm

http://blog.csdn.net/vking_wang/article/details/8574376



1.java内存模型:

     不同的平台,内存模型是不一样的,但是jvm的内存模型规范是统一的。java的多线程并发问题最终会反映在java的内存模型上。
     所谓线程安全是要控制多个线程对某个资源的有序访问或修改。
     
    要解决两个主要的问题:可见性和有序性。
    计算机有高速缓存的存在,处理器并不是每次处理数据都是取内存的。
    JVM定义了自己的内存模型,屏蔽了底层内存管理细节,对于java开发人员,要解决的是在jvm内存模型基础上,如何解决多线程的可见性和有序性。        

可见性:

       多个线程之间不能互相传递数据通信,它们之间的沟通只能通过共享变量进行。Java内存模型规定了jvm有主内存,主内存是多个线程共享的。当new一个对象的时候,也是被分配在主内存中,每个线程都有自己的工作内存,工作内存存储了主存的某些对象的副本,当然线程的工作内存大小是有限制的。
执行顺序如下:
[html] view plaincopy
  1. 1、从主存复制变量到当前工作内存(read and load)  
  2. 2、执行代码,改变共享变量值(use and assign)  
  3. 3、用工作内存数据刷新主存相关内容(store and write)  
  4.       当一个共享变量在多个线程的工作内容中都有副本时,如果一个线程修改了这个共享变量,那么其他线程应该能够看到这个被修改后的值,这就是多线程的可见性。  

有序性:

     线程在引用变量时不能直接从主内存中引用,如果线程工作内存中没有改变量,则会从主内存中copy一个副本到工作内存中,这个过程为read-load,完成后线程会引用该副本。当同一线程再度引用该字段时,有可能重新从内存中获取变量副本(read-load-use),也有可能直接引用原来的副本,也就是说read,load,use顺序可以由JVM实现系统决定;   
      线程不能直接为主存中字段赋值,它会将值指定给工作内存中的变量副本(assign),完成后这个变量副本会同步到主存储区,至于何时同步过去,根据JVM系统决定。


package com.thread.concurrent_;

/**
 * Created by IntelliJ IDEA.
 * User: wei.Li
 * Date: 14-8-6
 * Time: 16:32
 */
public class SimleSafe_ {

    public static void main(String[] args) {

        Runnable runnable = new Runnable() {
            int i = 0;
            int k = 0;
            Count count = new Count(i++);

            @Override
            public void run() {
                System.out.println("Thread [" + k++ + "] run ...");
                count.demo();
            }
        };
        for (int j = 0; j < 100; j++) {
            new Thread(runnable).start();
        }
    }


}

class Count {
    private int num;
    private int objNum;

    Count(int objNum) {
        this.objNum = objNum;
    }

    public void demo() {
        for (int i = 1; i <= 10000; i++) {
            num += i;
        }
        // Object objNum一值都是1,说明只有一个Count对象,保证多个线程共享一个Count对象。
        System.out.println(Thread.currentThread().getName() + " : " + num + " \t Object Number: " + objNum);

    }

    public int getNum() {
        return num;
    }

    public void setNum(int num) {
        this.num = num;
    }

    public int getObjNum() {
        return objNum;
    }

    public void setObjNum(int objNum) {
        this.objNum = objNum;
    }
}

/**
 *  ===========
 *  可见性,有序性
 *  ===========
 *  多个线程之间共享了Count类的一个对象,这个对象是被创建在主内存(堆内存)中,每个线程都有自己的工作内存(线程 栈),
 *  工作内存存储了主内存Count对象的一个副本,当线程操作Count对象时,首先从主内存复制Count对象到工作内存中,
 *  然后执行代码 count.demo(),改变了num值,最后用工作内存Count刷新主内存Count。
 *  当一个对象在多个内存中都存在副本时,如果一个内存修改了 共享变量,其它线程也应该能够看到被修改后的值,此为可见性。
 *  由上述可知,一个运算赋值操作并不是一个原子性操作,多个线程执行时,CPU对线程的调度是随机的,我们不知道当前程序被执行到哪步就切换到了下一个线程
 *  经典的例子即银行存取钱。
 */

/**
 *  ===========
 *  特别说明:
 *  ===========
 *  1. 10个线程,可能一开始都从主内存中读取到count对象的num的值都是1并放到各自的线程栈的工作内存中,
 *      但是当线程1执行完并刷新结果到主内存以后,线程2会在进行具体的操作之前,会去清楚自己的工作内存并重新从主内存中读取新的变量num的值。
 *  2. 有序性可以简单的理解为,无论是A线程还是B线程先执行,都要保证有序,
 *      即A线程要么先执行完,再执行B线程,或者B线程先执行完,再执行A线程。即要么先取款,或者要么先存款。
 *  3. 这一点大家一定要注意:特性1是可见性,这是多个线程共享同一个资源时,多个线程天然具有的特性,
 *      但是特性2 即有序性并不是天然具有的,而是我们要通过相关的API来解决的问题,我们往往要确保线程的执行是有序的,或者说是互斥的,即一个线程执行时,不允许另一个线程执行。
 */

2.Synchronized关键字

package com.thread.concurrent_;

/**
 * Created by IntelliJ IDEA.
 * User: wei.Li
 * Date: 14-8-6
 * Time: 16:55
 */


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

/**
 * 对synchronized(this)的一些理解
 * <p>
 * 一、当两个并发线程访问同一个对象object中的这个synchronized(this)同步代码块时,一个时间内只能有一个线程得到执行。
 * 另一个线程必须等待当前线程执行完这个代码块以后才能执行该代码块。
 * 二、然而,另一个线程仍然可以访问该object中的非synchronized(this)同步代码块。
 * 三、尤其关键的是,当一个线程访问object的一个synchronized(this)同步代码块时,其他线程对object中所有其它synchronized(this)同步代码块的访问将被阻塞。
 * 四、第三个例子同样适用其它同步代码块,它就获得了这个object的对象锁。结果,其它线程对该object对象所有同步代码部分的访问都被暂时阻塞。
 * 五、以上规则对其它对象锁同样适用。
 * <p>
 * 每个锁对象都有两个队列,一个是就绪队列,一个是阻塞队列,就绪队列存储了将要获得锁的线程,阻塞队列存储了被阻塞的线程,
 * 当一个线程被唤醒 (notify)后,才会进入到就绪队列,等待CPU的调度,
 * 反之,当一个线程被wait后,就会进入阻塞队列,等待下一次被唤醒,这个涉及到线程间的通 信,
 * 当第一个线程执行输出方法时,获得同步锁,执行输出方法,恰好此时第二个线程也要执行输出方法,
 * 但发现同步锁没有被 释放,第二个线程就会进入就绪队列,等待锁被释放。
 * <p>
 * 一个线程执行互斥代码过程如下:
 * 1. 获得同步锁;
 * 2. 清空工作内存;
 * 3. 从主内存拷贝对象副本到工作内存;
 * 4. 执行代码(计算或者输出等);
 * 5. 刷新主内存数据;
 * 6. 释放同步锁。
 * 所以,synchronized不仅保证了多线程的内存可见性,也解决了线程的随机执行性的问题,即保证了多线程的并发有序性。
 */
public class Synchronized_ {
    public static void main(String[] args) {
        final Outputer outputer = new Outputer();
        outputer.aVoid2SetValue();
        outputer.aVoid2GetValue();
    }
}

class Outputer {

    public static List<String> STRING_LIST = new ArrayList<>();

    public void output(String name) {
        // 为了保证对name的输出不是一个原子操作,这里逐个的输出的name的每个字符
        for (int i = 0; i < name.length(); i++) {
            System.out.print(name.charAt(i));
        }
    }

    public void aVoid2Output() {
        final Outputer output = new Outputer();
        new Thread() {
            public void run() {
                output.output("11111111111111111111111111111111111111111111111111111" +
                        "11111111111111111111111111111111111111111111111111111111111111" +
                        "111111111111111111111111111111111111111111111");
            }
        }.start();

        new Thread() {
            public void run() {
                output.output("222");
            }
        }.start();
/**
 * aVoid2Output()输出一次:
 * 1122211111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111
 *
 * 这就是线程同步问题,我们希望output方法被一个线程完整的执行完之后再切换到下一个线程,Java中使用synchronized保证一段代码在多线程执行时是互斥的,
 * 有两种用法:
 *  1. 使用synchronized将需要互斥的代码包含起来,并上一把锁。
 synchronized (this) {
 for(int i = 0; i < name.length(); i++) {
 System.out.print(name.charAt(i));
 }
 }
 *  2.将synchronized加在需要互斥的方法上。
 public synchronized void output(String name) {...}
 */
    }

    public void aVoid2SetValue() {
        Runnable runnable;
        runnable = () -> {
            for (int i = 0; i < 100; i++) {
                List<String> list = new ArrayList<>();
                list.add("currentThread -> " + i);
                try {
                    Thread.sleep(2000L);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                STRING_LIST = list;
                System.out.println("Update Value ...");

            }
        };


        Thread thread = new Thread(runnable);
        thread.start();
    }


    public void aVoid2GetValue() {
        Runnable runnable = () -> {
            for (int i = 0; i < 100; i++) {
                try {
                    Thread.sleep(new Random().nextInt(2000));
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println("Read Value : " + STRING_LIST);

            }
        };


        for (int i = 0; i < 10; i++) {
            new Thread(runnable).start();
        }
    }
}


3.Volatile关键字


package com.thread.concurrent_;

/**
 * Created by IntelliJ IDEA.
 * User: wei.Li
 * Date: 14-8-6
 * Time: 17:06
 */
public class Volatile_ {
    static final int NUM = 100;
    static int i = 0, j = 0;
    static volatile int m = 0, n = 0 ;

    static void add() {
        while (i <= 10000000) {
            i++;
            j++;
            m++;
            n = NUM + 1;
        }
    }

    static void outPut() {
        System.out.println("i=" + i + "\tj=" + j + "\tm=" + m + "\tn=" + n);
    }

    /**
     * @param args
     */
    public static void main(String[] args) {
        // final VolatileTest test = new VolatileTest();

        for (int k = 0; k < 10; k++) {


            new Thread() {
                public void run() {
                    Volatile_.add();
                }
            }.start();

            new Thread() {
                public void run() {
                    Volatile_.outPut();
                }
            }.start();
        }
    }
}
/**
 * 输出结果:
 * i=559415 j=560994
 * i=42344  j=42511
 */

/**
 * 一些线程执行add方法,另一些线程执行outPut方法,outPut方法有可能打印出不同的i和j的值,按照之前分析的线程执行过程分析一下:

 1. 将变量i从主内存拷贝到工作内存;
 2. 改变i的值;
 3. 刷新主内存数据;
 4. 将变量j从主内存拷贝到工作内存;
 5. 改变j的值;
 6. 刷新主内存数据;

 */

/**
 * 加上volatile可以将共享变量m和n的改变直接响应到主内存中,这样保证了m和n的值可以保持一致,
 * 然而我们不能保证执行outPut方法的线程是在i和j执行到什么程度获取到的,
 * 所以volatile可以保证内存可见性,不能保证并发有序性,因此在上面的输出结果中,m和n的值是不一样的。
 *
 * 在使用volatile关键字时要慎重,并不是只要简单类型变量使用volatile修饰,对这个变量的所有操作都是原来操作,
 * 当变量的值由自身的上一个决定时,如n=n+1、n++等,volatile关键字将失效,只有当变量的值和自身上一个值无关时对该变量的操作才是原子级别的,
 * 如n = m + 1,这个就是原级别的。所以在使用volatile关键时一定要谨慎,如果自己没有把握,可以使用synchronized来代替volatile。
 */



你可能感兴趣的:(编程,并发,线程,内存,线程安全)