前言

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. mysql表ERROR 144 (HY000)Table 'dede_archives' is marked

    1.故障现象 mysql> select count(*) from dede_archives;ERROR 144 (HY000): Table '.xx' is marked as cras ...

  2. mysqldump 使用规范

    数据库很重要,没有备份,数据丢失只能跑路.所以还是做好备份吧! 目录 一.工具介绍 二.工具特点 三.备份权限 四.工具使用限制 五.已知BUG 六.备份前注意事项 6.1 需要长时间备份或导入时,请 ...

  3. 【NX二次开发】Block UI 反向

    属性说明 属性   类型   描述   常规           BlockID    String    控件ID    Enable    Logical    是否可操作    Group    ...

  4. 【C++】map容器的用法

    检测map容器是否为空: 1 #include <iostream> 2 #include<map> 3 #include<string> 4 using name ...

  5. Telnet查看端口是否通

    1. 查看端口是否通畅   telnet IP 端口号 如:telnet 10.150.159.71 5516 2.查看本机是否开启某个端口:netstat -an |findstr "端口 ...

  6. Jenkins 构建自动化 .NET Core 发布镜像

    Jenkins 构建自动化 .NET Core 发布镜像 导读 在本章中,将介绍如何在 Linux 下使用 Docker 部署.启动 Jenkins,编写脚本,自动化构建 .NET Core 应用,最 ...

  7. Java8-四个函数式接口(Consumer,Supplier,Predicate,Function)

    Java8---函数式接口 Consumer---消费者(accept方法,Lambda与方法引用返回都是Consumer) Supplier---供给型(get方法,返回数据,与Optional可以 ...

  8. Room-数据持久化存储(入门)

    @ 目录 一.简单使用 1.Entity 2.Dao 3.DataBase 4.使用 二.参数解析 1.Entity 2.Dao 3.查询方式 总结 # 前言 官方简介: Room 持久性库在 SQL ...

  9. 最新Unity 与Android 交互通信(基于Unity 2019.4 和 Android Studio 4.1.1)

    原文章链接:https://blog.csdn.net/woshihaizeiwang/article/details/115395519 CLSays:网上找了一圈,真的是很多都不能用,要么太老,要 ...

  10. webpack(2)webpack核心概念

    前言   本质上,webpack 是一个用于现代 JavaScript 应用程序的 静态模块打包工具.当 webpack 处理应用程序时,它会在内部构建一个 依赖图(dependency graph) ...