如何又简单又高效地解决线程安全问题?

一、什么是线程同步?

处理多线程问题时,多个线程访问同一个对象,并且某些线程还想修改这个对象。这时候, 我们就需要用到“线程同步” 。

线程同步其实就是一种等待机制,多个需要同时访问此对象的线程进入这个对象的等待池形成队列,等待前面的线程使用完毕后,下一个线程再使用。

二、什么是锁机制?

由于同一进程的多个线程共享同一块存储空间,在带来方便的同时,也带来了访问冲突的问题。为了保证数据在方法中被访问时的正确性,在访问时加入锁机制(synchronized)。

当一个线程获得对象的排它锁,独占资源,其他线程必须等待,使用后释放锁即可。存在以下问题:

  • 1、一个线程持有锁会导致其它所有需要此锁的线程挂起
  • 2、在多线程竞争下,加锁、释放锁会导致比较多的上下文切换和调度延时引起性能问题
  • 3、如果一个优先级高的线程等待一个优先级低的线程释放锁会导致优先级倒置,引起性能问题。

三、如何实现同步?

由于我们可以通过private关键字来保证数据对象只能被方法访问,所以我们只需针对方法提出一套机制,这套机制就是synchronized关键字,它包括两种用法: synchronized 方法和synchronized 块。

1、同步方法

public synchronized void method(int args) {}

synchronized方法控制对“成员变量|类变量”对象的访问:

每个对象对应一把锁, 每个synchronized方法都必须获得调用该方法的对象的锁方能执行,否则所属线程阻塞,方法一旦执行,就独占该锁,直到从该方法返回时才将锁释放,此后被阻塞的线程方能获得该锁,重新进入可执行状态。

缺陷:若将一个大的方法声明为synchronized将会大大影响效率。

2、同步块

同步块: synchronized (obi) {}, obi称之为同步监视器。

obj可以是任何对 象,但是推荐使用共享资源作为同步监视器。

同步方法中无需指定同步监视器,因为同步方法的同步监视器是this即该对象本身,或class即类的模子。

同步监视器的执行过程

  • 第一个线程访问,锁定同步监视器,执行其中代码
  • 第二个线程访问,发现同步监视器被锁定,无法访问
  • 第一个线程访问完毕,解锁同步监视器
  • 第二个线程访问,发现同步监视器未锁,锁定并访问

四、认识线程不安全

1、可能有重复数据:每个线程的工作空间与主内存在进行数据交互时,不一致(来不及更新数据,就完成了拷贝工作)。

2、不应该出现的负数:在模拟12306抢票中,对于最后一张票,即数据到临界值时,sleep等待完的线程拿不到“余票”。

实例一:12306抢票

我们之前有模拟过12306抢票,我们再来看一下:

public class UnsafeTest1 {
	public static void main(String[] args) {
		//一份资源
		Web12306 web = new Web12306();
		//多个代理
		new Thread(web,"小明").start();
		new Thread(web,"小军").start();
		new Thread(web,"小强").start();
	}
}
class Web12306 implements Runnable{
	//票数
	private int ticketNums = 20;
	private boolean flag = true;
	public void run(){
		while(flag){
			test();
	}
}
	public void test(){
	if(ticketNums<0){
		flag=false; 
		return;
	}
	try {
		Thread.sleep(200);
	} catch (InterruptedException e) {
		e.printStackTrace();
	}
	System.out.println(Thread.currentThread().getName()+"-->"+ticketNums--);
	}
}

结果

在这里插入图片描述
抢到的票数不能为0,或者负数。

实例二:操作容器

对于数据又改又读取的情况,需要并发控制

public class UnsafeTest2 {
	public static void main(String[] args) {
		List<String> list = new ArrayList<String>();
		for(int i=0;i<10000;i++){
			new Thread(()->{
				list.add(Thread.currentThread().getName());
			}).start();
		}
		System.out.println(list.size());
	}
}

结果

在这里插入图片描述
本该是10000的数据,现在丢失了。

五、实现线程安全

在并发是保证数据的准确性,效率尽可能高。

synchronized

  • 1、同步方法
  • 2、同步块

实例一:安全抢票模拟

 public class SynTest1 {
	public static void main(String[] args) {
		//一份资源
		SafeWeb12306 web = new SafeWeb12306();
		//多个代理
		new Thread(web,"小明").start();
		new Thread(web,"小军").start();
		new Thread(web,"小强").start();
	}
}
class SafeWeb12306 implements Runnable{
	//票数
	private int ticketNums = 20;
	private boolean flag = true;
	public void run(){
		while(flag){
			test();
	}
}
	//线程安全,同步
	//给资源上锁,即锁的是使用此方法的对象,让其他方法无法再访问,只能排队等待
	public synchronized void test(){
	if(ticketNums<=0){
		flag=false; 
		return;
	}

	try {
		Thread.sleep(200);
	} catch (InterruptedException e) {
		e.printStackTrace();
	}
	System.out.println(Thread.currentThread().getName()+"-->"+ticketNums--);
	}
}

给资源上锁,即锁的是使用此方法的对象,让其他方法无法再访问,只能排队等待。

