變量的線程安全分析
成員變量和靜態變量是否線程安全?
如果它們沒有共享,則線程安全
如果它們被共享了,根據它們的狀態是否能夠改變,又分兩種情況
如果它們沒有共享,則線程安全
如果它們被共享了,根據它們的狀態是否能夠改變,又分兩種情況
如果只有讀操作,則線程安全
如果有讀寫操作,則這段代碼是臨界區,需要考慮線程安全
局部變量是否線程安全?
局部變量是線程安全的
但局部變量引用的對象則未必
如果該對象沒有逃離方法的作用訪問,它是線程安全的
如果該對象逃離方法的作用範圍,需要考慮線程安全
局部變量線程安全分析
局部變量如果引用的對象沒有逃離整個方法的作用範圍,哪它就是線程安全的,每個線程調用 test1() 方法時局部變量 i,會在每個線程的棧幀內存中被創建多份,因此不存在共享,因此不存在線程安全問題
public static void test1() {
int i = 10;
i++;
}
javac 後使用javap -v 查看字節碼
public static void test1();
descriptor: ()V
flags: (0x0009) ACC_PUBLIC, ACC_STATIC
Code:
stack=1, locals=1, args_size=0
0: bipush 10
2: istore_0
3: iinc 0, 1
6: return
LineNumberTable:
line 11: 0
line 12: 3
line 13: 6
LocalVariableTable:
Start Length Slot Name Signature
3 4 0 i I
由此我們可以看出這裏的i++,轉化成字節點是incr,他是原子的,不像靜態的變量中的i++操作
成員變量線程安全問題分析
成員變量在多線程環境下訪問是存在共享 ,因此會引起線程安全問題
public class ThreadSafeTest {
public static void main(String[] args){
UserService userService = new UserService();
for (int i = 0; i < 2; i++){
new Thread(()->{
userService.method(200);
}, "Thread" + (i+1)).start();
}
}
}
class UserService{
// 成員變量在多線程環境中線程不安全
List<String> list = new ArrayList<>();
private void addUser(){
list.add("test");
}
private void deleteUser(){
list.remove(0);
}
public void method(int times){
for (int i = 0; i < times; i++){
addUser();
deleteUser();
}
}
}
運行結果異常:
內存分析:
修改爲局部變量:
public class ThreadSafeTest {
public static void main(String[] args){
UserService userService = new UserService();
for (int i = 0; i < 2; i++){
new Thread(()->{
userService.method(200);
}, "Thread" + (i+1)).start();
}
}
}
class UserService{
private void addUser(List<String> list){
list.add("test");
}
private void deleteUser(List<String> list){
list.remove(0);
}
public void method(int times){
List<String> list = new ArrayList<>();
for (int i = 0; i < times; i++){
addUser(list);
deleteUser(list);
}
}
}
運行結果正常
內存分析:list是局部變量,每個線程調用時會生成不同的線程棧,每個線程棧創建不同的實例,沒有共享,因此不存在線程安全問題
局部變量-暴露引用:
情況一:如果將以上方法 addUser、deleteUser修改爲public意味着可以在外部調用,這時候如果是線程1調用addUser,線程2調用deleteUser,他們是傳的list參數是在不同的線程棧的,因此也是安全的
情況二:
public class ThreadSafeTest {
public static void main(String[] args){
UserService userService = new UserServiceSubClass();
for (int i = 0; i < 2; i++){
new Thread(()->{
userService.method(200);
}, "Thread" + (i+1)).start();
}
}
}
class UserService{
private void addUser(List<String> list){
list.add("test");
}
public void deleteUser(List<String> list){
list.remove(0);
}
public void method(int times){
List<String> list = new ArrayList<>();
for (int i = 0; i < times; i++){
addUser(list);
deleteUser(list);
}
}
}
class UserServiceSubClass extends UserService{
public void deleteUser(List<String> list){
new Thread(()->{
list.remove(0);
}).start();
}
}
因此,private在一定程序上保護方法的線程安全問題,限制子類重寫父類方法無效,如果徹底不想讓子類重寫還可以直接加一個final修飾
再論關鍵字private、 final 安全意義所在
不想被子類重寫的方法請記用final修飾
不想給外面訪問的方法請記用private修飾
常見線程安全類
- 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,線程2
if( table.get("key") == null) {
table.put("key", value);
}
分析:
這時想要線程安全,就要在這兩個操作之外使用synchronized包裹
Hashtable table = new Hashtable(); // 線程1,線程2
synchronized(table){
if( table.get("key") == null) {
table.put("key", value);
}
}
常見不可變類:
String、Integer 等都是不可變類,因爲其內部的成員變量不可以改變,因此它們的方法都是線程安全的,每次都是在重新操作後返回一個新的對象
實例分析
示例一
// Servlet只有一個實例,會被tomcat的多個線程訪問
public class UserServlet extends HttpServlet {
Map<String, Object> map = new HashMap<>(); // 不安全
String s1 = "...."; // 安全,String是不可變類
final String s2 = "...."; // 安全,String是不可變類
Date d1 = new Date(); // 不安全
final Date d2 = new Date(); // 不安全, final只是表示d2不能變,但是Date實例中的屬性是可以改變的,因此在多線程環境下不安全
public void doGet(HttpServletRequest req, HttpServletResponse resp) {
}
}
示例二:
// Servlet只有一個實例,會被tomcat的多個線程訪問
public class UserServlet extends HttpServlet {
private UserService userService = new UserServiceImpl(); // 不是線程安全的,servlet只有一份,因爲userService是成員變量所以也只有一份
@Override
public void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
userService.update();
}
}
interface UserService {
void update();
}
class UserServiceImpl implements UserService {
// 記錄調用數次
private int count = 0;
@Override
public void update() {
// ....
count ++;
}
}
示例三:
在Spring中默認每個對象都是單例的,意味着是被多個線程共享的,因此也裏面的成員變量也是補共享的,所在before、after方法對成員變量的修改會存在線程安全問題,在這裏可以使用around環繞通知,使用把開始時間,結束時間定義成局部變量。如果將LogAspect定義成多例bean,也是不行的,有可能進入before時是一個實例,進入after時是另一個實例,計算出的時間有問題。
示例四:
// Servlet只有一個實例,會被tomcat的多個線程訪問
public class UserServlet extends HttpServlet {
private UserService userService = new UserServiceImpl(); // 雖然userService是成員變量,但是由於userService的成員變量userDao,沒有地方可以修改它,所以也是線程安全的
@Override
public void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
userService.update();
}
}
interface UserService {
void update();
}
class UserServiceImpl implements UserService {
private UserDao userDao = new UserDaoImpl(); // 雖然userDao是成員變量,但是由於UserDao沒有可更改的成員變量(無狀態),所以也是線程安全的
@Override
public void update() {
}
}
interface UserDao{
void update();
}
// Dao沒有成員變量,它們是線程安全的
class UserDaoImpl implements UserDao{
@Override
public void update() {
String sql = "update user set password = ? where username = ?";
// Connection是線程安全的,因爲屬於局部變量,在多線程環境中,每個線程棧中會創建一份
try(Connection conn = DriverManager.getConnection("localhost", "admin", "123456")){
}catch (Exception e){
}
}
}
因此MVC模式中的這種貧血模型架構是線程安全的
示例五:
// Servlet只有一個實例,會被tomcat的多個線程訪問
public class UserServlet extends HttpServlet {
private UserService userService = new UserServiceImpl(); // 雖然userService是成員變量,但是由於userService的成員變量userDao,沒有地方可以修改它,所以也是線程安全的
@Override
public void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
userService.update();
}
}
interface UserService {
void update();
}
class UserServiceImpl implements UserService {
private UserDao userDao = new UserDaoImpl(); // 雖然userDao是成員變量,但是由於UserDao沒有可更改的成員變量(無狀態),所以也是線程安全的
@Override
public void update() {
}
}
interface UserDao{
void update() throws SQLException;
}
// useDao會被多個線程共享,它的成員變量conn也會被多個線程共享,因此會發生線程安全
class UserDaoImpl implements UserDao{
private Connection conn = null;
@Override
public void update() throws SQLException {
String sql = "update user set password = ? where username = ?";
// Connection是線程安全的,因爲屬於局部變量,在多線程環境中,每個線程棧中會創建一份
conn = DriverManager.getConnection("localhost", "admin", "123456");
conn.close();
}
}
示例六:
// Servlet只有一個實例,會被tomcat的多個線程訪問
public class UserServlet extends HttpServlet {
private UserService userService = new UserServiceImpl(); // 雖然userService是成員變量,但是由於userService的成員變量userDao,沒有地方可以修改它,所以也是線程安全的
@Override
public void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
userService.update();
}
}
interface UserService {
void update();
}
class UserServiceImpl implements UserService {
@Override
public void update() {
try {
// UserDao是局部變量,每個線程棧中存在一份userDao,哪它內部的conn成員變量也是各存在一份,所以不會有線程安全問題
UserDao userDao = new UserDaoImpl();
userDao.update();
} catch (SQLException e) {
e.printStackTrace();
}
}
}
interface UserDao{
void update() throws SQLException;
}
class UserDaoImpl implements UserDao{
private Connection conn = null;
@Override
public void update() throws SQLException {
String sql = "update user set password = ? where username = ?";
// Connection是線程安全的,因爲屬於局部變量,在多線程環境中,每個線程棧中會創建一份
conn = DriverManager.getConnection("localhost", "admin", "123456");
conn.close();
}
}
每一個請求,userService中的userDao會創建一份實例,它不存在線程安全問題,但是性能有一些隱患
示例七:
public class ThroughVariableTest{
public static void main(String[] args){
new DateFormatUtil().getDate();
}
}
abstract class BaseDateFormat {
public void getDate(){
SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MMM-dd HH:mm:ss");
parse(simpleDateFormat);
}
// 這時子類的形爲不確定,可能導致不安全的發生,被稱之爲外星方法
abstract void parse(SimpleDateFormat simpleDateFormat);
}
class DateFormatUtil extends BaseDateFormat{
@Override
void parse(SimpleDateFormat simpleDateFormat) {
String datastr = "1990-05-25 00:00:00";
for (int i = 0; i < 20; i++){
new Thread(()->{
try {
simpleDateFormat.parse(datastr);
} catch (ParseException e) {
e.printStackTrace();
}
}).start();
}
}
}
這時發生了多個線程(包括main線程)訪問同一個simpleDateFormat對象 simpleDateForma對象本身是線程不安全的,所以整個代碼造成線程不安全問題,