java多线程系列 ---- 第四篇 数据同步与线程锁

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接: https://blog.csdn.net/u013513053/article/details/100601732

终于到了深入的线程内容了。数据同步,线程安全,锁等这些概念是多线程中最复杂也是最重要的内容之一。串行化没那么多麻烦事,但也相对的运行效率低,不能最大化利用CPU。我们想要实现更高效的处理,多线程是必须的。但多线程就出现了 多个线程访问一个资源,出现数据不一致或者错误的情况,接下来就深入探讨如何安全的高效的处理共享数据。

数据同步

数据不一致问题

在我的第一篇博客中,举了一个模拟叫号的例子。如果多运行几次,就会发现一些问题。这里不再赘述,只说一下出现的问题:
1、有的号码被跳过
2、有的号码重复
3、最后超过最大值或者不到最大值

分析原因

1、号码跳过

当我们线程1访问到index为2,执行加操作,index变为3还未输出。这时候线程1进入等待,线程2运行。线程2执行完成加操作未输出前操作权又到了线程1,线程1输出4。数字3就被忽略过去了

2、号码重复

当线程1访问index,还未对index加操作,此时index是2,线程2也访问到了index,得到的数字也是2,因为线程1没有加完。线程2计算的结果与线程1是一样的,这就出现了号码重复。

3、超过最大值

当线程1和线程2都运行到49的时候,都不满足最大值。这个时候线程2进入等待,线程1对数字进行加操作,变成了50输出。线程2再次拿index时变成了50,运算结果就成了51

synchronize关键字

synchronize关键字介绍

synchronize关键字可以实现一个简单的策略来防止线程干扰和内存一直性的错误,如果一个对象对多个线程是可见的,那么对该对象的所有读或写都将通过同步的方式来进行。具体内容如下:

  • synchronize关键字提供了一种锁机制,能够确保共享变量的互斥访问,从而防止数据不一致的问题出现。
  • synchronize关键字包括monitor enter和monitor exit两个JVM指令,它能保证在任何时候任何线程执行到monitor enter成功之前都必须从主内存中获取数据,而不是从缓存中。在monitor exit运行成功后,共享变量被更新后的值必须收入主内存
  • synchronize的指令严格遵守java happens-before规则,一个monitor exit指令之前必定要有一个monitor enter

synchronize用法

同步方法

同步方法的语法非常简单,只需要在方法前面加上synchronize关键字即可

//普通方法
 public synchronized void sync(){
        //····
 }
 
 //静态方法 
public synchronized static void staticSync(){
        //····
}

同步代码块

同步代码块示例

    private final Object MUTEX = new Object();
    public  void staticSync(){
        synchronized (MUTEX){
            // 具体代码
        }
    }

深入学习synchronize

对象的monitor锁

synchronize关键字提供了一种互斥机制,也就是说在同一时刻,只能有一个线程访问同步资源。synchronize获取的是与对象关联的monitor锁。

monitor enter

每个对对象都与一个monitor相关联,一个monitor的lock的锁只能被一个线程在同一时间获得,在每个线程尝试获得与对象关联的monitor的所有权时会发生以下几件事:

  • 如果monitor的计数器为0,则意味着该monitor的lock还没有被获得。某个线程获得后将立即对其计数器加一,从此该线程是这个monitor的拥有者。
  • 如果一个已经拥有该monitor所有权的线程再次调用,则会导致monitor计数器再次累加。
  • 如果monitor已经被其他线程拥有,则其他线程尝试获取该monitor的所有权时,会进入block阻塞状态直到monitor计数器为0,才能再次尝试获取

monitor exit

释放monitor的所有权,想要释放对某个对象关联的monitor的所有权的前提是,该线程曾获取了所有权。释放monitor所有权的过程比较简单,就是将monitor的计数器减一。如果计数器的结果为0,就意味着该线程不再拥有对该monitor的所有权,通俗的讲就是解锁。与此同时其他等待的线程会再次尝试获得monitor的所有权

注意事项

1、与monitor关联的对象不能为空

每一个对象和 一个monitor关联,对象都为null了,monitor也无从谈起

2、synchronize作用域太大

synchronize关键字具有排他性,也就是说所有的线程必须串行的经过synchronize保护的区域。如果synchronize作用域越大,就意味着效率越低,甚至于丧失并发带来的优势。如果在run方法层级上 就添加synchronize关键字,那么跟普通的串行没什么区别。

3、不同对象monitor企图锁相同的方法

不同线程使用同一个对象 锁才能互斥的使用,那如果每个线程的锁都是单独的,不同的锁之间也是没有关联的。下面的代码猛一看好像是没什么毛病,但是我们是new了5个Task,每个Task都是新的MUTEX,所以说这五个线程根本也谈不上互斥,因为就没有访问到同一个资源

    public static class Task implements Runnable{

        private final Object MUTEX = new Object();
        @Override
        public void run() {
            //···
            synchronized (MUTEX){
                // ···
            }
            //···
        }
    }
    
    public static void main(String[] args) {
        for (int i= 0; i <5 ; i++) {
            new Thread(Task::new).start();
        }

    }

