有锁编程

当多线程共同读写共享资源的时候,为了达到线程安全的目的,从而有了有锁编程。先从基本概念谈起:

什么叫多线程?

一段程序加载到内存,引导启动后,操作系统会给该程序创建一个以PID为唯一标示的进程。进程简而言之就是这段程序在操作系统之上的一次动态执行(从加载到内存,引导启动,运行,到结束)。进程包含很丰富的信息(PID,进程控制单元,进程空间,分配的内存资源,以及操作系统调度得到的CPU资源,还有别的资源),同时也是比较重量级的。而一个进程中为了让CPU以及IO资源使用的更到位,从而有了线程。所以线程诞生的目的:尽量吃满资源(CPU,IO),或者换句话说就是尽量更充分的使用计算机资源,从而达到有更高的吞吐量。一个进程内部的线程可以共享该进程的资源,同时线程也有自己的TID,线程栈空间,线程间公用的内存空间。

 

好,说完上面基本信息,所以将你的程序设计成多线程程序就水到渠成了。

什么叫共享资源?

共享资源可以是一个变量,可以是一个文件,也可以是一个复杂的数据结构。因为某团厕所坑位比较紧张,所以用WC坑位举栗子。

正常情况下一个坑位同时只能有一个人占用,这个是不允许同个人同时享用的。所以在这里坑位就可以理解成一个共享资源。那多个人(多线程)想使用一个坑位,怎么办呢?很简单给每个坑位上一把锁,有个这个锁就可以保证坑位这个共享资源同时只被一个人使用。

通俗的栗子讲完,那进入操作系统的世界中,如何理解共享资源?下面将两个计算机世界的例子

线程间状态变量可见的栗子

两个线程共享isRunning变量,第一个线程对isRunning有修改成false,但是第二个线程一直读取不到该变量的修改。

public class VolatileExampleV2 {
    boolean isRunning = true;
    long gap = 50;

    public static void main(String[] args) {

        new VolatileExampleV2().test();
    }

    private void test() {
        new Thread(new Runnable() {
            @Override
            public void run() {
                doSomeThing(2000);

                isRunning = false;
                System.out.println("first thread currentTime:" + System.currentTimeMillis());

                doSomeThing(500);
            }
        }).start();

        new Thread(new Runnable() {
            @Override
            public void run() {
                while (isRunning) {

                }

                System.out.println("Second Thread currentTime:" + System.currentTimeMillis());
            }
        }).start();
    }

    private static void doSomeThing(long time) {
        long sum = 0;
        long size = time * 100000;

        for (int i = 0; i < size; i++) {
            sum += i;
        }
    }
}


###########只打印一行内容,程序一直在第二个线程跑,并未结束
first thread currentTime:1442215575596

通过volatile关键字就可以控制线程间变量的可见性,栗子如下:

public class VolatileExampleV2 {
    volatile boolean isRunning = true;
    long gap = 50;

    public static void main(String[] args) {

        new VolatileExampleV2().test();
    }

    private void test() {
        new Thread(new Runnable() {
            @Override
            public void run() {
                doSomeThing(2000);

                isRunning = false;
                System.out.println("first thread currentTime:" + System.currentTimeMillis());

                doSomeThing(500);
            }
        }).start();

        new Thread(new Runnable() {
            @Override
            public void run() {
                while (isRunning) {

                }

                System.out.println("Second Thread currentTime:" + System.currentTimeMillis());
            }
        }).start();
    }

    private static void doSomeThing(long time) {
        long sum = 0;
        long size = time * 100000;

        for (int i = 0; i < size; i++) {
            sum += i;
        }
    }
}

#####################这次两个线程都能跑完,第一个线程对共享变量的修改,被第二个线程看见了。

Second Thread currentTime:1442215856761
first thread currentTime:1442215856761

PS:想写这个栗子很不容易,因为必须确保第二个线程每次读取共享状态变量是从该CPU的独立缓存中读取,不能从内存中读取,不然就实现不了线程可见性的例子。

 

读写修改共享参数的栗子

public class SynchronizedExample {
    private Count wangxin = new Count();

    public static void main(String[] args) {
        new SynchronizedExample().test();
    }

    private void test() {
        System.out.println("user count:" + wangxin);

        List<Thread> threadList = new ArrayList<Thread>();
        for (int i = 0; i < 10; i++) {
            threadList.add(new Thread(new Runnable() {
                @Override
                public void run() {
                    try {
                        Thread.sleep(200);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }

                    saveMoeny();
                }
            }));
        }

        for (Thread each : threadList) {
            each.start();
        }

        try {
            Thread.currentThread().join(5000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        System.out.println("user count:" + wangxin);
    }

    public void saveMoeny() {
        int moeny = wangxin.getMoeny();
        wangxin.setMoeny(moeny + 100);
    }
}


public class Count {
    private int moeny = 100;

    public int getMoeny() {
        return moeny;
    }

    public void setMoeny(int moeny) {
        this.moeny = moeny;
    }

    @Override
    public String toString() {
        return "Count{" +
                "moeny=" + moeny +
                '}';
    }
}

##########一共存10份钱,最后账户只剩下了800块钱。原因就是账户这个共享资源没有做好保护,导致账户钱少了。
user count:Count{moeny=100}
user count:Count{moeny=800}

  通过synchronized关键字进行共享资源的保护,从而安全的保证了多人存款。

public class SynchronizedExample {
    private Count wangxin = new Count();

    public static void main(String[] args) {
        new SynchronizedExample().test();
    }

    private void test() {
        System.out.println("user count:" + wangxin);

        List<Thread> threadList = new ArrayList<Thread>();
        for (int i = 0; i < 10; i++) {
            threadList.add(new Thread(new Runnable() {
                @Override
                public void run() {
                    try {
                        Thread.sleep(200);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }

                    saveMoeny();
                }
            }));
        }

        for (Thread each : threadList) {
            each.start();
        }

        try {
            Thread.currentThread().join(5000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        System.out.println("user count:" + wangxin);
    }

    public synchronized void saveMoeny() {
        int moeny = wangxin.getMoeny();
        wangxin.setMoeny(moeny + 100);
    }
}

什么叫线程安全? 

从上面第二个栗子应该可以看到线程安全的本质含义了。针对共享的资源,如果不做保护的话,同时被多个线程写操作会导致结果不可预期,可能每次跑的结果都不一致。所以线程安全就是在任何情况下同一段代码被多线程执行完毕,结果都一致,并且符合预期。

如何做到线程安全?

从上面的例子也可以看出有两种手段:

  1. 采用volatile关键字保证线程间状态变量的可见性
  2. 采用synchronized关键字保证线程对临界区的串行读写

volatile关键字在JVM中如何实现的?

JVM的内存模型中保证:

  1. volatile关键字修饰的变量都只从内存读取。不会从CPU的缓存中读取
  2. volatile关键字修饰的变量写操作,写回内存,同时CPU各级缓存该变量失效

由JVM上面两条就能保证,CPU的各个核都能从内存中读取到数据,从而保证了各个线程可见。

synchronized关键字在JVM中如何实现?

synchronized关键字要搞定的事情是临界区代码保证线程串行访问,就是上面的厕所坑串行的被使用。

Java编译器在.java文件被编译的时候,在临界区代码的入口处和出口处插入了monitorenter和monitorexit字节码指令。

同时在对象创建的时候,在其对象头部用两个字节(MarkWord)来表示该对象上是否有锁,同时被那条线程占用。

 

posted @ 2015-09-14 17:25  欣欣王  阅读(751)  评论(0编辑  收藏  举报