java多线程同步 synchronized

java 专栏收录该内容
86 篇文章 2 订阅

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

多线程同步概述

提到多线程就不得不提共享资源。当有多个线程去访问同一个共享资源时,会造成错误。比如比较常见的:同时写一个变量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
    评论
  • 0
    收藏
  • 一键三连
    一键三连
  • 扫一扫,分享海报

©️2021 CSDN 皮肤主题: 大白 设计师:CSDN官方博客 返回首页
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、C币套餐、付费专栏及课程。

余额充值