Java线程同步中关键字synchronized详述

标签: java 线程 同步 | 发表时间:2014-08-02 17:48 | 作者:kljjack
出处:http://www.iteye.com
synchronized关键可以修饰函数、函数内语句。无论它加上方法还是对象上,它取得的锁都是对象,而不是把一段代码或是函数当作锁。

1,当两个并发线程访问同一个对象object中的这个synchronized(this)同步代码块时,一段时间只能有一个线程得到执行,而另一个线程只有等当前线程执行完以后才能执行这块代码。
2,当一个线程访问object中的一个synchronized(this)同步代码块时,其它线程仍可以访问这个object中是其它非synchronized (this)代码块。
3,这里需要注意的是,当一个线程访问object的一个synchronized(this)代码块时,其它线程对这个object中其它synchronized (this)同步代码块的访问将被阻塞。
4,以上所述也适用于其它的同步代码块,也就是说,当一个线程访问object的一个synchronized(this)同步代码块时,这个线程就获得了object的对象锁。而且每个对象(即类实例)对应着一把锁,每个synchronized(this)都必须获得调用该代码块儿(可以函数,也可以是变量)的对象的锁才能执行,否则所属线程阻塞,方法一旦执行就会独占该锁,直到从方法返回时,也释放这个锁,重新进入可执行状态。这种机制确保了同一时刻对于每一个对象,其所有声明为synchronized的成员函数中至多只有一个处于可执行状态(因为至多只有一个线程可以获取该对象的锁),从而避免了类成员变量的访问冲突。

synchronized方式的缺点:
由于synchronized锁定的是调用这个同步方法的对象,也就是说,当一个线程P1在不同的线程中执行这个方法时,它们之间会形成互斥,从而达到同步的效果。但这里需要注意的是,这个对象所性的Class的另一个对象却可以任意调用这个被加了synchronized关键字的方法。同步方法的实质是将synchronized作用于object reference,对于拿到了P1对象锁的线程才可以调用这个synchronized方法,而对于P2来说,P1与它毫不相干,程序也可能在这种情况下摆脱同步机制的控制,造成数据混乱。以下我们将对这种情况进行详细地说明:
首先我们先介绍synchronized关键字的两种加锁对象:对象和类——synchronized可以为资源加对象锁或是类锁,类锁对这个类的所有对象(实例)均起作用,而对象锁只是针对该类的一个指定的对象加锁,这个类的其它对象仍然可以使用已经对前一个对象加锁的synchronized方法。
在这里我们主要讨论的一个问题就是:“同一个类,不同实例调用同一个方法,会产生同步问题吗?”
同步问题只和资源有关系,要看这个资源是不是静态的。同一个静态数据,你相同函数分属不同线程同时对其进行读写,CPU也不会产生错误,它会保证你代码的执行逻辑,而这个逻辑是否是你想要的,那就要看你需要什么样的同步了。即便你两个不同的代码,在CPU的不同的两个core里跑,同时写一个内存地址,Cache机制也会在L2里先锁定一个。然后更新,再share给另一个core,也不会出错,不然intel,amd就白养那么多人了。
因此,只要你没有两个代码共享的同一个资源或变量,就不会出现数据不一致的情况。而且同一个类的不同对象的调用有完全不同的堆栈,它们之间完全不相干。
以下我们以一个售票过程举例说明,在这里,我们的共享资源就是票的剩余张数。
package com.test;
 
public  class ThreadSafeTest  extends Thread  implements Runnable {
    
   private  static  int  num = 1;
 
     public ThreadSafeTest(String name) {
        setName(name);
    }
 
     public  void run() {
        sell(getName());     
    }
   
     private  synchronized  void sell(String name){
         if ( num > 0) {
            System.  out.println(name + ": 检测票数大于0" );
            System.  out.println(name + ": \t正在收款(大约5秒完成)。。。" );
             try {
                Thread.  sleep(5000);
                System.  out.println(name + ": \t打印票据,售票完成" );
                 num--;
                 printNumInfo();
            }  catch (InterruptedException e) {
                e.printStackTrace();
            }
        }  else {
            System.  out.println(name+": 没有票了,停止售票" );
        }
    }
    
