java synchronized 关键字详解
synchronized定义: java语言的关键字、修饰词,当它用来修饰一个方法或者一段代码块的时候,能保证在同一时刻最多只有一个线程执行该代码。
什么是锁:
java的内置锁:每个java对象都可以用作实现一个同步的锁,这些锁成为内置锁。线程进入同步代码块或者方法的时候回自动获取该锁,在退出同步代码块或者方法的时候,释放锁。获得锁的唯一方法就是进这个锁保护的同步代码块或者方法。
java内置锁是一个互斥的锁。这就意味着最多只有一个线程能够获取该锁,当线程A尝试去获取线程B持有的内置锁时,线程A必须等待获取阻塞,直到B释放锁,如果B一直没有释放,A则一直等待。
参考:java 中的锁 – 偏向锁、轻量级锁、自旋锁、重量级锁
synchronized的三种应用方式
synchronized关键字最主要有以下3种应用方式,下面分别介绍
修饰实例方法,作用于当前实例加锁,进入同步代码前要获得当前实例的锁
修饰代码块,指定加锁对象,对给定对象加锁,进入同步代码库前要获得给定对象的锁。
修饰静态方法,作用于当前类对象加锁,进入同步代码前要获得当前类对象的锁
废话不多说,直接上代码:
synchronized同步代码块 :当有两个线程并发访问同一个对象中的synchronized(this)同步代码块时,同一时间只能有一个线程执行,另一个需等待前一个线程执行完成后才能执行:
public class Thread1 implements Runnable {
@Override
public void run() {
synchronized (this){
for (int i = 0; i < 5; i++) {
System.out.println(Thread.currentThread().getName() + " synchronized loop " + i);
}
}
}
public static void main(String[] args) {
Thread1 thread1 = new Thread1();
Thread threadA = new Thread(thread1,"threadA");
Thread threadB = new Thread(thread1,"threadB");
threadA.start();
threadB.start();
}
}
执行结果:
threadB synchronized loop 0
threadB synchronized loop 1
threadB synchronized loop 2
threadB synchronized loop 3
threadB synchronized loop 4
threadA synchronized loop 0
threadA synchronized loop 1
threadA synchronized loop 2
threadA synchronized loop 3
threadA synchronized loop 4
被synchronize修饰的代码块同一时间纸杯一个线程访问了,另一个线程在等待前一个线程执行完成后才执行。我们看看不加关键字的执行情况:
threadB synchronized loop 0
threadA synchronized loop 0
threadB synchronized loop 1
threadB synchronized loop 2
threadA synchronized loop 1
threadA synchronized loop 2
threadA synchronized loop 3
threadA synchronized loop 4
threadB synchronized loop 3
threadB synchronized loop 4
这时看到的就是乱序的执行,两个线程都能随时执行。
当一个线程访问object的一个synchronize(this) 同步代码块时,另一个线程任然可以访问该object中的非synchronize(this)同步代码块。
public class Thread2 {
public void test1(){
synchronized (this){
int i =5;
while ((i-- > 0)) {
System.out.println(Thread.currentThread().getName() + ":" + i);
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
public void test2(){
int i =5;
while ((i-- > 0)) {
System.out.println(Thread.currentThread().getName() + ":" + i);
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public static void main(String[] args) {
final Thread2 thread2 = new Thread2();
Thread threadA = new Thread(new Runnable() {
@Override
public void run() {
thread2.test1();
}
},"threadA");
Thread threadB = new Thread(new Runnable() {
@Override
public void run() {
thread2.test2();
}
},"threadB");
threadA.start();
threadB.start();
}
}
执行结果:
threadA:4
threadB:4
threadA:3
threadB:3
threadB:2
threadA:2
threadB:1
threadA:1
threadB:0
threadA:0
可以看到只有被synchronize修饰的方法才会被同步,而类则不会受影响。
修饰静态方法:当修饰静态方法时,其锁就是当前类的class对象锁。由于静态成员不属于任何一个实例对象,是类的成员,因此通过class对象锁可以同步静态成员的并发操作。需要注意的是,修饰该类中的非静态方法时,不会同步并发操作,因为非静态方法的锁时当前实例对象锁。
public class Thread3 implements Runnable{
private static int i = 0;
/**
* 作用于静态方法,锁是当前class对象,也就是
* AccountingSyncClass类对应的class对象
*/
public static synchronized void increase(){
i ++;
System.out.println(Thread.currentThread().getName()+ ":" + i);
}
/**
* 非静态,访问时锁不一样不会发生互斥
*/
public synchronized void increase4Obj(){
i++;
}
@Override
public void run() {
for (int i1 = 0; i1 < 5; i1++) {
increase();
}
}
public static void main(String[] args) {
Thread3 thread3 = new Thread3();
Thread threadA = new Thread(thread3,"threadA");
Thread threadB = new Thread(thread3,"threadB");
threadA.start();
threadB.start();
}
}
运行结果:
threadB:1
threadB:2
threadB:3
threadB:4
threadB:5
threadA:6
threadA:7
threadA:8
threadA:9
threadA:10
由于synchronize关键字修饰的是静态的increase()方法,与修饰实例方法不同的是,其对象锁是当前的class对象。注意代码中的increase4Obj()方法是实例方法,其对象锁是当前实例,如果别的线程调用该方法,将不会产生互斥现象,因为用的是不同的对象锁,但是这种情况就是线程安全问题了,他们都共享了静态变量。
public static void main(String[] args) {
final Thread3 thread3 = new Thread3();
Thread threadA = new Thread(thread3,"threadA");
Thread threadB = new Thread(thread3,"threadB");
Thread threadC = new Thread(new Runnable() {
@Override
public void run() {
for (int i1 = 0; i1 < 5; i1++) {
thread3.increase4Obj();
}
}
}, "threadC");
threadA.start();
threadB.start();
threadC.start();
}
运行结果:
threadA:2
threadC:2
threadA:3
threadC:4
threadA:5
threadC:6
threadA:7
threadC:8
threadA:9
threadC:10
threadB:11
threadB:12
threadB:13
threadB:14
threadB:15
关于synchronized 可能需要了解的关键点
1.synchronized的可重入性 从锁的互斥设计上来说,当一个线程试图想操作一个有其它线程持有的对象锁的临界资源时,将会处于阻塞状态,但当一个线程再次请求自己持有对象锁的临界资源时,这种情况属于重入锁,请求将会成功,在java中synchronize是基于原子性的内部锁机制,是可以重入的,因此在线程调用synchronize方法的同时,在其方法体内部调用该对象另一个synchronize方法,也就是说一个线程得到一个对象锁后再次请求该对象锁,是允许的,这就是synchronize的可重入性。如下:
public class Thread4 implements Runnable {
static Thread4 thread4 = new Thread4();
int x = 0;
int y = 0;
@Override
public void run() {
for (int i = 0; i < 5; i++) {
x ++;
// System.out.println(Thread.currentThread().getName()+ " : x=" + x);
increase();
}
}
public synchronized void increase(){
y++;
System.out.println(Thread.currentThread().getName() + " : y=" + y);
}
public static void main(String[] args) {
Thread threadA = new Thread(thread4,"threadA");
Thread threadB = new Thread(thread4,"threadB");
threadA.start();
threadB.start();
}
}
运行结果:
threadA : y=1
threadB : y=2
threadB : y=3
threadB : y=4
threadB : y=5
threadB : y=6
threadA : y=7
threadA : y=8
threadA : y=9
threadA : y=10
此时实例中的increase()方法虽然都是使用的该实例对象锁,但是运行结果显示该方法没有被同步,而是被重入。
2.线程中断与synchronized :
线程中断:线程正在运行时打断,在java中,提供了三个方法:
//中断线程(实例方法)
public void Thread.interrupt();
//判断线程是否被中断(实例方法)
public boolean Thread.isInterrupted();
//判断是否被中断并清除当前中断状态(静态方法)
public static boolean Thread.interrupted();
当一个线程处于被阻塞状态或者试图执行一个阻塞操作时,使用==Thread.interrupt()== 方法中断该线程,此时将会抛出==InterruptedException==异常,同时中断状态将会被复位(由中断状态改为非中断状态),如下:
public class Thread5 {
public static void main(String[] args) throws InterruptedException {
Thread thread = new Thread(){
@Override
public void run() {
while (true) {
try {
//当前线程处于阻塞状态,异常必须捕捉处理,无法往外抛出
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
System.out.println("Interruted When Sleep");
boolean interrupted = this.isInterrupted();
System.out.println("interrupted :" + interrupted );
}
}
}
};
thread.start();
TimeUnit.SECONDS.sleep(2);
thread.interrupt();
}
}
运行结果:
Interruted When Sleep
interrupted :false
代码说明:创建一个线程,并在线程的run方法中调用sleep方法进行阻塞,启动后再调用线程的interrupt方法中断线程,抛出InterruptedException异常,此时中断状态也被复位(interrupted :false)。如果在线程未阻塞的情况下调用该方法去中断运行线程时,没有任何影响:
public static void main(String[] args) throws InterruptedException {
Thread thread = new Thread(){
@Override
public void run() {
while (true) {
System.out.println("线程未中断");
}
}
};
thread.start();
TimeUnit.SECONDS.sleep(2);
thread.interrupt();
}
此时调用interrupt方法,而线程未被中断,所以我们需要手段检测中断情况,并结束程序,如下:
public static void main(String[] args) throws InterruptedException {
Thread thread = new Thread(){
@Override
public void run() {
while (true) {
if(this.isInterrupted()){
System.out.println("线程中断");
break;
}
}
System.out.println("已跳出循环,线程中断!");
}
};
thread.start();
TimeUnit.SECONDS.sleep(2);
thread.interrupt();
}
运行结果:
线程中断
已跳出循环,线程中断!
我们在代码中使用了实例方法isInterrupted判断线程是否已被中断,如果被中断将跳出循环以此结束线程,注意非阻塞状态调用interrupt()并不会导致中断状态重置。综合所述,可以简单总结一下中断两种情况,一种是当线程处于阻塞状态或者试图执行一个阻塞操作时,我们可以使用实例方法interrupt()进行线程中断,执行中断操作后将会抛出interruptException异常(该异常必须捕捉无法向外抛出)并将中断状态复位,另外一种是当线程处于运行状态时,我们也可调用实例方法interrupt()进行线程中断,但同时必须手动判断中断状态,并编写中断线程的代码(其实就是结束run方法体的代码)。有时我们在编码时可能需要兼顾以上两种情况,那么就可以如下编写:
public void run(){
try {
//判断当前线程是否已中断,注意interrupted方法是静态的,执行后会对中断状态进行复位
while (!Thread.interrupted()) {
TimeUnit.SECONDS.sleep(2);
}
} catch (InterruptedException e) {
}
}
3.中断与synchronized
事实上线程的中断操作对于正在等待获取的锁对象的synchronized方法或者代码块并不起作用,也就是对于synchronized来说,如果一个线程在等待锁,那么结果只有两种,要么它获得这把锁继续执行,要么它就保存等待,即使调用中断线程的方法,也不会生效。演示代码如下:
public class Thread6 implements Runnable{
/**
* 在构造器中创建新线程并启动获取对象锁
*/
public Thread6() {
//该线程已持有当前实例锁
new Thread(){
@Override
public void run() {
y();
}
}.start();
}
public synchronized void y() {
System.out.println("Trying to call y()");
while (true) {
Thread.yield();
}
}
@Override
public void run() {
while (true) {
if(Thread.interrupted()){
System.out.println("中断线程");
break;
}else {
y();
}
}
}
public static void main(String[] args) throws InterruptedException {
Thread6 thread6 = new Thread6();
Thread threadA = new Thread(thread6,"threadA");
//启动后调用y()方法,无法获取当前实例锁处于等待状态
threadA.start();
TimeUnit.SECONDS.sleep(1);
//中断线程,无法生效
threadA.interrupt();
}
}
运行后,中断线程失败。当程序执行时,我们先初始化了Thread6类,并且执行了Thread6类的构造方法,构造方法中创建了一个子线程,并且线程调用了y()方法,y方法是被synchronize修饰的非静态方法,所以该子线程持有了thread6类的实例对象锁,然后threadA线程执行时,再调用y方法时,锁已被占用,所以只能等待所释放,才能执行,但并不是阻塞,所以调用interrupt()方法时,不能中断线程。
4.等待唤醒机制与synchronized
所谓等待唤醒机制本篇主要指的是notify/notifyAll和wait方法,在使用这3个方法时,必须处于synchronized代码块或者synchronized方法中,否则就会抛出IllegalMonitorStateException异常,这是因为调用这几个方法前必须拿到当前对象的监视器monitor对象,也就是说notify/notifyAll和wait方法依赖于monitor对象,在前面的分析中,我们知道monitor 存在于对象头的Mark Word 中(存储monitor引用指针),而synchronized关键字可以获取 monitor ,这也就是为什么notify/notifyAll和wait方法必须在synchronized代码块或者synchronized方法调用的原因。
synchronized (obj) {
obj.wait();
obj.notify();
obj.notifyAll();
}
需要特别理解的一点是,与sleep方法不同的是wait方法调用完成后,线程将被暂停,但wait方法将会释放当前持有的监视器锁(monitor),直到有线程调用notify/notifyAll方法后方能继续执行,而sleep方法只让线程休眠并不释放锁。同时notify/notifyAll方法调用后,并不会马上释放监视器锁,而是在相应的synchronized(){}/synchronized方法执行结束后才自动释放锁。
水平有限,欢迎批评指正
参考: