一、Synchronized同步锁回顾
1.锁介绍
分类:
1.乐观锁:以其他的方式实现了线程安全,实际无锁的操作。
2.悲观锁:真正意义的锁。
在Java中每个对象或类都可以当做锁使用,这些锁称为内置锁。
Java中内置锁都是互斥锁。也就是说一个线程获取到锁,其他线程必须等待或阻塞。 如果占用锁的线程不释放锁,其他线程将一直等待下去。锁在同一时刻,只能被一个线程持有。
如果锁是作用于对象,称对象锁。如果锁作用整个类称为类锁。
2.synchronized介绍
-
synchronized是Java中的关键字。使用synchronized关键字是锁的一种实现。
-
synchronized的加锁和解锁过程不需要程序员手动控制,只要执行到synchronized作用范围会自动加锁(获取锁/持有锁),执行完成后会自动解锁(释放锁)。加锁范围中的代码出现异常,自动解锁。
-
synchronized可以保证可见性,因为每次执行到synchronized代码块时会清空线程区(工作内存|高速缓存)。
-
synchronized 会不禁用指令重排,但可以保证有序性。因为同一个时刻只有一个线程能操作。
-
synchronized 可以保证原子性,一个线程的操作一旦开始,就不会被其他线程干扰,只能当前线程执行完,其他线程才可以执行。
-
synchronized 在Java老版本中属于重量级锁(耗费系统资源比较多的锁),随着Java的不停的更新、优化,在Java8中使用起来和轻量级锁(耗费系统资源比较少的锁)已经几乎无差别了。
-
主要分为下面几种情况:
-
修饰实例方法,非静态方法(对象锁) 需要在类实例化后,再进行调用。
-
修饰静态方法(类锁)静态方法属于类级别的方法,静态方法可以类不实例化就使用。
-
修饰代码块(对象锁、类锁)。
-
3.修饰实例方法
锁类型:使用synchronized修饰实例方法时为对象锁。
锁范围:锁范围时加锁的方法。
锁生效:必须为同一个对象调用该方法该锁才有作用。
public class Demo08 {
//创建成员变量
static int a = 0;
public static void main(String[] args) {
//5个线程使用的为同一个对象,该对象锁生效
Demo06 demo06 = new Demo06();
System.out.println(demo06);
//创建5个子线程
for (int i = 0; i < 5; i++) {
new Thread(new Runnable() {
@Override
public void run() {
//五个子线程都调用test()方法
demo06.test();
//输出当前执行线程的名字
System.out.println(Thread.currentThread().getName());
}
}).start();
}
try {
//主线程休眠3秒
Thread.sleep(3000);
//输出a的值
System.out.println(Demo06.a);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//实例方法加锁,锁的范围是加锁的方法,必须为同一个对象调用该方法该锁才会生效
public synchronized void test(){
for (int i = 0; i < 10000; i++) {
a++;
}
}
}
4.修饰静态方法
锁类型:使用synchronized修饰静态方法时为类锁。
锁范围:锁的范围是加锁的方法。
锁生效:该类所有的对象调用加锁方法,锁都会生效。
public class Demo10 {
//创建成员变量
static int a = 0;
public static void main(String[] args) {
//创建5个子线程
for (int i = 0; i < 5; i++) {
new Thread(new Runnable() {
@Override
public void run() {
//五个子线程都调用test()方法
/* 方式一 */
Demo06.test();
/* 方式二 */
/*Demo06 demo06 = new Demo06();
System.out.println(demo06);
demo06.test();*/
//输出当前执行线程的名字
System.out.println(Thread.currentThread().getName());
}
}).start();
}
try {
//主线程休眠3秒
Thread.sleep(3000);
//输出a的值
System.out.println(Demo06.a);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//静态方法加锁,锁的范围是加锁的方法,该类所有的对象调用加锁方法,锁都生效
public synchronized static void test(){
for (int i = 0; i < 10000; i++) {
a++;
}
}
}
5.修饰代码块
语法:
synchronized(锁){
//内容
}
锁代码块是非常重要的地方。添加锁的类型是Object类型。
运行过程:
多线程执行时,每个线程执行到这个代码块时首先会判断是否有其他线程持有这个锁,如果没有,执行synchronized代码块。如果已经有其他线程持有锁,必须等待线程释放锁。当一个线程执行完成synchronized代码块时会自动释放所持有的锁。
1.锁为固定值
当锁为固定值时,每个线程执行到synchronized代码块时都会判断这个锁是否被其他线程持有,哪个线程抢到先执行哪个线程。当抢到的线程执行完synchronized代码块后,会释放锁,其他线程竞争,抢锁,抢到的持有锁,其他没抢到的继续等待。
由于值固定不变,所有的对象调用加锁的代码块,都会争夺锁资源,属于类锁。
2.锁为不同内容
每个线程中的synchronized锁不相同时,相当于没有加锁。
因为没有需要竞争锁的线程,线程执行到synchronized时,直接获取锁,进入到代码块。这样的话会出现线程安全问题。
3.锁为this
当锁为this时,需要看线程中是否为同一个对象调用的包含synchronized所在的方法。这种写法也是比较常见的写法。
1.同一个对象调用加锁方法时:
如果是同一个对象调用synchronized所在方法时,this代表的都是一个对象。this就相当于固定值。所以可以保证结果正确性,属于对象锁。
public class Demo13 {
//创建成员变量
static int a = 0;
public static void main(String[] args) {
Demo06 demo06 = new Demo06();
//创建5个子线程
for (int i = 0; i < 5; i++) {
new Thread(new Runnable() {
@Override
public void run() {
//五个子线程都调用test()方法
demo06.test();
}
}).start();
}
try {
//主线程休眠3秒
Thread.sleep(3000);
//输出a的值
System.out.println(Demo06.a);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public void test(){
//当锁为this时,每个线程执行到synchronized代码块时都会判断这个锁是否被其他线程持有,this就相当于调用加锁代码块的对象,同一个对象调用,锁生效
synchronized (this){
for (int i = 0; i < 10000; i++) {
a++;
}
}
}
}
2.不同对象调用加锁方法时:
如果不是同一个对象调用synchronized所在方法时,this所代表的对象就不同。相当于锁为不同内容时,锁失效。
public class Demo14 {
//创建成员变量
static int a = 0;
public static void main(String[] args) {
//创建5个子线程
for (int i = 0; i < 5; i++) {
new Thread(new Runnable() {
@Override
public void run() {
//五个子线程都调用test()方法
Demo06 demo06 = new Demo06();
demo06.test();
}
}).start();
}
try {
Thread.sleep(3000);
System.out.println(Demo06.a);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public void test(){
//当锁为this时,每个线程执行到synchronized代码块时都会判断这个锁是否被其他线程持有,this就相当于调用加锁代码块的对象,不同对象调用,锁失效
synchronized (this){
for (int i = 0; i < 10000; i++) {
a++;
}
}
}
}
4.锁为class
锁为Class时,是一个标准的类锁,所有的对象调用加锁的代码块都生效。
public class Demo15 {
//创建成员变量
static int a = 0;
public static void main(String[] args) {
//创建5个子线程
for (int i = 0; i < 5; i++) {
new Thread(new Runnable() {
@Override
public void run() {
//五个子线程都调用test()方法
Demo06 demo06 = new Demo06();
demo06.test();
}
}).start();
}
try {
Thread.sleep(3000);
System.out.println(Demo06.a);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public void test(){
//当锁为类时,所有对象执行加锁代码块,锁都生效
synchronized (Demo20.class){
for (int i = 0; i < 10000; i++) {
a++;
}
}
}
}
6.对象锁和类锁(面试题)
当synchronized修饰静态方法或代码块参数为Class时或代码块参数为固定值,锁为类锁,作用整个类。同一个类使用,锁生效。
当synchronized修饰实例方法或代码块参数为this时,为对象锁,只对当前对象有效。
体现在:
多个对象使用时,锁生效,使用类锁。
同一对象使用时,锁生效,使用对象锁。
7.什么是可重入锁(面试题)
某个线程已经获得了某个锁,允许再次获得锁,就是可重入锁。如果不允许再次获得锁就称为不可重入锁。
synchronized为可重入锁。但可重入锁不仅仅只有synchronized。后面还会学习ReentrantLock也是可重入锁。
可重入锁底层原理
可重入锁底层原理特别简单,就是计数器。
当一个线程第一次持有某个锁时会由monitor(监控器)对持有锁的数量加1,当这个线程再次需要碰到这个锁时,如果是可重入锁就对持有锁数量再次加1(如果是不可重入锁,发现持有锁为1了,就不允许多次持有这个锁了,阻塞),当释放锁时对持有锁数量减1,直到减为0,表示完全释放了这个锁。