Java高级工程师蜕变·多线程·线程安全与数据同步

        在串行化的任务执行过程中,由于不存在资源的共享,线程安全的问题几乎不用考虑,但是串行化的程序,运行效率低下,不能最大化地利用CPU的计算能力。

        无论是互联网系统,还是企业级系统,在追求稳定计算的同时,也在追求更高的系统吞吐量。并发或者并行的程序并不意味着可以满足越多Thread,Thread的多少对系统的性能来讲是一个抛物线,同时多线程的引入也带来了共享资源安全的隐患。

  • 什么是共享资源

        共享资源指的是多个线程同时对同一份资源进行访问(读写操作),被多个线程访问的资源就被称为共享资源,如何保证多个线程访问到的数据是一致的,则被称为数据同步或者资源同步。

一、数据同步

1.数据不一致问题的引入

        运行下方一个简单的营业大厅叫号程序,就会出现数据不一致的情况,具体如下:

package com.jseeker.thread;

public class TicketWindowRunable implements Runnable {

    private int index = 1;

    private final static int MAX = 50;

    @Override
    public void run() {
        while (index <= MAX) {
            System.out.println(Thread.currentThread() + " 的号码是:" + (index++));
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    public static void main(String[] args) {
        final TicketWindowRunable task = new TicketWindowRunable();

        Thread windowThead1 = new Thread(task, "一号窗口");

        Thread windowThead2 = new Thread(task, "二号窗口");

        Thread windowThead3 = new Thread(task, "三号窗口");

        Thread windowThead4 = new Thread(task, "四号窗口");

        windowThead1.start();
        windowThead2.start();
        windowThead3.start();
        windowThead4.start();
    }
}
/*
* 程序输出:
*
Thread[一号窗口,5,main] 的号码是:1
Thread[三号窗口,5,main] 的号码是:3
Thread[四号窗口,5,main] 的号码是:4
Thread[二号窗口,5,main] 的号码是:2
Thread[三号窗口,5,main] 的号码是:5
Thread[四号窗口,5,main] 的号码是:7
Thread[一号窗口,5,main] 的号码是:6
Thread[二号窗口,5,main] 的号码是:5
Thread[二号窗口,5,main] 的号码是:8
Thread[一号窗口,5,main] 的号码是:9
Thread[四号窗口,5,main] 的号码是:8
Thread[三号窗口,5,main] 的号码是:8
Thread[二号窗口,5,main] 的号码是:10
Thread[四号窗口,5,main] 的号码是:10
Thread[一号窗口,5,main] 的号码是:10
Thread[三号窗口,5,main] 的号码是:11
Thread[四号窗口,5,main] 的号码是:12
Thread[一号窗口,5,main] 的号码是:12
Thread[三号窗口,5,main] 的号码是:12
Thread[二号窗口,5,main] 的号码是:13
Thread[二号窗口,5,main] 的号码是:14
Thread[一号窗口,5,main] 的号码是:14
Thread[四号窗口,5,main] 的号码是:14
Thread[三号窗口,5,main] 的号码是:14
Thread[二号窗口,5,main] 的号码是:15
Thread[三号窗口,5,main] 的号码是:16
Thread[一号窗口,5,main] 的号码是:17
Thread[四号窗口,5,main] 的号码是:17
Thread[二号窗口,5,main] 的号码是:18
Thread[四号窗口,5,main] 的号码是:20
Thread[三号窗口,5,main] 的号码是:19
Thread[一号窗口,5,main] 的号码是:19
Thread[二号窗口,5,main] 的号码是:21
Thread[四号窗口,5,main] 的号码是:22
Thread[三号窗口,5,main] 的号码是:23
Thread[一号窗口,5,main] 的号码是:24
Thread[二号窗口,5,main] 的号码是:25
Thread[三号窗口,5,main] 的号码是:26
Thread[一号窗口,5,main] 的号码是:27
Thread[四号窗口,5,main] 的号码是:28
Thread[二号窗口,5,main] 的号码是:29
Thread[四号窗口,5,main] 的号码是:30
Thread[三号窗口,5,main] 的号码是:31
Thread[一号窗口,5,main] 的号码是:32
Thread[二号窗口,5,main] 的号码是:33
Thread[四号窗口,5,main] 的号码是:34
Thread[一号窗口,5,main] 的号码是:35
Thread[三号窗口,5,main] 的号码是:35
Thread[二号窗口,5,main] 的号码是:36
Thread[四号窗口,5,main] 的号码是:37
Thread[一号窗口,5,main] 的号码是:38
Thread[三号窗口,5,main] 的号码是:39
Thread[二号窗口,5,main] 的号码是:40
Thread[四号窗口,5,main] 的号码是:41
Thread[一号窗口,5,main] 的号码是:42
Thread[三号窗口,5,main] 的号码是:43
Thread[二号窗口,5,main] 的号码是:44
Thread[一号窗口,5,main] 的号码是:45
Thread[四号窗口,5,main] 的号码是:45
Thread[三号窗口,5,main] 的号码是:46
Thread[二号窗口,5,main] 的号码是:47
Thread[一号窗口,5,main] 的号码是:48
Thread[四号窗口,5,main] 的号码是:49
Thread[三号窗口,5,main] 的号码是:50

Process finished with exit code 0
* */

        多次运行上述程序,每次都会有不一样的发现,总结起来主要有三个问题,如下所示:

  • 第一,某个号码被略过没有出现
  • 第二,某个号码被多次显示
  • 第三,号码超过了最大值

2.数据不一致问题原因分析

  1. 号码被略过:线程的执行是由CPU时间轮询调度的,假设线程1和2都执行到了index=65的位置,其中线程2将index修改为66之后未输出之前,CPU将执行权力交给了线程1,线程1直接将其累加到67,66则被忽略了。
  2. 号码重复出现:线程1执行index+1,然后CPU执行权落入线程2手里,由于线程1并没有给index赋计算后的结果393,线程2执行index+1的结果仍然是393,所以出现重复号码。
  3. 号码超过最大值:当index=最大值减1的时候,线程1和2都看到条件满足,线程2短暂停顿,线程1将index增加到最大值,线程2又将最大值再增加1,就出现了超过最大值的情况。

3.synchronized关键字

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

        synchronized提供了一种排他机制,也就是在同一时间只能有一个线程执行某些操作。

4.synchronized关键字的用法

        synchronized可以用于对代码块或方法进行修饰,而不能用于对class以及变量进行修饰。

4.1 同步方法

    public synchronized void sync() {
        ...
        ...
    }

    public synchronized static void staticSync() {
        ...
        ...
    }

4.2 同步代码块

    private final Object MUTEX = new Object();
    
    public void sync() {
        synchronized (MUTEX) {
            ...
            ...
        }
    }

4.3 使用synchronized关键字修改叫号程序

package com.jseeker.thread;

public class TicketWindowRunable implements Runnable {

    private int index = 1;

    private final static int MAX = 50;

    private final Object MUTEX = new Object();

    @Override
    public void run() {
        while (index <= MAX) {
            synchronized (MUTEX) {
                System.out.println(Thread.currentThread() + " 的号码是:" + (index++));
            }
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    public static void main(String[] args) {
        final TicketWindowRunable task = new TicketWindowRunable();

        Thread windowThead1 = new Thread(task, "一号窗口");

        Thread windowThead2 = new Thread(task, "二号窗口");

        Thread windowThead3 = new Thread(task, "三号窗口");

        Thread windowThead4 = new Thread(task, "四号窗口");

        windowThead1.start();
        windowThead2.start();
        windowThead3.start();
        windowThead4.start();
    }
}
/*
* 程序输出:
*
Thread[一号窗口,5,main] 的号码是:1
Thread[四号窗口,5,main] 的号码是:2
Thread[三号窗口,5,main] 的号码是:3
Thread[二号窗口,5,main] 的号码是:4
Thread[三号窗口,5,main] 的号码是:5
Thread[一号窗口,5,main] 的号码是:6
Thread[四号窗口,5,main] 的号码是:7
Thread[二号窗口,5,main] 的号码是:8
Thread[一号窗口,5,main] 的号码是:9
Thread[二号窗口,5,main] 的号码是:10
Thread[四号窗口,5,main] 的号码是:11
Thread[三号窗口,5,main] 的号码是:12
Thread[一号窗口,5,main] 的号码是:13
Thread[二号窗口,5,main] 的号码是:14
Thread[三号窗口,5,main] 的号码是:15
Thread[四号窗口,5,main] 的号码是:16
Thread[一号窗口,5,main] 的号码是:17
Thread[二号窗口,5,main] 的号码是:18
Thread[四号窗口,5,main] 的号码是:19
Thread[三号窗口,5,main] 的号码是:20
Thread[一号窗口,5,main] 的号码是:21
Thread[三号窗口,5,main] 的号码是:22
Thread[二号窗口,5,main] 的号码是:23
Thread[四号窗口,5,main] 的号码是:24
Thread[三号窗口,5,main] 的号码是:25
Thread[四号窗口,5,main] 的号码是:26
Thread[二号窗口,5,main] 的号码是:27
Thread[一号窗口,5,main] 的号码是:28
Thread[四号窗口,5,main] 的号码是:29
Thread[一号窗口,5,main] 的号码是:30
Thread[二号窗口,5,main] 的号码是:31
Thread[三号窗口,5,main] 的号码是:32
Thread[二号窗口,5,main] 的号码是:33
Thread[四号窗口,5,main] 的号码是:34
Thread[三号窗口,5,main] 的号码是:35
Thread[一号窗口,5,main] 的号码是:36
Thread[二号窗口,5,main] 的号码是:37
Thread[三号窗口,5,main] 的号码是:38
Thread[四号窗口,5,main] 的号码是:39
Thread[一号窗口,5,main] 的号码是:40
Thread[二号窗口,5,main] 的号码是:41
Thread[四号窗口,5,main] 的号码是:42
Thread[三号窗口,5,main] 的号码是:43
Thread[一号窗口,5,main] 的号码是:44
Thread[二号窗口,5,main] 的号码是:45
Thread[四号窗口,5,main] 的号码是:46
Thread[三号窗口,5,main] 的号码是:47
Thread[一号窗口,5,main] 的号码是:48
Thread[二号窗口,5,main] 的号码是:49
Thread[四号窗口,5,main] 的号码是:50

Process finished with exit code 0
* */

        上面的程序无论运行多少次,都不会出现数据不一致的问题。

4.4 使用synchronized需要注意的问题

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

    private final Object MUTEX = null;

    public void syncMethod() {
        synchronized (MUTEX) {
            //
        }
    }

        MUTEX为null,每一个对象和一个monitor关联,对象都为null了,monitor肯定无从谈起。

       <2> synchronized作用域太大

        由于synchronized关键字存在排他性,也就是说所有的线程必须串行地经过synchronized保护的共享区域,如果synchronized作用域越大,则代表着其效率越低,甚至还会丧失并发的优势,如下所示:

    public static class Task implements Runnable {
        @Override
        public synchronized void run() {
            //
        }
    }

        上面的代码对整个线程的执行逻辑单元都进行了synchronized同步,从而丧失了并发的能力,synchronized关键字应该尽可能地只作用于共享资源(数据)的读写作用域

       <3> 不同的monitor企图锁相同的方法

    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();
        }
    }

        上面的代码构造了5个线程,同时也构造了5个Runnable实例,Runnable作为线程逻辑执行单元传递给Thread,然后你讲发现,synchronized根本互斥不了与之对应的作用域,线程之间进行monitor lock的争抢只能发生在与monitor关联的同一个引用上,上面的代码每一个线程争抢的monitor关联引用都是彼此独立的,因此不可能起到互斥的作用

       <4> 多个锁的交叉导致死锁

         多个锁的交叉很容易引起线程出现死锁的情况,程序并没有任何错误输出,但就是不工作。

        private final Object MUTEX_READ = new Object();
        private final Object MUTEX_WRITE = new Object();

        public void read() {
            synchronized (MUTEX_READ) {
                synchronized (MUTEX_WRITE) {
                    //...
                }
            }
        }
        
        public void write() {
            synchronized (MUTEX_WRITE) {
                synchronized (MUTEX_READ) {
                    //...
                }
            }
        }

5.程序死锁的原因

  • 交叉锁可导致程序出现死锁
  • 内存不足
  • 一问一答式的数据交换
  • 数据库锁
  • 文件锁
  • 死循环引起的死锁

6.死锁诊断

        死锁诊断工具,可以使用:jstack、jconsole、jvisualvm或者jProfiler(收费)。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值