前文回顾
上一次我们讲过了在操作系统中,进程是一个非常重要的概念,它是计算机程序运行的基础。我们使用计算机应用程序的时候,操作系统实际上管理着这个应用程序背后运行的进程。我们现在来总结一下,进程有以下几个特点:
- 进程有创建、就绪、运行、阻塞、退出五个状态;
- 进程在创建态的时候,需要分配资源,在就绪态的时候,资源进入内存,准备等待获取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();
}
}
}