1、通过同步机制,来解决线程的安全问题

1.1、方式一:同步代码块

synchronized(同步监视器){
//需要同步的代码
}
说明:

  • 操作共享数据的代码,即为需要被同步的代码 ——>不能包含代码多了,也不能包含代码少了
  • 共享数据:多线程共同操作的变量。比如 ticket就是共享数据
  • 同步监视器:俗称 锁,任何一个类的对象,都可以来充当锁
    • 要求:多个线程必须要共用同一把锁

补充:在实现Runnable接口创建多线程的方式中,我们可以考虑使用this充当同步监视器。

  1. package com.haiyang.java;
  2. /**
  3. * @author 杨磊
  4. * @create 2021-09-24 22:35
  5. */
  6. class Window1 implements Runnable{
  7. private int ticket = 100;
  8. private Object obj = new Object();
  9. @Override
  10. public void run() {
  11. while (true) {
  12. synchronized (this){
  13. if (ticket > 0) {
  14. try {
  15. Thread.sleep(100);
  16. } catch (InterruptedException e) {
  17. e.printStackTrace();
  18. }
  19. System.out.println(Thread.currentThread().getName() + "卖票,票号为:" + ticket);
  20. ticket--;
  21. } else {
  22. System.out.println("票已经卖完了");
  23. break;
  24. }
  25. }
  26. }
  27. }
  28. }
  29. public class WindowTest1 {
  30. public static void main(String[] args) {
  31. Window1 w1 = new Window1();
  32. Thread t1 = new Thread(w1);
  33. Thread t2 = new Thread(w1);
  34. Thread t3 = new Thread(w1);
  35. t1.setName("窗口一");
  36. t2.setName("窗口二");
  37. t3.setName("窗口三");
  38. t1.start();
  39. t2.start();
  40. t3.start();
  41. }
  42. }
  1. /**
  2. * 使用同步代码块解决继承Thread类的方式的线程安全问题
  3. *
  4. * 例子:创建三个c窗口卖票,总票数为100张
  5. */
  6. class Windows extends Thread{
  7. private static int ticket = 100;
  8. private static Object obj = new Object();
  9. @Override
  10. public void run() {
  11. while(true){
  12. //正确的
  13. // synchronized (obj) {
  14. synchronized (Windows.class){ //Class clazz = Windows.class
  15. //错误的,因为此时this表示的是t1,t2,t3三个对象
  16. // synchronized (this) {
  17. if (ticket > 0) {
  18. try {
  19. Thread.sleep(100);
  20. } catch (InterruptedException e) {
  21. e.printStackTrace();
  22. }
  23. System.out.println(getName() + ":卖票,票号为: " + ticket);
  24. ticket--;
  25. } else {
  26. break;
  27. }
  28. }
  29. }
  30. }
  31. }
  32. public class WindowsTest2 {
  33. public static void main(String[] args) {
  34. Windows t1 = new Windows();
  35. Windows t2 = new Windows();
  36. Windows t3 = new Windows();
  37. t1.setName("窗口1");
  38. t2.setName("窗口2");
  39. t3.setName("窗口3");
  40. t1.start();
  41. t2.start();
  42. t3.start();
  43. }
  44. }

1.2、方式二:同步方法

如果操作共享数据的代码完整的声明在一个方法中,我们不妨将此方法声明同步的

关于同步方法的总结

  • 同步方法仍然涉及到同步监视器,只是不需要我们显示的声明
  • 非静态的同步方法,同步监视器是 :this
  • 静态的同步方法,同步监视器是 : 当前类本身

