前言

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. 狂神说redis笔记(一)

    一.Nosql概述 1.单机Mysql时代 90年代,一个网站的访问量一般不会太大,单个数据库完全够用.随着用户增多,网站出现以下问题: 数据量增加到一定程度,单机数据库就放不下了 数据的索引(B+ ...

  2. Headline 项目总结中

    目录 1.项目准备 1.1 rem适配 1.2 通用样式CSS 1.3删除测试代码 1.4Git托管 2.login页面 2.1 页面布局和表单校验 2.2login页的接口抽取 2.5.loadin ...

  3. 『动善时』JMeter基础 — 41、使用JMeter连接数据库(MySQL)

    目录 1.为什么要使用JMeter连接数据库 2.JMeter连接数据库的前提 3.JDBC连接配置组件界面介绍 4.JMeter连接数据库演示 (1)测试计划内包含的元件 (2)测试计划中添加链接数 ...

  4. Spring Boot WebFlu-05——WebFlux 中 Thymeleaf 和 MongoDB 实践

    第05课:WebFlux 中 Thymeleaf 和 MongoDB 实践 前言 本节内容主要还是总结上面两篇内容的操作,并实现一个复杂查询的小案例,那么没安装 MongoDB 的可以进行下面的安装流 ...

  5. 为什么 Python 没有函数重载?如何用装饰器实现函数重载?

    英文:https://arpitbhayani.me/blogs/function-overloading 作者:arprit 译者:豌豆花下猫("Python猫"公众号作者) 声 ...

  6. 在 NUC980 上运行 RT-Thread

    NUC980 & RT-Thread (1) NUC980 nuc980 是新塘推出的基于 ARM926EJ-S,集成 64 MB 或 128 MB DDR-II 的处理器,主频可以达到300 ...

  7. [Django REST framework - 视图组件之视图基类、视图扩展类、视图子类、视图集]

    [Django REST framework - 视图组件之视图基类.视图扩展类.视图子类.视图集] 视图继承关系 详图见文章末尾 视图组件可点我查看 两个视图基类:APIView.GenericAP ...

  8. 21、nginx之ngx_http_proxy_module模块

    Nginx 的代理功能是通过 ngx_http_proxy_module 模块来实现的.默认在安装 Nginx 时已经安装了ngx_http_proxy_module模 块,因此可直接使用 ngx_h ...

  9. 14、WindowsServer修改NTP时间同步服务器

    1. 2. 3.

  10. flex PopUpManager createPopUp方式弹出窗口

    createPopUp:是上来先设置好弹出方式,然后在new出来一个TitleWindow的对象,然后通过此对象设置他的变现形式. addPopUp:是先new出来对象,设置好表现形式,然后在选择弹出 ...