Java_多线程(二十七)——多线程(线程安全、同步、Lock接口、死锁、等待唤醒机制)

目录

第一章 多线程

1.1 线程安全

1.2 线程同步(线程安全处理Synchronized)

1.2.1 同步代码块

1.2.2 同步方法(建议使用)

1.3 Lock接口

1.4 死锁

1.5  等待唤醒机制

第二章 总结


第一章 多线程

1.1 线程安全

如果有多个线程在同时运行,而这些线程可能会同时运行这段代码。程序每次运行结果和单线程运行的结果是一样的,而且其他的变量的值也和预期的是一样的,就是线程安全的。

 我们通过一个案例,演示线程的安全问题:

我们来模拟电影院的售票窗口,实现多个窗口同时卖 “功夫熊猫3”这场电影票(多个窗口一起卖这100张票)

需要窗口,采用线程对象来模拟;需要票,Runnable接口子类来模拟

测试类:

package day27.demo1;

/*
 * 多线程并发访问同一个数据资源
 * 3个线程,对一个票资源,出售
 */
public class ThreadDemo {
	public static void main(String[] args) {
		//创建Runnable接口实现类对象
		Tickets t = new Tickets();
		//创建3个Thread类对象,传递Runnable接口实现类
		Thread t0 = new Thread(t);
		Thread t1 = new Thread(t);
		Thread t2 = new Thread(t);
		
		t0.start();t1.start();t2.start();
		
	}
}

 模拟票:

package day27.demo1;

public class Tickets implements Runnable{
	
	//定义出售的票源
	private int ticket = 100;
	private Object obj = new Object();
	
	public void run(){
		while(true){
			//对票数判断,大于0,可以出售,变量--操作
				if( ticket > 0){
					System.out.println(Thread.currentThread().getName()+" 出售第 "+ticket--);
				}
			}
		}
	}

运行结果: 

 

run方法只有一个,但要进栈3次,使用的是同一个票源ticket。假如票剩1张是,t0先抢占了cpu,刚判断完ticket>0,但还未执行ticket--时,cpu被t1抢占,此时ticket依然大于0。此时这种情况会导致最后输出的ticket出现负数。这是线程不安全。

 若我们用休眠来演示cpu被抢占

只需要修改模拟票类:

package day27.demo1;

/*
 *  通过线程休眠,出现安全问题
 */
public class Tickets implements Runnable{
	
	//定义出售的票源
	private int ticket = 100;
	private Object obj = new Object();
	
	public void run(){
		while(true){
			//对票数判断,大于0,可以出售,变量--操作
				if( ticket > 0){
					try{
					   Thread.sleep(10);
					}catch(Exception ex){}
					System.out.println(Thread.currentThread().getName()+" 出售第 "+ticket--);
				}
			}
		}
	}

运行结果:

 

1.2 线程同步(线程安全处理Synchronized)

java中提供了线程同步机制,它能够解决上述的线程安全问题。

线程同步的方式有两种:

方式1:同步代码块

方式2:同步方法

 

1.2.1 同步代码块

同步代码块: 在代码块声明上,加上synchronized。

synchronized (锁对象) {

    可能会产生线程安全问题的代码

}

同步代码块中的锁对象可以是任意的对象;

此对象不能写匿名对象,因为每次while循环,匿名对象就变了,因此必须写有名对象。即多个线程时,要使用同一个锁对象才能够保证线程安全。

使用同步代码块,对电影院卖票案例中Ticket类进行如下代码修改:

package day27.demo1;

/*
 *  通过线程休眠,出现安全问题
 *  解决安全问题,Java程序,提供技术,同步技术
 *  公式:
 *    synchronized(任意对象){
 *      线程要操作的共享数据
 *    }
 *    同步代码块
 */
public class Tickets implements Runnable{
	
	//定义出售的票源
	private int ticket = 100;
	private Object obj = new Object();
	
	public void run(){
		while(true){
			//线程共享数据,保证安全,加入同步代码块
			synchronized(obj){
			//对票数判断,大于0,可以出售,变量--操作
				if( ticket > 0){
					try{
					   Thread.sleep(10);
					}catch(Exception ex){}
					System.out.println(Thread.currentThread().getName()+" 出售第 "+ticket--);
				}
			}
		}
	}
}

