一、简介
Synchronized一句话来解释其作用就是:能够保证同一时刻最多只有一个线程执行该段代码,以达到并发安全的效果。也就是说Synchronized就好比是一把锁,某个线程把资源锁住了之后,别人就不能使用了,只有当这个线程用完了别人才能用。
对于Synchronized关键字来说,它是并发编程中一个元老级角色,也就是说你只要学习并发编程,就必须要学习Synchronized关键字。由此可见其地位。
说了这么多,好像我们还没体验过它的威力。我们就直接举个例子,来分析一下。
public class SynTest01 implements Runnable{ static int a=0; public static void main(String[] args) throws InterruptedException { SynTest01 syn= new SynTest01(); Thread thread1 = new Thread(syn); Thread thread2 = new Thread(syn); thread1.start();thread1.join(); thread2.start();thread2.join(); System.out.println(a); } @Override public void run() { for(int i=0;i<1000;i++) { a++; } } }
上面代码要完成的功能就是,thread1对a进行增加,一直到1000,thread2再对a进行增加,一直到2000。不过如果我们运行过之后我们就会发现,最后的输出值总是小于2000,这是为什么呢?
这是因为我们在执行a++的时候其实包含了以下三个操作:
(1)线程1读取a
(2)线程1将a加1
(3)将a的值写入内存
出错原因的关键就在于第二操作和第三个操作之间,此时线程1还没来得及把a的值写入内存,线程2就把旧值读走了,这也就造成了a加了两次,但是内存中的a的值只增加了1。这也就是不同步现象。
但是如果说我们使用了Synchronized关键字之后呢?
public class SynTest01 implements Runnable{ static int a=0; Object object = new Object(); public static void main(String[] args) throws InterruptedException { SynTest01 syn= new SynTest01(); Thread thread1 = new Thread(syn); Thread thread2 = new Thread(syn); thread1.start();thread1.join(); thread2.start();thread2.join(); System.out.println(a); } @Override public void run() { synchronized (object) { for(int i=0;i<1000;i++) { a++; } }//结束 } }
现在我们使用synchronized关键字把这一块代码锁住,不管你怎么输出都是2000了,锁住之后,同一时刻只有一个线程进入。也就不会发生上面a写操作不同步的现象了。
现在相信你开始觉得synchronized关键字的确很实用,可以解决多线程中的很多问题。上面这个小例子只是带我们去简单的认识一下,下面我们就来看看其详细的使用。
二、使用
对于synchronized关键字来说,一共可以分为两类:对象锁和类锁。
我们一个一个来看如何使用。
1、对象锁
对于对象锁来说,又可以分为两个,一个是方法锁,一个是同步代码块锁。
(1)同步代码块锁
同步代码块锁主要是对代码块进行加锁,其实已经演示过了,就是上面的那个案例。不过为了保持一致我们再举一个例子。
public class SynTest01 implements Runnable { Object object = new Object(); public static void main(String[] args) throws InterruptedException { SynTest01 syn = new SynTest01(); Thread thread1 = new Thread(syn); Thread thread2 = new Thread(syn); thread1.start(); thread2.start(); //线程1和线程2只要有一个还存活就一直执行 while (thread1.isAlive() || thread2.isAlive()) {} System.out.println("main程序运行结束"); } @Override public void run() { synchronized (object) { try { System.out.println(Thread.currentThread().getName() + "线程执行了run方法"); Thread.sleep(2000); System.out.println(Thread.currentThread().getName() + "执行2秒钟之后完毕"); } catch (InterruptedException e) { e.printStackTrace(); } } } }
在这个例子中,我们使用了synchronized锁住了run方法中的代码块。表示同一时刻只有一个线程能够进入代码块。就好比是去医院挂号,前面一个人办完了业务,下一个人才开始。
在这里面我们看到,线程1和线程2使用的是同一个锁,也就是我们new的Object。如果我们让线程1和线程2每一个人拥有一个锁对象呢?
public class SynTest01 implements Runnable { Object object1 = new Object(); Object object2 = new Object(); public static void main(String[] args) throws InterruptedException { SynTest01 syn = new SynTest01(); Thread thread1 = new Thread(syn); Thread thread2 = new Thread(syn); thread1.start(); thread2.start(); //线程1和线程2只要有一个还存活就一直执行 while (thread1.isAlive() || thread2.isAlive()) {} System.out.println("main程序运行结束"); } @Override public void run() { synchronized (object1) { try { System.out.println(Thread.currentThread().getName() + "线程执行了object1"); Thread.sleep(2000); System.out.println(Thread.currentThread().getName() + "执行object1完毕"); } catch (InterruptedException e) { e.printStackTrace(); } } synchronized (object2) { try { System.out.println(Thread.currentThread().getName() + "线程执行object2"); Thread.sleep(2000); System.out.println(Thread.currentThread().getName() + "执行object2完毕"); } catch (InterruptedException e) { e.printStackTrace(); } } } }
现在线程1和线程2每个人拥有一把锁,去访问不同的方法资源。这时候会出现什么情况呢?
我们同样用一张图看一下其原理。
也就是说,相当于两个业务有俩窗口都可以办理,但是两个任务都需要排队办理。
同步代码块锁总结:
同步代码块锁主要是对代码块进行加锁,此时同一时刻只能有一个线程获取到该资源,要注意每一把锁只负责当前的代码块,其他的代码块不管。
以上就是同步代码快的使用方法。下面我们看对象锁的另外一种形式,那就是方法锁。这里的方法锁指代的是普通方法。
(2)方法锁
方法锁相比较同步代码块锁就简单很多了,就是在普通方法上添加synchronized关键字修饰即可。
public class SynTest2 implements Runnable { public static void main(String[] args) throws InterruptedException { SynTest2 syn = new SynTest2(); Thread thread1 = new Thread(syn); Thread thread2 = new Thread(syn); thread1.start(); thread2.start(); // 线程1和线程2只要有一个还存活就一直执行 while (thread1.isAlive() || thread2.isAlive()) { } System.out.println("main程序运行结束"); } @Override public void run() { method(); } public synchronized void method() { try { System.out.println(Thread.currentThread().getName() + "进入到了方法"); Thread.sleep(2000); System.out.println(Thread.currentThread().getName() + "执行完毕"); } catch (InterruptedException e) { e.printStackTrace(); } } }
在这个例子中我们使用两个线程对同一个普通方法进行访问,结果可想而知,也就是同一时刻只能有一个线程进入到此方法。我们运行一下,看一下结果。
跟我们预想的一样,很简单。不过我们想过一个问题没有,此时我们synchronized关键字加了一把锁,这个锁指代是谁呢?像同步代码块锁synchronized (object),这里面都有object,但是方法锁是谁呢?
答案就是this对象,也就是说我们在方法锁里面synchronized其实锁的就是当前this对象。我们如何去验证this锁的存在呢?不如我们再举一个例子:
public class SynTest3 implements Runnable { public static void main(String[] args) throws InterruptedException { SynTest3 syn = new SynTest3(); Thread thread1 = new Thread(syn); Thread thread2 = new Thread(syn); thread1.start(); thread2.start(); // 线程1和线程2只要有一个还存活就一直执行 while (thread1.isAlive() || thread2.isAlive()) {} System.out.println("main程序运行结束"); } @Override public void run() { method1(); method2(); } public synchronized void method1() { try { System.out.println(Thread.currentThread().getName() + "进入到了方法1"); Thread.sleep(2000); System.out.println(Thread.currentThread().getName() + "离开方法1,并释放锁"); } catch (InterruptedException e) { e.printStackTrace(); } } public synchronized void method2() { try { System.out.println(Thread.currentThread().getName() + "进入到了方法2"); Thread.sleep(2000); System.out.println(Thread.currentThread().getName() + "离开方法2,并释放锁"); } catch (InterruptedException e) { e.printStackTrace(); } } }
上面这个例子中,我们定义了两个synchronized关键字修饰的方法method1和method2,然后让两个线程同时运行,我们测试一下看看会出现什么结果:
从结果来看,会发现不管是method1还是method2,同一个时刻两个方法只能有一个线程在运行。这也就是this锁导致的。我们再给一张图描述一下其原理。
现在应该明白了吧,这也就验证了方法锁的存在。也验证了方法锁的原理。下面我们继续。讨论一下类锁。
2、类锁
上面的锁都是对象锁,下面我们看看类锁。类锁其实也有两种形式,一种是static方法锁,一种是class锁。
(1)static方法锁
在java中,java的类对象可能有无数个,但是类却只有一个。首先我们看第一种形式。
public class SynTest4 implements Runnable { public static void main(String[] args) throws InterruptedException { SynTest4 instance1 = new SynTest4(); SynTest4 instance2 = new SynTest4(); Thread thread1 = new Thread(instance1); Thread thread2 = new Thread(instance2); thread1.start(); thread2.start(); System.out.println("main程序运行结束"); } @Override public void run() { method1(); } public static synchronized void method1() { try { System.out.println(Thread.currentThread().getName() + "进入到了静态方法"); Thread.sleep(2000); System.out.println(Thread.currentThread().getName() + "离开静态方法,并释放锁"); } catch (InterruptedException e) { e.printStackTrace(); } } }
在这个例子中我们定义了两个不同的对象instance1和instance2。分别去执行了method1。会出现什么结果呢?
如果我们把static关键字去掉,很明显现在就是普通方法了,如果我们再去运行,由于instance1和instance2是两个不同的对象,那么也就是两个不同的this锁,这时候就能随便进入了。我们去掉static关键字之后运行一下:
现在看到了,由于是两个不同的this锁,所以都能进入,就好比是一个门有两把钥匙,每一把都能打开门。
(2)class锁
这种用法我们直接看例子再来分析一下:
public class SynTest5 implements Runnable { public static void main(String[] args) throws InterruptedException { SynTest5 instance1 = new SynTest5(); SynTest5 instance2 = new SynTest5(); Thread thread1 = new Thread(instance1); Thread thread2 = new Thread(instance2); thread1.start(); thread2.start(); // 线程1和线程2只要有一个还存活就一直执行 while (thread1.isAlive() || thread2.isAlive()) {} System.out.println("main程序运行结束"); } @Override public void run() { method1(); } public void method1() { synchronized (SynTest5.class) { try { System.out.println(Thread.currentThread().getName() + "进入到了方法"); Thread.sleep(2000); System.out.println(Thread.currentThread().getName() + "离开方法"); } catch (InterruptedException e) { e.printStackTrace(); } } } }
在这个例子中我们使用了同步代码块,不过synchronized关键字包装的可不是object了,而是SynTest5.class。我们还定义了两个不同的对象实例instance1和instance2。运行一下我们会发现,线程1和线程2依然会依次执行。
以上就是synchronized关键字的几种常见的用法,到这里我们来一个总结:
对于同步不同步,关键点在于锁,两个线程执行的是同一把锁,那么就依次排队等候,两个线程执行的不是同一把锁,那就各干各的事。
基本的使用我们也讲完了,下面我们进入下一个专题,那就是我们需要注意的事项。这是面试常考的一个问题,不管是机试还是面试。