浅谈线程安全

本文探讨了Java中的线程安全概念,通过模拟购票场景展示了线程不安全问题,并介绍了Java内存模型(JMM)的角色。文章进一步讲解了解决线程安全的策略,包括synchronized关键字、同步代码块以及JDK 1.5的Lock接口,通过实例分析了这些机制如何确保线程之间的同步交互,防止数据竞争问题。

浅谈线程安全

1.什么是线程安全?

多个线程同一时刻对同一个全局变量(同一份资源)做写操作(读操作不会涉及线程安全)时,如果跟我们预期的结果一样,我们就称之为线程安全,反之,线程不安全。

2.举个栗子

同学们在过年时候买火车票、 那家伙、那场面那是相当大呀!那真是:锣鼓喧天,鞭炮齐鸣,红旗招展,人山人海呀。 我们下面用代码模拟一下业务场景。

package com.example.snailthink;

/**
 * @program: snailthink
 * @description:
 * @author: SnailThink
 * @create: 2021-09-03 14:19
 **/
public class MyThread implements Runnable {


	private int ticketCount=50;

	@Override
	public void run() {

		try {
			while (ticketCount<50){
				Thread.sleep(50);
			}
		}catch (InterruptedException e) {
			e.printStackTrace();
		}
		System.out.println(Thread.currentThread().getName()+":  抢到第"+ticketCount--+"张");
	}
}

测试类

package com.example.snailthink;

/**
 * @program: snailthink
 * @description:
 * @author: SnailThink
 * @create: 2021-09-03 14:09
 **/
public class MyThreadTests{

	private  int count=50;

	public static void main(String[] args) {
		MyThread myThread = new MyThread();
		Thread thread = new Thread(myThread,"小李");
		Thread thread1 = new Thread(myThread,"小张");
		Thread thread2 = new Thread(myThread,"小王");
		thread.start();
		thread1.start();
		thread2.start();
	}

}

运行结果

通过上面的测试结果,三个线程,同时抢票,有时候会抢到同一张票?为什么会有这种问题发现?

在回答这个问题之前,我们应该了解一下 Java 的内存模型(JMM)

3.什么是JMM?

**JMM(Java Memory Model),**是一种基于计算机内存模型,保证了Java程序在各种平台下对内存的访问都能保证效果一致的机制及规范。保证共享内存的原子性、可见性、有序性

可见性: 多线程操作共享内存时,执行结果能够及时的同步到共享内存,确保其他线程对此结果及时可见。

我们通过图,把之前抢票的业务画出来。

同一进程下的多个线程,内存资源是共享的。主内存的count才是共享资源。 小李、小张、小王 实际上不是直接对主内存的count进行写入操作 程序运行过程中,他们每个人,都有各自的工作内存。实际上就是把主内存的count,每个人,都copy一份,对各自的工作内存的变量进行操作。操作完后,再把对应的结果通知到主内存。

模拟抢票demo为什么会有线程安全问题的原因就是:

因为各自都操作自己的工作内存,拿到主内存的值就开始操作。假设,这时候count为23,同一时间,来了俩个线程,那这俩个线程的工作内存拿到的值都是23,这样就会导致,这俩个线程,都会抢到23这张票。

4. 怎么解决线程安全问题?

要想解决线程安全的问题,我们就需要解决一个问题,就是线程之间进行同步交互。了解可见性后,我们知道是没有办法相互操作对方的工作内存的。
一般有如下几种方法

  • synchronized关键字(放在方法上)
  • 同步代码块
  • jdk1.5的Lock
4.1 synchronized关键字(放在方法上)
package com.example.snailthink.test.thread;

/**
 * @program: snailthink
 * @description:
 * @author: SnailThink
 * @create: 2021-09-03 14:43
 **/
public class MySynchronizedThread implements Runnable {
	private  int count=50;

	@Override
	public  void run() {
		while(true){
			buy();
		}

	}
	public synchronized  void  buy(){
		if(count>0){
			try {
				Thread.sleep(50);
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
			System.out.println(Thread.currentThread().getName()+":  抢到第"+count--+"张,"+System.currentTimeMillis());
		}
	}
}

测试结果

通过图片我们可以发现,同一时间,抢票的间隔差不多都是50ms . 为什么多线程会失效???

因为在抢票的方法上,增加了synchronized,导致同一时候,只能有一个线程运行,需要等这个线程运行完后,下一个线程才能运行。

4.2 同步代码块
  • 这种方式就是利用synchronized+锁对象

  • 相对于synchronized 放在方法上方式,性能有提升,只锁了代码块,而不是把这个方法都锁咯。

package com.example.snailthink.test.thread;

/**
 * @program: snailthink
 * @description:
 * @author: SnailThink
 * @create: 2021-09-03 14:41
 **/
public class SynchronizedBlockThreadTest implements Runnable {
	private int count = 50;
	private Object object = new Object();

