1.成员变量和静态变量是否线程安全?
- 如果他们没有共享,则线程安全
- 如果被共享:
- 只有读操作,则线程安全
- 有写操作,则这段代码是临界区,需要考虑线程安全
2.局部变量是否线程安全
- 局部变量是线程安全的
- 当局部变量引用的对象则未必
- 如果给i对象没有逃离方法的作用访问,则是线程安全的
- 如果该对象逃离方法的作用范围,需要考虑线程安全
3.局部变量的线程安全分析
public static void test1() {
int i = 10;
i++;
}
每个线程调用该方法时局部变量i
,会在每个线程的栈帧内存中被创建多分,因此不存在共享
当局部变量的引用有所不同
先来看一个成员变量的里例子:
public class ThreadUnsafeDemo {
static final int THREAD_NUMBER = 2;
static final int LOOP_NUMBER = 200;
public static void main(String[] args) {
ThreadUnsafe test = new ThreadUnsafe();
for (int i = 0; i < THREAD_NUMBER; i++) {
new Thread(() -> {
test.method1(LOOP_NUMBER);
}, "Thread" + i).start();
}
}
}
class ThreadUnsafe {
ArrayList<String> list = new ArrayList<>();
public void method1(int loopNumber) {
for (int i = 0; i < loopNumber; i++) {
// 临界区,会产生竞态条件
method2();
method3();
}
}
private void method2() {
list.add("1");
}
private void method3() {
list.remove(0);
}
}
可能会发生一种情况:线程1和线程2都去执行method2,但是由于并发执行导致最后只有一个元素添加成功,当执行了两次移除操作,所以就会报错。
Exception in thread "Thread1" java.lang.IndexOutOfBoundsException: Index: 0, Size: 0
at java.util.ArrayList.rangeCheck(ArrayList.java:659)
at java.util.ArrayList.remove(ArrayList.java:498)
at org.example.juc.ThreadUnsafe.method3(ThreadUnsafeDemo.java:39)
at org.example.juc.ThreadUnsafe.method1(ThreadUnsafeDemo.java:30)
at org.example.juc.ThreadUnsafeDemo.lambda$main$0(ThreadUnsafeDemo.java:17)
at java.lang.Thread.run(Thread.java:750)
进程已结束,退出代码0
分析:
- 无论哪个线程中的 method2 引用的都是同一个对象中的list成员变量
- method2 和 method3 分析相同
但如果将list修改为局部变量,就不会有上诉的问题了。
class Threadsafe {
public void method1(int loopNumber) {
for (int i = 0; i < loopNumber; i++) {
ArrayList<String> list = new ArrayList<>();
// 临界区,会产生竞态条件
method2(list);
method3(list);
}
}
private void method2(ArrayList<String> list) {
list.add("1");
}
private void method3(ArrayList<String> list) {
list.remove(0);
}
}
分析:
- list 是局部变量,每个线程调用时会创建其不同实例,没有共享
- 而 method2 的参数是从 method1 中传递过来的,与 method1 中引用通过一个对象
- menthod3 的参数分析与 method2 相同
方法访问修饰符带来的思考,如果把 method2 和 method3 的方法修改为 public 会不会代理线程安全问题?
- 情况1:有其他线程调用 mthod2 和 method3
- 情况2:在情况1的基础上,为 ThreadSafe 类添加子类,子类覆盖为 method2 或 method3 方法
我们先来看情况1,这两个方法的访问修饰符修改为public,其他线程就可以调用了,但是它们不能调用 method1,所以 method1里的局部变量list是安全的,其他线程要调用 method2 的话只能使用自己创建新的list变量。
我们再来看情况2,访问修饰符修改为 public ,也就意味着子类可以去覆盖重写 method2 和 method3 方法,即
class ThreadUnsafe {
public void method1(int loopNumber) {
for (int i = 0; i < loopNumber; i++) {
ArrayList<String> list = new ArrayList<>();
// 临界区,会产生竞态条件
method2(list);
method3(list);
}
}
public void method2(ArrayList<String> list) {
list.add("1");
}
public void method3(ArrayList<String> list) {
list.remove(0);
}
}
class ThreadSafeSubClass extends ThreadUnsafe {
@Override
public void method3(ArrayList<String> list) {
new Thread(() -> {
list.remove(0);
}).start();
}
}
我们重写方法中,开启了一个新的线程,这个线程就能够去操作method1方法中的局部变量 list,此时 list就变成共享变量了,会有多个线程去修改它,也就产生了线程不安全的问题。也就是我们前面提到的局部变量的引用逃离了方法的作用范围(有其他线程去使用)就可能会产生安全问题。
4.常见线程安全类
- String
- Integer
- StringBuffer
- Random
- Vector
- Hashtable
- java.util.concurrent 包下的类
这里说的线程安全是指,多个线程调用他们同一个实例的方法时,时线程安全的,也可以理解为:
Hashtable table = new Hashtable();
new Thread(()->{
table.put("key", "value1");
}).start();
new Thread(()->{
table.put("key", "value2");
}).start();
他们的每个方法是原子的,但它们多个方法的组合不是原子的,比如:
Hashtable table = new Hashtable();
// 线程1
if( table.get("key") == null) {
table.put("key", "t1");
}
// 线程2
if( table.get("key") == null) {
table.put("key", "t2");
}
这里也就是检查和上锁不同步导致的线程不安全。
不可变线程安全性
String、Integer 等都是不可变类,因为其内部的状态不可以改变,因为他们的方法都是线程安全的。
有同学或许有疑问,String 有 replace,substring 等方法【可以】改变值啊,那么这些方法又是如何保证线程安 全的呢?
5.深入刨析String类为什么不可变?
什么是不可变?
String s = "aaa";
s = "bbb";
我们现在有一个字符串 s = "aaa"
,如果我把它第二次赋值 s = "bbb"
,这个操作并不会在原内存地址上修改数据,也就是不会吧 “aaa” 的那块地址里的数据修改为"bbb",而是重新指向了一个新的 内存地址,即”bbb"的内存地址,所以说 String 类是不可变的,一旦创建不可被修改的。
String 类里的replace方法
我们可以看到就是创建了一个新的String对象。
public String replace(char oldChar, char newChar) {
if (oldChar != newChar) {
int len = value.length;
int i = -1;
char[] val = value; /* avoid getfield opcode */
while (++i < len) {
if (val[i] == oldChar) {
break;
}
}
if (i < len) {
char buf[] = new char[len];
for (int j = 0; j < i; j++) {
buf[j] = val[j];
}
while (i < len) {
char c = val[i];
buf[i] = (c == oldChar) ? newChar : c;
i++;
}
return new String(buf, true);
}
}
return this;
}
不可变的本质
我们看String类的源码就可以发现,
- String 类是一个 final 类
String类由final修饰,我们都知道当final修饰一个类时,该类不可以被其他类继承,自然String类就没有子类,也更没有方法被子类重写的说法了,所以这就保证了外界无法通过继承String类,来实现对String不可变性的破坏。
- String底层是通过一个char[]来存储数据的,且该char[]由private final修饰。
该value数组被final修饰,我们知道被final修饰的引用类型的变量就不能再指向其他对象了,也就是说value数组只能指向堆中属于自己的那一个数组,不可以再指向其他数组了。但是我们可以改变它指向的这个数组里面的内容啊,比如咱们随便举个例子:
public class StringDemo {
public static void main(String[] args) {
final char[] c = {'a', 'b', 'c'};
c[0] = 'd';
System.out.println(Arrays.toString(c));
}
}
其实不然,我们虽然可以修改一个对象的内容,但是我们根本无法修改String类里的数据,因为 String 类里的 value 数组是私有的,也没有对外修改的public方法,所以根本就没有可以修改的机会。
保证String类不可变靠的就是以下三点:
-
String 类被 final 修饰导致其不能被继承,进而避免了子类破坏 String 不可变性。
-
保存字符串的value数组被 final 修饰且为私有的。
-
String 类里没有提供或暴露修改这个value数组的方法。
6.实例分析
我们来看几个例子,检验一下我们学的怎么样吧
线程安不安全,看这几个方便:
- 是否是共享变量
- 是否存在多个线程并发
- 是否有写操作
**前置知识:**tomcat中一个servet类只会有一个实例,所以多个请求用的都是同一个servet对象
例1
public class MyServlet extends HttpServlet {
// 是否安全?
Map<String,Object> map = new HashMap<>();
// 是否安全?
String S1 = "...";
// 是否安全?
final String S2 = "...";
// 是否安全?
Date D1 = new Date();
// 是否安全?
final Date D2 = new Date();
public void doGet(HttpServletRequest request, HttpServletResponse response) {
// 使用上述变量
}
}
他们都是成员变量
- map:HashMap是线程不安全的类,所以不安全
- S1 :可以修改其对象的引用地址,线程不安全
- S2 :被final修饰,所以不能修改它的引用地址,也不可能修改它的值
- D1 :Date()是线程不安全的类
- D2:虽然被final修饰,但可以修改它里面的值
例2
public class MyServlet extends HttpServlet {
// 是否安全?
private UserService userService = new UserServiceImpl();
public void doGet(HttpServletRequest request, HttpServletResponse response) {
userService.update(...);
}
}
public class UserServiceImpl implements UserService {
// 记录调用次数
private int count = 0;
public void update() {
// ...
count++;
}
}
- userService:成员变量,不安全,有多个线程会修改它的count变量
例三
@Aspect
@Component
public class MyAspect {
// 是否安全?
private long start = 0L;
@Before("execution(* *(..))")
public void before() {
start = System.nanoTime();
}
@After("execution(* *(..))")
public void after() {
long end = System.nanoTime();
System.out.println("cost time:" + (end-start));
}
}
MyAspect没有指定是单例对象还是多例对象,Spring默认是单例。所以多个线程都共享一个MyAspect
- start:成员变量,线程不安全
例四
public class MyServlet extends HttpServlet {
// 是否安全
private UserService userService = new UserServiceImpl();
public void doGet(HttpServletRequest request, HttpServletResponse response) {
userService.update(...);
}
}
public class UserServiceImpl implements UserService {
// 是否安全
private UserDao userDao = new UserDaoImpl();
public void update() {
userDao.update();
}
}
public class UserDaoImpl implements UserDao {
public void update() {
String sql = "update user set password = ? where username = ?";
// 是否安全
try (Connection conn = DriverManager.getConnection("","","")){
// ...
} catch (Exception e) {
// ...
}
}
}
UserDaoImpl
中的update
方法中的 conn 是局部变量,并且没有逃离方法的作用范围,所以 conn是线程安全的,UserServiceImpl 中的 UserDao是成员变量,但是userDao
它调用的方法是线程安全的,所以userDao
也是线程安全的,同理,userService
也是线程安全的。
例5
public class MyServlet extends HttpServlet {
// 是否安全
private UserService userService = new UserServiceImpl();
public void doGet(HttpServletRequest request, HttpServletResponse response) {
userService.update(...);
}
}
public class UserServiceImpl implements UserService {
// 是否安全
private UserDao userDao = new UserDaoImpl();
public void update() {
userDao.update();
}
}
public class UserDaoImpl implements UserDao {
public void update() {
// 是否安全
private Connection conn = null;
public void update() throws SQLException {
String sql = "update user set password = ? where username = ?";
conn = DriverManager.getConnection("","","");
// ...
conn.close();
}
}
conn是成员变量,多个线程用的是同一个conn,所以是线程不安全的,同时 userDao 也是线程不安全的,userService也是线程不安全的。
例6
public class MyServlet extends HttpServlet {
// 是否安全
private UserService userService = new UserServiceImpl();
public void doGet(HttpServletRequest request, HttpServletResponse response) {
userService.update(...);
}
}
public class UserServiceImpl implements UserService {
public void update() {
UserDao userDao = new UserDaoImpl();
userDao.update();
}
}
public class UserDaoImpl implements UserDao {
// 是否安全
private Connection = null;
public void update() throws SQLException {
String sql = "update user set password = ? where username = ?";
conn = DriverManager.getConnection("","","");
// ...
conn.close();
}
}
UserServiceImpl
中不在用的是成员变量而是局部变量,所以 conn 虽然是局部变量但是不被多个线程之间共享,所以conn是线程安全的,所以userDao也是线程安全的,userService也是线程安全的。
例7
public abstract class Test {
public void bar() {
// 是否安全
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
foo(sdf);
}
public abstract foo(SimpleDateFormat sdf);
public static void main(String[] args) {
new Test().bar();
}
}
foo 方法是抽象方法,所以它的行为是不确定的,可能导致不安全的方法,被称之为外星方法
public void foo(SimpleDateFormat sdf) {
String dateStr = "1999-10-11 00:00:00";
for (int i = 0; i < 20; i++) {
new Thread(() -> {
try {
sdf.parse(dateStr);
} catch (ParseException e) {
e.printStackTrace();
}
}).start();
}
}
例8文章来源:https://www.toymoban.com/news/detail-476246.html
private static Integer i = 0;
public static void main(String[] args) throws InterruptedException {
List<Thread> list = new ArrayList<>();
for (int j = 0; j < 2; j++) {
Thread thread = new Thread(() -> {
for (int k = 0; k < 5000; k++) {
synchronized (i) {
i++;
}
}
}, "" + j);
list.add(thread);
}
list.stream().forEach(t -> t.start());
list.stream().forEach(t -> {
try {
t.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
});
log.debug("{}", i);
}
这里虽然i
是静态变量,但是又synchronized
给修改i的代码块上了锁,所以是线程安全的。文章来源地址https://www.toymoban.com/news/detail-476246.html
到了这里,关于【Java并发编程】变量的线程安全分析的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!