本章首先从一个简单的例子入手,讲解了数据同步的概念,以及会引发数据不一致性问题的情况,然后非常详细地介绍了synchronized关键字以及与其对应的JVM指令。本章的最后还分析了几种可能引起程序进入死锁的原因,以及如何使用工具进行诊断,线程安全与数据同步是线程中最重要也是最复杂的知识点之一,掌握好本章的内容可以使得程序在多线程的情况下既高效又安全的运行。
数据不一致问题分析(以叫号程序举例)
1.号码略过
2.号码重复出现
3.号码超过最大值
初识synchronized关键字
synchronized关键字可以实现一个简单的策略来防止线程干扰和内存一致性错误。如果一个对象对于多个线程是可见的,那么该对象的所有读写都将通过同步的方式进行
1.synchronized关键字提供了一种锁的机制,确保共享变量的互斥访问,防止数据不一致问题的出现
2.synchronized关键字包括monitor enter 和monitor exit两个jvm指令,能保证在任何时候,任何线程执行到monitor enter成功之间都从主内存中获取数据,而不是从缓存中。在monitor exit 运行成功后,共享变量被更新后的值必须刷入主内存。
3.synchronized的指令严格准守java happens-before 规则, 一个monitor exit 指令之间必定要有一个monitor enter
synchronized关键字的用法
可以用于代码块或者方法进行修饰,而不能用于对class以及变量进行修饰。
1.同步方法
[default|public|private|protected ] synchronized [static] type method()
代码示例:
public synchronized void sync() {
.....
}
public synchronized static void staticSync() {
.....
}
2.同步代码块
private final Object MUTEX = new Object();
public void sync(){
synchronized(MUTEX){
.....
.....
}
}
叫号程序改写:
public class TicketWindowRunnable implements Runnable {
private int index = 1 ;
private final static int MAX = 500;
private final static Object MUTEX = new Object();
@Override
public void run() {
synchronized (MUTEX) {
while (index <=MAX) {
System.out.println(Thread.currentThread()+" 的号码是: " + (index++));
}
}
}
public static void main(String[] args) {
final TicketWindowRunnable task = new TicketWindowRunnable();
Thread windowThread1 = new Thread(task,"一号窗口") ;
Thread windowThread2 = new Thread(task,"二号窗口") ;
Thread windowThread3 = new Thread(task,"三号窗口") ;
Thread windowThread4 = new Thread(task,"四号窗口") ;
windowThread1.start();
windowThread2.start();
windowThread3.start();
windowThread4.start();
}
}
深入synchronized关键字
线程堆栈分析
monitorenter 计数器为0,标识monitor的lock还没有被获得, 某个线程获得之后,立即对计数器加1.
monitorexit 释放锁 计数器减1.
使用synchronized需要注意的问题
1.与monitor关联的对象不能为空
2.synchronized作用域过大
由于synchronized关键字存在排他性,也就是说所有的线程必须串行地通过synchronized保护的共享区域,
如果synchronized作用域越大,则代表着其效率越低,甚至会丧失并发优势
public static class Task implements Runnable {
@Override
public synchronized void run (){
.................
}
}
上面的代码对整个线程的执行逻辑单元都进行了synchronized同步,从而丧失了并发能力。
synchronized关键字应该尽可能地只作用于共享资源(数据)的读写作用域
3.不同的monitor企图锁相同的方法
4.多个锁的交叉,导致死锁
多个锁的交叉很容易引起线程出现死锁的状况,程序没有任何错误输出,就是不工作
This Monitor和Class Monitor的详细介绍
this monitor
synchronized关键字修饰了同一个实例对象的两个不同方法,只有一个方法被调用,另一个方法并没有被调用。
synchronized关键字同步类的不同实例方法,争抢的事同一个monitor的lock,而与之关联的引用则是ThisMonitor的实例引用。
import java.util.concurrent.TimeUnit;
import static java.lang.Thread.currentThread ;
public class ThisMonitor {
public static void main(String args[]){
ThisMonitor thisMonitor = new ThisMonitor();
new Thread(thisMonitor::method1,"T1").start();
new Thread(thisMonitor::method2,"T2").start();
}
public synchronized void method1(){
System.out.println(currentThread().getName() + " enter to method 1 ");
while (true) {
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public void method2(){
synchronized (this){
System.out.println(currentThread().getName() + " enter to method 2 ");
while (true) {
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
class monitor
有两个类方法(静态方法)分别使用synchronized对其进行同步,只有一个方法被调用,另一个方法并没有被调用。
package com.zl.step4;
import java.util.concurrent.TimeUnit;
import static java.lang.Thread.currentThread;
public class ClassMonitor {
public static void main(String args[]){
ClassMonitor thisMonitor = new ClassMonitor();
new Thread(ClassMonitor::method1,"T1").start();
new Thread(ClassMonitor::method2,"T2").start();
}
public static synchronized void method1(){
System.out.println(currentThread().getName() + " enter to method 1 ");
while (true) {
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public static void method2(){
synchronized (ClassMonitor.class){
System.out.println(currentThread().getName() + " enter to method 2 ");
while (true) {
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
该代码会有问题。
解决方式,请看下一章节。多线程间通讯
程序死锁的原因以及如何诊断
1.交叉锁可导致程序出现死锁
线程A持有R1的锁等待获取R2的锁,
线程B持有R2的锁等待获取R1的锁。
2.内存不足
线程T1, 已经获取了10M内存,还需要20M内存
线程T2,已经获取了20M内存,还需要10M内存
两个线程都需要等待彼此能够释放内存资源。才能继续执行
3.一问一答的数据交换
服务端开启某个端口,等待客户端访问,客户端发送请求立即等待接收,由于某种原因服务端错过了客户端的请求。
仍然在等待一问一答式的数据交换,此时服务端和客户端都在等待着对方发送数据
4.数据库锁
无论是数据库表级别的锁,还是行级别的锁,比如某个线程执行了for update 语句退出了事务,其他线程访问该数据库时都将陷入死锁。
5.文件锁
某线程获得了文件锁,意外退出,其他读取该文件的线程也将会进入死锁,知道系统释放文件句柄资源
6.死循环引起的死锁
程序由于代码原因或者对某些异常处理不当,进入死循环。查看线程堆栈信息不会发现任何死锁的迹象,但是程序不工作。cpu居高不下
这种死锁一般称为系统假死。排查较困难。
本文整理来源于:
《Java高并发编程详解:多线程与架构设计》 --汪文君