并发编程线程安全问题

发布时间:2023年12月20日

并发编程的原则:设计并发编程的目的是为了使程序获得更高的执行效率,但绝不能出现数据一致性(数据准确)问题,如果并发程序连最基本的执行结果准确性都无法保证,那并发编程就没有任何意义。

如何控制多线程操作共享数据引起的数据准确性问题呢?

使用“序列化访问临界资源”的方案,即在同一时刻,只能有一个线程访问临界资源,也称作同步互斥访问,也就是**保证我们的共享资源每次只能被一个线程使用,一旦该资源被线程使用,其他线程将不得拥有使用权。**在Java中,提供了两种方式来实现同步互斥访问:synchronized和Lock。

互斥访问——synchronized(同步方法或同步块)

互斥锁:顾名思义,就是互斥访问目的的锁

在java里每个对象都拥有一个锁标记(monitor),也称为监视器。

多线程同时访问某对象时,只有拥有该对象锁的线程才能访问。

在java中可以使用synchronized关键字来标记一个需要同步方法或者同步代码块,当某线程调用该对象的synchronized方法或者访问synchronized代码块,该线程便获得了该对象的锁,其他线程暂时无法访问该对象的锁,只有等待这个方法执行完毕或者代码块执行完毕,这个线程才会释放该对象的锁,其他线程才能执行这个方法或者代码块。通过这个方法达到我们上面提到的在同一时刻,只有一个线程能访问临界资源

**synchronized用法:**分析synchronized同步锁的核心在于他是个对象锁,找清楚锁的对象

synchronized是对象锁,即线程获得的锁是施加在一个实例对象上的,如果不同的线程访问的是同一对象上的不同的同步方法,那么显然不能同时进行。
如果是不同对象上的不同的同步方法,那么就是可以同时进行的。

Synchronized 的基本使用

Synchronized 的作用主要有三个:

  • 确保线程互斥的访问同步代码
  • 保证共享变量的修改能够及时可见
  • 有效解决重排序问题

