Java多线程同步问题:一个小Demo完全搞懂

Posted leipdao

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Java多线程同步问题:一个小Demo完全搞懂相关的知识,希望对你有一定的参考价值。

版权声明:本文出自汪磊的博客,转载请务必注明出处。

Java线程系列文章只是自己知识的总结梳理,都是最基础的玩意,已经掌握熟练的可以绕过。

一、一个简单的Demo引发的血案

关于线程同步问题我们从一个简单的Demo现象说起。Demo特别简单就是开启两个线程打印字符串信息。

OutPutStr类源码:

1 public class OutPutStr {
2 
3     public void out(String str) {
4         for (int i = 0; i < str.length(); i++) {
5             System.out.print(str.charAt(i));
6        }
7         System.out.println();
8     }
9 }

很简单吧,就是一个方法供外界调用,调用的时候传进来一个字符串,方法逐个取出字符串的字符并打印到控制台。

接下来,我们看main方法中逻辑:

 1 public static void main(String[] args) {
 2         //
 3         final OutPutStr o = new OutPutStr();
 4         new Thread(new Runnable() {
 5 
 6             @Override
 7             public void run() {
 8                 //
 9                 while(true){
10                     o.out("111111111111");
11                 }
12             }
13         }).start();
14         new Thread(new Runnable() {
15 
16             @Override
17             public void run() {
18                 //
19                 while(true){
20                     o.out("222222222222");
21                 }
22             }
23         }).start();
24 }

也很简单,就是开启两个线程分别调用OutPutStr中out方法不停打印字符串信息,运行程序打印信息如下:

1 222222222222
2 222222222222
3 22222222222111111111
4 2
5 111111111111
6 111111111111
7 1111222222222211111111
8 111111111111

咦?和我们想的不一样啊,怎么还会打印出22222222222111111111这样子的信息,这是怎么回事呢?

二、原因解析

我们知道线程的执行是CPU随机调度的,比如我们开启10个线程,这10个线程并不是同时执行的,而是CPU快速的在这10个线程之间切换执行,由于切换速度极快使我们感觉同时执行罢了。发生上面线程的本质就是CPU对线程执行的随机调度,比如A线程此时正在打印信息还没打印完毕此时CPU切换到B线程执行了,B线程执行完了又切换回A线程执行就会导致上面现象发生。

线程同步问题往往发生在多个线程调用同一方法或者操作同一变量,但是我们要知道其本质就是CPU对线程的随机调度,CPU无法保证一个线程执行完其逻辑才去调用另一个线程执行。

三、同步方法解决上述问题

既然知道了问题发生的原因,记下来我们就要想办法解决问题啊,解决的思路就是保证一个线程在调用out方法的时候如果没执行完那么另一个不能执行此方法,换句话说就是只能等待别的线程执行完毕才能执行。

针对线程同步问题java早就有解决方法了,最简单的就是给方法加上synchronized关键字,如下:

1 public synchronized void out(String str) {
2         for (int i = 0; i < str.length(); i++) {
3             System.out.print(str.charAt(i));
4         }
5         System.out.println();
6 }

这是什么意思呢?加上synchronized关键字后,比如A线程执行out方法就相当于拿到了一把锁,只有获取这个锁才能执行此方法,如果在A线程执行out方法过程中B线程也想插一脚进来执行out方法,对不起此时这是不能够的,因为此时锁在A线程手里,B线程无权拿到这把锁,只有等到A线程执行完后放弃锁,B线程才能拿到锁执行out方法。

为out方法加上synchronized后其就变成了同步方法,普通同步方法的锁是this,也就是当前对象,比如demo中,外部要想调用out方法就必须创建OutPutStr类实例对象o,此时out同步方法的锁就是这个o。

四、同步代码块解决上述问题

我们也可以利用同步代码块解决上述问题,修改out方法如下:

1 public void out(String str) {
2         synchronized (this) {
3             for (int i = 0; i < str.length(); i++) {
4                 System.out.print(str.charAt(i));
5             }
6             System.out.println();
7         }
8 }

同步代码块写法:synchronized(obj){},其中obj为锁对象,此处我们传入this,同样方法的锁也为当前对象,如果此处我们传入str,那么这里的锁就是str对象了。

为了说明不同锁带来的影响我们修改OutPutStr代码如下:

 1 public class OutPutStr {
 2 
 3     public synchronized void out(String str) {
 4         for (int i = 0; i < str.length(); i++) {
 5             System.out.print(str.charAt(i));
 6         }
 7         System.out.println();
 8     }
 9     
10     public void out1(String str) {
11         
12         synchronized (str) {
13             for (int i = 0; i < str.length(); i++) {
14                 System.out.print(str.charAt(i));
15             }
16             System.out.println();
17         }
18     }
19 }

很简单我们就是加入了一个out1方法,out方法用同步函数保证同步,out1用同步代码块保证代码块,但是锁我们用的是str。

