⭐⭐⭐ Spring Boot 项目实战 ⭐⭐⭐ Spring Cloud 项目实战
《Dubbo 实现原理与源码解析 —— 精品合集》 《Netty 实现原理与源码解析 —— 精品合集》
《Spring 实现原理与源码解析 —— 精品合集》 《MyBatis 实现原理与源码解析 —— 精品合集》
《Spring MVC 实现原理与源码解析 —— 精品合集》 《数据库实体设计合集》
《Spring Boot 实现原理与源码解析 —— 精品合集》 《Java 面试题 + Java 学习指南》

摘要: 原创出处 blog.csdn.net/fuzhongmin05/article/details/59110866 「fuzhongmin05」欢迎转载,保留摘要,谢谢!


🙂🙂🙂关注**微信公众号:【芋道源码】**有福利:

  1. RocketMQ / MyCAT / Sharding-JDBC 所有源码分析文章列表
  2. RocketMQ / MyCAT / Sharding-JDBC 中文注释源码 GitHub 地址
  3. 您对于源码的疑问每条留言将得到认真回复。甚至不知道如何读源码也可以请教噢
  4. 新的源码解析文章实时收到通知。每周更新一篇左右
  5. 认真的源码交流微信群。

当我们查看JDK API的时候,总会发现一些类说明写着,线程安全或者线程不安全,比如说到StringBuilder中,有这么一句,“将StringBuilder 的实例用于多个线程是不安全的。如果需要这样的同步,则建议使用StringBuffer。”

提到StringBuffer时,说到“StringBuffer是线程安全的可变字符序列,一个类似于String的字符串缓冲区,虽然在任意时间点上它都包含某种特定的字符序列,但通过某些方法调用可以改变该序列的长度和内容。可将字符串缓冲区安全地用于多个线程。可以在必要时对这些方法进行同步,因此任意特定实例上的所有操作就好像是以串行顺序发生的,该顺序与所涉及的每个线程进行的方法调用顺序一致”。

StringBuilder是一个可变的字符序列,此类提供一个与StringBuffe兼容的API,但不保证同步。该类被设计用作StringBuffer的一个简易替换,用在字符串缓冲区被单个线程使用的时候(这种情况很普遍)。如果可能,建议优先采用该类,因为在大多数实现中,它比StringBuffer要快。将StringBuilder的实例用于多个线程是不安全的,如果需要这样的同步,则建议使用StringBuffer。

根据以上JDK文档中对StringBuffer和StringBuilder的描述,得到对String、StringBuilder与StringBuffer三者使用情况的总结:

  • 如果要操作少量的数据用String
  • 单线程操作字符串缓冲区下操作大量数据StringBuilder
  • 多线程操作字符串缓冲区下操作大量数据StringBuffer

那么下面手动创建一个线程不安全的类,然后在多线程中使用这个类,看看有什么效果。

public class Count {
private int num;
//public void count() {
// for(int i = 1; i <= 100; i++) {
// num += i;
// }
// System.out.println(Thread.currentThread().getName() + "-" + num);
//}

public int getNum() {
return num;
}

public void increment(int i) {
num = num + i;
}
}

在这个类中的increment方法实现num变量与指定变量作加法。