从语法上讲,Synchronized 总共有三种用法:

  • 修饰普通方法
  • 修饰静态方法
  • 修饰代码块
  1. 同步代码块

    sychronized(synObject){
        
    }
    

    将synchronized作用于一个给定的对象或者类的一个属性,每当有线程执行到这段代码块,该线程会先请求获取对象synObject的锁,如果该锁已被占用,那么新线程只能等待,从而使得其他线程无法同时访问代码块。

    /*使用当前对象作为互斥锁
    */
    public class SynchronizedTest implements Runnable{
        /*在java中可有两种方式实现多线程,一种是继承Thread类,一种是实现Runnable接口
        Thread类是在java.lang包中定义的。一个类只要继承了Thread类同时覆写了本类中的run()方法就可以实现多线程操作了,但是一个类只能继承一个父类,这是此方法的局限。
    在使用Runnable定义的子类中没有start()方法,只有Thread类中才有。
    */
        private static volatile int m=0;
        /*
        Java提供了volatile关键字来保证可见性。
      当一个共享变量被volatile修饰时,它会保证修改的值会立即被更新到主存,当有其他线程需要读取时,它会去内存中读取新值。
      通过synchronized和Lock也能够保证可见性,synchronized和Lock能保证同一时刻只有一个线程获取锁然后执行同步代码,并且在释放锁之前会将对变量的修改刷新到主存当中。因此可以保证可见性。
       1)保证了不同线程对这个变量进行操作时的可见性,即一个线程修改了某个变量的值,这新值对其他线程来说是立即可见的。
        2)禁止进行指令重排序。
        */
        public static void main(String[] args) {
            Runnable run=new SynchronizedTest();
            Thread thread1=new Thread(run);
            Thread thread2=new Thread(run);
            thread1.start();
            thread2.start();
            try {
                //join() 使main线程等待这连个线程执行结束后继续执行下面的代码
                thread1.join();
                thread2.join();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("m的最终结果:"+m);
        }
        public void run() {
            synchronized (this) {
                for(int i=0;i<10000;i++){
                    m++;
                }
            }
        }
    }
    

    特别注意:

    • Java中真正能创建新线程的只有Thread类对象
    • 通过实现Runnable的方式,最终还是通过Thread类对象来创建线程

    所以对于 实现了Runnable接口的类,称为 线程辅助类Thread类才是真正的线程类

    // 步骤1:创建线程辅助类,实现Runnable接口
     class MyThread implements Runnable{
        ....
        @Override
    // 步骤2:复写run(),定义线程行为
        public void run(){
        }
    

    1.1:Synchronized是一个重量级锁
    Synchronized 是通过对象内部的一个叫做监视器锁(monitor)来实现的,监视器锁本质又是依赖于底层的操作系统的 Mutex Lock(互斥锁)来实现的。而操作系统实现线程之间的切换需要从用户态转换到核心态,这个成本非常高,状态之间的转换需要相对比较长的时间,这就是为什么 Synchronized 效率低的原因。因此,这种依赖于操作系统 Mutex Lock 所实现的锁我们称之为 “重量级锁”。

    1.2:Synchronized底层实现原理
    同步方法通过ACC_SYNCHRONIZED 关键字隐式的对方法进行加锁。当线程要执行的方法被标注上ACC_SYNCHRONIZED时,需要先获得锁才能执行该方法。
    同步代码块通过monitorenter和monitorexit执行来进行加锁。当线程执行到monitorenter的时候要先获得锁,才能执行后面的方法。当线程执行到monitorexit的时候则要释放锁。每个对象自身维护着一个被加锁次数的计数器,当计数器不为0时,只有获得锁的线程才能再次获得锁。

    lock

    synchronized修饰的代码块,当一个线程获取了对应的锁,并执行该代码块时,其他线程便只能一直等待,等待获取锁的线程释放锁,如果没有释放则需要无限的等待下去。获取锁的线程释放锁只会有两种情况:

    1、获取锁的线程执行完了该代码块,然后线程释放对锁的占有。

    2、线程执行发生异常,此时JVM会让线程自动释放锁。

    Lock与synchronized对比:

    1、Lock不是Java语言内置的,synchronized是Java语言的关键字,因此是内置特性。Lock是一个类,通过这个类可以实现同步访问。

    2、synchronized不需要手动释放锁,当synchronized方法或者synchronized代码块执行完之后,系统会自动让线程释放对锁的占用;而Lock则必须要用户去手动释放锁,如果没有主动释放锁,就有可能导致出现死锁现象。

    lock常用接口

    public interface Lock {
        //用来获取锁。如果锁已被其他线程获取,则进行等待。
        void lock();
       // 当通过这个方法去获取锁时,如果线程正在等待获取锁,则这个线程能够响应中断,即中断线程的等待状态
        void lockInterruptibly() throws InterruptedException;
        //它表示用来尝试获取锁,如果获取成功,则返回true,如果获取失败(即锁已被其他线程获取),则返回false
        boolean tryLock();
        //与tryLock()方法是类似的,只不过区别在于这个方法在拿不到锁时会等待一定的时间,在时间期限之内如果还拿不到锁,就返回false。如果如果一开始拿到锁或者在等待期间内拿到了锁,则返回true。
        boolean tryLock(long time, TimeUnit unit) throws InterruptedException;
        //释放锁
        void unlock();
        Condition newCondition();
    }
    

    lock与unlock

    一般使用Lock时必须在try{}catch{}中进行,并且将释放锁的操作放在finally中,以保证锁一定被释放,防止死锁发送。

    lock锁使用

    Lock接口的常用实现类有ReentrantLock和ReentrantReadWriteLock,它们提供了可重入的互斥锁和读写锁。

    使用Lock锁的一般步骤如下:

1. 创建一个`Lock`对象实例。
    Lock lock = new ReentrantLock();
2. 在同步的代码块执行完之后,通过调用`unlock()`方法释放锁。
    lock.lock();
    try{
        ....
    } finally{
        lock.unlock();
    }
文章来源:https://blog.csdn.net/m0_53098280/article/details/135085490
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。