1.多线程并发环境下,数据的安全问题
1.1为什么这个是重点?
在服务器上运行的项目都是在多线程环境下进行的,线程的定义、线程对象的创建以及线程的启动等都已经由服务器实现,我们无需编写这些代码。
然而,我们需要关注的是在多线程并发的环境下,数据是否安全。
1.2什么时候数据在多线程并发的环境下会存在安全问题呢?
当满足以下三个条件时,数据就有可能存在线程安全问题:
- 多线程并发。
- 存在共享数据。
- 共享数据被修改。
只有满足以上三个条件,线程安全问题才会出现。
1.3怎么解决线程安全问题呢?
当在多线程并发的环境下,存在共享数据,并且这些数据会被修改时,就会出现线程安全问题。那么如何解决呢?
可以通过让线程排队执行来解决线程安全问题,即将多线程改为串行执行。
这种机制被称为线程同步机制。
1.4两个专业术语:
异步编程模型: 线程之间相互独立,各自执行各自的任务,不需要等待。
同步编程模型: 线程之间发生等待关系,一个线程必须等待另一个线程执行完成后才能继续执行。
2.线程安全
2.1synchronized-线程同步
使用synchronized实现线程同步的语法为:
java
synchronized(共享对象){
// 线程同步代码块
}
在这里,共享对象是非常关键的。它必须是多个线程共享的对象,才能实现线程排队。
2.1.1()中写什么?
取决于希望哪些线程进行同步。
假设有线程t1、t2、t3、t4、t5,希望t1、t2、t3进行同步,而t4、t5不需要同步时,可以在括号中写一个t1、t2、t3共享的对象,对于t4、t5来说这个对象不是共享的,所以它们不会被同步。示例代码如下:
java
public class ThreadSafeExample {
// 共享对象
private Object lock = new Object();
public void synchronizedMethod() {
synchronized (lock) {
// 线程同步代码块
}
}
}
在上面的示例中,只有在调用synchronizedMethod()
方法时,才会进行线程同步。其他非同步的方法则不受影响。
2.1.2 synchronized关键字的应用场景
- 多线程对共享资源进行读写操作时,使用
synchronized
可以确保数据的一致性。 - 在并发场景下,需要保护共享资源的完整性和正确性时,使用
synchronized
可以防止多个线程同时修改数据导致的问题。 - 在实现单例模式、生产者消费者模式等需要线程同步的设计模式中,可以使用
synchronized
来实现线程同步。
2.2volatile-保证可见性
volatile
关键字用于修饰共享变量,保证了可见性和禁止指令重排序。
2.2.1 什么是可见性?
可见性是指当一个线程修改了共享变量的值后,其他线程能够立即看到最新的值。
2.2.2 volatile关键字的作用
- 保证共享变量对所有线程的可见性,当一个线程修改了共享变量的值后,其他线程能够立即看到最新的值。
- 禁止指令重排序,保证指令执行的顺序和代码中的顺序一致。
2.2.3 volatile关键字的使用场景
- 对于多个线程共享的变量,在一个线程中修改了该变量的值后,其他线程需要立即看到最新的值时,可以使用
volatile
来保证可见性。 - 在单例模式的双重检查锁定中,使用
volatile
关键字可以防止指令重排序,保证线程安全。
需要注意的是,volatile
关键字只能保证可见性和禁止指令重排序,并不能保证原子性。如果需要保证原子性,可以使用synchronized
关键字或者使用java.util.concurrent
包下的原子类。
2.2.4 volatile关键字的使用
在Java中,我们可以使用volatile
关键字来修饰实例变量或静态变量,确保它们的可见性和有序性。当一个变量被声明为volatile
时,所有线程都能够看到这个变量的最新值,而不管它是在哪个线程中修改的。
java
public class VolatileExample {
private volatile boolean flag = false;
public void setFlag(boolean flag) {
this.flag = flag;
}
public boolean isFlag() {
return flag;
}
}
在上面的示例中,flag
变量被声明为volatile
,表示尽管它在一个线程中被修改,其他线程也能够立即看到这个最新值。因此,setFlag()
和isFlag()
方法都是线程安全的。
2.2.5 volatile关键字的使用场景
1. 可见性问题
当多个线程访问同一个共享变量时,如果其中一个线程修改了这个变量的值,其他线程并不一定能够立即看到这个最新值,这就会导致数据不一致的问题。因此,我们需要使用volatile
关键字来保证可见性。
java
public class VisibilityDemo {
private volatile boolean flag = false;
public void setFlag(boolean flag) {
this.flag = flag;
}
public void printFlag() {
System.out.println(flag);
}
}
在上面的示例中,flag
变量被声明为volatile
,确保了它的可见性。如果没有使用volatile
,那么printFlag()
方法可能会输出旧值,因为其他线程可能还没有看到最新的值。
2. 禁止指令重排序
在JVM中,由于处理器的运行速度和缓存系统等因素的影响,可能会对指令进行重排序,这就可能会导致程序的正确性受到影响。使用volatile
关键字可以防止指令重排序,确保指令执行的顺序和代码中的顺序一致。
java
public class SingletonDemo {
private volatile static SingletonDemo instance;
private SingletonDemo() {}
public static SingletonDemo getInstance() {
if (instance == null) { // 第一次检查
synchronized (SingletonDemo.class) {
if (instance == null) { // 第二次检查
instance = new SingletonDemo();
}
}
}
return instance;
}
}
在上面的示例中,instance
变量被声明为volatile
,保证了它的可见性和禁止指令重排序。
2.3 Lock-显示锁
除了使用synchronized
关键字外,还可以使用java.util.concurrent.locks
包下的Lock
接口及其实现类来实现线程同步。
通过显式地获取锁和释放锁,可以更灵活地控制线程的同步。
2.3.1 Lock的基本用法
java
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class ThreadSafeExample {
private Lock lock = new ReentrantLock();
public void synchronizedMethod() {
lock.lock();
try {
// 线程同步代码块
} finally {
lock.unlock();
}
}
}
在上面的示例中,通过创建一个ReentrantLock
对象作为锁,然后使用lock()
方法获取锁,在线程同步代码块执行完成后使用unlock()
方法释放锁。
与synchronized
不同,Lock
接口还提供了一些额外的功能,比如可以在指定时间内尝试获取锁、可中断的获取锁等。
2.3.2 Lock的灵活性和注意事项
使用Lock
接口可以获得更灵活的线程同步,但也需要注意以下几点:
- 必须在
finally
块中释放锁,以确保锁在异常情况下仍能被正确释放。 - 需要手动获取和释放锁,容易出现忘记释放锁导致死锁的问题。
- 可以通过
tryLock()
方法尝试获取锁并返回布尔值来避免线程阻塞,但需要注意处理获取锁失败的情况。 - 如果多次对
lock()
方法进行调用,必须相应多次调用unlock()
方法,否则会导致其他线程无法获取锁而发生死锁。
2.4 ThreadLocal-线程局部变量
在多线程环境下,使用ThreadLocal
可以实现线程安全的局部变量。
ThreadLocal
为每个线程提供了独立的变量副本,每个线程都可以独立地修改自己的副本,而不会影响其他线程的副本。
2.4.1 ThreadLocal的基本用法
java
public class ThreadLocalExample {
private static ThreadLocal<String> threadLocal = new ThreadLocal<>();
public static void main(String[] args) {
threadLocal.set("Hello, World!");
String value = threadLocal.get();
System.out.println(value); // 输出:Hello, World!
threadLocal.remove();
}
}
在上面的示例中,通过ThreadLocal
的set()
方法设置线程局部变量的值,通过get()
方法获取线程局部变量的值,通过remove()
方法清除线程局部变量。
每个线程都有自己独立的线程局部变量副本,互不干扰。
2.4.2 ThreadLocal的应用场景
- 在多线程环境下,需要保存每个线程自己的状态或数据时,可以使用
ThreadLocal
来实现线程安全。 - 在Web开发中,每个请求对应一个线程,可以使用
ThreadLocal
来保存当前请求的用户信息、请求参数等,方便在不同的组件中访问。
需要注意的是,ThreadLocal
并不能解决共享数据的线程安全问题,它只是提供了一种线程局部的机制。要确保共享数据的线程安全,还需要结合其他线程同步方法来使用。
2.5 线程池
线程池是管理和复用线程的一种机制。通过使用线程池,可以减少线程创建和销毁的开销,并提高系统的性能和资源利用率。
Java提供了java.util.concurrent
包下的Executor
框架,其中的ExecutorService
接口和ThreadPoolExecutor
类可以用于创建和管理线程池。
2.5.1 创建线程池
可以使用Executors
类提供的静态方法来创建不同类型的线程池。
java
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class ThreadPoolExample {
public static void main(String[] args) {
// 创建一个固定大小的线程池
ExecutorService executorService = Executors.newFixedThreadPool(5);
// 提交任务给线程池执行
executorService.execute(new MyTask());
// 关闭线程池
executorService.shutdown();
}
private static class MyTask implements Runnable {
@Override
public void run() {
// 任务执行逻辑
}
}
}
在上面的示例中,通过Executors.newFixedThreadPool()
方法创建一个固定大小的线程池,然后使用execute()
方法提交任务给线程池执行。最后使用shutdown()
方法关闭线程池。
2.5.2 线程池的工作原理
线程池内部通过一个任务队列来保存等待执行的任务,并根据需要创建或销毁线程。当有任务提交给线程池时,线程池会从任务队列中取出任务,并将其分配给空闲的工作线程执行。
线程池还可以控制并发线程的数量、处理超时和异常情况等。
2.5.3 线程池的优势和注意事项
使用线程池可以带来以下优势:
- 降低线程创建和销毁的开销,减少系统资源的消耗。
- 提高系统的性能和吞吐量,避免因频繁创建线程导致的线程过多而系统崩溃。
- 可以对线程进行统一的管理和监控,提供更好的可扩展性和稳定性。
在使用线程池时需要注意以下几点:
- 需要选择合适的线程池大小,避免线程数过多导致资源浪费,或者线程数过少导致任务处理不及时。
- 在使用完线程池后需要及时关闭,释放资源。
- 需要合理处理任务的拒绝策略,避免任务被丢弃。
- 尽量使用
submit()
方法而不是execute()
方法来提交任务,以便获取任务执行的结果。
以上就是Java中常用的线程同步和线程池的相关内容,可以相互学习.