结果

在这里插入图片描述
不再出现抢到重复的票,或者负数的票数了。

实例二:继续优化(double checking

public class SynTest3 {
	public static void main(String[] args) {
		//一份资源
		SynWeb12306 web = new SynWeb12306();
		//多个代理
		new Thread(web,"小明").start();
		new Thread(web,"小军").start();
		new Thread(web,"小强").start();
		
	}
}
class SynWeb12306 implements Runnable{
	//票数
	private int ticketNums = 20;
	private boolean flag = true;
	public void run(){
		while(flag){
			//test1();
			test2();
	}
}
	//double checking双重检测
	public void test2(){
		if(ticketNums<=0){ //考虑的是没有票的情况
			flag=false;
			return;
		}
		synchronized(this){
		if(ticketNums<=0){  //考虑的是最后一张票的情况
			flag=false; 
			return;
		}
		//模拟网络延时
		try {
			Thread.sleep(200);
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
		System.out.println(Thread.currentThread().getName()+"-->"+ticketNums--);
		}
	}
	//线程安全:同步方法
	public synchronized void test1(){
	if(ticketNums<=0){
		flag=false; 
		return;
	}
	try {
		Thread.sleep(200);
	} catch (InterruptedException e) {
		e.printStackTrace();
	}
	System.out.println(Thread.currentThread().getName()+"-->"+ticketNums--);
	}
}

分析

  • 改成同步块:只锁其中一个成员变量无法实现,需要锁对象
  • 锁的范围太小会锁不住,例如只锁if代码块的内容会失败
  • 锁的范围太大,会出现性能低下的情况
  • 所以尽可能锁定合理的范围(不是指代码,而是指数据的完整性)

六、死锁怎么办?

案例:

小红持有口红,1S后想要拿到镜子,而此时小强持有镜子,2S后想要拿到口红。两人彼此等待对方放下手中的镜子或口红,从而陷入僵局。

代码演示:

public class DeadLock {
	public static void main(String[] args) {
		Markup g1 = new Markup(1,"小红");
		Markup g2 = new Markup(0,"小强");
		g1.start();
		g2.start();
	}
}
//口红
class Lipstick{
}
//镜子
class Mirror{
}
//化妆
class Markup extends Thread{
	static Lipstick lipstick=new Lipstick();
	static Mirror mirror=new Mirror();
	//选择
	int choice;
	//名字
	String girl;
	public Markup(int choice,String girl){
		this.choice=choice;
		this.girl = girl;
	}
	public void run(){
		markup();
	}
	private void markup(){
		if(choice==0){
			synchronized(lipstick){
				System.out.print(this.girl+"涂口红\r\n");
				//1s后想拥有镜子的锁
				try {
					Thread.sleep(1000);
				} catch (InterruptedException e) {
					e.printStackTrace();
				}
				synchronized(mirror){
					System.out.print(this.girl+"照镜子\r\n");
				}
			}
		}else{
			synchronized(mirror){
				System.out.print(this.girl+"照镜子");
				//2s后想拥有口红的锁
				try {
					Thread.sleep(2000);
				} catch (InterruptedException e) {
					e.printStackTrace();
				}
				synchronized(lipstick){
					System.out.print(this.girl+"涂口红");
				}
			}
		}
	}
}

结果

在这里插入图片描述

分析

  • 过多的同步,可能造成线程相互不释放资源,从而相互等待。
  • 一般发生于同步中持有多个对象的锁。

解决方法:不要在同一个代码块中,同时持有多个对象的锁。

改进

不要锁套锁

synchronized(mirror){
	System.out.print(this.girl+"照镜子");
	//2s后想拥有口红的锁
	try {
			Thread.sleep(2000);
	} catch (InterruptedException e) {
			e.printStackTrace();
	}
}
//把锁分开
synchronized(lipstick){
	System.out.print(this.girl+"涂口红");
}

改进后的结果

在这里插入图片描述
结果是我们想要的,我们解决了死锁的问题!!

七、处理并发协作

生产者消费者模式

在这里插入图片描述

  • 假设仓库中只能存放件产品,生产者将生产出来的产品放入仓库,消费者将仓库中产品取走消费;
  • 如果仓库中没有产品,则生产者将产品放入仓库,否则停止生产并等待,直到仓库中的产品被消费者取走为止;
  • 如果仓库中放有产品,则消费者可以将产品取走消费,否则停止消费并等待,直到仓库中再次放入产品为止。

分析

这是一个线程同步问题,生产者和消费者共享同一个资源,并且生产者和消费者之间相互依赖,互为条件:

对于生产者,没有生产产品之前,要通知消费者等待。而生产了产品之后,又需要 马上通知消费者消费。

对于消费者,在消费之后,要通知生产者已经消费结束,需要继续生产新产品以供消费。

  • synchronized可阻止并发更新同一 个共享资源,实现了同步;
  • synchronized不能用来实现不同线 程之间的消息传递(通信)

解决方法一:管程法

  • 用容器来实现,叫缓存区(消息队列)
  • 优点:解耦,提高效率

解决方法二:红绿灯法

红绿灯法又叫标识法,通过设定标志位,提醒对象释放锁。

Java中的方法实现

方法名作用
final void wait()标识线程一直等待,直到其他线程通知,与sleep不同,会释放锁
final void wait(long timeout)指定等待的毫秒数
final void notifiy()唤醒一个处于等待状态的线程
final void notifiyAll()唤醒同一个对象上所有调用wait()方法的线程,优先级别高的线程优先调度
### 线程安全的定义 线程安全是指在多线程环境下运行某个程序片段时,无论有多少线程同时执行该代码段,其行为都能保持一致并符合预期[^2]。换句话说,即使多个线程访问共享资源,它们之间的操作也不会相互干扰,从而保证数据的一致性和完整性。 如果一段代码在线程间存在互相影响的情况,则被认为是线程不安全的。这种情况下,结果可能会因线程调度的不同而发生变化,甚至引发不可预测的行为或错误[^2]。 --- ### 解决线程安全问题的方法及最佳实践 以下是几种常见的解决线程安全问题的方式及其适用场景: #### 1. 使用 `synchronized` 关键字 `synchronized` 是 Java 中用于实现同步的关键字,可以用来修饰方法或者代码块。它通过加锁机制确保同一时间只有一个线程能够进入被标记为 synchronized 的区域,从而避免了多线程间的冲突[^1]。 示例代码如下: ```java public class Counter { private int count = 0; public synchronized void increment() { count++; } public synchronized int getCount() { return count; } } ``` --- #### 2. 利用 `volatile` 关键字 当变量可能被多个线程修改且不需要复杂的原子性保障时,可以使用 `volatile` 关键字声明该变量。这能确保每次读取到的是最新的值,而不是缓存中的旧值。 注意:`volatile` 并不能替代锁定机制,仅适用于简单的状态标志或其他无需复合操作的情形。 --- #### 3. 原子类的应用 Java 提供了一系列位于 `java.util.concurrent.atomic` 包下的原子类(如 `AtomicInteger`, `AtomicLong`),这些类提供了高效的无锁算法来完成一些基本的操作,比如自增、比较交换等[^1]。 实例演示: ```java import java.util.concurrent.atomic.AtomicInteger; public class AtomicCounter { private AtomicInteger count = new AtomicInteger(0); public void increment() { count.incrementAndGet(); } public int getCount() { return count.get(); } } ``` --- #### 4. 锁机制 (Lock 接口) 相比传统的 `synchronized` 方法/块,显式的 Lock 对象提供了更加灵活的功能,例如尝试获取锁、定时等待锁释放等功能。 下面是一个基于 ReentrantLock 的例子: ```java import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; public class LockedResource { private final Lock lock = new ReentrantLock(); private int value; public void setValue(int newValue) { lock.lock(); // 获取锁 try { this.value = newValue; } finally { lock.unlock(); // 确保最终解锁 } } public int getValue() { lock.lock(); try { return value; } finally { lock.unlock(); } } } ``` --- #### 5. 并发集合 标准的 Collection 类型并非线程安全设计,因此在高并发场景下应考虑采用专门优化过的并发集合类型,像 `ConcurrentHashMap`, `CopyOnWriteArrayList` 等。 举例说明如何利用 ConcurrentHashMap 完成高效的数据存储与检索任务: ```java import java.util.concurrent.ConcurrentHashMap; public class ConcurrentMapExample { private static final ConcurrentHashMap<String, String> map = new ConcurrentHashMap<>(); public static void put(String key, String value) { map.put(key, value); } public static String get(String key) { return map.get(key); } } ``` --- #### 6. 避免共享可变状态 一种更为激进但也非常有效的策略是完全消除对象内部的状态共享,转而依赖于函数式编程风格或是不可变对象的设计理念[^3]。这样从根本上杜绝了由于竞态条件引起的潜在风险。 例如,在 Python 编程语言里,我们可以借助只读视图代替原始列表传递给不同工作单元处理: ```python from copy import deepcopy class SafeProcessor: def __init__(self, data): self._data = tuple(data) @property def safe_data(self): return deepcopy(list(self._data)) ``` --- #### 7. 替代方案——ThreadLocal 和 DateTimeFormatter 针对某些特定场合,还可以引入 ThreadLocal 技术让每个线程拥有独立副本;或者是选用现代表达形式(如 Java 8 的 DateTimeFormatter)规避传统工具带来的隐患[^4]。 典型应用案例展示如下: ```java // 使用 ThreadLocal 维护 SimpleDateFormat 实例 private static final ThreadLocal<SimpleDateFormat> dateFormatHolder = ThreadLocal.withInitial(() -> new SimpleDateFormat("yyyy-MM-dd")); public static String formatDate(Date date) { return dateFormatHolder.get().format(date); } // 或者升级至更现代的选择 —— DateTimeFormatter DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd"); String formattedDate = LocalDate.now().format(formatter); ``` --- ### 总结 为了构建健壮可靠的软件系统,开发者应当深入理解各种技术手段的特点,并依据实际情况选取最合适的解决方案。与此同时,始终牢记良好的编码习惯和架构原则也是达成目标不可或缺的一部分。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值