前言
前几篇博客算是总结完成了Java中多线程的基础内容,但是基础内容中缺失了synchronized关键字的角色,这里还是补充介绍一下,到这里Java多线程中基础中的基础算是完成了,后面会简单介绍一下JMM之后,开始学习总结各种JUC的工具类。
如果对Java多线程稍微有了解的,都应该知道synchronized关键字,这个关键字能够保证同一时刻最多只有一个线程执行其保护的代码,以达到保证并发安全的效果。
对象锁&类锁
针对synchronized这个关键字可以修饰方法,属性,代码块,从其基础使用层面来看,其有两种大的方式,一种是类锁,一种是对象锁。从字面意思理解并不能完全明白其含义,还是通过实例吧
对象锁
对象锁的形式包括方法锁(这个时候锁对象为this)和同步代码块(自己指定锁对象)两种形式
1、同步代码块
/**
* autor:liman
* createtime:2021-10-17
* comment:对象锁-同步代码块
*/
public class ObjectLockBlock implements Runnable {
private static ObjectLockBlock instance = new ObjectLockBlock();
Object lock = new Object();
public static void main(String[] args) {
Thread threadOne = new Thread(instance);
Thread threadTwo = new Thread(instance);
threadOne.start();
threadTwo.start();
//循环等待两个线程运行完毕
while(threadOne.isAlive() || threadTwo.isAlive()){
}
System.out.println("all thread finished");
}
@Override
public void run() {
//synchronize代码块,默认对象锁就是当前这个对象,也可以指定任意对象
synchronized (this) {
System.out.println("对象锁的代码块,当前线程为:" + Thread.currentThread().getName());
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "运行结束");
}
}
}
2、修饰方法
/**
* autor:liman
* createtime:2021-10-17
* comment:对象锁示例2 方法锁
*/
public class ObjectLockMethod implements Runnable {
private static ObjectLockMethod instance = new ObjectLockMethod();
Object lock = new Object();
@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 threadOne = new Thread(instance);
Thread threadTwo = new Thread(instance);
threadOne.start();
threadTwo.start();
//简单粗暴的等待线程运行的方法
while(threadOne.isAlive() || threadTwo.isAlive()){
}
System.out.println("all thread finished");
}
}
类锁
与对象锁相对应,synchronized修饰的静态方法,和指定类对象为锁对象的方式,这种方式下锁对象为Class对象
1、synchronized修饰静态方法
/**
* autor:liman
* createtime:2021-10-17
* comment:类锁的第一种形式,static形式
*/
public class ClassLockStatic implements Runnable{
private static ClassLockStatic instance1 = new ClassLockStatic();
private static ClassLockStatic instance2 = new ClassLockStatic();
@Override
public void run() {
method();
}
//修饰静态方法,当前的锁对象为 ClassLockStatic.class
public synchronized static 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 threadOne = new Thread(instance1);
Thread threadTwo = new Thread(instance2);
threadOne.start();
threadTwo.start();
while(threadOne.isAlive() || threadTwo.isAlive()){
}
System.out.println("all thread finished");
}
}
2、指定类对象为锁对象
/**
* autor:liman
* createtime:2021-10-17
* comment:类锁的第二种形式,synchronized(*.class)
*/
public class ClassLockStaticBlock implements Runnable{
private static ClassLockStaticBlock instance1 = new ClassLockStaticBlock();
private static ClassLockStaticBlock instance2 = new ClassLockStaticBlock();
@Override
public void run() {
method();
}
public void method(){
//这里直接指定类锁
synchronized (ClassLockStaticBlock.class) {
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 threadOne = new Thread(instance1);
Thread threadTwo = new Thread(instance2);
threadOne.start();
threadTwo.start();
//这里是一种简单的等待线程运行结束的方式【不推荐】
while(threadOne.isAlive() || threadTwo.isAlive()){
}
System.out.println("all thread finished");
}
}
上述四种情况下,线程都能正常排队运行,这里就不贴出运行结果了。
一些复杂情况
1、对象锁情况下,如果多个线程访问不同的同步方法,依旧会同步运行
/**
* autor:liman
* createtime:2021-10-17
* comment:同时访问不同的同步方法
*/
public class SynchronizedMethodAndYesSame implements Runnable{
private static SynchronizedMethodAndYesSame instance = new SynchronizedMethodAndYesSame();
@Override
public void run() {
//不同线程调用的方法不同
String threadName = Thread.currentThread().getName();
if("Thread-0".equals(threadName)){
syncMethod();
}else{
otherSyncMethod();
}
}
//同步方法1
public synchronized void syncMethod(){
System.out.println("这个是同步加锁的方法,当前线程为:" + Thread.currentThread().getName());
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "运行结束");
}
//同步方法2
public synchronized void otherSyncMethod(){
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 threadOne = new Thread(instance);
Thread threadTwo = new Thread(instance);
threadOne.start();
threadTwo.start();
while(threadOne.isAlive() || threadTwo.isAlive()){
}
System.out.println("all thread finished");
}
}
两个线程依旧能同步运行
这里需要补充一下,这种情况下两个线程依旧会出现同步运行的情况,但是如果其中一个方法抛出异常了,是会释放锁的,如果syncMethod方法改成如下代码
public synchronized void syncMethod(){
System.out.println("这个是同步加锁的方法,当前线程为:" + Thread.currentThread().getName());
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
//这里抛出异常
throw new RuntimeException("测试异常,释放锁");
}
这个异常的抛出,是会释放锁的,锁释放之后,会让另一个线程进入其他同步方法运行。
2、对象锁情况下,一个方法加锁,一个方法不加锁,不同的线程调用不同的方法,并不会出现同步的情况
/**
* autor:liman
* createtime:2021-10-17
* comment:同时访问同步方法和非同步方法
*/
public class SynchronizedMethodAndNoSame implements Runnable{
private static SynchronizedMethodAndNoSame instance = new SynchronizedMethodAndNoSame();
@Override
public void run() {
//不同线程调用不用的方法
String threadName = Thread.currentThread().getName();
if("Thread-0".equals(threadName)){
syncMethod();
}else{
noSyncMethod();
}
}
//加锁的同步方法
public synchronized void syncMethod(){
System.out.println("这个是同步加锁的方法,当前线程为:" + Thread.currentThread().getName());
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "运行结束");
}
//没有加锁
public void noSyncMethod(){
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 threadOne = new Thread(instance);
Thread threadTwo = new Thread(instance);
threadOne.start();
threadTwo.start();
while(threadOne.isAlive() || threadTwo.isAlive()){
}
System.out.println("all thread finished");
}
}
运行结果:
这种情况,并不会出现同步互斥的情况,而是出现并行运行。
3、类锁和方法锁,并不是同一个,二者修饰的方法被不同线程调用,并不会出现同步运行的情况。
/**
* autor:liman
* createtime:2021-10-17
* comment:同时访问对象锁的同步方法和类锁的同步方法
*/
public class StaticSynchronizedMethodAndNormalSame implements Runnable{
private static StaticSynchronizedMethodAndNormalSame instance = new StaticSynchronizedMethodAndNormalSame();
@Override
public void run() {
String threadName = Thread.currentThread().getName();
//不同的线程访问不同的方法
if("Thread-0".equals(threadName)){
syncMethod();
}else{
noSyncMethod();
}
}
//类锁的同步方法
public static synchronized void syncMethod(){
System.out.println("这个是同步加锁的方法【静态】,当前线程为:" + Thread.currentThread().getName());
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "运行结束");
}
//对象锁的同步方法
public synchronized void noSyncMethod(){
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 threadOne = new Thread(instance);
Thread threadTwo = new Thread(instance);
threadOne.start();
threadTwo.start();
while(threadOne.isAlive() || threadTwo.isAlive()){
}
System.out.println("all thread finished");
}
}
运行结果:
synchronized原理简述
可重入与不可中断
在简述synchronized原理之前,先说下synchronized的性质,synchronized锁加的锁是一个可重入的锁(另一个是ReentrantLock),同时也是不可中断的锁
所谓的可重入,指的是同一线程的外层函数获取锁之后,内层函数可以直接再次获取该锁。
所谓的不可中断,指的是一旦这个锁被其他线程获取了,如果当前线程想继续获取该锁,只能选择等待或者阻塞,直到其他线程释放这个锁,如果其他线程永远不释放锁,则当前线程就只能等待
关于synchronized可重入的实例
/**
* autor:liman
* createtime:2021-10-17
* comment:synchronized可重入
*/
public class SynchronizedCanReEnter {
int a = 0;
public synchronized void method01(){
System.out.println("这里是method01,a="+a);
if(0==a){
a++;
method01();
}
//外层method01里头调用了method02同步方法
method02();
}
public synchronized void method02(){
System.out.println("这里是method02");
}
public static void main(String[] args) {
SynchronizedCanReEnter synchronizedCanReEnter = new SynchronizedCanReEnter();
synchronizedCanReEnter.method01();
}
}
并不会出现同步运行的情况,因为synchronized是可重入的锁
原理简述
通过一段代码,编译成字节码看看其原理
/**
* autor:liman
* createtime:2021-10-17
* comment:
*/
public class SyncDeCompileDemo {
private Object object = new Object();
public void syncMethod(){
synchronized (object){
}
}
}
进入到这个类的文件目录下之后,通过如下两行命令得到对应的字节码
javac SyncDeCompileDemo.java
javap -verbose SyncDeCompileDemo.class
对应字节码如下
public void syncMethod();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=2, locals=3, args_size=1
0: aload_0
1: getfield #3 // Field object:Ljava/lang/Object;
4: dup
5: astore_1
6: monitorenter ##获取锁的指令
7: aload_1
8: monitorexit ##正常释放
9: goto 17
12: astore_2
13: aload_1
14: monitorexit ##抛出异常释放
15: aload_2
16: athrow
17: return
Exception table:
from to target type
7 9 12 any
12 15 12 any
LineNumberTable:
line 13: 0
line 15: 7
line 16: 17
StackMapTable: number_of_entries = 2
frame_type = 255 /* full_frame */
offset_delta = 12
locals = [ class com/learn/concurrency/foundataion/synchronizelearn/Sy
ncDeCompileDemo, class java/lang/Object ]
stack = [ class java/lang/Throwable ]
frame_type = 250 /* chop */
offset_delta = 4
}
比较容易看到,synchronized是在需要运行的代码块上加上了monitorenter
和monitorexit
的指令。之前的实例已经证明,synchronized在出现异常的时候,是会释放锁的,因此其一条monitorenter对应两条monitorexit指令。
同时需要说明的是,这个monitorenter和monitorexit指令虽然无法保证原子性,但是在Java的内存模型中,这个指令是保证了变量数据的可见性的。
synchronized缺陷
关于synchronized关键字的缺陷,其实也有一些,无非就是效率低,不够灵活(相对读写锁而言),同时无法保证数据修改的原子性,这个确实没法保证,毕竟monitorenter和monitorexit之间可相隔那么多指令。但是Java在不断的迭代之后,对synchronized也做了很多优化,其中锁膨胀机制的引入使得synchronized的效率也不是那么不堪了。
总结
关于synchronized的内容,先简单介绍上面的内容,后面在学习Java内存模型的时候,还会学习synchronized的内容,后续再进行补充