在并发编程中,经常遇到多个线程访问同一个 共享资源 ,这时候作为开发者必须考虑如何维护数据一致性,在java中synchronized关键字被常用于维护数据一致性。synchronized机制是给共享资源上锁,只有拿到锁的线程才可以访问共享资源,这样就可以强制使得对共享资源的访问都是顺序的,因为对于共享资源属性访问是必要也是必须的,下文会有具体示例演示。

一.java中的锁
一般在java中所说的锁就是指的内置锁,每个java对象都可以作为一个实现同步的锁,虽然说在java中一切皆对象, 但是锁必须是引用类型的,基本数据类型则不可以 。每一个引用类型的对象都可以隐式的扮演一个用于同步的锁的角色,执行线程进入synchronized块之前会自动获得锁,无论是通过正常语句退出还是执行过程中抛出了异常,线程都会在放弃对synchronized块的控制时自动释放锁。 获得锁的唯一途径就是进入这个内部锁保护的同步块或方法 。
正如引言中所说,对共享资源的访问必须是顺序的,也就是说当多个线程对共享资源访问的时候,只能有一个线程可以获得该共享资源的锁,当线程A尝试获取线程B的锁时,线程A必须等待或者阻塞,直到线程B释放该锁为止,否则线程A将一直等待下去,因此java内置锁也称作互斥锁,也即是说锁实际上是一种互斥机制。
根据使用方式的不同一般我们会将锁分为对象锁和类锁,两个锁是有很大差别的,对象锁是作用在实例方法或者一个对象实例上面的,而类锁是作用在静态方法或者Class对象上面的。一个类可以有多个实例对象,因此一个类的对象锁可能会有多个,但是每个类只有一个Class对象,所以类锁只有一个。 类锁只是一个概念上的东西,并不是真实存在的,它只是用来帮助我们理解锁定的是实例方法还是静态方法区别的 。
在java中实现锁机制不仅仅限于使用synchronized关键字,还有JDK1.5之后提供的Lock,Lock不在本文讨论范围之内。一个synchronized块包含两个部分:锁对象的引用,以及这个锁保护的代码块。如果作用在实例方法上面,锁就是该方法所在的当前对象,静态synchronized方法会从Class对象上获得锁。
二.synchronized使用示例
1.多窗口售票
假设一个火车票售票系统,有若干个窗口同时售票,很显然在这里票是作为多个窗口的共享资源存在的,由于座位号是确定的,因此票上面的号码也是确定的,我们用多个线程来模拟多个窗口同时售票,首先在不使用synchronized关键字的情况下测试一下售票情况。
先将票本身作为一个共享资源放在单独的线程中,这种作为共享资源存在的线程很显然应该是实现Runnable接口,我们将票的总数num作为一个入参传入,每次生成一个票之后将num做减法运算,直至num为0即停止,说明票已经售完了,然后开启多个线程将票资源传入。
public class Ticket implements Runnable{
private int num;//票数量
private boolean flag=true;//若为false则售票停止
public Ticket(int num){
this.num=num;
}
@Override
public void run() {
while(flag){
ticket();
}
}
private void ticket(){
if(num<=0){
flag=false;
return;
}
try {
Thread.sleep(20);//模拟延时操作
} catch (InterruptedException e) {
e.printStackTrace();
}
//输出当前窗口号以及出票序列号
System.out.println(Thread.currentThread().getName()+"售出票序列号:"+num--);
}
}
public class MainTest {
public static void main(String[] args) {
Ticketticket = new Ticket(5);
Threadwindow01 = new Thread(ticket, "窗口01");
Threadwindow02 = new Thread(ticket, "窗口02");
Threadwindow03 = new Thread(ticket, "窗口03");
window01.start();
window02.start();
window03.start();
}
}
程序的输出结果如下:
窗口02售出票序列号:5 窗口03售出票序列号:4 窗口01售出票序列号:5 窗口02售出票序列号:3 窗口01售出票序列号:2 窗口03售出票序列号:2 窗口02售出票序列号:1 窗口03售出票序列号:0 窗口01售出票序列号:-1
从上面程序运行结果可以看出不但票的序号有重号而且出票数量也不对,这种售票系统比12306可要烂多了,人家在繁忙的时候只是刷不到票而已,而这里的售票系统倒好了,出票比预计的多了而且会出现多个人争抢做同一个座位的风险。如果是单个售票窗口是不会出现这种问题,多窗口同时售票就会出现争抢共享资源因此紊乱的现象,解决该现象也很简单,就是在ticket()方法前面加上synchronized关键字或者将ticket()方法的方法体完全用synchronized块包括起来。
//方式一
private synchronized void ticket(){
if(num<=0){
flag=false;
return;
}
try {
Thread.sleep(20);//模拟延时操作
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"售出票序列号:"+num--);
}
//方式二
private void ticket(){
synchronized (this) {
if (num <= 0) {
flag = false;
return;
}
try {
Thread.sleep(20);//模拟延时操作
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "售出票序列号:" + num--);
}
}
再看一下加入synchronized关键字的程序运行结果:
窗口01售出票序列号:5 窗口03售出票序列号:4 窗口03售出票序列号:3 窗口02售出票序列号:2 窗口02售出票序列号:1
从这里可以看出在实例方法上面加上synchronized关键字的实现效果跟对整个方法体加上synchronized效果是一样的。 另外一点需要注意加锁的时机也非常重要 ,本示例中ticket()方法中有两处操作容易出现紊乱,一个是在if语句模块,一处是在num–,这两处操作本身都不是原子类型的操作,但是在使用运行的时候需要这两处当成一个整体操作,所以synchronized将整个方法体都包裹在了一起。如若不然,假设num当前值是1,但是窗口01执行到了num–,整个操作还没执行完成,只进行了赋值运算还没进行自减运算,但是窗口02已经进入到了if语句模块,此时num还是等于1,等到窗口02执行到了输出语句的时候,窗口01的num–也已经将自减运算执行完成,这时候窗口02就会输出序列号0的票。再者如果将synchronized关键字加在了run方法上面,这时候的操作不会出现紊乱或者错误,但是这种加锁方式无异于单窗口操作,当窗口01拿到锁进入run()方法之后,必须等到flag为false才会将语句执行完成跳出循环,这时候的num就已经为0了,也就是说票已经被售卖完了,这种方式摒弃了多线程操作,违背了最初的设计原则-多窗口售票。
2.懒汉式单例模式
创建单例模式有很多中实现方式,本文只讨论懒汉式创建。在Android开发过程中单例模式可以说是最常使用的一种设计模式,因为它操作简单还可以有效减少内存溢出。下面是懒汉式创建单例模式一个示例:
public class Singleton {
private static Singletoninstance;
private Singleton() {
}
public static SingletongetInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}
如果对于多窗口售票逻辑已经完全明白了的话就可以看出这里的实现方式是有问题的,我们可以简单的创建几个线程来获取单例输出对象的hascode值。
com.sunny.singleton.Singleton@15c330aa com.sunny.singleton.Singleton@15c330aa com.sunny.singleton.Singleton@41aff40f
在多线程模式下发现会出现不同的对象,这种单例模式很显然不是我们想要的,那么根据上面多窗口售票的逻辑我们在getInstance()方法上面加上一个synchronized关键字,给该方法加上锁,加上锁之后可以避免多线程模式下生成多个不同对象,但是同样会带来一个效率问题,因为不管哪个线性进入getInstance()方法都会先获得锁,然后再次释放锁,这是一个方面,另一个方面就是只有在第一次调用getInstance()方法的时候,也就是在if语句块内才会出现多线程并发问题,而我们却索性将整个方法都上锁了。讨论到这里就引出了另外一个问题,究竟是synchronized方法好还是synchronized代码块好呢? 有一个原则就是锁的范围越小越好 ,加锁的目的就是将锁进去的代码作为原子性操作,因为非原子操作都不是线程安全的,因此synchronized代码块应该是在开发过程中优先考虑使用的加锁方式。
public static SingletongetInstance() {
if (instance == null) {
synchronized (Singleton.class) {
instance = new Singleton();
}
}
return instance;
}
这里也会遇到类似上面的问题,多线程并发下回生成多个实例,如线程A和线程B都进入if语句块,假设线程A先获得锁,线程B则等待,当new一个实例后,线程A释放锁,线程B获得锁后会再次执行new语句,同样不能保证单例要求,那么下面代码再来一个null判断,进行双重检查上锁呢?
public static SingletongetInstance() {
if (instance == null) {
synchronized (Singleton.class) {
if(instance==null){
instance = new Singleton();
}
}
}
return instance;
}
该模式就是双重检查上锁实现的单例模式,这里在代码层面我们已经 基本 保证了线程安全了,但是还是有问题的, 双重检查锁定的问题是:并不能保证它会在单处理器或多处理器计算机上顺利运行。双重检查锁定失败的问题并不归咎于 JVM 中的实现bug,而是归咎于java平台内存模型。内存模型允许所谓的“无序写入”,这也是这些习语失败的一个主要原因。 更为详细的介绍可以参考 Java单例模式中双重检查锁的问题 。所以单例模式创建比较建议使用恶汉式创建或者静态内部类方式创建。
3.synchronized不具有继承性
我们可以通过一个简单的demo验证这个问题,在一个方法中顺序的输出一系列数字,并且输出该数字所在的线程名称,在父类中加上synchronized关键字,子类重写父类方法测试一下加上synchronized关键字和不加关键字的区别即可。
public class Parent {
public synchronized void test() {
for (int i = 0; i < 5; i++) {
System.out.println("Parent " + Thread.currentThread().getName() + ":" + i);
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
子类继承父类Parent,重写test()方法.
public class Child extends Parent {
@Override
public void test() {
for (int i = 0; i < 5; i++) {
System.out.println("Child " + Thread.currentThread().getName() + ":" + i);
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
测试代码如下:
final Child c = new Child();
new Thread() {
public void run() {
c.test();
};
}.start();
new Thread() {
public void run() {
c.test();
};
}.start();
输出结果如下:
Parent Thread-0:0 Child Thread-0:0 Parent Thread-0:1 Child Thread-1:0 Parent Thread-0:2 Child Thread-0:1 Parent Thread-0:3 Child Thread-1:1 Parent Thread-0:4 Child Thread-0:2 Parent Thread-1:0 Child Thread-1:2 Parent Thread-1:1 Child Thread-0:3 Parent Thread-1:2 Child Thread-1:3 Parent Thread-1:3 Child Thread-0:4 Parent Thread-1:4 Child Thread-1:4
通过输出信息可以知道,父类Parent中会将单个线程中序列号输出完成才会执行另一个线程中代码,但是子类Child中确是两个线程交替输出数字,所以synchronized不具有继承性。
4.死锁示例
死锁是多线程开发中比较常见的一个问题。若有多个线程访问多个资源时,相互之间存在竞争,就容易出现死锁。下面就是一个死锁的示例,当一个线程等待另一个线程持有的锁时,而另一个线程也在等待该线程锁持有的锁,这时候两个线程都会处于阻塞状态,程序便出现死锁。
public class Thread01 extends Thread{
private Object resource01;
private Object resource02;
public Thread01(Object resource01, Object resource02) {
this.resource01 = resource01;
this.resource02 = resource02;
}
@Override
public void run() {
synchronized(resource01){
System.out.println("Thread01 locked resource01");
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (resource02) {
System.out.println("Thread01 locked resource02");
}
}
}
}
public class Thread02 extends Thread{
private Object resource01;
private Object resource02;
public Thread02(Object resource01, Object resource02) {
this.resource01 = resource01;
this.resource02 = resource02;
}
@Override
public void run() {
synchronized(resource02){
System.out.println("Thread02 locked resource02");
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (resource01) {
System.out.println("Thread02 locked resource01");
}
}
}
}
public class MainTest {
public static void main(String[] args) {
final Object resource01="resource01";
final Object resource02="resource02";
Thread01thread01=new Thread01(resource01, resource02);
Thread02thread02=new Thread02(resource01, resource02);
thread01.start();
thread02.start();
}
}
执行上面的程序就会一直等待下去,出现死锁。当线程Thread01获得resource01的锁后,等待500ms,然后尝试获取resource02的锁,但是此时resouce02锁已经被Thread02持有,同样Thread02也等待了500ms尝试获取resouce01锁,但是该所已经被Thread01持有,这样两个线程都在等待对方所有的资源,造成了死锁。
三.其它
关键字synchronized具有锁重入功能,当一个线程已经持有一个对象锁后,再次请求该对象锁时是可以得到该对象的锁的,这种方式是必须的,否则在一个synchronized方法内部就没有办法调用该对象的另外一个synchronized方法了。锁重入是通过为每个所关联一个计数器和一个占有它的线程,当计数器为0时,认为锁是未被占有的。线程请求一个未被占有的锁时,JVM会记录锁的占有者,并将计数器设置为1。如果同一个线程再次请求该锁,计数器会递增,每次占有的线程退出同步代码块时计数器会递减,直至减为0时锁才会被释放。
在声明一个对象作为锁的时候要注意字符串类型锁对象,因为字符串有一个常量池,如果不同的线程持有的锁是具有相同字符的字符串锁时,两个锁实际上同一个锁。
以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,同时也希望多多支持!
# java
# 锁
# Java中SpringSecurity密码错误5次锁定用户的实现方法
# Java 锁的知识总结及实例代码
# Java 高并发九:锁的优化和注意事项详解
# java 多线程-锁详解及示例代码
# 详解Java如何实现基于Redis的分布式锁
# Java锁之可重入锁介绍
# 使用JAVA实现高并发无锁数据库操作步骤分享
# Java锁之自旋锁详解
# java使用zookeeper实现的分布式锁示例
# Java 多线程同步 锁机制与synchronized深入解析
# 多个
# 死锁
# 是在
# 多线程
# 就会
# 是有
# 子类
# 这时候
# 多窗口
# 作为一个
# 加锁
# 会将
# 用在
# 会有
# 还没
# 两处
# 过程中
# 有一个
# 才会
# 会在
相关文章:
香港服务器租用每月最低只需15元?
全景视频制作网站有哪些,全景图怎么做成网页?
php能控制zigbee模块吗_php通过串口与cc2530 zigbee通信【介绍】
如何制作算命网站,怎么注册算命网站?
如何正确下载安装西数主机建站助手?
高性价比服务器租赁——企业级配置与24小时运维服务
小建面朝正北,A点实际方位是否存在偏差?
大连网站制作公司哪家好一点,大连买房网站哪个好?
制作农业网站的软件,比较好的农业网站推荐一下?
专业制作网站的公司哪家好,建立一个公司网站的费用.有哪些部分,分别要多少钱?
道歉网站制作流程,世纪佳缘致歉小吴事件,相亲网站身份信息伪造该如何稽查?
如何在腾讯云服务器快速搭建个人网站?
微信网站制作公司有哪些,民生银行办理公司开户怎么在微信网页上查询进度?
网站制作壁纸教程视频,电脑壁纸网站?
网站制作公司排行榜,抖音怎样做个人官方网站
,购物网站怎么盈利呢?
动图在线制作网站有哪些,滑动动图图集怎么做?
C++中的Pimpl idiom是什么,有什么好处?(隐藏实现)
网站制作软件有哪些,制图软件有哪些?
,网页ppt怎么弄成自己的ppt?
百度网页制作网站有哪些,谁能告诉我百度网站是怎么联系?
如何在阿里云虚拟机上搭建网站?步骤解析与避坑指南
宁波自助建站系统如何快速打造专业企业网站?
TestNG的testng.xml配置文件怎么写
无锡制作网站公司有哪些,无锡优八网络科技有限公司介绍?
如何快速使用云服务器搭建个人网站?
零基础网站服务器架设实战:轻量应用与域名解析配置指南
如何通过虚拟主机快速完成网站搭建?
网站制作公司,橙子建站是合法的吗?
如何通过虚拟机搭建网站?详细步骤解析
阿里云高弹*务器配置方案|支持分布式架构与多节点部署
建站主机SSH密钥生成步骤及常见问题解答?
单页制作网站有哪些,朋友给我发了一个单页网站,我应该怎么修改才能把他变成自己的呢,请求高手指点迷津?
建站之星伪静态规则如何正确配置?
如何选择高性价比服务器搭建个人网站?
湖北网站制作公司有哪些,湖北清能集团官网?
c# Task.Yield 的作用是什么 它和Task.Delay(1)有区别吗
如何选购建站域名与空间?自助平台全解析
浅析上传头像示例及其注意事项
西安大型网站制作公司,西安招聘网站最好的是哪个?
大学网站设计制作软件有哪些,如何将网站制作成自己app?
上海网站制作开发公司,上海买房比较好的网站有哪些?
香港服务器建站指南:外贸独立站搭建与跨境电商配置流程
c++怎么使用类型萃取type_traits_c++ 模板元编程类型判断【方法】
重庆网站制作公司哪家好,重庆中考招生办官方网站?
如何使用Golang table-driven基准测试_多组数据测量函数效率
外贸公司网站制作哪家好,maersk船公司官网?
如何制作一个表白网站视频,关于勇敢表白的小标题?
如何通过免费商城建站系统源码自定义网站主题与功能?
宝塔建站无法访问?如何排查配置与端口问题?
*请认真填写需求信息,我们会在24小时内与您取得联系。