java线程安全问题以及使用synchronized解决线程安全问题的几种方式

1、线程安全问题

1.产生缘由

  咱们使用java多线程的时候,最让咱们头疼的莫过于多线程引发的线程安全问题,那么线程安全问题究竟是如何产生的呢?究其本质,是由于多条线程操做同一数据的过程当中,破坏了数据的原子性。所谓原子性,就是不可再分性。有物理常识的小伙伴可能要反驳了,谁说原子不可再分?原子里边还有质子和中子。咱们不在这里探讨物理问题,我确实也没深究过为何被称为原子性,也许是这个原则出现的时候尚未发现质子和中子,咱们只要记住在编程中所提到的原子性指的是不可再分性就行了。回到正题,为何说破坏了数据的原子性就会产生的线程安全问题呢?咱们用一个很是简单的例子来讲明这个问题。java

  咱们来看下面这段很是简单的代码:面试

1 int i = 1;
2 int temp; 
3 
4 while(i < 10){
5 temp = i; //读取i的值
6 i = temp + 1; //对i进行+1操做后再从新赋给i
7 };

  细心的小伙伴可能已经发现了,这不就是i++作的事情吗。没错,其实i++就是作了上面的两件事:编程

  1. 读取i当前的值
  2. 对读取到的值加1而后再赋给i

  咱们知道,在某一个时间点,系统中只会有一条线程去执行任务,下一时间点有可能又会切换为其余线程去执行任务,咱们没法预测某一时刻到底是哪条线程被执行,这是由CPU来统一调度的。所以如今假设咱们有t一、t2两条线程同时去执行这段代码。假设t1执行完第5行代码停住了(须要等待CPU下次调度才能继续向下执行),此时t1读到i的值是1。而后CPU让t2执行,注意刚才t1只执行完了第5行,也就是说t1并无对i进行加1操做而后再赋回给i,所以这是i的值仍是1,t2拿到i=1后一路向下执行直到结束,当执行到第6行的时候对i进行加1并赋回给i,完成后i的值变为2。好了,此时CPU又调度t1让其继续执行,重点在这里,还记不记得t1暂停前读取到的i是几?没错是1,此时t1执行第6行代码,对i进行加1获得的结果是2而后赋回给i。好了,问题出来了,咱们清楚的直到循环进行了两次,按正常逻辑来讲,对i进行两次加1操做后,此时i应该等于3,可是两条线程完成两次加1操做后i的值居然是2,当进行第三次循环的时候,读取到i的值将会是2,这样的结果是否是很诡异,这就是线程安全问题的产生。那么引起这个问题的缘由是什么呢?其实就是将读和写进行了分割,当读和写分割开后,若是一条线程读完但未写时被CPU停掉,此时其余线程就有可能趁虚而入致使最后产生奇怪的数据。安全

  那么上面这段代码怎么修改才能不产生线程安全问题呢?咱们知道一条线程被CPU调度执行任务时,最少要执行一行代码,因此解决办法很简单,只要将读和写合并到一块儿就能够了,下面的代码是经过JUC中的原子操做来完成自增的操做(不熟悉的同窗能够简单的将其理解成读写是一块儿执行的,不能够被分开执行):多线程

1 private static AtomicInteger count = new AtomicInteger(0);
2 
3 while(count < 10){
4    count.incrementAndGet(); 
5 };

这样,咱们将读和写用原子操做count.incrementAndGet()来替代,此时线程不管在哪行中止,其余线程也不会对数据产生干扰,我画一个图来形象的说明这一点(图有点丑,不要介意):ide

咱们能够把左边的圆当作是符合原子性(即一步执行)的代码,而右边的圆是被分割成了两步执行的代码。若是数据没有破坏原子性,因为线程被调度一次的最少要执行1行代码,那么t1只要执行了这行代码,就会连读带写所有完成,其余线程再拿到的数据就是被写过的最新数据,不会有任何安全隐患;而若是数据破坏了原子性,将读写进行了分割,那么t1,读取完数据若是停掉的话,t2执行的时候拿到的就是一个老数据(没有被更新的数据),接下来t1,t2同时对相同的老数据进行更新势必会所以数据的异常。函数

