前言

1、理解同步关键词synchronized

2、同步方法与同步代码块的区别

3、理解锁的对象this

脏读

一个常见的概念。在多线程中,难免会出现在多个线程中对同一个对象的实例变量进行并发访问的情况,如果不做正确的同步处理,那么产生的后果就是"脏读",也就是取到的数据其实是被更改过的。

多线程线程安全问题示例

看一段代码:

public class ThreadDomain13
{
private int num = 0; public void addNum(String userName)
{
try
{
if ("a".equals(userName))
{
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)
{
e.printStackTrace();
}
}
}

写两个线程分别去add字符串"a"和字符串"b":

public class MyThread13_0 extends Thread
{
private ThreadDomain13 td; public MyThread13_0(ThreadDomain13 td)
{
this.td = td;
} public void run()
{
td.addNum("a");
}
}
public class MyThread13_1 extends Thread
{
private ThreadDomain13 td; public MyThread13_1(ThreadDomain13 td)
{
this.td = td;
} public void run()
{
td.addNum("b");
}
}

写一个主函数分别运行这两个线程:

public static void main(String[] args)
{
ThreadDomain13 td = new ThreadDomain13();
MyThread13_0 mt0 = new MyThread13_0(td);
MyThread13_1 mt1 = new MyThread13_1(td);
mt0.start();
mt1.start();
}

看一下运行结果:

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

按照正常来看应该打印"a num = 100"和"b num = 200"才对,现在却打印了"b num = 200"和"a num = 200",这就是线程安全问题。我们可以想一下是怎么会有线程安全的问题的:

  1. mt0先运行,给num赋值100,然后打印出"a set over!",开始睡觉
  2. mt0在睡觉的时候,mt1运行了,给num赋值200,然后打印出"b set over!",然后打印"b num = 200"
  3. mt1睡完觉了,由于mt0的num和mt1的num是同一个num,所以mt1把num改为了200了,mt0也没办法,对于它来说,num只能是100,mt0继续运行代码,打印出"a num = 200"

分析了产生问题的原因,解决就很简单了,给addNum(String userName)方法加同步即可:

public class ThreadDomain13
{
private int num = 0; public synchronized void addNum(String userName)
{
try
{
if ("a".equals(userName))
{
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)
{
e.printStackTrace();
}
}
}

看一下运行结果:

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

多个对象多个锁

在同步的情况下,把main函数内的代码改一下:

public static void main(String[] args)
{
ThreadDomain13 td0 = new ThreadDomain13();
ThreadDomain13 td1 = new ThreadDomain13();
MyThread13_0 mt0 = new MyThread13_0(td0);
MyThread13_1 mt1 = new MyThread13_1(td1);
mt0.start();
mt1.start();
}

看一下运行结果:

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

打印结果的方式变了,打印的顺序是交叉的,这又是为什么呢?

这里有一个重要的概念。关键字synchronized取得的锁都是对象锁,而不是把一段代码或方法(函数)当作锁,哪个线程先执行带synchronized关键字的方法,哪个线程就持有该方法所属对象的锁,其他线程都只能呈等待状态。但是这有个前提:既然锁叫做对象锁,那么势必和对象相关,所以多个线程访问的必须是同一个对象

如果多个线程访问的是多个对象,那么Java虚拟机就会创建多个锁,就像上面的例子一样,创建了两个ThreadDomain13对象,就产生了2个锁。既然两个线程持有的是不同的锁,自然不会受到"等待释放锁"这一行为的制约,可以分别运行addNum(String userName)中的代码。

synchronized方法与锁对象

上面我们认识了对象锁,对象锁这个概念,比较抽象,确实不太好理解,看一个例子,在一个实体类中定义一个同步方法和一个非同步方法:

public class ThreadDomain14_0
{
public synchronized void methodA()
{
try
{
System.out.println("Begin methodA, threadName = " +
Thread.currentThread().getName());
Thread.sleep(5000);
System.out.println("End methodA, threadName = " +
Thread.currentThread().getName() + ", end Time = " +
System.currentTimeMillis());
}
catch (InterruptedException e)
{
e.printStackTrace();
}
} public void methodB()
{
try
{
System.out.println("Begin methodB, threadName = " +
Thread.currentThread().getName() + ", begin time = " +
System.currentTimeMillis());
Thread.sleep(5000);
System.out.println("End methodB, threadName = " +
Thread.currentThread().getName());
}
catch (InterruptedException e)
{
e.printStackTrace();
}
}
}

一个线程调用其同步方法,一个线程调用其非同步方法:

public class MyThread14_0 extends Thread
{
private ThreadDomain14_0 td; public MyThread14_0(ThreadDomain14_0 td)
{
this.td = td;
} public void run()
{
td.methodA();
}
}
public class MyThread14_1 extends Thread
{
private ThreadDomain14_0 td; public MyThread14_1(ThreadDomain14_0 td)
{
this.td = td;
} public void run()
{
td.methodB();
}
}

写一个main函数去调用这两个线程:

public static void main(String[] args)
{
ThreadDomain14_0 td = new ThreadDomain14_0();
MyThread14_0 mt0 = new MyThread14_0(td);
mt0.setName("A");
MyThread14_1 mt1 = new MyThread14_1(td);
mt1.setName("B");
mt0.start();
mt1.start();
}

看一下运行效果:

Begin methodA, threadName = A
Begin methodB, threadName = B, begin time = 1443697780869
End methodB, threadName = B
End methodA, threadName = A, end Time = 1443697785871

从结果看到,第一个线程调用了实体类的methodA()方法,第二个线程完全可以调用实体类的methodB()方法。但是我们把methodB()方法改为同步就不一样了,就不列修改之后的代码了,看一下运行结果:

Begin methodA, threadName = A
End methodA, threadName = A, end Time = 1443697913156
Begin methodB, threadName = B, begin time = 1443697913156
End methodB, threadName = B

从这个例子我们得出两个重要结论:

