synchronized 怎么使用

文章目录

  • 前言
  • 通过一系列的例子,了解synchronized 使用
  • 总结
前言

上一篇了解了synchronized,但是呢光懂理论没用,关键是要会用,用demo的形式写一下各种使用场景,这么一来,就会对synchronized的使用更加透彻。

通过一系列的例子,了解synchronized 使用
1、synchronized 都会在哪些地方使用?

修饰一个代码块,作用的对象是调用这个代码块的对象。
修饰一个方法,作用的对象是调用这个方法的对象。
修饰一个静态方法,作用是这个类的所有对象。
修饰一个类,作用的是这个类的所有对象

2、怎么使用同步代码块?

两个线程访问同一个对象代码块,只有一个线程执行,另外一个线程被阻塞。
比如:

class MyRunnable implements Runnable{
  private static int count;//定义一个变量count;
  public MyRunnable(){
    count = 0;
  }
  @Override public void run() {
    synchronized (this){//同步代码块,锁住的是MyRunnable这个实例对象
      for (int i = 0; i < 5; i++){
        try {
          System.out.println(Thread.currentThread().getName() + ":" + (count++));
          Thread.sleep(100);
        } catch (InterruptedException e) {
          e.printStackTrace();
        }
      }

    }
  }
  public int getCount(){
    return count;
  }
}

输出的日志如下
2022-12-31 14:36:03.266 8015-8053/com.ssz.mvvmdemo I/System.out: 线程1:0
2022-12-31 14:36:03.366 8015-8053/com.ssz.mvvmdemo I/System.out: 线程1:1
2022-12-31 14:36:03.467 8015-8053/com.ssz.mvvmdemo I/System.out: 线程1:2
2022-12-31 14:36:03.567 8015-8053/com.ssz.mvvmdemo I/System.out: 线程1:3
2022-12-31 14:36:03.667 8015-8053/com.ssz.mvvmdemo I/System.out: 线程1:4
2022-12-31 14:36:03.768 8015-8054/com.ssz.mvvmdemo I/System.out: 线程2:5
2022-12-31 14:36:03.868 8015-8054/com.ssz.mvvmdemo I/System.out: 线程2:6
2022-12-31 14:36:03.968 8015-8054/com.ssz.mvvmdemo I/System.out: 线程2:7
2022-12-31 14:36:04.068 8015-8054/com.ssz.mvvmdemo I/System.out: 线程2:8
2022-12-31 14:36:04.169 8015-8054/com.ssz.mvvmdemo I/System.out: 线程2:9

可以看到先执行线程1,再执行线程2,达到了同步目的。

2.1 这个时候,我们将执行的对象换一下

比如:

    MyRunnable myRunnable = new MyRunnable();
    MyRunnable myRunnable2 = new MyRunnable();
    Thread thread1 = new Thread(myRunnable, "线程1");
    Thread thread2 = new Thread(myRunnable2, "线程2");
    thread1.start();
    thread2.start();

执行结果:
2022-12-31 14:42:33.957 8237-8273/com.ssz.mvvmdemo I/System.out: 线程1:0
2022-12-31 14:42:33.957 8237-8274/com.ssz.mvvmdemo I/System.out: 线程2:1
2022-12-31 14:42:34.057 8237-8273/com.ssz.mvvmdemo I/System.out: 线程1:2
2022-12-31 14:42:34.057 8237-8274/com.ssz.mvvmdemo I/System.out: 线程2:2
2022-12-31 14:42:34.157 8237-8273/com.ssz.mvvmdemo I/System.out: 线程1:3
2022-12-31 14:42:34.157 8237-8274/com.ssz.mvvmdemo I/System.out: 线程2:3
2022-12-31 14:42:34.257 8237-8273/com.ssz.mvvmdemo I/System.out: 线程1:4
2022-12-31 14:42:34.257 8237-8274/com.ssz.mvvmdemo I/System.out: 线程2:4
2022-12-31 14:42:34.358 8237-8274/com.ssz.mvvmdemo I/System.out: 线程2:5
2022-12-31 14:42:34.358 8237-8273/com.ssz.mvvmdemo I/System.out: 线程1:5

可以看到线程1和线程2,变成随机的执行代码块,为什么呢?
因为他们作用的不是同一个对象,线程1 执行的是 myRunnable对象,
而线程2 执行的是 myRunnable2对象。所以他们互不干扰,两个线程就能同时执行。

