一、线程安全概述

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

当多个线程共享同一个全局变量,做写的操作时(即修改该全局变量),可能会受到其他的线程干扰,发生线程安全问题。
eg:

public class Thread01  implements  Runnable{
    //定义一个全局变量
    private static Integer count = 100;

    @Override
    public void run() {
        while (count >1){
            cal();
        }
    }

    private void cal(){
        try {
            Thread.sleep(30);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        count--;
        System.out.println(Thread.currentThread().getName()+"--"+count);
    }

    public static void main(String[] args) {
        Thread01 thread01 = new Thread01();
        Thread thread1 = new Thread(thread01);
        Thread thread2 = new Thread(thread01);
        thread1.start();
        thread2.start();
    }
}

运行后打印结果出现了线程安全问题,如下图:

java全局变量多线程修改 java多线程全局变量加锁_java全局变量多线程修改

  1. 多线程如何解决线程安全问题(多线程如何实现同步)

当多个线程共享同一个全局变量时,将可能会发生线程安全的代码上锁,保证只有拿到锁的线程才可以执行,没有拿到锁的线程不可以执行,需要阻塞等待。
(1)使用synchronized锁;
(2)使用Lock锁 ,需要自己实现锁的升级过程,底层是基于aqs实现;
(3)使用Threadlocal,需要注意内存泄漏的问题。
(4)原子类CAS 非阻塞式。

二、线程安全问题发生的原因分析

1、线程安全问题举例

public class SecurityDemo extends Thread{
    private static int sum=0;

    @Override
    public void run(){
        for(int i=0; i<10000; i++){
            sum++;
        }
    }

    public static void main(String[] args) throws InterruptedException {
        SecurityDemo t1 = new SecurityDemo();
        SecurityDemo t2 = new SecurityDemo();
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        System.out.println(sum);
    }
}

运行结果为:

java全局变量多线程修改 java多线程全局变量加锁_System_02

上面的代码运行后打印的sum结果正常情况下是等于20000的,因为发生了线程安全问题,所以大概率运行时是小于20000。那为何会发生线程安全问题呢?请看接下来的分析:

2、线程安全问题发生的原因分析

(1)查看字节码
把SecurityDemo.java 文件编译成 SecurityDemo.class 文件,再用cmd访问该SecurityDemo.class 文件目录,在该目录下输入javap -p -v SecurityDemo.class,得到反编译文件。
部分截图:

截图中常量sum描述为描述错误,是全局共享变量sum

java全局变量多线程修改 java多线程全局变量加锁_java_03

(2)从上下文角度分析当两个线程同时运行对全局变量sum进行自增加操作,可能会发生上下文切换的情况,当CPU执行第一个线程运行到 13:iadd (自增=+1)时,如果发生上下文切换,则CPU切换到第二个线程运行,第二个线程将sum=0改成了sum=1,CPU再切换到第一个线程继续运行 14:putstatic (截图中赋值给全局变量sum),全局变量sum的结果还是sum = 1,此时就是发生了线程安全问题。

java全局变量多线程修改 java多线程全局变量加锁_java_04

三、synchronized锁的基本用法

  1. 修饰代码块
    修饰代码块,指定加锁对象,对指定对象加锁,执行该代码块前要获得指定对象的锁。
    eg:
public class Thread01 implements Runnable{
    private static Integer count = 100;

    @Override
    public void run() {
        while (count > 1) {
            test();
        }
    }

    private void test() {
        synchronized (this) {
            try {
                Thread.sleep(20);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            count--;
            System.out.println(Thread.currentThread().getName() + "," + count);
        }
    }

    public static void main(String[] args) {
        Thread01 thread01 = new Thread01();
        Thread thread1 = new Thread(thread01);
        Thread thread2 = new Thread(thread01);
        thread1.start();
        thread2.start();
    }
}
  1. 修饰实例方法
    修饰实例方法,作用于当前实例加锁,进入同步代码前要获得当前实例的锁,在实例方法上默认加上synchronized 默认使用this锁。
    eg:
public class Thread02 implements Runnable {
    private static Integer count = 100;

    @Override
    public void run() {
        while (count > 1) {
            test();
        }
    }

    private synchronized void test() {
        try {
            Thread.sleep(10);
        } catch (Exception e) {

        }
        count--;
        System.out.println(Thread.currentThread().getName() + "," + count);
    }
    public static void main(String[] args) {
        Thread02 thread02 = new Thread02();
        Thread thread1 = new Thread(thread02);
        Thread thread2 = new Thread(thread02);
        thread1.start();
        thread2.start();
    }
}
  1. 修饰静态方法
    修饰静态方法,作用于当前类对象加锁,进入同步代码前要获得当前类对象的锁,默认使用当前类的 类名.class 锁。
    eg:
public class Thread03 implements Runnable{
    private static Integer count = 100;
    private static String lock = "lock";

