线程安全

线程安全定义

线程安全是一个非常重要的话题。Java通过 Thread 提供多线程环境,从相同的Object共享对象变量创建的多个线程,当线程用于读取和更新共享数据时,这可能导致数据不一致。

线程安全

数据不一致的原因是因为更新任何字段值不是原子过程,它需要三个步骤;

  • 首先读取当前值
  • 第二个读取必要的操作以获取更新的值
  • 第三个将更新的值分配给字段引用
package coreofjava.javathread.threadsafety;

public class ThreadSafety {
    public static void main(String[] args) throws InterruptedException {
        ProcessingThread pt = new ProcessingThread();
        Thread t1 = new Thread(pt, "t1");
        t1.start();
        Thread t2 = new Thread(pt, "t2");
        t2.start();

        // 等待线程结束
        t1.join();
        t2.join();
        System.out.println("正在处理 count = " + pt.getCount());
    }
}

class ProcessingThread implements Runnable {
    private int count;


    @Override
    public void run() {
        for (int i = 1; i < 5; i++) {
            processSomething(i);
            count++;
        }
    }

    public int getCount() {
        return this.count;
    }

    private void processSomething(int i) {
        try {
            Thread.sleep(i * 1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

上述例子中,count增加了四次,有两个线程,所以值应该是8,但实际中是6或7或8

count++导致了数据损坏

让线程安全

  • 同步是 java 中最简单和最广泛使用的线程安全工具
  • 使用java.util.concurrent.atomic包中的Atomic Wrapper类,如AtomicInteger
  • 使用java.util.locks包中的锁
  • 使用线程安全集合类
  • 使用带有变量的volatile关键字使每个线程从内存中读取数据,而不是从线程缓存中读取

Java 同步

JVM保证同步代码一次只能由一个线程执行

synchronized 用于创建同步,内部使用Object、Class上的锁来确保只有一个线程正在执行同步代码

  • 同步在锁定或解锁资源时起作用,任何线程进入同步代码前,必须获取对象的锁定,当代码执行结束时,解锁可以被其他线程锁定的资源。同时,其他线程处于等待状态以锁定同步资源
  • 2种方式使用synchronized, 一种是一个完整的方法同步,二是创建 synchronized 块
  • 方法同步时,会锁定 Object,若方法是静态的,会锁定Class,因此最好使用synchronized块来锁定需要同步的方法的位移部分。
  • 在创建synchronized块时,需要提供将获取锁的资源,可以是任意类或类的任意字段
  • synchronized(this) 将在进入同步块之前锁定对象
  • 应该使用最低级别的锁定,若有多个 synchronized块,并且其中一个锁了Object,则其他同步块也将无法由其他线程执行,当锁定一个Object,它会获取Object的所有字段。
  • Java同步提供了性能成本的数据完整性,因此因此只有在绝对必要时才使用
  • Java同步仅在同一个 JVM中工作,如果需要在多个JVM中锁定某些资源,将无法工作,可能需要处理一些全局锁定机制
  • Java同步可能会导致死锁
  • Java synchronized 关键字不能用于构造函数和变量
  • 最好创建一个用于同步块的虚拟私有对象,它的引用就不能被任何其他代码更改,若过正在同步Object setter方法,则可以通过某些其他代码更改其引用,并执行 synchronized 块
  • 不应该使用包含常量池的对象。 例如,String 不应该被用在同步方法中,如果任何其他代码给String加了锁,尽管两个代码不相关,它将要求要求锁住来自String池同一个引用对象,这将导致互相锁住。

下面代码我们需要进行改进来使得线程安全

package coreofjava.javathread.threadsafety;

public class HackerCode {
    public synchronized void doSomething() {
        System.out.println("Doing");
    }

    public static void main(String[] args) {
        while (true) {
            try {
                Thread.sleep(Integer.MAX_VALUE);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

正在尝试锁定 myObject 示例,一旦获得锁定,就永远不会释放它,这导致doSomething() 方法在等待锁定时阻塞,这将导致系统死锁并导致拒绝服务 (Dos)

Not safe example

package coreofjava.javathread.threadsafety;

import java.util.Arrays;

public class NotSafe {
    public static void main(String[] args) throws InterruptedException {
        String[] arr = {"1", "2", "3", "4", "5", "6"};
        HashMapProcessor hmp = new HashMapProcessor(arr);
        Thread t1 = new Thread(hmp, "t1");
        Thread t2 = new Thread(hmp, "t2");
        Thread t3 = new Thread(hmp, "t3");
        long start = System.currentTimeMillis();

        t1.start();
        t2.start();
        t3.start();

        t1.join();
        t2.join();
        t3.join();

        System.out.println("Time taken = " + (System.currentTimeMillis()-start));

        System.out.println(Arrays.asList(hmp.getMap()));
    }
}

class HashMapProcessor implements Runnable {

    private String[] strArr = null;

    public HashMapProcessor(String[] m) {
        this.strArr = m;
    }

    public String[] getMap() {
        return strArr;
    }

    @Override
    public void run() {
        processArr(Thread.currentThread().getName());
    }

    private void processArr(String name) {
        for (int i = 0; i < strArr.length; i++) {
            processSomething(i);
            addThreadName(i, name);
        }
    }

    private void addThreadName(int i, String name) {
        strArr[i] = strArr[i] + ":" + name;
    }

    private void processSomething(int index) {
        try {
            Thread.sleep(index * 1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

程序输出

Time taken = 15004
[1:t1, 2:t1:t3:t2, 3:t1, 4:t3, 5:t1:t3, 6:t1:t2:t3]

为何不是 1:t1:t2:t3, 2:t1,t2,t3, ...., 而是以上结果?

这是因为String数组值因共享数据而没有同步导致的损坏,我们将更改代码将使得线程安全

改进代码如下

package coreofjava.javathread.threadsafety;


import java.util.Arrays;

public class Safe extends NotSafe {
    public static void main(String[] args) throws InterruptedException {
        String[] arr = {"1", "2", "3", "4", "5", "6"};
        SafeHashMapProcessor hmp = new SafeHashMapProcessor(arr);
        Thread t1 = new Thread(hmp, "t1");
        Thread t2 = new Thread(hmp, "t2");
        Thread t3 = new Thread(hmp, "t3");
        long start = System.currentTimeMillis();

        t1.start();
        t2.start();
        t3.start();

        t1.join();
        t2.join();
        t3.join();

        System.out.println("Time taken = " + (System.currentTimeMillis()-start));

        System.out.println(Arrays.asList(hmp.getMap()));
    }
}

class SafeHashMapProcessor implements Runnable{

    private final Object lock = new Object();

    private String[] strArr = null;

    SafeHashMapProcessor(String[] m) {
        this.strArr = m;
    }


    @Override
    public void run() {
        processArr(Thread.currentThread().getName());
    }

    public String[] getMap() {
        return strArr;
    }

    private void processArr(String name) {
        for (int i = 0; i < strArr.length; i++) {
            processSomething(i);
            addThreadName(i, name);
        }
    }

    private void processSomething(int index) {
        try {
            Thread.sleep(index * 1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    private void addThreadName(int i, String name) {
        synchronized (lock) {
            strArr[i] = strArr[i] + ":" + name;
        }
    }
}

通过改进,程序运行输出为

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

推荐阅读更多精彩内容