文档章节

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

文文1
 文文1
发布于 2017/09/12 11:10
字数 3406
阅读 10
收藏 2
点赞 0
评论 0

 

最近工作有用到一些多线程的东西,之前吧,有用到synchronized同步块,不过是别人怎么用就跟着用,并没有搞清楚锁的概念。最近也是遇到一些问题,不搞清楚锁的概念,很容易碰壁,甚至有些时候自己连用没用对都不知道。

今天把一些疑惑都解开了,写篇文章分享给大家,文章还算比较全面。当然可能有小宝鸽理解得不够深入透彻的地方,如果说得不正确还望指出。

看之前有必要跟某些猿友说一下,如果看一遍没有看明白呢,也没关系,当是了解一下,等真正使用到了,再回头看。

本文主要是将synchronized关键字用法作为例子来去解释Java中的对象锁和类锁。特别的是希望能帮大家理清一些概念。

一、synchronized关键字

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

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

synchronized 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中的对象锁和类锁

小宝鸽似乎并没有办法用清晰简短的语言来描述对象锁和类锁的概念。即便能用简单的语句概况,也会显得抽象。猿友们耐心看完自然会明白。

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

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

这段话,除了最后一句,讲得都是挺合理的。”这样就保证了同步代码在统一时刻只有一个线程在执行。”这句话显然不对,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=200
a 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();
        }
    }
 
}

运行结果是线程安全的:

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

实验结论:两个线程访问同一个对象中的同步方法是一定是线程安全的。本实现由于是同步访问,所以先打印出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=200
a 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=1505185464032
end    end=1505185466033
begin time=1505185466033
end    end=1505185468041

这样也是同步的,线程获取的是同步块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");
 
    }
 
}

运行结果:

线程名称为:A在1505185616094进入同步块
线程名称为:A在1505185619095离开同步块
线程名称为:B在1505185619095进入同步块
线程名称为:B在1505185622099离开同步块

不难看出,这里线程争夺的是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();
    }
}

运行结果:

线程名称为:A在1505189301378进入printA
线程名称为:A在1505189304379离开printA
线程名称为:B在1505189304379进入printB
线程名称为:B在1505189304380离开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");
        }
    }
}

运行结果:

线程名称为:A在1505189420376进入printA
线程名称为:A在1505189423377离开printA
线程名称为:B在1505189423378进入printB
线程名称为:B在1505189423378离开printB

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

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

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

 

 


 

© 著作权归作者所有

共有 人打赏支持
文文1
粉丝 19
博文 328
码字总数 113821
作品 0
长沙
程序员
Java多线程学习(二)synchronized关键字(2)

系列文章传送门: Java多线程学习(一)Java多线程入门 Java多线程学习(二)synchronized关键字(1) java多线程学习(二)synchronized关键字(2) Java多线程学习(三)volatile关键字 Ja...

一只蜗牛呀 ⋅ 04/16 ⋅ 0

Java多线程学习(四)等待/通知(wait/notify)机制

系列文章传送门: Java多线程学习(一)Java多线程入门 Java多线程学习(二)synchronized关键字(1) java多线程学习(二)synchronized关键字(2) Java多线程学习(三)volatile关键字 Ja...

一只蜗牛呀 ⋅ 04/16 ⋅ 0

Java 编程之美:并发编程基础晋级篇

本文来自作者 加多 在 GitChat 上分享 「Java 并发编程之美:并发编程基础晋级篇」 编辑 | Mc Jin 借用 Java 并发编程实践中的话,编写正确的程序并不容易,而编写正常的并发程序就更难了! ...

gitchat ⋅ 04/18 ⋅ 0

Java多线程学习(五)线程间通信知识点补充

系列文章传送门: Java多线程学习(一)Java多线程入门 Java多线程学习(二)synchronized关键字(1) java多线程学习(二)synchronized关键字(2) Java多线程学习(三)volatile关键字 Ja...

一只蜗牛呀 ⋅ 04/16 ⋅ 0

Java并发--synchronized实现原理及锁优化

注:本文中的部分内容摘抄自他人博客,如有侵权,请联系我,侵删~ 本篇博客主要讲述 synchronized 关键字的实现原理以及 JDK 1.6 后对 synchronized 的种种优化。synchronized 的使用不再赘述...

championhengyi ⋅ 04/27 ⋅ 0

悲观的并发策略——synchronized互斥锁

互斥锁是最常见的同步手段,在并发过程中,当多条线程对同一个共享数据竞争时,它保证共享数据同一时刻只能被一条线程使用,其他线程只有等到锁释放后才能重新进行竞争。 对于Java开发人员,...

