需求:有两个线程,一个打印奇数,一个打印偶数,两个线程启动后,每次打印都必须是连续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();
}
}
虽然使用了这个思路后,两种实现接口的方式也可以解决问题,但应该还是没有第一种『继承类+同步代码块』效率高,所以推荐使用第一种方式。
最后作一些总结:
- 『继承类的方式』更适用于创建不同功能的线程
- 『实现接口的方式』更适用于创建同样功能的线程
- 「同步代码块」更适用于不同功能的线程
- 「同步方法」更适用于同样功能的线程
解决问题的方法有很多,而具体方案还要根据具体问题进行分析。