美文网首页
多线程(二)线程安全篇

多线程(二)线程安全篇

作者: 丶行一 | 来源:发表于2019-11-19 11:16 被阅读0次

上一篇讲到多线程如何使用,多线程使用时特别应该注意的是线程安全问题,本篇将专门讲述问题原因和解决方案

为什么:

为什么出现多线程安全问题

  • 多线程安全问题原因要从jmm(java内存模型,非jvm模型)讲起。jmm简易模型如下图,

jmm模型.jpg
  • 分为主内存和线程工作内存,多个线程使用共享变量时,都是先从主内存中拷贝到工作内存,使用完成之后如果有写入操作则再写入主内存。即线程A与线程B要使用共享变量c,都是从主内存中拷贝一份副本到自己的工作内存中,改后再将变更修改回主内存,存在A线程修改变量C后,B线程不清楚C的修改,用的仍是C的副本,导致B完成后再次改变变量C,把A线程对变量的改动覆盖了。或者B没读到A对C的修改。这就存在数据不一致的事,A完成的任务又被B覆盖了。这就是多线程并发使用共享变量时的不安全问题。

什么时候出现多线程安全问题

  • 多线程并发访问共享资源引起,即多个线程同时读写相同的资源或者共享变量

怎么办:如何解决多线程安全的问题

多线程安全的三个特性

  • 原子性:即在执行一个或者多个操作的过程中,要么一起成功要么一起失败。典型的i++就是非原子操作,分三步,取出i,i+1,再把结果赋给i。中途存在i在取出后再次赋值前,存在被其他线程修改的可能性。示例见下面demo,每次运行结果都不同,存在线程安全问题。
@Slf4j
public class ThreadNotSafeDemo {
    public static void main(String[] args) throws Exception{
        JobAdd jobAdd=new JobAdd();
        for(int i=0;i<10;i++){
            new Thread(jobAdd,"thread "+i).start();
        }
        Thread.sleep(1000);
        log.debug("thread is {} ,total is {}",Thread.currentThread().getName(),jobAdd.getTotal());
    }
}
@Slf4j
@Data
class JobAdd implements Runnable{
    private int total=0;
    @Override
    public void run() {
        for (int i=0;i<100;i++){
            try {
                // 必须加简单的sleep,否则可能当前线程在下一个线程启动前就跑完了,演示不出效果
                Thread.sleep(10l);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            total++;
        }
        log.debug("thread is {} ,total is {}",Thread.currentThread().getName(),total);
    }
}

打印结果如下:
05:56:18.109 [thread 1] DEBUG com.dz.demo.multiThread.JobAdd - thread is thread 1 ,total is 912
05:56:18.119 [thread 4] DEBUG com.dz.demo.multiThread.JobAdd - thread is thread 4 ,total is 919
05:56:18.119 [thread 9] DEBUG com.dz.demo.multiThread.JobAdd - thread is thread 9 ,total is 915
05:56:18.119 [thread 8] DEBUG com.dz.demo.multiThread.JobAdd - thread is thread 8 ,total is 917
05:56:18.119 [thread 0] DEBUG com.dz.demo.multiThread.JobAdd - thread is thread 0 ,total is 919
05:56:18.132 [thread 5] DEBUG com.dz.demo.multiThread.JobAdd - thread is thread 5 ,total is 923
05:56:18.132 [thread 3] DEBUG com.dz.demo.multiThread.JobAdd - thread is thread 3 ,total is 924
05:56:18.132 [thread 6] DEBUG com.dz.demo.multiThread.JobAdd - thread is thread 6 ,total is 924
05:56:18.132 [thread 2] DEBUG com.dz.demo.multiThread.JobAdd - thread is thread 2 ,total is 925
05:56:18.132 [thread 7] DEBUG com.dz.demo.multiThread.JobAdd - thread is thread 7 ,total is 923
05:56:18.993 [main] DEBUG com.dz.demo.multiThread.ThreadNotSafeDemo - thread is main ,total is 925

  • 可见性:可见性是指当一个线程对共享变量进行修改后,能立刻被其他正在使用该变量的线程感知,包含两步:对变量修改后立马同步回主内存;使其他线程的该共享变量的副本值失效,必须重新从主内存中获取。
  • 有序性:一般情况下,处理器为了提高运行效率,在不影响本线程的前提下会对指令的执行顺序进行重排序,代码运行顺序可能与编写的顺序不一致。但是对于多线程情况下,就容易出现问题。当前线程指令执行先后对其他线程产生影响,这就是无序性。

要实现线程安全的几个方案