另外须要说明的是,为何上面我要使用JUC的AtomicInteger类而不是count++?这里涉及到了一个经典的面试题,count++操做是不是线程安全的?答案是否是,有兴趣的同窗能够参考http://www.javashuo.com/article/p-tdstvhyb-gw.htmlthis

2.注意

  对于线程安全问题,须要注意如下两点:spa

  1. 只存在读数据的时候,不会产生线程安全问题。
  2. 在java中,只有同时操做成员(全局)变量的时候才会产生线程安全问题,局部变量不会(每一个线程执行时将会把局部变量放在各自栈帧的工做内存中,线程间不共享,故不存在线程安全问题,这里不展开描述内存问题,有兴趣可自行百度)。

3.代码演示

  基于上面的分析,咱们经过最经典的卖票的例子来进行代码演示。需求:使用两个线程来模拟两个窗口同时出售100张票:.net

 1 public class TicketThread implements Runnable{
 2 
 3     private int ticketCount = 100;
 4     
 5     @Override
 6     public void run() {
 7         while (ticketCount > 0) {
 8             try {
 9                 Thread.sleep(50);
10             } catch (InterruptedException e) {
11                 e.printStackTrace();
12             }
13             sale();
14         }
15     }
16     
17     public void sale(){
18         if (ticketCount > 0) {
19             System.out.println(Thread.currentThread().getName() + "正在出售第" + (100-ticketCount+1) + "张票");
20             ticketCount --;
21         }
22     }
23 }

 

 

 1 public class Main {
 2     public static void main(String[] args) {
 3 
 4       TicketThread ticketThread = new TicketThread(); 
 5       Thread t1 = new Thread(ticketThread, "窗口1--");
 6       Thread t2 = new Thread(ticketThread, "窗口2--");
 7       t1.start();
 8       t2.start();
 9     }
10 }

运行结果:

结果分析:

从结果来看出现了不少诡异的数据,很明显是发生了线程安全问题,根据上面的分析,相信你应该知道是哪里致使的了。正式因为TicketThread类中第 19,20行的代码对成员变量ticketCount的读和写进行了分割才形成的,另外count--也是形成线程安全问题的缘由之一,上面已经提过,这里不作详述。至于线程安全问题的解决方法之一,经过synchronized关键字会在下面进行讲解。

 

2、使用synchronized解决线程安全问题

1.synchronized的概念

  synchronized在英语中翻译成同步,同步想必你们都不陌生。例如同步调用,有A,B两个方法,必需要先调用A而且得到A的返回值才能去调用B,也就是说,想作下一步,必需要拿到上一步的返回值。一样的道理,使用了synchronized的代码,当线程t1进入的时候,另外一个线程若t2想进入,就必需要获得返回值才能进入,怎么获得返回值呢?那就要等t1出来了才会有返回值。这就是多线程中常说的加锁,使用synchronized的代码咱们能够想象成将他们放到了一个房间,我前边所说的返回值就至关于这个房间的钥匙,进入这个房间的线程同时会把钥匙带进去,当它出来的时候会将钥匙仍在地上(释放资源),而后其余线程过来抢钥匙(争夺CPU执行权),以此类推。

  被放到房间里代码,其实就是为了让其保持原子性,由于当线程t1进入被synchronized修饰的代码当中的时候,其余线程是被锁在外边进不来的,知道线程t1执行完里边的全部代码(或抛出异常),才会释放资源。咱们换个角度想,这不就是让房间(synchronized)里面的代码保持了原子性吗,某一线程只要进去了,就必需要执行完毕里边的代码别的线程再进去,期间不会有其余线程趁虚而入来干扰它,就像我上面图中左边那个圆同样,也就是至关于将原本分割的读和写的操做合并在了一块儿,让一个线程要么不执行,只要执行就得把读和写所有执行完(且期间不会受干扰)。

  理解了我上边所说的,就不再用纠结到底把什么代码放入synchronized中了,只要把读和写分割的代码,而且分割后会引起线程安全问题的代码放入让其保持原子性就能够了。很明显在上面TicketThread类中,就是第19和20行。

2.synchronized的三种用法

