Java 对象锁和类锁 死锁(多线程synchronized关键字)

来源:互联网 发布:中信证券开户交易软件 编辑:程序博客网 时间:2024/06/12 01:02

本文主要是将synchronized关键字用法作为例子来去解释Java中的对象锁 和 类锁。

对于同步,要时刻清醒在 哪个锁对象 上同步,这是关键。
对于同步代码块,要看清楚什么对象已经用于锁定(synchronized后面括号的内容)。在同一个对象上进行同步的线程将彼此阻塞,在不同对象上锁定的线程将永远不会彼此阻塞。

一、synchronized关键字

synchronized关键字有如下两种用法:

1、 在需要同步的方法的方法签名中加入synchronized关键字。

synchronized public void getValue() {    System.out.println("getValue method thread name="            + Thread.currentThread().getName() + " username=" + username            + " password=" + password);}

上面的代码修饰的synchronized是非静态方法,如果修饰的是静态方法(static)含义是完全不一样的。具体不一样在哪里,后面会详细说清楚。

synchronized static public void getValue() {    System.out.println("getValue method thread name="            + Thread.currentThread().getName() + " username=" + username            + " password=" + password);}

2、使用synchronized块对需要进行同步的代码段进行同步。

public void serviceMethod() {    try {        synchronized (this) {            System.out.println("begin time=" + System.currentTimeMillis());            Thread.sleep(2000);            System.out.println("end    end=" + System.currentTimeMillis());        }    } catch (InterruptedException e) {        e.printStackTrace();    }}

上面的代码块是synchronized (this)用法,还有synchronized (非this对象)以及synchronized (类.class)这两种用法,这些使用方式的含义也是有根本的区别的。我们先带着这些问题继续往下看。

二、Java中的对象锁和类锁

之前网上有找一些相关资料,有篇博客是这样描述的(看的是转载的,原创连接我也不知道):

1.一段synchronized的代码被一个线程执行之前,他要先拿到执行这段代码的权限,2.在Java里边就是拿到某个同步对象的锁(一个对象只有一把锁);3.如果这个时候同步对象的锁被其他线程拿走了,他(这个线程)就只能等了(线程阻塞在锁池等待队列中)。 4.取到锁后,他就开始执行同步代码(被synchronized修饰的代码);5.线程执行完同步代码后马上就把锁还给同步对象,其他在锁池中等待的某个线程就可以拿到锁执行同步代码了。6.这样就保证了同步代码在同一时刻只有一个线程在执行。

这段话,除了最后一句,讲得都是挺合理的。”这样就保证了同步代码在统一时刻只有一个线程在执行。”这句话显然不对,synchronized并非保证同步代码同一时刻只有一个线程执行,同步代码同一时刻应该可以有多个线程执行。

上面提到锁,这里先引出锁的概念。先来看看下面这些啰嗦而必不可少的文字。

多线程的线程同步机制实际上是靠锁的概念来控制的。

在Java程序运行时环境中,JVM需要对两类线程共享的数据进行协调:
1)保存在堆中的实例变量
2)保存在方法区中的类变量

这两类数据是被所有线程共享的。
(程序不需要协调保存在Java 栈当中的数据。因为这些数据是属于拥有该栈的线程所私有的。)

这里插播一下广告:关于JVM内存,如果想了解可以看看博主的另外一篇文章:

Java内存管理:http://blog.csdn.net/u013142781/article/details/50830754

方法区(Method Area)与Java堆一样,是各个线程共享的内存区域,它用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。虽然Java虚拟机规范把方法区描述为堆的一个逻辑部分,但是它却有一个别名叫做Non-Heap(非堆),目的应该是与Java堆区分开来。

:在Java中,JVM中的栈记录了线程的方法调用。每个线程拥有一个栈。在某个线程的运行过程中,如果有新的方法调用,那么该线程对应的栈就会增加一个存储单元,即帧(frame)。在frame中,保存有该方法调用的参数、局部变量和返回地址。

是JVM中一块可自由分配给对象的区域。当我们谈论垃圾回收(garbage collection)时,我们主要回收堆(heap)的空间。
Java的普通对象存活在堆中。与栈不同,堆的空间不会随着方法调用结束而清空。因此,在某个方法中创建的对象,可以在方法调用结束之后,继续存在于堆中。这带来的一个问题是,如果我们不断的创建新的对象,内存空间将最终消耗殆尽。