	@Override
	public void run() {
		while (true) {
			buy();
		}

	}

	public void buy() {
		synchronized (object) {
			if (count > 0) {
				try {
					Thread.sleep(50);
				} catch (InterruptedException e) {
					e.printStackTrace();
				}
				System.out.println(Thread.currentThread().getName() + ":  抢到第" + count-- + "张," + System.currentTimeMillis());
			}
		}
	}
}
4.3 jdk1.5的Lock
package com.example.snailthink.test.thread;

import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

/**
 * @program: snailthink
 * @description:
 * @author: SnailThink
 * @create: 2021-09-03 14:40
 **/
public class LockThreadTests implements Runnable {
	private int count = 50;
	//定义锁对象
	private Lock lock = new ReentrantLock();

	@Override
	public void run() {
		while (true) {
			buy();
		}

	}

	public void buy() {
		lock.lock();
		if (count > 0) {
			try {
				Thread.sleep(50);
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
			System.out.println(Thread.currentThread().getName() + ":  抢到第" + count-- + "张," + System.currentTimeMillis());
		}
		lock.unlock();
	}
}

<think> 首先,用户的问题是:“我想获取Java线程安全的讲解内容 请问Java线程安全详细讲解”。我需要基于提供的引用内容回答。 关键指令: - 使用中文回答。 - 结构清晰,帮助用户逐步解决问题。 - 引用内容末尾添加引用标识,如[^1]。 - 生成相关问题。 - 所有行内数学表达式用$...$,独立公式用$$...$$格式,但在这个上下文中,可能涉及数学表达式较少,但如果有类似概念,如缓存或内存模型,我需要正确格式化。 - 回答真实可靠,基于提供的引用。 提供的引用: - [^1]: 浅谈Java线程安全。定义严格,绝对线程安全很难实现。Java API中标注线程安全的类是相对线程安全。 - [^2]: java 线程安全的理论讲解。事实不可变对象,不需要加锁访问。 - [^3]: java---线程安全详解。线程安全问题源自变量从主内存复制到工作内存,导致可见性问题。指令重排序问题。 - [^4]: java线程安全-通俗易懂的讲解。线程执行顺序不可预见,导致不同步。synchronized保证互斥、有序性和可见性;volatile只解决可见性问题。 我的回答结构: 1. **引入线程安全概念**:解释什么是线程安全,基于引用[^1]的定义。 2. **线程安全级别**:讨论绝对线程安全和相对线程安全(引用[^1])。 3. **核心问题**:解释为什么线程安全问题发生: - 可见性问题(引用[^3]和[^4]):变量从主内存到工作内存的复制。 - 有序性问题(指令重排序,引用[^3])。 - 原子性问题(隐含在[^4]中,执行顺序不可预见)。 4. **解决方案**: - synchronized关键字:解决互斥、有序性和可见性(引用[^4])。 - volatile关键字:只解决可见性问题(引用[^4])。 - 事实不可变对象:不需要加锁(引用[^2])。 5. **示例和通俗解释**:参考[^4]的示例,展示同步问题。 6. **总结**:强调Java线程安全的实践。 7. **生成相关问题**:在末尾添加相关问题。 确保格式: - 行内数学表达式:如涉及概率或公式,用$...$。但线程安全主题可能不需要数学,除非有计算问题。 - 引用标识:在每个引用内容后添加,如[^1]。 逐步讲解: 1. **定义线程安全**:开头解释线程安全是什么,引用Brian Goetz的定义。 2. **问题来源**:详细说明可见性、有序性、原子性问题。 3. **解决方法**:描述synchronized和volatile。 4. **实际例子**:可能添加一个简单代码示例来展示问题。 5. **总结和实践建议**:如使用线程安全类、避免常见错误。 生成相关问题: - 基于内容,如“如何避免线程安全问题?”、“synchronized和volatile的区别?”、“什么是事实不可变对象?”等。 最终回答结构: - 清晰标题或段落。 - 使用中文。 - 结尾添加相关问题。 </think> ### Java线程安全的详细讲解 在Java多线程编程中,线程安全是指多个线程并发访问共享资源时,程序仍能正确执行,不会出现数据不一致或意外行为。线程安全的核心问题源于内存模型和并发控制机制,下面我将一步步解析Java线程安全,结合提供的引用内容,确保讲解清晰可靠。 #### 1. **线程安全的定义与级别** - **什么是线程安全?** 根据Brian Goetz的定义,线程安全要求“不管运行时环境如何,调用者都不需要任何额外的同步措施”。这是一种严格的绝对线程安全标准,但实际上很难实现,因为涉及复杂的并发控制和性能开销。在Java API中,大多数标注为线程安全的类(如`Vector`或`ConcurrentHashMap`)只是相对线程安全,即在特定条件下安全,而非绝对安全[^1]。 - **线程安全级别**: - **绝对线程安全**:几乎不可达,需保证所有操作在任何并发场景下都正确。代价高昂,很少在真实系统中使用[^1]。 - **相对线程安全**:Java中最常见。例如,`synchronized`方法或集合类能保证对象内部状态一致,但调用者仍需注意复合操作(如先检查后执行)[^1]。 #### 2. **线程安全问题的根源** Java线程安全问题主要由三个因素引发:可见性、有序性和原子性问题。这源自Java内存模型(JMM),它将内存分为主内存(共享)和工作内存(线程私有)。 - **可见性问题**: 当一个线程修改共享变量时,修改可能只存在于其工作内存中,不被其他线程立即看到。这导致其他线程使用旧值。例如: - 引用[^3]解释:线程从主内存复制变量到工作内存操作后,如果主内存值被其他线程修改,当前线程无法感知。 - 引用[^4]示例:两个线程同时递增一个计数器变量。不加同步时,每次执行结果不确定(如10200或1060),因为线程看不到彼此的更新[^4]。数学表示为:如果有共享变量$x$,初始为0,线程A执行$x = x + 1$,线程B同时执行相同操作,理想结果$x=2$,但实际可能为1,因为可见性问题导致丢失更新。公式表达为:$x_{\text{final}} \neq 2$的概率很高。 - **有序性问题(指令重排序)**: 编译器或处理器可能优化代码执行顺序(指令重排),改变操作序列。例如,一个初始化操作被重排,可能导致其他线程访问未完全初始化的对象[^3]。这破坏了程序逻辑的预期顺序。 - **原子性问题**: 复合操作(如递增`i++`)并非原子性,涉及多个步骤(读取、修改、写入)。多线程同时执行时,这些步骤可能交织,造成数据错乱。引用[^4]强调线程执行顺序不可预见,是同步问题的根源[^4]。 #### 3. **解决线程安全问题的方法** 针对上述根源,Java提供了多种同步机制。 - **synchronized关键字**: 这是最常用的解决方案。它通过锁定对象来实现互斥访问: - 保证原子性:同一时间只有一个线程能执行同步块。 - 保证可见性:线程退出同步块时,强制工作内存更新到主内存。 - 保证有序性:禁止指令重排进入或跨越同步块。 - 示例:引用[^4]提到,`synchronized`能解决有序性和内存可见性问题。例如: ```java public class Counter { private int count = 0; public synchronized void increment() { count++; // 原子操作,避免线程冲突 } } ``` 在这个例子中,不加`synchronized`时,`count++`可能因可见性和原子性问题出错;加上后,确保了线程安全[^4]。 - **volatile关键字**: 仅解决可见性问题,不保证原子性: - 强制变量读写直接操作主内存,避免工作内存缓存。 - 示例:`volatile boolean flag = true;` 其他线程能立即看到`flag`的变化。 - 局限性:引用[^4]对比`synchronized`,指出`volatile`只处理可见性,无法用于复合操作(如`i++`)[^4]。 - **事实不可变对象**: 这是一种高效的非同步方案。引用[^2]定义:如果共享变量在运行时不会被修改(尽管语法上可变),则无需加锁。例如: - 字符串或配置对象定义为事实不可变。 - 当这些对象被放入线程安全容器(如`ConcurrentHashMap`)时,访问是安全的,因为容器本身处理同步[^2]。 - **其他方法**: - 使用线程安全类:如`java.util.concurrent`包中的`AtomicInteger`(基于CAS实现原子性)。 - 避免共享状态:设计无状态或线程局部变量(ThreadLocal)。 #### 4. **实践建议与常见误区** - **代码示例分析**: 参考引用[^4]的场景:多个线程操作共享变量。如果不加同步,输出结果随机(如10200或1060);使用`synchronized`后,输出稳定。这展示了同步的必要性[^4]。 - **性能权衡**: 过度同步(如滥用`synchronized`)会降低性能。优先使用事实不可变对象或并发容器来减少锁开销[^2][^4]。 - **陷阱**: - 误以为`volatile`万能:它只解决可见性,不处理原子操作。 - 忽略复合操作:即使对象线程安全,复合操作(如`if (map.containsKey(key)) map.put(key, value)`)仍需额外同步[^1]。 #### 5. **总结** Java线程安全的核心在于管理共享资源的并发访问。通过理解可见性、有序性和原子性问题,并合理使用`synchronized`、`volatile`或不可变对象,可以构建可靠的多线程程序。绝对线程安全虽理想化,但现实中相对线程安全已足够实用。记住: "在Java中,线程安全不是二元状态,而是连续谱系"[^1],设计时应根据需求权衡安全性和性能。
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值