java并发之synchronized详解
Posted sxkgeek
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了java并发之synchronized详解相关的知识,希望对你有一定的参考价值。
前言
多个线程访问同一个类的synchronized方法时, 都是串行执行的 ! 就算有多个cpu也不例外 ! synchronized方法使用了类java的内置锁, 即锁住的是方法所属对象本身. 同一个锁某个时刻只能被一个执行线程所获取, 因此其他线程都得等待锁的释放. 因此就算你有多余的cpu可以执行, 但是你没有锁, 所以你还是不能进入synchronized方法执行, CPU因此而空闲. 如果某个线程长期持有一个竞争激烈的锁, 那么将导致其他线程都因等待所的释放而被挂起, 从而导致CPU无法得到利用, 系统吞吐量低下.甚至导致死锁的产生, 因此要尽量避免某个线程对锁的长期占有 !
一、修饰方法
方法声明时使用,即一次只能有一个线程进入该方法,其他线程要想在此时调用该方法,只能排队等候
用法
public synchronized void synMethod() {
//方法体
}
demo
public class SyncMethod {
public synchronized void syncMethod2() {
try {
System.out.println("@@@@@@@@@@@@@@@@@@@@@@@@ (syncMethod2, 已经获取内置锁`SyncMethod.this`)");
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("@@@@@@@@@@@@@@@@@@@@@@@@ (syncMethod2, 即将释放内置锁`SyncMethod.this`)");
}
public synchronized void syncMethod1() {
System.out.println("######################## (syncMethod1, 已经获取内置锁`SyncMethod.this`, 并即将退出)");
}
static class Thread1 extends Thread {
SyncMethod syncMethod;
public Thread1(SyncMethod syncMethod) {
this.syncMethod = syncMethod;
}
@Override
public void run() {
syncMethod.syncMethod2();
}
}
static class Thread2 extends Thread {
SyncMethod syncMethod;
public Thread2(SyncMethod syncMethod) {
this.syncMethod = syncMethod;
}
@Override
public void run() {
System.out.println("Thread2 running ...");
syncMethod.syncMethod1();
}
}
public static void main(String[] args) throws InterruptedException {
SyncMethod syncMethod = new SyncMethod();
Thread1 thread1 = new Thread1(syncMethod);
Thread2 thread2 = new Thread2(syncMethod);
thread1.start(); //先执行, 以便抢占锁
Thread.sleep(500); //放弃cpu, 让thread1执行, 以便获的锁
thread2.start(); //在syncMethod1()方法获得锁时, 看看syncMethod2()方法能否执行
}
}
console打印:
@@@@@@@@@@@@@@@@@@@@@@@@ (syncMethod2, 已经获取内置锁`SyncMethod.this`)
Thread2 running ...
@@@@@@@@@@@@@@@@@@@@@@@@ (syncMethod2, 即将释放内置锁`SyncMethod.this`)
######################## (syncMethod1, 已经获取内置锁`SyncMethod.this`, 并即将退出)
上述代码synchronized修饰的方法,锁住的是类的实例化对象syncMethod,所以Thread1执行syncMethod2的方法将syncMethod对象锁住,使得Thread2受到阻塞必须在Thread1释放锁之后才能执行syncMethod1方法。
将上述代码中的Main方法修改如下:
SyncMethod syncMethod1 = new SyncMethod();
SyncMethod syncMethod2 = new SyncMethod();
Thread1 thread1 = new Thread1(syncMethod1);
Thread2 thread2 = new Thread2(syncMethod2);
thread1.start();
Thread.sleep(500);
thread2.start();
console打印:
@@@@@@@@@@@@@@@@@@@@@@@@ (syncMethod2, 已经获取内置锁`SyncMethod.this`)
Thread2 running ...
######################## (syncMethod1, 已经获取内置锁`SyncMethod.this`, 并即将退出)
@@@@@@@@@@@@@@@@@@@@@@@@ (syncMethod2, 即将释放内置锁`SyncMethod.this`)
上述代码中Thread1锁的是syncMethod1对象,而Thread2锁的是syncMethod2对象,所以Thread1线程执行syncMethod2并不会阻塞Thread2
当然还有第二种改进措施:
public class SyncObject {
private Object lock1 = new Object();
private Object lock2 = new Object();
public void syncMethod2() {
synchronized (lock1) {
try {
System.out.println("@@@@@@@@@@@@@@@@@@@@@@@@ (syncMethod2, 已经获取内置锁`SyncMethod.this`)");
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("@@@@@@@@@@@@@@@@@@@@@@@@ (syncMethod2, 即将释放内置锁`SyncMethod.this`)");
}
}
public void syncMethod1() {
synchronized (lock2) {
System.out.println("######################## (syncMethod1, 已经获取内置锁`SyncMethod.this`, 并即将退出)");
}
}
static class Thread1 extends Thread {
SyncObject syncObject;
public Thread1(SyncObject syncObject) {
this.syncObject = syncObject;
}
@Override
public void run() {
syncObject.syncMethod2();
}
}
static class Thread2 extends Thread {
SyncObject syncObject;
public Thread2(SyncObject syncObject) {
this.syncObject = syncObject;
}
@Override
public void run() {
System.out.println("Thread2 running ...");
syncObject.syncMethod1();
}
}
public static void main(String[] args) throws InterruptedException {
SyncObject syncObject = new SyncObject();
Thread1 thread1 = new Thread1(syncObject);
Thread2 thread2 = new Thread2(syncObject);
thread1.start(); //先执行, 以便抢占锁
Thread.sleep(500); //放弃cpu, 让thread1执行, 以便获的锁
thread2.start(); //在syncMethod1()方法获得锁时, 看看syncMethod2()方法能否执行
}
}
下面是一些关于使用锁的一些建议: 为了避免对锁的竞争, 你可以使用锁分解,锁分段以及减少线程持有锁的时间, 如果上诉程序中的syncMethod1和syncMethod2方法是两个不相干的方法(请求的资源不存在关系), 那么这两个方法可以分别使用两个不同的锁。
上面Thread1锁的是对象lock1,而Thread2锁的是对象lock2。
二、修饰静态方法
用法
public synchronized static void method() {
// todo
}
demo
我们知道静态方法是属于类的而不属于对象的。同样的,synchronized修饰的静态方法锁定的是这个类的所有对象。我们对第一节的Demo进行一些修改如下:
class SyncThread implements Runnable {
private static int count;
public SyncThread() {
count = 0;
}
public synchronized static void method() {
for (int i = 0; i < 5; i ++) {
try {
System.out.println(Thread.currentThread().getName() + ":" + (count++));
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public synchronized void run() {
method();
}
}
public static void main(String[] args) {
SyncStatic syncThread1 = new SyncStatic();
SyncStatic syncThread2 = new SyncStatic();
Thread thread1 = new Thread(syncThread1, "SyncThread1");
Thread thread2 = new Thread(syncThread2, "SyncThread2");
thread1.start();
thread2.start();
}
console打印:
SyncThread1:0
SyncThread1:1
SyncThread1:2
SyncThread1:3
SyncThread1:4
SyncThread2:5
SyncThread2:6
SyncThread2:7
SyncThread2:8
SyncThread2:9
印证了我们刚开始说的
synchronized修饰的静态方法锁定的是这个类的所有对象
三、修饰代码块
当两个并发线程(thread1和thread2)访问同一个对象(syncThread)中的synchronized代码块时,在同一时刻只能有一个线程得到执行,另一个线程受阻塞,必须等待当前线程执行完这个代码块以后才能执行该代码块。Thread1和thread2是互斥的,因为在执行synchronized代码块时会锁定当前的对象,只有执行完该代码块才能释放该对象锁,下一个线程才能执行并锁定该对象。
class SyncThread implements Runnable {
private static int count;
public SyncThread() {
count = 0;
}
public void run() {
synchronized(this) {
for (int i = 0; i < 5; i++) {
try {
System.out.println(Thread.currentThread().getName() + ":" + (count++));
Thread.sleep(100);
//this.wait(100);释放锁,其他线程可以执行
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
public int getCount() {
return count;
}
}
//main函数调用
SyncThread的调用:
SyncThread syncThread = new SyncThread();
Thread thread1 = new Thread(syncThread, "SyncThread1");
Thread thread2 = new Thread(syncThread, "SyncThread2");
thread1.start();
thread2.start();
console打印:
SyncThread1:0
SyncThread1:1
SyncThread1:2
SyncThread1:3
SyncThread1:4
SyncThread2:5
SyncThread2:6
SyncThread2:7
SyncThread2:8
SyncThread2:9
值得注意的是:
当一个线程访问对象的一个synchronized(this)同步代码块时,另一个线程仍然可以访问该对象中的非synchronized(this)同步代码块。
给指定的对象加锁:
public void method(SomeObject obj)
{
//obj 锁定的对象
synchronized(obj)
{
// todo
}
}
四、修饰类
public class SyncClass {
public void methodA(){
try {
synchronized (SyncClass.class){
System.out.println("methodA begin 线程名称:"+Thread.currentThread().getName()+"times:"+System.currentTimeMillis());
Thread.sleep(3000);
System.out.println("methodA end 线程名称:"+Thread.currentThread().getName()+"times:"+System.currentTimeMillis());
}
}catch (InterruptedException e){
e.printStackTrace();
}
}
public void methodB(){
synchronized (SyncClass.class){
System.out.println("methodB begin 线程名称:"+Thread.currentThread().getName()+"times:"+System.currentTimeMillis());
System.out.println("methodB end 线程名称:"+Thread.currentThread().getName()+"times:"+System.currentTimeMillis());
}
}
static class Thread1 extends Thread {
private SyncClass syncClass;
public Thread1(SyncClass syncClass) {
super();
this.syncClass = syncClass;
}
@Override
public void run() {
syncClass.methodA();
}
}
static class Thread2 extends Thread {
private SyncClass syncClass;
public Thread2(SyncClass syncClass) {
super();
this.syncClass = syncClass;
}
@Override
public void run() {
syncClass.methodB();
}
}
public static void main(String[] args) {
SyncClass syncClass1 = new SyncClass();
SyncClass syncClass2 = new SyncClass();
Thread1 thread1=new Thread1(syncClass1);
Thread2 thread2 = new Thread2(syncClass2);
thread1.setName("A");
thread2.setName("B");
thread1.start();
thread2.start();
}
}
console打印:
methodA begin 线程名称:Atimes:1533208268430
methodA end 线程名称:Atimes:1533208271431
methodB begin 线程名称:Btimes:1533208271431
methodB end 线程名称:Btimes:1533208271431
由打印结果以及与第一节改进1结果相比可得结论:
synchronized作用于一个类T时,是给这个类T加锁,T的所有实例化对象用的是同一把锁。
所以才会出现methodB在等methodA执行完毕才执行,收到阻塞。
五、synchronized原理
修饰静态代码块
将一个synchronized静态代码块反编译会看到两个专有名词
monitorenter
每个对象有一个监视器锁(monitor)。当monitor被占用时就会处于锁定状态,线程执行monitorenter指令时尝试获取monitor的所有权,过程如下:
1、如果monitor的进入数为0,则该线程进入monitor,然后将进入数设置为1,该线程即为monitor的所有者。
2、如果线程已经占有该monitor,只是重新进入,则进入monitor的进入数加1.
3.如果其他线程已经占用了monitor,则该线程进入阻塞状态,直到monitor的进入数为0,再重新尝试获取monitor的所有权。
monitorexit:
执行monitorexit的线程必须是objectref所对应的monitor的所有者。
指令执行时,monitor的进入数减1,如果减1后进入数为0,那线程退出monitor,不再是这个monitor的所有者。其他被这个monitor阻塞的线程可以尝试去获取这个 monitor 的所有权。
总结:
通过这两段描述,我们应该能很清楚的看出Synchronized的实现原理,Synchronized的语义底层是通过一个monitor的对象来完成,其实wait/notify等方法也依赖于monitor对象,这就是为什么只有在同步的块或者方法中才能调用wait/notify等方法,否则会抛出java.lang.IllegalMonitorStateException的异常的原因。
修饰方法
将一个synchronized同步方法反编译:
ACC_SYNCHRONIZED:
从反编译的结果来看,方法的同步并没有通过指令monitorenter和monitorexit来完成(理论上其实也可以通过这两条指令来实现),不过相对于普通方法,其常量池中多了ACC_SYNCHRONIZED标示符。JVM就是根据该标示符来实现方法的同步的:当方法调用时,调用指令将会检查方法的 ACC_SYNCHRONIZED 访问标志是否被设置,如果设置了,执行线程将先获取monitor,获取成功之后才能执行方法体,方法执行完后再释放monitor。在方法执行期间,其他任何线程都无法再获得同一个monitor对象。 其实本质上没有区别,只是方法的同步是一种隐式的方式来实现,无需通过字节码来完成。
六、总结
- 无论synchronized关键字加在方法上还是对象上,如果它作用的对象是非静态的,则它取得的锁是对象;如果synchronized作用的对象是一个静态方法或一个类,则它取得的锁是对类,该类所有的对象同一把锁。
- 每个对象只有一个锁(lock)与之相关联,谁拿到这个锁谁就可以运行它所控制的那段代码。
- 实现同步是要很大的系统开销作为代价的,甚至可能造成死锁,所以尽量避免无谓的同步控制。
以上是关于java并发之synchronized详解的主要内容,如果未能解决你的问题,请参考以下文章