多线程-线程安全问题的解决

在上一篇文章中,我们说了造成线程不安全的原因,那今天我们就来聊聊线程安全问题的解决方案~

目录

一、volatile解决内存可见性和指令重排序

二、锁(synchronized和lock)

2.1 synchronized

2.1.1 synchronized的特性

2.1.2 synchronized的基本使用

2.1.3 synchronized的底层实现

2.1.4 锁升级

2.2 Lock(手动锁/可重入锁)

2.2.1 Lock的实现步骤

2.2.2 Lock公平锁和非公平锁 

2.2.3 Lock的应用

2.3 synchronized VS Lock​​​​​​​

一、volatile解决内存可见性和指令重排序

volatile修饰的变量,能够保证可见性和指令重排序问题。

代码在写入volatile修饰的变量时:

· 改变线程工作内存中volatile变量副本的值

· 将改变后的副本的值从工作内存中刷新到主内存

代码在读取volatile修饰的变量时:

· 从主内存读取volatile变量的最新值到线程的工作内存中

· 从工作内存中读取 volatile变量的副本

 

这样子的工作流程就解决了内存可见性问题,加上了volatile强制读写内存,速度是慢了,但是数据变得更准确了。

代码演示volatile用途:

public class ThreadDemo16 {
    private static volatile boolean flag=true;
    public static void main(String[] args) {
        Thread thread1=new Thread(()->{
            System.out.println("开始执行:"+ LocalDateTime.now());
            while (flag){
            }
            System.out.println("flag被修改为false"+LocalDateTime.now());
        });
        thread1.start();
        Thread thread2=new Thread(()->{
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("将flag修改为false:"+LocalDateTime.now());
            flag=false;
        });
        thread2.start();
    }
}

volatile缺点:

volatile虽然可以解决内存可见性问题和指令重排序的问题,但还是不能保证原子性,对于++和--这种非原子性的问题还是解决不了。

如下代码所示:

public class ThreadDemoVolatile {
    static class Counter{
        private volatile int num=0;
        private  int MAX_COUNT=0;
        public Counter(int MAX_COUNT){
            this.MAX_COUNT=MAX_COUNT;
        }
        //++方法
        public void increment(){
            for (int i = 0; i < MAX_COUNT; i++) {
                num++;
            }
        }
        //--方法
        public void  decrement(){
            int temp=0;
            for (int i = 0; i < MAX_COUNT; i++) {
                num--;
            }
        }
        public  int getNum(){
            return num;
        }
    }
    public static void main(String[] args) throws InterruptedException {
        ThreadDemo15.Counter counter=new ThreadDemo15.Counter(100000);
        Thread  thread1=new Thread(()->{
            counter.increment();
        });
        Thread thread2=new Thread(()->{
            counter.decrement();
        });
        thread1.start();
        thread2.start();
        thread1.join();
        thread2.join();
        System.out.println("最终结果:"+counter.getNum());

    }
}

因此我们就引出了锁的概念~

二、锁(synchronized和lock)

java中的锁是解决线程安全问题的最主要手段,锁主要分为两种:①内存锁synchronized  ②可重入锁lock。

2.1 synchronized

2.1.1 synchronized的特性

1)  互斥性(排他性)

synchronized会起到互斥的作用,某个线程执行到某个对象的synchronized中时,其他线程如果也执行到同一个对象的synchronized就会阻塞等待

· 进入synchronized修饰的代码块相当于加锁

· 退出synchronized修饰的代码块相当于解锁

2) 刷新内存

 synchronized的工作过程:

1.获得互斥锁

2.从主内存拷贝变量的最新副本到工作内存中

3.执行代码

4.将更改后的共享变量的值刷新到主内存

5.释放主内存

由上述的工作过程可以看出来,synchronized也可以解决内存可见性问题。

代码示例:

import java.time.LocalDateTime;

public class ThreadDemo16 {
    static class Counter{
        public boolean flag=true;
    }
    public static void main(String[] args) {
        Counter counter=new Counter();
        Thread thread1=new Thread(()->{
            System.out.println("开始执行:"+ LocalDateTime.now());
                while (true) {
                    synchronized (counter) {
                        if(!counter.flag){
                            System.out.println("flag被修改为false"+LocalDateTime.now());
                            break;
                        }
                    }
                }
        });
        thread1.start();
        Thread thread2=new Thread(()->{
            synchronized (ThreadDemo16.class) {
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("将flag修改为false:"+LocalDateTime.now());
            counter.flag= false;
            }
        });
        thread2.start();
    }
}

3) 可重入性

synchronized对于同一条线程来说是可重入的,不会出现把自己锁死的问题。

/**
 * synchronized的可重入性测试
 */