2.2 当一个线程访问一个对象的 synchronized(this) 代码块的时候,另外一个线程还是可以访问, 该对象的没有被synchronized(this) 修饰的代码块。

比如:

class Counter implements Runnable{
  private int count;
  public Counter(){
    count = 0;
  }

  /**
   * 对count 执行自增操作
   * */
  public void countAdd(){
    synchronized (this){
      for (int i = 0; i < 5; i++){
        try {
          System.out.println(Thread.currentThread().getName()+ ":" + (count++));
          Thread.sleep(100);
        } catch (InterruptedException e) {
          e.printStackTrace();
        }
      }
    }
  }
  //没有对count 执行自增操作,只是打印
  public void printCount(){
    for(int i = 0; i < 5; i++){
      try {
        System.out.println(Thread.currentThread().getName() + "  count:" + count);
        Thread.sleep(100);
      } catch (InterruptedException e) {
        e.printStackTrace();
      }
    }
  }
  @Override public void run() {
    String threadName = Thread.currentThread().getName();
    if ("线程A".equals(threadName)){//线程A 要执行自增操作
       countAdd();
    }else if("线程B".equals(threadName)){
       printCount();
    }
  }
}

使用:
    Counter counter = new Counter();
    Thread threadA = new Thread(counter, "线程A");
    Thread threadB = new Thread(counter, "线程B");
    threadA.start();
    threadB.start();

结果:
2022-12-31 15:03:52.249 9366-9403/com.ssz.mvvmdemo I/System.out: 线程A:0
2022-12-31 15:03:52.249 9366-9404/com.ssz.mvvmdemo I/System.out: 线程B  count:1
2022-12-31 15:03:52.349 9366-9404/com.ssz.mvvmdemo I/System.out: 线程B  count:1
2022-12-31 15:03:52.349 9366-9403/com.ssz.mvvmdemo I/System.out: 线程A:1
2022-12-31 15:03:52.449 9366-9403/com.ssz.mvvmdemo I/System.out: 线程A:2
2022-12-31 15:03:52.449 9366-9404/com.ssz.mvvmdemo I/System.out: 线程B  count:2
2022-12-31 15:03:52.549 9366-9403/com.ssz.mvvmdemo I/System.out: 线程A:3
2022-12-31 15:03:52.549 9366-9404/com.ssz.mvvmdemo I/System.out: 线程B  count:3
2022-12-31 15:03:52.650 9366-9403/com.ssz.mvvmdemo I/System.out: 线程A:4
2022-12-31 15:03:52.650 9366-9404/com.ssz.mvvmdemo I/System.out: 线程B  count:5

可以看到线程A 和 线程B 他们是互不影响的,线程B还是随机执行的。
也就是说一个线程 在执行同一个对象的 synchronized(this)的代码块时候,
另外一个线程可以执行该对象的没有被synchronized(this)修饰的代码块,不会阻塞。

3、怎么给一个对象加锁呢?
class Account {
  String name;
  float amount;

  public Account(String name, float amount){
    this.name = name;
    this.amount = amount;
  }

  /**
   * 存钱
   * */
  public void save(float money){
    amount += money;
    try {
      Thread.sleep(100);
    } catch (InterruptedException e) {
      e.printStackTrace();
    }
  }

  /**
   * 取钱
   * */
  public void out(float money){
    amount -= money;
    try {
      Thread.sleep(100);
    } catch (InterruptedException e) {
      e.printStackTrace();
    }
  }


  public float getAmount(){
    return amount;
  }
}

class AccountOperator implements Runnable{
  private Account account;
  public AccountOperator(Account account){
    this.account = account;
  }
  @Override public void run() {
    synchronized (account){//对这个对象进行加锁操作
      account.save(500);//存钱500;
      account.out(500);//取钱500;
      System.out.println(Thread.currentThread().getName() + ":" + account.amount);
    }
  }
}

使用:
    Account account = new Account("ssz", 10000.0f);
    AccountOperator accountOperator = new AccountOperator(account);
    final int THREAD_NUM = 10;
    //创建5个线程去随机执行。
    Thread[] threads = new Thread[THREAD_NUM];
    for (int i = 0; i < THREAD_NUM; i++){
      threads[i] = new Thread(accountOperator, "Thread" + i);
      threads[i].start();
    }



