Java线程基础part3

线程安全

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

以电影院售票为例:
一个电影院在网页,APP和线下同时售票,一共有100张票。三个售票方式可以看作三个线程。

/*
 * 多线程并发访问同一个数据资源
 * 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();
	}
}

public class Ticket implements Runnable {
	//共100票
	int ticket = 100;

	@Override
	public void run() {
		//模拟卖票
		while(true){
			if (ticket > 0) {
				//模拟选坐的操作
				try {
					Thread.sleep(1);
				} catch (InterruptedException e) {
					e.printStackTrace();
				}
				System.out.println(Thread.currentThread().getName() + "正在卖票:" + ticket--);
			}
		}
	}

运行几次可以发现这种情况:
在这里插入图片描述
不同窗口卖出了同一张票,而且出现了错误的票0和-1。
这是因为某一线程在操作变量ticket时,进入了循环,但还没有完成打印操作该线程就被终止,下一线程开始执行,回到该线程时继续执行就会出现错误。

为了避免共享数据不同步问题,Java提供了线程同步机制(Synchronized)。
线程同步有两种方式:

  • 同步代码块
  • 同步方法

同步代码块

将可能产生同步问题的代码放入同步代码块中。

//	线程共享数据,保证安全,加入同步代码块
			synchronized(obj) {
			//	对票数判断,大于0,可以出售,--操作
				if(ticket > 0) {
					try {
						Thread.sleep(10);					
					}catch(Exception ex) {}
					System.out.println(Thread.currentThread().getName() + " 出售第" + ticket--);
				}
			}

obj可以是任意对象,作为对象锁。线程进入同步代码块时将对象锁obj拿走,直到同步代码块执行完毕后将锁归还。其他线程只有在线程锁还在时才能进入同步代码块,这样就避免了发生不同步问题。

同步方法

/*
 *  采用同步方法形式,解决线程的安全问题
 *  好处:代码简洁
 *  将线程共享数据和同步抽取到方法中
 *  在方法的声明上加入同步关键字
 *  
 *  问题:
 *  	同步方法有锁吗?
 *  		有,同步方法中的对象锁,是本类对象引用this
 *  	如果方法是静态的,同步有锁吗,是this吗?
 *  	有锁,但绝不能是this,锁是本类自己.class。
 */
public class Tickets implements Runnable {
	// 定义可以出售的票源
	private int ticket = 100;

	public void run() {
		while (true) {
			payTicket();
		}
	}

	public synchronized void payTicket() {
		// 对票数判断,大于0,可以出售,--操作
		if (ticket > 0) {
			try {
				Thread.sleep(10);
			} catch (Exception ex) {
			}
			System.out.println(Thread.currentThread().getName() + " 出售第" + ticket--);
		}
	}
}

代码简洁,同步方法的对象锁是本类对象自己this,如果方法是静态的,那么锁是本类.class

Lock()接口

使用关键字synchronized看不到对象锁的行为,Lock提供了一个更加面对对象的锁,在该锁中提供了更多的操作锁的功能。

/*
 *   使用JDK1.5的接口Lock替换同步代码块,替换同步代码块,实现线程 的安全性
 *   Lock接口方法
 *   	lock()获取锁
 *   	unlock()释放锁
 *   
 */
public class Tickets implements Runnable {
	// 定义可以出售的票源
	private int ticket = 100;
	// 在类的成员位置,创建Lock接口的实现类对象
	private Lock lock = new ReentrantLock();
	

	public void run() {
		while (true) {
			//	调用Lock接口中的方法获取锁
			lock.lock();
			if (ticket > 0) {
				try {
					Thread.sleep(10);
					System.out.println(Thread.currentThread().getName() + " 出售第" + ticket--);
				} catch (Exception ex) {
					
				}finally {
					//	释放锁
					lock.unlock();
				}				
			}			
		}
	}
}

死锁

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

synchronzied(A锁){
	synchronized(B锁){
         
	}
}

在这里插入图片描述

//	测试代码
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();
	}
}
//	线程代码,同步嵌套,产生死锁
public class DeadLock implements Runnable{
	private int i = 0;
	public void run() {
		while(true) {
			if(i % 2 == 0) {
				//	先进入A同步,再进入B同步
				synchronized (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++;
		}
	}
}
//两个对象锁类
public class LockA {
	private LockA() {}
	
	public final static LockA locka = new LockA();
}

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

等待唤醒

在开始讲解等待唤醒机制之前,有必要搞清一个概念——线程之间的通信:多个线程在处理同一个资源,但是处理的动作(线程的任务)却不相同。通过一定的手段使各个线程能有效的利用资源。而这种手段即—— 等待唤醒机制
等待唤醒机制所涉及到的方法(以下均为Object类的方法):

  • wait() :等待,将正在执行的线程释放其执行资格 和 执行权,并存储到线程池中。
  • notify():唤醒,唤醒线程池中被wait()的线程,一次唤醒一个,而且是任意的。
  • notifyAll(): 唤醒全部:可以将线程池中的所有wait() 线程都唤醒。
    下面是一个输入输出示例:
    在这里插入图片描述
//	测试类
/*
 * 开启输入输出线程,实现程序的赋值和打印值
 */
public class ThreadDemo {
	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();
	}
/*
 * 定义资源类,有2个成员变量
 * name, sex
 * 同时有两个线程对资源中的变量操作
 * 1个对name,age赋值
 * 1个对name,age做变量的输出打印
 */
public class Resource {
	public String name;
	public String sex;
	public boolean flag = false;
}


/*
 * 输入的线程,对资源对象Resource中成员变量赋值
 * 一次赋值张三,男
 * 下一次赋值李四,女
 */
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";				
				}
				//将对方线程唤醒,flag改为true
				r.flag = true;
				r.notify();
			}
			
			i++;
		}
	}

}
/*
 * 输出线程,对资源对象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();
			}
		}			
	}

}

输入输出的对象锁应该是同一个对象r,保证输入输出值的正确性。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值