java多线程同步 synchronized

来源:互联网 发布:三星网络电视50寸 编辑:程序博客网 时间:2024/04/29 12:30

注:本文内容参考自网络,源头以无法追溯,但是纯手工总结敲打。如有不准确之处,还请各位指教。

多线程同步概述

提到多线程就不得不提共享资源。当有多个线程去访问同一个共享资源时,会造成错误。比如比较常见的:同时写一个变量i。初始时i = 0,当一个线程读取 i 要对其加一操作时,另一个线程也到来了,读取 i 并对其加一操作,这时最后得到的结果i = 1。但是,理想情况下应该是 i = 2。这就是需要对多线程进行同步的典型情况。
下面举一个比较好理解的、发生在日常生活中的、略微有些dirty的,但是很形象的例子。(也是来自一个网友举的例子)假设在一个餐厅中只有一个洗手间,每个人如厕时就会锁上门,这样其他想如厕的人就要在外面等待,只有里面的人如厕完,打开锁出来之后,在外面等待的人才可以去竞争洗手间门的钥匙。在这里,每个人可以看成是一个线程,这个洗手间就是共享资源,洗手间门锁就是防止多人如厕的互斥锁。

多线程数数

假设有5个线程,每个线程都从1数到10。要求这5个线程依次从1数到10。

  1. 第一种实现
package org.fan.learn.synchronize;public class ThreadSynchronized extends Thread{    private int threadNo;    public ThreadSynchronized(int threadNo) {        this.threadNo = threadNo;    }    @Override    public synchronized void run() {        for (int i = 0; i < 10; i++) {            System.out.println("No." + threadNo + " : " + i);        }    }    public static void main(String[] args) {        for (int i = 0; i < 5; i++) {            new ThreadSynchronized(i).start();        }    }}

这个例子的执行结果如下所示:
这里写图片描述
从上面的执行结果中可以看出,多个线程都很随意的数完了,而没有一个一个的数。虽然细心的读者可能也看到run方法使用了synchronized。为什么呢?因为,每一个线程都有一个synchronized,这样每个线程只是在自己内部做同步,没有任何意义。这相当于在一个餐厅中有5个洗手间,而5个顾客每人都有一把钥匙其匹配这5个洗手间。

2.第二种实现
既然已经知道第一种实现的症结所在,现在就可以解决它了。需要考虑这多个线程共用synchronized。可以使用下面这种实现。

package org.fan.learn.synchronize;public class ThreadSynchronized2 extends Thread{    private int threadNo;    private String lock;    public ThreadSynchronized2(int threadNo, String lock) {        this.threadNo = threadNo;        this.lock = lock;    }    @Override    public void run() {        synchronized (lock) {            for (int i = 0; i < 10; i++) {                System.out.println("No." + threadNo + " : " + i);            }        }    }    public static void main(String[] args) {        String lock = new String("lock");        for (int i = 0; i < 5; i++) {            new ThreadSynchronized2(i, lock).start();        }    }}

这种实现的执行结果如下所示:
这里写图片描述
从上图可以看到这次满足了需求。
由于java方法参数的传递都是按值传递,所以在这5个线程中的成员变量lock都是指向的同一个地址,因此在synchronized(lock)时会请求同一个锁。最终,可以满足要求,每个线程依次数数。当然,每个线程的执行顺序就无法保证了。

3.第三种实现
那么还有没有其他的实现方式呢?其实所需要做的就是:使这5个线程能够共享某一个“对象”,这样请求锁时是请求的同一个,就可以满足依次数数的需求。我们知道静态变量和静态方法都是属于这个类的,而不是每个对象都有。这样就有了第三种实现。

package org.fan.learn.synchronize;public class ThreadSynchronized3 extends Thread{    private int threadNo;    public ThreadSynchronized3(int threadNo) {        this.threadNo = threadNo;    }    private static synchronized void abc(int threadNo) {        for (int i = 0; i < 10; i++) {            System.out.println("No." + threadNo + " : " + i);        }    }    @Override    public void run() {        abc(threadNo);    }    public static void main(String[] args) {        for (int i = 0; i < 5; i++) {            new ThreadSynchronized3(i).start();        }    }}

这种实现的执行结果如下所示:
这里写图片描述
从程序执行结果来看也满足了要求。
这里使用了一个static synchronized的方法,使得5个线程共享这个锁资源。

多线程累加

下面再看一个例子。需求是:创建5个,每个线程都能够从0加到9,也就会得出45这个数。

  1. 第一种实现
    代码如下:
package org.fan.learn.synchronize;public class ThreadTest {    class Count {        private int num;        public Count() {            this.num = 0;        }        public void count() {            for (int i = 0; i < 10; i++) {                num += i;            }            System.out.println(Thread.currentThread().getName() + " num = " + num);        }    }    public static void main(String[] args) {        Runnable runnable = new Runnable() {            ThreadTest threadTest = new ThreadTest();            ThreadTest.Count count = threadTest.new Count();            public void run() {                count.count();            }        };        for (int i = 0; i < 5; i++) {            new Thread(runnable).start();        }    }}

这个程序的执行的结果可以说是千奇百怪,当然也会有满足需求的时候,而且一些奇怪的数据“可遇而不可求”,我运行了好多次才得到两个奇怪的数据:
这里写图片描述

这里写图片描述

这里写图片描述

这里写图片描述
为什么会得到这么奇怪的数据呢?
因为,对象count的实例化ThreadTest.Count count = threadTest.new Count();在run方法的外面,这样多个线程就会共享这个count对象,count中的num成员变量也是被多个线程共享的。所以,多线程在同时运行时会对相同的num进行操作,而没有互斥。

2.第二种实现
第一种实现中既然没有互斥,那么,可以考虑给run方法加上个synchronized,代码如下所示:

