同步与线程安全

什么是线程安全?

多个线程共享同一个全局变量或静态变量,在做写操作的时候,可能会受其他线程干扰,导致数据有问题,这种现象就是线程安全问题。在读的时候不会出现这种情况。

如何保证线程安全

使用线程同步:
synchronized:自动锁
lock: jdk1.5里面的,手动上锁,手动释放锁

同步-synchronized

可以理解为在执行完一个函数或方法之后,一直等待系统返回值或消息,这时程序是出于阻塞的,只有接收到返回的值或消息后才往下执行其他的命令。 即按顺序执行,必须一个执行完另一个才能执行。

同步就是共享,如果不是共享的资源,就没有必要进行同步。

同步与线程安全

同步的目的就是为了线程安全,其实对于线程安全来说,至少需要满足两个特性:

	原子性(同步)

	可见性
原子性

原子操作指的是不可再分的指令操作,即在执行原子操作时不可能被打断,要么原子操作没有执行,要么已经执行完毕。

比如:a+=b 可以分为三步:

取出a和b

计算a+b

写入内存

如果有两个线程t1,t2在进行这样的操作。t1在第二步做完之后还没来得及把数据写回内存就被线程调度器中断了,于是t2开始执行,t2执行完毕后t1又把没有完成的第三步做完。这个时候就出现了错误,相当于t2的计算结果被无视掉了。

这时就不具备原子性

可见性

可见性volatile修饰词,可以应对多线程同时访问修改同一变量,由于相互的不可见性所带来的不可预期的结果,存在二义性的现象,出现的。

多线程变量不可见:当一个线程对一变量a修改后,还没有来得及将修改后的a值回写到主存,而被线程调度器中断操作(或收回时间片),然后让另一线程进行对a变量的访问修改,这时候,后来的线程并不知道a值已经修改过,它使用的仍旧是修改之前的a值,这样修改后的a值就被另一线程覆盖掉了。

多线程变量可见:被volatile修饰的成员变量在每次被线程访问时,都强迫从内存中重读该成员变量的值;而且,当成员变量发生变化时,强迫线程将变化值回写到共享内存,这样在任何时刻两个不同线程总是看到某一成员变量的同一个值,这就是保证了可见性。

volatile使用场景

在两个或者更多的线程访问的成员变量上使用volatile。当要访问的变量已在synchronized代码块中,或者为常量时,不必使用。

由于使用volatile屏蔽掉了JVM中必要的代码优化,所以在效率上比较低,因此一定在必要时才使用此关键字。

注:

如果给一个变量加上volatile修饰符,就相当于:每一个线程中一旦这个值发生了变化就马上刷新回主存,使得各个线程取出的值相同。编译器不要对这个变量的读、写操作做优化。但是值得注意的是,除了对long和double的简单操作之外,volatile并不能提供原子性。所以,就算你将一个变量修饰为volatile,但是对这个变量的操作并不是原子的,在并发环境下,还是不能避免错误的发生!

除了上面两个,多线程还有 有序性

有序性

可以用join,wait,notfi来设计线程执行优先顺序

异步-asynchronized

异步就是独立,相互之间不受到任何制约

synchronized 加锁

必要条件
1、必须要有两个线程及以上,需要发生同步

2、多个线程同步必须用同一把锁

3、保证只有一个线程进行执行
锁的原理
1、一个线程已经拿到锁,得到cpu执行权的线程会进行等待,等待该线程释放锁。

2、当代码执行完毕或抛出异常时会释放锁。

3、锁释放后其他线程会取锁进入同步区
缺点
1、由于锁的资源竞争,效率会非常低

2、有时会出现死锁
synchronized – 同步代码块

Object锁

public class TheadTest implements Runnable {
    private Object obj = new Object();
    private int count = 100;

    @Override
    public void run() {
        synchronized (obj) {
            while (count > 0) {
                    System.out.println(Thread.currentThread().getName() + " " + (100-count+1));
                    count--;
            }
        }
    }
}

对于上面代码,创建了一个Object对象,synchronized (obj) 就是给Object上锁,线程之间争取这个资源。

this锁

public class TheadTest implements Runnable {
    private int count = 100;