public class ThreadSynchronized4 {
    public static void main(String[] args) {
        synchronized (ThreadSynchronized4.class) {
            System.out.println("当前线程已经得到了锁");
            synchronized (ThreadSynchronized4.class) {
                System.out.println("当前线程再次得到了锁");
            }
        }
    }
}

synchronized使用的锁是存在java对象头中的。

2.1.2 synchronized的基本使用

1) 修饰普通方法

/**
 * Synchronized修饰普通方法
 */
public class ThreadSynchronized2 {
    private static int num=0;
    static class Counter{
        private static int MAX_COUNT=100000;
        //++方法
        public  synchronized void increment(){
            for (int i = 0; i < MAX_COUNT; i++) {
                num++;
            }
        }
        //--方法
        public  synchronized void decrement(){
            int temp=0;
            for (int i = 0; i < MAX_COUNT; i++) {
                num--;
            }
        }
    }
    public static void main(String[] args) throws InterruptedException {
        Counter counter=new Counter();
        Thread  thread1=new Thread(()->{
            counter.increment();
        });
        Thread thread2=new Thread(()->{
            counter.decrement();
        });
        thread1.start();
        thread2.start();
        thread1.join();
        thread2.join();
        System.out.println("最终结果:"+num);
    }
}

2) 修饰静态方法

/**
 * Synchronized修饰静态方法
 */
public class ThreadSynchronized {
    private static int num=0;
    static class Counter{
        private static int MAX_COUNT=100000;
        //++方法
        public static synchronized void increment(){
            for (int i = 0; i < MAX_COUNT; i++) {
                num++;
            }
        }
        //--方法
        public static synchronized void decrement(){
            int temp=0;
            for (int i = 0; i < MAX_COUNT; i++) {
                num--;
            }
        }
    }
    public static void main(String[] args) throws InterruptedException {
        Thread  thread1=new Thread(()->{
            Counter.increment();
        });
        Thread thread2=new Thread(()->{
            Counter.decrement();
        });
        thread1.start();
        thread2.start();
        thread1.join();
        thread2.join();
        System.out.println("最终结果:"+num);
    }
}


3)修饰代码块

注意事项:

1.修饰代码块时,对于同一个业务的多个对象的加锁对象,注意加锁的应是同一个对象(即加同一把锁);

2.synchronized修饰代码块,代码块在静态方法中时,不能使用this。

 synchronized中的对象可以有以下三种形式:

public class SynchronizedDemo {
    public void method(){
        //1.使用this锁当前对象
        synchronized (this){
        }
        
        //2.锁当前类对象
        synchronized (SynchronizedDemo.class){
        }
        
        //3.自定义的锁对象
        Object obj=new Object();
        synchronized (obj){
        }
    }
}
/**
 * Synchronized修饰代码块
 */
public class ThreadSynchronized3 {
    private static int num=0;
    static class Counter{
        //自定义锁对象
        private Object myLock=new Object();
        private static int MAX_COUNT=100000;
        //++方法
        public void increment(){
            for (int i = 0; i < MAX_COUNT; i++) {
                synchronized (myLock) {
                    num++;
                }
            }
        }
        //--方法
        public void decrement(){
            int temp=0;
            for (int i = 0; i < MAX_COUNT; i++) {
                synchronized (myLock) {
                    num--;
                }
            }
        }
        public static void fun(){
            synchronized (Counter.class){
                num++;
            }
        }
    }
    public static void main(String[] args) throws InterruptedException {
        Counter counter=new Counter();
        Thread  thread1=new Thread(()->{
            counter.increment();
        });
        Thread thread2=new Thread(()->{
            counter.decrement();
        });
        thread1.start();
        thread2.start();
        thread1.join();
        thread2.join();
        System.out.println("最终结果:"+num);
    }
}

2.1.3 synchronized的底层实现

· 在JVM层面synchronized是依靠监视器实现的;

·  从操作系统层面来看,synchronized是基于操作系统的互斥锁(mutex)实现的。

监视器是一个概念或者说是一个机制,它用来保障在任何时候,只有一个线程能够执行指定区域的代码。

下面,我们来看以下synchronized在字节码层面是如何实现的:

public class SynchronizedToMonitorExample {
    public static void main(String[] args) {
        int count = 0;
        synchronized (SynchronizedToMonitorExample.class) {
            for (int i = 0; i < 10; i++) {
                count++;
            }
        }
        System.out.println(count);
    }
}

将上述代码翻译成字节码之后是这样的:

 从上述结果中我们看出,在main方法中多了monitorenter和moniterexit两个指令,他们分别表示进入监视器和退出监视器,由此得出在JVM层面synchronized是依靠监视器实现的。

JVM监视器的执行流程:

