1、对象与锁
每一个Object类及其子类的实例都拥有一个锁。其中,标量类型int,float等不是对象类型,但是标量类型可以通过其包装类来作为锁。单独的成员变量是不能被标明为同步的。锁只能用在使用了这些变量的方法上。成员变量可以被声明为volatile,这种方式会影响该变量的原子性,可见性以及排序性。
类似的,持有标量变量元素的数组对象拥有锁,但是其中的标量元素却不拥有锁。(也就是说,没有办法将数组成员声明为volatile类型的)。如果锁住了一个数组并不代表其数组成员都可以被原子的锁定。也没有能在一个原子操作中锁住多个对象的方法。
Class实例本质上是个对象。正如下所述,在静态同步方法中用的就是类对象的锁。
2、同步方法和同步块
使用synchronized关键字,有两种语法结构:同步代码块和同步方法。同步代码块需要提供一个作为锁的对象参数。这就允许了任意方法可以去锁任一一个对象。但在同步代码块中使用的最普通的参数却是this。
同步代码块被认为比同步方法更加的基础。如下两种声明方式是等同的:
1
synchronized void f() { /* body */ }
2
void f() { synchronized(this) { /* body */ } }
synchronized关键字并不是方法签名的一部分。所以当子类覆写父类中的同步方法或是接口中声明的同步方法的时候,synchronized修饰符是不会被自动继承的,另外,构造方法不可能是真正同步的(尽管可以在构造方法中使用同步块)。
同步实例方法在其子类和父类中使用同样的锁。但是内部类方法的同步却独立于其外部类, 然而一个非静态的内部类方法可以通过下面这种方式锁住其外部类:
1
synchronized(OuterClass.this) { /* body */ }
3、等待锁与释放锁
使用synchronized关键字须遵循一套内置的锁等待-释放机制。所有的锁都是块结构的。当进入一个同步方法或同步块的时候必须获得该锁,而退出的时候(即使是异常退出)必须释放这个锁。你不能忘记释放锁。
锁操作是建立在独立的线程上的而不是独立的调用基础上。一个线程能够进入一个同步代码的条件是当前锁未被占用或者是当前线程已经占用了这个锁,否则线程就会阻塞住。(这种可重入锁或是递归锁不同于POSIX线程)。这就允许一个同步方法可以去直接调用同一个锁管理的另一个同步方法,而不需要被冻结(注:即不需要再经历释放锁-阻塞-申请锁的过程)。
同步方法或同步块遵循这种锁获取/锁释放的机制有一个前提,那就是所有的同步方法或同步块都是在同一个锁对象上。如果一个同步方法正在执行中,其他的非同步方法也可以在任何时候执行。也就是说,同步不等于原子性,但是同步机制可以用来实现原子性。
当一个线程释放锁的时候,另一个线程可能正等待这个锁(也可能是同一个线程,因为这个线程可能需要进入另一个同步方法)。但是关于哪一个线程能够紧接着获得这个锁以及什么时候,这是没有任何保证的。另外,没有什么办法能够得到一个给定的锁正被哪个线程拥有着。
除了锁控制之外,同步也会对底层的内存系统带来副作用。
4、静态变量/方法
锁住一个对象并不会原子性的保护该对象类或其父类的静态成员变量。而应该通过同步的静态方法或代码块来保证访问一个静态的成员变量。静态同步使用的是静态方法锁声明的类对象所拥有的锁。类C的静态锁可以通过内置的实例方法获取到:synchronized(C.class) { /* body */ }
每个类所对应的静态锁和其他的类(包括其父类)没有任何的关系。通过在子类中增加一个静态同步方法来试图保护父类中的静态成员变量是无效的。应使用显式的代码块来代替。
如下这种方式也是一种不好的实践:
synchronized(getClass()) { /* body */ } // Do not use
这种方式,可能锁住的实际中的类,并不是需要保护的静态成员变量所对应的类(有可能是其子类)
Java虚拟机在类加载和类初始化阶段,内部获得并释放类锁。除非你要去写一个特殊的类加载器或者需要使用多个锁来控制静态初始顺序,这些内部机制不应该干扰普通类对象的同步方法和同步块的使用。Java虚拟机没有什么内部操作可以独立的获取你创建和使用的类对象的锁。然而当你继承java.*的类的时候,你需要特别小心这些类中使用的锁机制
关于Java线程问题,推荐上海尚学堂Java线程相关技术文章,如:
《认知Java 同步块(synchronized block)》;
《Java多线程爬虫实现》;
《Java线程的知识要点概述》;
《Java 线程池(ThreadPoolExecutor)原理分析与使用》等等。
感谢阅读上海Java培训文章,更多请关注收藏,后续Java相关技术文章陆续奉上,转载请注明出处!