黑马程序员——【学习笔记】多线程——多线程的安全问题与解决


------- android培训 java培训 、期待与您交流!----------


多线程较好地实现了提升程序运行多部分代码同时运行的效率。但是需要注意,多线程在执行“任务”是存在着“安全问题”。


1 线程的运行状态

1.1 被创建:通过new Thread或其子类,线程被创建。

1.2 运行:线程start()后开始运行,执行run()里的任务。

1.3 消亡:即线程消失。线程被stop()处理或run()运行完毕后,线程会消亡。

1.4 冻结:可以理解为线程被暂停了。一般用sleep(时间)或wait()让线程暂停。此时线程放弃了运行资格,不允许系统去调用它。

1.5 阻塞:这是线程的一个临时状态,是一个特别的存在形态。意思是该线程虽然被语句赋予(或恢复)了运行的资格,但未有运行权,直到系统调用它。简单的说就是线程希望活动,只等被系统调用时的状态。实际上,线程被start()运行后并不是真的马上就被调用,而是去到阻塞状态,等待调用,系统调用到它,它才算真正地开始执行语句。多线程运作的时候,每个线程就是这样在阻塞状态与运行状态来回游走,但是系统具体每个时刻调用谁是无规律的。所以这个过程也可以理解为线程在抢系统资源,谁抢到了谁就执行自己的语句。

同样,当线程从冻结状态中恢复(即sleep时间结束了或wait后被notify叫起),也是先回到阻塞状态,再”抢资源“。


线程的各种存在状态如下简图:





2. 多线程的“安全问题”的要素

①多个线程在操作共享的数据

②操作共享数据的线程代码有多条

③当一个线程在执行操作共享数据的多条代码过程中,其他线程参与了运算。—— 一旦出现了这样的情况,就产生安全问题。


例子:3个线程同时数数,从100倒数到1。

class Demo implements Runnable {
	private int num = 100;
	public void run(){
		while (true){
			if (num>0){
				try {
					Thread.sleep(100); //设置sleep语句是为了故意创造多线程问题的条件。该语句产生InterruptedException异常
				} catch (InterruptedException e) {
					// TODO Auto-generated catch block
					e.printStackTrace();
<span style="white-space:pre">					</span>//throw e; //因为父类Runnable中该错误没有抛出,只能try catch,
				}
				System.out.println(Thread.currentThread().getName()+"...count..."+num--);
			}
		}
	}
}
class Day13{
	public static void main(String[] args){
		Demo d = new Demo(); 
		Thread t1 = new Thread(d);
		Thread t2 = new Thread(d);
		Thread t3 = new Thread(d);
		t1.start();
		t2.start();
		t3.start();
	}
}



结果输出了带0,-1的结果,明显是不符合程序的实现的。

结合程序分析该多线程出现安全问题的原因:

①多个线程在操作共享的数据:

因为程序只创建了1个Demo实例对象,所以实际上Demo里的int num成员是被3个线程共享的。3个线程执行run任务的时候指向的都是同一个int num。

②操作共享数据的线程代码有多条:

即if判断语句和后面的输出语句,都需要调用num。

③当一个线程在执行操作共享数据的多条代码过程中,其他线程参与了运算:

即对上图红框的解释:在本程序里,其中一个线程在if(num>0)里用num=1判断为真,进入,但在sleep(100)里进入了冻结状态,丢失了运行资格。

这时程序调用另一个线程在if(num>0)同样用num=1(因为此时Thread-2并没有执行到num--)判断为真,进入。同理,丢失资格。

第三个线程同理,在sleep(100)语句中冻结。

冻结结束后,Thread-1重获运行资格并率先被赋予运行权,执行接下来输出语句,得到Thread-1...count...1,同时num--,num=0

Thread-2随后被赋予运行权,输出语句,因为此时num已经是0,所以得到Thread-2...count...0,同时num--,num=-1

最后Thread-0运行,输出Thread-0...count...-1


简单的说,就是一个线程在执行多条多线程代码调用共享数据途中被其它线程“插队”,导致后续数据产生错乱。


3.安全问题的解决——同步锁

要解决该问题,可行的办法是将那些调用共享数据的代码封装起来,并规定在一个线程执行的时候,其它线程不能再进入该封装。如果该线程在执行过程中冻结了,其它线程只能在封装外等待。直到该线程执行完成。


3.1 java就给了一个专门处理这些问题的方法——同步代码块

synchronized(对象){访问共享数据的代码}

其中:该”对象“可以称为该同步的锁。是任意一个类的对象。


上个例子进行改良:

