JUC并发编程系列详解篇五(线程基础理论进阶)
线程安全
什么是线程安全?当多线程运行了同一代码的时候,如果产生了不同的结果会怎么样?就好比如家里养的鸡下的蛋结果却孵出来一个老鹰,这怎么也显得不合适了,所以线程安全说白了就一句话,当多线程运行同一代码,不会产生不一样的结果。即代码所在的进程中有多个线程在同时运行,而这些线程可能会同时运行这段代码。如果每次运行结果和单线程运行的结果是一样的,而且其他的变量的值也和预期的是一样的,就是线程安全的。
在多线程环境中,当各线程不共享数据的时候,即都是私有(private)成员,那么一定是线程安全的。因为共享数据没有,那线程之间就互不影响。但这种情况并不多见,在多数情况下需要共享数据,这时就需要进行适当的同步控制了。线程安全一般都涉及到synchronized, 就是一段代码同时只能有一个线程来操作 不然中间过程可能会产生不可预制的结果。
举一个简单的例子,本来你在银行存有1000块钱,你要取1500块钱,你去银行办理两个业务,取钱和存钱,假如你的两个业务是同步进行的,这个时候是不是你的钱就取不出来了?因为你的银行了就只有1000块钱,你取不了1500块钱,但是你的钱却是可以存进去。钱取不了,是不是你去银行办理业务的这个进程就废掉了?因为没有完全运行成功嘛。
确保线程安全
通过合理的时间调度,避开共享资源的存取冲突。另外,在并行任务设计上可以通过适当的策略,保证任务与任务之间不存在共享资源,设计一个规则来保证一个客户的计算工作和数据访问只会被一个线程或一台工作机完成,而不是把一个客户的计算工作分配给多个线程去完成。
确保多线程安全的方法:
1、对非安全的代码进行加锁控制
2、使用线程安全的类
3、多线程并发情况下,线程共享的变量改为方法级的局部变量
线程安全在三个方面体现:
原子性: 提供互斥访问,同一时刻只能有一个线程对数据进行操作(atomic, synchronized);
可见性: 一个线程对主内存的修改可以及时地被其他线程看到,(synchronized、 volatile);
有序性: 一个线程观察其他线程中的指令执行顺序,由于指令重排序,该观察结果一般杂乱⽆无序,(happensbefore 原则)
共享数据
第一种:将共享数据封装到一个对象中,把这个共享数据所在的对象传递给不同的Runnable。
第二种:将这些Runnable对象作为某一个类的内部类,共享的数据作为外部类的成员变量,对共享数据的操作分配给外部类的方法来完成,以此实现对操作共享数据的互斥和通信,作为内部类的Runnable来操作外部类的方法,实现对数据的操作。
class shareData {
private int x = 0;
public synchronized void addX(){
x++;
System.out.println("X++:"+x);
}
public synchronized void subX(){
x--;
System.out.println("X--:"+x);
}
}
public class ThreadsVisitData{
public static shareData share = new shareData();
public static void main(String[] args) {
new Thread(new Runnable() {
@Override
public void run() {
for (int i = 0; i < 5; i++) {
share.addX();
}
}
}).start();
new Thread(new Runnable() {
@Override
public void run() {
for (int i = 0; i < 5; i++) {
share.subX();
}
}
}).start();
}
}
ThreadLocal
从名字可以看出这叫线程变量,意思是ThreadLocal中填充的的变量输入当前线程,该变量对其他线程而言是隔离的,ThreadLocal为变量在每个线程中都创建了一个副本,那么每个线程可以访问自己内部的副本变量。
作用: 让某个需要用到的对象在线程间隔离(每个线程都有自己独立的对象)在任何方法中都可以轻松获取到该对象。
使用场景:
1、在进行对象跨层传递的时候,使用ThreadLocal可以避免多次传递,打破层次间的约束。
2、线程间数据隔离
3、进行事务操作,用于存储线程事务信息。
4、数据库连接,Session会话管理。
场景举例1:每个线程需要一个独享的对象(通常是工具类如:SimpleDateFormat和Random)
/**
* @description threadLocal用法1:SimpleDateFormat
* 利用ThreadLocal,给每个线程分配自己的SimpleDateFormat对象,保证线程安全,高效率利用内存
*
* 使用场景:
* 1.在ThreadLocal第一次get的时候把对象给初始化,重写initialValue()方法和返回值, 对象初始化时间可以由我们控制
* 2.如果需要保存到ThreadLocal里的对象的生成时机不由我们随意控制,用ThreadLocal.set直接放到我们ThreadLocal中,以便后续使用
*
* ThreadLocal好处:
* 1.达到线程安全
* 2.不需要加锁,提高执行效率
* 3.更高效的利用内存、节省创建对象的开销
* 4.免去传参的繁琐
*/
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.concurrent.*;
public class ThreadLocal01 {
private static ExecutorService executorService = Executors.newFixedThreadPool(10);
public static void main(String[] args) {
for (int i = 0; i < 1000; i++) {
int index = i;
executorService.submit(new Runnable() {
@Override
public void run() {
String result = new ThreadLocal01().data(index);
System.out.println(result);
}
});
}
executorService.shutdown();
}
public String data(int index){
Date date = new Date(1000 * index);
SimpleDateFormat simpleDateFormat = ThreadSafeSimpleDateFormat.threadLocal2.get();
return simpleDateFormat.format(date);
}
}
class ThreadSafeSimpleDateFormat {
public static ThreadLocal<SimpleDateFormat> threadLocal = new ThreadLocal<SimpleDateFormat>(){
@Override
protected SimpleDateFormat initialValue() {
return new SimpleDateFormat("yyyy-MM-dd hh:mm:ss");
}
};
public static ThreadLocal<SimpleDateFormat> threadLocal2 = ThreadLocal.withInitial(()-> new SimpleDateFormat("yyyy-MM-dd hh:mm:ss"));
}
场景举例2:每个线程内需要保持全局变量(如:拦截器中获取用户信息)可以让不同方法直接使用,避免参数传递的麻烦
/**
* @description threadLocal用法2:每个线程内需要保持全局变量,可以让不同方法直接使用,避免参数传递的麻烦
* 防止内存泄露,用完里ThreadLocal里面值,要进行remove
*/
public class ThreadLocal02 {
public static void main(String[] args) {
Server1 server1 = new Server1();
server1.process();
}
}
class Server1{
public void process(){
User user = new User("张三");
UserContextHolder.userThreadLocal.set(user);
Server2 server2 = new Server2();
server2.process();
}
}
class Server2{
public void process(){
User user = UserContextHolder.userThreadLocal.get();
System.out.println("Server2 获取用户名字:" + user.getName());
UserContextHolder.userThreadLocal.remove();
User newUser = new User("李四");
UserContextHolder.userThreadLocal.set(newUser);
Server3 server3 = new Server3();
server3.process();
}
}
class Server3{
public void process(){
User user = UserContextHolder.userThreadLocal.get();
System.out.println("Server3 获取新用户名字:" + user.getName());
// 用完后一定要remove
UserContextHolder.userThreadLocal.remove();
}
}
class User{
private String name;
public String getName() {
return name;
}
public User(String name) {
this.name = name;
}
}
class UserContextHolder{
public static ThreadLocal<User> userThreadLocal = new ThreadLocal<>();
}
结果: