Java学习(十七)多线程安全问题

前文回顾

上一次我们讲过了在操作系统中,进程是一个非常重要的概念,它是计算机程序运行的基础。我们使用计算机应用程序的时候,操作系统实际上管理着这个应用程序背后运行的进程。我们现在来总结一下,进程有以下几个特点:

  • 进程有创建、就绪、运行、阻塞、退出五个状态;
  • 进程在创建态的时候,需要分配资源,在就绪态的时候,资源进入内存,准备等待获取CPU时间片,执行程序;
  • 进程与进程间需要通信,通信的方式通常包括管道、共享内存、PV量、socket等等;
  • 进程与进程间的切换需要调用内核,时间消耗通常比较久;

基于此,在进程中引入了线程。同一个进程中的多个线程只保存少量自己的数据,可以共享进程内的I/O读取文件以及其他数据。在同一个进程中的多个线程进行切换不需要调用内核,执行效率高。

上文的结尾我们讲了Java中四种实现多线程启动的方式。分别是:

  • 继承Thread类,编写run()方法;
  • 实现Runnable接口,实现run()方法;
  • 实现Callable接口,实现call()方法;
  • 开启线程池,加入实现了Runnable或者Callable接口的类;

在本文中,我们继续来讲多线程之间的通信、数据共享以及安全的问题。

Java内存运行机制

在JVM中,运行程序的实体是线程,每个线程创建时JVM都会为其创建一个工作内存。如下图所示。一个Java程序创建了线程A和线程B,那么JVM就会分配本地内存A和本地内存B。其实不管Java程序创建了多少个线程(JVM为其都会创建工作内存),每个线程所拥有的本地内存,都是由JMM控制从主内存中拷贝过来的共享变量的副本;工作内存是每个线程的私有数据区域,Java内存模型中规定所有变量都存储在主内存,主内存是数据共享区域,所有线程都可以访问。但是线程对变量的操作(比如读取、赋值等)必须在工作内存中进行。即首先要将变量从主内存拷贝到自己的工作内存空间,然后对变量进行操作,操作完成后再将变量写回主内存。注意,不能直接操作主内存中的变量。各个线程中的工作内存是主内存中的变量副本拷贝,因此不同的线程间无法访问对方的工作内存,线程间的通信(传值)必须通过主内存来完成;

比如说,现在线程A和线程B都需要对主内存的共享变量count进行计数,代码如下:

import java.util.concurrent.TimeUnit;


class MyThread extends Thread {
    private int count = 0;


    public void run() {
        count += 1;
        System.out.println(this.currentThread().getName() + "的count值是:" + count);
    }
}


public class Main {


    public static void main(String[] args) {
        MyThread myThread = new MyThread();
        Thread A = new Thread(myThread);
        Thread B = new Thread(myThread);
        A.start();
        B.start();


        try {
            TimeUnit.SECONDS.sleep(3);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }


    }
}
/*
结果:
Thread-2的count值是:2
Thread-1的count值是:2
*/

 可以看到得到的结果都是2,为什么会这样呢。下面我们来解释一下什么是线程不安全的问题,以及有什么解决的办法。

Java多线程不安全

1、概述

因为每个线程的工作内存都是从主内存中拷贝副本而来,在读取的时候,可能出现线程A和线程B同时执行run()方法中的代码,因为count += 1这个代码其实是分成三步进行的:

  • 从主内存拷贝count到工作内存,从工作内存中取得原有的count值;
  • 在工作内存中计算count + 1;
  • 在工作内存中对count进行赋值,并且将工作内存的计算结果同步上传到主内存中;

在多线程的场景下,可能出现A和B两个线程分别卡在count+1这个步骤中,导致本来应该一次递增的结果出现了问题。这个时候我们能够想到的解决办法就是对run()方法进行控制,只允许一个线程同时执行,其他线程需要等待。这个判断run()方法只运行的只有一个线程的办法,就是加锁控制。每次线程进入run()方法时候,需要判断是否被锁住,如果被锁住,代表已经有线程在执行,该线程需要等待。synchronize关键字可以在任意对象及方法上加锁,而加锁的这段代码称“互斥区”或“临界区”。大家可以试一下,在代码中的run()方法前加上synchronize关键字修饰。

synchronized public void run() {
    super.run();
    count += 1;
    System.out.println(this.currentThread().getName() + "的count值是:" + count);
}


/*
Thread-1的count值是:1
Thread-2的count值是:2
*/

synchronize是后面学习Java线程安全知识的基础,它的主要实现就是让多个线程并发操作的时候,用锁机制来控制队列排队。这种实现机制同步效率低下,没有发挥到多线程的优势。除此之外,我们可以利用Java提供的Lock锁,对变量的加减进行加锁;我们还可以使用JUC提供的原子操作类AtomicInteger等等;

 

