多线程基础概念 | 线程安全

it2025-03-14  19

线程安全: 如果多线程环境下代码运行的结果是符合我们预期的,即在单线程环境应该的结果,则说这个程序是线程安全的。

观察下边代码:

package UnsafeThread; public class UnsafeThread { private static int COUNT; public static void main(String[] args) { for (int i = 0; i < 20; i++) { Thread t = new Thread(new Runnable() { @Override public void run() { for(int j = 0;j < 10000;j++){ COUNT++; } } }); t.start(); } while (Thread.activeCount() > 1){ Thread.yield(); } System.out.println(COUNT); } }

按经验来说,答案应该是20,0000。我们运行代码看看:

Connected to the target VM, address: '127.0.0.1:55920', transport: 'socket' 194290 Disconnected from the target VM, address: '127.0.0.1:55920', transport: 'socket'

结果不到20,0000。这就是线程不安全。


线程安全的三大特性

1.原子性: 把一段代码想象成一个房间,每个线程就是要进入这个房间的人。如果没有任何机制,A进入房间之后,还没有出来。B 也可以进入房间,打断 A 在房间里的隐私。这个就是不具备原子性的。

解决这个问题呢,只要给房间加一把锁,A 进去就把门锁上,其他人就进不来了。这样就保证了这段代码的原子性了。

有时也把这个现象叫做同步互斥,表示操作是互相排斥的。 一条 java 语句不一定是原子的,也不一定只是一条指令 比如刚才我们常用的 n++,其实是由三步操作组成的

从内存把数据读到 CPU进行数据更新把数据写回到 CPU

比如 Object object = new Object(); 也分为三条指令:

分配内存为变量赋值写回到内存

不保证原子性会带来的问题: 一个线程正在对一个变量操作,中途其他线程插入进来了,如果这个操作被打断了,结果就可能是错误的。

2.可见性 为了提高效率,JVM在执行过程中,会尽可能的将数据在工作内存中执行,但这样会造成一个问题,共享变量在多线程之间不能及时看到改变,这个就是可见性问题。比如,两个线程同时执行,一个先获取到n = 1,对其进行++操作,还没有返回的时候。另一个线程也获取到n ,准备进行++操作,由于上个线程还没有把数据写回cpu,所以此时获取到n =1。当两个线程都结束的时候,n结果为2,发生了错误

3.代码顺序性 一段代码是这样的:

去A店吃饭去B店吃饭去C店吃饭

如果是在单线程情况下,JVM、CPU指令集会对其进行优化,比如,按 1->3->2的方式执行,也是没问题,这种叫做指令重排序 代码重排序会给多线程带来什么问题

刚才那个例子中,单线程情况是没问题的,优化是正确的,但在多线程场景下就有问题了,什么问题呢。可能你去A吃饭,吃完之后,B店人满了进不去或者直接关门了,如果指令重排序了,代码就会是错误的。 线程内看自己代码运行都是有序的(保证代码执行的依赖关系),看其他线程代码都是无序的


解决线程不安全问题

1.synchronized关键字(满足线程安全三大特性)

实现原理: 线程进入到synchronized代码行时,需要获取对象锁: 1.获取成功,往下执行代码 2.获取失败,阻塞在synchronized代码行,jvm将竞争失败的线程全部放在同步队列。

当线程退出synchronized修饰的代码块或者synchronized方法时, 1.退回对象锁 2.通知JVM及系统,唤醒同步队列的其他线程,开始竞争这把锁


语法使用: 1.作用于静态方法(static )

//相当于对SafeThread.class,当前类对象加锁 package SafeThread; public class synchronizedTest implements Runnable{ //共享资源 static int i =0; /** * synchronized 修饰静态方法 */ public static synchronized void increase(){ i++; } @Override public void run(){ for (int j =0 ; j<10000;j++){ increase(); } } public static void main(String[] args) throws InterruptedException { Thread t1 = new Thread(new synchronizedTest()); Thread t2 = new Thread(new synchronizedTest()); t1.start(); t2.start(); t1.join(); t2.join(); System.out.println(i); } } Connected to the target VM, address: '127.0.0.1:51292', transport: 'socket' 20000 Disconnected from the target VM, address: '127.0.0.1:51292', transport: 'socket'

分析:由例子可知,两个线程实例化两个不同的对象,但是访问的方法是静态的,两个线程发生了互斥(即一个线程访问,另一个线程只能等着),因为静态方法是依附于类而不是对象的,当synchronized修饰静态方法时,锁是class对象。

2.实例方法(无static )