    @Override
    public void run() {
        while (count > 1) {
            test();
        }
    }

    private static void test() {
        synchronized (Thread03.class) {
            try {
                Thread.sleep(10);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            count--;
            System.out.println(Thread.currentThread().getName() + "," + count);
        }
    }

    public static void main(String[] args) {
        Thread03 Thread031 = new Thread03();
        Thread03 Thread032 = new Thread03();
        Thread thread1 = new Thread(Thread031);
        Thread thread2 = new Thread(Thread032);
        thread1.start();
        thread2.start();
    }
}
  1. synchronized死锁问题
    在使用synchronized时需要注意 synchronized锁嵌套的问题,避免死锁的问题发生。
    eg:
public class ThreadDeadlock implements Runnable{
    private int count = 1;
    private String lock = "lock";

    @Override
    public void run() {
        while (true) {
            count++;
            if (count % 2 == 0) {
                // 线程1需要先获取到自定义对象的lock锁,执行test1方法需要再获取this锁;线程2需要先获取this锁,执行test2方法再获取lock锁
                synchronized (lock) {
                    test1();
                }
            } else {
                synchronized (this) {
                    test2();
                }
            }
        }
    }

    public synchronized void test1() {
        System.out.println(Thread.currentThread().getName() + ",test1方法");
    }

    public void test2() {
        synchronized (lock) {
            System.out.println(Thread.currentThread().getName() + ",test2方法");
        }
    }

    public static void main(String[] args) {
        ThreadDeadlock threadDeadlock = new ThreadDeadlock();
        Thread thread1 = new Thread(threadDeadlock);
        Thread thread2 = new Thread(threadDeadlock);
        thread1.start();
        thread2.start();
    }
}
  1. springmvc 接口中使用
    Spring MVC 的Controller默认是单例的,需要注意线程安全问题。
    eg:
@RestController
public class ThreadService {
    private int count = 0;

    @RequestMapping("/test")
    public synchronized String count() {
        try {
            System.out.println(">count<" + count++);
			Thread.sleep(3000);
        } catch (Exception e) {
			e.printStackTrace();
        }
        return "count";
    }
}
  1. 多线程线程之间通讯

等待/通知机制:
等待/通知的相关方法是任意Java对象都具备的,因为这些方法被定义在所有对象的超类java.lang.Object上,方法如下:
(1)notify():通知一个在对象上等待的线程,使其从main()方法返回,而返回的前提是该线程获取到了对象的锁;
(2)notifyAll():通知所有等待在该对象的线程;
(3)wait():调用该方法的线程进入WAITING状态,只有等待其他线程的通知或者被中断,才会返回。需要注意调用wait()方法后,会释放对象的锁 。

注意:wait()、notify()和notifyAll()方法要与synchronized一起使用。

eg:

public class Thread02 {
    private  Object objectLock = new Object();

    public static void main(String[] args) throws InterruptedException {
        new Thread02().print();
    }

    public void print() throws InterruptedException {
        new Thread(new Runnable() {
            @Override
            public void run() {
                synchronized (objectLock){
                    System.out.println(Thread.currentThread().getName()+"---1---");
                    try {
                        objectLock.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println(Thread.currentThread().getName()+"---2---");
                }
            }
        }).start();

        Thread.sleep(3000);
        synchronized (objectLock){
            objectLock.notify();
        }
    }
}
  1. 多线程通讯实现生产者与消费者
    eg:
public class Thread03 {

    class Student {
        public String name;
        public char sex;
        public boolean flag = true;//true:输入,false:输出
    }

    class InputThread extends Thread {
        private Student student;

        public InputThread(Student student) {
            this.student = student;
        }

        @Override
        public void run() {
            int count = 0;
            while (true) {
                synchronized (student) {
                    if (!student.flag) {
                        try {
                            student.wait();
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    }
                    if (count == 0) {
                        student.name = "毛毛";
                        student.sex = '男';
                    } else {
                        student.name = "天天";
                        student.sex = '女';
                    }
                    count = (count + 1) % 2;
                    student.flag = false;
                    student.notify();
                }
            }
        }
    }

    class PrintThread extends Thread {
        private Student student;

        public PrintThread(Student student) {
            this.student = student;
        }

        @Override
        public void run() {
            while (true) {
                synchronized (student) {
                    if (student.flag) {
                        try {
                            student.wait();
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    }
                    System.out.println("姓名:" + student.name + ",性别:" + student.sex);
                    student.flag = true;
                    student.notify();
                }
            }
        }
    }

    public void print() {
        Student student = new Student();
        InputThread inputThread = new InputThread(student);
        PrintThread printThread = new PrintThread(student);
        inputThread.start();
        printThread.start();
    }

    public static void main(String[] args) {
        new Thread03().print();
    }

}