Javaの多线程基础(2)

一、多线程的同步

1. 同步的问题

如果线程间的运行完全取决于系统和CPU的调度,而不适当地加以控制,可能会出现多线程间不同步的问题。
假设有一个电影院共有三个售票柜台,每个柜台每隔3秒售出一张电影票。模拟过程的程序如下:

class TicketServer implements Runnable {		//售票服务器
	private int seatNum = 1;
	static private final int MAX_NUM = 25;		//总票数
	
	@Override
	public void run() {
		while (true) {
			try {
				Thread.sleep(3000);
			} catch (InterruptedException e) {
				System.out.println(e.getMessage());
				break;
			}
			if (seatNum > MAX_NUM) {
				System.out.println("Sorry! Server " + "'s Tickets sold out!");
				break;
			}
			System.out.println("Thanks for buying ticket" + "!");
			System.out.println("Your seat number is " + seatNum++ + "");
		}
	}
}

public class Demo {
	public static void main(String[] args) throws Exception {
		TicketServer server1 = new TicketServer();
		Thread courter1 = new Thread(server1);
		Thread courter2 = new Thread(server1);
		Thread courter3 = new Thread(server1);
		courter1.start();
		courter2.start();
		courter3.start();
	}
}
/*
最后几行出现了如下输出:
Thanks for buying ticket!
Thanks for buying ticket!
Your seat number is 26
Thanks for buying ticket!
Your seat number is 25
Your seat number is 27
Sorry! Server 's Tickets sold out!
Sorry! Server 's Tickets sold out!
Sorry! Server 's Tickets sold out!
*/

虽然我们在run方法中规定了当seatNum > 25时跳出循环,但由于系统和CPU调度的不确定性,有可能当courter1完成if语句执行后,切换到了courter2执行if语句,之后又切换到了courter3;而此时,seatNum = 24,因而三个线程均不满足if语句,而执行下面的语句。而当seatNum = 25时,由于if语句已经被执行过,不会再次判断,因此三个线程就会将seatNum的26,27也售出。这就是一个典型的多线程同步引发的问题。

2. 同步代码块

通过上面的例子可以知道,完全依靠系统和CPU调度线程是存在隐患的。我们需要在程序中设计相应的限制避免问题的发生。
Java内置了synchronzied关键字来解决这一问题。该关键字用于规定代码块必须在执行完后才能切换到其它线程,或者说同时只能有一个线程访问这一对象
synchronized修饰代码块时,有两种用法,第一种就是修饰一个给定的代码块。其原型为:

synchronized(同步访问的对象)
{
	同步代码块
}

被synchronized关键字上锁的是对象而不是代码块。它只规定对同一实例访问的同步,对不同实例的多线程访问不受影响。
对上面的程序做如下更改:

public void run() {		//添加synchronized代码块
		while (true) {
			try {
				Thread.sleep(3000);
			} catch (InterruptedException e) {
				System.out.println(e.getMessage());
				break;
			}
			synchronized (this) {
				if (seatNum > MAX_NUM) {
					System.out.println("Sorry! Server " + "'s Tickets sold out!");
					break;
				}
				System.out.println("Thanks for buying ticket"  + "!");
				System.out.println("Your seat number is " + seatNum++ + "");
			}
		}
	}
/*
最后几行:
Thanks for buying ticket!
Your seat number is 24
Thanks for buying ticket!
Your seat number is 25
Sorry! Server 's Tickets sold out!
Sorry! Server 's Tickets sold out!
Sorry! Server 's Tickets sold out!
*/

3. 同步方法

sychronized关键字的第二种用法直接修饰一个方法,相当于在synchronized代码块中添加整个方法的内容。

注意:sychronized虽然是修饰方法的关键字,但它不可继承。一个父类的synchronized方法在子类覆写后,默认不是同步的。

将上面的程序需要同步的部分写在另一个方法中,并用sychronized方法修饰。

public void run() {
		while (true) {
			try {
				Thread.sleep(3000);
			} catch (InterruptedException e) {
				System.out.println(e.getMessage());
				break;
			}
			if(!sellTicket())	//退出条件
				break;
		}
	}
	
	private synchronized boolean sellTicket() {		//同步方法
		if (seatNum > MAX_NUM) {
			System.out.println("Sorry! Server " + "'s Tickets sold out!");
			return false;
		}
		System.out.println("Thanks for buying ticket"  + "!");
		System.out.println("Your seat number is " + seatNum++ + "");
		return true;
	}
