案例引入
多个线程访问同一个对象,实现两个线程,t1,t2同时对变量i进行++运算,预想最后的结果是20000
public class SynchronizedDemo1 implements Runnable { static SynchronizedDemo1 ins = new SynchronizedDemo1(); static int i = 0; public static void main(String[] args) throws InterruptedException { Thread t1 = new Thread(ins); Thread t2 = new Thread(ins); t1.start(); t2.start(); t1.join(); t2.join(); System.out.println(i); } @Override public void run() { for (int j = 0; j < 10000; j++) { i++; } } }
代码运行结果
由结果可以看出,每次运行都会得到不同的结果,那么这是什么原因导致的呢?
i++,它看上去只是一个操作,实际上包含了三个动作:
1.读取 i
2.将 i 加一
3.将 i 的值写入到内存中由于线程并发执行,三个步骤中任何一个步骤,被其他线程打断都可能会导致值丢失,因此当第二个线程取数据时,取到还是原来的数据,所以导致了结果的不准确,这种行为我们称之为线程不安全。
synchronized
作用
官方定义
Synchronized methods enable a simple strategy for preventing thread interference and memory consistency errors: if an object is visible to more than one thread, all reads or writes to that object'svariables are done through synchronized methods.
自己总结
能够保证在同一时刻最多只有一个线程执行该段代码,以达到保证并发安全的效果。
Synchronized的两种方法
对象锁
包括方法锁(默认锁对象为this当前实例对象)和同步代码块锁)(自己指定锁对象)
- 代码块形式:手动指定锁对象
public class SynchronizedDemo2 implements Runnable { static SynchronizedDemo2 sy = new SynchronizedDemo2(); @Override public void run() { synchronized (this){ System.out.println("对象锁代码块形式,线程:"+Thread.currentThread().getName()+", 开始运行"); try { Thread.sleep(3000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName() + "运行结束"); } } public static void main(String[] args) { Thread t1 = new Thread(sy); Thread t2 = new Thread(sy); t1.start(); t2.start(); while (t1.isAlive() || t2.isAlive()){} System.out.println("finished ..."); } }
- 方法锁形式:synchronized修饰普通方法,锁对象默认为this,或者自己创建一个对象锁
public class SynchronizedDemo3 implements Runnable { static SynchronizedDemo3 sy = new SynchronizedDemo3(); @Override public void run() { methon(); } public static void main(String[] args) { Thread t1 = new Thread(sy); Thread t2 = new Thread(sy); t1.start(); t2.start(); while (t1.isAlive() || t2.isAlive()){} System.out.println("finished ..."); } public synchronized void methon() { System.out.println("线程对象为:" + Thread.currentThread().getName()); try { Thread.sleep(2000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("线程:" + Thread.currentThread().getName() + " 运行结束"); } }
类锁
指synchronized修饰静态的方法或指定锁为Class对象。
- 只有一个Class对象:Java类可能会有很多个对象,但是只有1个Class对象。
- 本质∶所以所谓的类锁,不过是Class对象的锁而已。------概念上的存在
- 用法和效果:类锁只能在同一时刻被一个对象拥有。
类锁的两种形式
- 形式1: synchronized加在static方法上
public class SynchronizedDemo4 implements Runnable{ @Override public void run() { methon(); } static SynchronizedDemo4 sy1 = new SynchronizedDemo4(); static SynchronizedDemo4 sy2 = new SynchronizedDemo4(); public static void main(String[] args) { Thread t1 = new Thread(sy1); Thread t2 = new Thread(sy2); t1.start(); t2.start(); while (t1.isAlive() || t2.isAlive()){} System.out.println("finished ..."); } public static synchronized void methon() { System.out.println("线程对象:" + Thread.currentThread().getName()); try { Thread.sleep(2000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("线程:" + Thread.currentThread().getName() + " 运行结束"); } }
- 形式2: synchronized ( *.class )代码块
public class SynchronizedDemo5 implements Runnable{ @Override public void run() { methon(); } static SynchronizedDemo5 sy1 = new SynchronizedDemo5(); static SynchronizedDemo5 sy2 = new SynchronizedDemo5(); public static void main(String[] args) { Thread t1 = new Thread(sy1); Thread t2 = new Thread(sy2); t1.start(); t2.start(); while (t1.isAlive() || t2.isAlive()){} System.out.println("finished ..."); } public void methon() { synchronized (SynchronizedDemo5.class){ System.out.println("线程对象:" + Thread.currentThread().getName()); try { Thread.sleep(2000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("线程:" + Thread.currentThread().getName() + " 运行结束"); } } }
常见问题总结
1.两个线程同时访问一个对象的同步方法
1 public class SynchronizedDemo4 implements Runnable{ 2 @Override 3 public void run() { 4 methon(); 5 } 6 static SynchronizedDemo4 sy1 = new SynchronizedDemo4(); 7 static SynchronizedDemo4 sy2 = new SynchronizedDemo4(); 8 9 public static void main(String[] args) { 10 Thread t1 = new Thread(sy1); 11 Thread t2 = new Thread(sy2); 12 t1.start(); 13 t2.start(); 14 while (t1.isAlive() || t2.isAlive()){} 15 System.out.println("finished ..."); 16 } 17 18 public static synchronized void methon() { 19 System.out.println("线程对象:" + Thread.currentThread().getName()); 20 try { 21 Thread.sleep(2000); 22 } catch (InterruptedException e) { 23 e.printStackTrace(); 24 } 25 System.out.println("线程:" + Thread.currentThread().getName() + " 运行结束"); 26 } 27 }
由代码运行结果可以看出,其运行结果为串行,这是因为修饰的锁为this对象,为同一把锁,必然相互等待只能由一个线程所有。
2.两个线程访问的是两个对象的同步方法
1 public class SynchronizedDemo6 implements Runnable{ 2 @Override 3 public void run() { 4 methon(); 5 } 6 static SynchronizedDemo6 sy1 = new SynchronizedDemo6(); 7 static SynchronizedDemo6 sy2 = new SynchronizedDemo6(); 8 9 public static void main(String[] args) { 10 Thread t1 = new Thread(sy1); 11 Thread t2 = new Thread(sy2); 12 t1.start(); 13 t2.start(); 14 while (t1.isAlive() || t2.isAlive()){} 15 System.out.println("finished ..."); 16 } 17 18 public void methon() { 19 synchronized (this){ 20 System.out.println("线程对象:" + Thread.currentThread().getName()); 21 try { 22 Thread.sleep(2000); 23 } catch (InterruptedException e) { 24 e.printStackTrace(); 25 } 26 System.out.println("线程:" + Thread.currentThread().getName() + " 运行结束"); 27 } 28 } 29 }
由上结果可以看出,两个线程为并行执行,因为两个线程所用的锁对象this不相同,两个线程互不干扰,呈现了并行执行的效果。
3.两个线程访问的是synchronized的静态方法
public class SynchronizedDemo4 implements Runnable{ @Override public void run() { methon(); } static SynchronizedDemo4 sy1 = new SynchronizedDemo4(); static SynchronizedDemo4 sy2 = new SynchronizedDemo4(); public static void main(String[] args) { Thread t1 = new Thread(sy1); Thread t2 = new Thread(sy2); t1.start(); t2.start(); while (t1.isAlive() || t2.isAlive()){} System.out.println("finished ..."); } public static synchronized void methon() { System.out.println("线程对象:" + Thread.currentThread().getName()); try { Thread.sleep(2000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("线程:" + Thread.currentThread().getName() + " 运行结束"); } }
由执行结果可以看出为串行,会一个一个执行,由此可以把看出锁生效。
4.同时访问同步方法与非同步方法
public class SynchronizedDemo7 implements Runnable { @Override public void run() { // 使线程再同一时刻访问同一个方法 if (Thread.currentThread().getName().equals("Thread-0")) { methon1(); } else { methon2(); } } static SynchronizedDemo7 sy1 = new SynchronizedDemo7(); static SynchronizedDemo7 sy2 = new SynchronizedDemo7(); public static void main(String[] args) { Thread t1 = new Thread(sy1); Thread t2 = new Thread(sy2); t1.start(); t2.start(); while (t1.isAlive() || t2.isAlive()) { } System.out.println("finished ..."); } public void methon1() { synchronized (this) { System.out.println("同步线程对象:" + Thread.currentThread().getName()); try { Thread.sleep(2000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("线程:" + Thread.currentThread().getName() + " 运行结束"); } } public void methon2() { System.out.println("非同步线程对象:" + Thread.currentThread().getName()); try { Thread.sleep(2000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("线程:" + Thread.currentThread().getName() + " 运行结束"); } }
由执行结果可以看出,synchronized只执行于被修饰的方法,其他方法并不受到影响。所以非同步方法不受到影响
5.访问同一个对象的不同的普通同步方法
public class SynchronizedDemo8 implements Runnable { @Override public void run() { // 使线程再同一时刻访问同一个方法 if (Thread.currentThread().getName().equals("Thread-0")) { methon1(); } else { methon2(); } } static SynchronizedDemo8 sy1 = new SynchronizedDemo8(); public static void main(String[] args) { Thread t1 = new Thread(sy1); Thread t2 = new Thread(sy1); t1.start(); t2.start(); while (t1.isAlive() || t2.isAlive()) { } System.out.println("finished ..."); } public synchronized void methon1() { System.out.println("同步线程1对象:" + Thread.currentThread().getName()); try { Thread.sleep(2000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("线程:" + Thread.currentThread().getName() + " 运行结束"); } public synchronized void methon2() { System.out.println("同步线程2对象:" + Thread.currentThread().getName()); try { Thread.sleep(2000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("线程:" + Thread.currentThread().getName() + " 运行结束"); } }
访问同一个对象的不同的普通方法,由于时同一个对象,所以可以推断出锁对象this指向为同一个锁,所以会串行执行。
6.同时访问静态synchronized和非静态synchronized方法
public class SynchronizedDemo9 implements Runnable { @Override public void run() { // 使线程再同一时刻访问同一个方法 if (Thread.currentThread().getName().equals("Thread-0")) { methon1(); } else { methon2(); } } static SynchronizedDemo9 sy1 = new SynchronizedDemo9(); public static void main(String[] args) { Thread t1 = new Thread(sy1); Thread t2 = new Thread(sy1); t1.start(); t2.start(); while (t1.isAlive() || t2.isAlive()) { } System.out.println("finished ..."); } public static synchronized void methon1() { System.out.println("同步静态线程1对象:" + Thread.currentThread().getName()); try { Thread.sleep(2000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("线程:" + Thread.currentThread().getName() + " 运行结束"); } public synchronized void methon2() { System.out.println("同步非静态线程2对象:" + Thread.currentThread().getName()); try { Thread.sleep(2000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("线程:" + Thread.currentThread().getName() + " 运行结束"); } }
由结果可以看出为串行。这是因为被static修饰之后就变成了类锁,锁的对象为class;而没有被static修饰的锁的对象时方法对象本身;他们之间锁不一样,所以互不干扰。
7.方法抛异常后,会释放锁
说明:synchronizatied出现异常会自动释放锁,而lock需要再findly中手动释放锁。
通过展示不抛出异常前和抛出异常后的对比:一旦抛出了异常,第二个线程会立刻进入同步方法,意味着锁已经释放。
手动抛出异常
1 public class SynchronizedDemo10 implements Runnable { 2 @Override 3 public void run() { 4 // 使线程再同一时刻访问同一个方法 5 if (Thread.currentThread().getName().equals("Thread-0")) { 6 methon1(); 7 } else { 8 methon2(); 9 } 10 } 11 static SynchronizedDemo10 sy1 = new SynchronizedDemo10(); 12 public static void main(String[] args) { 13 Thread t1 = new Thread(sy1); 14 Thread t2 = new Thread(sy1); 15 t1.start(); 16 t2.start(); 17 while (t1.isAlive() || t2.isAlive()) { 18 } 19 System.out.println("finished ..."); 20 } 21 22 public synchronized void methon1() { 23 System.out.println("同步线程1对象:" + Thread.currentThread().getName()); 24 try { 25 Thread.sleep(2000); 26 } catch (InterruptedException e) { 27 e.printStackTrace(); 28 } 29 throw new RuntimeException(); 30 // System.out.println("线程:" + Thread.currentThread().getName() + " 运行结束"); 31 } 32 33 public synchronized void methon2() { 34 System.out.println("同步线程2对象:" + Thread.currentThread().getName()); 35 try { 36 Thread.sleep(2000); 37 } catch (InterruptedException e) { 38 e.printStackTrace(); 39 } 40 System.out.println("线程:" + Thread.currentThread().getName() + " 运行结束"); 41 } 42 }
总结
1.一把锁只能同时被一个线程获取,没有拿到锁的线程必须等待(对应第1、5种情况);
2.每个实例都对应有自己的一把锁,不同实例之间互不影响; 例外:锁对象是*.class以及synchronized修饰的是static方法的时候,所有对象共用同一把类锁(对应第2、3、4、6种情况) ;
3.无论是方法正常执行完毕或者方法抛出异常,都会释放锁(对应第7种情况)
synchronizatied的性质
1.可重入
什么是可重入︰指的是同一线程的外层函数获得锁之后,内层函数可以直接再次获取该锁(也叫做递归锁)
好处:避免死锁、提升封装性
2.不可中断
一旦这个锁已经被别人获得了,如果我还想获得,我只能选择等待或者阻塞,直到别的线程释放这个锁。如果别人永远不释放锁,那么我只能永远地等下去。
相比之下,未来会介绍的Lock类,可以拥有中断的能力,第一点,如果我觉得我等的时间太长了,有权中断现在已经获取到锁的线程的执行;第二点,如果我觉得我等待的时间太长了不想再等了,也可以退出。
加锁和释放锁的原理:现象、时机、深入JVM看字节码
现象:每个类都有一个锁,不同的线程都要获取到相应的锁才能执行,否则只能阻塞;而释放、加锁等功能由jvm管理。
获取释放锁的时机:内置锁
加锁和释放锁的 等价代码
public class SynchronizedDemo11 { Lock lock = new ReentrantLock(); public static void main(String[] args) { SynchronizedDemo11 sy1 = new SynchronizedDemo11(); sy1.methon1(); sy1.methon2(); } // method1 等价于 method2 public synchronized void methon1() { System.out.println("synchronized lock method"); } public void methon2() { lock.lock(); // 加锁 try { System.out.println("lock method"); } finally { lock.unlock(); // 释放锁 } } }
深入JVM看字节码
字节码中哪里体现的某个线程已经获取了该对象的锁:monitorenter 和 monitorexit
反编译流程
首先新建一个Java类
package concurrency_code; public class SynchronizedDemo12 { private Object object = new Object(); public void insert(Thread thread){ synchronized (object){ } } }
接着通过Javac 编译成字节码文件
接着使用 javap -verbose SynchronizedDemo12.class 命令查看字节码文件
反编译后可以看到 第六行的 monitorenter 和 第8行的 monitorexit
可重入原理∶加锁次数计数器
保证可见性的原理︰内存模型