多线程(五):解决线程不安全方案

Posted 头发都哪去了

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了多线程(五):解决线程不安全方案相关的知识,希望对你有一定的参考价值。

线程不安全原因解决方案
①CPU抢占 执行(万恶之源)无法解决
②代码非原子性在关键代码处,让使用的CPU排队执行(加锁)
③(内存)不可见可使用 volatile 关键字
④编译器/代码优化(指令重排序)可使用 volatile 关键字
⑤多个线程同时修改了同一个变量不通用,修改难度大

volatile关键字

volatile 关键字 轻量级解决线程不安全的方案
代码示例如下:

public class ThreadDemo29 {
    private static volatile boolean flag = false;

    public static void main(String[] args) {
        Thread t1 = new Thread(new Runnable() {
            @Override
            public void run() {
                while (!flag) {

                }
                System.out.println("终止执行");
            }
        });
        t1.start();

        Thread t2 = new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println("设置flag = true");
                flag = true;
            }
        });
        t2.start();
    }
}

该代码执行结果为:
在这里插入图片描述

我们发现,此代码与上篇博客中的ThreadDemo27的代码基本相同,就是在就是在定义全局变量 flag 时,添加了 volatile 关键字,通过解决内存不可见的方法,解决了线程不安全的问题。

volatile 作用:
①禁止指令重排序
②解决线程可见性的问题,实现原理:当操作完变量之后,强制删除掉线程工作内存中的此变量。

注意:
volatile 关键字,无法解决多线程非原子性问题。

public class ThreadDemo30 {

    static class Counter {
        //定义私有变量
        private volatile int num = 0;
        //定义任务执行次数
        private final int maxSize = 100000;

        //num++
        public void incrment() {
            for (int i = 0; i < maxSize; i++) {
                num++;
            }
        }

        //num--
        public void decrment() {
            for (int i = 0; i < maxSize; i++) {
                num--;
            }
        }

        public int getNum() {
            return num;
        }
    }

    public static void main(String[] args) throws InterruptedException {
        Counter counter = new Counter();

        Thread t1 = new Thread(() -> {
            counter.incrment();
        });
        t1.start();

        Thread t2 = new Thread(() -> {
            counter.decrment();
        });
        t2.start();
        
        t1.join();
        t2.join();

        System.out.println("最终执行结果:" + counter.getNum());
    }
}

代码执行结果:
在这里插入图片描述
可见,volatile 关键字,无法解决多线程非原子性问题,进而无法解决线程非安全。

锁操作

Java中解决线程安全操作(锁的操作)

1.使用 synchronized 关键字来加锁和释放锁【JVM层面的解决方案,自动帮我们进行加锁和释放锁的操作】
2.Lock 手动锁【Java层面的解决方案,需要程序员自己去加锁和释放锁】

公平锁与非公平锁

公平锁可以按顺序进行执行,而非公平锁执行的效率更高。在Java中所有锁默认的策略都是非公平锁。
synchronized 的锁机制是非公平锁。
Lock 默认的锁策略也是非公平锁,但是 Lock 也可以声明为公平锁。

锁操作的关键步骤

1.尝试获取(如果成功拿到锁,加锁,进行排队等待)
2.释放锁

synchronized 的使用

synchronized的底层是使用操作系统的mutex lock实现的。

1.当线程释放锁时,JMM会把该线程对应的工作内存中的共享变量刷新到主内存中
2.当线程获取锁时,JMM会把该线程对应的本地内存置为无效。从而使得被监视器保护的临界区代码必须从主内存中读取共享变量

synchronized用的锁是存在Java对象头里的。

synchronized同步快对同一条线程来说是可重入的,不会出现自己把自己锁死的问题;
同步块在已进入的线程执行完之前,会阻塞后面其他线程的进入。

程序的关键操作加锁,示例代码如下:

public class ThreadDemo31 {
    //全局变量
    private static int number = 0;
    //定义循环次数
    private static final int maxSize = 100000;

    public static void main(String[] args) throws InterruptedException {
        //创建锁
        Object lock = new Object();

        //线程1:自增10W次
        Thread t1 = new Thread(new Runnable() {
            @Override
            public void run() {
                for (int i = 0; i < maxSize; i++) {
                    //实现加锁操作
                    synchronized (lock) {
                        number++;
                    }

                }
            }
        });
        t1.start();

        //线程2:自减10W次
        Thread t2 = new Thread(new Runnable() {
            @Override
            public void run() {
                for (int i = 0; i < maxSize; i++) {
                    synchronized (lock) {
                        number--;
                    }
                }
            }
        });
        t2.start();

        //等待线程1和线程2执行完毕
        t1.join();
        t2.join();

        System.out.println("最终执行结果:" + number);

    }
}