package org.fan.learn.synchronize;public class ThreadTest2 {    class Count {        private int num;        public Count() {            this.num = 0;        }        public synchronized void count() {            for (int i = 0; i < 10; i++) {                num += i;            }            System.out.println(Thread.currentThread().getName() + " num = " + num);        }    }    public static void main(String[] args) {        Runnable runnable = new Runnable() {            ThreadTest2 threadTest2 = new ThreadTest2();            ThreadTest2.Count count = threadTest2.new Count();            public void run() {                count.count();            }        };        for (int i = 0; i < 5; i++) {            new Thread(runnable).start();        }    }}

这个代码的执行结果如下所示:
这里写图片描述
从执行结果来看也没有满足需求。为什么呢?其实,跟第一种实现方式的原因是一样的。因为,对象count的实例化ThreadTest.Count count = threadTest.new Count();在run方法的外面,这样多个线程就会共享这个count对象,count中的num成员变量也是被多个线程共享的。由于,为run方法添加了synchronized,这样当一个线程在操作num时,其他线程就要等待。
那么,这个与“多线程数数”的第一种实现有什么不同呢?他们同样都是为run方法添加了synchronized修饰,“多线程数数”的第一种实现的执行结果就是杂乱的,没有一个线程一个线程的数数,但是这个累加却能一个线程一个线程的累加,而不出现第一种情况中奇怪的数据呢?因为,在“多线程数数”的第一种实现中,每个线程对象都有一个run方法,而“多线程累加”中每个线程共享count对象,使用同一个run方法。PS:虽然这种解释方式能够解释得通,但是java的内存对象模型是什么样的?对于“多线程数数”的第一种情况实现来说,是不是每个线程对象,都有一个run方法?看样是这样的。在“多线程数数”的第三种实现中,添加了方法private static synchronized void abc(int threadNo);使得多个线程共享这个方法。 内部的原理不是很清楚,”待从头,收拾旧山河,朝内存对象模型”。

3.第三种实现
在第二种实现中,实现了多线程累加的互斥,但是依然没有满足需求。其症结无非在:多个线程共享count对象。现在,使得每个线程都有一个count对象不就好了。
代码实现如下:

package org.fan.learn.synchronize;public class ThreadTest3 {    class Count {        private int num;        public Count() {            this.num = 0;        }        public void count() {            for (int i = 0; i < 10; i++) {                num += i;            }            System.out.println(Thread.currentThread().getName() + " num = " + num);        }    }    public static void main(String[] args) {        Runnable runnable = new Runnable() {            ThreadTest3 threadTest3 = new ThreadTest3();            public void run() {                ThreadTest3.Count count = threadTest3.new Count();                count.count();            }        };        for (int i = 0; i < 5; i++) {            new Thread(runnable).start();        }    }}

这种实现的执行结果如下所示:
这里写图片描述
将count对象的实例化ThreadTest3.Count count = threadTest3.new Count();放在run方法内,这样每个线程在执行时都会new出一个新的对象,每个count对象都有一个num,这样就解决了问题。但是,每个线程都new一个count对象,是不是有点浪费?现在线程只有5个,多了怎么办?

4.第四种实现
在第三种实现中,有了新的问题。其实无非还是关于num是不是会被共享的问题。我们能不能让多个线程都共享一个count对象,但是不共享num呢?
代码如下:

package org.fan.learn.synchronize;public class ThreadTest4 {    class Count {        public void count() {            int num = 0;            for (int i = 0; i < 10; i++) {                num += i;            }            System.out.println(Thread.currentThread().getName() + " num = " + num);        }    }    public static void main(String[] args) {        Runnable runnable = new Runnable() {            ThreadTest4 threadTest4 = new ThreadTest4();            ThreadTest4.Count count = threadTest4.new Count();            public void run() {                count.count();            }        };        for (int i = 0; i < 5; i++) {            new Thread(runnable).start();        }    }}

代码的执行结果如下所示:
这里写图片描述
满足需求,而且多个线程共享同一个count对象。只是将int num = 0;放在了Count类的count方法内。

5.第五种实现
上面一种实现其实就是在说,不要存在“状态性对象”。但是,往往很难不存在状态性对象。如果,存在“状态性对象”,就像第四种实现一样。但是第四种实现,有一种很显然的弊端,就是每一个线程都存在一个独立的count对象,但是在web中,很多对象都是单例的。如何实现count对象是单例的,而又是“状态性对象”呢。ThreadLocal就可以做到这一点。代码如下:

package threadlocal;/** * Created by fan on 15-12-2. */public class Count {    private ThreadLocal<Integer> num = new ThreadLocal<Integer>() {        @Override        protected Integer initialValue() {            return 0;        }    };    public Integer count() {        for (int i = 0; i < 10; i++) {            num.set(num.get() + i);        }        return num.get();    }}
package threadlocal;/** * Created by fan on 15-12-2. */public class ThreadTest extends Thread {    private Count count;    public ThreadTest(Count count) {        this.count = count;    }    public void run() {        System.out.println("Current Thread : " + Thread.currentThread().getName() + " " + count.count());    }    public static void main(String[] args) {        Count count = new Count();        ThreadTest[] threadTest = new ThreadTest[5];        for (int i = 0; i < 5; i++) {            threadTest[i] = new ThreadTest(count);            threadTest[i].start();        }    }}

TheadLocal,顾名思义,就是说,线程的本地化。他使得对象能够在每个执行线程中都有一个本地化的副本,这样,每个线程操作就是不同的、独立的对象。这是一种典型的“空间换时间”的例子。
运行结果如下所示:
这里写图片描述
关于ThreadLocal的详细介绍请参考这边文章(待写)。

0 0
原创粉丝点击