4、多个锁交叉导致死锁

多个锁交叉很容易导致死锁问题,通常是多个锁互相等待,你等我释放,我等你释放,结果就死锁了。下面这个就是比较典型的死锁代码

    private final Object MUTEX_RED = new Object();
    private final Object MUTEX_WRITE = new Object();
    
    public void red(){
        synchronized (MUTEX_RED){
            synchronized (MUTEX_WRITE){
                //···
            }
        }
    }
    
    public void write(){
        synchronized (MUTEX_WRITE){
            synchronized (MUTEX_RED){
                //···
            }
        }
    }

线程锁介绍

下面这几种也是经常见到的锁的写法,分两大类:方法和对象。然后方法又分为普通方法和静态方法。对象分为this、class和创建的对象

synchronize方法

我们直接在方法上面添加synchronize关键字是表示的什么呢?前面说了synchronize是获取对象关联的monitor对象,那这里也没有对象啊。其实这里是有对象的,这个对象就是this。我们普通的方法都是需要通过new出来的对象去调用,我们new出来的对象在内部可以用this代指。直接在方法上添加synchronize关键字跟synchronize(this) 是一样的。

public synchronized void red(){
 	//···      
 }

等同于

public void red(){
	synchronized(this){
 		//···      
 	}
 }

synchronize静态方法

静态方法是不需要new对象调用,而是直接通过类去调用。这里monitor锁是谁呢?其实这里的静态方法上面的synchronize就相当于synchronize(Task.class)。在下面就有解释synchronize(类.class)

public class Task {
    public static synchronized void method(){
        //···
    }
    
    public static void main(String[] args) {
        new Thread(Task::method).start();
    }
}

synchronize(this)

在前面说了,直接在方法上面加synchronize关键字就相当于synchronize(this)。这里的对象就是当前的对象。
方法上面添加synchronize相当于在方法根上添加synchronize(this),但是单独的synchronize(this)却可以使用在方法的任何位置,相较于方法层面,会比较灵活,控制的范围减小。

synchronize(类.class)

也是如同上面的例子,静态方法的synchronize相当于synchronize(Task.class)。在Java中一切都是对象,我们一般所使用的对象都直接或间接继承自Object类。Object类中包含一个方法名叫getClass,利用这个方法就可以获得一个实例的类型类。类型类指的是代表一个类型的类,因为一切皆是对象,类型也不例外,在Java使用类型类来表示一个类型。类的Class类实例是通过.class获得的。
其实还是关联一个对象,只不过这个对象是系统类对象而已。
方法上面的相当于在方法根部添加,synchronize(类.class)也是可以单独使用,与上面synchronize(this)类似。

synchronize(objcet)

前面一直在使用这种方式,上面又引出了几种方式,那么对比一下这几种锁的方式
synchronize(this)只能在当前对象中去使用,也就是说必须是同一个对象才有效。如果是创建多个对象,那么这个锁是没用的。
synchronize(类.class)则是在同一个类的创建所有对象都是一把锁,使用的同步锁都是对应的类作为对象锁,在jvm中类是唯一的,那也就是说对任何对象都是同步的,因为具有唯一的对象锁。
synchronize(objcet)可以更加灵活的控制,可以在一个类里面使用,也可以作为参数传入在多个类之间使用。相较限制小一些。不过用这种方式就要注意对象不能为空

死锁问题

我们在日常中也会见到大塞车,有时候塞车就是由于死锁引起的。互相阻挡了去路,这个时候没有打破的情况下,塞车也会越来越严重。
在这里插入图片描述
在这里插入图片描述

程序死锁

1. 交叉锁

线程A持有R1的锁等待R2的锁,线程B持有R2的锁等待R1的锁。这就是一种典型的哲学专家吃面问题,这种情况也是最容易发生的问题。在前面也有举例。

2. 内存不足

现在内存剩余30M,线程A已经获取了10M,线程B获取了20M的内存,两个线程执行都需要30M的资源,那这两条线程可能就会互相等待对方释放资源。

3 数据库锁

当某线程进行数据库操作比如执行for update语句退出了事务,其他线程访问数据库时都会陷入死锁

4. 文件锁

同样的,线程获取了文件 锁但是意外退出,其他线程读取该文件也会陷入死锁知道系统释放文件资源。

5. 死循环引起的死锁

由于代码处理不当进入了死循环,虽然查看线程堆栈信息看不到任何死锁的迹象,但程序不工作,CPU占有率又居高不下,这种一般称为假死。

死锁涉及的东西太多,在这不再展开来说了,之后有功夫再写一篇死锁的文章

猜你喜欢

转载自blog.csdn.net/u013513053/article/details/100601732