运行结果:

 

1. 线程遇到同步代码块后,线程判断还有没有同步锁。若有同步锁,则此线程获取锁,进入同步代码块中,去执行代码,执行完毕后,出同步代码块,并且将锁对象还回去。   若没有锁,则此线程被挡在同步代码块的外面。

2. 通过运行我们明显感觉到执行速度变慢。这是因为这是线程安全的,执行速度会变慢。

     因为有了同步之后,线程每次要判断同步锁,获取锁,使用完之后还有释放锁,导致程序运行速度降低。

 

1.2.2 同步方法(建议使用)

同步方法:在方法声明上加上synchronized

比同步代码块好,代码量少。

public synchronized void method(){

   可能会产生线程安全问题的代码

}

同步方法中,锁对象是 this。

	//同步方法中的对象锁,是本类对象引用 this
	public   void payTicket(){
		//静态方法中的成员也要为静态,因此ticket设为静态
		synchronized(this) {
		if( ticket > 0){
			try{
			   Thread.sleep(10);
			}catch(Exception ex){}
			System.out.println(Thread.currentThread().getName()+" 出售第 "+ticket--);
		}
		}
}

静态里面不能写this和super,因为静态是类的共享数据,不属于对象。而this属于对象引用,并且静态优先于非静态存在。

静态的同步方法中,锁对象是本类类名.class。

	//静态方法,同步锁,是本类类名.class属性	
	public  static void payTicket(){
		
		//静态方法中的成员也要为静态,因此ticket设为静态
		synchronized(Tickets1.class)//与反射有关 {
		if( ticket > 0){
			try{
			   Thread.sleep(10);
			}catch(Exception ex){}
			System.out.println(Thread.currentThread().getName()+" 出售第 "+ticket--);
		}
		}
}

使用同步方法,对电影院卖票案例中Ticket类进行如下代码修改:

package day27.demo1;

/*
 *  采用同步方法形式,解决线程的安全问题
 *  好处: 代码简洁
 *  将线程共享数据,和同步,抽取到一个方法中
 *  在方法的声明上,加入同步关键字
 *  
 *  问题:
 *    同步方法有锁吗,肯定有,同步方法中的对象锁,是本类对象引用 this
 *    如果方法是静态的呢,同步有锁吗,绝对不是this
 *    锁是本类自己.class 属性
 *    静态方法,同步锁,是本类类名.class属性
 */
public class Tickets1 implements Runnable{

	//定义出售的票源
	private  static int ticket = 100;
	
	public void run(){
		while(true){
			payTicket();
		}
	}
	
	public  synchronized void payTicket(){	
			if( ticket > 0){
				try{
				   Thread.sleep(10);
				}catch(Exception ex){}
				System.out.println(Thread.currentThread().getName()+" 出售第 "+ticket--);
			}
		
	}
	
//	//静态方法,同步锁,是本类类名.class属性	
//	public  static void payTicket(){
//		
//		//静态方法中的成员也要为静态,因此ticket设为静态
//		synchronized(Tickets1.class)//与反射有关 {
//		if( ticket > 0){
//			try{
//			   Thread.sleep(10);
//			}catch(Exception ex){}
//			System.out.println(Thread.currentThread().getName()+" 出售第 "+ticket--);
//		}
//		}
//}
	
//	//同步方法中的对象锁,是本类对象引用 this
//	public   void payTicket(){
//		//静态方法中的成员也要为静态,因此ticket设为静态
//		synchronized(this) {
//		if( ticket > 0){
//			try{
//			   Thread.sleep(10);
//			}catch(Exception ex){}
//			System.out.println(Thread.currentThread().getName()+" 出售第 "+ticket--);
//		}
//		}
//}
	
}

 

1.3 Lock接口

public  synchronized void payTicket(){	
			if( ticket > 0){
				try{
				   Thread.sleep(10);
				}catch(Exception ex){}
				System.out.println(Thread.currentThread().getName()+" 出售第 "+ticket--);
			}
		
	}

