1.什么是线程安全性

  • 当多个线程访问某个类,不管运行时环境采用何种调度方式或者这些线程如何交替执行,并且在主调代码中不需要任何额外的同步或协同,这个类都能表现出正确的行为,那么就称这个类为线程安全的。
  • 线程不安全的原因
    • 线程是抢先执行的
    • 原子性操作,当CPU执行一个线程过程时,调度器可能调走CPU,去执行另一个线程,此线程的操作可能还没有结束。
    • 多个线程尝试修改同一个变量。
    • 内存可变性
    • 指令重排

2.原子性操作

  • 一个操作或这多个操作,要么全部执行并且执行过程中不被任何因素打断,要么就都不执行。
  • 如何把非原子性操作变成原子性
    • volatile关键字仅仅保证可见性,并不保证原子性,synchronized关键字使得操作具有原子性。

3.深入理解synchronized

(1)内置锁

  • 每个java对象都可以做一个实现同步的锁,这些锁成为内置锁。线程进入同步代码块或者方法的时候会自动获得锁,在退出同步代码块或者方法的时候释放该锁。获得内置锁的唯一途径就是进入这个锁的保护的同步代码块或者方法。

(2)互斥锁

  • 内置锁是一个互斥锁,这就意味着最多只有一个线程能够获得该锁,当线程A尝试去获得线程B持有的内置锁时,线程A必须等待或者阻塞,知道线程B释放这个锁,如果B线程不释放这个锁,那么A线程将永远等待下去。

(3)synchronized修饰普通方法

  • synchronzied修饰普通方法锁住的是当前调用的对象,假如开两个线程两个实例去掉方法,那么两个实例各自持有一个锁,互相不干扰,但是如果是两个线程用同一实例去调用,那么就持有一个锁,第一个线程释放锁后,第二个才会拿到锁。
public class SyncDemo {

