一、什么时候使用synchronized关键字
在多线程编程永远都逃不开线程安全的问题,影响线程安全的因素主要有两:1、存在共享数据;2、多线程共同操作共享数据。关键字synchronized可以保证在同一时刻,只有一个线程可以执行某个方法或某个代码块,同时synchronized可以保证一个线程的变化可见(可见性),即可以代替volatile关键字(后面也会详细讲解)。
二、特性
synchronized关键子可以修饰类、方法或者代码块,有以下特性:
1、当多个并发的线程访问sychronized关键字修饰的类对象、方法或方法块时,同一时刻最多有一个线程去执行这段代码;
2、当一个线程执行sychronized关键字修饰的代码块,其他线程仍然可以访问未用synchronized修饰的代码块。
三、应用方式
Java中每一个对象都可以作为锁,这是synchronized实现同步的基础:
普通同步方法(实例方法),锁是当前实例对象 ,进入同步代码前要获得当前实例的锁
静态同步方法,锁是当前类的class对象 ,进入同步代码前要获得当前类对象的锁
同步方法块,锁是括号里面的对象,对给定对象加锁,进入同步代码库前要获得给定对象的锁。
四、举例子
情况一:synchronized作用于实例方法上
①首先我们先看一个没有使用synchronized修饰的情况:
public class SynchronizedExample implements Runnable{ //共享资源 static int i=0; public void increase(){ i++; } @Override public void run() { for(int i=0;i<1000;i++){ increase(); } } public static void main(String[] args) throws InterruptedException{ SynchronizedExample example=new SynchronizedExample(); Thread thd1=new Thread(example); Thread thd2=new Thread(example); thd1.start(); thd2.start(); thd1.join(); thd2.join(); System.out.println(i); } }
测试结果:
1634
分析:按照我们的意愿,应该输出为2000才对,但经过几次测试,结果往往都是小于2000。这是因为自增操作不具有原子性,它还可以分解,它包括读取变量初始值、进行加1操纵、写入工作内存。这样就会出现当一个线程读取变量初始值时上一个线程还未完成写入内存工作的情况。如何避免这种情况呢,实现当一个线程进行这些操作时,其他线程无法进入,这时就要引入synchronized关键字。
②使用synchronized修饰实例方法
public class SynchronizedExample implements Runnable{ //共享资源 static int i=0; public synchronized void increase(){ i++; } @Override public void run() { for(int i=0;i<1000;i++){ increase(); } } public static void main(String[] args) throws InterruptedException{ SynchronizedExample example=new SynchronizedExample(); Thread thd1=new Thread(example); Thread thd2=new Thread(example); thd1.start(); thd2.start(); thd1.join(); thd2.join(); System.out.println(i); } }
测试结果:
2000
无论我们进行多少次测试,结果都是2000。
分析:当两个线程同时对一个对象的一个方法进行操作,只有一个线程能够抢到锁。因为一个对象只有一把锁,一个线程获取了该对象的锁之后,其他线程无法获取该对象的锁,就不能访问该对象的其他synchronized实例方法,但是可以访问非synchronized修饰的方法。
③一个线程获得锁之后,其他线程能否访问其他synchronized方法。
public class SynchronizedTest { private synchronized void method1(){ System.out.println("Start Method1"); try{ System.out.println("Execute Method1"); Thread.sleep(3000); } catch (InterruptedException ex){ ex.printStackTrace(); } System.out.println("End method1"); } private synchronized void method2(){ System.out.println("Start Method2"); try{ System.out.println("Execute Method2"); Thread.sleep(3000); } catch (InterruptedException ex){ ex.printStackTrace(); } System.out.println("End method2"); } public static void main(String[] args){ SynchronizedTest test=new SynchronizedTest(); Thread thd1=new Thread(test::method1); Thread thd2=new Thread(test::method2); thd1.start(); thd2.start(); } }
测试结果:
Start Method1
Execute Method1
End method1
Start Method2
Execute Method2
End method2
分析:对于synchronized修饰的方法,锁的是当前实例,只有当线程将锁释放后,才能被其他线程获取。
④一个线程获得锁之后,其他线程能否访问其他非synchronized方法。
public class SynchronizedTest { private synchronized void method1(){ System.out.println("Start Method1"); try{ System.out.println("Execute Method1"); Thread.sleep(3000); } catch (InterruptedException ex){ ex.printStackTrace(); } System.out.println("End method1"); } private void method2(){ System.out.println("Start Method2"); try{ System.out.println("Execute Method2"); Thread.sleep(3000); } catch (InterruptedException ex){ ex.printStackTrace(); } System.out.println("End method2"); } public static void main(String[] args){ SynchronizedTest test=new SynchronizedTest(); Thread thd1=new Thread(test::method1); Thread thd2=new Thread(test::method2); thd1.start(); thd2.start(); } }
测试结果:
Start Method1
Execute Method1
Start Method2
Execute Method2
End method2
End method1
分析:线程访问非sychronized方法时并不需要检查是否获取锁,所以可以执行,不会等待。
⑤多个线程作用多个对象时
public class SynchronizedTest { private synchronized void method1(){ System.out.println("Start Method1"); try{ System.out.println("Execute Method1"); Thread.sleep(3000); } catch (InterruptedException ex){ ex.printStackTrace(); } System.out.println("End method1"); } private void method2(){ System.out.println("Start Method2"); try{ System.out.println("Execute Method2"); Thread.sleep(3000); } catch (InterruptedException ex){ ex.printStackTrace(); } System.out.println("End method2"); } public static void main(String[] args){ SynchronizedTest test1=new SynchronizedTest(); SynchronizedTest test2=new SynchronizedTest(); Thread thd1=new Thread(test1::method1); Thread thd2=new Thread(test2::method2); thd1.start(); thd2.start(); } }
测试结果:
Start Method1
Start Method2
Execute Method1
Execute Method2
End method2
End method1
分析:两个线程作用于不同的对象,获取是不同的锁,所以也不需要等待。
情况二:synchronized作用于静态方法上
public class SynchronizedTest implements Runnable{ private synchronized static void method(){ System.out.println(Thread.currentThread().getName()+": Start Method1"); try{ System.out.println(Thread.currentThread().getName()+": Execute Method"); Thread.sleep(3000); } catch (InterruptedException ex){ ex.printStackTrace(); } System.out.println(Thread.currentThread().getName()+": End method"); } @Override public void run() { method(); } public static void main(String[] args){ SynchronizedTest test1=new SynchronizedTest(); SynchronizedTest test2=new SynchronizedTest(); Thread thd1=new Thread(test1); Thread thd2=new Thread(test2); thd1.setName("Thread1"); thd2.setName("Thread2"); thd1.start(); thd2.start(); } }
测试结果:
Thread1: Start Method1
Thread1: Execute Method
Thread1: End method
Thread2: Start Method1
Thread2: Execute Method
Thread2: End method
分析:当synchronized修饰静态方法时,锁的是class对象,所以线程Thread1和Thread2共用一把锁,需要等待。
情况三:synchronized修饰的方法块。
为什么要同步代码块呢?在某些情况下,我们编写的方法体可能比较大,同时存在一些比较耗时的操作,而需要同步的代码又只有一小部分,如果直接对整个方法进行同步操作,可能会得不偿失,此时我们可以使用同步代码块的方式对需要同步的代码进行包裹,这样就无需对整个方法进行同步操作了。
public class SynchronizedExample implements Runnable{ //共享资源 static int i=0; static SynchronizedExample instance=new SynchronizedExample(); public synchronized void increase(){ i++; } @Override public void run() { //省略其他耗时操作.... //使用同步代码块对变量i进行同步操作,锁对象为instance synchronized(instance){ for(int i=0;i<1000;i++){ increase(); } } } public static void main(String[] args) throws InterruptedException{ Thread thd1=new Thread(instance); Thread thd2=new Thread(instance); thd1.start(); thd2.start(); thd1.join(); thd2.join(); System.out.println(i); } }
测试结果:
2000
分析:将synchronized作用于一个给定的实例对象instance,即当前实例对象就是锁对象,每次当线程进入synchronized包裹的代码块时就会要求当前线程持有instance实例对象锁,如果当前有其他线程正持有该对象锁,那么新到的线程就必须等待,这样也就保证了每次只有一个线程执行i++;操作。当然除了instance作为对象外,我们还可以使用this对象(代表当前实例)或者当前类的class对象作为锁,如下代码:
synchronized(this){ for(int i=0;i<1000;i++){ increase(); } }
四、锁重入
在使用synchronized时,当一个线程得到一个对象锁后,再次请求此对象锁时是可以再次得到该对象的锁的。这也证明在一个synchronizes方法/块的内部调用本类的其他synchronized方法/块时,也是 永远可以得到锁的。
public class SynchronizedTest implements Runnable{ private synchronized void method1(){ System.out.println("method1"); method2(); } private synchronized void method2(){ System.out.println("method2"); method3(); } private synchronized void method3(){ System.out.println("method3"); } @Override public void run() { method1(); } public static void main(String[] args){ SynchronizedTest test=new SynchronizedTest(); new Thread(test).start(); } }
测试结果:
method1
method2
method3
分析:在执行方法method1()时获取对象实例锁,同一线程在执行方法method2()时再此获取对象锁。这也证明在一个synchronizes方法/块的内部调用本类的其他synchronized方法/块时,也是 永远可以得到锁的。
五、死锁
死锁:是指两个或两个以上的线程在执行中,因争夺资源而造成一种相互等待的现象,若无无力作用,它们都将无法推进下去。
死锁必要的四个条件:
- 互斥条件:一个资源每次只能被一个进程使用。
- 请求与保持条件:一个进程因请求资源而阻塞时,对已获得的资源保持不放。
- 不剥夺条件:进程已获得的资源,在末使用完之前,不能强行剥夺。
- 循环等待条件:若干进程之间形成一种头尾相接的循环等待资源关系。
public class DeadLockTest implements Runnable{ private Boolean flag; public DeadLockTest(Boolean flag){ this.flag=flag; } static Object A=new Object(); static Object B=new Object(); static void test(Boolean b){ if(b){ synchronized (A){ System.out.println("A锁"); try{ Thread.sleep(3000); }catch (InterruptedException ex){ ex.printStackTrace(); } synchronized (B){ System.out.println("B锁住了A"); } } }else{ synchronized (B){ System.out.println("B锁"); try{ Thread.sleep(3000); }catch (InterruptedException ex){ ex.printStackTrace(); } synchronized (A){ System.out.println("A锁住了B"); } } } } @Override public void run() { test(flag); } public static void main(String[] args){ Thread thd1=new Thread(new DeadLockTest(true)); Thread thd2=new Thread(new DeadLockTest(false)); thd1.start(); thd2.start(); } }
测试结果:
A锁
B锁
此程序运行之后,由于A在等待B,B在等待A,就会进入相互等待的僵持状态,产生死锁。
而此时,稍作修改,我不用Thread类的sleep方法,改用了Object 类的wait方法,此时就不会产生死锁,这也验证了wait和sleep的区别:
wait方法会释放当前对象的锁,而sleep方法不会释放当前对象锁。
public class DeadLockTest implements Runnable{ private Boolean flag; public DeadLockTest(Boolean flag){ this.flag=flag; } static Object A=new Object(); static Object B=new Object(); static void test(Boolean b){ if(b){ synchronized (A){ System.out.println("A锁"); try{ A.wait(3000); }catch (InterruptedException ex){ ex.printStackTrace(); } synchronized (B){ System.out.println("B锁住了A"); } } }else{ synchronized (B){ System.out.println("B锁"); try{ B.wait(1000);//注意此处B中wait时间和A中要有所不同,否者还可能造成死锁 }catch (InterruptedException ex){ ex.printStackTrace(); } synchronized (A){ System.out.println("A锁住了B"); } } } } @Override public void run() { test(flag); } public static void main(String[] args){ Thread thd1=new Thread(new DeadLockTest(true)); Thread thd2=new Thread(new DeadLockTest(false)); thd1.start(); thd2.start(); } }
测试结果:
B锁
A锁
A锁住了B
B锁住了A
注意wait()和notify()方法只能放在同步代码块当中,如果不在同步代码块中使用,尽管在编译时不会出错,运行时会抛出java.lang.IllegalMonitorStateException异常。
-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
参考博文:https://blog.csdn.net/zjy15203167987/article/details/82531772
参考博文:https://blog.csdn.net/zjy15203167987/article/details/80558515
参考博文:https://www.cnblogs.com/weibanggang/p/9470718.html
参考博文:https://blog.csdn.net/qq_40574571/article/details/91546156