   private  static  void printNumInfo() {
 
        System.  out.println("系统:当前票数:" +  num);
         if ( num < 0) {
            System.  out.println("警告:票数低于0,出现负数" );
        }
    }
 
     public  static  void main(String args[]) {
         try {
             new ThreadSafeTest("售票员李XX" ).start();
            Thread.  sleep(2000);
             new ThreadSafeTest("售票员王X" ).start();
           
        }  catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}
运行上述代码,我们得到的输出是:
售票员李XX: 检测票数大于0
售票员李XX:       正在收款(大约5秒完成)。。。
售票员王X: 检测票数大于0
售票员王X:  正在收款(大约5秒完成)。。。
售票员李XX:       打印票据,售票完成
系统:当前票数:0
售票员王X:  打印票据,售票完成
系统:当前票数:-1
警告:票数低于0,出现负数
根据输出结果,我们可以发现,剩余票数为-1,出现了同步错误的问题。之所以出现这种情况的原因是,我们建立的两个实例对象,对共享的静态资源 static  int  num = 1同时进行了修改。那么我们将上面代码中方框内的修饰词static去掉,然后再运行程序,可以得到:
售票员李XX: 检测票数大于0
售票员李XX:       正在收款(大约5秒完成)。。。
售票员王X: 检测票数大于0
售票员王X:  正在收款(大约5秒完成)。。。
售票员李XX:       打印票据,售票完成
系统:当前票数:0
售票员王X:  打印票据,售票完成
系统:当前票数:0
对程度修改之后,程序运行貌似没有问题了,每个对象拥有各自不同的堆栈,分别独立运行。但这样却违背了我们希望多线程同时对共享资源的处理(去static后,num就从共享资源变成了每个实例各自拥有的成员变量),这显然不是我们想要的。
在以上两种代码中,采取的主要是对对象的锁定。由于我之前谈到的原因,当一个类的两个不同的实例对同一共享资源进行修改时,CPU为了保证程序的逻辑会默认这种做法,至于是不是想要的结果,这个只能由程序员自己来决定。因此,我们需要改变锁的作用范围,若作用对象只是实例,那么这种问题是无法避免的;只有当锁的作用范围是整个类的时候,才可能排除同一个类的不同实例对共享资源同时修改的问题。
package com.test;
 
public  class ThreadSafeTest  extends Thread  implements Runnable {
     private  static  int  num = 1;
 
     public ThreadSafeTest(String name) {
        setName(name);
    }
 
     public  void run() {
         sell(getName());     
    }   
    
  private  synchronized  static  void sell(String name){
 
         if ( num > 0) {
            System.  out.println(name + ": 检测票数大于0" );
            System.  out.println(name + ": \t正在收款(大约5秒完成)。。。" );
             try {
                Thread.  sleep(5000);
                System.  out.println(name + ": \t打印票据,售票完成" );
                 num--;
                 printNumInfo();
            }  catch (InterruptedException e) {
                e.printStackTrace();
            }
        }  else {
            System.  out.println(name+": 没有票了,停止售票" );
        }
    }
 
     private  static  void printNumInfo() {
        System.  out.println("系统:当前票数:" +  num);
         if ( num < 0) {
            System.  out.println("警告:票数低于0,出现负数" );
        }
    }
 
