前言
Synchronized在多线程中使用得比较多的,这两天看了下慕课网相关课程,在此总结下其使用和原理
作用:
Synchronized据有可重入,不可中断性,能够保证在同一时刻最多只有一个线程执行该段代码,以达到保证并发安全的效果,内部是通过monitor来加锁和解锁的。
地位:
1.Synchronized是java的关键字,被java语言原生支持。
2.是最基本的互斥同步手段。
3.是并发编程中的元老级角色,是并发编程的必学内容。
不使用并发的后果
两个线程同时a++,如果不加锁,最后结果会比预计的少
public class DisappearRequest {
int i = 0;
private Runnable runnable = new Runnable() {
@Override
public void run() {
for (int j = 0; j < 100000; j++) {
i++;
}
}
};
@Test
public void test01() throws InterruptedException {
Thread thread = new Thread(runnable);
Thread thread2 = new Thread(runnable);
thread.start();
thread2.start();
thread.join();
thread2.join();
System.out.println(i);
}
}
运行上面代码,期望结果是200000,但是每次运行结果都是小于这个数的。
原因:
i++不是一个原子操作,实际上包含三个动作
1.读取i;
2.将i加1;
3.将i的值写入到内存中。
在多线程环境下这样是不安全的。在解决问题之前,还是先来学习下Synchronized的用法。
Synchronized的两个用法
对象锁
包括方法锁(默认锁对象为this当前实例对象)和同步代码块锁(自己指定的锁对象)
类锁
指synchronized修饰静态的方法或指定锁对象为class对象
一、对象锁
1、代码块形式
public Object lock = new Object();
//代码块锁
public Runnable runnable = new Runnable() {
@Override
public void run() {
synchronized (lock) { //this或者自建对象
System.out.println("我是对象锁的代码块形式,我叫:" + Thread.currentThread().getName());
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + ":运行结束");
}
}
};
@Test
public void test01() {
Thread t1 = new Thread(runnable);
Thread t2 = new Thread(runnable);
t1.start();
t2.start();
while (t1.isAlive() || t2.isAlive()) {
}
System.out.println("finished");
}
对象锁只对同一个对象的方法起到作用,如果是不同对象同一个方法,锁会失效
2、方法锁形式
//方法锁
public Runnable runnable2 = new Runnable() {
@Override
public synchronized void run() {
System.out.println("我是对象锁的方法修饰符形式,我叫:" + Thread.currentThread().getName());
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + ":运行结束");
}
};
@Test
public void test01() {
Thread t1 = new Thread(runnable2);
Thread t2 = new Thread(runnable2);
t1.start();
t2.start();
while (t1.isAlive() || t2.isAlive()) {
}
System.out.println("finished");
}
方法锁形式,锁对象默认为this
二、类锁
概念:java类可能有很多个对象,但只有一个class对象
本质:所以所谓的类锁,不过是Class对象而已。
1、静态方法锁
class RunnableTest implements Runnable{
@Override
public void run() {
method();
}
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() + ":运行结束");
}
}
@Test
public void test01() {
Thread t1 = new Thread(new RunnableTest());
Thread t2 = new Thread(new RunnableTest());
t1.start();
t2.start();
while (t1.isAlive() || t2.isAlive()) {
}
System.out.println("finished");
}
如果不加static,RunnableTest里面的method方法可以同时运行
2、class对象代码块
//class对象锁
public Runnable runnable5 = new Runnable() {
@Override
public void run() {
synchronized (SynchronizedTest.class){
System.out.println("我是对象锁的class对象修饰符形式,我叫:" + Thread.currentThread().getName());
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + ":运行结束");
}
}
};
@Test
public void test01() {
Thread t1 = new Thread(runnable5);
Thread t2 = new Thread(runnable5);
t1.start();
t2.start();
while (t1.isAlive() || t2.isAlive()) {
}
System.out.println("finished");
}
对象锁和类锁学完了,前面的例子就可以很容易的解决了,这里就不多说了。
多线程访问同步方法的7种情况
一、两个线程同时访问一个对象的同步方法
前面的对象锁已经学习过
二、两个线程访问的是两个对象的同步方法
前面的类锁的静态方法同步锁已经学过,如果不加静态的,那么两个线程运行毫无影响,不受干扰
三、两个线程访问的是synchronized的静态方法
加了静态方法就变成类锁,会一个一个执行
四、同时访问同步方法与非同步方法
public class SynchronizedYesAndNoTest {
private Runnable runnable = new Runnable(){
@Override
public void run() {
if(Thread.currentThread().getName().equals("Thread-0")){
method();
}else {
method2();
}
}
};
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 void method2(){
System.out.println("我是没加锁的方法,我叫:" + Thread.currentThread().getName());
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + ":运行结束");
}
@Test
public void test01() {
Thread t1 = new Thread(runnable);
Thread t2 = new Thread(runnable);
t1.start();
t2.start();
while (t1.isAlive() || t2.isAlive()) {
}
System.out.println("finished");
}
}
非同步方法不受影响
五、访问同一个对象的不同的普通同步方法
public class SynchronizedDifferentMethod {
private Runnable runnable = new Runnable(){
@Override
public void run() {
if(Thread.currentThread().getName().equals("Thread-0")){
method();
}else {
method2();
}
}
};
public synchronized void method(){
System.out.println("我是加锁的方法method,我叫:" + Thread.currentThread().getName());
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + ":运行结束");
}
public synchronized void method2(){
System.out.println("我是加锁的方法method2,我叫:" + Thread.currentThread().getName());
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + ":运行结束");
}
@Test
public void test01() {
Thread t1 = new Thread(runnable);
Thread t2 = new Thread(runnable);
t1.start();
t2.start();
while (t1.isAlive() || t2.isAlive()) {
}
System.out.println("finished");
}
}
一个一个执行
六、同时访问静态synchronized和非静态synchronized方法
public class SynchronizedStaticAndNormal {
private Runnable runnable = new Runnable(){
@Override
public void run() {
if(Thread.currentThread().getName().equals("Thread-0")){
method();
}else {
method2();
}
}
};
public static synchronized void method(){
System.out.println("我是静态加锁的方法method,我叫:" + Thread.currentThread().getName());
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + ":运行结束");
}
public synchronized void method2(){
System.out.println("我是非静态加锁的方法method2,我叫:" + Thread.currentThread().getName());
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + ":运行结束");
}
@Test
public void test01() {
Thread t1 = new Thread(runnable);
Thread t2 = new Thread(runnable);
t1.start();
t2.start();
while (t1.isAlive() || t2.isAlive()) {
}
System.out.println("finished");
}
}
不受影响,同时进行
七、方法抛出异常后,会释放锁
public class SynchronizedException {
private Runnable runnable = new Runnable(){
@Override
public void run() {
if(Thread.currentThread().getName().equals("Thread-0")){
method();
}else {
method2();
}
}
};
public synchronized void method(){
System.out.println("我是方法method,我叫:" + Thread.currentThread().getName());
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
throw new RuntimeException();
// System.out.println(Thread.currentThread().getName() + ":运行结束");
}
public synchronized void method2(){
System.out.println("我是方法method2,我叫:" + Thread.currentThread().getName());
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + ":运行结束");
}
@Test
public void test01() {
Thread t1 = new Thread(runnable);
Thread t2 = new Thread(runnable);
t1.start();
t2.start();
while (t1.isAlive() || t2.isAlive()) {
}
System.out.println("finished");
}
}
抛出异常后,jvm会自动释放锁
八、七种情况总结
1.一把锁只能同时被一个线程获取,没有拿到锁的线程必须等待(对应第1,5种情况)
2.每个实例都对应有自己的一把锁,不同实例之前互不影响;例外:锁对象是*.class以及synchronized修饰的是static方法的时候,所有对象共用同一把类锁(对应第2,3,4,6种情况)
3.无论是正常执行完毕或者抛出异常,都会释放锁。
Synchronized的性质
一、可重入性
定义:指的是同一个线程的外层函数获得锁之后,内层函数可以直接再次获取到锁。(Synchronized,Lock)
好处:避免死锁、提升封装性
粒度:线程而非调用(用3种情况来说明和pthread的区别)
1、情况1:证明同一个方法是可重入的
public class SynchronizedRecursion {
private int a = 0;
private synchronized void method(){
System.out.println("这是method1,a = " + a);
if(a==0){
a ++;
method();
}
}
@Test
public void test01(){
method();
}
}
递归调用自己可以成功
2、情况2:证明可重入不要求是同一个方法
public class SynchronizedOtherMethod {
private synchronized void method(){
System.out.println("这是method1");
method2();
}
private synchronized void method2(){
System.out.println("这是method2");
}
@Test
public void test01(){
method();
}
}
3、情况3:证明可重入不要是同一个类种的
public class SynchronizedSuperClass {
@Test
public void test01(){
TestClass testClass = new TestClass();
testClass.method();
}
}
class SuperClass{
public synchronized void method(){
System.out.println("我是父类的方法");
}
}
class TestClass extends SuperClass{
@Override
public synchronized void method() {
super.method();
System.out.println("我是子类的方法");
}
}
二、不可中断性
一旦这个锁已经被别人获取到了,如果我还想获得,我只能选择等待或者阻塞,直到别的线程释放这个锁。如果别人永远不释放锁,那么我就只能永远等待下去。
相比之下,Lock可以拥有中断的能力,第一点,如果我觉得我等的时间太长,有权中断现在已经获取到锁的线程的执行;第二点,如果我等待的时间太长了不想再等了,也可以退出。
原理
一、加锁和释放锁的原理
获取和释放的时机:内置锁,每个java对象可以用做实现同步的锁,这个锁就是内置锁,或者称做监视锁。
表达成lock的形式
public class SynchronizedToLock {
Lock lock = new ReentrantLock();
public synchronized void method1(){
System.out.println("我是Synchronized形式的锁");
}
public void method2(){
lock.lock();
try{
System.out.println("我是lock形式的锁");
}finally {
lock.unlock();
}
}
@Test
public void test01(){
method1();
method2();
}
}
深入JVM看字节码
javac 编译成class文件
javap [-verbose] 反编译
//java文件
public class Decompilation {
private Object object = new Object();
public void insert(Thread thread){
synchronized (object){
}
}
}
//反编译之后的文件
Last modified 2020-8-9; size 503 bytes
MD5 checksum 1832d1176898be312930160e30a29bf2
Compiled from "Decompilation.java"
public class com.example.threadlocaldemo.Decompilation
minor version: 0
major version: 52
flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
#1 = Methodref #2.#20 // java/lang/Object."<init>":()V
#2 = Class #21 // java/lang/Object
#3 = Fieldref #4.#22 // com/example/threadlocaldemo/Decompilation.object:Ljava/lang/Object;
#4 = Class #23 // com/example/threadlocaldemo/Decompilation
#5 = Utf8 object
#6 = Utf8 Ljava/lang/Object;
#7 = Utf8 <init>
#8 = Utf8 ()V
#9 = Utf8 Code
#10 = Utf8 LineNumberTable
#11 = Utf8 insert
#12 = Utf8 (Ljava/lang/Thread;)V
#13 = Utf8 StackMapTable
#14 = Class #23 // com/example/threadlocaldemo/Decompilation
#15 = Class #24 // java/lang/Thread
#16 = Class #21 // java/lang/Object
#17 = Class #25 // java/lang/Throwable
#18 = Utf8 SourceFile
#19 = Utf8 Decompilation.java
#20 = NameAndType #7:#8 // "<init>":()V
#21 = Utf8 java/lang/Object
#22 = NameAndType #5:#6 // object:Ljava/lang/Object;
#23 = Utf8 com/example/threadlocaldemo/Decompilation
#24 = Utf8 java/lang/Thread
#25 = Utf8 java/lang/Throwable
{
public com.example.threadlocaldemo.Decompilation();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=3, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: aload_0
5: new #2 // class java/lang/Object
8: dup
9: invokespecial #1 // Method java/lang/Object."<init>":()V
12: putfield #3 // Field object:Ljava/lang/Object;
15: return
LineNumberTable:
line 3: 0
line 5: 4
public void insert(java.lang.Thread);
descriptor: (Ljava/lang/Thread;)V
flags: ACC_PUBLIC
Code:
stack=2, locals=4, args_size=2
0: aload_0
1: getfield #3 // Field object:Ljava/lang/Object;
4: dup
5: astore_2
6: monitorenter //monitorenter和monitorexit两个指令实现加锁和释放锁
7: aload_2
8: monitorexit
9: goto 17
12: astore_3
13: aload_2
14: monitorexit
15: aload_3
16: athrow
17: return
Exception table:
from to target type
7 9 12 any
12 15 12 any
LineNumberTable:
line 8: 0
line 10: 7
line 11: 17
StackMapTable: number_of_entries = 2
frame_type = 255 /* full_frame */
offset_delta = 12
locals = [ class com/example/threadlocaldemo/Decompilation, class java/lang/Thread, class java/lang/Object ]
stack = [ class java/lang/Throwable ]
frame_type = 250 /* chop */
offset_delta = 4
monitorenter和monitorexit两个指令实现加锁和释放锁
二、可重入原理:加锁次数计数器
1.jvm负责跟踪对象被加锁的次数
2.线程第一次给对象加锁的时候,计数变为1。每当这个相同的线程在此对象上再次获得锁时,计数会递增(可重入)。
3.每当任务离开时,计数递减,当计数为0的时候,锁会被完全释放。
三、保存可见性的原理:内存模型
Synchonized对象释放锁前,任何对象的修改都会被写入到主内存,保证每次执行都是可靠的,保证可见性
缺陷
效率低:锁的释放情况少、试图获得锁时不能设定超时、不能中断一个正在试图获得锁的线程
不够灵活(读写锁更灵活):加锁和释放的时机单一,每个锁仅有单一的条件(某个对象),可能是不够的
无法知道是否成功获取到锁
常见问题
1.使用注意点:锁对象不能为空(锁信息是保存在对象头中,锁为空,那么锁信息就没有了)、作用域不宜过大、避免死锁
2.如何选择Lock和Synchronized关键字
如果可以两个都不要用,而是选择java.util.courrent包中的类,优先Synchronized,避免出错的发生
3.多线程访问同步方法的各种情况
思考
1.多个线程等待同一个