三、并发安全

作者: 一直想上树的猪 | 来源:发表于2019-10-10 14:29 被阅读0次

线程间的共享

一、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(线程封闭)

相关文章

  • 三、并发安全

    线程间的共享 一、synchronized 内置锁 Java语言的关键字作用:多个线程在同一时刻只能有一个线程进入...

  • golang_并发安全: slice和map并发不安全及解决方法

    并发安全 并发安全也叫线程安全,在并发中出现了数据的丢失,称为并发不安全 map和slice都是并发不安全的 切片...

  • JAVA并发编程与高并发解决方案 - 并发编程 三

    JAVA并发编程与高并发解决方案 - 并发编程 三 版本作者内容2018.5.17chuIllusions线程安全...

  • golang并发map记录

    1.普通map+mutex,实现map的并发安全读写 2.sync.Map 3.第三方并发安全map库

  • JVM之内存模型

    如何保证并发安全 我们说,并发编程,为了保证数据的安全,需要满足以下三个特性: 原子性是指在一个操作中就是cpu不...

  • know un known

    1 线程安全与不安全的区别 2 并发与并行,c#的高并发解决方案有哪些 3 并发还分为io并发和cpu并发 4 长...

  • java并发(二)线程安全性与Java内存模型

    谈到并发,首先要提到的就是安全,所有的并发编程的前提都是安全。在java中, 什么是线程安全性?在多线程并发访问一...

  • 并发安全

    在并发的情况下使用和调度一个类,这个类总是能表现出正确的行为,那么我们就说这个类是并发安全的类。 类线程安全的表现...

  • 线程安全的List:CopyOnWriteArrayList

    线程安全的List:CopyOnWriteArrayList 并发包中的并发List只有CopyOnWriteAr...

  • Go sync.Map

    map并发读线程安全,并发读写线程不安全。 sync.Map 读写分离 空间换时间 Map Golang1.6之前...

网友评论

    本文标题:三、并发安全

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