     public  static  void main(String args[]) {
         try {
             new ThreadSafeTest("售票员李XX" ).start();
            Thread.  sleep(2000);
             new ThreadSafeTest("售票员王X" ).start();
           
        }  catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}
将程序做如上修改,可以得到运行结果:
售票员李XX: 检测票数大于0
售票员李XX:       正在收款(大约5秒完成)。。。
售票员李XX:       打印票据,售票完成
系统:当前票数:0
售票员王X: 没有票了,停止售票
对sell()方法加上了static修饰符,这样就将锁的作用对象变成了类,当该类的一个实例对共享变量进行操作时将会阻塞这个类的其它实例对其的操作。从而得到我们如期想要的结果。
总结:
1,synchronized关键字有两种用法:synchronized方法和synchronized块。
2,在Java中不单是类实例,每一个类也可以对应一把锁

在使用synchronized关键字时,有以下几点儿需要注意:
1,synchronized关键字不能被继承。虽然可以用synchronized来定义方法,但是synchronized却并不属于方法定义的一部分,所以synchronized关键字并不能被继承。如果父类中的某个方法使用了synchronized关键字,而子类中也覆盖了这个方法,默认情况下子类中的这个方法并不是同步的,必须显示的在子类的这个方法中加上synchronized关键字才可。当然,也可以在子类中调用父类中相应的方法,这样虽然子类中的方法并不是同步的,但子类调用了父类中的同步方法,也就相当子类方法也同步了。如,
在子类中加synchronized关键字:
class Parent { 
     public  synchronized  void method() {   } 
class Child  extends Parent { 
     public  synchronized  void method () {   } 
}
调用父类方法:
class Parent { 
     public  synchronized  void method() {   } 
class Child  extends Parent { 
     public  void method() {  super.method();   } 
}
2,在接口方法定义时不能使用synchronized关键字。
3,构造方法不能使用synchronized关键字,但可以使用synchronized块来进行同步。
4,synchronized位置可以自由放置,但是不能放置在方法的返回类型后面。
5,synchronized关键字不可以用来同步变量,如下面代码是错误的:
public  synchronized  int n = 0; 
public  static  synchronized  int n = 0;
6,虽然使用synchronized关键字是最安全的同步方法,但若是大量使用也会造成不必要的资源消耗以及性能损失。从表面上看synchronized锁定的是一个方法,但实际上锁定的却是一个类,比如,对于两个非静态方法method1()和method2()都使用了synchronized关键字,在执行其中的一个方法时,另一个方法是不能执行的。静态方法和非静态方法情况类似。但是静态方法和非静态方法之间不会相互影响,见如下代码:
public  class MyThread1  extends Thread { 
     public String methodName ; 
 
