线程安全:继承类方式和实现接口方式以及同步代码块和同步方法的相关使用

需求:有两个线程,一个打印奇数,一个打印偶数,两个线程启动后,每次打印都必须是连续5个奇数或连续5个偶数,奇偶之间的交替不作要求。

①继承类+同步代码块

继承两个「Thread」类,分别实现打印奇数和偶数的方法,将打印的代码加锁,每次只能是一个线程进行打印

public class Test0 {
	//继承Thread类方式,创建两种功能的Thread类:ThreadOdd、ThreadEven
	//并使用「字符串同步锁」保证线程安全
	public static void main(String[] args) {
        //创建奇、偶功能的线程
		new ThreadOdd().start();
		new ThreadEven().start();
	}

}
//打印奇数的线程
class ThreadOdd extends Thread{
	@Override
	public void run() {
		//进入无限循环打印
		while(true) {
			//通过「字符串同步锁」来实现不同类的线程之间的互斥访问
			//使得每次只能有一个线程在打印
			synchronized ("print") {
				try {
					//连续打印5个奇数
					for(int i = 1; i <= 5; i++) {
						//每次打印间隔0.1秒
						Thread.sleep(100);
						System.out.println(i+" Printing Odd...");
					}
					//每打印5个空一行
					System.out.println();
				} catch (InterruptedException e) {
					e.printStackTrace();
				}
			}
		}
	}
}
//打印偶数的线程
class ThreadEven extends Thread{
	@Override
	public void run() {
		//进入无限循环打印
		while(true) {
			//通过「字符串同步锁」来实现不同类的线程之间的互斥访问
			//使得每次只能有一个线程在打印
			synchronized ("print") {
				try {
					//连续打印5个偶数
					for(int i = 1; i <= 5; i++) {
						//每次打印间隔0.1秒
						Thread.sleep(100);
						System.out.println(i+" Printing Even...");
					}
					//每打印5个空一行
					System.out.println();
				} catch (InterruptedException e) {
					e.printStackTrace();
				}
			}
		}
	}
}

这个例子验证了,即使在是不同的类中,只要同步代码块的锁对象相同(上例中都使用字符串「print」作为锁对象),就能对代码进行锁定。

这样看来,『能在不同类中进行同步的操作』的依据是:可以使用「同步代码块」的「锁对象」,进行跨类锁定,不管线程对象是否是同类,只要「同步代码块」的「锁对象」一致,就能保证线程安全。

因此可以推测出,「同步方法」是不能进行跨类同步的,因为在『继承类的方式』中,同步方法的本质是『隐含的锁对象是类的Class对象』,所以不同类的线程的「Class对象」不一致,也就不能够满足方法的锁对象一致,正是这一点导致要进行跨类同步不能使用**「同步方法」**。(『实现接口的方式』就更不可能使用「同步方法」了)

而要想两个线程实现不同的功能,就只能是使用『继承类的方式』来创建线程。因为『实现接口的方式』创建的「Target」对象都是同一个类,而用其创建出的「Thread」对象都是同样的实现功能,无法满足不同线程实现不同功能的需求。

不过也是可以强行使用『实现接口的方式』的:

创建两个不同功能的「Target」对象,再分别用这两个「Target」对象创建两个「Thread」类,这样是可以达到『不同功能的线程实现同步锁』的需求的,代码如下:

public class Test1 {

	//实现Runnable接口方式,分别用两种功能的Target来创建对应两种功能的Thread类:TargetOdd、TargetEven
	//并使用「字符串同步锁」保证线程安全
	public static void main(String[] args) {
		
        //用奇偶功能的Target分别创建奇、偶线程
		new Thread(new TargetOdd()).start();
		new Thread(new TargetEven()).start();

	}

}
//打印奇数的Target
class TargetOdd implements Runnable{
	@Override
	public void run() {
		//进入无限打印循环
		while(true) {
			//通过「字符串同步锁」来实现不同类的线程之间的互斥访问
			//使得每次只能有一个线程在打印
			synchronized ("print") {
				try {
					//连续5次打印
					for(int i = 1; i <= 5; i++) {
						Thread.sleep(100);
						System.out.println(i+" Printing Odd...");
					}
					//每5个空一行
					System.out.println();
				} catch (InterruptedException e) {
					e.printStackTrace();
				}
			}
		}
	}
}
//打印偶数的Target
class TargetEven implements Runnable{
	@Override
	public void run() {
		//进入无限打印循环
		while(true) {
			//通过「字符串同步锁」来实现不同类的线程之间的互斥访问
			//使得每次只能有一个线程在打印
			synchronized ("print") {
				try {
					//连续5次打印
					for(int i = 1; i <= 5; i++) {
						Thread.sleep(100);
						System.out.println(i+" Printing Even...");
					}
					//每5个空一行
					System.out.println();
				} catch (InterruptedException e) {
					e.printStackTrace();
				}
			}
		}
	}
}

不过这样做的意义不大,甚至可能没必要这样做,因为这跟上面『继承类的方式』的原理一样,都是通过创建两个不同的「Thread」类来区分功能的。