public class ThreadTest {
public static void main(String[] args) {
Runnable runnable = new Runnable() {
Count count = new Count();
@Override
public void run() {
for (int i = 0; i < 1000; i++) {
count.increment(1);
}
System.out.println(Thread.currentThread().getName() + "-" + count.getNum());
try {
Thread.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
};

for(int i = 0; i < 10; i++) {
Thread thread = new Thread(runnable);
thread.start();
}
}
}

这里启动了10个线程,看一下输出结果:

Thread-0-1660
Thread-2-2660
Thread-3-3660
Thread-1-1660
Thread-4-4882
Thread-5-5579
Thread-6-6579
Thread-7-7579
Thread-8-8579
Thread-9-9579

期望的结果是每个线程都能输出1000,但实际上每个线程的输出值都不一样而且不是整数,多运行几次每次的输出结果都不一样,要想得到我们期望的结果,有几种解决方案:

1、将累加逻辑移到Count类中,并且使用局部变量而不是成员变量;

public class Count {
public void count() {
int number = 0;
for(int i = 0; i < 1000; i++) {
number += 1;
}
System.out.println(Thread.currentThread().getName() + "-" + number);
}
}

~

public class ThreadTest {
public static void main(String[] args) {
Runnable runnable = new Runnable() {
Count count = new Count();
@Override
public void run() {
count.count();
try {
Thread.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
};

for(int i = 0; i < 10; i++) {
Thread thread = new Thread(runnable);
thread.start();
}
}
}

运行结果如下:

Thread-0-1000
Thread-3-1000
Thread-4-1000
Thread-1-1000
Thread-2-1000
Thread-5-1000
Thread-6-1000
Thread-7-1000
Thread-8-1000
Thread-9-1000

2、将线程类成员变量拿到run方法中,这时count引用是线程内的局部变量;

public class ThreadTest {
public static void main(String[] args) {
Runnable runnable = new Runnable() {
@Override
public void run() {
Count count = new Count();
for (int i = 0; i < 1000; i++) {
count.increment(1);
}
System.out.println(Thread.currentThread().getName() + "-" + count.getNum());
try {
Thread.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
};

for(int i = 0; i < 10; i++) {
Thread thread = new Thread(runnable);
thread.start();
}
}
}

运行结果如下:

Thread-1-1000
Thread-3-1000
Thread-2-1000
Thread-0-1000
Thread-5-1000
Thread-4-1000
Thread-6-1000
Thread-7-1000
Thread-8-1000
Thread-9-1000

3、每次启动一个线程使用不同的线程类,不推荐。

通过上述测试,我们发现,存在成员变量的类(即有状态的类)用于多线程时是不安全的,不安全体现在这个成员变量可能发生非原子性的操作,而变量定义在方法内也就是局部变量是线程安全的。

想想在使用struts1时,不推荐创建成员变量,因为action是单例的,如果创建了成员变量,就会存在线程不安全的隐患,而struts2是每一次请求都会创建一个action,就不用考虑线程安全的问题。所以,日常开发中,通常需要考虑成员变量或者说全局变量在多线程环境下,是否会引发一些问题。

要说明线程同步问题首先要说明Java线程的两个特性,可见性和有序性。

多个线程之间是不能直接传递数据进行交互的,它们之间的交互只能通过共享变量来实现。拿上面的例子来说明,在多个线程之间共享了Count类的一个实例,这个对象是被创建在主内存(堆内存)中,每个线程都有自己的工作内存(线程栈),工作内存存储了主内存count对象的一个副本,当线程操作count对象时,首先从主内存复制count对象到工作内存中,然后执行代码count.count(),改变了num值,最后用工作内存中的count刷新主内存的 count。当一个对象在多个工作内存中都存在副本时,如果一个工作内存刷新了主内存中的共享变量,其它线程也应该能够看到被修改后的值,此为可见性。

多个线程执行时,CPU对线程的调度是随机的,我们不知道当前程序被执行到哪步就切换到了下一个线程,一个最经典的例子就是银行汇款问题,一个银行账户存款100,这时一个人从该账户取10元,同时另一个人向该账户汇10元,那么余额应该还是100。那么此时可能发生这种情况,A线程负责取款,B线程负责汇款,A从主内存读到100,B从主内存读到100,A执行减10操作,并将数据刷新到主内存,这时主内存数据100-10=90,而B内存执行加10操作,并将数据刷新到主内存,最后主内存数据100+10=110,显然这是一个严重的问题,我们要保证A线程和B线程有序执行,先取款后汇款或者先汇款后取款,此为有序性。

在Web开发方面,Servlet是否是线程安全的呢?

Servlet不是线程安全的。要解释为什么Servlet为什么不是线程安全的,需要了解Servlet容器(如Tomcat)是如何响应HTTP请求的。当Tomcat接收到Client的HTTP请求时,Tomcat从线程池中取出一个线程,之后找到该请求对应的Servlet对象并进行初始化,之后调用service()方法。

要注意的是每一个Servlet对象在Tomcat容器中只有一个实例对象,即是单例模式。如果多个HTTP请求请求的是同一个Servlet,那么这两个HTTP请求对应的线程将并发调用Servlet的service()方法。如果的Thread1和Thread2调用了同一个Servlet1,Servlet1中定义了成员变量或静态变量,那么可能会发生线程安全问题(因为所有的线程都可能使用这些变量)。

像Servlet这样的类,在Web 容器中创建以后,会被传递给每个访问Web应用的用户线程执行,这个类就不是线程安全的。但这并不意味着一定会引发线程安全问题,如果Servlet类里没有成员变量,即使多线程同时执行这个Servlet实例的方法,也不会造成成员变量冲突。

这种对象被称作无状态对象,也就是说对象不记录状态,执行这个对象的任何方法都不会改变对象的状态,也就不会有线程安全问题了。事实上,Web开发实践中,常见的Service类、DAO类,都被设计成无状态对象,所以虽然我们开发的Web应用都是多线程的应用,因为Web容器一定会创建多线程来执行我们的代码,但是我们开发中却可以很少考虑线程安全的问题。

文章目录