  1. A线程持有Object对象的Lock锁,B线程可以以异步方式调用Object对象中的非synchronized类型的方法
  2. A线程持有Object对象的Lock锁,B线程如果在这时调用Object对象中的synchronized类型的方法则需要等待,也就是同步

synchronized锁重入

关键字synchronized拥有锁重入的功能。所谓锁重入的意思就是:当一个线程得到一个对象锁后,再次请求此对象锁时可以再次得到该对象的锁。看一个例子:

public class ThreadDomain16
{
public synchronized void print1()
{
System.out.println("ThreadDomain16.print1()");
print2();
} public synchronized void print2()
{
System.out.println("ThreadDomain16.print2()");
print3();
} public synchronized void print3()
{
System.out.println("ThreadDomain16.print3()");
}
}
public class MyThread16 extends Thread
{
public void run()
{
ThreadDomain16 td = new ThreadDomain16();
td.print1();
}
}
public static void main(String[] args)
{
MyThread16 mt = new MyThread16();
mt.start();
}

看一下运行结果:

ThreadDomain16.print1()
ThreadDomain16.print2()
ThreadDomain16.print3()

看到可以直接调用ThreadDomain16中的打印语句,这证明了对象可以再次获取自己的内部锁。这种锁重入的机制,也支持在父子类继承的环境中

异常自动释放锁

最后一个知识点是异常。当一个线程执行的代码出现异常时,其所持有的锁会自动释放。模拟的是把一个long型数作为除数,从MAX_VALUE开始递减,直至减为0,从而产生ArithmeticException。看一下例子:

public class ThreadDomain17
{
public synchronized void testMethod()
{
try
{
System.out.println("Enter ThreadDomain17.testMethod, currentThread = " +
Thread.currentThread().getName());
long l = Integer.MAX_VALUE;
while (true)
{
long lo = 2 / l;
l--;
}
}
catch (Exception e)
{
e.printStackTrace();
}
}
}
public class MyThread17 extends Thread
{
private ThreadDomain17 td; public MyThread17(ThreadDomain17 td)
{
this.td = td;
} public void run()
{
td.testMethod();
}
}
public static void main(String[] args)
{
ThreadDomain17 td = new ThreadDomain17();
MyThread17 mt0 = new MyThread17(td);
MyThread17 mt1 = new MyThread17(td);
mt0.start();
mt1.start();
}

看一下运行结果:

Enter ThreadDomain17.testMethod, currentThread = Thread-0
Enter ThreadDomain17.testMethod, currentThread = Thread-1
java.lang.ArithmeticException: / by zero
at com.xrq.example.e17.ThreadDomain17.testMethod(ThreadDomain17.java:14)
at com.xrq.example.e17.MyThread17.run(MyThread17.java:14)
java.lang.ArithmeticException: / by zero
at com.xrq.example.e17.ThreadDomain17.testMethod(ThreadDomain17.java:14)
at com.xrq.example.e17.MyThread17.run(MyThread17.java:14)

因为打印结果是静态的,所以不是很明显。在l--前一句加上Thread.sleep(1)结论会更明显,第一句打出来之后,整个程序都停住了,直到Thread-0抛出异常后,Thread-1才可以运行,这也证明了我们的结论。

后记

文章里面的这些个结论,记一下都是很快的,但是是否记一下就好了?我认为记住这些结论一点都不重要,重要的应该是学习如何通过代码去验证这些结论。因为只有知道了如何通过代码去验证结论,才可以说真正对于synchronized关键字的各种细节有了感性、有了深入的理解,以后碰到其他synchronized的场景就可以以自己的理解去正确分析问题。

参看链接:https://www.cnblogs.com/xrq730/p/4851350.html

synchronized锁机制(六)的更多相关文章

  1. Java多线程5:Synchronized锁机制

    一.前言 在多线程中,有时会出现多个线程对同一个对象的变量进行并发访问的情形,如果不做正确的同步处理,那么产生的后果就是“脏读”,也就是获取到的数据其实是被修改过的. 二.引入Synchronized ...

  2. synchronized锁机制的实现原理