在java虚拟机中,每个对象和类在逻辑上都是和一个监视器相关联的。
对于对象来说,相关联的监视器保护对象的实例变量。

对于类来说,监视器保护类的类变量。

(如果一个对象没有实例变量,或者一个类没有变量,相关联的监视器就什么也不监视。)
为了实现监视器的排他性监视能力,java虚拟机为每一个对象和类都关联一个锁。代表任何时候只允许一个线程拥有的特权。线程访问实例变量或者类变量不需锁。

但是如果线程获取了锁,那么在它释放这个锁之前,就没有其他线程可以获取同样数据的锁了。(锁住一个对象就是获取对象相关联的监视器)

类锁实际上用对象锁来实现。当虚拟机装载一个class文件的时候,它就会创建一个java.lang.Class类的实例。当锁住一个对象的时候,实际上锁住的是那个类的Class对象。

一个线程可以多次对同一个对象上锁。对于每一个对象,java虚拟机维护一个加锁计数器,线程每获得一次该对象,计数器就加1,每释放一次,计数器就减 1,当计数器值为0时,锁就被完全释放了。

java编程人员不需要自己动手加锁,对象锁是java虚拟机内部使用的。

在java程序中,只需要使用synchronized块或者synchronized方法就可以标志一个监视区域。当每次进入一个监视区域时,java 虚拟机都会自动锁上对象或者类。

三、synchronized关键字各种用法与实例

看完了”二、Java中的对象锁和类锁”,我们再来结合”一、synchronized关键字”里面提到的synchronized用法。

事实上,synchronized修饰非静态方法、同步代码块的synchronized (this)用法和synchronized (非this对象)的用法锁的是对象,线程想要执行对应同步代码,需要获得对象锁。

synchronized修饰静态方法以及同步代码块的synchronized (类.class)用法锁的是类,线程想要执行对应同步代码,需要获得类锁。

因此,事实上synchronized关键字可以细分为上面描述的五种用法。

本文的实例均来自于《Java多线程编程核心技术》这本书里面的例子。

1、我们先看看非线程安全实例(Run.java):