在Java中,synchronized是非公平锁,也是可重入锁。

非公平锁是指:线程获取锁的顺序不是按照访问顺序先来先得的,而是由线程自己竞争,随机获取到锁。

可重入锁是指:一个线程获取到锁之后,可以重复得到该锁。

在Java虚拟机中(hotSpot)中,Monitor 底层是由 C++实现的,它的实现对象是 ObjectMonitor,
ObjectMonitor 结构体的实现如下:

ObjectMonitor::ObjectMonitor() {
        _header = NULL;
        _count = 0;
        _waiters = 0,
        _recursions = 0; //线程的重⼊次数
        _object = NULL;
        _owner = NULL; //标识拥有该monitor的线程
        _WaitSet = NULL; //等待线程组成的双向循环链表,_WaitSet是第⼀个节点
        _WaitSetLock = 0 ;
        _Responsible = NULL ;
        _succ = NULL ;
        _cxq = NULL ; //多线程竞争锁进⼊时的单向链表
        FreeNext = NULL ;
        _EntryList = NULL ; //_owner从该双向循环链表中唤醒线程结点,_EntryList
        是第⼀个节点
        _SpinFreq = 0 ;
        _SpinClock = 0 ;
        OwnerIsThread = 0 ;
        }

监视器执⾏的流程如下:
1. 线程通过 CAS(对比并替换)尝试获取锁,如果获取成功,就将 _owner 字段设置为当前线程,说明当前线程已经持有锁,并将 _recursions 重⼊次数的属性 +1。如果获取失败则先通过⾃旋 CAS尝试获取锁,如果还是失败则将当前线程放⼊到 EntryList 监控队列(阻塞)。
2. 当拥有锁的线程执⾏了 wait ⽅法之后,线程释放锁,将 owner 变量恢复为 null 状态,同时将该线程放入 WaitSet 待授权队列中等待被唤醒。
3. 当调用 notify ⽅法时,随机唤醒 WaitSet 队列中的某⼀个线程,当调用 notifyAll 时唤醒所有的
WaitSet 中的线程尝试获取锁。
4. 线程执行完释放了锁之后,会唤醒 EntryList 中的所有线程尝试获取锁。
以上就是监视器的执⾏流程,执⾏流程如下图所示:

2.1.4 锁升级

在jdk1.6之前synchronized使用的较少,因为synchronized默认使用重量级锁实现,所以性能较差。jdk1.6对synchronized做了优化,实现了锁升级。

无锁  ->  偏向锁  ->  轻量级锁  ->  重量级锁

2.2 Lock(手动锁/可重入锁)

2.2.1 Lock的实现步骤

1.创建Lock 2.加锁lock.lock()  3.释放锁lock.unlock()。

import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

/**
 * 手动锁(可重入锁)
 */
public class ThreadLock {
    public static void main(String[] args) {
        //1、创建锁对象
        Lock lock=new ReentrantLock();

        //2、加锁操作
        lock.lock();

        try {
            //业务代码(可能会非常复杂->导致异常)
            System.out.println("你好,ReentrantLock");
        }finally {//unlock一定要放在finally中
            //3.释放锁
            lock.unlock();
        }
    }
}

注意事项:

1.释放锁unlock操作一定要放在finally代码块中,防止因为业务代码有异常直接结束执行,而导致的锁资源永久占用的问题。

2.加锁lock.lock()一定要放在try之前,或者是try的首行。原因有两个:①如果没放在try的首行或try前面,如果因为try中代码异常导致加锁失败,还会执行finally中释放锁的操作;②释放锁的异常会覆盖try中的业务异常,增加排查难度。

import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

/**
 * 手动锁(可重入锁)
 */
public class ThreadLock {
    public static void main(String[] args) {
        //1、创建锁对象
        Lock lock=new ReentrantLock();
        
        try {
            //业务代码(可能会非常复杂->导致异常)
            int y=10/0;
            //2、加锁操作
            lock.lock();

        }finally {//unlock一定要放在finally中
            //3.释放锁
            lock.unlock();
        }
    }
}

2.2.2 Lock公平锁和非公平锁 

Lock可以指定锁的类型,默认情况下创建一个非公平锁,非公平锁的执行效率较高。

创建锁时传递参数true会创建一个公平锁。

源码如下:

2.2.3 Lock的应用

应用场景:对0进行10万次的++操作,再进行10万次的--操作。看一下公平锁和非公平锁的性能。

公平锁:

import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