2、实战

我们再来看一个例子,比如现在需要进行售票,有5张票,开启10个线程同时去抢票,看看各个实现方式会出现的结果;

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

class MyThread extends Thread {

    public void run() {
//        sellTicket();
//        sellTicketSynchronized();
//        sellTicketStaticSynchronized();
//        sellTicketAtomic();
//        sellTicketLockGlobal();
//        sellTicketLockLocal();
    }

    /*
        方式一:不考虑并发操作,不做任何处理
        结果:线程不安全,发生超卖
        原因:多线程判断Main.num的时候会出现并发不安全
     */
    public void sellTicket() {
        if (Main.num > 0) {
            try {
                TimeUnit.MILLISECONDS.sleep(500);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            Main.num--;
            System.out.println(this.currentThread().getName() + "抢票成功");
        } else {
            System.out.println(this.currentThread().getName() + "无法买票,票已经卖光");
        }
    }

    /*
        方式二:考虑并发操作,用synchronize修饰方法
        结果:线程不安全,发生超卖
        原因:对于非静态方法,synchronize锁的只是实例对象,对于new出来的多实例,每个实例都有自己的对象锁,因此并不能保证并发安全性;
     */
    public synchronized void sellTicketSynchronized() {
        if (Main.num > 0) {
            try {
                TimeUnit.MILLISECONDS.sleep(500);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            Main.num--;
            System.out.println(Thread.currentThread().getName() + "抢票成功");
        } else {
            System.out.println(Thread.currentThread().getName() + "无法买票,票已经卖光");
        }
    }

    /*
        方式三:考虑并发操作,用synchronize修饰静态方法
        结果:线程安全,不会超卖
        原因:对于静态方法,synchronize锁的是类,这把锁只有一个,无论new多少实例,都只能公用一把类锁;
     */
    public static synchronized void sellTicketStaticSynchronized() {
        if (Main.num > 0) {
            try {
                TimeUnit.MILLISECONDS.sleep(500);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            Main.num--;
            System.out.println(Thread.currentThread().getName() + "抢票成功");
        } else {
            System.out.println(Thread.currentThread().getName() + "无法买票,票已经卖光");
        }
    }

    /*
        方式四:考虑并发操作,使用原子操作
        结果:线程不安全,发生超卖
        原因:以后再说,这个涉及到Atomic原子操作的底层
     */
    public void sellTicketAtomic() {
        if (Main.atomicIntegerNum.get() > 0) {
            try {
                TimeUnit.MILLISECONDS.sleep(500);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            Main.atomicIntegerNum.decrementAndGet();
            System.out.println(Thread.currentThread().getName() + "抢票成功");
        } else {
            System.out.println(Thread.currentThread().getName() + "无法买票,票已经卖光");
        }
    }

    /*
       方式五:考虑并发操作,使用锁,锁全局
       结果:线程安全,不会超卖
       原因:都锁了,还有啥好说的
    */
    public void sellTicketLockGlobal() {
        Main.lock.lock();
        if (Main.num > 0) {
            try {
                TimeUnit.MILLISECONDS.sleep(500);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            Main.num--;
            System.out.println(Thread.currentThread().getName() + "抢票成功");
        } else {
            System.out.println(Thread.currentThread().getName() + "无法买票,票已经卖光");
        }
        Main.lock.unlock();
    }

    /*
       方式六:考虑并发操作,使用锁,锁局部
       结果:线程不安全,发生超卖
       原因:以后再说
    */
    public void sellTicketLockLocal() {
        if (Main.num > 0) {
            Main.lock.lock();

            try {
                TimeUnit.MILLISECONDS.sleep(500);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            Main.num--;
            System.out.println(Thread.currentThread().getName() + "抢票成功");

            Main.lock.unlock();
        } else {
            System.out.println(Thread.currentThread().getName() + "无法买票,票已经卖光");
        }
    }

}

public class Main {

    public static Integer num = 5;
    public static AtomicInteger atomicIntegerNum = new AtomicInteger(5);
    public static Lock lock = new ReentrantLock();

    public static void main(String[] args) {
        for (int i = 1; i <= 10; i++) {
            Thread thread = new Thread(new MyThread(), "线程" + i);
            thread.start();
        }

        try {
            TimeUnit.SECONDS.sleep(3);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

    }
}
发布了622 篇原创文章 · 获赞 150 · 访问量 31万+

猜你喜欢

转载自blog.csdn.net/feizaoSYUACM/article/details/104730401
今日推荐