JAVA多线程核心技术 1.2.3 非线程安全 解析

   在看《JAVA多线程核心技术》 1.2.3线程安全 发现了一个有趣的例子,在这里和大家分享一下,同时对运行结果进行解析和扩展,帮助新手朋友理解。同时请大神朋友不吝赐教。

   话不多说,先来撸两行代码

  

一、非线程安全


    首先一个LoginServlet类,此类用于模拟用户登录。注意此类中的 变量和方法全部使用static关键字,代表变量和方法全都是静态的,java只会分配一个资源,多实例需要共享资源,这就是祸根所在,正式如此,引发了 非线程安全。

 

package chapter1.section2;

public class LoginServlet {

	private static String usernameRef;
	private static String passwordRef;

	public static void doPost(String username, String password) {
		try {
			usernameRef = username;
			if ("a".equals(username)) {
				Thread.sleep(1000);
			}
			passwordRef = password;
			System.out.println("username=" + usernameRef + " password=" + password);
		} catch (InterruptedException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
	}

}

接着我们定义两个类,用于模拟用户登录

package chapter1.section2;

public class ALogin extends Thread{
	@Override
	public void run(){
		LoginServlet.doPost("a", "aa");
	}
}
package chapter1.section2;

public class BLogin extends Thread{
	@Override
	public void run(){
		LoginServlet.doPost("b", "bb");
	}
}

最后定义一个测试类


package chapter1.section2;

public class Run {
	public static void main(String[] args) {
		ALogin a = new ALogin();
		a.start();
		BLogin b = new BLogin();
		b.start();
	}
}


显然我们希望当 a 用户登录时,输出用户信息 username=a password=aa ,b用户登录时,输出用户信息 username=b password=bb

但实际结果时什么样呢?


username=b password=bb
username=b password=aa

下面我们来分析一下,到底发生了什么事情。这里注意线程a和b操作的 usernameRef、passwordRef为同一资源。

a线程 b线程 usernameRef passwordRef
a获取资源usernameRef
usernameRef = “a”
  a  
a休眠1秒   a  
  b获取资源usernameRef
usernameRef = “b”
b  
  b获取资源passwordRef
passwordRef = “bb”
b bb
  输出 username=b password=bb b bb
a醒来,a获取资源passwordRef
passwordRef = “aa”
  b aa
输出 username=b password=aa   b aa


这里需要注意的是,线程并不是顺序执行的,而是随机的,之所以上面的例子每次运行结果一致,主要原因在于

if ("a".equals(username)) {
   Thread.sleep(1000);
}
a线程进入休眠,让出了资源。系统将资源分配给了b线程。下面我们将a的休眠去掉,来看一下线程的随机性。

二、随机性

  修改LoginServlet去掉a线程的休眠

package chapter1.section2;

public class LoginServlet {

	private static String usernameRef;
	private static String passwordRef;

	public static void doPost(String username, String password) {
			usernameRef = username;
			passwordRef = password;
			System.out.println("username=" + usernameRef + " password=" + password);
	}

}


这里为了方便测试我们修改一下Run类

package chapter1.section2;

public class Run {
	public static void main(String[] args) throws InterruptedException {
		for (int i = 0; i < 20; i++) {
			run();
			System.out.println();
			Thread.sleep(10000);
		}
	}