public class ThreadLock2 {
    private static int num=0;
    static class Counter{
        //1.创建锁对象
        private Lock lock=new ReentrantLock(true);
        private static int MAX_COUNT=100000;
        //++方法
        public void increment(){
            for (int i = 0; i < MAX_COUNT; i++) {
                //2.加锁操作
                lock.lock();
                try {
                    num++;
                }finally {
                    //3.释放锁
                    lock.unlock();
                }
            }
        }
        //--方法
        public void decrement(){
            int temp=0;
            for (int i = 0; i < MAX_COUNT; i++) {
                //2.加锁操作
                lock.lock();
                try {
                    num--;
                }finally {
                    //3.释放锁
                    lock.unlock();
                }
            }
        }
    }
    public static void main(String[] args) throws InterruptedException {
        Counter counter=new Counter();
        long stime=System.currentTimeMillis();
        Thread  thread1=new Thread(()->{
            counter.increment();
        });
        Thread thread2=new Thread(()->{
            counter.decrement();
        });
        thread1.start();
        thread2.start();
        thread1.join();
        thread2.join();
        long etime=System.currentTimeMillis();
        System.out.println("最终结果:" + num + "| 执行时间:" + (etime-stime));
    }
}

 非公平锁:

import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

public class ThreadLock2 {
    private static int num=0;
    static class Counter{
        //1.创建锁对象
        private Lock lock=new ReentrantLock();
        private static int MAX_COUNT=100000;
        //++方法
        public void increment(){
            for (int i = 0; i < MAX_COUNT; i++) {
                //2.加锁操作
                lock.lock();
                try {
                    num++;
                }finally {
                    //3.释放锁
                    lock.unlock();
                }
            }
        }
        //--方法
        public void decrement(){
            int temp=0;
            for (int i = 0; i < MAX_COUNT; i++) {
                //2.加锁操作
                lock.lock();
                try {
                    num--;
                }finally {
                    //3.释放锁
                    lock.unlock();
                }
            }
        }
    }
    public static void main(String[] args) throws InterruptedException {
        Counter counter=new Counter();
        long stime=System.currentTimeMillis();
        Thread  thread1=new Thread(()->{
            counter.increment();
        });
        Thread thread2=new Thread(()->{
            counter.decrement();
        });
        thread1.start();
        thread2.start();
        thread1.join();
        thread2.join();
        long etime=System.currentTimeMillis();
        System.out.println("最终结果:" + num + "| 执行时间:" + (etime-stime));
    }
}

由此我们可以看出,非公平锁的性能要好于公平锁的性能。

2.3 synchronized VS Lock

1.synchronized是JVM层面提供的锁,它是自动进行加锁和释放锁操作,对于开发者是无感的,而 Lock需要开发者自己进行加锁和释放锁的操作。

2.锁的类型不同,Lock默认是非公平锁,但可以设置为公平锁,而synchronized只能是非公平锁。

3.Lock更灵活,有更多的方法:比如tryLock()...

public class ThreadLock4 {
    public static void main(String[] args) throws InterruptedException {
        Lock lock=new ReentrantLock();
        for (int i = 0; i < 15; i++) {
            //每次等待1+i的时间获取锁,如果超时未获取到,返回false,否则返回true
            boolean result = lock.tryLock(1+i, TimeUnit.SECONDS);
        }
    }
}

4.调用lock()方法和synchronized线程等待锁的状态不同,lock方法会让线程状态变为waiting,而synchronized会让线程状态变成blocked

使用synchronized加锁:

public class ThreadLock4 {
    public static void main(String[] args) throws InterruptedException {
        Lock lock=new ReentrantLock();

        Thread t1=new Thread(()->{
            synchronized (ThreadLock4.class) {
                System.out.println("线程1得到了锁");
                try {
                    Thread.sleep(3000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println("线程1释放了锁");
            }
        });
        t1.start();

        Thread t2=new Thread(()->{
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
           synchronized (ThreadLock4.class) {
                System.out.println("线程2获取到了锁");
           }
        });
        t2.start();
        Thread.sleep(1500);
        System.out.println("线程2的状态:"+t2.getState());

    }
}

调用lock方法加锁:

import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

public class ThreadLock4 {
    public static void main(String[] args) throws InterruptedException {
        Lock lock=new ReentrantLock();

        Thread t1=new Thread(()->{
            lock.lock();
            System.out.println("线程1得到了锁");
            try {
                Thread.sleep(3000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }finally {
                System.out.println("线程1释放锁");
                lock.unlock();
            }
        });
        t1.start();

        Thread t2=new Thread(()->{
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
           lock.lock();
            try {
                System.out.println("线程2获取到了锁");
            }finally {
                lock.unlock();
            }
        });
        t2.start();
        Thread.sleep(1500);
        System.out.println("线程2的状态:"+t2.getState());

    }
}

5.synchronized可以修饰方法(静态方法、普通方法)和代码块,而Lock只能修饰代码块。

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值