synchronized是Jdk的内置同步锁,用于实现多线程对共享资源的同步访问。
使用方式:
1.synchronized普通方法
// 同步方法
public synchronized void test() {
// 同步代码块
// 锁对象为当前对象
}
2.synchronized静态方法
// 同步静态方法
public synchronized static void test() {
// 同步代码块
// 锁对象为当前类对象
}
3.synchronized普通代码块
public void test() {
synchronized (this){
// 同步代码块
// 锁对象为当前对象
}
}
4.synchronized静态代码块
public static void test() {
synchronized (Demo.class){
// 同步代码块
// 锁对象为当前类对象
}
}
同步锁
在Java中每一个对象有且仅有一个同步锁,同步锁是依赖于对象而存在的。
同步锁使用场景:多个线程对同一个对象中的实例变量进行并发访问。
对某个方法或某个代码块使用synchronized时,那么当某个线程访问时,就会尝试获取相应对象的同步锁,实现多个线程对加锁部分代码的互斥访问,也就是在某个时间点,对象的同步锁只能被一个线程持有。
例:现有A、B两个线程都需要访问加锁资源,如果A线程先抢到了同步锁,那么B就会获取失败,处于阻塞状态,只能等到A线程释放了该锁资源之后,B线程才能获取到该同步锁,进而访问共享资源。
锁对象
synchronized是对存在同步问题的对象进行加锁。从上面4种不同synchronized方式可以看出,实现对共享资源同步操作的加锁机制:
非静态方法的锁对象是当前类实例;
静态方法的锁对象是当前类;
非静态代码块和静态代码块的锁对象则基于括号里配置的实例;
可重入锁
可重入就是某个线程已经获取某个锁,可以再次获取相同的锁而不会发生死锁。如下,一个线程进入同步方法test1则已经持有了锁,再调用同步方法test2,两个方法的同步锁都是demo对象,然而并不会发生死锁。test3中同步代码块同是如此(经典的案例就是双重检查锁的单例模式)。
public class Demo {
public static void main(String[] args) {
Demo demo = new Demo();
demo.test1();
}
public synchronized void test1() {
// 同步代码块
test2();
}
public synchronized void test2() {
// 同步代码块
}
public synchronized void test3(){
synchronized (this){
synchronized (this){
// 同步代码块
}
}
}
}
Java的可重入锁还有ReentrantLock。
原子性和可见性
综上,synchronized能够实现多线程对共享资源操作的原子性,使多个线程互斥地访问共享资源。此外,synchronized还具有内存可见性。
public class Demo {
public static void main(String[] args) throws InterruptedException {
MyThread myThread = new MyThread();
myThread.start();
while (true) {
if (myThread.flag) {
System.out.println("主线程读到flag=" + myThread.flag);
break;
}
}
}
}
class MyThread extends Thread {
public boolean flag = false;
@Override
public void run() {
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
flag = true;
System.out.println("MyThread线程修改后flag=" + flag);
}
}
以上程序,启动子线程MyThread,并修改子线程中的变量。主线程在启动子线程后监听读取子线程对象中的变量flag,如果flag==true,则结束监听。
但是运行程序后会发现,在子线程中正常修改了flag后,主线程依旧处于while死循环状态。以上存在问题就是:
1.子线程从主存读取flag到自己的工作空间,然后修改flag值,子线程还未来得及将修改后的flag值更新到主存,主线程就已经从主存把flag读到自己的工作内存,工作内存中的flag是false。
2.即使子线程修改flag后并同步到主存,主线程依然使用的的自己工作内存中的flag。(这里可以先去了解下JMM)
以上问题产生的原因就是:多线程操作共享变量时,彼此不可见。
所以如果能让flag被修改后及时更新到主存,并且主线程每次都是从主存访问flag,那么主线程就能正常读取到更新后的值,及时结束while循环。
内存可见性
某个线程更改了某个对象的状态,也需要访问该对象的其他线程能够及时地看到该对象修改后的状态。
JMM关于synchronized的规定:
线程加锁时,将清空工作内存中共享变量的值,从而重新读取最新的值;
线程解锁前必须把共享变量的最新值刷新到主内存中;
线程执行同步代码块的过程:
1.获得互斥锁
2.清空工作内存
3 从主内存中拷贝最新的变量副本到工作内存中
4 执行代码块
5.将更改后的共享变量的值刷新到主存中
6.释放互斥锁
要解决上述while死循环问题,就是对flag的访问使用synchronized加锁实现可见性,如下:
synchronized (myThread){
while (true) {
if (myThread.flag) {
System.out.println("主线程读到flag=" + myThread.flag);
break;
}
}
}
但是,有的小伙伴可能会这么想,使用如下的方式在子线程中对flag的修改进行加锁是否可行呢?
synchronized (this){
flag = true;
}
根据以上的可见性描述,执行完上述同步代码后,子线程会及时将flag的值同步到主存。但是主线程依旧使用的自己工作空间中的副本变量,并没有再次从主线程读取flag。所以在子线程中对flag的修改加锁也不会解决上述的主线程的可见性问题。
在之后锁机制章节中还会对synchronized的实现原理和JDK1.6的优化进行进一步的分析。