(1)同步代码块

 1 public class SynchronizedBlockThread implements Runnable {
 2 
 3     private Object obj = new Object();
 4     private int ticketCount = 100;
 5     
 6     @Override
 7     public void run() {
 8         while (ticketCount > 0) {
 9             try {
10                 Thread.sleep(50);
11             } catch (InterruptedException e) {
12                 e.printStackTrace();
13             }
14             sale();
15         }
16     }
17     
18     public void sale(){
19         
20         synchronized (obj) { //使用同步代码块使线程间同步 21         if (ticketCount > 0) {
22             System.out.println(Thread.currentThread().getName() + "正在出售第" + (100-ticketCount+1) + "张票");
23             ticketCount --;
24             }
25         }
26     }
27 }
 1 public class Main {
 2 
 3     public static void main(String[] args) {
 4         SynchronizedBlockThread blockThread = new SynchronizedBlockThread();
 5         Thread t1 = new Thread(blockThread, "窗口1--");
 6         Thread t2 = new Thread(blockThread, "窗口2--");
 7         t1.start();
 8         t2.start();
 9     }
10 }

 

代码分析:

SynchronizedBlockThread类中须要注意一点,第20行,多个线程之间的同步代码块中必须使用相同的锁(体如今代码中就是同一个对象)才能保证同步,才能使其余不进入干扰,两条线程若是使用的不是同一把锁,那么一条线程进入synchronized中且未释放资源前,另外一条线程依然能够进入。同步代码块中使用的锁要求必须是引用数据类型,最经常使用的就是传入一个Object对象,或者使用当前类的对象,即this。

 

运行结果:使用synchronized是读写数据同步后没有再出现线程安全问题

 

(2)同步函数

 1 public class SynchronizedMethodThread implements Runnable{
 2 
 3     private int ticketCount = 100;
 4     
 5     @Override
 6     public void run() {
 7         while (ticketCount > 0) {
 8             try {
 9                 Thread.sleep(50);
10             } catch (InterruptedException e) {
11                 e.printStackTrace();
12             }
13             sale();
14         }
15     }
16     
17     public synchronized void sale(){ //使用同步函数使线程间同步 18         if (ticketCount > 0) {
19             System.out.println(Thread.currentThread().getName()
20                     + "正在出售第" + (100-ticketCount+1) + "张票");
21             ticketCount --;
22             }
23     }
24 }
 1 public class SellTicketMain {
 2 
 3     public static void main(String[] args) {
 4         SynchronizedMethodThread methodThread = new SynchronizedMethodThread();
 5         Thread t1 = new Thread(methodThread, "窗口1--");
6      Thread t2 = new Thread(methodThread, "窗口2--");
7
    t1.start();
8
    t2.start();
9   }
10 }

 

代码分析:

在(1)同步代码块中,咱们建立了Object对象并将其当作锁来使用,那么在同步函数中,咱们没法本身传入锁,那是否是同步函数中有默认的锁呢?没错,同步函数中默认使用的锁是当前类的对象,即this。下面代码证实了同步函数中使用的锁是this:

 1 public class VerifySynchronizedThread implements Runnable {
 2 
 3     private static int trainCount = 100;
 4     private Object obj = new Object();
 5     public boolean flag = true;
 6 
 7     @Override
 8     public void run() {
 9         if (flag) {
10             // 执行同步代码块this锁
11             while (trainCount > 0) {
12                 synchronized (this) {
13                     if (trainCount > 0) {
14                         try {
15                             Thread.sleep(50);
16                         } catch (Exception e) {
17                             e.printStackTrace();
18                         }
19                         System.out.println(Thread.currentThread().getName() + ",出售第" + (100 - trainCount + 1) + "票");
20                         trainCount--;
21                     }
22                 }
23 
24             }
25         } else {
26             // 执行同步函数
27             while (trainCount > 0) {
28                 sale();
29             }
30         }
31 
32     }
33 
34     public synchronized void sale() { // 同步函数 35         if (trainCount > 0) {
36             try {
37                 Thread.sleep(50);
38             } catch (Exception e) {
39                 e.printStackTrace();
40             }
41             System.out.println(Thread.currentThread().getName() + ",出售第" + (100 - trainCount + 1) + "票");
42             trainCount--;
43         }
44     }
45 }
 1 public class Main {
 2 
 3     public static void main(String[] args) {    
 4         VerifySynchronizedThread thread = new VerifySynchronizedThread();
 5 
 6         Thread t1 = new Thread(thread, "窗口1--");
 7         Thread t2 = new Thread(thread, "窗口2--");
 8         t1.start();
 9         try {
10             Thread.sleep(40);
11         } catch (InterruptedException e) {
12             e.printStackTrace();
13         }
14         thread.flag = false;
15         t2.start();
16     }
17 }

 

