1、多线程中出现的问题
我们在使用多线程中,发现代码有时候会和我们的预期结果不一致:
我们发现,在多线程中,我们开启两个线程对我们的i值进行增加,但是最后的结果不等于200000,他最后输出的结果是小于200000的,这是为啥呢?
原因很简单,因为我们的i++这个操作,并不是一个原子操作。
i++分解后其实是三个操作:
- 读取i的值
- 将i的值进行+1
- 将i的值写回内存
两个线程同时进程,可能A线程刚读取完i的值。B线程就进来,将i的值进行读取并对i+1写回内存。B线程结束后A线程并不知道i的值被修改,所以他再对i进行+1操作其实并不准确。
2、Synchronize锁的介绍
synchronize锁的作用:
就是保证同一时刻最多只有一个线程执行该段代码,以保证并发安全的问题。
3、Synchronize锁的用法
- 对象锁:包括方法锁(默认锁对象为this当前实例对象)和同步代码块锁(自己指定锁对象)
- 类锁:Synchronize修饰静态方法或者锁对象为class对象
3.1、对象锁
用代码来演示我们的对象锁:
public class MethodSynchronize implements Runnable { private static MethodSynchronize methodSynchronize = new MethodSynchronize(); @Override public void run() { synchronized (this) { System.out.println("我是对象锁代码块," + Thread.currentThread().getName() + "开始执行"); try { Thread.sleep(500); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("我是对象锁代码块," + Thread.currentThread().getName() + "执行结束"); } } public static void main(String[] args) { Thread thread1 = new Thread(methodSynchronize); Thread thread2 = new Thread(methodSynchronize); thread1.start(); thread2.start(); while (thread1.isAlive() || thread2.isAlive()){ } System.out.println("game over"); } } //执行结果 我是对象锁代码块,Thread-0开始执行 我是对象锁代码块,Thread-0执行结束 我是对象锁代码块,Thread-1开始执行 我是对象锁代码块,Thread-1执行结束 game over
我们发现,对方法加了锁后,就是顺序执行的。执行完了thread1之后才回去执行thread2。
如果我们不加对象锁,我们的执行效果是这样的:
public class MethodSynchronize implements Runnable { private static MethodSynchronize methodSynchronize = new MethodSynchronize(); @Override public void run() { // synchronized (this) { System.out.println("我是对象锁代码块," + Thread.currentThread().getName() + "开始执行"); try { Thread.sleep(500); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("我是对象锁代码块," + Thread.currentThread().getName() + "执行结束"); // } } public static void main(String[] args) { Thread thread1 = new Thread(methodSynchronize); Thread thread2 = new Thread(methodSynchronize); thread1.start(); thread2.start(); while (thread1.isAlive() || thread2.isAlive()){ } System.out.println("game over"); } } //执行效果 我是对象锁代码块,Thread-0开始执行 我是对象锁代码块,Thread-1开始执行 我是对象锁代码块,Thread-0执行结束 我是对象锁代码块,Thread-1执行结束 game over
使用同步代码块,自己指定锁:
public class MethodSynchronize implements Runnable { private static MethodSynchronize methodSynchronize = new MethodSynchronize(); Object lock1 = new Object(); Object lock2 = new Object(); @Override public void run() { synchronized (lock1) { System.out.println("我是lock1对象锁代码块," + Thread.currentThread().getName() + "开始执行"); try { Thread.sleep(5000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("我是lock1对象锁代码块," + Thread.currentThread().getName() + "执行结束"); } synchronized (lock2) { System.out.println("我是lock2对象锁代码块," + Thread.currentThread().getName() + "开始执行"); try { Thread.sleep(5000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("我是lock2对象锁代码块," + Thread.currentThread().getName() + "执行结束"); } } public static void main(String[] args) { Thread thread1 = new Thread(methodSynchronize); Thread thread2 = new Thread(methodSynchronize); thread1.start(); thread2.start(); while (thread1.isAlive() || thread2.isAlive()){ } System.out.println("game over"); } } //执行结果 我是lock1对象锁代码块,Thread-0开始执行 我是lock1对象锁代码块,Thread-0执行结束 我是lock2对象锁代码块,Thread-0开始执行 我是lock1对象锁代码块,Thread-1开始执行 我是lock1对象锁代码块,Thread-1执行结束 我是lock2对象锁代码块,Thread-0执行结束 我是lock2对象锁代码块,Thread-1开始执行 我是lock2对象锁代码块,Thread-1执行结束 game over Process finished with exit code 0
这里我们可以看到,因为是不同的锁,所以thread-0的lock2锁的代码执行时间和thread-1的lock1锁执行的时间是同步。因为锁对象不是同一个,所以是并行执行的。
如果将lock2的锁对象也替换成lock1,那么他们就还是顺序执行:
public void run() { synchronized (lock1) { System.out.println("我是lock1对象锁代码块," + Thread.currentThread().getName() + "开始执行"); try { Thread.sleep(5000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("我是lock1对象锁代码块," + Thread.currentThread().getName() + "执行结束"); } synchronized (lock1) { System.out.println("我是lock2对象锁代码块," + Thread.currentThread().getName() + "开始执行"); try { Thread.sleep(5000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("我是lock2对象锁代码块," + Thread.currentThread().getName() + "执行结束"); } } //执行效果 我是lock1对象锁代码块,Thread-0开始执行 我是lock1对象锁代码块,Thread-0执行结束 我是lock2对象锁代码块,Thread-0开始执行 我是lock2对象锁代码块,Thread-0执行结束 我是lock1对象锁代码块,Thread-1开始执行 我是lock1对象锁代码块,Thread-1执行结束 我是lock2对象锁代码块,Thread-1开始执行 我是lock2对象锁代码块,Thread-1执行结束 game over
方法锁代码示例:
public class MethodSychroize2 implements Runnable { private static MethodSychroize2 methodSychroize2= new MethodSychroize2(); @Override public void run() { method(); } public synchronized void method(){ 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 thread1 = new Thread(methodSychroize2); Thread thread2 = new Thread(methodSychroize2); thread1.start(); thread2.start(); while (thread1.isAlive() || thread2.isAlive()){ } System.out.println("game over!"); } } //执行效果 我是对象锁的方法修饰符,我叫Thread-0开始 我是对象锁的方法修饰符,我叫Thread-0结束 我是对象锁的方法修饰符,我叫Thread-1开始 我是对象锁的方法修饰符,我叫Thread-1结束 game over!
synchronize修饰普通方法,默认锁对象就是当前实例对象
同步代码块,锁对象需要自己指定
3.2、类锁
概念:一个类,可以有多个实例对象,但是只有一个class对象
类锁有两种方式:
- static静态方法上synchronize关键字
- 代码块中用.class作为锁对象
不适用类锁,而是使用对象锁出现的问题:
public class StaticMethodSynchronize implements Runnable { private static StaticMethodSynchronize instance1 = new StaticMethodSynchronize(); private static StaticMethodSynchronize instance2 = new StaticMethodSynchronize(); @Override public void run() { method(); } public synchronized void method(){ 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 thread1 = new Thread(instance1); Thread thread2 = new Thread(instance2); thread1.start(); thread2.start(); while (thread1.isAlive() || thread2.isAlive()) { } System.out.println("game over"); } } //执行结果 我是类锁,我叫Thread-1开始执行 我是类锁,我叫Thread-0开始执行 Thread-0执行结束 Thread-1执行结束 game over
我们发现这种情况下,使用同步代码块锁,线程为啥还是并行执行的呢?
原因是我们的同步代码块中,锁对象默认是当前实例,但是我们可以看到,thread1和thread2的传递进去的实例对象并不是同一个,所以无法锁住。
想要解决这个问题,我们就需要使用我们的类锁,使用静态方法上加synchronize:
public static synchronized void method(){ System.out.println("我是类锁静态方法,我叫" + Thread.currentThread().getName() + "开始执行"); try { Thread.sleep(3000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName() + "执行结束"); } //执行结果 我是类锁静态方法,我叫Thread-0开始执行 Thread-0执行结束 我是类锁静态方法,我叫Thread-1开始执行 Thread-1执行结束 game over
Synchronize(.class)这种形式的代码示例:
public void method(){ synchronized (StaticMethodSynchronize.class) { System.out.println("我是类锁静态方法,我叫" + Thread.currentThread().getName() + "开始执行"); try { Thread.sleep(3000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName() + "执行结束"); } } //执行结果 我是类锁静态方法,我叫Thread-0开始执行 Thread-0执行结束 我是类锁静态方法,我叫Thread-1开始执行 Thread-1执行结束 game over
4、核心思想
- 一把锁同时只能被一个线程持有,没有持有锁的线程必须等待
- 每个实例之间都有一把锁,不同实例之间互不影响;例外,只有锁对象是.class的时候和synchronize修饰static方法的时候,所有对象共用一把类锁
- synchronize无论是正常执行完毕还是抛出异常,都会释放锁对象
synchronize修饰的方法调用了其他的方法,不能保证其他方法是线程安全的。没有被synchronize修饰的方法,是可以同时被多个线程访问的
5、synchronize性质
- 可重入:同一线程的外层函数获取了锁之后,内层函数不需要再去竞争锁,可以直接使用
- 不可中断:一旦这个锁被人获得了,我还想获得锁对象,我只能选择等待或者阻塞,直到没得线程释放了这把锁
5.1可重入性
- 证明同一个方法是可重入的:
public class SynchronizeRecusion { int a = 0; public static void main(String[] args) { SynchronizeRecusion recusion = new SynchronizeRecusion(); recusion.method(); } private synchronized void method() { System.out.println("我是method,a="+a); if (a == 0){ a++; method(); } } } //执行结果 我是method,a=0 我是method,a=1
- 证明可重入不要求是同一个方法:
public class MethodOtherSynchorize { public synchronized void method(){ System.out.println("我是method"); method1(); } public synchronized void method1(){ System.out.println("我是method1"); } public static void main(String[] args) { MethodOtherSynchorize otherSynchorize = new MethodOtherSynchorize(); otherSynchorize.method(); } } //执行结果 我是method 我是method1
- 证明可重入不要求是同一个类中
public class SuperClassSynchroize { public synchronized void doSomething(){ System.out.println("我是父类方法"+this); } } class TestClass extends SuperClassSynchroize{ public synchronized void doSomething(){ System.out.println("我是子类方法"+this); super.doSomething(); } public static void main(String[] args) { TestClass testClass = new TestClass(); testClass.doSomething(); } } //执行结果 我是子类方法com.wx.mythread.thread.TestClass@7adf9f5f 我是父类方法com.wx.mythread.thread.TestClass@7adf9f5f
粒度:是线程中,只要是同一个线程中,需要的是同一把锁,就可以直接使用
5.2不可中断
一旦这个锁被人获得了,我还想获得锁对象,我只能选择等待或者阻塞,直到没得线程释放了这把锁。如果别人永远不释放,我将会永远等待下去。
相比之下,之后会介绍Lock锁,他有中断的能力。我等待的时间太久了,可以选择中断正在执行的锁对象;等待太久了,也可以选择不等待了,直接退出
6、原理
加锁和释放锁的等价代码:
public class LockSynchroize { Lock lock = new ReentrantLock(); public synchronized void method(){ System.out.println("我是synchronized形式的锁"); } public void method1(){ try { lock.lock(); System.out.println("我是lock锁的形式"); }finally { lock.unlock(); } } public static void main(String[] args) { LockSynchroize lockSynchroize = new LockSynchroize(); lockSynchroize.method(); lockSynchroize.method1(); } }
我们的synchronize关键字在方法内部也是做的这么下面这个操作。
将我们的同步代码块进行反编译:
monitorenter为0,说明可以获取到锁,获取到锁会+1。monitorexit操作会-1,减到0就会释放锁,否则就说明是重入进来的。
6.1可重入原理
利用加锁计数器实现的。
- JVM会负责跟踪对象呗加锁的次数
- 线程第一次给对象加锁的时候,计数变为1,每当相同的线程在对象上再次获得锁时,计数会递增
- 每当任务离开时,计数递减,当计数为0的时候,锁会被完全释放
6.2可见性原理
Java内存模型
加锁后
加锁后,线程也是无法直接操作主内存的,只能通过本地内存去同步主内存中的数据。
7、缺陷
- 效率低:锁释放的情况少,不能设置超时时间,没有获取到锁只能一直等待
- 不够灵活(读写锁更灵活):加锁释放锁的时机单一
- 无法知道是够成功获取到锁
8、面试
- 使用synchronize关键字注意点:锁对象不能为空,作用域不宜过大,避免死锁
- 如何选择synchronize和lock:尽量选择juc提供的工具类,其次选择synchronize,因为写的代码少,减少出错的概率