1.成員變量和靜態(tài)變量是否線程安全?
- 如果他們沒有共享,則線程安全
- 如果被共享:
- 只有讀操作,則線程安全
- 有寫操作,則這段代碼是臨界區(qū),需要考慮線程安全
2.局部變量是否線程安全
- 局部變量是線程安全的
- 當(dāng)局部變量引用的對(duì)象則未必
- 如果給i對(duì)象沒有逃離方法的作用訪問,則是線程安全的
- 如果該對(duì)象逃離方法的作用范圍,需要考慮線程安全
3.局部變量的線程安全分析
public static void test1() {
int i = 10;
i++;
}
每個(gè)線程調(diào)用該方法時(shí)局部變量i
,會(huì)在每個(gè)線程的棧幀內(nèi)存中被創(chuàng)建多分,因此不存在共享
當(dāng)局部變量的引用有所不同
先來看一個(gè)成員變量的里例子:
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++) {
// 臨界區(qū),會(huì)產(chǎn)生競(jìng)態(tài)條件
method2();
method3();
}
}
private void method2() {
list.add("1");
}
private void method3() {
list.remove(0);
}
}
可能會(huì)發(fā)生一種情況:線程1和線程2都去執(zhí)行method2,但是由于并發(fā)執(zhí)行導(dǎo)致最后只有一個(gè)元素添加成功,當(dāng)執(zhí)行了兩次移除操作,所以就會(huì)報(bào)錯(cuò)。
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)
進(jìn)程已結(jié)束,退出代碼0
分析:
- 無論哪個(gè)線程中的 method2 引用的都是同一個(gè)對(duì)象中的list成員變量
- method2 和 method3 分析相同
但如果將list修改為局部變量,就不會(huì)有上訴的問題了。
class Threadsafe {
public void method1(int loopNumber) {
for (int i = 0; i < loopNumber; i++) {
ArrayList<String> list = new ArrayList<>();
// 臨界區(qū),會(huì)產(chǎn)生競(jìng)態(tài)條件
method2(list);
method3(list);
}
}
private void method2(ArrayList<String> list) {
list.add("1");
}
private void method3(ArrayList<String> list) {
list.remove(0);
}
}
分析:
- list 是局部變量,每個(gè)線程調(diào)用時(shí)會(huì)創(chuàng)建其不同實(shí)例,沒有共享
- 而 method2 的參數(shù)是從 method1 中傳遞過來的,與 method1 中引用通過一個(gè)對(duì)象
- menthod3 的參數(shù)分析與 method2 相同
方法訪問修飾符帶來的思考,如果把 method2 和 method3 的方法修改為 public 會(huì)不會(huì)代理線程安全問題?
- 情況1:有其他線程調(diào)用 mthod2 和 method3
- 情況2:在情況1的基礎(chǔ)上,為 ThreadSafe 類添加子類,子類覆蓋為 method2 或 method3 方法
我們先來看情況1,這兩個(gè)方法的訪問修飾符修改為public,其他線程就可以調(diào)用了,但是它們不能調(diào)用 method1,所以 method1里的局部變量list是安全的,其他線程要調(diào)用 method2 的話只能使用自己創(chuàng)建新的list變量。
我們?cè)賮砜辞闆r2,訪問修飾符修改為 public ,也就意味著子類可以去覆蓋重寫 method2 和 method3 方法,即
class ThreadUnsafe {
public void method1(int loopNumber) {
for (int i = 0; i < loopNumber; i++) {
ArrayList<String> list = new ArrayList<>();
// 臨界區(qū),會(huì)產(chǎn)生競(jìng)態(tài)條件
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();
}
}
我們重寫方法中,開啟了一個(gè)新的線程,這個(gè)線程就能夠去操作method1方法中的局部變量 list,此時(shí) list就變成共享變量了,會(huì)有多個(gè)線程去修改它,也就產(chǎn)生了線程不安全的問題。也就是我們前面提到的局部變量的引用逃離了方法的作用范圍(有其他線程去使用)就可能會(huì)產(chǎn)生安全問題。
4.常見線程安全類
- String
- Integer
- StringBuffer
- Random
- Vector
- Hashtable
- java.util.concurrent 包下的類
這里說的線程安全是指,多個(gè)線程調(diào)用他們同一個(gè)實(shí)例的方法時(shí),時(shí)線程安全的,也可以理解為:
Hashtable table = new Hashtable();
new Thread(()->{
table.put("key", "value1");
}).start();
new Thread(()->{
table.put("key", "value2");
}).start();
他們的每個(gè)方法是原子的,但它們多個(gè)方法的組合不是原子的,比如:
Hashtable table = new Hashtable();
// 線程1
if( table.get("key") == null) {
table.put("key", "t1");
}
// 線程2
if( table.get("key") == null) {
table.put("key", "t2");
}
這里也就是檢查和上鎖不同步導(dǎo)致的線程不安全。
不可變線程安全性
String、Integer 等都是不可變類,因?yàn)槠鋬?nèi)部的狀態(tài)不可以改變,因?yàn)樗麄兊姆椒ǘ际蔷€程安全的。
有同學(xué)或許有疑問,String 有 replace,substring 等方法【可以】改變值啊,那么這些方法又是如何保證線程安 全的呢?
5.深入刨析String類為什么不可變?
什么是不可變?
String s = "aaa";
s = "bbb";
我們現(xiàn)在有一個(gè)字符串 s = "aaa"
,如果我把它第二次賦值 s = "bbb"
,這個(gè)操作并不會(huì)在原內(nèi)存地址上修改數(shù)據(jù),也就是不會(huì)吧 “aaa” 的那塊地址里的數(shù)據(jù)修改為"bbb",而是重新指向了一個(gè)新的 內(nèi)存地址,即”bbb"的內(nèi)存地址,所以說 String 類是不可變的,一旦創(chuàng)建不可被修改的。
String 類里的replace方法
我們可以看到就是創(chuàng)建了一個(gè)新的String對(duì)象。
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;
}
不可變的本質(zhì)
我們看String類的源碼就可以發(fā)現(xiàn),
- String 類是一個(gè) final 類
String類由final修飾,我們都知道當(dāng)final修飾一個(gè)類時(shí),該類不可以被其他類繼承,自然String類就沒有子類,也更沒有方法被子類重寫的說法了,所以這就保證了外界無法通過繼承String類,來實(shí)現(xiàn)對(duì)String不可變性的破壞。
- String底層是通過一個(gè)char[]來存儲(chǔ)數(shù)據(jù)的,且該char[]由private final修飾。
該value數(shù)組被final修飾,我們知道被final修飾的引用類型的變量就不能再指向其他對(duì)象了,也就是說value數(shù)組只能指向堆中屬于自己的那一個(gè)數(shù)組,不可以再指向其他數(shù)組了。但是我們可以改變它指向的這個(gè)數(shù)組里面的內(nèi)容啊,比如咱們隨便舉個(gè)例子:
public class StringDemo {
public static void main(String[] args) {
final char[] c = {'a', 'b', 'c'};
c[0] = 'd';
System.out.println(Arrays.toString(c));
}
}
其實(shí)不然,我們雖然可以修改一個(gè)對(duì)象的內(nèi)容,但是我們根本無法修改String類里的數(shù)據(jù),因?yàn)?String 類里的 value 數(shù)組是私有的,也沒有對(duì)外修改的public方法,所以根本就沒有可以修改的機(jī)會(huì)。
保證String類不可變靠的就是以下三點(diǎn):
-
String 類被 final 修飾導(dǎo)致其不能被繼承,進(jìn)而避免了子類破壞 String 不可變性。
-
保存字符串的value數(shù)組被 final 修飾且為私有的。
-
String 類里沒有提供或暴露修改這個(gè)value數(shù)組的方法。
6.實(shí)例分析
我們來看幾個(gè)例子,檢驗(yàn)一下我們學(xué)的怎么樣吧
線程安不安全,看這幾個(gè)方便:
- 是否是共享變量
- 是否存在多個(gè)線程并發(fā)
- 是否有寫操作
**前置知識(shí):**tomcat中一個(gè)servet類只會(huì)有一個(gè)實(shí)例,所以多個(gè)請(qǐng)求用的都是同一個(gè)servet對(duì)象
例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 :可以修改其對(duì)象的引用地址,線程不安全
- 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 {
// 記錄調(diào)用次數(shù)
private int count = 0;
public void update() {
// ...
count++;
}
}
- userService:成員變量,不安全,有多個(gè)線程會(huì)修改它的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沒有指定是單例對(duì)象還是多例對(duì)象,Spring默認(rèn)是單例。所以多個(gè)線程都共享一個(gè)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
它調(diào)用的方法是線程安全的,所以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是成員變量,多個(gè)線程用的是同一個(gè)conn,所以是線程不安全的,同時(shí) 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 雖然是局部變量但是不被多個(gè)線程之間共享,所以conn是線程安全的,所以u(píng)serDao也是線程安全的,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 方法是抽象方法,所以它的行為是不確定的,可能導(dǎo)致不安全的方法,被稱之為外星方法
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文章來源:http://www.zghlxwxcb.cn/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
是靜態(tài)變量,但是又synchronized
給修改i的代碼塊上了鎖,所以是線程安全的。文章來源地址http://www.zghlxwxcb.cn/news/detail-476246.html
到了這里,關(guān)于【Java并發(fā)編程】變量的線程安全分析的文章就介紹完了。如果您還想了解更多內(nèi)容,請(qǐng)?jiān)谟疑辖撬阉鱐OY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!