	public static void run() {
		ALogin a = new ALogin();
		a.start();
		BLogin b = new BLogin();
		b.start();
	}
}


输出结果包含几种

username=b password=aa
username=b password=bb
a线程 b线程 usernameRef passwordRef
a获取资源usernameRef
usernameRef = “a”
  a  
  b获取资源usernameRef
usernameRef = “b”
b  
a获取资源passwordRef
passwordRef = “aa”
  b aa
输出 username=b password=aa   b aa
  b获取资源passwordRef
passwordRef = “bb”
b bb
  输出 username=b password=bb b bb


username=a password=aa
username=b password=bb

a线程 b线程 usernameRef passwordRef
a获取资源usernameRef
usernameRef = “a”
  a  
a获取资源passwordRef
passwordRef = “aa”
  a aa
输出 username=a password=aa   a aa
  b获取资源usernameRef
usernameRef = “b”
b aa
  b获取资源passwordRef
passwordRef = “bb”
b bb
  输出 username=b password=bb b bb

当然还有其他可能,这里不再赘述。

观察输出结果,你可以发现,它并不是想我们期待的那样,两两一对的出现,而是无序的不规律的。这恰恰说明了多线程的随机性。

username=b password=aa
username=b password=bb

username=a password=aa
username=b password=bb

username=a password=aa
username=b password=bb

username=a password=aa
username=b password=bb

username=a password=aa
username=b password=bb

username=a password=aa
username=b password=bb
username=a password=aa
username=b password=bb

username=a password=aa

username=b password=bb
username=a password=aa
username=b password=bb

username=a password=aa
username=b password=bb

username=a password=aa
username=b password=bb


username=a password=aa
username=b password=bb
username=a password=aa
username=b password=bb

username=a password=aa

username=b password=bb

username=b password=bb
username=a password=aa
username=a password=aa
username=b password=bb

username=a password=aa
username=b password=bb

username=a password=aa
username=b password=bb

username=a password=aa

username=b password=bb
username=a password=aa
username=b password=bb



三、线程安全

解决以上问题有三种方案:

1.添加 synchronized 

修改LoginServlet 为doPost 方法添加 synchronized 。这样当一个线程正在执行此方法时,另一个线程不能进入。

package chapter1.section2;

public class LoginServlet {
	
	private static String usernameRef;
	private static String passwordRef;
	
	synchronized public static void doPost(String username,String password){
		try {
			usernameRef = username;
			if("a".equals(username)){
				Thread.sleep(5000);
			}
			passwordRef = password;
			System.out.println("username="+usernameRef+" password="+password);
		} catch (InterruptedException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
	}
	
}

a线程 b线程 usernameRef passwordRef
a获取资源usernameRef
usernameRef = “a”
  a  
a休眠1秒 b请求执行doPost方法时,
因为a还没有执行完毕,所以b无法进入
a  
a获取资源passwordRef
passwordRef = “aa”
  a aa
输出 username=a password=aa   a aa
a执行结束 b获取资源usernameRef
usernameRef = “b”
b aa
  b获取资源passwordRef
passwordRef = “bb”
b bb
  输出 username=b password=bb b bb

2.修改变量和方法,去除static关键字。使用对象调用方法

package chapter1.section2;

public class LoginServlet {
	
	private  String usernameRef;
	private  String passwordRef;
	
	 public  void doPost(String username,String password){
		try {
			usernameRef = username;
			if("a".equals(username)){
				Thread.sleep(5000);
			}
			passwordRef = password;
			System.out.println("username="+usernameRef+" password="+password);
		} catch (InterruptedException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
	}
	
}
package chapter1.section2;

public class ALogin extends Thread{
	@Override
	public void run(){
		new LoginServlet().doPost("a", "aa");
	}
}
package chapter1.section2;

public class BLogin extends Thread{
	@Override
	public void run(){
		new LoginServlet().doPost("b", "bb");
	}
}

此时a、b线程请求的资源完全独立,不存在线程交互。


3.同步执行线程

这里只需要将线程启动的start方法换成run方法即可

package chapter1.section2;

public class Run {
	public static void main(String[] args) {
		ALogin a = new ALogin();
		a.run();
		BLogin b = new BLogin();
		b.run();
	}
}

比较一下start和run方法的区别,就明白了其中的道理。

使用run方法,线程改为同步执行,而不是异步执行。此时线程对象是由main主线程来控制,而不是交给“线程规划器”来管理,失去了线程的意义。



评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值