	public synchronized void exampleOut(){
        try {
            Thread.sleep(5000L);
            RuntimeMXBean runtimeMXBean = ManagementFactory.getRuntimeMXBean();
            long uptime = runtimeMXBean.getUptime();
            System.out.println(Thread.currentThread().getName()+"-线程运行时长:"+uptime+"ms");
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}
  • 两个线程调用两个实例案例
public static void main(String[] args) {
    SyncDemo syncDemo1 = new SyncDemo();
    SyncDemo syncDemo2 = new SyncDemo();

    new Thread(() -> {
        syncDemo1.exampleOut();
    }).start();

    new Thread(() -> {
        syncDemo2.exampleOut();
    }).start();
}

【并发编程】线程安全性问题_代码块

  • 两个线程调用一个实例案例
public static void main(String[] args) {
    SyncDemo syncDemo1 = new SyncDemo();
    SyncDemo syncDemo2 = new SyncDemo();

    new Thread(() -> {
        syncDemo1.exampleOut();
    }).start();

    new Thread(() -> {
        syncDemo1.exampleOut();
    }).start();
}

【并发编程】线程安全性问题_代码块_02

(4)synchronized修饰静态方法

  • synchronized修饰静态方法锁住的是整个类的对象,无论有多少个实例,只要是当前类的实例,都持有一个锁,一般生产不建议用静态同步方法,可能会导致程序运行阻塞。
public class SyncDemo {

    public static synchronized void staticOut(){
        try {
            Thread.sleep(5000L);
            RuntimeMXBean runtimeMXBean = ManagementFactory.getRuntimeMXBean();
            long uptime = runtimeMXBean.getUptime();
            System.out.println(Thread.currentThread().getName()+"-线程运行时长:"+uptime+"ms");
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}
public static void main(String[] args) {
    SyncDemo syncDemo1 = new SyncDemo();
    SyncDemo syncDemo2 = new SyncDemo();

    new Thread(() -> {
        syncDemo1.staticOut();
    }).start();

    new Thread(() -> {
        syncDemo2.staticOut();
    }).start();
}

【并发编程】线程安全性问题_后端_03

(5)synchronized修饰代码块

  • synchronized修饰代码块锁住的是当前对象,用法和synchronized修饰普通方法一样,但是更细粒度确定锁的位置,比synchronized修饰普通方法的效率要高。
public class SyncDemo {
	private Object lock = new Object();
    public void blockOut(){

        try {
            Thread.sleep(5000L);
            RuntimeMXBean runtimeMXBean = ManagementFactory.getRuntimeMXBean();
            long uptime = runtimeMXBean.getUptime();
            System.out.println(Thread.currentThread().getName()+"-线程运行时长:"+uptime+"ms");
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}
  • 两个线程调用两个实例案例
public static void main(String[] args) {
    SyncDemo syncDemo1 = new SyncDemo();
    SyncDemo syncDemo2 = new SyncDemo();

    new Thread(() -> {
        syncDemo1.blockOut();
    }).start();

    new Thread(() -> {
        syncDemo2.blockOut();
    }).start();
}

【并发编程】线程安全性问题_java_04

  • 两个线程调用一个实例案例
public static void main(String[] args) {
    SyncDemo syncDemo1 = new SyncDemo();
    SyncDemo syncDemo2 = new SyncDemo();

    new Thread(() -> {
        syncDemo1.blockOut();
    }).start();

    new Thread(() -> {
        syncDemo1.blockOut();
    }).start();
}

【并发编程】线程安全性问题_线程安全_05

3.4.volatile关键字

(1)volatile关键字的作用

  • 保证了变量的可见性(visibility)。被volatile关键字修饰的变量,如果值发生了变更,其他线程立马可见,避免出现脏读的现象。

(2)为什么会出现脏读

  • Java内存模型规定所有的变量都是存在主存当中,每个线程都有自己的工作内存。线程对变量的所有操作都必须在工作内存中进行,而不能直接对主存进行操作。并且每个线程不能访问其他线程的工作内存。变量的值何时从线程的工作内存写回主存,无法确定。

(3)volatile案例实战

public class VolatileDemo {
    private volatile boolean flag = false;

    public void work(){
        while (!flag) {
            System.out.println("线程开始工作");
        }
    }

    public void down(){
        flag = true;
        System.out.println("线程停止工作");
    }

    public static void main(String[] args) {
        VolatileDemo work = new VolatileDemo();
        new Thread(work::work).start();
        new Thread(work::work).start();
        new Thread(work::down).start();
        new Thread(work::work).start();
        new Thread(work::work).start();
    }
}
  • 不加volatile关键字的运行结果

【并发编程】线程安全性问题_线程安全_06

  • 加volatile关键字的运行结果

【并发编程】线程安全性问题_线程安全_07

(4)volatile只能保证变量的可见性,不能保证对volatile变量操作的原子性

  • 案例实战,分别对num进行+1000,+2000,+3000的操作
public class VolatileDemo {

    private volatile int num = 0;

    //执行方法加上synchronized
    public synchronized void addNum(){
        num++;
    }
	//执行方法加上synchronized
    public synchronized int getNum(){
        return num;
    }

    public static void main(String[] args) {

        VolatileDemo volatileDemo = new VolatileDemo();

        new Thread(()->{
            for (int i = 0; i < 1000; i++) {
                volatileDemo.addNum();
            }
            System.out.println(volatileDemo.getNum());
        }).start();

        new Thread(()->{
            for (int i = 0; i < 2000; i++) {
                volatileDemo.addNum();
            }
            System.out.println(volatileDemo.getNum());
        }).start();

        new Thread(()->{
            for (int i = 0; i < 3000; i++) {
                volatileDemo.addNum();
            }
            System.out.println(volatileDemo.getNum());
        }).start();

    }
}

【并发编程】线程安全性问题_后端_08

  • 加上synchronized之后

【并发编程】线程安全性问题_后端_09

3.5.happens-before规则

(1)理解happens-before

  • 如果一个操作happens-before另一个操作,那么第一个操作的执行结果将对第二个操作可见,而且第一个操作执行顺序排在第二个操作之前。
  • 两个操作之间存在happens-before关系,并不意味着java平台的具体实现必须按照happens-before关系指定的顺序来执行。如果重排序之后的执行结果,与按happens-before关系来执行的结果一致,那么JMM也允许这样的重排序。
  • 如果操作A happens-before操作B,那么操作A在内存上所做的所有操作对于操作B都是可见的,不管它们在不在同一个线程。
  • happens-before关系保证正确同步的多线程程序执行的结果不被重排序改变。

(2)happens-before六大规则

  • 前三个规则用这个例子来看
class VolatileExample{
    int a=0;
    volatile boolean flag=false;
    public void writer(){
        a=1;                                  // 操作1
        flag=true;                            // 操作2
    }
    public void reader(){
        if(flag){                             // 操作3
            int i=a;                          // 操作4
            //这里i会是多少呢?
        }
    }
}
  • 程序顺序规则

    • 一个线程中的每一个操作,happens-before于该线程中的任意后续操作可见。
    • 程序前面对某个变量的修改一定是对后续操作可见的。
    • 例如上面代码块,按照程序顺序执行规则,1 happens-before 2,3 happens-before 4。
  • volatile变量规则

    • 对一个volatile域的写,happens-before于任意后续对这个volatile域的读。
    • 例如上面代码块,按照volatile变量规则,2 happens-before 3。
  • 传递性规则

    • 如果A appens-before B,B happens-before C,那么A happens-before C。
    • 例如上面代码块,按照传递性规则,1 happens-before 4。
  • 管程中锁的规则

    • 对一个锁的解锁,happens-before于随后对这个锁的加锁。
    • 管程是一种通用的同步原语,在java中指的就是synchronized,synchronized是java里对管程的实现。
synchronized(this){
    if(this.x < 12){
        this.x = 12;
    }
}

//根据管程中锁的规则,线程A执行完成后x会变成12,执行完释放锁,线程B进入代码块的时候,能够看到线程A对x的操作,也就睡说B看到的x值为12。
  • 线程start规则
    • 父线程A启动后,启动子线程B,子线程B能够看到主线程在启动B前的所有操作(指共享变量的操作)。
public class StartDemo {
    private static int num = 0;
    public static void main(String[] args) {

        Thread A = new Thread(()->{
            Thread B = new Thread(()->{
                System.out.println("B线程中读取num:"+num); //操作2
            });
            num = 1;   //操作1
            B.start();
        });
        A.start();
    }
}

//根据线程start规则,1 happens-before 2,线程A对共享变量a=1的操作对于线程B是可见的。

【并发编程】线程安全性问题_java_10

  • 线程join规则
    • 父线程A等待子线程B完成, 当子线程B完成后 ,父线程A能够看到子线程B的操作(指的是对共享变量的操作)。
public class JoinDemo {

    private static int num = 0;

    public static void main(String[] args) {

        Thread A = new Thread(()->{
            Thread B = new Thread(()->{
                num = 2;
            });
            num = 1;
            B.start();
            try {
                B.join();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("A线程中读取num:"+num);
        });
        A.start();
    }
}

【并发编程】线程安全性问题_java_11

3.6.如何避免线程安全性问题

(1)线程安全性问题成因

  • 多线程环境
  • 多个线程操作同意共享资源
  • 对该共享资源进行了非原子性操作

(2)如何避免线程安全性问题

  • 多线程环境–将多线程改为单线程(加锁)
  • 多个线程操作同一共享资源–让其资源不进行共享(ThreadLocal、资源不可变、操作无状态化)
  • 对该共享资源进行了非原子性操作–将非原子性的操作改成原子性的操作(加锁)