Java核心(十三):多线程、synchronized同步控制(第二篇))

一、synchronized关键字、同步方法、同步块
1、synchronized关键字说明
  synchronized关键字,代表这个方法或者代码块被加锁,相当于不管哪一个线程A每次运行到这个方法时,都要检查有没有其它正在用这个方法的线程B。如果有的话,就要等正在使用这个方法的线程B运行完后,此线程A才能运行这个方法;如果没有的话,此线程A可以直接运行这个方法。
  总的说来,synchronized关键字可以作为函数的修饰符,也可作为函数内的语句,也就是平时说的同步方法和同步语句块。如果再细的分类,synchronized可作用于instance变量(成员变量)、object reference(对象实例引用)、static函数和class literals(类名称字面常量)身上。
  在进一步阐述之前,我们需要明确几点:
  • 无论synchronized关键字加在方法上还是对象上,它取得的锁都是对象,而不是把一段代码或函数当作锁――而且同步方法很可能还会被其他线程的对象访问。
  • 每个对象只有一个锁(lock)与之相关联。JVM会给类的每个实例化的对象赋予一个单独的锁。
  • 实现同步是要很大的系统开销作为代价的,甚至可能造成死锁,所以尽量避免无谓的同步控制。
  注意:在同步块和同步方法中,是给类或类的对象进行加锁,而不是给方法加锁。所谓的需要获得对象的锁才能执行方法,也是针对线程而言的。
 
2、synchronized 方法:
通过在方法声明中加入 synchronized关键字来声明 synchronized 方法。如下:
class MyClass{
     private String name;
     public synchronized void setName(String name){
         this.name = name;
     }
}
  synchronized方法控制对类成员变量的访问:每个类实例对应一把锁,每个 synchronized 方法都必须获得调用该方法的类实例的锁方能执行,否则所属线程阻塞,方法一旦执行,就独占该锁,直到从该方法返回时才将锁释放,此后被阻塞的线程方能获得该锁,重新进入可执行状态。这种机制确保了同一时刻对于每一个类实例,其所有声明为 synchronized 的成员函数中至多只有一个处于可执行状态(因为至多只有一个能够获得该类实例对应的锁),从而有效避免了类成员变量的访问冲突(只要所有可能访问类成员变量的方法均被声明为 synchronized)。
  在 Java 中,不光是类实例,每一个类也对应一把锁,这样我们也可将类的静态成员函数声明为 synchronized ,以控制其对类的静态成员变量的访问。
  synchronized 方法的缺陷:若将一个大的方法声明为synchronized 将会大大影响效率,典型地,若将线程类的方法 run()声明为 synchronized ,由于在线程的整个生命期内它一直在运行,因此将导致它对本类任何 synchronized 方法的调用都永远不会成功。
 
3、synchronized 块:
  synchronized 块是这样一个代码块,其中的代码必须获得对象 syncObject (如前所述,可以是类实例或类)的锁方能执行,具体机制同前所述。由于可以针对任意代码块,且可任意指定上锁的对象,故灵活性较高。
  通过 synchronized关键字来声明synchronized 块。语法如下:
synchronized(syncObject){  
    //允许访问控制的代码  
} 

二、synchronized (this)的理解说明

  • 当一个线程正在执行object的一个synchronized(this)同步代码块时,该线程就获得了这个object的对象锁。
  • 当两个并发线程访问同一个对象object中的这个synchronized(this)同步代码块时,一个时间内只能有一个线程得到执行。另一个线程必须等待当前线程执行完这个代码块以后才能执行该代码块。  
  • 当一个线程访问object的一个synchronized(this)同步代码块时,其他线程对object中所有其它synchronized(this)同步代码块的访问将被阻塞。  
  • 但是,当一个线程访问object的一个synchronized(this)同步代码块时,另一个线程仍然可以访问该object中的除synchronized(this)同步代码块以外的部分。
三、总结
1、同步块、同步方法的锁定说明:
  •  对于同步的方法或者代码块来说,必须获得对象锁才能够进入同步方法或者代码块进行操作;
  •  如果采用普通方法级别的同步,则对象锁即为该方法所在的对象,如果是静态方法,对象锁即指该方法所在的类的锁(类的锁,对所有实例化对象都是唯一的)。
  • 对于代码块,对象锁即指synchronized(obj)中的obj;
  • 静态方法则一定会同步,非静态方法需在单例模式才生效,推荐用静态方法