注意事项:在进行加锁操作的时候,同一组业务必须为共同的锁对象。
该代码的执行结果如下:
在这里插入图片描述
我们发现,此程序就是线程安全的。
在这里插入图片描述
synchronized 实现原理:
1.操作:互斥锁 mutex
在这里插入图片描述

2.JVM:帮我们实现的监视器锁的加锁和释放锁的操作
在这里插入图片描述

3.Java:
a) 锁对象 mutex
b) 锁存放的地方:变量的对象头
在这里插入图片描述

synchronized 在 JDK 6 之前,使用重量级锁实现的,性能非常低,所以用到的并不多。
JDK 6 对 synchronized 做了优化(锁升级 )
在这里插入图片描述

synchronized 的使用场景:
1.使用 synchronized 来修饰代码块 (加锁对象可以自定义)
上述 ThreadDemo31 就是 synchronized 来修饰代码块的使用场景

2.使用 synchronized 来修饰静态方法,示例如下:

public class ThreadDemo35 {
    //全局变量
    private static int number = 0;
    //定义循环次数
    private static final int maxSize = 100000;

    public static synchronized void incrment() {
        for (int i = 0; i < maxSize; i++) {
            number++;
        }
    }

    public static synchronized void decrment() {
        for (int i = 0; i < maxSize; i++) {
            number--;
        }
    }

    public static void main(String[] args) throws InterruptedException {

        //线程1:自增10W次
        Thread t1 = new Thread(new Runnable() {
            @Override
            public void run() {
                incrment();
            }
        });
        t1.start();

        //线程2:自减10W次
        Thread t2 = new Thread(new Runnable() {
            @Override
            public void run() {
                decrment();
            }
        });
        t2.start();

        //等待线程1和线程2执行完毕
        t1.join();
        t2.join();

        System.out.println("最终执行结果:" + number);

    }
}

该代码的执行结果:
在这里插入图片描述
我们发现,使用synchronized 来修饰静态方法,也能使该程序线程安全。

3.使用 synchronized 可以用来修饰普通方法(加锁对象是当前类的实例),示例如下:

public class ThreadDemo35 {
    //全局变量
    private static int number = 0;
    //定义循环次数
    private static final int maxSize = 100000;

    //    public static synchronized void incrment() {
    public synchronized void incrment() {
        for (int i = 0; i < maxSize; i++) {
            number++;
        }
    }

    //    public static synchronized void decrment() {
    public synchronized void decrment() {
        for (int i = 0; i < maxSize; i++) {
            number--;
        }
    }

    public static void main(String[] args) throws InterruptedException {

        ThreadDemo35 threadDemo35 = new ThreadDemo35();

        //线程1:自增10W次
        Thread t1 = new Thread(new Runnable() {
            @Override
            public void run() {
                threadDemo35.incrment();
            }
        });
        t1.start();

        //线程2:自减10W次
        Thread t2 = new Thread(new Runnable() {
            @Override
            public void run() {
                threadDemo35.decrment();
            }
        });
        t2.start();

        //等待线程1和线程2执行完毕
        t1.join();
        t2.join();

        System.out.println("最终执行结果:" + number);

    }
}

该代码的执行结果:
在这里插入图片描述
我们发现,使用synchronized 来修饰普通方法,也能使该程序线程安全。

Lock 的使用

Lock 的使用,示例代码如下:

public class ThreadDemo32 {
    //全局变量
    private static int number = 0;
    //定义循环次数
    private static final int maxSize = 100000;

    public static void main(String[] args) throws InterruptedException {

        //1.创建手动锁
        Lock lock = new ReentrantLock();

        //线程1:自增10W次
        Thread t1 = new Thread(new Runnable() {
            @Override
            public void run() {
                for (int i = 0; i < maxSize; i++) {
                    //2.加锁
                    lock.lock();
                    try {
                        number++;
                    } finally {
                        //3.释放锁
                        lock.unlock();
                    }
                }
            }
        });
        t1.start();

        //线程2:自减10W次
        Thread t2 = new Thread(new Runnable() {
            @Override
            public void run() {
                for (int i = 0; i < maxSize; i++) {
                    lock.lock();
                    try {
                        number--;
                    } finally {
                        lock.unlock();
                    }
                }
            }
        });
        t2.start();

        //等待线程1和线程2执行完毕
        t1.join();
        t2.join();

        System.out.println("最终执行结果:" + number);

    }
}

