Java并发编程实战(二)

一、线程安全问题

1. 出现线程安全问题的三个必要条件:  

(1)多线程环境下    

(2)多个线程共享一个资源    

(3)对资源进行读写(非原子性)操作   

在以上三个条件下,将会出现线程安全问题    

2. 解决线程安全问题的途径   

   (1) Synchronized(偏向锁,轻量级锁,重量级锁)    

   (2) volatile 解决内存可见性问题,并不能保证原子性操作   

   (3) JDK提供的原子类     

   (4) 使用Lock锁(共享锁,独占锁)     

3. 共享资源   

所谓共享资源,就是说该资源被多个线程所持有或者说多个线程都可以去访问该资源。   

4. 什么是线程安全问题?   

线程安全问题是指当多个线程同时读写一个共享资源并且没有任何同步措施时,导致出现脏数据或者其他不可预见的结果的问题。    

5. 线程安全问题与共享资源之间是什么关系呢?是不是说多个线程共享了资源,当它们都去访问这个共享资源时,就会出现线程安全问题呢?   

不是的。  

如果多个线程都只是读取共享资源,而没有修改共享资源,就不会存在线程安全问题;

只有当至少一个线程修改共享资源时,就会出现线程安全问题。典型的实例就是计数器类的实现。   

6. Java中共享变量的内存可见性问题   

Java内存模型规定,将所有的变量都存放在主内存中,当线程使用变量时,会把主内存里面的变量复制到自己的工作空间或者叫做工作内存中,线程读写变量时操作的是自己工作内存中的变量。   

  

      可见:一个线程修改了这个变量的值,在另外一个线程中能够读到这个修改后的值。   

      当一个线程操作共享变量时,它首先从主内存(公共)复制共享变量(共享变量副本)到自己的工作内存,然后对工作内存里的变量进行处理(读写操作),处理完之后将变量值更新到主内存。

      这时当有多个线程同时操作这个共享变量时,由于变量的内存不可见问题,就会出现脏数据,比如在计数器累加时,出现重复的情况,为了解决共享变量内存不可见问题,就需要使用volatile关键字来避免出现内存不可见问题。   

      共享变量内存可见性问题主要是由于线程的工作内存导致的。    

   实例代码:

package com.lhf.thread2;

/**
 * @ClassName: ThreadDemo2
 * @Description:  多线程实现数值系列生成
 * 出现的问题:数值系列出现了大量的重复
 * 为了解决这个问题,需要加锁实现。在getNext()方法上加上synchronized锁,即可解决
 *
 * @Author: liuhefei
 * @Date: 2019/3/29
 * @blog: https://www.imooc.com/u/1323320/articles
 **/
public class ThreadDemo2 {

    //共享变量
    private static int value;

    //加上synchronized,实现同步,保证数值系列不会重复
    //synchronized 放在普通方法上,内置锁就是当前类的实例
    public synchronized int getNext(){
        return value++;
    }

    //synchronized修饰静态方法,内置锁是当前的Class字节码对象,也就是方法所在类的Class字节码对象
    public static synchronized int getPrevious(){
        return value--;
    }

    /**
     * synchronized修饰代码块
     * @return
     */
    public int method1(){
        synchronized (this){
            if(value > 0){
                return value;
            }else {
                return -1;
            }
        }
    }

