synchronized 同步代码块组成
在上一篇文章: 线程安全性的基本概念中,讲到过当多个线程访问状态变量,并且至少有一个线程会修改变量的值时,就需要采用同步机制来协调这些线程对对象的访问。这里说的”同步机制“包括:synchronized关键字、volatile变量、显式锁、ThreadLoacl变量以及原子变量。
今天梳理一下synchronized 关键字,也就是Java的内置锁-同步代码块。
同步代码块由2部分组成:一个作为锁的对象引用,一个作为由这个锁保护的代码块。synchronized修饰的方法就是一个横跨整个方法体的代码块,其中该同步代码块的锁就是方法调用所在的对象。静态的synchronized 方法的锁是Class对象
synchronized (lock) {
// 锁保护的代码块
}
每个Java对象都可以作为一个实现同步的锁。每个线程在进入同步代码块之前会自动获得锁,同时在退出代码块或方法时释放锁,而且不论是正常执行退出还是抛出异常退出,都会释放锁。获得内置锁的唯一途径就是进入由这个锁保护的同步代码块或方法。
内置锁相当于一个互斥锁,这意味着最多只有一个线程能持有这个锁,当线程A持有某内置锁时,线程B也需要获得该锁,线程B则必须等待或者阻塞,直到线程A释放这个锁,否则B将永远等待下去。
示例:
public class MainTest {
private Integer count = 1;
public void test(){
synchronized (this) {
count++;
ThreadUtil.sleep(1);
System.out.println(count);
}
}
public synchronized void test1(){
count++;
ThreadUtil.sleep(1);
System.out.println(count);
}
public void test2(){
synchronized (count) {
System.out.println(count);
}
}
public synchronized static void test3(){
System.out.println("hello world");
}
}
// test 和 test1 方法效果是一致的,test里的同步代码块也就是整个方法体。
重入
当一个线程请求一个其它线程持有的锁时,该线程会阻塞。由于内置锁是可重入的,当一个线程试图获取一个它已经持有的锁时,是可以成功的。重入意味着获取锁的粒度是“线程”,而不是“调用”。重入的一种方法是为每一个锁关联一个获取计数器以及所有者线程。当计数器为0时,表示这个锁未被任何线程持有,一个线程获取一个未被持有的锁时,锁的获取计数器置为1,并表示所有者线程,当线程再次获取这个锁时,计数器递增。线程退出同步代码块时,计数器递减。当计数器为0时,锁被释放。
理解以及示例
上面是《Java并发编程实战》里的一段话,怎么理解呢。
第一部分,锁的对象引用。锁跟对象是关联的,内置锁是指对象的内置锁,每个对象都可以有一个内置锁。谁需要获得这个锁?线程!当一个线程需要访问锁保护的代码块的时候,它需要先获得这个锁。这也就意味着,内置锁只有在至少2个线程的时候才有意义,如果自始至终都是一个线程在执行代码,又如何证明锁的存在呢?内置锁存在的意义就在于互斥性:只有一个线程能持有锁,其余的线程如果也要获取锁,只能等待或阻塞!
2个重点:
一:对象!锁是对象的内置锁。那么如果一个类里有多个synchronized修饰的方法呢。第一个线程访问方法A,取得了内置锁。第二个线程再去调用方法B(A,B两个方法都被synchronized修饰),线程会等待么?按这个理解是需要等待的。一个方法里有多个synchronized代码块呢?访问父类的同名方法呢?
二、线程,只有多个线程才存在因为内置锁的互斥性而产生阻塞,是线程来获取锁。
synchronized 示例:
public class MainTest {
private StringBuilder stringBuilder = new StringBuilder();
public synchronized void doSomething(){
System.out.println("doSomething");
ThreadUtil.sleep(5000L);
}
public synchronized void doAnothing(){
System.out.println("doAnothing");
ThreadUtil.sleep(2000L);
}
public synchronized void invokeDoSomething(){
System.out.println("invoke doSomething method");
doSomething();
}
public void lockOther(){
synchronized (stringBuilder) {
System.out.println("lock stringBuilder");
}
}
public static void main(String[] args) throws Exception{
MainTest mainTest = new MainTest();
Thread thread1 = new Thread(new Runnable() {
@Override
public void run() {
mainTest.doSomething();
}
});
Thread thread2 = new Thread(new Runnable() {
@Override
public void run() {
mainTest.doAnothing();
}
});
Thread thread3 = new Thread(new Runnable() {
@Override
public void run() {
mainTest.invokeDoSomething();
}
});
Thread thread4 = new Thread(new Runnable() {
@Override
public void run() {
mainTest.lockOther();
}
});
thread1.start();
thread2.start();
thread3.start();
thread4.start();
}
}
执行结果:
doSomething
lock stringBuilder
invoke doSomething method
doSomething
doAnothing
输出结果中:第一个doSomething和lock stringBuilder很快就出来了,等了5秒之后输出 invoke doSomething method 和 doSomething 这2行,再等5秒中才输出doAnothing。
说明thread1运行之后,thread2 和 thread3阻塞,但是thread4没有阻塞,它也立即执行。直到thread1执行完毕释放锁,thread3开始执行,thread2继续阻塞,thread3执行完毕释放锁,thread2开始执行。
如果将 invokeDoSomething 方法前面的synchronized 修饰符去掉。再次执行、结果为
doSomething
invoke doSomething method
lock stringBuilder
doSomething
doAnothing
前3行几乎是同时出现,等待5秒钟之后输出doSomething,再等待5秒钟接着输出doAnothing。
thread1执行之后,invokeDoSomething 方法立刻执行,执行了System.out.println(“invoke doSomething method”); 这句话,但是在执行doSomething() 这一句时,它必须等待上一个线程也就是thread1释放锁。
从上面的例子我们可以看到,thread1,thread2, thread3都是竞争mainTest对象的锁。而thread4获取的是stringBuilder这个对象的内置锁。
内置锁是跟对象关联的,多个线程获取同一个对象的内置锁,则需要等待,符合内置锁的互斥性。但是获取同一个类里的不同的锁, 是相互不影响的。当一个非synchronized的方法调用另一个synchronized修饰的同步方法时,只有这一部分调用代码会受到synchronized的互斥性,其余部分则照常执行。
重入示例
public class Widget {
public synchronized void doSomething(){
System.out.println("parent do Something start");
ThreadUtil.sleep(3000L);
System.out.println("parent do Something end");
}
}
public class LoggingWidget extends Widget {
@Override
public synchronized void doSomething(){
System.out.println("calling super.doSomething");
// 如果内置锁不是可重入的,则会死锁
super.doSomething();
}
}
LoggingWidget 是 Widget 的子类,并且重写了父类中的doSomething方法。如果锁不是可重入的,则会死锁。LoggingWidget访问doSomething时,需要获取对象的锁,在调用super.doSomething()时,也要获取对象的锁。如果锁不是可重入的,则在第二次获取锁时会一致阻塞,它在请求一个已经被持有的锁。
这里涉及到内置锁以及父类子类的重写,情况就比较复杂了。父类里的方法可以是synchronized 和非 synchronized的,子类里的方法也可以synchronized 和 非 synchronized的,就产生了4种组合。
// 父类,thread1,thread2,thread3都是synchronized方法, thread4是非synchronized的
public class Widget {
public synchronized void thread1(){
System.out.println(Thread.currentThread().getName()+": parent do thread1 start");
ThreadUtil.sleep(3000L);
System.out.println(Thread.currentThread().getName()+": parent do thread1 end");
}
public synchronized void thread2(){
System.out.println(Thread.currentThread().getName()+": parent do thread2");
}
public synchronized void thread3(){
System.out.println(Thread.currentThread().getName()+": parent do thread3");
}
public void thread4(){
System.out.println(Thread.currentThread().getName()+": parent do thread4");
}
}
public class LoggingWidget extends Widget {
// 重写父类的synchronized方法,并且自己也是synchronized的
@Override
public synchronized void thread1(){
System.out.println("child thread1");
System.out.println("invoke super.thread1");
super.thread1();
}
// 重写父类的synchronized方法,但是自己并不是synchronized的
@Override
public void thread2(){
System.out.println("child thread2 and finish");
}
// 重写父类的synchronized方法,自己并不是synchronized的,但是方法里调用父类的synchronized方法
@Override
public void thread3(){
System.out.println("child thread3, invoke super.thread3");
super.thread3();
}
// 不重写父类方法,而是直接调用父类的synchronized方法
public void invokeParentThread2(){
System.out.println("child revokeParentThread2, invoke super.thread2");
super.thread2();
}
@Override
public synchronized void thread4(){
System.out.println("child thread4, invoke super.thread4");
super.thread4();
}
}
public class MainTest {
public static void main(String[] args) {
LoggingWidget widget = new LoggingWidget();
Thread thread1 = new Thread(new Runnable() {
@Override
public void run() {
widget.thread1();
}
});
Thread thread2 = new Thread(new Runnable() {
@Override
public void run() {
widget.thread2();
}
});
Thread thread3 = new Thread(new Runnable() {
@Override
public void run() {
widget.thread3();
}
});
Thread thread4 = new Thread(new Runnable() {
@Override
public void run() {
widget.thread4();
}
});
Thread thread5 = new Thread(new Runnable() {
@Override
public void run() {
widget.invokeParentThread2();
}
});
/**
* 第一个执行的线程,是synchronized方法
*/
thread1.start();
ThreadUtil.sleep(100);
/**
* thread2方法并非synchronized方法,也没有调用父类的synchronized方法,两者一起执行,无需等待
*/
thread2.start();
/**
* 子类的thread3是非synchronized方法,但是调用父类的synchronized方法,执行父类方法之前的代码无需等待
* 但是执行父类的synchronized方法时,也需要获得锁,会阻塞
*/
thread3.start();
/**
* 子类的thread4方法是synchronized方法,即使父类的thread4是非synchronized的,它整个方法是需要等待的
*/
thread4.start();
/**
* 直接调用父类的thread2方法(synchronized方法),调用之前的代码无需等待,但是执行父类thread2方法需要等待
*/
thread5.start();
}
}
执行结果
child thread1
invoke super.thread1
Thread-0: parent do thread1 start
child thread2 and finish
child thread3, invoke super.thread3
child revokeParentThread2, invoke super.thread2
// 上面6行是很快输出的, 没有等待。下面5行等待了3秒才出现
Thread-0: parent do thread1 end
child thread4, invoke super.thread4
Thread-3: parent do thread4
Thread-4: parent do thread2
Thread-2: parent do thread3
对于内置锁的可重入,网上有很多说法。例如: 执行子类synchronized方法也会获得父类的锁;只要子类和父类方法中有一个是synchronized的,则方法是synchronized的,令人糊涂。
其实,完全可以抛开子类、父类的圈圈绕绕。因为锁是对象的内置锁,你只要看对象的这个方法有没有加synchronized,没有加,那就无需等待,直接执行;如果方法里调用了super.method,那就看调用的父类方法上有没有加synchronized,加了则这部分代码需要等待锁的释放(锁是对象的锁,不是什么子类,父类的锁),没有加则无需等待;方法没有加synchronized,也没有调用父类的synchronized方法,那整个方法都是不需要同步的。
归根到底,看调的方法有没有加synchronized,不管这个方法是子类重写的还是继承自父类的(子类非synchronized方法重写父类的synchronized方法,方法也是非synchronized的),这个方法自身没有加synchronized则不需要同步,这个方法调用了另外的synchronized方法,则之前的部分不需要同步,执行synchronized方法的那一部分需要同步。
class对象锁
特别地,每个类也会有一个锁,静态的 synchronized方法 就是以Class对象作为锁。另外,它可以用来控制对 static 数据成员 (static 数据成员不专属于任何一个对象,是类成员) 的并发访问。并且,如果一个线程执行一个对象的非static synchronized 方法,另外一个线程需要执行这个对象所属类的 static synchronized 方法,也不会发生互斥现象。因为访问 static synchronized 方法占用的是class的锁,而访问非 static synchronized 方法占用的是对象锁,所以不存在互斥现象
注意事项
异常会自动释放内置锁
对于 synchronized方法 或者 synchronized代码块,当出现异常时,JVM会自动释放当前线程占用的锁,因此不会由于异常导致出现死锁现象
内置锁与字符串常量
由于字符串常量池的原因,在大多数情况下,同步synchronized代码块 都不使用 String 作为锁对象,而改用其它的对象,例如new Object()创建一个对象,因为它不会被放入缓存池中。
class Service {
public void print(String stringParam) {
try {
synchronized (stringParam) {
while (true) {
System.out.println(Thread.currentThread().getName());
Thread.sleep(1000);
}
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
//线程A
class ThreadA extends Thread {
private Service service;
public ThreadA(Service service) {
super();
this.service = service;
}
@Override
public void run() {
service.print("AA");
}
}
//线程B
class ThreadB extends Thread {
private Service service;
public ThreadB(Service service) {
super();
this.service = service;
}
@Override
public void run() {
service.print("AA");
}
}
public class MainTest {
//测试
public static void main(String[] args) {
//临界资源
Service service = new Service();
//创建并启动线程A
ThreadA a = new ThreadA(service);
a.setName("A");
a.start();
//创建并启动线程B
ThreadB b = new ThreadB(service);
b.setName("B");
b.start();
}
线程B会永远不会执行,线程A持有了“AA"对象的锁,并且永不释放,线程B也是需要获取”AA“的锁,因为常量池的存在,他们俩获取的锁是同一个锁,导致线程B无限等待。
锁定的是对象,而不是引用
在将任何数据类型作为同步锁时,需要注意的是,是否有多个线程将同时去竞争该锁对象:
1).若它们将同时竞争同一把锁,则这些线程之间就是同步的;
2).否则,这些线程之间就是异步的
例如:
//资源类
class Service {
private Object lockObj;
public Service(Object lockObj) {
this.lockObj = lockObj;
}
public void print() {
try {
synchronized (lockObj) {
int x = 0;
while (true) {
if (x == 0) {
lockObj = new Object();
x++;
}
System.out.println(Thread.currentThread().getName());
Thread.sleep(2000);
}
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
//线程A
class ThreadA extends Thread {
private Service service;
public ThreadA(Service service) {
super();
this.service = service;
}
@Override
public void run() {
service.print();
}
}
//线程B
class ThreadB extends Thread {
private Service service;
public ThreadB(Service service) {
super();
this.service = service;
}
@Override
public void run() {
service.print();
}
}
public class MainTest {
//测试
public static void main(String[] args) {
//临界资源
Object o = new Object();
Service service = new Service(o);
//创建并启动线程A
ThreadA a = new ThreadA(service);
a.setName("A");
a.start();
ThreadUtil.sleep(100);
//创建并启动线程B
ThreadB b = new ThreadB(service);
b.setName("B");
b.start();
}
}
A,B两个线程没有进行同步,因为B执行的时候,虽然都是lockObj这一个引用,但已经是另一个对象了。
本文详细介绍了Java的内置锁synchronized,包括同步代码块的组成、重入特性、类对象锁以及注意事项。通过示例解释了锁的对象是对象本身,而非引用,强调了锁的互斥性和可重入性,并提醒开发者避免使用字符串常量作为锁对象。

4万+

被折叠的 条评论
为什么被折叠?