    @Override
    public void run() {
        synchronized (this) {
            while (count > 0) {
                    System.out.println(Thread.currentThread().getName() + " " + (100-count+1));
                    count--;
            }
        }
    }
}

上面是对this上锁,this代表对象实例,所以它锁的就是对象实例,他和方法锁就是换个形式,下面会详细证明。

class锁

public class TheadTest implements Runnable {
    private int count = 100;

    @Override
    public void run() {
        synchronized (TheadTest.class) {
            while (count > 0) {
                    System.out.println(Thread.currentThread().getName() + " " + (100-count+1));
                    count--;
            }
        }
    }
}

class锁,锁的是该类的字节码文件。

synchronized – 同步方法

同步方法

public class TheadTest implements Runnable {
    private int count = 100;

    @Override
    public synchronized void run() {
            while (count > 0) {
                    System.out.println(Thread.currentThread().getName() + " " + (100-count+1));
                    count--;
            }
    }
}

在方法上面上锁,等同于this锁,同步方法使用的就是this锁,下面可以证明:

package test;

public class TheadTest implements Runnable {
    private int count = 100;
    public Boolean flag = true;
    @Override
    public void run() {
        if (flag){
            synchronized (this) {
                while (count > 0) {
                    System.out.println(Thread.currentThread().getName() + " " + (100 - count + 1));
                    count--;
                }
            }
        }else {
            sout();
        }
    }
    public synchronized void sout(){
        while (count > 0) {
            System.out.println(Thread.currentThread().getName() + " " + (100-count+1));
            count--;
        }
    }

    public static void main(String[] args) {
        TheadTest theadTest = new TheadTest();
        Thread thread1 = new Thread(theadTest, "线程1");
        Thread thread2 = new Thread(theadTest, "线程2");
        thread1.start();
        new TheadTest().flag = false;
        thread2.start();
    }
}

上面代码可以保证同步,所以可以得出用的是同一种锁,即同步方法用的是this锁。

注意:两个锁可以保持同步,则这两个锁使用的是同一种锁。所以一个线程使用this锁,一个线程使用同步方法,则可以进行同步,因为使用的是同一把锁。

synchronized – 静态同步方法

静态同步方法锁的是类的字节码文件,该类的所有静态方法使用的是同一把锁,当一个获得资源,其余方法无论是属于同一个对象的静态方法,还是多个对象的静态方法,都要进行等待。

public class TheadTest implements Runnable {
    private int count = 100;

    @Override
    public ststic synchronized void run() {
            while (count > 0) {
                    System.out.println(Thread.currentThread().getName() + " " + (100-count+1));
                    count--;
            }
    }
}

注意:如果有两个线程,一个线程使用同步方法,一个线程使用静态同步方法,这两个线程不能保证同步,因为同步方法是this锁,静态同步方法是锁的字节码文件。

死锁

死锁是两个或更多线程阻塞着等待其它处于死锁状态的线程所持有的锁。死锁通常发生在多个线程同时但以不同的顺序请求同一组锁的时候。

死锁可以用下面这张图来表达:

在这里插入图片描述

如上图:线程1锁住了A,然后尝试对B进行加锁,同时线程2已经锁住了B,接着尝试对A进行加锁,这时死锁就发生了。线程1永远得不到B,线程2也永远得不到A,并且它们永远也不会知道发生了这样的事情。为了得到彼此的对象(A和B),它们将永远阻塞下去。这种情况就是一个死锁。

代码演示

package test;

import static java.lang.Thread.sleep;