同步的方式,解决了线程的安全问题。—-好处
操作同步代码时,只能有一个线程参与,其他线程等待。相当于是一个单线程的过程,效率低。—-局限性

  1. package com.haiyang.java;
  2. /**
  3. * 使用同步方法解决实现Runnable接口的线程安全问题
  4. * @author 杨磊
  5. * @create 2021-09-25 12:47
  6. */
  7. public class WindowTest3 {
  8. public static void main(String[] args) {
  9. Window2 w2 = new Window2();
  10. Thread t1 = new Thread(w2);
  11. Thread t2 = new Thread(w2);
  12. Thread t3 = new Thread(w2);
  13. t1.setName("窗口一");
  14. t2.setName("窗口二");
  15. t3.setName("窗口三");
  16. t1.start();
  17. t2.start();
  18. t3.start();
  19. }
  20. }
  21. class Window2 implements Runnable{
  22. private int ticket = 100;
  23. boolean flag = true;
  24. @Override
  25. public void run() {
  26. while (flag){
  27. show();
  28. }
  29. }
  30. private synchronized void show() { //同步监视器是 this
  31. if (ticket > 0) {
  32. try {
  33. Thread.sleep(100);
  34. } catch (InterruptedException e) {
  35. e.printStackTrace();
  36. }
  37. System.out.println(Thread.currentThread().getName() + "买票,票号为" + ticket);
  38. ticket--;
  39. }else {
  40. flag = false;
  41. }
  42. }
  43. }
  1. package com.haiyang.java;
  2. /**
  3. * 使用同步方法完成对继承Thread类的线程安全问题
  4. * @author 杨磊
  5. * @create 2021-09-25 12:58
  6. *
  7. *
  8. *关于同步方法的总结
  9. * 1、同步方法仍然涉及到同步监视器,只是不需要我们显示的声明
  10. * 2、非静态的同步方法,同步监视器是 :this
  11. * 静态的同步方法,同步监视器是 : 当前类本身
  12. *
  13. */
  14. public class WindowTest4 {
  15. public static void main(String[] args) {
  16. Window4 w1 = new Window4();
  17. Window4 w2 = new Window4();
  18. Window4 w3 = new Window4();
  19. w1.setName("窗口一");
  20. w2.setName("窗口二");
  21. w3.setName("窗口三");
  22. w1.start();
  23. w2.start();
  24. w3.start();
  25. }
  26. }
  27. class Window4 extends Thread{
  28. private static int ticket = 100;
  29. static boolean flag = true;
  30. @Override
  31. public void run() {
  32. while (flag){
  33. show();
  34. }
  35. }
  36. private static synchronized void show() { //同步监视器 Window4.class
  37. if (ticket > 0){
  38. try {
  39. Thread.sleep(100);
  40. } catch (InterruptedException e) {
  41. e.printStackTrace();
  42. }
  43. System.out.println(Thread.currentThread().getName() + "买票,票号为" + ticket);
  44. ticket--;
  45. }else {
  46. flag = false;
  47. }
  48. }
  49. }

1.3、方式三:lock锁

  • 实例化ReentrantLock对象
  • 调用锁定方法lock()
  • 调用解锁方法:unlock()

注意:如果同步代码有异常,要将unlock()写入finally语句块

  1. import java.util.concurrent.locks.ReentrantLock;
  2. /**
  3. * 解决线程安全的方式三: lock锁 ---JDK5新增
  4. * @author 杨磊
  5. * @create 2021-09-25 15:20
  6. */
  7. public class LockTest {
  8. public static void main(String[] args) {
  9. Window w1 = new Window();
  10. Thread t1 = new Thread(w1);
  11. Thread t2 = new Thread(w1);
  12. Thread t3 = new Thread(w1);
  13. t1.setName("窗口一");
  14. t2.setName("窗口二");
  15. t3.setName("窗口三");
  16. t1.start();
  17. t2.start();
  18. t3.start();
  19. }
  20. }
  21. class Window implements Runnable{
  22. private int ticket = 100;
  23. //1、实例化 ReentrantLock
  24. private ReentrantLock lock = new ReentrantLock();
  25. @Override
  26. public void run() {
  27. while (true){
  28. try {
  29. //2、调用锁定方法lock()
  30. lock.lock();
  31. if (ticket > 0){
  32. Thread.sleep(100);
  33. System.out.println(Thread.currentThread().getName() + "卖票,票号为:" + ticket);
  34. ticket--;
  35. }else {
  36. System.out.println("票已经卖完了");
  37. break;
  38. }
  39. } catch (InterruptedException e) {
  40. e.printStackTrace();
  41. } finally {
  42. //3、调用解锁方法 : unlock()
  43. lock.unlock();
  44. }
  45. }
  46. }
  47. }

1.4、面试题:synchronized 与 Lock的异同?

相同:

  • 二者都可以解决线程安全问题

不同:

  • synchronized机制在执行完相应的同步代码以后,自动的释放同步监视器
  • Lock需要手动的启动同步(lock()),同时结束同步也需要手动的实现(unlock())

    1.5、优先使用顺序:

    Lock —-> 同步代码块(已经进入了方法体,分配了相应资源)——> 同步方法(在方法体之外) ```java package com.haiyang.exer;

import java.util.concurrent.locks.ReentrantLock;

/**

  • @author 杨磊
  • @create 2021-09-25 15:40 */ public class AccountTest { public static void main(String[] args) {

    1. Account acct = new Account(0);
    2. Customer c1 = new Customer(acct);
    3. Customer c2 = new Customer(acct);
    4. c1.setName("甲用户");
    5. c2.setName("乙用户");
    6. c1.start();
    7. c2.start();

    } } class Account { private static ReentrantLock lock = new ReentrantLock(); private double balance;

    public Account(double balance) {

    1. this.balance = balance;

    }

    public synchronized void deposit(double amt){

    1. if (amt > 0){
    2. balance += amt;
    3. try {
    4. Thread.sleep(1000);
    5. } catch (InterruptedException e) {
    6. e.printStackTrace();
    7. }
    8. System.out.println(Thread.currentThread().getName() + "存钱成功,余额为" + balance);
    9. }

    } } class Customer extends Thread{ private Account acct;

    public Customer(Account acct) {

    1. this.acct = acct;

    }

    @Override public void run() {

    1. for (int i = 0; i < 3; i++) {
    2. acct.deposit(1000);
    3. }

    } }

  1. <a name="RCmLi"></a>
  2. # 2、线程安全的单例模式之懒汉式
  3. ```java
  4. /**
  5. * 使用同步机制将单例模式中的懒汉式改写为线程安全的
  6. */
  7. public class BankTest {
  8. }
  9. class Bank{
  10. private Bank(){}
  11. private static Bank instance = null;
  12. public static Bank getInstance(){
  13. //方式一:效率稍差
  14. //快捷键:Alt+Shift+Z
  15. // synchronized (Bank.class) {
  16. // if(instance == null){
  17. // instance = new Bank();
  18. // }
  19. // return instance;
  20. // }
  21. //方式二:效率较高
  22. if(instance == null) {
  23. synchronized (Bank.class) {
  24. if (instance == null) {
  25. instance = new Bank();
  26. }
  27. }
  28. }
  29. return instance;
  30. }
  31. }

