多线程(中)
摘要:本文系统介绍了多线程编程中的线程安全问题及解决方案。主要内容包括:1)线程不安全的原因,如随机调度、共享数据修改、原子性缺失等;2)synchronized关键字的互斥和可重入特性;3)volatile关键字保证可见性;4)wait/notify机制实现线程同步;5)单例模式的线程安全实现(饿汉式和懒汉式)。通过代码示例详细演示了各种线程安全问题的表现及对应的解决方案,为开发多线程程序提供了
目录
一、多线程带来的风险 - 线程安全(重点)
1、了解线程不安全
public static int count=0;
public static void main(String[] args) throws InterruptedException {
Thread t1=new Thread(() ->{
for (int i = 0; i < 50000 ; i++) {
count++;
}
});
Thread t2=new Thread(() ->{
for (int i = 0; i < 50000 ; i++) {
count++;
}
});
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println(count);
}
此处就是线程不安全的情况,count的结果应该为100000,但实际结果并不是
2、线程安全的概念
线程安全是指当多个线程同时访问一个共享资源(如变量、对象、文件等)时,无论这些线程的执行调度顺序如何,都能保证程序的执行结果始终正确,且不会出现数据损坏、逻辑错误或不可预期的行为。
如果多线程环境下代码运行的结果是符合我们预期的,即在单线程环境应该的结果,则说这个程序是线程安全的。
3、线程不安全的原因
3.1线程调度是随机的
开发者无法保证线程对共享资源的访问顺序,随机调度使一个程序在多线程环境下,执行顺序存在很多变数。
3.2修改共享数据
多个线程修改同一个变量
上面的线程不安全的代码中,涉及到多个线程对 count 变量进行修改
此时这个 count 是一个多个线程都能访问到的 “ 共享数据 ”,若仅为 “ 只读操作 ”,则不会出现线程不安全问题
3.3操作原子性
原子性是指一个操作或一组操作要么全部执行完成,要么完全不执行,中间不会被其他线程打断。
一条Java语句不一定是原子的,也不一定是一条指令,比如 count++,其实是由三步操作组成的:
1、从内存把数据读到 CPU
2、进行数据更新
3、把数据写回到CPU
不保证原子性会给多线程带来什么问题
若线程 A 执行到步骤 2 时被暂停,线程 B 同时执行步骤 1 (读取到相同的5),后续两个线程都会写回 6 ,最终结果比预期少 1 ,引发数据错误。
3.4内存可见性
内存可见性是指当一个线程修改了共享变量的值后,其它线程能够立即看到这个修改后的最新值的特性
Java内存模型(JMM)
Java虚拟机规范中定义了Java内存模型,目的是屏蔽掉各种硬件和操作系统的内存访问差异,以实现让Java程序在各种平台下能达到一致的并发效果
- 线程之间的共享变量存在主内存(Main Memory)
- 每一个线程都有自己的 “ 工作内存 ” (Working Memory)
- 当线程要读取一个共享变量的时候,会先把变量从主内存拷贝到工作内存,再从工作内存读取数据
- 当线程要修改一个共享变量的时候,会先修改工作内存中的副本,再同步回主内存
由于每个线程有自己的工作内存,这些工作内存中的内容相当于同一个共享变量的 “ 副本 ” ,此时修改线程1的工作内存中的值,线程2的工作内存不一定会及时变化
3.5指令重排序
假设早上做早餐有三个步骤:
- 煮开水
- 用开水泡麦片
- 把泡好的麦片端到餐桌
正常顺序是1->2->3,但为了节省时间,可能会调整顺序:1->3->2
先煮开水,不等水开,先把麦片碗拿到餐桌,等水开后再泡麦片
这种就叫做指令重排序,可能在多线程下变量的读写顺序与代码逻辑顺序不一致,进而引发线程安全问题
二、synchronized 关键字 - 监视器锁
1、synchronized 的特性
1.1 互斥
synchronized 会起到互斥效果,某个线程执行到某个对象的 synchronized 中时,其他线程如果也执行到同一对象的 synchronized 就会阻塞等待
- 进入 synchronized 修饰的代码块,相当于加锁
- 退出 synchronized 修饰的代码块,相当于解锁
1.2 可重入
synchronized 同步块对同一条线程来说是可重入的,不会出现把自己锁死的问题
Java 的 synchronized 是可重入锁
public static int count=0;
public static void main(String[] args) {
Object locker=new Object();
Thread t1=new Thread(() ->{
synchronized (locker){
synchronized (locker){
for (int i = 0; i < 50000; i++) {
count++;
}
}
}
});
}
- 如果某个线程加锁的时候,发现锁已经被占用,但是恰好占用的是自己,那么仍然可以获取到锁,并让计数器自增
2、synchronized的使用
1、修饰代码块
锁任意对象
Object locker=new Object(); public void fun(){ synchronized (locker){ } }
锁当前对象
public void fun(){ synchronized (this){ } }
2、直接修饰普通方法
public synchronized void fun(){ }
3、修饰静态方法
public static synchronized void fun(){ }
两个线程分别尝试获取两把不同的锁,不会产生锁竞争
3、Java标准库中的线程安全类
线程不安全:ArrayList、LinkedList、HashMap、TreeMap、HashSet、TreeSet、StringBuilder
线程安全的:Vector(不建议使用)、HashTable(不建议)、ConcurrentHashMap、StringBuffer、String
三、volatile关键字
volatile用于保证变量在多线程环境下的可见性、禁止指令重排序,但不保证原子性
代码在写入volatile修饰的变量时:
- 改变线程工作内存中volatile变量副本的值
- 将改变后的副本的值从工作内存刷新到主内存
代码在读取volatile修饰的变量时:
- 从主内存中读取volatile的最新值到线程的工作内存中
- 从工作内存中读取volatile变量的副本
代码表示:
import java.util.Scanner;
public class Demo6 {
public static volatile int flag=0;//如果没有volatile这个关键字,下面的输入不是0循环也不会结束
public static void main(String[] args) {
Thread t1=new Thread(() ->{
while (flag==0){
}
System.out.println("循环结束");
});
Thread t2=new Thread(() -> {
Scanner scanner=new Scanner(System.in);
System.out.println("请输入一个数:");
flag=scanner.nextInt();
});
t1.start();
t2.start();
}
}
四、wait 和 notify
1、wait()方法
wait的执行逻辑:
- 使当前执行代码的线程进行等待(把线程放到等待队列中)
- 释放当前锁
- 满足一定条件时被唤醒,重新尝试获取这个锁
wait的结束条件:
- 其它线程调用该对象的 notify 方法
- wait 的超时等待方法
- 其它线程调用该等待线程的interrupted方法,导致 wait 抛出异常
注意:wait 要搭配 synchronized 来使用,否则会抛出异常
代码展示:
public class Demo7 {
public static void main(String[] args) throws InterruptedException {
Object locker=new Object();
synchronized (locker){
System.out.println("wait 之前");
locker.wait();//如果没有唤醒,那么wait就会一直等待,不会继续往下执行
System.out.println("wait 等待结束");
}
}
}
2、notify()方法
关键特性和使用规则:
- notify( ) 必须在同步代码块(synchronized 块)或同步方法中调用,否则会抛出异常
- 它会随机唤醒一个正在该对象上等待(通过 wait( ) 方法进入等待状态)的线程,而非所有等待线程
- 调用 notify( ) 后,当前线程不会立即释放持有的锁,而是会继续执行完同步代码块或同步方法后,才会释放锁,此时被唤醒的线程才能尝试获取到锁并继续执行
代码展示:
public class Demo8 {
public static void main(String[] args) {
Object locker=new Object();
Thread t1=new Thread(() ->{
synchronized (locker){
System.out.println("t1线程的wait之前");
try {
locker.wait();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println("t1线程结束");
}
});
Thread t2=new Thread(() ->{
synchronized (locker){
System.out.println("t2线程notify之前");
try {
Thread.sleep(10000);//为了让效果更明显
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
locker.notify();
System.out.println("t2线程notify之后");
}
});
t1.start();
t2.start();
}
}
3、notifyAll()方法
使用 notifyAll方法可以一次性唤醒所有等待的线程
代码展示:
import java.util.Scanner;
public class Demo9 {
private static void wait(Object locker) {
try {
locker.wait();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
public static void main(String[] args) {
Object locker = new Object();
Thread t1=new Thread(() ->{
synchronized (locker) {
System.out.println("t1 wait执行前");
wait(locker);
}
System.out.println("t1 线程结束");
});
Thread t2=new Thread(() ->{
synchronized (locker) {
System.out.println("t2 wait执行前");
wait(locker);
}
System.out.println("t2 线程结束");
});
Thread t3=new Thread(() ->{
synchronized (locker) {
System.out.println("t3 wait执行前");
wait(locker);
}
System.out.println("t3 线程结束");
});
Thread t4=new Thread(() ->{
synchronized (locker) {
System.out.println("t4 wait执行前");
wait(locker);
}
System.out.println("t4 线程结束");
});
t1.start();
t2.start();
t3.start();
t4.start();
Thread t5=new Thread(() ->{
synchronized (locker) {
System.out.println("输入任意内容唤醒");
Scanner scanner=new Scanner(System.in);
scanner.next();
locker.notifyAll();
}
System.out.println("t5 线程结束");
});
t5.start();
}
}
4、wait 和 sleep 的区别
都可以让线程阻塞,都可以指定阻塞时间
不同点:
- wait 的设计是为了被 notify ,超时时间只是 “ 后手 ”;sleep 的设计就是为了按照一定时间阻塞
- wait 必须搭配锁使用;sleep 不需要
- wait 一进来会先释放锁,再获取到锁;sleep 放到锁内部,休眠时不会释放锁
- wait 虽然也能够通过 interrupt 唤醒,实际上更希望通过 notify 唤醒,notify 唤醒后,还可以随时再 wait ,再 notify;sleep 在 interrupt 之后可能把线程终止掉
五、单例模式
确保在多线程环境下,一个类仅有一个实例
1、饿汉式
线程安全,但可能浪费资源
class Singleton{
private static Singleton instance=new Singleton();
private Singleton(){
}
public static Singleton getInstance(){
return instance;
}
}
2、懒汉式
首次调用 getInstance( ) 方法时创建实例,双重 if 判断加上 volatile 关键字解决线程安全问题
class SingletonLazy{
private static SingletonLazy instance=null;//如果加上volatile则线程安全
private static Object locker=new Object();
private SingletonLazy(){
}
public static SingletonLazy getInstance(){
//双重if判断
//判断是否加锁
if(instance==null) {
synchronized (locker) {
//判断实例是否存在
if (instance == null) {
instance = new SingletonLazy();
}
}
}
return instance;
}
}
更多推荐
所有评论(0)