/*
最后几行:
Thanks for buying ticket!
Your seat number is 24
Thanks for buying ticket!
Your seat number is 25
Sorry! Server 's Tickets sold out!
Sorry! Server 's Tickets sold out!
Sorry! Server 's Tickets sold out!
*/

4. 死锁问题

死锁问题指的是:多个进程或线程形成一个链,链中的每一个成员都在等待下一个成员持有的资源,而最后一个成员持有的资源又由于某些原因无法释放,从而使整个进程线程链处于完全停滞的状态。
在Java中,造成死锁问题的资源就是对象锁。每一个线程持有的资源就是一个对象锁。而造成链尾对象锁无法释放的原因主要是循环等待,即该线程正在等待链中的某一成员资源,使整个链路形成回路,造成死锁。
最简单的例子是,线程A持有对象1的对象锁,并正在等待对象2的对象锁;而线程B持有对象2的对象锁,并正在等待对象1的对象锁。两个线程间互相等待对方释放对象锁,引发循环等待,造成了死锁问题。
下面的程序模拟了上面所说的死锁情况:

class A{	//对象A
	public synchronized void funcA(B b) {
		String threadName = Thread.currentThread().getName();
		System.out.println(threadName + "has A.");
		try {
			Thread.sleep(1000);
		}
		catch (Exception e) {
			e.printStackTrace();
		}
		System.out.println(threadName + "trying to get B.");
		b.funcB(this);
	}
}

class B{	//对象B
	public synchronized void funcB(A a) {
		String threadName = Thread.currentThread().getName();
		System.out.println(threadName + "has B.");
		try {
			Thread.sleep(1000);
		}
		catch (Exception e) {
			e.printStackTrace();
		}
		System.out.println(threadName + "trying to get A.");
		a.funcA(this);
	}
}

class DeadLockThread1 implements Runnable{		//线程1
	A a;
	B b;
	
	public DeadLockThread1(A a, B b) {
		this.a = a;
		this.b = b;
	}
	
	@Override
	public void run() {		
		a.funcA(b);
	}
}

class DeadLockThread2 implements Runnable{		//线程2
	A a;
	B b;
	
	public DeadLockThread2(A a, B b) {
		this.a = a;
		this.b = b;
	}
	
	@Override
	public void run() {		
		b.funcB(a);
	}
}

public class Demo {
	public static void main(String[] args) throws Exception {
		A a = new A();
		B b = new B();
		Thread thread1 = new Thread(new DeadLockThread1(a, b));
		Thread thread2 = new Thread(new DeadLockThread2(a, b));
		thread1.start();
		thread2.start();
	}
}
/*
运行结果:
Thread-1has B.
Thread-0has A.
Thread-1trying to get A.
Thread-0trying to get B.
注:程序停在此处,且永远不会结束
*/

若要避免死锁的发生,需要保证在程序中有多个对象锁时,所有的线程以一致的逻辑和顺序获取锁。

二、线程间通信

1. 线程通信问题

假设有两个机床,机床A负责为工件添加产品名和产品号,完成后将工件再交给机床B打包后取出。模拟过程程序如下:

class Product{
	static public Integer num = 1;
	String productNumber;
	String productName;
}

class MachineA implements Runnable{
	private Product p;
	
	public MachineA(Product p) {
		this.p = p;
	}
	
	@Override
	public void run() {
		for(int i = 0;i < 10;i++) {
			if(i < 5) {
				p.productName = "Product1";
				p.productNumber = (Product.num).toString();
				Product.num++;
			}
			else {
				p.productName = "Product2";
				p.productNumber = (Product.num).toString();
				Product.num++;
			}
		}
	}
}

class MachineB implements Runnable{
	private Product p;
	
	public MachineB(Product p) {
		this.p = p;
	}
	
	@Override
	public void run() {
		for(int i = 0;i < 10;i++) {
			System.out.println(p.productName + ' ' + p.productNumber);
		}
	}
}

