synchronized浅析

一、synchronized原理及锁优化

是利用锁的机制来实现同步的

锁机制有如下两种特性

1、互斥性(原子性)

 即在同一时间只允许一个线程持有某个对象锁,通过这种特性来实现多线程中的协调机制,这样在同一时间只有一个线程对需同步的代码块进行访问。

2、互斥性(原子性)

 必须确保在锁被释放之前,对共享变量所做的修改,对于随后获得该锁的另一个线程是可见的(即在获得锁时应获得最新共享变量的值),否则另一个线程可能是在本地缓存的某个副本上继续操作从而引起不一致。 

二、synchronized在静态方法与普通方法中的区别

1、非静态方法被synchroized修饰后

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
private static int num=0;
  private synchronized void printNum(String tag){
    try {
    if(tag.equals("a")){
      num=100;
      System.out.println("tag a,num set over!");
      Thread.sleep(1000);//休眠1秒
    }else{
      num=200;
      System.out.println("tag b,num set over!");
    }
  } catch (InterruptedException e) {
    e.printStackTrace();
  }
  System.out.println("tag :" +tag+", num="+num);
  }
  public static void main(String[] args) {
    MyThread02 my1=new MyThread02();
    MyThread02 my2=new MyThread02();
    //匿名内部类方式启动线程
    new Thread(()->my1.printNum("a")).start();
    new Thread(()->my2.printNum("b")).start();
  }

执行结果为:
tag a,num set over!
tag b,num set over!
tag :b,num=200
tag :a,num=200

2、静态方法被synchroized修饰后

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
private static int num=0;
  private synchronized static void printNum(String tag){
    try {
    if(tag.equals("a")){
      num=100;
      System.out.println("tag a,num set over!");
      Thread.sleep(1000);//休眠1秒
    }else{
      num=200;
      System.out.println("tag b,num set over!");
    }
  } catch (InterruptedException e) {
    e.printStackTrace();
  }
  System.out.println("tag :" +tag+", num="+num);
  }
  public static void main(String[] args) {
    MyThread02 my1=new MyThread02();
    MyThread02 my2=new MyThread02();
    //匿名内部类方式启动线程
    new Thread(()->my1.printNum("a")).start();
    new Thread(()->my2.printNum("b")).start();
  }

执行结果为:
tag a,num set over!
tag :a,num=100
tag b,num set over!
tag :b,num=200

通过上述两方法比较后,可得出:
1、在非静态方法上使用synchroized后,两个线程之间同时执行了printNum方法,a线程修改了num为100,但是没有直接打印而是休眠了1秒,b线程在a线程修改值后又修改了一次num为200,并且num是静态变量,所以最终打印出来的值均为200,若num是普通变量,那么num值在a线程中打印出来的是100,在线程b中打印出来的就是200,两线程之间无联系,互不干扰,可以任意执行自己对象的锁。
2、在静态方法上使用synchroized后,两个线程都执行了printNum方法,但是打印是有顺序的,a线程修改值后,等待了1秒,打印出num值,然后再执行b线程,最后打印出num值,说明了静态方法上使用synchroized后,会锁定整个类,多个线程使用的是同一把锁,哪个线程优先执行代码块,该线程就持有该类的锁,其他线程就无法使用该对象。

总结:
普通方法上的锁是锁住这个对象的,静态方法上的锁是锁住这个类的。

注:
cmd进入class目录,进行反编译:javap -verbose SynchronizedDemo ,得到以下反编译的内容
同步方法,JVM使用ACC_SYNCHRONIZED标识来实现。即JVM通过在方法访问标识符(flags)中加入ACC_SYNCHRONIZED来实现同步功能。
同步代码块,JVM使用monitorenter和monitorexit两个指令实现同步。即JVM为代码块的前后真正生成了两个字节码指令来实现同步功能的。

三、synchronized和lock区别

synchronized和ReentrantLock两者都是可重入锁
两者区别在:
1、首先synchronized是在jvm层面的锁,属于java内置的关键字,而Lock是java代码控制的锁;
2、synchronized无法判断是否获取到锁,而Lock可以从java提供的interface判断是否获取到锁;
3、synchronized会自动释放锁(线程执行完同步代码会释放锁或者在执行过程中发生异常会释放锁),而Lock需在finally中手工释放锁(unlock()方法释放锁),否则容易造成线程死锁;
4、synchronized修饰的竞争到的线程会执行,未竞争到的线程会阻塞,而Lock锁中,未竞争到锁的线程不一定会继续等待,可能直接结束了。
5、synchronized的锁可重入、不可中断、非公平,而Lock锁可重入、可判断、可公平(两者皆可)
6、Lock锁适合大量同步的代码的同步问题,synchronized锁适合代码少量的同步问题。

注:可重入锁与不可重入锁
当一个线程获得当前实例的锁,并且进入了方法A,该线程在方法A没有释放该锁的时候,准备进入方法B,
若此刻是不可重入锁:在方法A释放锁之前,不可以再次进入方法B
若此刻是可重入锁:在方法A释放该锁之前可以再次进入方法B
可重入性简单的说就是:锁的分配机制基于线程的分配,而不是基于方法调用的分配。

四、延伸问题:怎样实现所有线程在等待某个事件发生后才会执行

解决方法:1.闭锁CountDownLatch 2.阻塞队列BlockingQueue 3.信号量Semaphore 4.栅栏CyclicBarrier

下面以闭锁CountDownLatch为例:
闭锁是典型的等待事件发生的同步工具类,将闭锁的初始值设置1,所有线程调用await方法等待,当事件发生时调用countDown将闭锁值减为0,则所有await等待闭锁的线程得以继续执行。
等待所有线程完成操作时,打印num最终的值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import java.util.concurrent.CountDownLatch;

public class TestCountDownLatch {
static final int T = 5;
static final CountDownLatch latch = new CountDownLatch(T);
static int num = 0;

public static void main(String[] args) throws InterruptedException {
TestCountDownLatch td = new TestCountDownLatch();
for (int i = 0; i < 5; i++) {
new Thread(() -> td.test()).start();
}
// 多线程运行结束一直等待
latch.await();
System.out.println("num:" + num);
}

public void test() {
for (int i = 0; i < 100; i++) {
num++;
}
latch.countDown();
}
}