    public static void main(String[] args) {
        ThreadDemo2 td = new ThreadDemo2();
        
        new Thread(new Runnable() {
            @Override
            public void run() {
                while (true){
                    System.out.println(Thread.currentThread().getName() + "---" + td.getNext());
                    try {
                        Thread.sleep(500);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        }).start();

        new Thread(new Runnable() {
            @Override
            public void run() {
                while (true){
                    System.out.println(Thread.currentThread().getName() + "---" + td.getNext());
                    try {
                        Thread.sleep(500);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        }).start();

        new Thread(new Runnable() {
            @Override
            public void run() {
                while (true){
                    System.out.println(Thread.currentThread().getName() + "---" + td.getNext());
                    try {
                        Thread.sleep(500);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        }).start();

    }
}

   

二、单例设计模式与线程安全性问题      

饿汉式:不存在线程安全性问题

实例代码:

package com.lhf.thread3;

/**
 * @ClassName: Singleton
 * @Description: 单例设计模式--饿汉式
 * @Author: liuhefei
 * @Date: 2019/3/30
 * @blog: https://www.imooc.com/u/1323320/articles
 **/
public class Singleton {

    //为了保证实例唯一,不让别人使用,需要私有化构造方法
    private Singleton() {

    }

    //饿汉式:在创建的过程就实例化
    //饿汉式:不存在线程安全性问题
    private static Singleton instance = new Singleton();

    //提供一个共有的方法来获取实例
    public static Singleton getInstance(){
        return instance;
    }

}
package com.lhf.thread3;

/**
 * @ClassName: SingletonMain
 * @Description: 饿汉式
 * @Author: liuhefei
 * @Date: 2019/3/30
 * @blog: https://www.imooc.com/u/1323320/articles
 **/
public class SingletonMain {

    public static void main(String[] args) {
        //因为是单例模式,设置了私有化,所以在这里是不能实例化new的
        //Singleton singleton = new Singleton();

        Singleton singleton1 = Singleton.getInstance();
        Singleton singleton2 = Singleton.getInstance();
        Singleton singleton3 = Singleton.getInstance();

        //看它的hash值是否相同,如果相同,说明是单实例的
        System.out.println(singleton1);
        System.out.println(singleton2);
        System.out.println(singleton3);

    }
}

懒汉式:存在线程安全性问题

懒汉式可以这样理解,比如一个软件,在你需要的时候,你再去打开它,不需要的时候就没必要打开它。(个人理解)    

两种解决办法:    

1. 直接使用synchronized来修饰懒汉式单例模式实例化对象的方法,升级为重量级锁,避免出现线程不安全问题,但是性能会随着线程数的增加而下降,不推荐使用。   

2. 在懒汉式单例模式实例化对象的方法内部使用双重检查加锁机制,同时使用volatile关键字修饰单实例变量实现。    

实例代码:(方式一)

package com.lhf.thread3;

/**
 * @ClassName: Singleton1
 * @Description:  单例设计模式————懒汉式
 * @Author: liuhefei
 * @Date: 2019/3/30
 * @blog: https://www.imooc.com/u/1323320/articles
 **/
public class Singleton1 {

    //私有化构造方法
    private Singleton1(){

    }

    //懒汉式比较懒惰,在创建的时候不需要是实例化它,只有在需要使用的时候,才会去实例化
    private static Singleton1 instance;

    /**
     * 使用synchronized来修饰该方法,升级为重量级锁,避免出现线程不安全问题,但是性能会随着线程数的增加而下降,
     * 为了解决性能问题,在此处建议不使用synchronized来修饰该方法。
     * 在此处偏向锁,轻量级锁都不能解决性能问题
     * @return
     */
    //方法一
    public synchronized static Singleton1 getInstance(){
        //这里就是非原子性操作,涉及线程安全性问题
        if(instance == null){  //如果instance为空,就去实例化它
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            instance = new Singleton1();
        }
        return instance;
    }

    /**
     * 线程安全性的三个条件:
     * 1.多线程环境下
     * 2.必须有共享资源
     * 3.对资源进行非原子性操作
     */
}

package com.lhf.thread3;

/**
 * @ClassName: Singleton1Main
 * @Description:
 * @Author: liuhefei
 * @Date: 2019/3/30
 * @blog: https://www.imooc.com/u/1323320/articles
 **/
public class Singleton1Main {

    public static void main(String[] args) {
        Singleton1 singleton1 = Singleton1.getInstance();
        Singleton1 singleton2 = Singleton1.getInstance();
        Singleton1 singleton3 = Singleton1.getInstance();

        //看它的hash值是否相同,如果相同,说明是单实例的
        System.out.println(singleton1);
        System.out.println(singleton2);
        System.out.println(singleton3);
    }
}

实例代码:(方式二)

package com.lhf.thread3;

/**
 * @ClassName: Singleton2
 * @Description:  单例设计模式————懒汉式
 * @Author: liuhefei
 * @Date: 2019/3/30
 * @blog: https://www.imooc.com/u/1323320/articles
 **/
public class Singleton2 {

    //私有化构造方法
    private Singleton2(){

    }

    //懒汉式在需要的时候才会去实例化
    private volatile static Singleton2 instance;

    //方法二: 双重检查加锁机制,这里会发生指令重排序,依然会存在线程安全问题,
    // 为了解决这个问题,需要使用volatile关键字来修饰instance1,避免出现指令重排序,进而避免发生线程不安全问题
    public static Singleton2 getInstance(){
        //这里就是非原子性操作,涉及线程安全性问题
        if(instance == null){  //如果instance为空,就去实例化它
            synchronized (Singleton2.class){
                if(instance == null){    //使用双重检查加锁机制
                    instance = new Singleton2();   //指令重排序(为了提高性能)

                    //1.申请一块内存空间
                    //2.在这块空间里实例化对象
                    //3. instance的引用指向这块空间地址
                    //但是正真的运行且不是这样的,会发生指令重排序,代码执行的顺序会发生变化
                }
            }
        }
        return instance;
    }

}

package com.lhf.thread3;

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

/**
 * @ClassName: MultiThreadMain
 * @Description:  方式一:验证懒汉式单例设计模式的线程安全性问题
 * @Author: liuhefei
 * @Date: 2019/3/30
 * @blog: https://www.imooc.com/u/1323320/articles
 **/
public class MultiThreadMain {

    public static void main(String[] args) {
        //创建带有缓存的线程池
        //ExecutorService threadPool = Executors.newCachedThreadPool();

        //创建执行线程数的线程池
        ExecutorService threadPool = Executors.newFixedThreadPool(20);

        /**
         * 执行下面这段代码,你会发现懒汉式单例设计模式的hash值会出现不同值,说明存在线程不安全
         * 解决方法:使用synchronized去修饰懒汉式的实例化方法,但是性能会随着线程数的增加而下降,不推荐使用
         */
        //方式一
        for(int i=0;i<20;i++){
            //执行线程
            threadPool.execute(new Runnable() {
                @Override
                public void run() {
                    System.out.println(Thread.currentThread().getName() + "---" + Singleton1.getInstance());
                }
            });
        }

        //销毁线程池
        threadPool.shutdown();
    }
}

package com.lhf.thread3;

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

/**
 * @ClassName: MultiThreadMain1
 * @Description:  方式二:验证懒汉式单例模式的线程安全问题
 * @Author: liuhefei
 * @Date: 2019/3/30
 * @blog: https://www.imooc.com/u/1323320/articles
 **/
public class MultiThreadMain1 {

    public static void main(String[] args) {
        //创建指定线程数量的线程池
        ExecutorService threadPool = Executors.newFixedThreadPool(20);

        /**
         * 使用双重检查加锁机制来避免懒汉式单例模式的线程安全问题
         * 单实例变量一定要加volatile关键字修饰,用于避免指令重排序
         */
        for(int i=0;i<20;i++){
            threadPool.execute(new Runnable() {
                @Override
                public void run() {
                    System.out.println(Thread.currentThread().getName() + "--" + Singleton2.getInstance());
                }
            });
        }

        //销毁线程池
        threadPool.shutdown();
    }
}

三、Java指令重排序    

Java内存模型允许编译器和处理器对指令重排序以提高运行性能,并且只会对不存在数据依赖性的指令重排序。   

在单线程下,重排序可以保证最终执行的结果与程序顺序执行的结果一致;   

在多线程下,指令重排序就会存在问题,会导致线程不安全。   

指令重排序在多线程下会导致非预期的程序执行结果,为了避免出现这个问题,可以使用 volatile关键字 来修饰与之有关的变量,就可以避免指令重排序和内存可见性问题。   

(1)写volatile变量时,可以确保volatile写之前的操作不会被编译器重排序到volatile写操作之后;   

(2)读volatile变量时,可以确保volatile读之后的操作不会被编译器重排序到volatile读操作之前;   

四、synchronized关键字

1. synchronized块是Java提供的一种原子性内置锁,Java中的每个对象都可以把它当作一个同步锁来使用,这些Java内置的使用者看不到的锁被称为内部锁,也叫做监视器锁。   

2. 线程的执行代码在进入synchronized代码块前会自动获取内部锁,这时候其他线程访问该同步代码块时会被阻塞挂起。拿到内部锁的线程会在正常退出同步代码块或者抛出异常后或者在同步块内调用了该内置锁资源的wait系列方法时释放该内置锁。   

3. 内置锁是排他锁,也就是当一个线程获取这个锁后,其他线程必须等待该线程释放锁后才能获取该锁。    

4. 另外,Java中的线程是与操作系统的原生线程一一对应的,所以当阻塞一个线程时,需要从用户态切换到内核态执行阻塞操作,这是很耗时的操作,而synchronized的使用就会导致上下文切换。   

5. synchronized的内存语义可以解决共享变量内存可见性问题。

   进入synchronized块的内存语义是把synchronized块内使用到的变量从线程的工作内存中清除,这样在synchronized块内使用到该变量时就不会从线程的工作内存中获取,而是直接从主内存中获取。   

   退出synchronized块的内存语义是把在synchronized块内对共享变量的修改刷新到主内存中。   

6. synchronized经常被用来实现原子性操作。使用synchronized关键字会引起线程上下文切换带来的线程调度开销。   

7. 使用synchronized关键字可以实现线程安全性,即内存可见性和原子性,但是synchronized是独占锁,没有获取内部锁的线程会被阻塞。     

实例代码:

package com.lhf.thread5;

/**
 * @ClassName: SynchronizedDemo
 * @Description: Synchronized的可见性
 * 保证可见性的前提:
 *  * 多个线程拿到的是同一把锁,否则保证不了线程安全.
 *  * 在没有加锁的情况下,两个线程对a变量进行了读和写操作,导致了线程不安全问题。
 *  * 为了解决这个问题就要加锁来实现,具体的方法:
 *  * 1. 使用synchronized来修饰getA()和setA()方法
 * @Author: liuhefei
 * @Date: 2019/3/30
 * @blog: https://www.imooc.com/u/1323320/articles
 **/
public class SynchronizedDemo {

    //共享变量
    private int a = 1;

    public synchronized int getA(){
        return a;
    }

    public synchronized void setA(int a){
        try {
            Thread.sleep(100);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        this.a = a;
    }

    public static void main(String[] args) {
        SynchronizedDemo sd = new SynchronizedDemo();

        new Thread(new Runnable() {
            @Override
            public void run() {
                sd.setA(10);
            }
        }).start();

        new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println("a = " + sd.getA());
            }
        }).start();

        try {
            Thread.sleep(200);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        System.out.println("最终的结果a= "+ sd.getA());
    }
}

五、volatile关键字   

1. volatile关键字用于解决Java内存可见性问题。该关键字可以确保对一个变量的更新对其他线程马上可见。   

2. 当一个变量被声明为volatile时,线程在写入变量时不会把值缓存在寄存器或者其他地方,而是会把值刷新回主内存。当其他线程读取该共享变量时,会从主内存重新获取最新值,而不是使用当前线程的工作内存中的值。      

3. 当线程写入了volatile变量值时就等价于线程退出synchronized同步块(把写入工作内存的变量值同步到主内存中)     

   当线程读取volatile变量值时就相当于进入synchronized同步代码块(先清空本地内存变量值,再从主内存获取最新值)     

4. volatile关键字解决了可见性问题,但是它不能保证操作的原子性。不能解决读-改-写等的原子性问题。    

5. 使用volatile关键字的场景:   

   (1) 写入变量值不依赖变量的当前值时。因为如果依赖当前值,将是获取-计算-写入三步操作,这三步操作不是原子性的。   

   (2) 读写变量值时没有加锁。因为加锁本身已经保证了内存可见性,这时候不需要把变量声明为volatile的。  

6. 由于volatile关键字不能解决原子性操作问题,Java JDK为我们提供了CAS操作来解决非阻塞原子操作,它通过硬件保证了比较-更新操作的原子性。

   CAS操作是由JDK里面的Unsafe类提供的一系列方法实现的。   

7. volatile称之为轻量级锁,被volatile修饰的变量,在线程之间是可见的。

8. 保证可见性的前提:多个线程拿到的是同一把锁,否则保证不了线程安全   

9. 使用volatile关键字来修饰变量,它只能保证变量的可见性,并不能保证变量的原子性操作   

实例代码:

package com.lhf.thread5;

/**
 * @ClassName: VolatitleDemo
 * @Description: volatile关键字可以保证可见性
 * 保证可见性的前提:
 * 多个线程拿到的是同一把锁,否则保证不了线程安全.
 * 在没有加锁的情况下,两个线程对a变量进行了读和写操作,导致了线程不安全问题。
 * 为了解决这个问题就要加锁来实现,具体的方法:
 * 1. 使用volatile关键字来修饰共享变量a
 *
 * 使用volatile关键字来修饰变量,它只能保证变量的可见性,并不能保证变量的原子性操作,因此这里要结合synchronized来一起使用。
 * @Author: liuhefei
 * @Date: 2019/3/30
 * @blog: https://www.imooc.com/u/1323320/articles
 **/
public class VolatileDemo {

    //共享变量
    public volatile int a = 1;

    public synchronized int getA(){
        return a++;
    }

    public synchronized void setA(int a){
        try {
            Thread.sleep(100);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        this.a = a;
    }

    public static void main(String[] args) {
        VolatileDemo vd = new VolatileDemo();

        vd.a = 100;

        new Thread(new Runnable() {
            @Override
            public void run() {

                System.out.println("a = " + vd.a);
            }
        }).start();

        try {
            Thread.sleep(200);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        System.out.println("最终的结果a= "+ vd.getA());
    }

}

synchronized与volatile的区别:   

synchronized关键字:它是重量级锁,它可以保证可见性和原子性,它可以替代volatile;   

volatile关键字:它是轻量级锁,它只能保证可见性,不能保证原子性操作,它替代不了synchronized关键字。     

六、Java中的原子性操作

所谓原子性操作,是指执行一系列操作时,这些操作要么全部执行,要么全部不执行,类似于数据库中的事务,不存在只执行其中一部分的情况。   

比如在设计计数器时一般先读取当前值,然后+1,在更新。这个过程是读-改-写的过程,如果不能保证原子性操作,就会出现线程安全性问题。    

参考:《Java并发编程之美》 翟陆续 薛宾田著

     

点赞