由于我们在使用多线程的过程中会出现线程安全的问题的。然后我们可以通过这几个方案来进行解决线程安全问题。
synchronized监视锁:
方案一:监视锁
synchronized关键字有以下几个特征:
1、互斥性
当程序进入到synchronized关键字修饰的代码块时,这个时候就被加锁了。当执行出该代码块时就解锁了。当我们的一个线程对该对象加锁后,另外一个线程执行到该对象时,就会阻塞等待。
我们可以简单的理解为:这里以上厕所为一个例子,当有人(一个线程)进入厕所(执行到监视的锁对象)进行解决的时候,就要进行锁门(也就是加锁),后面有人(另一个线程)也要来上厕所(执行到该对象)的时候就需要在门外等待(阻塞对待),直到里面的人出来了(解锁),再进去(再获取进行加锁)。
2、 可重入性
synchronized是可重入锁。可重入锁就是同一个线程对于同一个对象同时加锁两次或者两次以上。如果没有问题,没有产生bug,就是可重入锁。如果不能正常的运行,就是不可重入的锁了。
synchronized的用法:
1、synchronized修饰普通方法:
1. public synchronized void increase(){ 2. sum++; 3. }
此处的锁对象是this
2、 synchronized修饰静态方法
1. public class SynchronizedDemo1 { 2. private int sum=0; 3. 4. public synchronized static void fun(int a){ 5. a++; 6. } 7. 8. }
此处的锁对象是SynchronizedDemo1的类的对象。
3、synchronized修饰代码块
1. public void increase(){ 2. synchronized(this){ 3. sum++; 4. } 5. 6. }
此时的锁对象依然是this。
注意:在使用synchronized的时候一定要注意锁的对象是谁,一般有this,类名.class(类对象)和普通对象变量。
volatile :
方案二:volatile关键字
首先我们需要明确:volatile是为了解决内存可见性问题用到的关键字,其中文意思就是易变的,这是在提醒编译器,遇到这个关键字就不要擅作主张了。
volatile的用法:
修饰变量。
比如:
volatile public int flag=0;
这里举个例子:还是前面总结内存可见性的例子:
1. package MySynchronized; 2. 3. import java.util.Scanner; 4. 5. class MyCount{ 6. volatile public int flag=0; 7. } 8. 9. public class SynchronizedDemo2 { 10. public static void main(String[] args) { 11. 12. MyCount myCount=new MyCount(); 13. 14. Thread t1=new Thread(()->{ 15. while(myCount.flag==0){ 16. 17. } 18. System.out.println("循环结束!"); 19. 20. }); 21. 22. Thread t2=new Thread(()->{ 23. Scanner scan=new Scanner(System.in); 24. System.out.println("输入你要输入的数字:"); 25. int result=scan.nextInt(); 26. myCount.flag=result; 27. 28. }); 29. t1.start(); 30. t2.start(); 31. } 32. }
如果我们不加上述的volatile,就会出现这个情况:
只有加上volatile才会正常的显示我们想要的结果:
volatile和synchronized的区别:
synchronized保证原子性,volatile不保证原子性,保证的是内存可见性
wait和notify:
方案三:wait和notify
首先我们要知道线程在执行的过程中是随机调度的,抢占式执行的,所以我们是不知道它们执行的顺序的。但是我们在实际的开发中有些时候我们希望各个线程按照一定的顺序来进行执行,此时我们就需要用到我们的wait和notify了。
wait要做的事情:使当前执行代码的线程阻塞等待,释放锁,等待唤醒后重新获取这个锁。
notify要做的事情:唤醒等待的线程
wait被唤醒的条件:
1、其他线程调用该对象的notify方法
2、wait等待时间超时
3、wait抛出InterruptedException异常(别的线程调用了该等待线程的interrupted方法)
这里我们举一个如何使用3个线程,按照顺序打印ABC的例子:
1. package MySynchronized; 2. 3. public class SynchronizedDemo3 { 4. public static void main(String[] args) { 5. 6. Object loker1=new Object(); 7. Object loker2=new Object(); 8. Thread t1=new Thread(()->{ 9. System.out.println("A"); 10. //A打印完进行通知t2 11. synchronized (loker1){ 12. loker1.notify(); 13. } 14. }); 15. 16. Thread t2=new Thread(()->{ 17. //B在A前面,所以让t2阻塞等待 18. synchronized (loker1){ 19. try { 20. loker1.wait(); 21. } catch (InterruptedException e) { 22. throw new RuntimeException(e); 23. } 24. } 25. System.out.println("B"); 26. //打印B之后,进行通知 27. synchronized (loker2){ 28. loker2.notify(); 29. } 30. }); 31. //为保证ABC的顺序,所以我们要保证C要在B的后面 32. //所以我们在打印C之前,要让它阻塞等待 33. Thread t3=new Thread(()->{ 34. //阻塞等待,等待t2线程的通知 35. synchronized (loker2){ 36. try { 37. loker2.wait(); 38. } catch (InterruptedException e) { 39. throw new RuntimeException(e); 40. } 41. } 42. System.out.println("C"); 43. System.out.println("打印正确!"); 44. }); 45. //这里有个细节要注意:如果t1比t2先执行了,那么t1中的通知就失去了作用,也就是说t2还没有排队等待呢,t1已经通知了,这个时候通知就没有用了 46. //等到t2等待的时候,t1就没有等二次通知的机会了,所以要让t2先执行。 47. t2.start(); 48. try { 49. Thread.sleep(100); 50. } catch (InterruptedException e) { 51. throw new RuntimeException(e); 52. } 53. t1.start(); 54. t3.start(); 55. } 56. }
特别要注意:wait和notify的对象要要一致!
notifyAll方法:
与notify功能类似,都是通知同对象的线程。但是略有区别的是,notify是随机唤醒一个同对象所在的阻塞线程(假设有多个线程),notifyAll是唤醒所有的,剩下的进行所竞争。(风险大,不咋用,上面的wait和notify是重点!!!!)
wait和sleep的区别(面试):
其实理论上 wait 和 sleep 完全是没有可比性的,因为一个是用于线程之间的通信的,一个是让线程阻塞一段时间,唯一的相同点就是都可以让线程放弃执行一段时间。
区别:1、wait需要搭配synchronized使用 sleep不需要
2、wait是Object的方法sleep是Thread 的静态方法