结果:
2022-12-31 15:31:09.675 11884-11927/com.ssz.mvvmdemo I/System.out: Thread0:10000.0
2022-12-31 15:31:09.875 11884-11928/com.ssz.mvvmdemo I/System.out: Thread1:10000.0
2022-12-31 15:31:10.076 11884-11930/com.ssz.mvvmdemo I/System.out: Thread2:10000.0
2022-12-31 15:31:10.276 11884-11931/com.ssz.mvvmdemo I/System.out: Thread3:10000.0
2022-12-31 15:31:10.477 11884-11934/com.ssz.mvvmdemo I/System.out: Thread5:10000.0
2022-12-31 15:31:10.677 11884-11932/com.ssz.mvvmdemo I/System.out: Thread4:10000.0
2022-12-31 15:31:10.878 11884-11936/com.ssz.mvvmdemo I/System.out: Thread7:10000.0
2022-12-31 15:31:11.078 11884-11937/com.ssz.mvvmdemo I/System.out: Thread8:10000.0
2022-12-31 15:31:11.279 11884-11938/com.ssz.mvvmdemo I/System.out: Thread9:10000.0
2022-12-31 15:31:11.480 11884-11935/com.ssz.mvvmdemo I/System.out: Thread6:10000.0

可以看到线程是随机的执行,但是呢,对于存钱取钱的操作仍然是不受影响的,因为这块是进行了同步存取操作,是对Account进行了加锁操作,保证了不受其他线程的干扰。

3.1 假如没有明确的对象作为锁,只想同步一段代码块怎么办呢?
class Test implements Runnable{
  private byte[] lock = new byte[0]//一个特殊的实例对象

  public void test(){
    synchronized(lock){
        //todo 要同步的代码
    }
  }

  public void run(){
  }

}

为什么使用byte[] 作为对象呢?
因为byte 数组创建起来比任何对象都经济。
跟Object object = new Object() 比起来的话,查看编译后的字节码发现。
byte 数组 只需要3行操作码,而 object 需要7行操作码

3.2 synchronized 在修饰方法的时候能不能继承呢?

synchronized 是不能继承的,也就是如果子类要同步,
要嘛调用父类的同步方法,要嘛就是在方法前,添加一个synchronized关键字。
比如:

class Parent {
   public synchronized void method() {   }
}
class Child extends Parent {
   public void method() { super.method();   }//调用父类的同步方法
}

class Parent {
   public synchronized void method() { }
}
class Child extends Parent {
   public synchronized void method() { } //添加关键字
}

4、怎么修饰一个静态方法呢?

当我们在修饰静态方法的时候,因为静态方法是属于类的,所以锁住的是类的所有对象。
比如:

class NewRunnable implements Runnable{
  private static int count;
  public NewRunnable(){
    count = 0;
  }

  public synchronized static void method(){  //修饰的是一个静态方法
    for (int i = 0; i < 5; i++){
      try {
        System.out.println(Thread.currentThread().getName() + ":" + (count++));
        Thread.sleep(100);
      } catch (InterruptedException e) {
        e.printStackTrace();
      }
    }
  }

  @Override public void run() {
    method();
  }
}

使用:
    NewRunnable newRunnable = new NewRunnable();
    NewRunnable newRunnable2 = new NewRunnable();
    Thread thread1 = new Thread(newRunnable, "线程1"); //newRunnable
    Thread thread2 = new Thread(newRunnable2, "线程2");//newRunnable2
    thread1.start();
    thread2.start();

结果:
2022-12-31 17:02:21.483 20792-20824/com.ssz.mvvmdemo I/System.out: 线程1:0
2022-12-31 17:02:21.583 20792-20824/com.ssz.mvvmdemo I/System.out: 线程1:1
2022-12-31 17:02:21.683 20792-20824/com.ssz.mvvmdemo I/System.out: 线程1:2
2022-12-31 17:02:21.783 20792-20824/com.ssz.mvvmdemo I/System.out: 线程1:3
2022-12-31 17:02:21.884 20792-20824/com.ssz.mvvmdemo I/System.out: 线程1:4
2022-12-31 17:02:21.984 20792-20825/com.ssz.mvvmdemo I/System.out: 线程2:5
2022-12-31 17:02:22.084 20792-20825/com.ssz.mvvmdemo I/System.out: 线程2:6
2022-12-31 17:02:22.184 20792-20825/com.ssz.mvvmdemo I/System.out: 线程2:7
2022-12-31 17:02:22.285 20792-20825/com.ssz.mvvmdemo I/System.out: 线程2:8
2022-12-31 17:02:22.385 20792-20825/com.ssz.mvvmdemo I/System.out: 线程2:9