此代码中,有以下两个问题:

1. 何时获取锁我们知道,但是何时释放锁,我们并不知道。

2.若在sleep过程中,真的发生了异常,catch后此线程执行结束,但是锁没有释放,因为不出同步块,锁不释放。

因此出现了Lock来替换synchronized

Lock实现提供了比使用synchronized方法和语句可获得的更广泛的锁定操作。

Lock接口中的常用方法:

 

Lock提供了一个更加面对对象的锁,在该锁中提供了更多的操作锁的功能。

我们使用Lock接口,以及其中的lock()方法和unlock()方法替代同步,对电影院卖票案例中Ticket类进行如下代码修改:

package day27.demo1;

import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

/*
 *  使用JDK1.5 的接口Lock,替换同步代码块,实现线程的安全性
 *  Lock接口方法:
 *     lock() 获取锁
 *     unlock()释放锁
 *  实现类ReentrantLock
 */
public class Tickets_2 implements Runnable{
	
	//定义出售的票源
	private int ticket = 100;
	//在类的成员位置,创建Lock接口的实现类对象
	//写在run中不可(若有多个线程,则有多个锁,此应为共享资源),写在while循环中每次循环生成的锁不同。
	private Lock lock = new ReentrantLock();
	
	public void run(){
		
		while(true){
			//调用Lock接口方法lock获取锁
		    lock.lock();
			//对票数判断,大于0,可以出售,变量--操作
				if( ticket > 0){
					try{
					   Thread.sleep(10);
					   System.out.println(Thread.currentThread().getName()+" 出售第 "+ticket--);
					}catch(Exception ex){
						
					}finally{
						//释放锁,调用Lock接口方法unlock
						lock.unlock();
					}
				}
	
			
		}
	}
}

 

1.4 死锁

同步锁使用的弊端:当线程任务中出现了多个同步(多个锁)时,如果同步中嵌套了其他的同步。这时容易引发一种现象:程序出现无限等待,这种现象我们称为死锁。这种情况能避免就避免掉。

synchronzied(A锁){

    synchronized(B锁){        

    }

}

外部类要访问非静态成员需要对象调,但是此方法为私有的,外部类不能创建对象。因此外部类可以静态调,不需要对象。

1. 定义锁对象类

package day27.demo1;

public class LockA {
	private LockA(){}
	
	public  static final LockA locka = new LockA();
}
package day27.demo1;

public class LockB {
	private LockB(){}
	
	public static final LockB lockb = new LockB();
}

 2.线程任务类 

package day27.demo1;


public class DeadLock implements Runnable{
	private int i = 0;
	public void run(){
		while(true){
        //随着循环次数增多,发生死锁的几率就增加了
			if(i%2==0){
				//先进入A同步,再进入B同步
				synchronized(LockA.locka){
                //外部类不能创建对象,但是外部类可以静态调 LockA.locka
					System.out.println("if...locka");
					synchronized(LockB.lockb){
						System.out.println("if...lockb");
					}
				}
			}else{
				//先进入B同步,再进入A同步
				synchronized(LockB.lockb){
					System.out.println("else...lockb");
					synchronized(LockA.locka){
						System.out.println("else...locka");
					}
				}
			}
			i++;
		}
	}
}

3.测试类

package day27.demo1;
public class DeadLockDemo {
	public static void main(String[] args) {
		DeadLock dead = new DeadLock();
		Thread t0 = new Thread(dead);
		Thread t1 = new Thread(dead);
		t0.start();
		t1.start();
	}
}

运行结果: 

 

1.5  等待唤醒机制

线程之间的通信:多个线程在处理同一个资源,但是处理的动作(线程的任务)却不相同。

通过一定的手段使各个线程能有效的利用资源。而这种手段即—— 等待唤醒机制

等待唤醒机制所涉及到的方法:

  1. wait() :等待,将正在执行的线程释放其执行资格 和 执行权,并存储到线程池中。
  2. notify():唤醒,唤醒线程池中被wait()的线程,一次唤醒一个,而且是任意的。
  3. notifyAll(): 唤醒全部:可以将线程池中的所有wait() 线程都唤醒。