代码分析:

咱们经过flag控制,让t1执行同步代码块,让t2执行同步函数,因为两条线程同时操做trainCount这个成员变量,所以可能会引起线程安全问题,按照咱们前边的描述,使用synchronized让线程同步,可是如今t1使用的是同步代码块,t2使用的是同步函数,按照前边的分析若是他们俩使用的是通一把锁,那么当一个线程进入synchronized中的代码时,另外一个线程是进不去的,从而解决线程安全问题。咱们既然是在验证同步函数使用的是this锁,所以咱们将同步代码块中也使用this,通过几回反复的运行,并无发现数据错误,也就说明了同步函数使用的是this锁,为了更加准确,咱们再将同步代码块中的锁换成obj试一下,发现换成obj后出现了错误数据,所以咱们证实了同步函数使用的是this锁。 

 

(3)静态同步函数

 1 public class StaticSynchronizedThread implements Runnable {
 2     private static int ticketCount = 100;
 3     public boolean flag = true;
 4     @Override
 5     public void run() {
 6         if (flag) {
 7             while (ticketCount > 0) {
 8                 synchronized (StaticSynchronizedThread.class) { // 同步代码块
 9                     if (ticketCount > 0) {
10                         try {
11                             Thread.sleep(50);
12                         } catch (Exception e) {
13                             e.printStackTrace();
14                         }
15                         System.out.println(Thread.currentThread().getName() + ",出售第" + (100 - ticketCount + 1) + "票");
16                         ticketCount--;
17                     }
18                 }
19 
20             }
21         } else {
22             // 执行静态同步函数
23             while (ticketCount > 0) {
24                 sale();
25             }
26         }
27 
28     }
29     public static synchronized void sale() { //静态同步函数
30         if (ticketCount > 0) {
31             try {
32                 Thread.sleep(50);
33             } catch (Exception e) {
34                 e.printStackTrace();
35             }
36             System.out.println(Thread.currentThread().getName() + ",出售第" + (100 - ticketCount + 1) + "票");
37             ticketCount--;
38         }
39     }
40 }
 1 public class Main {
 2 
 3     public static void main(String[] args) {
 4         StaticSynchronizedThread thread = new StaticSynchronizedThread();
 5 
 6         Thread t1 = new Thread(thread, "窗口1--");
 7         Thread t2 = new Thread(thread, "窗口2--");
 8         t1.start();
 9         try {
10             Thread.sleep(40);
11         } catch (InterruptedException e) {
12             e.printStackTrace();
13         }
14         thread.flag = false;
15         t2.start();
16     }
17 }

代码分析:

静态同步函数的形式也比较简单,仅仅是将同步函数写成静态的形式。可是须要注意的是,静态同步函数使用的锁不是this,它也不可能使用this,由于咱们知道静态函数要先于对象加载,也就是说当静态同步函数被加载的时候,本类的对象即this在内存中还不存在,所以更不可能使用它。这里静态同步函数使用的锁实际上是本类的字节码文件,即StaticSynchronizedThread.class。一样还使用以前的代码,将同步代码块的锁设为StaticSynchronizedThread.class来验证,运行发现不会出现错误数据,当换成其余锁时,便会出现错误数据。

 3.对于synchronized的总结

  • 要使用synchronized,必需要有两个以上的线程。单线程使用没有意义,还会使效率下降。
  • 要使用synchronized,线程之间须要发生同步,不须要同步的不必使用synchronized,例如只读数据。
  • 使用synchronized的缺点是效率很是低,由于加锁、释放锁和释放锁后争抢CPU执行权的操做都很耗费资源。
相关文章
相关标签/搜索