多线程(二)线程安全篇

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

为什么:

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

  • 多线程安全问题原因要从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实现。具体使用将在后面单独讲一篇。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,125评论 6 498
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,293评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 162,054评论 0 351
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,077评论 1 291
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,096评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,062评论 1 295
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,988评论 3 417
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,817评论 0 273
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,266评论 1 310
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,486评论 2 331
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,646评论 1 347
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,375评论 5 342
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,974评论 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,621评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,796评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,642评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,538评论 2 352