    Synchronized 锁机制的实现原理 Synchronized是Java种用于进行同步的关键字,synchronized的底层使用的是锁机制实现的同步.在Java中的每一个对象都可以作为锁. J ...

  3. Java多线程4:synchronized锁机制

    脏读 一个常见的概念.在多线程中,难免会出现在多个线程中对同一个对象的实例变量进行并发访问的情况,如果不做正确的同步处理,那么产生的后果就是"脏读",也就是取到的数据其实是被更改过 ...

  4. java 多线程8 : synchronized锁机制 之 方法锁

    脏读 一个常见的概念.在多线程中,难免会出现在多个线程中对同一个对象的实例变量或者全局静态变量进行并发访问的情况,如果不做正确的同步处理,那么产生的后果就是"脏读",也就是取到的数 ...

  5. 线程 synchronized锁机制

    脏读 一个常见的概念.在多线程中,难免会出现在多个线程中对同一个对象的实例变量进行并发访问的情况,如果不做正确的同步处理,那么产生的后果就是"脏读",也就是取到的数据其实是被更改过 ...

  6. Java多线程学习——synchronized锁机制

    Java在多线程中使用同步锁机制时,一定要注意锁对对象,下面的例子就是没锁对对象(每个线程使用一个被锁住的对象时,得先看该对象的被锁住部分是否有人在使用) 例子:两个人操作同一个银行账户,丈夫在ATM ...

  7. Synchronized锁机制和ReentrantLock

    Synchronized Java中的每个对象都可以作为锁. 普通同步方法,锁是当前实例对象. 静态同步方法,锁是当前类的class对象. 同步代码块,锁是括号中的对象. 锁的内部机制 一般锁有4种状 ...

  8. synchronized锁机制 之 代码块锁(转)

    synchronized同步代码块 用关键字synchronized声明方法在某些情况下是有弊端的,比如A线程调用同步方法执行一个较长时间的任务,那么B线程必须等待比较长的时间.这种情况下可以尝试使用 ...

  9. java 多线程9 : synchronized锁机制 之 代码块锁

    synchronized同步代码块 用关键字synchronized声明方法在某些情况下是有弊端的,比如A线程调用同步方法执行一个较长时间的任务,那么B线程必须等待比较长的时间.这种情况下可以尝试使用 ...

随机推荐

  1. mybatis学习——类型别名(typeAliases)

    为什么要用类型别名? 答:类型别名可为 Java 类型设置一个缩写名字. 它仅用于 XML 配置,意在降低冗余的全限定类名书写. 举个例子说明: 在我们编写映射文件的时候: <?xml vers ...

  2. 五、自定义Zabbix监控项目

    要求: 沿用练习三,使用Zabbix实现自定义监控,实现以下目标:监控Linux服务器系统账户的数量. 方案: 需要使用Zabbix自定义key的方式实现自定义监控,参考如下操作步骤:1.创建自定义k ...

  3. 『言善信』Fiddler工具 — 15、使用Fiddler抓取HTTPS请求

    目录 1.Fiddler抓取HTTPS过程 2.拓展:SSL/TLS证书握手原理 3.Fiddler抓取HTTPS原理总结 4.Fiddler抓取HTTPS设置 步骤1:配置证书 步骤2:勾选设置 5 ...

  4. 你,确定了解Java的String字符串?

    本文将描述JDK6中String.intern()是如何实现的,以及在JDK7和JDK8中对字符串池化技术做了哪些改变. String池化介绍 String池化就是把一些值相同,但是标识符不同的字符串 ...

  5. Java将字符串反转的7种方法

    /方法1 递归方法 public static String reverse1(String s) { int length = s.length(); if(length <= 1){ ret ...

  6. HashMap 中7种遍历方式的性能分析

    随着 JDK 1.8 Streams API 的发布,使得 HashMap 拥有了更多的遍历的方式,但应该选择那种遍历方式?反而成了一个问题. 本文先从 HashMap 的遍历方法讲起,然后再从性能. ...

  7. Mongo集群搭建

    1.集群角色及架构 在搭建集群之前,需要首先了解几个概念:路由,分片.副本集.配置服务器等 mongos,数据库集群请求的入口,所有的请求都通过mongos进行协调,不需要在应用程序添加一个路由选择器 ...

  8. 基于xtrabackup的主从同步

    基于xtrabackup的主从同步 作者 刘畅 时间 2020-9-21 服务器版本:CentOS Linux release 7.5.1804 主机名 ip地址 服务器配置 安装软件 密码 mysq ...

  9. AcWing 1303. 斐波那契前 n 项和

    输出斐波那契数列前 n 项和 对m取摸的结果 #include<bits/stdc++.h> #define LL long long #define N 3 using namespac ...

  10. [心得]docker学习笔记

    1. docker是什么??? (1) docker是一台类似虚拟机的功能, 内部由一个个镜像组成, 镜像里可以运行容器, 而这个容器可以是任何东西, 比如mysql, 比如tomcat等等, 它的目 ...