哎嘿,CSDN的大佬您来啦,这来都来了,浅浅的给个赞呗!!!
系列文章目录
线程的创建与主要方法分析和其他基础知识点;可以参考以下文章
线程知识点总结_南斋孤鹤的博客-CSDN博客_线程知识(超全)线程知识点、及线程方面的一些理解性问题https://blog.csdn.net/m0_64231944/article/details/124069105?spm=1001.2014.3001.5502
一、什么是线程安全问题?
前言
线程安全是在项目中使用线程的关键点,尤其是在多线程的使用中,线程安全是很重要的,下面我们来详细介绍线程安全问题的出现原因和解决办法。
一、什么是线程安全问题?
线程安全是多线程领域的问题,(线程安全问题就是在多线程环境中 , 并且存在数据共享 (即多个线程操作同一个数据))线程安全可以简单理解为一个方法或者一个实例可以在多线程环境中使用而不会出现问题。** 在同一程序中运行多个线程本身不会导致问题,问题在于多个线程访问了相同的资源。**
在程序中如果使用成员变量, 且对成员变量进行数据修改 , 就存在数据共享问题, 也就会出现线程安全问题。
二、为什么会出现线程安全问题呢?
在 Java 程序中,存储数据的内存空间分为共享内存和本地内存。线程在读写主存的共享变量时,会先将该变量拷贝一份副本到自己的本地内存,然后在自己的本地内存中对该变量进行操作,完成操作之后再将结果同步至主内存。主内存数据和本地内存的不同步,导致多个线程同时操作主内存里的同一个变量时,变量数据可能会遭到破坏。可以举个例子来解释:
在存在多线程的环境中,并且有共享的数据,这中情况下就可能会出现线程安全问题:即一个线程访问的共享数据被其他线程修改了, 那么此时就发生了线程安全问题 。我们举一个例子:现有以int value=0;有A、B两个线程操作value,A线程进行+1操作,B线程进行+3操作同一时刻,AB、同时访问value的值,获取的结果都是0;然后同时进行操作,这个时候就会出现bug,value的值就可能是1或3 了,这就是出现了线程安全问题。
三、造成线程安全问题的原因还有那些?
** 下面我们提出的以下问题:后面我们会逐一解答;**
1)抢占式执行,调度过程随机(什么是抢占式呢?);
2)多个线程同时修改同一个变量(可以适当调整代码结构,避免这种情况);
3)针对变量的操作,不是原子的(什么是原子变量?是不是将变量定义为原子性后就一定能解决问题?);
4)内存可见性,一个线程频繁读,一个线程写(什么是内存可见性?);
2.问题解答:
2.1、什么是抢占式呢?:
** ** 这是一种CPU的调度方式:程序根据某种原则,去暂停某个正在执行的进程,将已分配给该线程的CPU的执行权重新分配给另一个线程,举例:也就是A线程在拥有CPU执行权的情况下,B线程抢断了A线程的执行权,进行B线程的部分操作。
为加深理解,我们拓展下与之对应的CPU执行方式:非抢占式方式:在采用这种调度方式时,一旦把CPU的执行权分配给某个线程后,就一直让它运行下去,决不会因为任何其它原因去抢占当前正在运行的线程的CPU执行权,直至该线程完成他的所有操作,或发生某事件而被阻塞时,才把执行权分配给其它线程。
2.1、****什么是原子变量?
** **说起原子变量,首先我们要理解原子是什么东西,说到原子,我们是熟悉的,我们会想到我们在化学中学习到的化学式,比如水分子的化学式,它有以下特性:对原子变量的操作是一个整体,是不会分割的(例如:compareAndSet),跨越到原子变量中,原子变量就是在变量的基础上添加了原子性,那么原子变量的原子性体现在哪里呢?原子变量我们可以理解为一个特殊的变量类型(实际是一个特殊类),java对他有专门的处理原子变量的独有的方法,比如 提供的特有方法boolean compareAndSet(expectedValue, updateValue) 的功能,compare和set这两个操作是不可分割的,这就是原子性的操作。
总结:
原子性是指一个操作,要么全部执行并且执行过程不会被打断,要么就都不执行。
- Java 语言本身只保证了基本类型变量的读取和赋值是原子性操作。
- 简单操作的原子性可以通过原子类实现。
- 通过 synchronized 和 ReenTrantLock 等锁结构可以保证更大范围的原子性。
原子变量的特殊之处(特有方法的不可分割性),就可以避免出现上文提到的多个线程操作同一个共享数据引起的线程安全问题。
提到原子变量不得不提:原子变量相关的CAS算法
CAS (Compare-And-Swap) 是一种硬件对并发的支持,针对多处理器操作而设计的处理器中的一种特殊指令,用于管理对共享数据的并发访问(这是书上的难以理解的屁话,我们不用管,只需要知道他是操作原子变量的一个方法)。
CAS 是一种无锁的非阻塞算法的实现。
CAS 中包含了3 个操作数:
1、需要读写的内存值V
2、进行比较的值A
3、拟写入的新值B
4、当且仅当V 的值等于A 时,CAS 通过原子方式用新值B 来更新V 的值,否则不会执行任何操作
下面以原子变量类型中的AtomicInteger(其他还有Atomiclong等等原子类型)简略示例使用步骤:
class MyRunnable2 implements Runnable{
private AtomicInteger serialNumber = new AtomicInteger(0);
@Override
public void run() {
try {
Thread.sleep(200);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+":"+getAndAdd());
}
private int getAndAdd(){
return serialNumber.getAndIncrement();
}
}
其他常用方法:
以下为方法为AtomicInteger基于原子操作常用方法
//获取当前原子变量中的值并为其设置新值
public final int getAndSet(int newValue)
//比较当前的value是否等于expect,如果是设置为update并返回true,否则返回false
public final boolean compareAndSet(int expect, int update)
//获取当前的value值并自增一
public final int getAndIncrement()
//获取当前的value值并自减一
public final int getAndDecrement()
//获取当前的value值并为value加上delta
public final int getAndAdd(int delta)
2.3、什么是内存可见性?
内存可见性就是(我们引用之前提到的A、B线程操作value值的例子),A线程操作value的值的结果B会知道(A的操作对B可见),B修改value的值的结果A线程也会道(B的操作对A可见),这样value的结果就只有一种情况了value=4;
总结:所谓内存可见性就是A线程对共享内存的操作其他线程可见;
总结:
可见性是指当多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即看得到修改的值。
- Java 语言会尽可能保证主内存数据和本地内存同步,但仍可能出现不可见问题。
- 通常用 volatile 关键字来保证可见性。
- 通过 synchronized 和 ReenTrantLock 等锁结构在释放锁之前会将对变量的修改刷新到主存当中,也能够保证可见性。
volatile 关键字
修饰成员变量,每次被线程访问时,强迫从主存中读写该成员变量的值。
volatile 关键字只能保证可见性,不能保证原子性。多个线程同时操作主内存里的同一个变量时,变量数据仍有可能会遭到破坏。
- 线程执行过程中如果 CPU 一直满载运转,就会默认使用本地内存中的值,而没有空闲读取主存同步数据。
- 线程执行过程中一旦 CPU 获得空闲,JVM 也会自动同步主存数据,尽可能保证可见性。
public class ThreadDemo {
public static void main(String[] args) throws InterruptedException {
MyThread t = new MyThread();
t.start();
try {
Thread.sleep(1000);
} catch(InterruptedException e) {}
t.setRun(false);
}
}
class MyThread extends Thread {
// 添加 volatile 关键字,强制同步主存数据。
// 删除 volatile 关键字,子线程将始终读取本地内存中 true 副本:陷入死循环。
private volatile boolean run = true;
public void setRun(boolean run) { this.run = run; }
@Override
public void run() {
while (this.run == true) {
int a = 2;
int b = 3;
int c = a + b;
// System.out.print("CPU rest"); 打印输出时 CPU 获得空闲,自动同步主存数据。
}
System.out.print("end");
return;
}
}
synchronized关键字
修饰方法或代码块。被线程访问时由线程抢占锁,直到执行完毕后自动释放锁。其他线程没有获得锁将无法访问上锁内容。保证了指定内容在同一时刻只有一个线程能访问。
- 修饰 static 方法实质是给当前类上锁:这个类的所有同步 static 方法共享一个锁。
- 修饰实例方法实质是给对象上锁:这个对象内所有的 synchronized 实例方法共享一个锁。
每一个对象都有且仅有一个与之对应的 monitor 对象。synchronized 关键字修饰方法时会对方法添加标志位,当线程执行到某个方法时,JVM会去检查该方法的访问标志是否被设置,如果设置了线程会先获取这个对象所对应的 monitor 对象,再执行方法体,方法执行完后释放 monitor 。
同步代码块则是在同步代码块前插入 monitorenter ,在同步代码块结束后插入 monitorexit 。
public class ThreadDemo {
public static void main(String[] args) {
ThreadDemo test = new ThreadDemo();
new Thread(test::m1).start();
new Thread(test::m2).start();
}
public synchronized void m1() {
System.out.println("1");
try {
Thread.sleep(1000);
} catch(InterruptedException e) {}
System.out.println("2");
}
public synchronized void m2() {
System.out.println("3");
try {
Thread.sleep(500);
} catch(InterruptedException e) {}
System.out.println("4");
}
}
二者的异同:
1. volatile 关键字用于修饰变量,synchronized 关键字用于修饰方法以及代码块。
- volatile 关键字是数据同步的轻量级实现,性能比 synchronized 关键字更好。
- volatile 关键字被多线程访问不会发生阻塞,synchronized 关键字可能发生阻塞。
- volatile 关键字能保证数据的可见性,但不能保证数据的原子性。synchronized 关键字两者都能保证。
四、那么出了线程安全问题我们如何解决线程安全问题呢?
我们还是通过一个例子来讲解理顺解决方法和思路;
例子:创建三个窗口卖票,总票数为100张.使用实现Runnable接口的方式
其他创建线程的方式可以参考
线程知识点总结_南斋孤鹤的博客-CSDN博客_线程知识(超全)线程知识点、及线程方面的一些理解性问题https://blog.csdn.net/m0_64231944/article/details/124069105?spm=1001.2014.3001.5502
此时的线程问题为:卖票过程中,出现了重票、错票-->出现了线程的安全问题
②.问题出现的原因:当某个线程操作共享数据(车票)的过程中,尚未操作完成时,其他线程参与进来,也同时操做共享数据(车票)。
③.如何解决:当一个线程a在操作共享数据(车票ticket)的时候,其他线程不能参与进来。直到线程a操作完共享数据(车票ticket)之后,所有线程在重新争抢CPU的执行权,谁抢到谁再操作共享数据。这种情况即使线程出现了阻塞,也不会出现多个线程同时出售同一张票。
④.在Java中,我们通过同步机制,来解决线程的安全问题。
方式一:同步代码块
synchronized( 同步监视器){
//需要被同步的代码}
说明:
1.操作共享数据的代码,即为需要被同步的代码;要包的不多不少
2.共享数据:多个线程共同操作的变量。比如:ticket就是共享数据。
3.同步监视器,俗称:锁。任何一个类的对象,都可以充当锁,我们常用object对象。
要求:多个线程必须要共用同一把锁。
补充:根据实际条件我们可以用this充当锁(继承Thread实现多线程时)我们也可以用类充当监视器,类也是对象。
方式二:同步方法
如果操作共享数据的代码我们封装在一个方法内,我们不妨将此方法声明为同步的(使用synchronized修饰方法);
也有锁,锁是this;
方式三:lock锁
(JDK5.0新增);
实现线程(实现Runnable)。
实例化Reentrantlock;
手动调用锁定方法lock();
手动调用解锁方法unlock();
下面我们演示一个案例,调用三个线程来出售100张电影票
方式四:使用原子变量(有弊端,谨慎使用)
有什么弊端呢,原子变量使用在存在多线程和有共享变量的程序中,他的弊端就体现在操作共享变量的线程不能太多,太多之后就会出现卡顿,性能下降,倒还不如使用synchronized关键字了。
所以:谨慎使用原子变量。
五、买票例子代码实现:
public class MyRunnableDemo {
public static void main(String[] args) {
MyRunnable mr = new MyRunnable();
Thread t1 = new Thread(mr,"第一窗口");
Thread t2 = new Thread(mr,"第二窗口");
Thread t3 = new Thread(mr,"第三窗口");
t1.start();
t2.start();
t3.start();
}
}
public class MyRunnable implements Runnable {
private int tickets = 100;
private Object obj = new Object();
private Lock lock = new ReentrantLock();
@Override
public void run() {
while (true) {
try {
lock.lock();
if (tickets > 0) {
System.out.println(Thread.currentThread().getName() + "开始售第:" + tickets + "张票");
tickets--;
} else {
break;
}
} finally {
lock.unlock();
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
**代码效果你可以复制代码到idea中查看 **
总结
以上就是今天要记录的内容,本文仅仅简单介绍了线程安全问题的一些相关知识,这仅仅是我对于该问题的认识和理解,不完全正确、并且后续会不断改进,希望可以帮到各位CSDN的读者。
版权归原作者 南斋孤鹤 所有, 如有侵权,请联系我们删除。