可以看到,虽然是不同的对象,但是呢,却还是按顺序执行了。最主要是同步的是静态方法,而静态方法是属于类的,这相当于锁住了这个类,所以,就能阻止其他线程调用,只能等待第一个线程执行完,再执行第二个线程。

4.1 怎么修饰一个类呢?

我们对上面修饰静态方法做个改造,就是把synchronized 放到方法里头,然后使用 synchronized (NewRunnable.class)
比如:

class NewRunnable implements Runnable{
  private static int count;
  public NewRunnable(){
    count = 0;
  }

  public void method(){    //不是静态方法,就是普通方法
    synchronized (NewRunnable.class){ //锁的作用对象是类
      for (int i = 0; i < 5; i++){
        try {
          System.out.println(Thread.currentThread().getName() + ":" + (count++));
          Thread.sleep(100);
        } catch (InterruptedException e) {
          e.printStackTrace();
        }
      }
    }
  }

  @Override public void run() {
    method();
  }
}
结果情况1:
2022-12-31 17:10:52.476 22805-22840/com.ssz.mvvmdemo I/System.out: 线程2:0
2022-12-31 17:10:52.577 22805-22840/com.ssz.mvvmdemo I/System.out: 线程2:1
2022-12-31 17:10:52.677 22805-22840/com.ssz.mvvmdemo I/System.out: 线程2:2
2022-12-31 17:10:52.778 22805-22840/com.ssz.mvvmdemo I/System.out: 线程2:3
2022-12-31 17:10:52.878 22805-22840/com.ssz.mvvmdemo I/System.out: 线程2:4
2022-12-31 17:10:52.978 22805-22839/com.ssz.mvvmdemo I/System.out: 线程1:5
2022-12-31 17:10:53.078 22805-22839/com.ssz.mvvmdemo I/System.out: 线程1:6
2022-12-31 17:10:53.179 22805-22839/com.ssz.mvvmdemo I/System.out: 线程1:7
2022-12-31 17:10:53.279 22805-22839/com.ssz.mvvmdemo I/System.out: 线程1:8
2022-12-31 17:10:53.379 22805-22839/com.ssz.mvvmdemo I/System.out: 线程1:9

结果情况2:
2022-12-31 17:12:17.787 23462-23496/com.ssz.mvvmdemo I/System.out: 线程1:0
2022-12-31 17:12:17.887 23462-23496/com.ssz.mvvmdemo I/System.out: 线程1:1
2022-12-31 17:12:17.987 23462-23496/com.ssz.mvvmdemo I/System.out: 线程1:2
2022-12-31 17:12:18.087 23462-23496/com.ssz.mvvmdemo I/System.out: 线程1:3
2022-12-31 17:12:18.187 23462-23496/com.ssz.mvvmdemo I/System.out: 线程1:4
2022-12-31 17:12:18.288 23462-23497/com.ssz.mvvmdemo I/System.out: 线程2:5
2022-12-31 17:12:18.388 23462-23497/com.ssz.mvvmdemo I/System.out: 线程2:6
2022-12-31 17:12:18.489 23462-23497/com.ssz.mvvmdemo I/System.out: 线程2:7
2022-12-31 17:12:18.589 23462-23497/com.ssz.mvvmdemo I/System.out: 线程2:8
2022-12-31 17:12:18.689 23462-23497/com.ssz.mvvmdemo I/System.out: 线程2:9

我们看到可能先执行的线程1,也可能先执行的线程2,但是不管是谁开始执行,只要谁先开始,另外一个线程就得等着。
所以,这就保证了同步。
这里是给这个类加锁,所以他们都是共用1把锁,只有一方把锁释放,另外一方才能执行。

5 使用synchronized 有哪些注意点呢?

1、定义接口的时候,不能使用synchronized 关键字。
2、构造方法不能使用synchronized 关键字,但可以使用synchronized来同步代码块。

总结

总的来讲:
1、synchronized 关键字用在非静态的方法上,或者对象上,所获得的锁是针对对象的;
如果是一个静态方法,或者一个类,那么锁是针对类的所有对象的。
2、每个对象只有一把锁与之关联,需要等一方释放,另一方才能执行。
3、使用同步是需要系统很大开销的,所以非必要情况下不要进行同步锁操作。

你可能感兴趣的:(java,知识要点,java,jvm,开发语言)