在并发编程中,经常遇到多个线程访问同一个 共享资源 ,这时候作为开发者必须考虑如何维护数据一致性,在java中synchronized关键字被常用于维护数据一致性。synchronized机制是给共享资源上锁,只有拿到锁的线程才可以访问共享资源,这样就可以强制使得对共享资源的访问都是顺序的,因为对于共享资源属性访问是必要也是必须的,下文会有具体示例演示。
一.java中的锁
一般在java中所说的锁就是指的内置锁,每个java对象都可以作为一个实现同步的锁,虽然说在java中一切皆对象, 但是锁必须是引用类型的,基本数据类型则不可以 。每一个引用类型的对象都可以隐式的扮演一个用于同步的锁的角色,执行线程进入synchronized块之前会自动获得锁,无论是通过正常语句退出还是执行过程中抛出了异常,线程都会在放弃对synchronized块的控制时自动释放锁。 获得锁的唯一途径就是进入这个内部锁保护的同步块或方法 。
正如引言中所说,对共享资源的访问必须是顺序的,也就是说当多个线程对共享资源访问的时候,只能有一个线程可以获得该共享资源的锁,当线程A尝试获取线程B的锁时,线程A必须等待或者阻塞,直到线程B释放该锁为止,否则线程A将一直等待下去,因此java内置锁也称作互斥锁,也即是说锁实际上是一种互斥机制。
根据使用方式的不同一般我们会将锁分为对象锁和类锁,两个锁是有很大差别的,对象锁是作用在实例方法或者一个对象实例上面的,而类锁是作用在静态方法或者Class对象上面的。一个类可以有多个实例对象,因此一个类的对象锁可能会有多个,但是每个类只有一个Class对象,所以类锁只有一个。 类锁只是一个概念上的东西,并不是真实存在的,它只是用来帮助我们理解锁定的是实例方法还是静态方法区别的 。
在java中实现锁机制不仅仅限于使用synchronized关键字,还有JDK1.5之后提供的Lock,Lock不在本文讨论范围之内。一个synchronized块包含两个部分:锁对象的引用,以及这个锁保护的代码块。如果作用在实例方法上面,锁就是该方法所在的当前对象,静态synchronized方法会从Class对象上获得锁。
二.synchronized使用示例
1.多窗口售票
假设一个火车票售票系统,有若干个窗口同时售票,很显然在这里票是作为多个窗口的共享资源存在的,由于座位号是确定的,因此票上面的号码也是确定的,我们用多个线程来模拟多个窗口同时售票,首先在不使用synchronized关键字的情况下测试一下售票情况。
先将票本身作为一个共享资源放在单独的线程中,这种作为共享资源存在的线程很显然应该是实现Runnable接口,我们将票的总数num作为一个入参传入,每次生成一个票之后将num做减法运算,直至num为0即停止,说明票已经售完了,然后开启多个线程将票资源传入。
public class Ticket implements Runnable{ private int num;//票数量 private boolean flag=true;//若为false则售票停止 public Ticket(int num){ this.num=num; } @Override public void run() { while(flag){ ticket(); } } private void ticket(){ if(num<=0){ flag=false; return; } try { Thread.sleep(20);//模拟延时操作 } catch (InterruptedException e) { e.printStackTrace(); } //输出当前窗口号以及出票序列号 System.out.println(Thread.currentThread().getName()+"售出票序列号:"+num--); } } public class MainTest { public static void main(String[] args) { Ticketticket = new Ticket(5); Threadwindow01 = new Thread(ticket, "窗口01"); Threadwindow02 = new Thread(ticket, "窗口02"); Threadwindow03 = new Thread(ticket, "窗口03"); window01.start(); window02.start(); window03.start(); } }</div>
程序的输出结果如下:
窗口02售出票序列号:5 窗口03售出票序列号:4 窗口01售出票序列号:5 窗口02售出票序列号:3 窗口01售出票序列号:2 窗口03售出票序列号:2 窗口02售出票序列号:1 窗口03售出票序列号:0 窗口01售出票序列号:-1</div>
从上面程序运行结果可以看出不但票的序号有重号而且出票数量也不对,这种售票系统比12306可要烂多了,人家在繁忙的时候只是刷不到票而已,而这里的售票系统倒好了,出票比预计的多了而且会出现多个人争抢做同一个座位的风险。如果是单个售票窗口是不会出现这种问题,多窗口同时售票就会出现争抢共享资源因此紊乱的现象,解决该现象也很简单,就是在ticket()方法前面加上synchronized关键字或者将ticket()方法的方法体完全用synchronized块包括起来。
//方式一 private synchronized void ticket(){ if(num<=0){ flag=false; return; } try { Thread.sleep(20);//模拟延时操作 } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName()+"售出票序列号:"+num--); } //方式二 private void ticket(){ synchronized (this) { if (num <= 0) { flag = false; return; } try { Thread.sleep(20);//模拟延时操作 } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName() + "售出票序列号:" + num--); } }</div>
再看一下加入synchronized关键字的程序运行结果:
窗口01售出票序列号:5 窗口03售出票序列号:4 窗口03售出票序列号:3 窗口02售出票序列号:2 窗口02售出票序列号:1</div>
从这里可以看出在实例方法上面加上synchronized关键字的实现效果跟对整个方法体加上synchronized效果是一样的。 另外一点需要注意加锁的时机也非常重要 ,本示例中ticket()方法中有两处操作容易出现紊乱,一个是在if语句模块,一处是在num–,这两处操作本身都不是原子类型的操作,但是在使用运行的时候需要这两处当成一个整体操作,所以synchronized将整个方法体都包裹在了一起。如若不然,假设num当前值是1,但是窗口01执行到了num–,整个操作还没执行完成,只进行了赋值运算还没进行自减运算,但是窗口02已经进入到了if语句模块,此时num还是等于1,等到窗口02执行到了输出语句的时候,窗口01的num–也已经将自减运算执行完成,这时候窗口02就会输出序列号0的票。再者如果将synchronized关键字加在了run方法上面,这时候的操作不会出现紊乱或者错误,但是这种加锁方式无异于单窗口操作,当窗口01拿到锁进入run()方法之后,必须等到flag为false才会将语句执行完成跳出循环,这时候的num就已经为0了,也就是说票已经被售卖完了,这种方式摒弃了多线程操作,违背了最初的设计原则-多窗口售票。
2.懒汉式单例模式
创建单例模式有很多中实现方式,本文只讨论懒汉式创建。在Android开发过程中单例模式可以说是最常使用的一种设计模式,因为它操作简单还可以有效减少内存溢出。下面是懒汉式创建单例模式一个示例:
public class Singleton { private static Singletoninstance; private Singleton() { } public static SingletongetInstance() { if (instance == null) { instance = new Singleton(); } return instance; } }</div>
如果对于多窗口售票逻辑已经完全明白了的话就可以看出这里的实现方式是有问题的,我们可以简单的创建几个线程来获取单例输出对象的hascode值。
com.sunny.singleton.Singleton@15c330aa com.sunny.singleton.Singleton@15c330aa com.sunny.singleton.Singleton@41aff40f</div>
在多线程模式下发现会出现不同的对象,这种单例模式很显然不是我们想要的,那么根据上面多窗口售票的逻辑我们在getInstance()方法上面加上一个synchronized关键字,给该方法加上锁,加上锁之后可以避免多线程模式下生成多个不同对象,但是同样会带来一个效率问题,因为不管哪个线性进入getInstance()方法都会先获得锁,然后再次释放锁,这是一个方面,另一个方面就是只有在第一次调用getInstance()方法的时候,也就是在if语句块内才会出现多线程并发问题,而我们却索性将整个方法都上锁了。讨论到这里就引出了另外一个问题,究竟是synchronized方法好还是synchronized代码块好呢? 有一个原则就是锁的范围越小越好 ,加锁的目的就是将锁进去的代码作为原子性操作,因为非原子操作都不是线程安全的,因此synchronized代码块应该是在开发过程中优先考虑使用的加锁方式。
public static SingletongetInstance() { if (instance == null) { synchronized (Singleton.class) { instance = new Singleton(); } } return instance; }</div>
这里也会遇到类似上面的问题,多线程并发下回生成多个实例,如线