public class TheadTest implements Runnable {
    private int count = 100;
    private Object obj = new Object();
    @Override
    public void run() {
        if (Thread.currentThread().getName().equals("线程1")){
            while (true) {
                synchronized (obj) {
                     try {
                        sleep(50);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    sout();
                }
            }
        }else {
            while (true) {
                sout();
            }
        }
    }
    public synchronized void sout(){
        synchronized (obj){
            if (count>0){
                System.out.println(Thread.currentThread().getName() + " " + (100-count+1));
                count--;
            }
        }
    }

    public static void main(String[] args) throws InterruptedException {
        TheadTest theadTest = new TheadTest();
        Thread thread1 = new Thread(theadTest, "线程1");
        Thread thread2 = new Thread(theadTest, "线程2");
        thread1.start();
        thread2.start();
    }
}

上面代码:

​ 线程1先拿到同步代码块obj锁,在拿到同步函数this锁

​ 线程2先拿到同步代码块this锁,在拿到同步函数obj锁

在运行时线程1先拿到obj锁,去访问this锁,但是此时线程2拿到了this锁,想获取obj锁,这两个线程同步中嵌套同步,互不释放资源,就形成了死锁。

Java内存模型

java内存模型和java内存结构不是不一样:

java内存结构:是管理内存分配,属于JVM。

java内存模型:决定了多线程的可见性,属于jmm,分为主内存和私有内存,主内存主要存放共享的全局变量;

私有内存主要存放本地线程私有变量以及存放共享数据副本。

线程写的操作:

在这里插入图片描述

如上图:

线程1做写操作先对私有内存里的副本进行操作,再刷新主内存,之后会通知各个线程。这个操作过程会发生线程安全问题,因为线程之间不可见。

线程安全问题

在这里插入图片描述

上图:线程一对副本进行操作,线程2对副本进行操作,后都向主内存刷新,此时主内存刷新两次,但是结果被覆盖了,就造成安全问题。

脏读

线程安全问题之脏读:如写操作图,线程1对私有内存里的副本进行操作,线程2进行读操作,线程一进行刷新主内存的同时,线程2读取完毕,这是就造成脏读。

代码演示:

public class Dirtyread {
    private String userName="pdz";
    public synchronized void updateUser(String userName) {
        try {
            Thread.currentThread().sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        this.userName = userName;
        System.out.println("更新用户信息姓名为:" + userName);
    }


    public void queryUser() {
        System.out.println("获取用户信息姓名:" + userName);
    }



    public static void main(String[] args) {
        final  Dirtyread dirtyread = new Dirtyread();

        Thread t1 = new Thread(new Runnable() {
            @Override
            public void run() {
                dirtyread.updateUser("张三");
            }
        });
        t1.start();
        dirtyread.queryUser();
    }
}

上述案例我们一个线程执行同步方法,一个线程执行非同步方法,此时我们让同步方法执行了休眠,来模拟脏读,这样在修改前会读取到修改前的值,所以就会造成脏读。

volatile 可见性

volatile 可以保证线程可见性,但是不保证原子性即不保证同步。

volatile 修饰全局变量,保证其可见,即当某一线程的私有内存里全局变量发生改变会第一时间通知其他内存,从而避免线程之间操作不可见,即在访问变量时强制刷新主内存,具体如下:

强制线程到主内存(共享内存)里去读取变量,
而不去线程工作内存区里去读取,
从而实现了多个线程间的变量可见。
也就是满足线程安全的可见性。

代码演示:

public class VolatileTest implements Runnable {


    public volatile Boolean flog = true;
    @Override
    public void run() {
        System.out.println("子线程开始...");
        while (flog){

        }
        System.out.println("子线程结束...");
    }
    public void setFlog(Boolean flog) {
        this.flog = flog;
        System.out.println("flog----"+flog);
    }

    public static void main(String[] args) throws InterruptedException {
        VolatileTest volatileTest = new VolatileTest();
        Thread thread = new Thread(volatileTest);
        thread.start();
        Thread.sleep(500);
        volatileTest.setFlog(false);
    }
}

对于上面,flog如果没有加 volatile 会因为不可见,导致false修改了,但是线程依然在循环,因为其私有内存没有修改。这是因为主线程进行休眠,导致主线程修改了flag的值,但是并没有通知到另一个线程,所以另一个线程本地内存里flog依然时true

上面volatile可以保证可见性,但是并不保证原子性,也就是不保证线程安全问题。

代码演示:

public class VolatileTest extends Thread {
    private static volatile int count = 0;

    @Override
    public void run() {
        for (int i = 0; i < 10000; i++){
            count++;
        }
        System.out.println(count);
    }