public class Demo {
	public static void main(String[] args) throws Exception {
		Product p = new Product();
		Thread A = new Thread(new MachineA(p));
		Thread B = new Thread(new MachineB(p));
		A.start();
		B.start();
	}
}
/*
运行时,有可能出现这样的情输出:
Product2 5
还有可能:
Product2 10
Product2 10
Product2 10
Product2 10
Product2 10
Product2 10
Product2 10
Product2 10
Product2 10
Product2 10
*/

同时输出product2和5也是多线程的同步问题导致的。由于两个线程之间没有很好地通信,线程B并不知道线程A什么时候完成工作,当它取出工件时,工件可能还没有加工完成,导致部分遗留的还是旧信息。
此外,还有可能出现的情况是,某个线程连续工作,而另一个线程在这期间内没有工作,导致了工件的遗漏或重复。

扫描二维码关注公众号,回复: 11193863 查看本文章

2. 利用同步解决问题

  1. 第一个问题
    利用sychronized关键字也可以解决线程通信的第一个问题。当线程B访问p,发现线程A持有锁时,就是A仍在加工;当A不持有锁时,表明A已经加工完毕,可以取出工件。
//A中的run方法
public void run() {
	for (int i = 0; i < 10; i++) {
		synchronized (p) {
			if (i < 5) {
				p.productName = "Product1";
				p.productNumber = (Product.num).toString();
				Product.num++;
			} 
			else {
				p.productName = "Product2";
				p.productNumber = (Product.num).toString();
				Product.num++;
			}
		}
	}
}
//B中的run方法
public void run() {
	for (int i = 0; i < 10; i++) {
		synchronized (p) {
			System.out.println(p.productName + ' ' + p.productNumber);
		}
	}
}
  1. 第二个问题
    为了实现线程A每执行一次,线程B就执行一次,需要使用wait方法notify方法
    wait方法:使持有调用对象锁的线程释放锁,并使线程进入睡眠状态,直到被notify唤醒
    notify方法:唤醒申请对象锁的第一个调用了wait方法的线程
    notifyAll方法:唤醒申请对象锁的所有调用了wait方法的线程
    这三个方法在Object类中定义,因此可以直接使用。
//线程A的run方法
public void run() {
	for (int i = 0; i < 10; i++) {
		synchronized (p) {
			if(p.isDone) {
				try {
					p.wait();
				}
				catch (Exception e) {}
			}
			if (i < 5) {
				p.productName = "Product1";
				p.productNumber = (Product.num).toString();
				Product.num++;
				p.isDone = true;
			} 
			else {
				p.productName = "Product2";
				p.productNumber = (Product.num).toString();
				Product.num++;
				p.isDone = true;
			}
			p.notify();
		}
	}
}
//线程B的run方法
public void run() {
	for (int i = 0; i < 10; i++) {
		synchronized (p) {
			if(!p.isDone) {
				try {
					p.wait();
				}
				catch (Exception e) {}
			}
			System.out.println(p.productName + ' ' + p.productNumber);
			p.isDone = false;
			p.notify();
		}
	}
}
/*
运行结果:
Product1 1
Product1 2
Product1 3
Product1 4
Product1 5
Product2 6
Product2 7
Product2 8
Product2 9
Product2 10
*/

三、线程的生命周期

任意一个线程都有一个生命周期,下面是线程在其生命周期内的产生、运行、挂起和消亡的示意图:
在这里插入图片描述
控制线程的生命周期,主要就是控制线程的挂起和中断。Java内置了suspend、resume和stop方法用于线程挂起、恢复和中断。
但是这三个方法已经被弃用(deprecated)。
stop方法会直接中断线程的运行,可能导致部分工作未完成而引发错误,是线程不安全的方法。
suspend和resume方法是配对使用的方法。suspend方法不会释放线程持有的对象锁,会造成死锁问题。并且这两个方法允许其它线程直接控制本线程,也是线程不安全的。

线程中断的控制方法在上一篇中已经介绍过。
线程挂起更多使用上面的wait/notify方法进行控制,或者使用其它库提供的一些代理类控制。

原创文章 49 获赞 5 访问量 2993

猜你喜欢

转载自blog.csdn.net/weixin_44712386/article/details/105775535