Java进阶之多线程安全性
- 一、多线程的安全问题
- 1.1 多线程的内存图
- 1.2 Java内存模型
- 1.3 线程的可见性
- 1.4 线程的有序性
- 1.5 线程的原子性
- 1.6 volatile关键字
- 1.7 原子类
- 1.7.1 AtomicInteger解决原子性问题
- 1.7.2 CAS机制
- 1.8 synchronized关键字
- 1.8.1 同步代码块
- 1.8.2 同步方法
- 1.8.3 同步小结
- 1.9 Lock接口解决线程安全问题
- 1.10 并发包
- 1.10.1 CopyOnWriteArrayList
- 1.10.2 CopyOnWriteArraySet
- 1.10.3 ConcurrentHashMap
一、多线程的安全问题
1.1 多线程的内存图
栈内存是线程私有,每一个线程都有一个自己的栈空间,用来运行自己的方法。
方法是通过哪个线程调用,那么方法就会在哪个线程的栈空间中运行。
1.2 Java内存模型
Java内存模型定义的是线程对于共享变量的访问规则。
- 主内存:线程共享的数据是保存在主内存中
- 线程工作内存:工作内存保存的数据的副本,线程操作主内存中的数据过程是会先将该数据保存到工作内存,完成操作后,再把新的数据赋值给主内存的数据
这就导致了线程操作时的三个问题:
- 可见性
- 有序性
- 原子性
1.3 线程的可见性
public class MyThread extends Thread{
boolean flag = false;
//定义run方法
@Override
public void run() {
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
//将flag的值进行修改
flag = true;
//输出flag的值
System.out.println("flag:" + flag);
}
}
public class Demo01Test {
public static void main(String[] args) {
//创建Thread子类对象
MyThread m = new MyThread();
//线程启动
m.start();
//死循环
while (true) {
//判断如果m对象中的flag是true,就输出内容
if (m.flag) {
System.out.println("线程执行了");
}
}
}
}
上面的案例没有执行打印操作,因为主线程中拿到的flag值还没有改变。
1.4 线程的有序性
程序在编译过程中,对一些没有顺序要求的代码顺序进行打乱,有可能会对程序的结果产生影响:
1.5 线程的原子性
我们以实际案例来分析:
public class Task implements Runnable{
//定义成员变量
int count = 0;
@Override
public void run() {
//对count的值进行增加100次
for(int i = 0; i < 100; i++) {
//线程休眠
try {
Thread.sleep(5);
} catch (InterruptedException e) {
e.printStackTrace();
}
//让count自增
count++;
//输出count的值
System.out.println("count->" + count);
}
}
}
public class Demo01Test {
public static void main(String[] args) {
//创建Task对象
Task t = new Task();
//创建线程
new Thread(t).start();
new Thread(t).start();
new Thread(t).start();
}
}
结果:
按照我们的设想,一个线程打印100次,3个线程应该打印300次,但结果并没有达到,造成这个结果的原因在于++
操作不具有原子性。
原子性是指不可分割的操作,而某个线程要对共享数据执行++
操作会分为三个步骤:
- 1.将主内存中的数据读取到自己的工作内存中
- 2.将工作内存中的数据+1
- 3.将新的数据赋值给主内存中的原数据
由于++
不具备原子性,在执行操作时可能会被其他线程插队,导致count值多次重复。
1.6 volatile关键字
volatile关键字可以保证变量对于线程的可见性,对于被volatile修饰的变量,如果某个线程对该变量进行修改并保存到主内存时,对于其他线程来说,也是可见的,即其他线程也会读取到新的值。
volatile关键字也可以解决有序性问题,当变量被修饰为volatile时,会禁止代码重排。
volatile只能保证保证单一变量的可见性,不能解决代码块的原子性问题。
1.7 原子类
在Java开发编程中,要想保证一些操作不被其他线程干扰,就需要保证原子性,JDK中提供了13个原子操作类来帮助我们进行开发,这里我们以整数原子类AtomicInteger举例学习原子类。
AtomicInteger构造方法:
-
AtomicInteger()
:使用该构造方法创建出来的AtomicInteger对象表示整数0 -
AtomicInteger(int initialValue)
:根据指定的数字值创建AtomicInteger对象
常用成员方法:
-
public final int getAndIncrement()
:获取当前的值然后进行自增操作,返回的是自增前的值 -
public final int incrementAndGet()
:获取当前的值然后进行自增操作,返回的是自减后的值 -
int get()
:获取当前AtomicInteger对应的整数值
public class Demo02AtomicIntegerTest {
public static void main(String[] args) {
//创建AtomicInteger对象
//AtomicInteger atomicInteger = new AtomicInteger(10);
//System.out.println(atomicInteger);//10
//int getAndIncrement():获取当前的值然后自增,返回的是自增前的值
int count = atomicInteger.getAndIncrement();
System.out.println(count);//10
//int incrementAndGet():先自增然后获取值。 返回的是自增后的值
int count1 = atomicInteger.incrementAndGet();
System.out.println("count1:" + count1);//12
System.out.println("atomicInteger:" + atomicInteger);//12
//int get():获取当前AtomicInteger对应的整数值
int num = atomicInteger.get();
System.out.println("num:" + num);//12
}
}
1.7.1 AtomicInteger解决原子性问题
public class Task implements Runnable{
//定义成员变量,初始值是0
AtomicInteger count = new AtomicInteger();
@Override
public void run() {
//对count的值进行增加100次
for(int i = 0; i < 100; i++) {
//线程休眠
try {
Thread.sleep(5);
} catch (InterruptedException e) {
e.printStackTrace();
}
//让count自增
int num = count.incrementAndGet();
//输出num的值
System.out.println("num->" + num);
}
}
}
public class Test02 {
public static void main(String[] args) {
//创建Task对象
Task task = new Task();
//创建线程对象,执行任务
new Thread(task).start();
new Thread(task).start();
new Thread(task).start();
}
}
输出结果:
1.7.2 CAS机制
原子类是使用CAS机制解决原子性问题的。
1.8 synchronized关键字
synchronized可以解决多行代码的原子性问题。
synchronized表示同步,可以修饰代码块,也可以修饰方法。
1.8.1 同步代码块
如果synchronized修饰代码块,那么这个代码块就是同步代码块
同步代码块格式:
synchronized (锁对象) {
...
}
锁对象就是一个普通的Java对象,锁对象可以是任何类型的,可以是Object, ArrayList
锁对象仅仅起到一个标记的作用,除此之外,就没有其他含义了
同步代码块特点:
- 只有持有锁的线程才能够进入到同步代码块,保证每次只有一个线程执行同步代码块
- 当线程离开同步代码块,线程会释放掉自己的锁。这样线程就又可以去竞争这个锁了,哪个线程能抢到,哪个线程去执行
/*
如果多线程同时操作共享数据,就有可能会引发线程安全问题。
*/
@SuppressWarnings("all")
public class Ticket2 implements Runnable{
//定义变量,表示票的数量。
int count = 100;
//创建对象,当做锁对象,该对象可以是任何类型的,该对象仅仅起到一个标记作用。
//锁对象一定要是唯一的。 多个线程使用的锁必须是同一个锁对象,否则也不能保证线程安全。
Object lock = new Object();
//在run方法中编写线程要执行的任务,线程要执行的任务是卖票任务。
@Override
public void run() {
//定义死循环模拟售票窗口一直卖票的过程
while (true) {
//当线程执行到同步代码块时会看一下同步代码块上面有没有锁。
//如果同步代码块上面有锁,那么线程会获取到锁,然后进入到同步代码块中。
//如果同步代码块上面没有锁,那么线程会在同步代码块位置一直等着获取锁
synchronized (lock) {
//如果有票,那么才往外卖票
if (count > 0) {
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "正在卖票:" + count);
count--;
}
}
}
}
}
public class Demo02TicketTest {
public static void main(String[] args) {
//创建Ticket2对象
Ticket2 t = new Ticket2();
new Thread(t).start();
new Thread(t).start();
new Thread(t).start();
}
}
1.8.2 同步方法
如果synchronized修饰方法,那么该方法就是同步方法,同步方法同样可以解决线程安全问题。
synchronized格式:
修饰符 synchronized 返回值类型 方法名(参数列表) {
方法体;
return 返回值;
}
同步方法相当于将整个的方法体都加了同步代码块
同步方法也是有锁的
- 如果同步方法是非静态的,那么锁对象是this。
- 如果同步方法是静态的,那么锁对象是类名.class(字节码文件对象)
/*
如果多线程同时操作共享数据,就有可能会引发线程安全问题。
*/
@SuppressWarnings("all")
public class Ticket3 implements Runnable{
//定义变量,表示票的数量。
int count = 100;
@Override
public void run() {
while (true) {
//调用方法,卖票
sell2();
}
}
//定义同步方法,用来卖票
//同步方法,相当于将整个的方法体都加了同步代码块。
//同步方法是非静态的,锁对象是this
public synchronized void sell2() {
//如果有票,那么才往外卖票
if (count > 0) {
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "正在卖票:" + count);
count--;
}
}
}
1.8.3 同步小结
线程同步可以解决线程安全问题,会牺牲效率
同步代码块:
- 优点:灵活,可以对任意代码进行同步。
- 缺点:语法不如同步方法简洁
同步方法:
- 优点:语法简洁。
- 缺点:不如同步代码块灵活。 是直接将整个的方法体都加了同步
1.9 Lock接口解决线程安全问题
在JDK5的时候,提供了Lock接口,里面的方法可以手动的获取锁以及释放锁
-
void lock()
:获取锁 -
void unlock()
:释放锁
Lock是一个接口,如果要用,需要使用实现类,最常用的实现类是ReentrantLock
public class Ticket4 implements Runnable{
//定义变量,表示票的数量。
int count = 100;
Lock lock = new ReentrantLock();
@Override
public void run() {
while (true) {
//获取锁
lock.lock();
//如果有票,那么才往外卖票
if (count > 0) {
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "正在卖票:" + count);
count--;
}
//释放锁
lock.unlock();
}
}
}
public class Demo04TicketTest {
public static void main(String[] args) {
//创建Ticket4对象
Ticket4 t = new Ticket4();
//创建线程对象,并执行
new Thread(t).start();
new Thread(t).start();
new Thread(t).start();
}
}
1.10 并发包
我们通常所说的并发包也就是 java.util.concurrent 及其子包,集中了 Java 并发的各种基础工具类,今天简单介绍其中几个。
1.10.1 CopyOnWriteArrayList
ArrayList是线程不安全的,多个线程一起对ArrayList进行操作有可能会有安全问题
CopyOnWriteArrayList
是线程安全的,可以使用它来解决线程安全问题
public class Task implements Runnable{
//定义集合【ArrayList是线程不安全的,会引发线程安全问题】
//List<Integer> list = new ArrayList<>();
//CopyOnWriteArrayList是线程安全的,可以使用它来解决这个问题
List<Integer> list = new CopyOnWriteArrayList<>();
@Override
public void run() {
for (int i = 0; i < 100; i++) {
try {
Thread.sleep(5);
} catch (InterruptedException e) {
e.printStackTrace();
}
list.add(i);
}
System.out.println("添加完毕");
}
}
public class Demo01Test {
public static void main(String[] args) throws InterruptedException {
//创建Task对象
Task task = new Task();
//创建三个线程,并执行任务
new Thread(task).start();
new Thread(task).start();
new Thread(task).start();
//线程休眠
Thread.sleep(3000);
//输出集合的大小
System.out.println(task.list.size());
}
}
1.10.2 CopyOnWriteArraySet
HashSet线程不安全,如果多线程同时对HashSet集合进行操作,有可能会产生安全性的问题
CopyOnWriteArraySet
是线程安全的,我们可以使用这个集合进行多线程操作
public class Task implements Runnable{
//创建HashSet集合【HashSet线程不安全,如果多线程同时对HashSet集合进行操作,有可能会产生安全性的问题】
//Set<Integer> set = new HashSet<>();
//CopyOnWriteArraySet是线程安全的,我们可以使用这个集合进行多线程操作
Set<Integer> set = new CopyOnWriteArraySet<>();
@Override
public void run() {
//向集合中添加数据
for (int i = 0; i < 1000; i++) {
try {
Thread.sleep(5);
} catch (InterruptedException e) {
e.printStackTrace();
}
//添加数据
set.add(i);
}
System.out.println("添加完成");
}
}
public class Demo01Test {
public static void main(String[] args) throws InterruptedException {
//创建Task对象
Task t = new Task();
//创建线程,启动
new Thread(t).start();
//在main线程中向集合中添加2000-3000之间的数字
for (int i = 2000; i < 3000; i++) {
Thread.sleep(5);
t.set.add(i);
}
//休眠
Thread.sleep(7000);
//输出集合的大小
System.out.println("集合的大小是:" + t.set.size());
}
}
1.10.3 ConcurrentHashMap
HashMap集合不是线程安全的,如果多个线程同时操作HashMap,那么有可能会引发线程安全问题。
Hashtable
集合是线程安全的,多个线程同时操作hashtable不会引发线程安全问题,效率非常低,目前基本已经淘汰了。
ConcurrentHashMap
集合是线程安全的,但是效率相对Hashtable要高,其内部使用的是CAS+分段锁。
public class Task implements Runnable{
//创建Map集合
Map<String, String> map = new ConcurrentHashMap<>();
@Override
public void run() {
//定义循环,在循环中向Map集合中添加10000条数据
for (int i = 0; i < 10000; i++) {
//向map集合中添加数据
map.put(Thread.currentThread().getName() + i, i);
}
}
}
public class Demo01Test {
public static void main(String[] args) throws InterruptedException {
//创建Task对象
Task task = new Task();
//创建三个线程对象,执行任务
new Thread(task).start();
new Thread(task).start();
new Thread(task).start();
//线程休眠
Thread.sleep(3000);
//输出map集合的大小
System.out.println(task.map.size());
}
}