三、并发安全

线程间的共享

一、synchronized 内置锁

Java语言的关键字
作用:多个线程在同一时刻只能有一个线程进入这个方法或者代码块中。可以保证线程对于变量或者属性的原子性和可见性、排他性。
用处:作用于代码块或者方法上,进行修饰
synchronized关键字本质上是把对象做了一把锁,在代码块中需要对当前对象进行加锁,在方法上加锁缺省是对这个类的当前实例加锁,所以synchronized加的锁不是在方法上也不是在代码块上,本质上是在类的当前实例上。

加锁

synchronized关键字锁的是对象,锁的对象不同,线程就可以并行地执行

二、对象锁

首先先来运行两段代码

代码1
package com.tinner.thread;

/**
 * @Author Tinner
 * @create 2019/9/20 17:33
 */
public class DiffObj {
    private static class Obj1 implements Runnable{

        private DiffObj diffObj;

        public Obj1(DiffObj diffObj) {
            this.diffObj = diffObj;
        }

        @Override
        public void run() {
            System.out.println("TestObj1 is running ...." + diffObj);
            diffObj.instance();
        }
    }

    private static class Obj2 implements Runnable{

        private DiffObj diffObj;

        public Obj2(DiffObj diffObj) {
            this.diffObj = diffObj;
        }

        @Override
        public void run() {
            System.out.println("TestObj2 is running ...." + diffObj);
            diffObj.instance2();
        }
    }