class Demo implements Runnable {
	Object obj = new Object();
	private int num = 100 ;
	public void run(){
		while (true){
			synchronized(obj){
			if (num>0){
				try {
					Thread.sleep(100);
				} catch (InterruptedException e) {
					// TODO Auto-generated catch block
					e.printStackTrace();
//					throw e;
				}
				System.out.println(Thread.currentThread().getName()+"...count..."+num--);
			}}
		}
	}
}
class Day13{
	public static void main(String[] args){
		Demo d = new Demo();
		Thread t1 = new Thread(d);
		Thread t2 = new Thread(d);
		Thread t3 = new Thread(d);
		t1.start();
		t2.start();
		t3.start();
	}
}

无论运行多少次,都不会再出现之前的安全问题。因为操作num的多个代码已经被同步了。


synchronized的运作原理是:给“任务”加上锁,所有的线程只有读取到synchronized拿到锁后才能执行”任务“,所有语句结束后,释放锁,允许其它线程进入。由于这个锁有一开一关的动作,可以粗略地理解为0和1,线程读取synchronized后将锁(即对象)置为1,并由synchronized规定得到1后不允许其它线程再获取锁。直到正在执行的线程执行完毕,将锁置为0。


3.2 同步锁运用对程序编写的影响


以下程序演示两个线程共同对100进行倒数到1。

class Demo extends Thread{
	Object obj = new Object();
	private static int i = 100;
	public  void run(){
		for(; i>0;i--){
			synchronized (obj){
			try {
				Thread.sleep(10);
			} catch (InterruptedException e) {
				// TODO Auto-generated catch block
				e.printStackTrace();
			}
			System.out.println(Thread.currentThread().getName()+"..."+i);
			}
		}
	}
}
public class Day13{
	public static void main(String[] args){
		Demo t1 = new Demo();
		Demo t2 = new Demo();
		t1.start();
		t2.start();
	}
}

输出结果:同步失败。生成了如Thread-1...0这样的输出。


原因分析:

①为了实现多线程,使用了Demo继承Thread的方法,这样导致一个隐患:我们每次创建线程都相当于创建一次Demo,注意此时Demo里的各种成员都被创建一次,除非静态。此时,虽然在synchronized上用了Object obj作为锁,但是创建了两个线程之后,每个线程实际上各自拿着一个自己的对象里的锁。这样相当于各自有一个房间,各自拿着各自的锁,互不想干。

②因为for语句上含有i>0的判断以及后来的i--操作,多条语句操作了int i这个共享数据,实际上for语句从一开始就应该被同同步,但这里没有。


解决方案:

①尝试把锁obj静态化,让各个线程们被“同一个锁”控制。

②把for语句整个同步。


得到如下:

class Demo extends Thread{
	<span style="color:#ff0000;">static </span>Object obj = new Object(); <span style="color:#ff0000;">//让每个线程的锁指向同一个对象,被“同一个锁”控制</span>
	private static int i = 100;
	public  void run(){
<pre name="code" class="java">	<span style="color:#ff0000;">synchronized (obj){ //因为for循环调用了i,把整个for循环同步</span>
for(; i>0;i--){ // synchronized (obj){try {Thread.sleep(10);
			} catch (InterruptedException e) {
				// TODO Auto-generated catch block
				e.printStackTrace();
			}
			System.out.println(Thread.currentThread().getName()+"..."+i);
			}
		}
	}

 

输出结果:不再出现记数为0的错误,但是发现整个倒数只被1个线程执行了。另一条线程虽然启动了,但并未执行到。

原因分析:

这里是同步出了问题:从读取synchronized后开始,线程一直在for循环里读取,全输出完了。这样就成了单线程。

解决方案:

也就是说要把处理的数据提取出来,但循环本身不要进入同步。这样才能允许各程序”抢资源“进入同步,参与循环,实现多线程。

程序修改如下。

class Demo extends Thread{
	static Object obj = new Object(); //让每个线程的锁指向同一个对象,被“同一个锁”控制
	private static int i = 100;
	public  void run(){
<span style="white-space:pre"></span><pre name="code" class="java"><span style="white-space:pre">	</span>for(; ;){
synchronized (obj){ //因为for循环调用了i,把整个for循环同步 if(i>0){ //把判断独立// synchronized (obj){try {Thread.sleep(10);
<pre name="code" class="java">			} catch (InterruptedException e) {
				// TODO Auto-generated catch block
				e.printStackTrace();
			}
			System.out.println(Thread.currentThread().getName()+"..."+i);
			}
<span style="white-space:pre">			</span><span style="color:#ff0000;">i--; //把递进独立</span>
		}
	}}
......

 
 


输出结果:把判断和递减都独立出来后,程序ok。实际上相当于用了while(ture)语句。这里得出while的好处:更方便于把循环和循环中用到的元素,各自处理。所以如果多线程中用到的循环了判断本身就操作到共享数据的话,不要用for。它太简洁,不好被synchronized操作。


总结:同步的两个前提:

①必须要有多个线程;

②需要同步处理的多个线程必须使用同一个锁。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值