    public static void main(String[] args) {
        VolatileTest[] volatileTests = new VolatileTest[10];
        for (int i = 0; i < 10; i++) {
            volatileTests[i] = new VolatileTest();
            volatileTests[i].setName("Thead-"+i);
        }
        for(VolatileTest volatileTest:volatileTests){
            volatileTest.start();
        }
    }
}

上面count 被static修饰,即10个线程共享一个变量,volatile修饰可以保证可见性,如果保证原子性,按照理论应该每次输出应该加10000,但是实际上并不是,这是因为各线程之间不是同步的,对应上面线程安全的图。

解决volatile原子性问题

对于上面原子性问题,有两种解决办法:

synchronized锁

方法一:使用volatile修饰共享变量的同时使用synchronized锁,来保证同步。

代码如下:

public class VolatileTest extends Thread {
    private static volatile int count = 0;
    public static Object obj = new Object();
    @Override
    public synchronized void run() {
        synchronized (obj){
            for (int i = 0; i < 10000; i++){
                count++;
            }
            System.out.println(count);
        }
    }

    public static void main(String[] args) {
        VolatileTest[] volatileTests = new VolatileTest[10];
        for (int i = 0; i < 10; i++) {
            volatileTests[i] = new VolatileTest();
            volatileTests[i].setName("Thead-"+i);
        }
        for(VolatileTest volatileTest:volatileTests){
            volatileTest.start();
        }
    }
}

由于这里10个线程创建了多个对象,所以使用锁要用static修饰,即多个线程共用一把锁,此时保证了原子性,结果是每次输出加10000。

原子类

方法2:使用AtomicInteger :AtomicInteger 等一系列是jdk1.5添加的,可以保证可见性的同时保证原子性。

演示:

public class VolatileTest extends Thread {
    private static AtomicInteger count = new AtomicInteger(0);
    public static Object obj = new Object();
    @Override
    public synchronized void run() {
        synchronized (obj){
            for (int i = 0; i < 10000; i++){
                count.incrementAndGet();
            }
            System.out.println(count.get());
        }
    }

    public static void main(String[] args) {
        VolatileTest[] volatileTests = new VolatileTest[10];
        for (int i = 0; i < 10; i++) {
            volatileTests[i] = new VolatileTest();
            volatileTests[i].setName("Thead-"+i);
        }
        for(VolatileTest volatileTest:volatileTests){
            volatileTest.start();
        }
    }
}

上面使用AtomicInteger可以保证其可见性以及原子性,原理是因为: 底层通过 volatile 和 CAS(Unsafe.class) 来保证了内存可见性与原子性。

常见原子类

常见的原子类:

AtomicBoolean           boolean 原子类
AtomicInteger           int 原子类
AtomicIntegerArray      int 数组原子类
AtomicLong              long 原子类
AtomicLongArray         long 数组原子类
AtomicReference         引用对象原子类
AtomicReferenceArray    引用对象数组原子类

原子类底层方法类似,这里以 AtomicInteger 为例。

AtomicInteger

源码:

public class AtomicInteger extends Number implements java.io.Serializable {
    private static final long serialVersionUID = 6214790243416807050L;

    /**
     * 使用 Unsafe CAS 来更新操作
     */
    private static final Unsafe unsafe = Unsafe.getUnsafe();
    /**
     * 变量 value 的内存偏移量,数据存储的内存地址
     */
    private static final long valueOffset;

    static {
        try {
            // 通过反射获取字段 value 的内存偏移量
            valueOffset = unsafe.objectFieldOffset
                    (AtomicInteger.class.getDeclaredField("value"));
        } catch (Exception ex) {
            throw new Error(ex);
        }
    }

    /**
     * 变量 value
     */
    private volatile int value;

    /**
     * 构造方法,指定 value
     */
    public AtomicInteger(int initialValue) {
        value = initialValue;
    }

    /**
     * 构造方法,value 初始为 0
     */
    public AtomicInteger() {
    }

    /**
     * 返回 value
     */
    public final int get() {
        return value;
    }

    /**
     * 设置 value
     */
    public final void set(int newValue) {
        value = newValue;
    }

    /**
     * 懒加载设置 value,使用该方法后,其他线程在一段时间内还会获取到旧值
     */
    public final void lazySet(int newValue) {
        unsafe.putOrderedInt(this, valueOffset, newValue);
    }

    /**
     * 设置新值并返回旧值
     */
    public final int getAndSet(int newValue) {
        return unsafe.getAndSetInt(this, valueOffset, newValue);
    }

