【多线程进阶并发编程一】synchronized应用

2018-03-26  本文已影响71人  5d44bc28b93d
synchronized基本应用.png

并发问题的产生


java-memory-model-3.png

为什么会产生并发问题呢?这个得从Java内存模型(JMM)说起。
JMM将java虚拟机内部划分为堆栈,栈也称线程栈是每个线程独有的。而堆是线程共享的。

每一个运行在Java虚拟机里的线程都拥有自己的线程栈。这个线程栈包含了这个线程调用的方法当前执行点相关的信息。一个线程仅能访问自己的线程栈,即方法的本地变量是不存在线程安全问题的。一个线程创建的本地变量对其它线程不可见,仅自己可见。即使两个线程执行同样的代码,这两个线程任然在在自己的线程栈中的代码来创建本地变量。因此,每个线程拥有每个本地变量的独有版本。


20160921182337904.png

所有原始类型的本地变量都存放在线程栈上,因此对其它线程不可见。一个线程可能向另一个线程传递一个原始类型变量的拷贝,但是它不能共享这个原始类型变量自身。

堆上包含在Java程序中创建的所有对象,无论是哪一个对象创建的。这包括原始类型的对象版本。如果一个对象被创建然后赋值给一个局部变量,或者用来作为另一个对象的成员变量,这个对象任然是存放在堆上。

两程序员小A与小Q的讨论:

Q:为甚么说方法中的变量不存在线程安全问题?

A:因为线程的内部变量属于线程私有,每个线程都有自己的线程栈。

Q:为什么两个线程调用同一个方法时会产生线程安全问题呢?不是说线程栈是线程私有的吗?

A:线程栈确实是线程私有,但是如果线程访问的方法中访问实例变量就会产生线程安全问题。

Q:等等,你前面不是说线程的内部变量是属于线程私有的吗?

A:是的这个说法很正确,对于线程来说调用方法就会创建一个栈帧,然后入线程栈,而方法访问的本地变量是存放在栈里面的,但是成员变量会随着对象存储在堆空间,而对象的引用同样是存储在栈空间的,方法通过引用去访问堆中的对象,又因为堆中的对象是共享的,那么如果两个线程同时对这个共享对象操作你觉得会产生什么结果.

synchronized应用

Java 同步块(synchronized block)用来标记方法或者代码块是同步的。Java同步块用来避免竞争。

背景故事:MT小镇开了一家电影院,电影院老板觉得每次都那么多人过来排队,只有一个窗口效率低下,很多顾客反映本来想看个电影,但是排队等待太久了就放弃了,希望影院能提供更好高效的售票服务。老板一想觉得现在都互联网时代了,这么落后的手工售票方式是时候淘汰了,于是打算招聘技术人员开发这套系统。参加应试的小Q、小A、小M 都想获得这个职位,于是老板就让他们各自设计一个系统

最后老板选择了小M的方案。

现在假设有两个用户服务窗口分别调用saleTicket01,saleTicket02最终我们发现两个窗口能够做到很好的同步。从而进一步证明加锁的为同一个对象。

    public synchronized void saleTicket01() {
        System.out.println(Thread.currentThread().getName() + "请稍等");
        if (totalNum > 0) {
            System.out.println(Thread.currentThread().getName() + "saleTicket01窗口 正在为您出票 票号为 " + this.totalNum);
            this.totalNum--;
        }
    }

    public void saleTicket02() {
        System.out.println(Thread.currentThread().getName() + "请稍等");
        synchronized (this) {
            if (totalNum > 0) {
                System.out.println(Thread.currentThread().getName() + "saleTicket02窗口 正在为您出票 票号为 " + this.totalNum);
                this.totalNum--;
            }
        }
    } 
   private static int totalNum = 10;
   public static synchronized void saleTicket03() {
       System.out.println(Thread.currentThread().getName() + "请稍等");
       if (totalNum > 0) {
           System.out.println(Thread.currentThread().getName() + "正在为您出票 票号为 " + totalNum);
           totalNum--;
       }
   } 

将次方法与saleTicket02对比就能发现他们锁的不是同一个对象,会导致一张票卖多人的问题,那么如何证明这个锁的对象是TicketService.class呢?只需要将saleTicket02改成如下方式,即可保证售票系统正确运行

    public void saleTicket02() {
       System.out.println(Thread.currentThread().getName() + "请稍等");
       synchronized (TicketService.class) {
           if (totalNum > 0) {
               System.out.println(Thread.currentThread().getName() + "saleTicket02窗口 正在为您出票 票号为 " + this.totalNum);
               this.totalNum--;
           }
       }
   }

在JVM中存在String常量池缓存的功能。由于传递的参数都是LOCK,两个线程获取的是同一把锁,谁先抢到cpu资源,谁先执行,那么另外一个线程将永远处于等待中,这就是String常量池带来的问题,所以synchronized都不用String做锁,而是采用new Object()。

class PrintService{
    public  void print(String param){
        synchronized (param){
            while(true){
                System.out.println(Thread.currentThread().getName());
            }
        }
    }
    public static void main(String[] args){
        PrintService printService = new PrintService();
        ThreadA threadA = new ThreadA(printService);
        ThreadB threadB = new ThreadB(printService);
        threadA.start();
        threadB.start();
    }
}
class ThreadA extends Thread{
    private PrintService printService;
    ThreadA(PrintService printService){
        this.printService = printService;
    }

    @Override
    public void run() {
        super.run();
        this.setName("AA");
        printService.print("LOCK");
    }
}
class ThreadB extends Thread{
    private PrintService printService;
    ThreadB(PrintService printService){
        this.printService = printService;
    }

    @Override
    public void run() {
        super.run();
        this.setName("BB");
        printService.print("LOCK");
    }
}

任意对象都可以作为锁,这个后面的深入理解synchronized的原理章节中会说到。

    Object object = new Object();
    public  void print(){
        synchronized (object){
            while(true){
                System.out.println(Thread.currentThread().getName());
            }
        }
    }

当一个线程请求一个由其他线程持有的对象锁时,该线程会阻塞。当线程请求自己持有的对象锁时,如果该线程是重入锁,请求就会成功,否则阻塞。但是在synchronized中,如果一个线程获得一个锁后再次获取同一把锁是可以获取的,这就是锁的可重入性。

优点:避免了重复获取同一把锁造成的死锁

上一篇 下一篇

猜你喜欢

热点阅读