Java多线程的线程同步和线程通信的一些小问题(顺便分享几篇质量高的博文)
- 前言:在学习多线程时,遇到了一些问题,这里我将这些问题都分享出来,同时也分享了几篇其他博客主的博客,并且将我个人的理解也分享给大家,本人的水平有限,如果我的分析或者结论有错误希望大家一定要帮我指出来,我好能改正和提高。
一、对于线程同步和同步锁的理解(注:分享了三篇高质量的博客)
以下我精心的挑选了几篇博文,分别是关于对线程同步的理解和如何选择线程锁以及了解线程锁的作用范围。
<一>线程同步锁的选择
1. 这里我推荐下陆先生的Java代码质量改进之:同步对象的选择这篇博文。
2. 以上推荐的博文是以卖火车票为例,引出了非同步会导致的错误以及同步锁(监视器)应该如果选择,应该能够帮助大家理解同步锁。
<二>线程同伴锁用法及同步锁的作用范围
1. 这里我推荐下Java中synchronized同步锁用法及作用范围这篇博文。
2. 以上的博文将静态锁(字节码文件锁)和非静态锁(this)进行了对比,以及将线程非同步和线程同步下进行了对比,对大家了解线程锁的用法和作用范围有很大的帮助。
<三>对线程同步的理解
1. 这里我推荐下java中线程同步的理解(非常通俗易懂)这篇博文。
2. 以上推荐的博文以非常通俗易懂的观点解释了到时什么同步,将同步理解成了线程同步就是线程排队,而且举了一些日常生活中的例子来让大家理解到底什么是同伴。
<四>同步的作用场景
1. 并不是说同步在什么情况下都是好的,因为线程的同步会带来较低效率,因为线程同步就代表着线程要排队,即线程同步锁会带来的同步阻塞状态。
2. 因为CPU是随意切换线程的,当我们想让当前线程执行之后CPU不随意切换到其他线程,或者我们想要让某个线程的代码能够在完全执行之前不会被抢夺执行权,不会导致从而无法连续执行,那么我们就需要线程的帮助。
二、线程同步和线程通信的几个小细节
以下是我在学习线程同步时,遇到的小问题和我的小感悟
<一>线程sleep方法的基本用法和注意细节
1.sleep方法的基本用法
Thread.sleep(long millis),传入毫秒数(1秒 = 1000毫秒),在指定的毫秒数内让当前正在执行的线程休眠(暂停执行),此操作受到系统计时器和调度程序精度和准确性的影响。该线程不丢失任何监视器的所属权。(~注:Java技术文档的意思就是该线程休眠指定的毫秒数,而且休眠状态暂时失去CPU执行权,而且线程醒来后,该线程不会释放锁。)
/**
*
* Thread.sleep的计时器用法
*
*/
public class ThreadSleepTest { public static void main(String[] args) {
new Thread() {
@Override
public void run() {
int timeCount = 10;
while (timeCount >= 0) {
if (timeCount == 0) {
System.out.println("新年快乐!~");
break;
}
System.out.println("还剩" + timeCount-- + "秒");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}.start();
} }
2.sleep方法使用的位置选择
我在使用sleep方法时发现,当sleep的位置不一致所放的位置不同时,线程所运行的结果也是大不相同的,以下的代码是为了举例子,并不是说这个同步代码块就是应这样写(其实这段代码这么写是有很大的问题的,因为同步资源的选择不准确),至于同步资源的选择我在第二个大问题会讲到。
- A 以下的代码是sleep方法出现在了售票的代码块之前,这时出现了负票。(可能时间上也会导致差异,但是这里先不考虑时间时间因素,时间因素等下讲。)
package javase.week4; public class SellTrainTickets { public static void main(String[] args) {
new MyThread("窗口1").start();
new MyThread("窗口2").start();
new MyThread("窗口3").start();
new MyThread("窗口4").start();
} } class MyThread extends Thread { static int tickets = 100; public MyThread(String name) {
super(name);
} @Override
public void run() {
while (tickets > 0) {//假设这已经减到了1 1>0 然后窗口1 窗口2 窗口3 窗口4 都进入循环
try {
Thread.sleep(20); } catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (MyThread.class) {
System.out.println(getName() + "卖出了第" + tickets-- + "张票!");//然后0 -1 -2 -3,这时就出现了负票
}
}
}
}
- B 以下的代码是sleep方法出现在了售票的代码块之后,这里没有出现负票了,在票数100的情况下,而且时间是20毫秒的情况下,该段代码正好保证了时间点上的合理性,但是相同情况下sleep方法出现在输出售票之前的代码就会出现错误,即使改变时间和票数其sleep方法出现的位置错误,还是会导致了在票数为负的情况。(其实如果票数更改或者时间的改变也可能导致sleep方法出现在售票代码块之后的情况下负票的出现)
public class SellTrainTickets { public static void main(String[] args) {
new MyThread("窗口1").start();
new MyThread("窗口2").start();
new MyThread("窗口3").start();
new MyThread("窗口4").start();
} } class MyThread extends Thread { static int tickets = 100; public MyThread(String name) {
super(name);
} @Override
public void run() {
while (tickets > 0) {//假设这已经减到了0 5>0 然后窗口1 窗口2 窗口3 窗口4 都进入循环
synchronized (MyThread.class) {
System.out.println(getName() + "卖出了第" + tickets-- + "张票!");//然后 3 2 1 0
}
try {
Thread.sleep(20);//此时票数等于0,这时窗口1 窗口2 窗口3 窗口4 都处于休眠,然后如果这里的时间合理的话,再次判断的话,正好在等于0的时候,都没有线程再次进入循环,也就不会出现负票了 } catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
- C 总结下其实sleep方法出现的位置可能会影响到线程的结果,但是其实一般情况下是不会这么样去使用的,这里只是为了演示下sleep方法在位置不同的情况下出现的不同的结果,目的是为了让大家注意编程的细节。
3.sleep方法的传入参数的选择
sleep方法的传入的毫秒数对于线程的运行结果是有较大的影响的,最直接简单的影响就是让运行延迟了,但是除了这个以外其实也让线程的运行结果发生了变化,顺便分享一篇一篇高质量的博文Sleep(0)的妙用。
- A 当传入的参数为100时,以下是代码演示和执行结果,几乎每一个窗口(线程)都可以抢夺到运行权,而且比较分散。
public class TicketsThreadTest { public static void main(String[] args) {
new TicketThread("窗口1").start();
new TicketThread("窗口2").start();
new TicketThread("窗口3").start();
new TicketThread("窗口4").start();
} } class TicketThread extends Thread { public TicketThread(String name) {
super(name);
} private static int ticket = 100; public void run() {
while (true) {
synchronized (TicketThread.class) {
if (ticket <= 0) {
break;
}
System.out.println(getName() + "卖出了第" + ticket-- + "张票!");
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
- B 当传入的参数为1时,以下是代码演示和执行结果,这次执行的效果就不是很好,并不是每一个线程都能很好的执行到,或者执行得不是很分散。
public class TicketsThreadTest { public static void main(String[] args) {
new TicketThread("窗口1").start();
new TicketThread("窗口2").start();
new TicketThread("窗口3").start();
new TicketThread("窗口4").start();
} } class TicketThread extends Thread { public TicketThread(String name) {
super(name);
} private static int ticket = 100; public void run() {
while (true) {
synchronized (TicketThread.class) {
if (ticket <= 0) {
break;
}
System.out.println(getName() + "卖出了第" + ticket-- + "张票!");
try {
Thread.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
- C 总结以下时间参数对线程的结果的影响,以卖火车票为例,当我们在sleep方法中输入不同的参数,那么线程的运行结果就发生了变化,因为当我们给定的休眠期长了,那么线程的抢夺CPU执行权的速度就放缓了,此时运行的结果就变得比较分散,如果几乎没有休眠期那么抢到执行权的窗口(线程)可能还是处于领先优势,sleep方法其实让处于优先地位的暂时休眠让出了CPU执行权,然后sleep醒来又处于就绪状态来抢夺资源。这样不会让其他线程变成无法执行的尴尬境遇。当然后续可以使用wait和notify以及notifyAll的方法,让线程进行有规律地交替运行。
<二>明确需要同步的共享资源
如果这里同步的是代码块不是代码方法,那么这里需要对要同步的共享资源的选择要准确,如果选择得不准确会导致结果不理想。
- A 以下代码表示选择的共享代码块为售票的单个输出语句,此时可以看出结果,结果出现了负票
public class TicketsThreadTest { public static void main(String[] args) {
new TicketThread("窗口1").start();
new TicketThread("窗口2").start();
new TicketThread("窗口3").start();
new TicketThread("窗口4").start();
} } class TicketThread extends Thread { public TicketThread(String name) {
super(name);
} private static int ticket = 100; public void run() {
while (true) {
if (ticket <= 0) {
break;
}
synchronized (TicketThread.class) {
System.out.println(getName() + "卖出了第" + ticket-- + "张票!");
}
try {
Thread.sleep(0);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
- B 以下代码表示选择的共享代码块是while循环的整个代码块,此时可以看出结果,结果没有出现负票,而且经过了多次尝试也没有出现
public class TicketsThreadTest { public static void main(String[] args) {
new TicketThread("窗口1").start();
new TicketThread("窗口2").start();
new TicketThread("窗口3").start();
new TicketThread("窗口4").start();
} } class TicketThread extends Thread { public TicketThread(String name) {
super(name);
} private static int ticket = 100; public void run() {
while (true) {
synchronized (TicketThread.class) {
if (ticket <= 0) {
break;
}
System.out.println(getName() + "卖出了第" + ticket-- + "张票!");
try {
Thread.sleep(0);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
- C 总结在选择需要同步的代码块是一定要注意哪些代码块(资源是需要共享的),这里就要判断下这些代码是否是需要共享,将需要共享的资源用synchronized代码块包起来
<三>线程通信之while和if的选择
- A 以下的代码使用的是if选择结构进行线程通信之间的判断,可以发现三个线程之间没有有规律地交替进行。
package javase.week4; /**
*
* 三个线程之间的通信使用if选择语句
*
*/
public class ComunicatedThreadTest {
public static void main(String[] args) {
Printer1121 p = new Printer1121();
new Thread() {
@Override
public void run() {
while (true) {
try {
p.print1();
} catch (Exception e) {
e.printStackTrace();
}
}
}
}.start();
new Thread() {
@Override
public void run() {
while (true) {
try {
p.print2();
} catch (Exception e) {
e.printStackTrace();
}
}
}
}.start(); new Thread() {
@Override
public void run() {
while (true) {
try {
p.print3();
} catch (Exception e) {
e.printStackTrace();
}
}
}
}.start();
} } class Printer1121 {
private int flag = 1; public void print1() throws Exception {
synchronized (this) {
if (flag != 1) {
this.wait();
}
Thread.sleep(100);
System.out.print(1);
System.out.print(2);
System.out.print(3);
System.out.print(4);
System.out.print(5);
System.out.println();
flag = 2;
this.notifyAll();
}
} public void print2() throws Exception {
synchronized (this) {
if (flag != 2) {
this.wait();
}
Thread.sleep(100);
System.out.print("a");
System.out.print("b");
System.out.print("c");
System.out.print("d");
System.out.print("e");
System.out.println();
flag = 3;
this.notifyAll();
}
} public void print3() throws Exception {
synchronized (this) {
if (flag != 3) {
this.wait();
}
Thread.sleep(100);
System.out.print("A");
System.out.print("B");
System.out.print("C");
System.out.print("D");
System.out.print("E");
System.out.println();
flag = 1;
this.notifyAll();
}
}
}
- B 以下是用while对通信条件进行循环判断的,可以发现三个线程是有规律地循环进行运行的。
package javase.week4;
/**
*
* 三个线程之间的通信使用while循环判断语句
*
*/
public class ComunicatedThreadTest {
public static void main(String[] args) {
Printer1121 p = new Printer1121();
new Thread() {
@Override
public void run() {
while (true) {
try {
p.print1();
} catch (Exception e) {
e.printStackTrace();
}
}
}
}.start();
new Thread() {
@Override
public void run() {
while (true) {
try {
p.print2();
} catch (Exception e) {
e.printStackTrace();
}
}
}
}.start(); new Thread() {
@Override
public void run() {
while (true) {
try {
p.print3();
} catch (Exception e) {
e.printStackTrace();
}
}
}
}.start();
} } class Printer1121 {
private int flag = 1; public void print1() throws Exception {
synchronized (this) {
while (flag != 1) {
this.wait();
}
Thread.sleep(100);
System.out.print(1);
System.out.print(2);
System.out.print(3);
System.out.print(4);
System.out.print(5);
System.out.println();
flag = 2;
this.notifyAll();
}
} public void print2() throws Exception {
synchronized (this) {
while (flag != 2) {
this.wait();
}
Thread.sleep(100);
System.out.print("a");
System.out.print("b");
System.out.print("c");
System.out.print("d");
System.out.print("e");
System.out.println();
flag = 3;
this.notifyAll();
}
} public void print3() throws Exception {
synchronized (this) {
while (flag != 3) {
this.wait();
}
Thread.sleep(100);
System.out.print("A");
System.out.print("B");
System.out.print("C");
System.out.print("D");
System.out.print("E");
System.out.println();
flag = 1;
this.notifyAll();
}
}
}
- C 这里进行下原因分析,为什么会出现这样的情况?首先wait方法在同步代码块里被调用了,那么此时调用者直接在wait处等待了,然后等待下次被notify或者notifyAll唤醒。而if选择结构在判断一次之后就顺序执行了,当线程被唤醒时,我们希望的是再次判断一次条件看能够继续进行,但是if无法做到,因为上次已经判断正确了,它只会向下继续执行此时就会又出现随意无规律交替运行,但是while是循环判断,即使判断过一次了,但是每次执行完它会再次判断,这时就会让三个线程的运行结果有规律了。