注意事项:一定要把 lock( ) 放在 try 外面,原因如下:
1.如果将1ock()方法放在 try 里面,那么当 try 里面的代码出现异常之后,那么就会执行 finally 里面的释放锁的代码,但这个时候加锁还没成功,就去释放锁。
2.如果将 lock( ) 方法放在try里面,那么当执行 finally 里面释放锁的代码的时候就会报错(线程状态异常),释放锁的异常会覆盖掉业务代码的异常报错,从而增加了排除错误成本。

演示:将 lock( ) 方法放入 try 里面,示例代码如下:

public class ThreadDemo33 {
    public static void main(String[] args) {

        Lock lock = new ReentrantLock();

        try {
            int num = 1 / 0;//异常业务
            lock.lock();
        } finally {
            lock.unlock();
        }
    }
}

该代码的执行结果如下:
在这里插入图片描述
我们发现,该程序执行时会报错,且异常类型为锁操作异常,并非业务异常信息。
如果将lock()方法放在try的外面,示例代码如下:

public class ThreadDemo33 {
    public static void main(String[] args) {

        Lock lock = new ReentrantLock();

        lock.lock();
        try {
            int num = 1 / 0;//异常业务
        } finally {
            lock.unlock();
        }
    }
}

该代码的执行结果如下:
在这里插入图片描述
我们发现:异常类型为我们预期的业务异常类型。

Lock 声明公平锁示例代码如下:

public class ThreadDemo34 {
    public static void main(String[] args) throws InterruptedException {

        //声明一个公平锁
        Lock lock = new ReentrantLock(true);

        //业务逻辑处理:打印AABBCCDD
        //公共任务
        Runnable runnable = new Runnable() {
            @Override
            public void run() {
                for (char item : "ABCD".toCharArray()) {
                    lock.lock();
                    try {
                        System.out.print(item);
                    } finally {
                        lock.unlock();
                    }

                }
            }
        };

        Thread t1 = new Thread(runnable, "t1");
        Thread t2 = new Thread(runnable, "t1");

        Thread.sleep(10);
        t1.start();
        t2.start();

    }
}

该代码执行结果为:
在这里插入图片描述
Lock 的使用场景:只能用来修饰代码块。

小结

volatile 和 synchronized 有什么区别?

  1. volatile 可以解决内存可见性问题和禁止指令重排序,但 volatile 不能解决原子性问题;
  2. synchronized 可以解决任何关于线程安全的问题(关键代码排队执行,始终只有一个线程会执行加锁操作;原子性问题…)

synchronized 和 Lock 有什么区别?

  1. synchronized 既可以修饰代码块,又可以修饰静态方法和普通方法;而 Lock 只能修饰代码块。
  2. synchronized 只有非公平锁的锁策略,而 Lock(ReentrantLock) 默认也是非公平锁策略,也可以通过构造函数声明成公平锁。
  3. 使用 Lock(ReentrantLock) 更加灵活(比如 tryLock )。
  4. synchronized 是自动加锁释放锁的,而 Lock(ReentrantLock) 需要程序员来加锁和手动释放锁的。

死锁问题

基本概念

线程和锁的关系(一对多):一个线程可以拥有多把锁;但是一个锁只能被一个线程拥有。

定义:在多线程编程中(两个或两个以上的线程),因为资源抢占,造成线程无限等待的问题。
在这里插入图片描述
死锁问题,示例代码:

public class ThreadDemo36 {
    public static void main(String[] args) {
        //创建锁A(资源A)
        Object lockA = new Object();
        //创建锁B(资源B)
        Object lockB = new Object();

        Thread t1 = new Thread(new Runnable() {
            @Override
            public void run() {
                //线程1得到锁A
                synchronized (lockA) {
                    System.out.println(以上是关于多线程(五):解决线程不安全方案的主要内容,如果未能解决你的问题,请参考以下文章

Java并发多线程编程——集合类线程不安全之HashMap的示例及解决方案

Java并发多线程编程——集合类线程不安全之HashSet的示例及解决方案

Java并发多线程编程——集合类线程不安全之ArrayList的示例及解决方案

Java中线程安全问题

多线程基础线程安全解决方案

SimpleDateFormat线程不安全的5种解决方案!