    /**
     * 如果当前值为 expect,则设置为 update
     */
    public final boolean compareAndSet(int expect, int update) {
        return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
    }

    /**
     * 同 compareAndSet() ,底层调用方法相同,现在一样可能是暂时的,将来可能会不一样
     */
    public final boolean weakCompareAndSet(int expect, int update) {
        return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
    }

    /**
     * 当前值 +1,并返回旧值
     */
    public final int getAndIncrement() {
        return unsafe.getAndAddInt(this, valueOffset, 1);
    }

    /**
     * 当前值 -1,并返回旧值
     */
    public final int getAndDecrement() {
        return unsafe.getAndAddInt(this, valueOffset, -1);
    }

    /**
     * 当前值 +delta,并返回旧值
     */
    public final int getAndAdd(int delta) {
        return unsafe.getAndAddInt(this, valueOffset, delta);
    }

    /**
     * 当前值 +1,并返回新值
     */
    public final int incrementAndGet() {
        return unsafe.getAndAddInt(this, valueOffset, 1) + 1;
    }

    /**
     * 当前值 -1,并返回新值
     */
    public final int decrementAndGet() {
        return unsafe.getAndAddInt(this, valueOffset, -1) - 1;
    }

    /**
     * 当前值 +delta,并返回新值
     */
    public final int addAndGet(int delta) {
        return unsafe.getAndAddInt(this, valueOffset, delta) + delta;
    }

    /**
     * 使用 IntBinaryOperator 对当前值进行计算,并更新当前值,返回旧值
     */
    public final int getAndUpdate(IntUnaryOperator updateFunction) {
        int prev, next;
        do {
            prev = get();
            next = updateFunction.applyAsInt(prev);
        } while (!compareAndSet(prev, next));
        return prev;
    }

    /**
     * 同 getAndUpdate()
     */
    public final int updateAndGet(IntUnaryOperator updateFunction) {
        int prev, next;
        do {
            prev = get();
            next = updateFunction.applyAsInt(prev);
        } while (!compareAndSet(prev, next));
        return next;
    }

    /**
     * 使用 IntBinaryOperator 对当前值和x进行计算,并更新当前值,返回旧值
     */
    public final int getAndAccumulate(int x,
                                      IntBinaryOperator accumulatorFunction) {
        int prev, next;
        do {
            prev = get();
            next = accumulatorFunction.applyAsInt(prev, x);
        } while (!compareAndSet(prev, next));
        return prev;
    }

    /**
     * 同 getAndAccumulate()
     */
    public final int accumulateAndGet(int x,
                                      IntBinaryOperator accumulatorFunction) {
        int prev, next;
        do {
            prev = get();
            next = accumulatorFunction.applyAsInt(prev, x);
        } while (!compareAndSet(prev, next));
        return next;
    }

    /**
     * value 转换 String
     */
    public String toString() {
        return Integer.toString(get());
    }

    /**
     * 同 get()
     */
    public int intValue() {
        return get();
    }

    /**
     * 同 get()
     */
    public long longValue() {
        return (long) get();
    }

    /**
     * 同 get()
     */
    public float floatValue() {
        return (float) get();
    }

    /**
     * 同 get()
     */
    public double doubleValue() {
        return (double) get();
    }
}

在这里说下其中的value,这里value使用了volatile关键字,volatile在这里可以做到的作用是使得多个线程可以共享变量,但是问题在于使用volatile将使得VM优化失去作用,导致效率较低,所以要在必要的时候使用,因此AtomicInteger类不要随意使用,要在使用场景下使用。

方法摘录:

public final int get() //获取当前的值
public final int getAndSet(int newValue)//获取当前的值,并设置新的值
public final int getAndIncrement()//获取当前的值,并自增
public final int getAndDecrement() //获取当前的值,并自减
public final int getAndAdd(int delta) //获取当前的值,并加上预期的值

注意:atomic类只保证本身方法原子性,并不保证多次操作的原子性,如果想要满足多次操作原子性,就结合关键字synchronized使用

发布了20 篇原创文章 · 获赞 40 · 访问量 1930

猜你喜欢

转载自blog.csdn.net/bing_bg/article/details/105709511
今日推荐