本文是学习 默课网-Java高并发之魂:synchronized深度解析 所做的笔记。
一、Synchronized
1.1 synchronized简介
- 官方解释:
同步方法支持一种简单的策略来防止线程干扰和内存一直想错误:如果一个对象对多个线程可见,则该对象变量的所有读取和写入都是通过同步方法完成的。
- 通俗易懂:
能够保证同一时刻只有一个线程执行该段代码,已达到并发安全的效果。
1.2 并发出现的问题
两个线程同时执行i++,最后的结果比我们预期的少。代码如下:
public class DisappearRequest1 implements Runnable{
private static DisappearRequest1 instance = new DisappearRequest1();
private static int i = 0;
@Override
public void run() {
for (int j = 0; j < 100000; j++) {
i ++;
}
}
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(instance);
Thread t2 = new Thread(instance);
t1.start();
t2.start();
// join()方法的作用是,当前线程(主线程)进入阻塞状态,等待执行join()方法的线程运行结束后,当前线程(主线程)才可以进入就绪状态。
t1.join();
t2.join();
// 理想的情况是输出200000,但由于多线程并发问题,结果小于200000
System.out.println(i);
/** 原因分析:
* i ++ ,看上去只有一个操作,实际上包含了三个动作
* 1. 读取i的值
* 2. 将i加1
* 3. 将i的值写入内存
* 由于在多线程情况下,上述三个步骤任何一步都有可能被打断(例如:t1读取到i的值为0,加1,
* 还没来得及写入内存,此时t2线程进来,读取到i的值依然是0,加1,写入内存,最终i执行了
* 两次加1,但是i的值不是2,而是1),所以造成最终结果与预期不一致。
*
*
*/
}
}
二、Synchronized的两种用法
2.1 对象锁
包括方法锁(默认锁对象是this当前实例对象)和同步代码块锁(自己指定锁对象)
2.11 方法锁形式
synchronized修饰普通方法,锁对象默认是this
public class SynchronizedObjectMethod3 implements Runnable {
private static SynchronizedObjectMethod3 instance = new SynchronizedObjectMethod3();
@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 t1 = new Thread(instance);
Thread t2 = new Thread(instance);
t1.start();
t2.start();
// while空循环的作用是,让t1,t2线程执行结束后再执行主线程
while (t1.isAlive() || t2.isAlive()) {
}
System.out.println("finished");
}
}
运行结果
我是对象锁的方法修饰方式,我叫 Thread-0
Thread-0 运行结束
我是对象锁的方法修饰方式,我叫 Thread-1
Thread-1 运行结束
finished
2.12 代码块锁形式
第一种,使用this作为锁对象
public class SynchronizedObjectBlock2 implements Runnable{
private static SynchronizedObjectBlock2 instance = new SynchronizedObjectBlock2();
@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(instance);
Thread t2 = new Thread(instance);
t1.start();
t2.start();
// while空循环的作用是,让t1,t2线程执行结束后再执行主线程
while (t1.isAlive() || t2.isAlive()) {
}
System.out.println("finished");
}
}
运行结果:
我是对象锁的代码块形式,我叫 : Thread-0
Thread-0运行结束
我是对象锁的代码块形式,我叫 : Thread-1
Thread-1运行结束
finished
第二种,使用自定义对象作为锁对象(有多个代码块时,就需要自定义锁对象)
public class SynchronizedObjectBlock2 implements Runnable{
private static SynchronizedObjectBlock2 instance = new SynchronizedObjectBlock2();
Object lock1 = new Object();
Object lock2 = new Object();
@Override
public void run() {
synchronized (lock1) {
System.out.println("我是lock1,我叫 : " + Thread.currentThread().getName());
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + " lock1部分运行结束");
}
synchronized (lock2) {
System.out.println("我是lock2,我叫 : " + Thread.currentThread().getName());
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + " lock2部分运行结束");
}
}
public static void main(String[] args){
Thread t1 = new Thread(instance);
Thread t2 = new Thread(instance);
t1.start();
t2.start();
while (t1.isAlive() || t2.isAlive()) {
}
System.out.println("finished");
}
}
运行结果
我是lock1,我叫 : Thread-1
Thread-1 lock1部分运行结束
我是lock2,我叫 : Thread-1
我是lock1,我叫 : Thread-0
Thread-0 lock1部分运行结束
Thread-1 lock2部分运行结束
我是lock2,我叫 : Thread-0
Thread-0 lock2部分运行结束
finished
2.2 类锁
指的是synchronized修饰静态的方法,或者指定锁为Class对象。
2.21 基础知识点
- Java类可能有很多个对象,但是只有一个Class对象(类类型class type),所谓的类锁,指的就是该类的Class对象的锁
- 类锁使用效果:类锁只能在同一时刻被一个对象拥有。
2.22 类锁的形式
- synchronized加在static方法上
public class SynchronizedClassStatic4 implements Runnable {
private static SynchronizedClassStatic4 instance1 = new SynchronizedClassStatic4();
private static SynchronizedClassStatic4 instance2 = new SynchronizedClassStatic4();
@Override
public void run() {
method();
}
// static方法
public static synchronized void method() {
System.out.println("我是类锁的第一种形式 :static形式,我叫 " + 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(instance1);
Thread t2 = new Thread(instance2);
t1.start();
t2.start();
// while空循环的作用是,让t1,t2线程执行结束后再执行主线程
while (t1.isAlive() || t2.isAlive()) {
}
System.out.println("finished");
}
}
运行结果:
我是类锁的第一种形式 :static形式,我叫 Thread-0
Thread-0 运行结束
我是类锁的第一种形式 :static形式,我叫 Thread-1
Thread-1 运行结束
finished
- synchronized(*.class)代码块
public class SynchronizedClassClass5 implements Runnable {
private static SynchronizedClassClass5 instance1 = new SynchronizedClassClass5();
private static SynchronizedClassClass5 instance2 = new SynchronizedClassClass5();
@Override
public void run() {
method();
}
public void method() {
synchronized (SynchronizedClassClass5.class) {
System.out.println("我是类锁的第二种形式:synchronized(*.class)代码块形式,我叫 " + 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(instance1);
Thread t2 = new Thread(instance2);
t1.start();
t2.start();
// while空循环的作用是,让t1,t2线程执行结束后再执行主线程
while (t1.isAlive() || t2.isAlive()) {
}
System.out.println("finished");
}
}
运行结果
我是类锁的第二种形式:synchronized(*.class)代码块形式,我叫 Thread-0
Thread-0 运行结束
我是类锁的第二种形式:synchronized(*.class)代码块形式,我叫 Thread-1
Thread-1 运行结束
finished
现在可以对1.2中的代码进行优化
- 使用对象锁的synchronized修饰普通方法形式
@Override
public synchronized void run() {
for (int j = 0; j < 100000; j++) {
i ++;
}
}
- 使用对象锁的代码块形式
@Override
public void run() {
synchronized (this) {
for (int j = 0; j < 100000; j++) {
i ++;
}
}
}
- 使用类锁的synchronized(*.class)代码块形式
@Override
public void run() {
synchronized (DisappearRequest1.class) {
for (int j = 0; j < 100000; j++) {
i ++;
}
}
}
- 使用类锁的synchronized修饰zestatic方法形式
@Override
public void run() {
count();
}
public synchronized static void count(){
for (int j = 0; j < 100000; j++) {
i ++;
}
}
以上优化后的代码最终输出结果都是:200000
三、sychonized集几种常见面试题
3.1 多线程访问(synchronized)同步方法的七种情况
- 两个线程同时访问一个对象的同步方法(串行)
- 两个线程访问的是两个对象的同步方法(并行:锁对象不一致)
- 两个线程访问的是sychronized的静态方法(串行:类锁,锁对象一致)
- 同时访问同步和非同步的方法(并行:synchronized只会对被修饰的方法起作用)
- 访问一个类的不同的普通(非static)同步方法(串行:此种情况是对象锁,因此两个同步方法拿到的锁对象是一致的)
- 同时访问静态的sychronized和非静态的synchronized方法(并行:前者是对象锁this,后者是类锁*.class)
- sychronized方法抛出异常后,会释放锁对象吗?(JVM会释放锁)
- 当一个同步方法中调用了另一个非同步方法的时候,该同步方法还是线程安全的吗?(不安全)
3.2 对以上七种情况的总结
- 一把锁同时只能被一个线程获取,没有拿到锁的线程必须等待(对应第1、5种情况)
- 每个实例都应有自己的一把锁,不同实例之间互不影响;例外:如果是类锁的时候,所有对象共用一把锁(对应第2、3、4、6种情况)
- 无论是方法正常执行完毕或者是方法抛出异常,都会释放锁对象(第7种情况)
四、Synchronized的性质
4.1 可重入性质
- 定义:可重入指的的是同一线程的外层函数获得锁之后,内层函数可以直接在此获取该锁。(可重入锁也叫递归锁)
- 好处:避免死锁,提升封装性
- 粒度(范围):线程而非调用(只要是在同一个线程内,都可以满足可重入性质,即无需释放锁、重新获取锁)
粒度总结:
- 同一个方法是可重入的,即递归调用同一个方法,是满足可重入性质的
- 可重入不要求是同一个方法,即一个同步方法调用其他同步方法,依然满足可重入性质
- 可重入不要求是同一个类,即一个类的同步方法调用其他类的同步方法也满足可重入性质
4.2 不可中断性质
- 定义:一旦这个锁已经被别人获得了,如果我还想获得,我只能选择等待或者阻塞,知道别的线程释放这个锁,如果别人永远不释放,那我只能永远等待下去。