  • 最简单的不使用或者慎重使用共享变量或者共享状态:不使用就不存在多线程并发访问共享变量安全问题
  • 使用jdk已有的线程安全的api,包括以下几种:java.util.concurrent.atomic包下的原子类如AtomicBoolean、AtomicInteger、AtomicIntegerArray、AtomicLong、DoubleAdder等;可变字符串StringBuffer;java并发包下线程安全的集合,如ConcurrentHashMap、CopyOnWriteArrayList、CopyOnWriteArraySet、ConcurrentSkipListMap、BlockingQueue的实现类;一些老的线程安全集合如Hashtable,不过不推荐,性能较低
  • 使用jdk自带的同步关键字synchronized,它可以用在方法和代码块上。使用在方法上是取该对象的监视器为同步对象。使用在代码块上则是取synchronized括号里的对象的监视器为同步对象,如果使用静态方法上,则是取该类对象的监视器为同步对象。synchronized同时是可重入的同步,即在同一线程中可以在释放锁前多次获取锁。将上面例子改进下为:
@Slf4j
public class SynchronizedUseDemo {
    public static void main(String[] args) throws Exception{
        SafeJobAdd jobAdd=new SafeJobAdd();
        for(int i=0;i<10;i++){
            new Thread(jobAdd,"thread "+i).start();
        }
        Thread.sleep(3000l);
        log.debug("thread is {} ,total is {}",Thread.currentThread().getName(),jobAdd.getTotal());
    }
}

@Slf4j
@Data
class SafeJobAdd implements Runnable{
    private int total=0;
    @Override
    public void run() {
        // 使用了同步关键字synchronized
        synchronized (this){
            for (int i=0;i<100;i++){
                try {
                    Thread.sleep(2l);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                total++;
            }
            log.debug("thread is {} ,total is {}",Thread.currentThread().getName(),total);
        }

    }
}
打印结果是:
06:36:35.800 [thread 0] DEBUG com.dz.demo.multiThread.SafeJobAdd - thread is thread 0 ,total is 100
06:36:36.042 [thread 9] DEBUG com.dz.demo.multiThread.SafeJobAdd - thread is thread 9 ,total is 200
06:36:36.290 [thread 8] DEBUG com.dz.demo.multiThread.SafeJobAdd - thread is thread 8 ,total is 300
06:36:36.539 [thread 7] DEBUG com.dz.demo.multiThread.SafeJobAdd - thread is thread 7 ,total is 400
06:36:36.787 [thread 6] DEBUG com.dz.demo.multiThread.SafeJobAdd - thread is thread 6 ,total is 500
06:36:37.040 [thread 5] DEBUG com.dz.demo.multiThread.SafeJobAdd - thread is thread 5 ,total is 600
06:36:37.292 [thread 4] DEBUG com.dz.demo.multiThread.SafeJobAdd - thread is thread 4 ,total is 700
06:36:37.541 [thread 3] DEBUG com.dz.demo.multiThread.SafeJobAdd - thread is thread 3 ,total is 800
06:36:37.793 [thread 2] DEBUG com.dz.demo.multiThread.SafeJobAdd - thread is thread 2 ,total is 900
06:36:38.043 [thread 1] DEBUG com.dz.demo.multiThread.SafeJobAdd - thread is thread 1 ,total is 1000
06:36:38.554 [main] DEBUG com.dz.demo.multiThread.SynchronizedUseDemo - thread is main ,total is 1000

