Java多线程同步问题:一个小Demo彻底搞懂

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

Java线程系列文章只是本身知识的总结梳理,都是最基础的玩意,已经掌握熟练的能够绕过。面试

1、一个简单的Demo引起的血案多线程

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

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方法不停打印字符串信息,运行程序打印信息以下:this

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

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

2、缘由解析线程

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

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

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

既然知道了问题发生的缘由,记下来咱们就要想办法解决问题啊,解决的思路就是保证一个线程在调用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。

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

咱们也能够利用同步代码块解决上述问题,修改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 }

5、静态函数的同步问题

咱们继续修改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 }

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

6、synchronized同步方式总结

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

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

你们是否是以为上面说的锁这个玩意咋这么抽象,看不见,摸不着的。从JDK1.5起咱们就能够根据须要显性的获取锁以及释放锁了,这样也更加符合面向对象原则。

Lock接口的实现子类之一ReentrantLock,翻译过来就是重入锁,就是支持从新进入的锁,该锁可以支持一个线程对资源的重复加锁,也就是说在调用lock()方法时,已经获取到锁的线程,可以再次调用lock()方法获取锁而不被阻塞,同时还支持获取锁的公平性和非公平性,所谓公平性就是多个线程发起lock()请求,先发起的线程优先获取执行权,非公平性就是获取锁与是否优先发起lock()操做无关。默认状况下是不公平的锁,为何要这样设计呢?现实生活中咱们都但愿公平的啊?咱们想一下,现实生活中要保证公平就必须额外开销,好比地铁站保证有序公平进站就必须配备额外人员维持秩序,程序中也是同样保证公平就必须须要额外开销,这样性能就降低了,因此公平与性能是有必定矛盾的,除非公平策略对你的程序很重要,好比必须按照顺序执行线程,不然仍是使用不公平锁为好。

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

 1 //true表示公平锁,false非公平锁
 2     private Lock lock = new ReentrantLock();
 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     }

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

8、Lock与synchronized同步方式优缺点

Lock 的锁定是经过代码实现的,而 synchronized 是在 JVM 层面上实现的(全部对象都自动含有单一的锁。JVM负责跟踪对象被加锁的次数。若是一个对象被解锁,其计数变为0。在线程第一次给对象加锁的时候,计数变为1。每当这个相同的线程在此对象上得到锁时,计数会递增。只有首先得到锁的线程才能继续获取该对象上的多个锁。每当线程离开一个synchronized方法,计数递减,当计数为0的时候,锁被彻底释放,此时别的线程就可使用此资源)。

synchronized 在锁定时若是方法块抛出异常,JVM 会自动将锁释放掉,不会由于出了异常没有释放锁形成线程死锁。可是 Lock 的话就享受不到 JVM 带来自动的功能,出现异常时必须在 finally 将锁释放掉,不然将会引发死锁。

在资源竞争不是很激烈的状况下,偶尔会有同步的情形下,synchronized是很合适的。缘由在于,编译程序一般会尽量的进行优化synchronize,另外可读性很是好。在资源竞争激烈状况下,Lock同步机制性能会更好一些。

 

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

声明:文章将会陆续搬迁到我的公众号,之后文章也会第一时间发布到我的公众号,及时获取文章内容请关注公众号

相关文章
相关标签/搜索