2、实现同步的一些技巧
  搞清楚synchronized锁定的是哪个对象,就能帮助我们设计更安全的多线程程序。 还有一些技巧可以让我们对共享资源的同步访问更加安全:
  • 定义private的instance变量(成员变量)+对应的get()方法,而不要定义public/protected的instance变量。如果将变量定义为public,对象在外界可以绕过同步方法的控制而直接取得它,并改动它。这也是JavaBean的标准实现方式之一。
  • 如果instance变量是一个对象(如数组或ArrayList),那上述方法仍然不安全,因为当外界对象通过get()方法拿到这个instance对象的引用后,又将其指向另一个对象,那么这个private变量也就变了,岂不是很危险。这个时候就需要将get()方法也加上synchronized同步,并且,只返回这个private对象的clone(),这样,调用端得到的就是对象副本的引用了。
  • 还有,比较常用的就有:Collections.synchronizedMap(new HashMap()),当然这个MAP就是生命在类中的全局变量,就是一个线程安全的HashMap,web的application是全web容器公用的,所以要使用线程安全来保证数据的正确。
3、Java中多线程锁释放的条件:
  • 执行完同步代码块,就会释放锁。(synchronized)
  • 在执行同步代码块的过程中,遇到异常而导致线程终止,锁也会被释放。(exception)
  • 在执行同步代码块的过程中,执行了锁所属对象的wait()方法,这个线程会释放锁,进入对象的等待池。(wait)
 
四、代码分析
  接着来讨论synchronized用到不同地方对代码产生的影响: 假设P1、P2是同一个类的不同对象,这个类中定义了以下几种情况的同步块或同步方法:
1、把synchronized当作普通方法的修饰符时,示例代码如下:
Public synchronized void methodAAA(){
    //
}

  这时synchronized锁定的是调用这个同步方法的实例对象。也就是说,当一个对象P1在不同的线程中执行这个同步方法时,它们之间会形成互斥,达到同步的效果。但是这个对象所属的类所产生的另一对象P2却可以任意调用这个被加了synchronized关键字的方法。上边的示例代码等同于如下代码:

public void methodAAA() {
synchronized (this)      {    // 【说明1】
       //…..
}
}
【说明1】处的this指的就是调用这个方法的当前实例对象,如P1。可见同步方法实质是将synchronized作用于object reference(当前对象引用)。那个拿到了P1对象锁的线程,才可以调用P1的同步方法,而对P2而言,P1这个锁与它毫不相干,所以不会受到影响。这种情况下,程序有可能会摆脱同步机制的控制,造成数据混乱。

2、同步块,示例代码如下:

public void method3(SomeObject obj){
    synchronized(obj) {    //锁定的是对象obj的对象锁
       //…..
    }
}
  这时,锁就是obj这个对象,谁拿到这个锁谁就可以运行它所控制的那段代码。当有一个明确的对象作为锁时,就可以按以上方式来写程序;当没有明确的对象作为锁时,但还想让一段代码同步时,可以创建一个特殊的instance变量(必须是一个对象)来充当锁,此时代码如下:
class Foo implements Runnable{
        private byte[] lock = new byte[0]; // 特殊的instance变量;也可以用String常量作为锁
        Public void methodA(){
           synchronized(lock) { //… }
        }
        //…..
}
//注:零长度的byte数组对象创建起来将比任何对象都经济――查看编译后的字节码:生成零长度的byte[]对象只需3条操作码,而Object lock = new Object()则需要7行操作码。
3、将synchronized作用于static 函数,示例代码如下:
Class Foo{
    public synchronized static void methodAAA()   // 同步的static 函数
    {
        //….
    }
    public void methodBBB()
    {
       synchronized(Foo.class)   // class literal(类名称字面常量)
    }
}
  代码中的methodBBB()方法是把class literal作为锁的情况,它和同步的static函数产生的效果是一样的,取得的锁很特别,是当前调用这个方法的对象所属的类(Class,而不再是由这个Class产生的某个具体对了)。
  记得在《Effective Java》一书中看到过将 Foo.class和 P1.getClass()用于作同步锁还不一样,不能用P1.getClass()来达到锁这个Class的目的。P1指的是由Foo类产生的对象。可以推断:如果一个类中定义了一个synchronized的static函数A,也定义了一个synchronized 的instance函数B,那么这个类的同一对象Obj在多线程中分别访问A和B两个方法时,不会构成同步,因为它们的锁都不一样。A方法的锁是Obj这个对象,而B的锁是Obj所属的那个Class。
 
本篇博客参考以下文章,整理完成:
(1)作者:ac噜噜噜
链接:https://www.jianshu.com/p/35c414fcb453
(2)链接:https://www.jb51.net/article/45094.htm
 
 
 
原文地址:https://www.cnblogs.com/newbie27/p/10552612.html