public class Run {    public static void main(String[] args) {        HasSelfPrivateNum numRef = new HasSelfPrivateNum();        ThreadA athread = new ThreadA(numRef);        athread.start();        ThreadB bthread = new ThreadB(numRef);        bthread.start();    }}class HasSelfPrivateNum {    private int num = 0;    public void addI(String username) {        try {            if (username.equals("a")) {                num = 100;                System.out.println("a set over!");                Thread.sleep(2000);            } else {                num = 200;                System.out.println("b set over!");            }            System.out.println(username + " num=" + num);        } catch (InterruptedException e) {            // TODO Auto-generated catch block            e.printStackTrace();        }    }}class ThreadA extends Thread {    private HasSelfPrivateNum numRef;    public ThreadA(HasSelfPrivateNum numRef) {        super();        this.numRef = numRef;    }    @Override    public void run() {        super.run();        numRef.addI("a");    }} class ThreadB extends Thread {    private HasSelfPrivateNum numRef;    public ThreadB(HasSelfPrivateNum numRef) {        super();        this.numRef = numRef;    }    @Override    public void run() {        super.run();        numRef.addI("b");    }}

运行结果为:

a set over!b set over!b num=200a num=200

修改HasSelfPrivateNum如下,方法用synchronized修饰如下:

class HasSelfPrivateNum {    private int num = 0;    synchronized public void addI(String username) {        try {            if (username.equals("a")) {                num = 100;                System.out.println("a set over!");                Thread.sleep(2000);            } else {                num = 200;                System.out.println("b set over!");            }            System.out.println(username + " num=" + num);        } catch (InterruptedException e) {            // TODO Auto-generated catch block            e.printStackTrace();        }    }}

运行结果是线程安全的:

b set over!b num=200a set over!a num=100

实验结论:两个线程访问同一个对象中的同步方法是一定是线程安全的。本实现由于是同步访问,所以先打印出a,然后打印出b

这里线程获取的是HasSelfPrivateNum的对象实例的锁——对象锁。

2、多个对象多个锁

就上面的实例,我们将Run改成如下:

public class Run {    public static void main(String[] args) {        HasSelfPrivateNum numRef1 = new HasSelfPrivateNum();        HasSelfPrivateNum numRef2 = new HasSelfPrivateNum();        ThreadA athread = new ThreadA(numRef1);        athread.start();        ThreadB bthread = new ThreadB(numRef2);        bthread.start();    }}

运行结果为:

a set over!b set over!b num=200a num=100

这里是非同步的,因为线程athread获得是numRef1的对象锁,而bthread线程获取的是numRef2的对象锁,他们并没有在获取锁上有竞争关系,因此,出现非同步的结果

这里插播一下:同步不具有继承性

3、同步块synchronized (this)

我们先看看代码实例(Run.java)

public class Run {    public static void main(String[] args) {        ObjectService service = new ObjectService();        ThreadA a = new ThreadA(service);        a.setName("a");        a.start();        ThreadB b = new ThreadB(service);        b.setName("b");        b.start();    }}class ObjectService {    public void serviceMethod() {        try {            synchronized (this) {                System.out.println("begin time=" + System.currentTimeMillis());                Thread.sleep(2000);                System.out.println("end    end=" + System.currentTimeMillis());            }        } catch (InterruptedException e) {            e.printStackTrace();        }    }}class ThreadA extends Thread {    private ObjectService service;    public ThreadA(ObjectService service) {        super();        this.service = service;    }    @Override    public void run() {        super.run();        service.serviceMethod();    }}class ThreadB extends Thread {    private ObjectService service;    public ThreadB(ObjectService service) {        super();        this.service = service;    }    @Override    public void run() {        super.run();        service.serviceMethod();    }}

运行结果:

begin time=1466148260341end    end=1466148262342begin time=1466148262342end    end=1466148264378

这样也是同步的,线程获取的是同步块synchronized (this)括号()里面的对象实例的对象锁,这里就是ObjectService实例对象的对象锁了。

需要注意的是synchronized (){}的{}前后的代码依旧是异步的

4、synchronized (非this对象)

我们先看看代码实例(Run.java)

public class Run {    public static void main(String[] args) {        Service service = new Service("xiaobaoge");        ThreadA a = new ThreadA(service);        a.setName("A");        a.start();        ThreadB b = new ThreadB(service);        b.setName("B");        b.start();    }}class Service {    String anyString = new String();    public Service(String anyString){        this.anyString = anyString;    }    public void setUsernamePassword(String username, String password) {        try {            synchronized (anyString) {                System.out.println("线程名称为:" + Thread.currentThread().getName()                        + "在" + System.currentTimeMillis() + "进入同步块");                Thread.sleep(3000);                System.out.println("线程名称为:" + Thread.currentThread().getName()                        + "在" + System.currentTimeMillis() + "离开同步块");            }        } catch (InterruptedException e) {            // TODO Auto-generated catch block            e.printStackTrace();        }    }}class ThreadA extends Thread {    private Service service;    public ThreadA(Service service) {        super();        this.service = service;    }    @Override    public void run() {        service.setUsernamePassword("a", "aa");    }}class ThreadB extends Thread {    private Service service;    public ThreadB(Service service) {        super();        this.service = service;    }    @Override    public void run() {        service.setUsernamePassword("b", "bb");    }}

不难看出,这里线程争夺的是anyString的对象锁,两个线程有竞争同一对象锁的关系,出现同步

现在有一个问题:一个类里面有两个线程要访问 同一个 非静态同步方法,会有影响么?

答案是:如果对象实例A,线程1获得了对象A的对象锁,那么其他线程就不能进入需要获得对象实例A的对象锁才能访问的同步代码(包括同步方法和同步块)。不理解可以细细品味一下!

5、静态synchronized同步方法

我们直接看代码实例:

public class Run {    public static void main(String[] args) {        ThreadA a = new ThreadA();        a.setName("A");        a.start();        ThreadB b = new ThreadB();        b.setName("B");        b.start();    }}class Service {    synchronized public static void printA() {        try {            System.out.println("线程名称为:" + Thread.currentThread().getName()                    + "在" + System.currentTimeMillis() + "进入printA");            Thread.sleep(3000);            System.out.println("线程名称为:" + Thread.currentThread().getName()                    + "在" + System.currentTimeMillis() + "离开printA");        } catch (InterruptedException e) {            e.printStackTrace();        }    }    synchronized public static void printB() {        System.out.println("线程名称为:" + Thread.currentThread().getName() + "在"                + System.currentTimeMillis() + "进入printB");        System.out.println("线程名称为:" + Thread.currentThread().getName() + "在"                + System.currentTimeMillis() + "离开printB");    }}class ThreadA extends Thread {    @Override    public void run() {        Service.printA();    }}class ThreadB extends Thread {    @Override    public void run() {        Service.printB();    }}

运行结果:

线程名称为:A1466149372909进入printA线程名称为:A1466149375920离开printA线程名称为:B在1466149375920进入printB线程名称为:B在1466149375920离开printB

两个线程在争夺同一个类锁,因此同步

6、synchronized (class)

对上面Service类代码修改成如下:

class Service {    public static void printA() {        synchronized (Service.class) {            try {                System.out.println("线程名称为:" + Thread.currentThread().getName()                        + "在" + System.currentTimeMillis() + "进入printA");                Thread.sleep(3000);                System.out.println("线程名称为:" + Thread.currentThread().getName()                        + "在" + System.currentTimeMillis() + "离开printA");            } catch (InterruptedException e) {                e.printStackTrace();            }        }    }    public static void printB() {        synchronized (Service.class) {            System.out.println("线程名称为:" + Thread.currentThread().getName()                    + "在" + System.currentTimeMillis() + "进入printB");            System.out.println("线程名称为:" + Thread.currentThread().getName()                    + "在" + System.currentTimeMillis() + "离开printB");        }    }}

运行结果:

线程名称为:A1466149372909进入printA线程名称为:A1466149375920离开printA线程名称为:B在1466149375920进入printB线程名称为:B在1466149375920离开printB

两个线程依旧在争夺同一个类锁,因此同步

补充概念(反射)1.类名.class  是来自 反射的概念,类名.class是获得这个类所对应的Class实例。2.从面向对象的角度上来看,类也是对象,它们是类这个类对象,听起来有些抽象,但是在java中的实现就是所有的加载进来的类在虚拟机中都是一个java.lang.Class类的对象,而“类名.class”就是获得这个类的对象(在同一个ClassLoader中,类对象都是单例的)3.Class是对某个类的描述4.当虚拟机载入某个class文件时,首先生成该class文件对应的类的Class对象,即 对象描述类*******即可以理解为:类名.class 是获得这个类的 类描述对象(一个类对应只有一个类描述对象【对象里面是对类的描述】)

需要特别说明:对于同一个类A,线程1争夺A对象实例的对象锁,线程2争夺类A的类锁,这两者不存在竞争关系。也就说对象锁和类锁互不干预内政

静态方法则一定会同步,非静态方法需在单例模式才生效,但是也不能都用静态同步方法,总之用得不好可能会给性能带来极大的影响。另外,有必要说一下的是spring的bean默认是单例的。

线程死锁

死锁对Java程序来说,是很复杂的,也很难发现问题。当两个线程被阻塞,每个线程在等待另一个线程时就发生死锁。

还是看一个比较直观的死锁例子:

public class DeadlockRisk {     private static class Resource {         public int value;     }    private Resource resourceA = new Resource();     private Resource resourceB = new Resource();    public int read() {         synchronized (resourceA) {             synchronized (resourceB) {                 return resourceB.value + resourceA.value;             }         }     }   public void write(int a, int b) {         synchronized (resourceB) {             synchronized (resourceA) {                 resourceA.value = a;                 resourceB.value = b;             }         }     } }

假设read()方法由一个线程启动,write()方法由另外一个线程启动。读线程将拥有resourceA锁,写线程将拥有resourceB锁,两者都坚持等待的话就出现死锁。

实际上,上面这个例子发生死锁的概率很小。因为在代码内的某个点,CPU必须从读线程切换到写线程,所以,死锁基本上不能发生。

但是,无论代码中发生死锁的概率有多小,一旦发生死锁,程序就死掉。有一些设计方法能帮助避免死锁,包括始终按照预定义的顺序获取锁这一策略。

2 0
原创粉丝点击