wangyangzhizhou ⋅ 04/16 ⋅ 0

Java并发基础你需要知道的基础知识

多线程和并发编程是Java里面的核心内容,通常有以下一些概念需要重点掌握。 线程; 锁; 同步器; 并发容器和框架; Java并发工具类; 原子操作类; Executor框架(执行机制); 并发基础概念 ...

异步社区 ⋅ 05/30 ⋅ 0

21、Java并发性和多线程-Java中的锁

以下内容转自http://ifeve.com/locks/: 锁像synchronized同步块一样,是一种线程同步机制,但比Java中的synchronized同步块更复杂。因为锁(以及其它更高级的线程同步机制)是由synchronize...

easonjim ⋅ 2017/06/16 ⋅ 0

深入浅出Java多线程,看着篇就够了

Java给多线程编程提供了内置的支持。一个多线程程序包含两个或多个能并发运行的部分。程序的每一部分都称作一个线程,并且每个线程定义了一个独立的执行路径。 多线程是多任务的一种特别的形...

java高级架构牛人 ⋅ 05/22 ⋅ 0

Java 多线程编程 — 锁优化2

Java多线程编程-(13)- 关于锁优化的几点建议 一、背景 在《 Java多线程编程-(11)-从volatile和synchronized的底层实现原理看Java虚拟机对锁优化所做的努力》 这一篇文章中,我们大致介绍...

晨猫 ⋅ 04/26 ⋅ 0

没有更多内容

加载失败,请刷新页面

加载更多

下一页

1.01-StringUtils的使用

import org.apache.commons.lang.StringUtils; 一、StringUtils 常用的方法: 1. 判断某一字符串是否为空 , 为空的标准是 str==null 或 str.length()==0 StringUtils.isEmpty(null) ==>tr......

静以修身2025 ⋅ 21分钟前 ⋅ 0

几道Spring 面试题

1、BeanFactory 接口和 ApplicationContext 接口有什么区别? ApplicationContext 接口继承BeanFactory接口 Spring核心工厂是BeanFactory BeanFactory采取延迟加载,第一次getBean时才会初始...

职业搬砖20年 ⋅ 23分钟前 ⋅ 0

包饺子

http://storage.slide.news.sina.com.cn/slidenews/77_ori/2018_24/74766_826131_625489.gif

霜叶情 ⋅ 24分钟前 ⋅ 0

兑吧:从自建HBase迁移到阿里云HBase实战经验

摘要: 业务介绍 兑吧集团包含兑吧网络和推啊网络,兑吧网络是一家致力于帮助互联网企业提升运营效率的用户运营服务平台,提供积分商城和媒体运营服务。推啊网络是一家互动式广告平台,经过多...

猫耳m ⋅ 35分钟前 ⋅ 0

xml解析

方法一: String s_xml1 = "<xml>" + "<head>lalalalal</head>" + "<body>1234</body>" + "</xml>"; try { DocumentBuilderFactory documentBuilderFactory......

GithubXD ⋅ 36分钟前 ⋅ 0

reuse stream

Although Java streams were designed to be operated only once, programmers still ask how to reuse a stream. From a simple web search, we can find many posts with this same issue ......

idoz ⋅ 36分钟前 ⋅ 0

兑吧:从自建HBase迁移到阿里云HBase实战经验

摘要: 业务介绍 兑吧集团包含兑吧网络和推啊网络,兑吧网络是一家致力于帮助互联网企业提升运营效率的用户运营服务平台,提供积分商城和媒体运营服务。推啊网络是一家互动式广告平台,经过多...

阿里云云栖社区 ⋅ 38分钟前 ⋅ 0

从世界杯看国内运动体育社交新能量

2018年世界杯已正式拉开帷幕,一场全世界球迷的狂欢也正式开始。 世界杯影响力:30亿+球迷的狂欢+社交话题 世界杯这个超级IP和对社交网络的引爆让更多的人目光聚焦到国内运动体育社交这个层...

ThinkSNS账号 ⋅ 39分钟前 ⋅ 0

不固定值替换

<?php$arr = 20;$data = str_replace(array(10,20,30,40),array("blue","red","green","yellow"),$arr);print_r($data);...

nsns ⋅ 40分钟前 ⋅ 0

Job for nginx.service failed 错误解决方案

今天刚在centos7上安装了nginx-1.2.11,/etc/init.d/nginx start启动时,出现 Job for nginx.service failed because the control process exited with error code. See "systemctl status n......

河图再现 ⋅ 42分钟前 ⋅ 0

没有更多内容

加载失败,请刷新页面

加载更多

下一页

返回顶部
顶部