目录
前言
一、什么是线程安全?
二、为什么需要线程安全?
三、实现线程安全的方法
四、synchronized
使用 synchronized 关键字时,需要注意以下几点:
五、Demo讲解
前言
在现代软件开发中,尤其是在多线程编程中,线程安全(Thread Safety)是一个至关重要但又复杂的话题。本文将从基础概念开始,逐步深入,帮助你理解什么是线程安全,以及如何在实际开发中实现线程安全。
一、什么是线程安全?
线程安全是指多个线程可以同时访问和修改共享数据而不导致数据的不一致性或程序错误。简单来说,当一个对象或函数在多线程环境下能够正确地执行,并且不会引起任何未定义的行为时,它就是线程安全的。
二、为什么需要线程安全?
多线程编程的主要目的是为了提高程序性能和响应速度。然而,当多个线程同时操作共享资源时,可能会出现竞争条件(Race Condition)、死锁(Deadlock)以及饥饿(Starvation)等问题。这些问题不仅会导致程序运行结果不正确,还可能引发崩溃和难以调试的错误。
三、实现线程安全的方法
-
互斥同步:使用锁(如 synchronized、ReentrantLock 等)来保护共享数据,在同一时刻只允许一个线程访问共享数据,其他线程需要等待锁释放后才能访问。
-
原子操作:使用原子操作来确保对共享数据的操作是不可分割的,不会被中断,常见的原子操作包括 atomic 包下的原子类,以及 volatile 关键字修饰的变量。
-
无锁并发编程:通过使用无锁的数据结构和算法来实现线程安全,例如 CAS(Compare and Swap)操作,乐观锁机制等。
-
线程封闭:将共享数据限制在单个线程内部,避免多个线程之间直接访问共享数据,从而避免竞态条件。
-
不可变对象:通过创建不可变对象来避免多线程并发修改共享状态,从而避免线程安全问题。
四、synchronized
当一个线程访问一个被 synchronized 修饰的方法或代码块时,会自动获取该方法或代码块对应的锁,其他线程必须等待该线程释放锁后才能获取锁并进入临界区。这种方式可以有效避免多个线程同时访问共享数据时发生竞态条件和其他线程安全问题。
使用 synchronized
关键字时,需要注意以下几点:
-
修饰方法:在修饰方法时,锁对象默认为当前对象(this),不同的线程需要获取的是同一个对象的锁。
-
修饰代码块:在修饰代码块时,需要指定锁对象,一般可以使用 this、类名.class 等作为锁对象。需要注意的是,锁对象必须是同一个对象才能保证线程同步。
-
可重入性:在同一个线程中,如果一个方法已经获取了锁,那么在调用另一个被 synchronized 修饰的方法时,该线程仍然能够获取到同一个锁,即 synchronized 具有可重入性。
-
锁的释放:在 synchronized 块执行完毕或者抛出异常时,锁会自动释放。如果在 synchronized 块中使用了 wait() 方法,则锁也会被释放,等待其他线程唤醒后再次获取锁。
五、Demo讲解
package com.ctb.demo;
/**
* 线程安全概念:当多个线程访问某一个类(对象或方法)时,这个对象始终都能表现出正确的行为,那么这个类(对象或方法)就是线程安全的
* synchronized:可以在任意对象或方法上加锁,而加锁的这段代码称为"互斥区"或"临界区"
* @author 彪
*
*/
public class MyThread extends Thread{
private int count = 5;
public void run() {
count--;
System.out.println(this.currentThread().getName()+"count = "+count);
}
public static void main(String[] args) {
/**
* 当多个线程访问MyThread的run方法时,以排队的方式进行处理(这里排队是按照CPU分配的先后顺序而定的)
* 一个线程想要执行synchronized修饰的方法里的代码
* 1.尝试获得锁
* 2.如果拿到锁,执行synchronized代码内容:拿不到锁,这个线程就会不断尝试获得这把锁,直道拿到为止,
* 而且是多个线程同时去竞争这把锁。(也就是会有锁竞争的问题)
*/
MyThread myThread = new MyThread();
Thread t1 = new Thread(myThread,"t1");
Thread t2 = new Thread(myThread,"t2");
Thread t3 = new Thread(myThread,"t3");
Thread t4 = new Thread(myThread,"t4");
Thread t5 = new Thread(myThread,"t5");
t1.start();
t2.start();
t3.start();
t4.start();
t5.start();
}
}
结果:并不是我们想要的,count--
package com.ctb.demo;
/**
* 线程安全概念:当多个线程访问某一个类(对象或方法)时,这个对象始终都能表现出正确的行为,那么这个类(对象或方法)就是线程安全的
* synchronized:可以在任意对象或方法上加锁,而加锁的这段代码称为"互斥区"或"临界区"
* @author 彪
*
*/
public class MyThread extends Thread{
private int count = 5;
//synchronized加锁
public synchronized void run() {
count--;
System.out.println(this.currentThread().getName()+"count = "+count);
}
public static void main(String[] args) {
/**
* 当多个线程访问MyThread的run方法时,以排队的方式进行处理(这里排队是按照CPU分配的先后顺序而定的)
* 一个线程想要执行synchronized修饰的方法里的代码
* 1.尝试获得锁
* 2.如果拿到锁,执行synchronized代码内容:拿不到锁,这个线程就会不断尝试获得这把锁,直道拿到为止,
* 而且是多个线程同时去竞争这把锁。(也就是会有锁竞争的问题)
*/
MyThread myThread = new MyThread();
Thread t1 = new Thread(myThread,"t1");
Thread t2 = new Thread(myThread,"t2");
Thread t3 = new Thread(myThread,"t3");
Thread t4 = new Thread(myThread,"t4");
Thread t5 = new Thread(myThread,"t5");
t1.start();
t2.start();
t3.start();
t4.start();
t5.start();
}
}
结果:
注意:
当多个线程访问MyThread的run方法时,以排队的方式进行处理(这里排队是按照CPU分配的先后顺序而定的) 一个线程想要执行synchronized修饰的方法里的代码 1.尝试获得锁 2.如果拿到锁,执行synchronized代码内容:拿不到锁,这个线程就会不断尝试获得这把锁,直道拿到为止, 而且是多个线程同时去竞争这把锁。(也就是会有锁竞争的问题:前面线程count不是按顺序的)