1. synchronized基本用法
- 定义:如果一个对象对多个线程可见,synchronized能够保证在同一时刻最多只有一个线程操作这个对象,以达到保证并发安全的效果。
- 作用:保证可见性和原子性,可以避免线程安全问题:运行结果错误
- 两种使用方法:
-
对象锁:
- 方法锁,默认锁对象为this当前实例对象
public class ObjectLock3 implements Runnable { @Override public void run() { method(); } public synchronized void method() { System.out.println(Thread.currentThread().getName() + "进入同步方法"); try { Thread.sleep(2000); } catch (InterruptedException e) { e.printStackTrace(); } } public static void main(String[] args) { ObjectLock3 objectLock3 = new ObjectLock3(); Thread t1 = new Thread(objectLock3); Thread t2 = new Thread(objectLock3); t1.start(); t2.start(); } }
- 同步代码块锁,自己指定锁对象
public class ObjectLock1 implements Runnable { @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) { ObjectLock1 objectLock1 = new ObjectLock1(); new Thread(objectLock1).start(); new Thread(objectLock1).start(); } } Thread-0进入同步代码块 Thread-0退出同步代码块 Thread-1进入同步代码块 Thread-1退出同步代码块
public class ObjectLock2 implements Runnable { private static final Object lock1 = new Object(); private static final Object lock2 = new Object(); @Override public void run() { synchronized (lock1) { System.out.println(Thread.currentThread().getName() + "进入同步代码块1"); try { Thread.sleep(3000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName() + "退出同步代码块1"); } synchronized (lock2) { System.out.println(Thread.currentThread().getName() + "进入同步代码块2"); try { Thread.sleep(3000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName() + "退出同步代码块2"); } } public static void main(String[] args) { ObjectLock2 objectLock2 = new ObjectLock2(); new Thread(objectLock2).start(); new Thread(objectLock2).start(); } } Thread-0进入同步代码块1 Thread-0退出同步代码块1 Thread-0进入同步代码块2 Thread-1进入同步代码块1 Thread-1退出同步代码块1 Thread-0退出同步代码块2 Thread-1进入同步代码块2 Thread-1退出同步代码块2
-
类锁:
- 静态方法锁,synchronized加在static方法上,锁对象为当前类
public class ObjectStaticLock1 implements Runnable { @Override public void run() { method(); } public static synchronized void method() { System.out.println(Thread.currentThread().getName() + "进入到同步静态方法中"); try { Thread.sleep(2000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName() + "退出同步静态方法"); } public static void main(String[] args) { ObjectStaticLock1 objectStaticLock1 = new ObjectStaticLock1(); ObjectStaticLock1 objectStaticLock2 = new ObjectStaticLock1(); Thread t1 = new Thread(objectStaticLock1); Thread t2 = new Thread(objectStaticLock2); t1.start(); t2.start(); } }
- 同步代码块锁,synchronized(*.class)代码块,指定锁对象为class对象,所谓的类锁,不过是Class对象的锁而已
public class ObjectStaticLock2 implements Runnable { @Override public void run() { synchronized (ObjectStaticLock2.class) { System.out.println(Thread.currentThread().getName() + "进入到同步代码块"); try { Thread.sleep(2000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName() + "退出同步代码块"); } } public static void main(String[] args) { ObjectStaticLock2 objectStaticLock1 = new ObjectStaticLock2(); ObjectStaticLock2 objectStaticLock2 = new ObjectStaticLock2(); Thread t1 = new Thread(objectStaticLock1); Thread t2 = new Thread(objectStaticLock2); t1.start(); t2.start(); } }
-
2. 多线程访问同步方法的7种情况
- 两个线程同时访问一个对象的同步方法:会发生同步,锁对象都为同一个实例对象;
- 两个线程同时访问两个对象的同步方法:互不影响,锁对象不同;
- 两个线程访问的是synchronized的静态方法:会发生同步,锁对象都为Class对象,Class对象只有一个;
- 同时访问同步方法和非同步方法:非同步方法不受影响,不发生同步;
- 访问同一个对象不同的普通同步方法:会发生同步,锁对象默认为同一个实例对象;
- 同时访问静态synchronized和非静态synchronized方法:互不影响,静态syn方法的锁对象为Class对象,非静态syn方法的锁对象为一个实例对象this,实例对象和Class对象不是同一个对象,实例对象在堆中,Class对象在方法区中;
- 方法抛出异常后,会释放锁
总结:
1. 一把锁只能同时被一个线程获取,没拿到锁的线程必须等待,如1、5;
2. 每个实例都有自己的一把锁,不同实例互不影响,当使用Class对象以及synchonized修饰的static方法的时候,所有对象共用同一把类锁,对应2、3、4、5;
3. 遇到异常,会释放锁,对应7;
3. synchronized关键字的性质
3.1 可重入
- 定义:一个线程已经获取到锁,想再次获取到这把锁时不需要释放,直接可以用;
- 什么是不可重入:一个线程获取到锁之后,想再次使用这个锁,必须释放锁之后还其他线程竞争;
- 好处:避免死锁:假如一个类有两个synchronized方法,当一个线程执行了方法1获得了默认的this对象锁,这个时候要执行方法2,如果synchronized不具备可重入性,那么这个线程就无法获取到访问方法2的锁,又无法释放锁,就造成了死锁。
- 粒度:线程范围,在一个线程中,只要这个线程拿到了这把锁,在这个线程内部就可以一直使用
- 同一个方法是可重入的;
- 可重入不要求是同一个方法;
- 可重入不要求是同一个类中;
3.2 不可中断
一旦这个锁已经被别的线程获得了,如果本线程还想获得,该线程只能等待或阻塞,直到别的线程释放这个锁。如果别的线程永远不释放锁,那么本线程则永远等待下去。
相比之下,Lock类,拥有可以中断的能力:
- 如果等的时间过长,可以中断现在已经获取的锁的线程的执行;
- 如果等待时间过长,也可以退出。
4. synchronized原理
4.1 加锁和释放锁原理
- 每个一个对象都有一个内置的monitor锁,这个锁存储在对象头中的,锁的获取和释放实际上需要执行两个指令:monitorenter和monitorexit,当线程执行到monitorenter的时候会尝试获取这个锁;
- 反编译:先javac demo.java,然后javap -verbose demo.class文件;
- monitorenter和monitorexit在执行的时候会让对象锁的计数+1或-1;
- 获取锁的过程:首先一个线程要获取一个对象锁的时候会查看这个monitor锁的计数器如果为0,那么就给他+1,这样别的线程就进不来了,如果一个线程有了这把锁,又重入了,在计数器再+1;如果monitor被其他线程持有了,直到计数器=0,才会获取这个锁。
- 释放锁的过程:将monitor的计数器-1,直到=0,表示不再拥有所有权了,如果不是0,说明刚才是可重入进来的
4.2 可重入原理
一个线程拿到一把锁之后,还想再次进入由这把锁所控制的方法,则可以再次进入,原理是用了monitor锁的计数器。
- JVM负责跟踪被加锁的次数
- 线程第一次给对象加锁的时候,计数+1.每当这个相同的线程再次获取该对象锁的时候,计数器会递增;
- 每当任务离开的时候,计数递减,当计数为0的时候,锁被完全释放;
4.3 可见性原理
线程A和线程B通信:
- 本地内存A把修改后的内容放到主内存中;
- 本地内存B从主内存从读取修改后的内容;
synchnized修饰的代码块对对象的任何修改,在释放锁之前都要将修改的内容先写回到主内存中,所以从主内存中读取的内容都是最新的。
5. synchronized的缺陷
- 效率低:锁的释放情况少(只有代码执行完和抛异常)、试图获得锁时候不能设定超时、不能中断一个正在试图获得锁的线程
- 不够灵活:加锁和释放的时机单一,每个锁仅仅有单一的条件,可能是不够的。读写锁更灵活。
- 无法知道是否成功获取到锁,没法去尝试获取,去判断。Lock是可以通过tryLock方法尝试获取,返回true代表成功加锁。