     public  static  void method(String s) { 
        System.  out .println(s); 
         while ( true ); 
    } 
     public  synchronized  void method1() { 
         method( "非静态的method1方法" ); 
    } 
     public  synchronized  void method2() { 
         method( "非静态的method2方法" ); 
    } 
     public  static  synchronized  void method3() { 
         method( "静态的method3方法" ); 
    } 
     public  static  synchronized  void method4() { 
         method( "静态的method4方法" ); 
    } 
     public  void run() { 
         try { 
            getClass().getMethod( methodName ).invoke(  this); 
        } 
         catch (Exception e) { 
        } 
    } 
     public  static  void main(String[] args)  throws Exception { 
        MyThread1 myThread1 =  new MyThread1(); 
         for ( int i = 1; i <= 4; i++) { 
            myThread1. methodName = "method" + String. valueOf (i); 
             new Thread(myThread1).start(); 
             sleep(100); 
        } 
    } 
}
运行结果为:
非静态的method1方法
静态的method3方法
从上面的运行结果可以看出,method2和method4在method1和method3运行完之前是不会运行的。因此,可以得出一个结论,如查在类中使用synchronized来定义非静态方法,那么将影响这个类中的所有synchronized定义的非静态方法;如果定义的静态方法,那么将影响这个类中所有以synchronized定义的静态方法。这有点儿像数据表中的表锁,当修改一条记录时,系统就将整个表都锁住了。因此,大量使用这种同步方法会使程序的性能大幅度地下降。

对共享资源的同步访问更加安全的技巧:
1,定义private的instance变量+它的get方法,而不要定义public/protected的instance变量。如果将变量定义为public,对象可以在外界绕过同步方法的控制而直接取得它,并且改动它。这也是JavaBean的标准实现之一。
2,如果instance变量是一个对象,如数组或ArrayList等,那上述方法仍然不安全,因为当外界通过get方法拿到这个instance对象的引用后,又将其指向另一个对象,那么这个private变量也就变了,岂不是很危险。这个时候就需要将get方法也加上synchronized同步,并且只返回这个private对象的clone()。这样,调用端得到的就只是对象副本的一个引用了。
 
 
 
 
参考资料:

 



已有 0 人发表留言,猛击->> 这里<<-参与讨论


ITeye推荐



相关 [java 线程 同步] 推荐:

Java多线程(二)同步

- - CSDN博客编程语言推荐文章
如果你的java基础较弱,或者不大了解java多线程请先看这篇文章 java多线程(一)线程定义、状态和属性. 同步一直是java多线程的难点,在我们做android开发时也很少应用,但这并不是我们不熟悉同步的理由. 希望这篇文章能使更多的人能够了解并且应用java的同步. 在多线程的应用中,两个或者两个以上的线程需要共享对同一个数据的存取.

(三)java线程:线程同步ReentrantLock,condition(await,signal)

- - ITeye博客
(三)线程同步ReentrantLock,condition(await,signal). 一.synchronized和 ReentrantLock 故事. 的线程同步(synchronized ,wait,notify. )讲了synchronized的同步方法,java 就是这么神奇,这里又有一个同步的方法.

Java线程同步中关键字synchronized详述

- - 编程语言 - ITeye博客
synchronized关键可以修饰函数、函数内语句. 无论它加上方法还是对象上,它取得的锁都是对象,而不是把一段代码或是函数当作锁. 1,当两个并发线程访问同一个对象object中的这个synchronized(this)同步代码块时,一段时间只能有一个线程得到执行,而另一个线程只有等当前线程执行完以后才能执行这块代码.

Java 多线程同步的五种方法

- - 编程语言 - ITeye博客
4、wait() 与 notify(). 前几天面试,被大师虐残了,好多基础知识必须得重新拿起来啊. 因为当我们有多个线程要同时访问一个变量或对象时,如果这些线程中既有读又有写操作时,就会导致变量值或对象的状态出现混乱,从而导致程序异常. 举 个例子,如果一个银行账户同时被两个线程操作,一个取100块,一个存钱100块.

Java线程池

- - 企业架构 - ITeye博客
线程的使用在java中占有极其重要的地位,在jdk1.4极其之前的jdk版本中,关于线程池的使用是极其简陋的. 在jdk1.5之后这一情况有了很大的改观. Jdk1.5之后加入了java.util.concurrent包,这个包中主要介绍java中线程以及线程池的使用. 为我们在开发中处理线程的问题提供了非常大的帮助.

Java 线程池

- - 编程语言 - ITeye博客
在项目中,系统启动一个新线程的成本是比较高的,因为它涉及与操作系统交互. 在这种情形下,使用线程池可以很好地提高性能,尤其是当程序中需要创建大量生存周期很短的线程时,更应该考虑使用线程池. 使用线程池可以有效地控制系统中并发线程的数量,当系统中包含大量并发线程时,会导致系统性能剧烈下降,甚至导致JVM崩溃,而线程池的最大线程数参数可以控制系统中并发线程数不超过此数.

Java同步块(Synchronized Blocks)

- - 并发编程网 - ifeve.com
原文链接  作者:Jakob Jenkov  译者:李同杰. Java 同步块(synchronized block)用来标记方法或者代码块是同步的. Java同步关键字(synchronzied). Java 同步关键字( synchronized ). Java中的同步块用synchronized标记.

Java Thread多线程

- - CSDN博客推荐文章
Java Thread多线程. Java 多线程例子1 小例子. super("zhuyong");//设置线程的名字,默认为“TestThread”. Java 多线程例子2 前台线程(用户线程) 后台线程(守护线程 ). 1,setDaemon(true)后就是后台线程(守护线程 ),反之就是前台线程(用户线程).

Java线程之FutureTask

- - zzm
FutureTask是Future和Callable的结合体. 然后通过Future来取得计算结果. 但是,若开启了多个任务,我们无从知晓哪个任务最先结束,因此,若要实现“当某任务结束时,立刻做一些事情,例如记录日志”这一功能,就需要写一些额外的代码. FutureTask正是为此而存在,他有一个回调函数protected void done(),当任务结束时,该回调函数会被触发.

[Java] Java 多线程案例分析

- - V2EX
现要从 hbase中导出 2016 年整年的,大约 10w只股票行情数据,数据总量约 100t. 汇总到 hdfs中供需求方使用. 已知数据量规模大概是 100t,那么单台机器处理肯定不是不行的,先不说大多数磁盘都没这么大,即便磁盘有这么大,单台机器处理对于内存和 cpu 要求也很高,所以我们将问题一般化,使用数量有限的低配机器.