package SafeThread; public class synchronizedTest implements Runnable { //共享资源 static int i = 0; /** * synchronized 修饰实例方法 */ public synchronized void increase() { i++; } @Override public void run() { for (int j = 0; j < 10000; j++) { increase(); } } public static void main(String[] args) throws InterruptedException { synchronizedTest test = new synchronizedTest(); Thread t1 = new Thread(test); Thread t2 = new Thread(test); t1.start(); t2.start(); t1.join(); t2.join(); System.out.println(i); } } Connected to the target VM, address: '127.0.0.1:51292', transport: 'socket' 20000 Disconnected from the target VM, address: '127.0.0.1:51292', transport: 'socket'

分析:当两个线程同时对一个对象的一个方法进行操作,只有一个线程能够抢到锁。因为一个对象只有一把锁,一个线程获取了该对象的锁之后,其他线程无法获取该对象的锁,就不能访问该对象的其他synchronized实例方法

3.代码块

package SafeThread; public class SafeThread { private static int COUNT; public static void main(String[] args) { for (int i = 0; i < 20; i++) { Thread t = new Thread(new Runnable() { @Override public void run() { for (int j = 0; j < 10000; j++) { //this指new的Runnable() synchronized (this){ //synchronized (SafeThread.class){ //SafeThread.class指当前类对象 //synchronized (getClass()){ //指new的Runnable()实例对象 COUNT++; } } } }); t.start(); } while (Thread.activeCount() > 1) { Thread.yield(); } System.out.println(COUNT); } }

其中 作用于静态方法

public static synchronized void add() { }

public static void add() { synchronized (SafeThread.class){ } }

的作用是一样的,锁的是当前类的class对象 ,进入同步代码前要获得当前类对象的锁


作用于实例方法

//new SafeThread().add() 相当于对SafeThread()对象加锁 public synchronized void add() { //谁调用它,对谁加锁 }

public void add() synchronized (this){ }

作用是一样的,锁的是当前实例对象 ,进入同步代码前要获得当前实例的锁 如果不在同步代码块内部,则没有同步互斥作用。


使用synchronized加锁操作的关注点: 1.对哪个对象加锁 —— 一个对象只有一把锁 2.只有同一个对象,才会有同步互斥的作用(我正在使用这个方法,你就不能使用,满足了线程安全的三大特性) 3.对于synchronized内代码,同一个时间只有一个线程在运行 4.运行的线程数越多,性能下降越快(因为线程越多,归还对象锁的时候,越多的线程就会不停的在唤醒、阻塞间互相切换) 5.synchronized内的代码执行时间越短,性能下降越快


2.Volatile关键字(满足可见性,顺序性)

1.被volatile修饰的变量保证对所有线程可见。 既然volatile变量对所有线程是立即可见的,在各个线程中不存在一致性问题。那么,我们是否能得出:volatile变量在并发运算下是线程安全的呢?不多说,直接上代码

package Volatile; public class Volatile { private volatile static int COUNT; public static void main(String[] args) { for (int i = 0; i < 20; i++) { Thread t = new Thread(new Runnable() { @Override public void run() { for(int j = 0;j < 10000;j++){ COUNT++; } } }); t.start(); } while (Thread.activeCount() > 1){ Thread.yield(); } System.out.println(COUNT); } }

结果:

Connected to the target VM, address: '127.0.0.1:54850', transport: 'socket' 193702 Disconnected from the target VM, address: '127.0.0.1:54850', transport: 'socket'

答案还是不对。 原因在于被volatile修饰的变量,不满足三大特性中的原子性, 所以线程还是不安全的

volatile赋值时候不能依赖变量(常量赋值可以保证线程安全)

使用场景: 可以结合线程加锁的一些手段,提高效率 只是变量的读取,常量的赋值,可以不加锁,而是使用volatile,可以提高效率

案例:

// 饿汉模式 class Singleton { private static Singleton instance = new Singleton(); private Singleton() { } public static Singleton getInstance() { return instance; } } // 懒汉模式-单线程版 class Singleton { private static Singleton instance = null; private Singleton() { } public static Singleton getInstance() { if (instance == null) { instance = new Singleton(); } return instance; } } // 懒汉模式-多线程版-性能低 class Singleton { private static Singleton instance = null; private Singleton() { } public synchronized static Singleton getInstance() { if (instance == null) { instance = new Singleton(); } return instance; } }

2.禁止指令重排序优化 -(单例模式-双重校验锁)

指令重排序是什么?简单点说就是jvm会把代码中没有依赖赋值的地方打乱执行顺序,由于一些规则限定,我们在单线程内观察不到打乱的现象(线程内表现为串行的语义),但是在并发程序中,从别的线程看另一个线程,操作是无序的。

双重校验锁描述: 1.volatile 修饰变量 2.私有构造方法 3.双重校验锁的写法保证线程安全

// 懒汉模式-多线程版-双重校验锁-性能高 class Singleton { private static volatile Singleton instance = null; private Singleton() { } public static Singleton getInstance() { //由于该步骤只是读取变量,具有原子性,用volatile修饰,可以保证可见性,提高效率 if (instance == null) { synchronized (Singleton.class) { // 如果少了内部校验,会产生多次new。保证单例 if (instance == null) { instance = new Singleton(); } } } return instance; } }

如果不禁止重排序,会发生什么问题? 上文说过 instance = new Singleton()可分解为:

1.分配对象的内存空间 2.初始化对象 3.设置instance指向刚分配的内存地址

操作2依赖1,但是操作3不依赖2,此时若是线程内出现指令重排序,则有可能出现1,3,2的顺序。 当出现这种顺序,也就是程序执行了1和3之后,变量已经赋值成功,但不是instance对象,instance仍然为空。 此时其他线程若也在运行,将会在第一个if判断错误,对象有可能没有还正确初始化instance,就直接返回了变量的值,而不是我们需要的对象,此时程序就会出错。

volatile通过禁止指令重排序保证了有序性,避免了这种错误出现

最新回复(0)