3、死锁的问题

  • 死锁的理解:不同的线程分别占用对方需要的同步资源不放弃,都在等待对方放弃自己需要的同步资源,就形成了线程的死锁
  • 出现死锁后,不会出现异常,不会出现提示,只是所有的线程都处于阻塞状态,无法继续
  • 我们使用同步时,要避免出现死锁。 ```java /**

    • 演示线程的死锁 *
    • 1.死锁的理解:不同的线程分别占用对方需要的同步资源不放弃,
    • 都在等待对方放弃自己需要的同步资源,就形成了线程的死锁
    • 2.说明:
    • 》出现死锁后,不会出现异常,不会出现提示,只是所有的线程都处于阻塞状态,无法继续
    • 》我们使用同步时,要避免出现死锁。 */ public class ThreadTest { public static void main(String[] args) {

      StringBuffer s1 = new StringBuffer(); StringBuffer s2 = new StringBuffer();

      new Thread(){ @Override public void run() {

      1. synchronized (s1){
      2. s1.append("a");
      3. s2.append("1");
      4. try {
      5. Thread.sleep(100);
      6. } catch (InterruptedException e) {
      7. e.printStackTrace();
      8. }
      9. synchronized (s2){
      10. s1.append("b");
      11. s2.append("2");
      12. System.out.println(s1);
      13. System.out.println(s2);
      14. }
      15. }

      } }.start();

      new Thread(new Runnable() { @Override public void run() {

      1. synchronized (s2){
      2. s1.append("c");
      3. s2.append("3");
      4. try {
      5. Thread.sleep(100);
      6. } catch (InterruptedException e) {
      7. e.printStackTrace();
      8. }
      9. synchronized (s1){
      10. s1.append("d");
      11. s2.append("4");
      12. System.out.println(s1);
      13. System.out.println(s2);
      14. }
      15. }

      } }).start(); } }

  1. ```java
  2. class A {
  3. public synchronized void foo(B b) {
  4. System.out.println("当前线程名: " + Thread.currentThread().getName()
  5. + " 进入了A实例的foo方法"); // ①
  6. try {
  7. Thread.sleep(200);
  8. } catch (InterruptedException ex) {
  9. ex.printStackTrace();
  10. }
  11. System.out.println("当前线程名: " + Thread.currentThread().getName()
  12. + " 企图调用B实例的last方法"); // ③
  13. b.last();
  14. }
  15. public synchronized void last() {
  16. System.out.println("进入了A类的last方法内部");
  17. }
  18. }
  19. class B {
  20. public synchronized void bar(A a) {
  21. System.out.println("当前线程名: " + Thread.currentThread().getName()
  22. + " 进入了B实例的bar方法"); // ②
  23. try {
  24. Thread.sleep(200);
  25. } catch (InterruptedException ex) {
  26. ex.printStackTrace();
  27. }
  28. System.out.println("当前线程名: " + Thread.currentThread().getName()
  29. + " 企图调用A实例的last方法"); // ④
  30. a.last();
  31. }
  32. public synchronized void last() {
  33. System.out.println("进入了B类的last方法内部");
  34. }
  35. }
  36. public class DeadLock implements Runnable {
  37. A a = new A();
  38. B b = new B();
  39. public void init() {
  40. Thread.currentThread().setName("主线程");
  41. // 调用a对象的foo方法
  42. a.foo(b);
  43. System.out.println("进入了主线程之后");
  44. }
  45. public void run() {
  46. Thread.currentThread().setName("副线程");
  47. // 调用b对象的bar方法
  48. b.bar(a);
  49. System.out.println("进入了副线程之后");
  50. }
  51. public static void main(String[] args) {
  52. DeadLock dl = new DeadLock();
  53. new Thread(dl).start();
  54. dl.init();
  55. }
  56. }