其实,所谓唤醒的意思就是让 线程池中的线程具备执行资格。必须注意的是,这些方法都是在 同步中才有效。同时这些方法在使用时必须标明所属锁,这样才可以明确出这些方法操作的到底是哪个锁上的线程。

仔细查看JavaAPI之后,发现这些方法 并不定义在 Thread中,也没定义在Runnable接口中,却被定义在了Object类中,为什么这些操作线程的方法定义在Object类中?

因为这些方法在使用时,必须要标明所属的锁,而锁又可以是任意对象。能被任意对象调用的方法一定定义在Object类中。

建在while循环内不对,没次循环,建立不同的Resource对象。 建在run内也不合适,若有三个线程,三个run,每个run都有自己的Resource,不合适。Resource应该为共享数据。

输出null..null的解决方法:

方法一:死锁中的静态调用

方法二:在测试类中创建一个对象,给input和output传参。 

但依然出现问题,lisi性别是nv,但打印出为男。 

性别与名字不匹配的问题,解决方法:写和读都要加锁,都为资源锁,不能为this锁。

需要锁的对象调用wait() 

输入线程任务类:

package day27.demo1;
/*
 *  输入的线程,对资源对象Resource中成员变量赋值
 *  一次赋值 张三,男
 *  下一次赋值 lisi,nv
 */
public class Input implements Runnable {
	private Resource r ;
	
	public Input(Resource r){
		this.r = r;
	}
	
	public void run() {
		int i = 0 ;
		while(true){
		  synchronized(r){
			  //标记是true,等待
			    if(r.flag){
			    	try{r.wait();}catch(Exception ex){}
			    }
			  
				if(i%2==0){
					r.name = "张三";
					r.sex = "男";
				}else{
					r.name = "lisi";
					r.sex = "nv";
				}
				//将对方线程唤醒,标记改为true
				r.flag = true;
				r.notify();
		  }
			i++;
		}
	}

}

输出线程任务类:

package day27.demo1;
/*
 *  输出线程,对资源对象Resource中成员变量,输出值
 */
public class Output implements Runnable {
	private Resource r ;
	
	public Output(Resource r){
		this.r = r;
	}
	public void run() {
		while(true){
		  synchronized(r){	
			  //判断标记,是false,等待
			if(!r.flag){
				try{r.wait();}catch(Exception ex){}
		    }
			System.out.println(r.name+".."+r.sex);
			//标记改成false,唤醒对方线程
			r.flag = false;
			r.notify();
		  }
		}
	}

}

模拟资源类: 

package day27.demo1;
/*
 *  定义资源类,有2个成员变量
 *  name,sex
 *  同时有2个线程,对资源中的变量操作
 *  1个对name,age赋值
 *  2个对name,age做变量的输出打印
 */
public class Resource {
	public String name;
	public String sex;
	public boolean flag = false;
}

测试类: 

package day27.demo1;
/*
 *  开启输入线程和输出线程,实现赋值和打印值
 */
public class ThreadDemo3{
	public static void main(String[] args) {
		
		Resource r = new Resource();
		
		Input in = new Input(r);
		Output out = new Output(r);
		
		Thread tin = new Thread(in);
		Thread tout = new Thread(out);
		
		tin.start();
		tout.start();
	}
}

运行结果:

 

第二章 总结

同步锁

多个线程想保证线程安全,必须要使用同一个锁对象

同步代码块

         synchronized (锁对象){

    可能产生线程安全问题的代码

}

同步代码块的锁对象可以是任意的对象

同步方法

         public synchronized void method()

              可能产生线程安全问题的代码

}

                           同步方法中的锁对象是 this

 

 

静态同步方法

public synchronized void method()

              可能产生线程安全问题的代码

}

静态同步方法中的锁对象是 类名.class

 

多线程有几种实现方案,分别是哪几种?

         a, 继承Thread类

         b, 实现Runnable接口

         c, 通过线程池,实现Callable接口

 

同步有几种方式,分别是什么?

         a,同步代码块

         b,同步方法

           静态同步方法

为什么wait(),notify(),notifyAll()等方法都定义在Object类中

         锁对象可以是任意类型的对象

 

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值