    private synchronized void instance(){
        try {
            Thread.sleep(3000);
            System.out.println("synInstance1 is going..." + this.toString());
            Thread.sleep(3000);
            System.out.println("synInstance1 ended + " + this.toString());
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

    }

    private synchronized void instance2(){
        try {
            Thread.sleep(3000);
            System.out.println("synInstance2 is going..." + this.toString());
            Thread.sleep(3000);
            System.out.println("synInstance2 ended + " + this.toString());
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    public static void main(String[] args) throws InterruptedException {
        DiffObj instance1 = new DiffObj();
        Thread t3 = new Thread(new Obj2(instance1));
        DiffObj instance2 = new DiffObj();
        Thread t4 = new Thread(new Obj1(instance1));
        //Thread t4 = new Thread(new Obj1(instance2));
        t3.start();
        t4.start();
        Thread.sleep(1000);
    }
}

运行结果1
同一实例
代码2

修改main方法

public static void main(String[] args) throws InterruptedException {
        DiffObj instance1 = new DiffObj();
        Thread t3 = new Thread(new Obj2(instance1));
        DiffObj instance2 = new DiffObj();
        //Thread t4 = new Thread(new Obj1(instance1));
        Thread t4 = new Thread(new Obj1(instance2));
        t3.start();
        t4.start();
        Thread.sleep(1000);
    }
运行结果2
不同实例
总结

比较两个运行结果,可以发现第一个方式是第一个线程执行完毕之后才执行第二个线程;第二个方式基本上是两个线程同时开始同时结束的。锁的实例不一样,也是可以并行的,只有两个线程锁的对象的实例是相同的时候,才能达到synchronized 那种效果。

三、类锁

将synchronized关键字加在一个类的static方法上的时候,才算是一个类锁。
类锁和对象锁之间还是可以相互的并行执行的。

代码
/**
 *类说明:演示实例锁和类锁是不同的,两者可以并行
 */
public class InstanceAndClass {
    
    private static class SynClass extends Thread{
        @Override
        public void run() {
            System.out.println("TestClass is running...");
            synClass();
        }
    }

    private static class ObjSyn implements Runnable{
        private InstanceAndClass SynClassAndInstance;

        public ObjSyn(InstanceAndClass SynClassAndInstance) {
            this.SynClassAndInstance = SynClassAndInstance;
        }

        @Override
        public void run() {
            System.out.println("TestInstance is running..."+SynClassAndInstance);
            SynClassAndInstance.instance();
        }
    }

    //实例方法
    private synchronized void instance(){
        SleepTools.second(1);
        System.out.println("synInstance is going..."+this.toString());
        SleepTools.second(1);
        System.out.println("synInstance ended "+this.toString());
    }

    //静态方法
    private static synchronized void synClass(){
        SleepTools.second(1);
        System.out.println("synClass going...");
        SleepTools.second(1);
        System.out.println("synClass end");
    }

    public static void main(String[] args) {
        InstanceAndClass synClassAndInstance = new InstanceAndClass();
        Thread t1 = new SynClass();
        Thread t2 = new Thread(new ObjSyn(synClassAndInstance));
        t2.start();
        SleepTools.second(1);
        t1.start();
    }
}
运行结果
类锁

可以发现,这两个进程是可以并行执行的。

注意:

synchronized关键字只能锁对象,但是在类锁中,synchronized关键字明明用到了static方面,那么它锁的是这个class对象。当我们需要创建一个对象的实例的时候,虚拟机会进行一个类加载的过程,每一个类在虚拟机里面都有一个唯一的class对象。synchronized本质上锁的是每个类所独有的class对象。
那么就可以知道,instance方法锁的是实例对象,synClass方法锁的是类的对象,本质上他们锁的也是两个完全不同的对象,所以可以并行执行。

结论

从严格意义上来讲,类锁只是一个概念上的东西,并不是真实存在的。本质上锁的是类的class对象。而且类锁和对象锁之间也是互不干扰的。

四、volatile关键字,最轻量的同步机制

保证了变量的可见性。但是并没有提供变量的原子性,不能够保证复杂计算的时候数据的正确性。

代码1
/**
 * 类说明:演示Volatile的提供的可见性
 */
public class VolatileCase {
    private static boolean ready;
    private static int number;

    //
    private static class PrintThread extends Thread{
        @Override
        public void run() {
            System.out.println("PrintThread is running.......");
            while(!ready);//无限循环
            System.out.println("number = "+number);
        }
    }

    public static void main(String[] args) {
        new PrintThread().start();
        SleepTools.second(1);
        number = 51;
        ready = true;
        SleepTools.second(5);
        System.out.println("main is ended!");
    }
}

运行这段代码,可以看到内存飙升,程序根本停不下来,原因就是主方法中修改了ready变量的值之后,在线程中检测不到ready的变化,所以程序会一直运行下去

稍作修改

当我们给ready变量加一个volatile关键字之后

private static volatile boolean ready;

可以看到系统正常运行了,正常停止


volatile关键字作用
题外话

当我们不加volatile关键字,而是在无限循环中去加入一条打印语句的时候,看代码:

public class VolatileCase {
    private static boolean ready;
    private static int number;

    //
    private static class PrintThread extends Thread{
        @Override
        public void run() {
            System.out.println("PrintThread is running.......");
            while(!ready){
                //无限循环
                System.out.println("jinping");

            }
            System.out.println("number = "+number);
        }
    }

    public static void main(String[] args) {
        new PrintThread().start();
        SleepTools.second(1);
        number = 51;
        ready = true;
        SleepTools.second(5);
        System.out.println("main is ended!");
    }
}
运行结果

i题外话运行结果

可以看到程序正常的停止了。
为什么会出现这种现象呢?我并没有加volatile关键字啊
我们仅仅在无限循环中加了一个普通的打印语句,这个打印语句中牵涉到了synchronized关键字的内存语义。
image.png

说起来就要扯到JMM的内存模型中去了,synchronized关键字在内存语义上面强制要求把共享变量刷回到主内存,以及强制将使用的这个变量读到当前工作的内存上去。

最常见的适用场景:一个线程写,多个线程读

五、什么是线程安全?怎么才能做到线程安全?

什么是线程安全
如果说有多个线程访问同一个类的实例的时候,不管运行环境如何,我们的类的实例都能表现出正确的预期结果及行为,这就是线程安全。
实现线程安全的方式
其实本质上线程安全就是解决“修改--共享变量”的问题

  • 栈封闭(线程封闭)
    栈封闭就是让变量或者实例变得不可共享。在我们运行任何一个线程的时候,JDK都会为每一个线程分配一个栈,程序计数器是每一个线程所独有的,其他线程看不到。堆和方法区是线程之间共享的。既然栈是每个线程所独有的,那么将栈封闭,里面的变量就不会被共享。比如:局部变量。
    实际开发过程中,多使用局部变量,少使用全局变量。
  • 无状态的类
    Java语言中,没有成员变量的类,就是无状态的类,只有方法。那么这种类就是线程安全的。
  • 让类不可变
    让这个类的所有的属性加final关键字。但是如果这个类中的成员变量中是个对象的话,这个属性还是不安全的。因为虽然final关键字修饰了实体类,但是它修饰的只是这个实体类的引用(修饰了之后引用不可变)。但是不代表这个类在堆上的实例的内容不可变。
    不可变类

    还有一种只提供读的方法,不提供写的方法,外面写不动。
/**
 * 类不可变--事实不可变
 */
public class ImmutableClassToo {
    private final List<Integer> list = Arrays.asList(1,2,3);

    public boolean isContain(int i){
        return list.contains(i);
    }
}
  • volatile
    如果只是简单的set和get方法,可以使用该关键字。但是如果是++等操作,不适用。
  • 加锁和CAS
  • 安全的发布
    写下代码自己体会
/**
 * 不安全的发布
 */
public class UnsafePublish {
    private List<Integer> list = new ArrayList<>(3);
    
    public UnsafePublish() {
        list.add(1);
        list.add(2);
        list.add(3);
    }
    
    public List getList() {
        return list;
    }

    public static void main(String[] args) {
        UnsafePublish unSafePublish = new UnsafePublish();
        List<Integer> list = unSafePublish.getList();
        System.out.println(list);
        list.add(4);
        System.out.println(list);
        System.out.println(unSafePublish.getList());
    }
}

/**
 * 安全的发布
 */
public class SafePublishToo {
    private List<Object> list
            = Collections.synchronizedList(new ArrayList<>(3));

    public SafePublishToo() {
        list.add(1);
        list.add(2);
        list.add(3);
    }

    public List getList() {
        return list;
    }

    public static void main(String[] args) {
        SafePublishToo safePublishToo = new SafePublishToo();
        List<Integer> list = safePublishToo.getList();
        System.out.println(list);
        list.add(4);
        System.out.println(list);
        System.out.println(safePublishToo.getList());
    }
}

Collections.synchronizedList()包装好的线程安全的。
自己封装

/**
 * 仿Collections对容器的包装,将内部成员对象进行线程安全包装
 */
public class SoftPublicUser {
    private final UserVo user;

    public UserVo getUser() {
        return user;
    }

    public SoftPublicUser(UserVo user) {
        this.user = new SynUser(user);
    }

    private static class SynUser extends UserVo{
        private final UserVo userVo;

        private final Object lock = new Object();

        public SynUser(UserVo userVo){
            this.userVo = userVo;
        }

        public int getAge() {
            synchronized (lock){
                return userVo.getAge();
            }
        }

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

推荐阅读更多精彩内容