综上所述,要实现这个需求(不同功能的线程在运行时保证线程安全),其关键两点是①两个线程分别实现两种功能②两个线程的功能还要保持互斥访问(线程安全)。第①点通过两个继承「Thread」类来实现不同功能,第②点通过同步代码块的「字符串锁对象」来实现互斥访问。

那么要想实现不同功能的线程线程安全问题,就只能使用『继承类的方式』+『同步代码块』这个组合吗?

其实还可以有另外一种思路,这种思路解放了前面被种种限制的方式。

这种思路是我在思考这个需求时最初就想到的(说实话,其实是因为我当时根本不知道还能有跨类同步这种操作),而当时我的想法是:实现同步的前提就是对两个线程的执行代码进行**「加锁」,而要「加锁」就只能是让两个线程对象属于同一个类,这样才能使它们共用同一个被「加锁」**代码。

但是,顾此就会失彼,让两个线程共用同一个「同步代码块」就无法达到分别实现它们各自的功能,真的无法吗?既然只能共用「同步代码块」,那么进入代码块之后有什么可以用来判断区分两个线程呢?**名字!**我突然想起线程是有名字的!这样问题就可以解决了。

根据这个思路,我们使用实现类也是可以做到的,使用同样的「Target」创建的两个「Thread」对象,进入到同步代码块之后,再根据各自的名字判断执行对应的功能(打印奇、偶)。

②实现接口+同步代码块

public class Test2 {

	//实现Runnable接口方式,用一个内部有两种功能的Target创建Thread类:Target1
	//并使用「当前对象同步锁」保证线程安全
	public static void main(String[] args) {
		//创建内部有两种功能的Target
		Target1 myTarget = new Target1();
		//使用同一个Target创建奇、偶线程
		new Thread(myTarget, "奇数").start();
		new Thread(myTarget, "偶数").start();
	}
}

class Target1 implements Runnable{

	@Override
	public void run() {
		//进入无限打印循环
		while(true) {
			//每次循环只能有一个线程在打印,并且连续打印5个,再进入下一次循环
			synchronized (this) {
				//连续打印5个
				for (int i = 1; i <= 5; i++) {
					// 奇数线程
					//如果是奇数线程,就打印奇数
					if ("奇数".equals(Thread.currentThread().getName())) {
						//省略打印奇数细节
						System.out.println(i+" Printing Odd...");
					}
					// 偶数线程
					//如果是偶数线程,就打印偶数
					if ("偶数".equals(Thread.currentThread().getName())) {
						//省略打印偶数细节
						System.out.println(i+" Printing Even...");
					}
					//每打印一个,停顿0.1秒
					try {
						Thread.sleep(100);
					} catch (InterruptedException e) {
						e.printStackTrace();
					}
				}
				//每打印5个空一行
				System.out.println();
			}
			
		}
	}
	
}

虽然代码有些繁琐,可读性有点低,但确实是可以实现这个需求的,这样就不用非要使用两个类来实现不同的功能。

前面说到,由于「同步方法」不能用于跨类同步,那么就不让它跨类呗。这个思路的好处就是,不管是使用『继承类的方式』还是『实现接口的方式』,都让线程进入到同步区域内再做判断,还是根据线程的名字决定其执行的功能。

③实现接口+同步方法

public class Test3 {

	//实现Runnable接口方式,用一个内部有两种功能的Target创建Thread类:Target2
	//并使用静态「同步方法」保证线程安全
	public static void main(String[] args) {
		
		Target2 oneTarget = new Target2();
		new Thread(oneTarget, "奇数").start();
		new Thread(oneTarget, "偶数").start();
		
	}

}

class Target2 implements Runnable{

	@Override
	public void run() {
		
		//调用打印方法
		while(true) {
			print();
		}
		
	}
	
	public synchronized static void print() {
			//连续打印5个
			for (int i = 1; i <= 5; i++) {
				// 奇数线程
				//如果是奇数线程,就打印奇数
				if ("奇数".equals(Thread.currentThread().getName())) {
						//省略打印奇数细节
						System.out.println(i+" Printing Odd...");
				}
				// 偶数线程
				//如果是偶数线程,就打印偶数
				if ("偶数".equals(Thread.currentThread().getName())) {
						//省略打印偶数细节
						System.out.println(i+" Printing Even...");
				}
				//每打印一个,停顿0.1秒
				try {
					Thread.sleep(100);
				} catch (InterruptedException e) {
					e.printStackTrace();
				}

			}
			//每打印5个空一行
			System.out.println();
	}
	
}

虽然使用了这个思路后,两种实现接口的方式也可以解决问题,但应该还是没有第一种『继承类+同步代码块』效率高,所以推荐使用第一种方式。

最后作一些总结:

  1. 『继承类的方式』更适用于创建不同功能的线程
  2. 『实现接口的方式』更适用于创建同样功能的线程
  3. 「同步代码块」更适用于不同功能的线程
  4. 「同步方法」更适用于同样功能的线程

解决问题的方法有很多,而具体方案还要根据具体问题进行分析。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值