  • 使用jdk的自带的锁相关api;如ReentrantLock(可重入锁)、ReentrantReadWriteLock.ReadLock(可重入的读写锁之读锁)、ReentrantReadWriteLock.WriteLock(可重入锁之写锁)。这几个锁都是基于jdk的同步框架AbstractQueuedSynchronizer实现的,具体可查看jdk源码。也可用AbstractQueuedSynchronizer实现自定义的同步锁。在代码块中使用lock,注意在finnaly中释放锁。实例如下:
@Slf4j
public class LockUseDemo {
    public static void main(String[] args) throws Exception{
        LockJobAdd jobAdd=new LockJobAdd();
        for(int i=0;i<10;i++){
            new Thread(jobAdd,"thread "+i).start();
        }
        Thread.sleep(3000l);
        log.debug("thread is {} ,total is {}",Thread.currentThread().getName(),jobAdd.getTotal());
    }
}

@Slf4j
@Data
class LockJobAdd implements Runnable{
    private int total=0;
    private ReentrantLock lock=new ReentrantLock();
    @Override
    public void run() {
            for (int i=0;i<100;i++){
                try {
                    Thread.sleep(2l);
                    lock.lock();
                    total++;
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                finally {
                    lock.unlock();
                }
            }
            log.debug("thread is {} ,total is {}",Thread.currentThread().getName(),total);
    }
}
打印如下:
06:54:22.861 [thread 7] DEBUG com.dz.demo.multiThread.LockJobAdd - thread is thread 7 ,total is 994
06:54:22.862 [thread 4] DEBUG com.dz.demo.multiThread.LockJobAdd - thread is thread 4 ,total is 997
06:54:22.861 [thread 9] DEBUG com.dz.demo.multiThread.LockJobAdd - thread is thread 9 ,total is 994
06:54:22.861 [thread 8] DEBUG com.dz.demo.multiThread.LockJobAdd - thread is thread 8 ,total is 994
06:54:22.862 [thread 3] DEBUG com.dz.demo.multiThread.LockJobAdd - thread is thread 3 ,total is 999
06:54:22.861 [thread 5] DEBUG com.dz.demo.multiThread.LockJobAdd - thread is thread 5 ,total is 994
06:54:22.862 [thread 0] DEBUG com.dz.demo.multiThread.LockJobAdd - thread is thread 0 ,total is 997
06:54:22.861 [thread 1] DEBUG com.dz.demo.multiThread.LockJobAdd - thread is thread 1 ,total is 998
06:54:22.861 [thread 2] DEBUG com.dz.demo.multiThread.LockJobAdd - thread is thread 2 ,total is 994
06:54:22.862 [thread 6] DEBUG com.dz.demo.multiThread.LockJobAdd - thread is thread 6 ,total is 1000
06:54:25.633 [main] DEBUG com.dz.demo.multiThread.LockUseDemo - thread is main ,total is 1000
  • 使用volatile关键字修饰共享变量。volatile关键字并没有实现lock或者synchronized关键字的完整的同步作用,只是保证了可见性与有序性。在写入是原子性操作或者写入时线程安全时,用volatile关键字,实现比synchronized性能更高一点的线程安全。这个文章讲的比较详细volatile,特此引用。正确使用Volatile变量

  • 分布式环境下的共享资源的安全问题不属于多线程概念内的,是多实例多线程共同使用共享资源引起的,但是也是存在类似的问题。一般的解决方案,是用锁的办法来实现,使用数据库实现乐观锁比较版本号,或者使用redis 的setnx操作或者使用zookeeper实现。具体使用将在后面单独讲一篇。

相关文章

  • 多线程(二)线程安全篇

    上一篇讲到多线程如何使用,多线程使用时特别应该注意的是线程安全问题,本篇将专门讲述问题原因和解决方案 为什么: 为...

  • 多线程介绍

    一、进程与线程 进程介绍 线程介绍 线程的串行 二、多线程 多线程介绍 多线程原理 多线程的优缺点 多线程优点: ...

  • iOS多线程-安全篇

    多个线程访问同一块资源的时候,很容易引发数据混乱问题。看下面的例子,3个线程分别买票7次,看下面打印结果 正常情况...

  • Python-day-18多线程

    1、多线程技术1 二、多线程技术2 三、多线程应用 四、jion函数

  • iOS - 多线程(一):初识

    iOS - 多线程 系列文章 iOS - 多线程(一):初识iOS - 多线程(二):pthread、NSThre...

  • iOS - 多线程(二):pthread、NSThread

    iOS - 多线程 系列文章 iOS - 多线程(一):初识iOS - 多线程(二):pthread、NSThre...

  • iOS - 多线程(四):NSOperation

    iOS - 多线程 系列文章 iOS - 多线程(一):初识iOS - 多线程(二):pthread、NSThre...

  • iOS - 多线程(三):GCD

    iOS - 多线程 系列文章 iOS - 多线程(一):初识iOS - 多线程(二):pthread、NSThre...

  • 多线程编程

    多线程编程之Linux环境下的多线程(一)多线程编程之Linux环境下的多线程(二)多线程编程之Linux环境下的...

  • 带你搞懂Java多线程(五)

    带你搞懂Java多线程(一)带你搞懂Java多线程(二)带你搞懂Java多线程(三)带你搞懂Java多线程(四) ...

网友评论

      本文标题:多线程(二)线程安全篇

      本文链接:https://www.haomeiwen.com/subject/gmrwictx.html