main代码:

 1 public static void main(String[] args) {
 2         //
 3         final OutPutStr o = new OutPutStr();
 4         new Thread(new Runnable() {
 5 
 6             @Override
 7             public void run() {
 8                 //
 9                 while(true){
10                     o.out("111111111111");
11                 }
12             }
13         }).start();
14         new Thread(new Runnable() {
15 
16             @Override
17             public void run() {
18                 //
19                 while(true){
20                     o.out1("222222222222");
21                 }
22             }
23         }).start();
24     }

也没什么,就是其中一个线程调用out方法,另一个调用out1方法,运行程序:

111111111111222
222222222222

111111111111222222222222
222222222222

看到了吧,打印信息又出问题了,就是因为out与out1方法的锁不一样导致的,线程A调用out方法拿到this这把锁,线程B调用out1拿到str这把锁,二者互不影响,解决办法也很简单,修改out1方法如下即可:

1 public void out1(String str) {
2         
3         synchronized (this) {
4             for (int i = 0; i < str.length(); i++) {
5                 System.out.print(str.charAt(i));
6             }
7             System.out.println();
8         }
9 }

五、静态函数的同步问题

我们继续修改OutPutStr类,加入out2方法:

 1 public class OutPutStr {
 2 
 3     public synchronized void out(String str) {
 4         for (int i = 0; i < str.length(); i++) {
 5             System.out.print(str.charAt(i));
 6         }
 7         System.out.println();
 8     }
 9 
10     public void out1(String str) {
11 
12         synchronized (this) {
13             for (int i = 0; i < str.length(); i++) {
14                 System.out.print(str.charAt(i));
15             }
16             System.out.println();
17         }
18     }
19 
20     public synchronized static void out2(String str) {
21 
22         for (int i = 0; i < str.length(); i++) {
23             System.out.print(str.charAt(i));
24         }
25         System.out.println();
26     }
27 }

main中两个子线程分别调用out1,ou2打印信息,运行程序打印信息如下;

1 222222222222
2 222222222222
3 222222222111111111111
4 111111111111

咦?又出错了,out2与out方法唯一不同就是out2就是静态方法啊,不是说同步方法锁是this吗,是啊,没错,但是静态方法没有对应类的实例对象依然可以调用,那其锁是谁呢?显然静态方法锁不是this,这里就直说了,是类的字节码对象,类的字节码对象是优先于类实例对象存在的。

将ou1方法改为如下:

1 public void out1(String str) {
2 
3         synchronized (OutPutStr.class) {
4             for (int i = 0; i < str.length(); i++) {
5                 System.out.print(str.charAt(i));
6             }
7             System.out.println();
8         }
9 }

再次运行程序,就会发现信息能正常打印了。

六、synchronized同步方式总结

到此我们就该小小的总结一下了,普通同步函数的锁是this,当前类实例对象,同步代码块锁可以自己定义,静态同步函数的锁是类的字节码文件。总结完毕,就是这么简单。说了一大堆理解这一句就够了。

七、JDK1.5中Lock锁机制解决线程同步

大家是不是觉得上面说的锁这个玩意咋这么抽象,看不见,摸不着的。从JDK1.5起我们就可以根据需要显性的获取锁以及释放锁了,这样也更加符合面向对象原则。

Lock接口的实现子类之一ReentrantLock,翻译过来就是重入锁,就是支持重新进入的锁,该锁能够支持一个线程对资源的重复加锁,也就是说在调用lock()方法时,已经获取到锁的线程,能够再次调用lock()方法获取锁而不被阻塞,同时还支持获取锁的公平性和非公平性,所谓公平性就是多个线程发起lock()请求,先发起的线程优先获取执行权,非公平性就是获取锁与是否优先发起lock()操作无关。

接下来我们修改OutPutStr类,添加out3方法:

 1 //true表示公平锁,false非公平锁
 2     ReentrantLock lock = new ReentrantLock(true);
 3     
 4     public void out3(String str) {
 5         
 6         lock.lock();//如果有其它线程已经获取锁,那么当前线程在此等待直到其它线程释放锁。
 7         try {
 8             for (int i = 0; i < str.length(); i++) {
 9                 System.out.print(str.charAt(i));
10             }
11             System.out.println();
12         } finally {
13             lock.unlock();//释放锁资源,之所以加入try{}finally{}代码块,
14             //是为了保证锁资源的释放,如果代码发生异常也可以保证锁资源的释放,
15             //否则其它线程无法拿到锁资源执行业务逻辑,永远处于等待状态。
16         }
17     }

关键注释都在代码中有所体现了,使用起来也很简单。

关于线程同步问题到这里就结束了,java多线程文章只是本人工作以来的一次梳理,都比较基础,但是却很重要的,最近招人面试的最大体会就是都喜欢那些所谓时髦的技术一问基础说的乱七八糟,浪费彼此的时间,好啦,吐槽了几句,本文到此为止,很基础的玩意,希望对你有用。

以上是关于Java多线程同步问题:一个小Demo完全搞懂的主要内容,如果未能解决你的问题,请参考以下文章

300 行代码带你搞懂 Java 多线程!

Java之多线程(6个demo)

Java并发系列终结篇:彻底搞懂Java线程池的工作原理

Java多线程的临界资源问题

关于Java多线程的线程同步和线程通信的一些小问题(顺便分享几篇质量高的博文)

Java——多线程高并发系列之synchronized关键字